From 0a072dd07b29b1bf4a894deec68633f6c429d049 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 1 Feb 2016 00:19:13 -0500 Subject: [PATCH 0001/1087] Advance version to 1.6.0-SNAPSHOT. --- pljava-ant/pom.xml | 2 +- pljava-api/pom.xml | 2 +- pljava-deploy/pom.xml | 2 +- pljava-examples/pom.xml | 2 +- pljava-packaging/pom.xml | 2 +- pljava-so/pom.xml | 2 +- pljava/pom.xml | 2 +- .../java/org/postgresql/pljava/internal/InstallHelper.java | 6 ++++-- pom.xml | 2 +- 9 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index 8875ec85..01ddaecb 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pljava-ant PL/Java Ant tasks diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 05898acf..352cd414 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pljava-api PL/Java API diff --git a/pljava-deploy/pom.xml b/pljava-deploy/pom.xml index a87705b6..2202e8e7 100644 --- a/pljava-deploy/pom.xml +++ b/pljava-deploy/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pljava-deploy PL/Java Deploy diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index e8267cfa..7fbde236 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pljava-examples PL/Java examples diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 9e2d7f99..30d8ada1 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pljava-packaging PL/Java packaging diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index ec780227..7e59f6f7 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pljava-so PL/Java backend native code diff --git a/pljava/pom.xml b/pljava/pom.xml index 8e778d74..cb54e7ab 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pljava PL/Java backend Java code diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 8f968fdb..aceaa075 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -366,11 +366,11 @@ private static SchemaVariant recognizeSchema( Connection c, Statement s) * up to date. */ private static final SchemaVariant currentSchema = - SchemaVariant.UNREL20130301b; + SchemaVariant.REL_1_5_0_BETA1; private enum SchemaVariant { - UNREL20130301b ("c51cffa34acd5a228325143ec29563174891a873") + REL_1_5_0_BETA1 ("c51cffa34acd5a228325143ec29563174891a873") { @Override void migrateFrom( SchemaVariant sv, Connection c, Statement s) @@ -419,6 +419,8 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant UNREL20130301b = REL_1_5_0_BETA1; + String sha; SchemaVariant( String sha) { diff --git a/pom.xml b/pom.xml index 4ed0347d..100d00d5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.6.0-SNAPSHOT pom PostgreSQL PL/Java https://tada.github.io/pljava/ From 8bd216e4dbf4192265983981a27afb46d7ea4edb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 29 Mar 2016 22:45:19 -0400 Subject: [PATCH 0002/1087] Bit of vacuuming after release 1.5.0. Old Deployer is obsolete, as are install/uninstall.sql and old fixes for GCJ. In 1.5.0 they've seen their last release. It's all in the history if ever needed. --- fixes/gcj/java_sql_Types.h | 73 --- pljava-deploy/.classpath | 26 - pljava-deploy/.gitignore | 1 - pljava-deploy/.project | 23 - .../org.eclipse.core.resources.prefs | 3 - .../.settings/org.eclipse.jdt.core.prefs | 5 - .../.settings/org.eclipse.m2e.core.prefs | 4 - pljava-deploy/pom.xml | 34 -- .../postgresql/pljava/deploy/Deployer.java | 507 ------------------ .../pljava/deploy/package-info.java | 13 - pom.xml | 1 - src/sql/install.sql | 100 ---- src/sql/uninstall.sql | 3 - 13 files changed, 793 deletions(-) delete mode 100644 fixes/gcj/java_sql_Types.h delete mode 100644 pljava-deploy/.classpath delete mode 100644 pljava-deploy/.gitignore delete mode 100644 pljava-deploy/.project delete mode 100644 pljava-deploy/.settings/org.eclipse.core.resources.prefs delete mode 100644 pljava-deploy/.settings/org.eclipse.jdt.core.prefs delete mode 100644 pljava-deploy/.settings/org.eclipse.m2e.core.prefs delete mode 100644 pljava-deploy/pom.xml delete mode 100644 pljava-deploy/src/main/java/org/postgresql/pljava/deploy/Deployer.java delete mode 100644 pljava-deploy/src/main/java/org/postgresql/pljava/deploy/package-info.java delete mode 100644 src/sql/install.sql delete mode 100644 src/sql/uninstall.sql diff --git a/fixes/gcj/java_sql_Types.h b/fixes/gcj/java_sql_Types.h deleted file mode 100644 index addae231..00000000 --- a/fixes/gcj/java_sql_Types.h +++ /dev/null @@ -1,73 +0,0 @@ -/* DO NOT EDIT THIS FILE - it is machine generated */ -#include -/* Header for class java_sql_Types */ - -#ifndef _Included_java_sql_Types -#define _Included_java_sql_Types -#ifdef __cplusplus -extern "C" { -#endif -#undef java_sql_Types_BIT -#define java_sql_Types_BIT -7L -#undef java_sql_Types_TINYINT -#define java_sql_Types_TINYINT -6L -#undef java_sql_Types_SMALLINT -#define java_sql_Types_SMALLINT 5L -#undef java_sql_Types_INTEGER -#define java_sql_Types_INTEGER 4L -#undef java_sql_Types_BIGINT -#define java_sql_Types_BIGINT -5L -#undef java_sql_Types_FLOAT -#define java_sql_Types_FLOAT 6L -#undef java_sql_Types_REAL -#define java_sql_Types_REAL 7L -#undef java_sql_Types_DOUBLE -#define java_sql_Types_DOUBLE 8L -#undef java_sql_Types_NUMERIC -#define java_sql_Types_NUMERIC 2L -#undef java_sql_Types_DECIMAL -#define java_sql_Types_DECIMAL 3L -#undef java_sql_Types_CHAR -#define java_sql_Types_CHAR 1L -#undef java_sql_Types_VARCHAR -#define java_sql_Types_VARCHAR 12L -#undef java_sql_Types_LONGVARCHAR -#define java_sql_Types_LONGVARCHAR -1L -#undef java_sql_Types_DATE -#define java_sql_Types_DATE 91L -#undef java_sql_Types_TIME -#define java_sql_Types_TIME 92L -#undef java_sql_Types_TIMESTAMP -#define java_sql_Types_TIMESTAMP 93L -#undef java_sql_Types_BINARY -#define java_sql_Types_BINARY -2L -#undef java_sql_Types_VARBINARY -#define java_sql_Types_VARBINARY -3L -#undef java_sql_Types_LONGVARBINARY -#define java_sql_Types_LONGVARBINARY -4L -#undef java_sql_Types_NULL -#define java_sql_Types_NULL 0L -#undef java_sql_Types_OTHER -#define java_sql_Types_OTHER 1111L -#undef java_sql_Types_JAVA_OBJECT -#define java_sql_Types_JAVA_OBJECT 2000L -#undef java_sql_Types_DISTINCT -#define java_sql_Types_DISTINCT 2001L -#undef java_sql_Types_STRUCT -#define java_sql_Types_STRUCT 2002L -#undef java_sql_Types_ARRAY -#define java_sql_Types_ARRAY 2003L -#undef java_sql_Types_BLOB -#define java_sql_Types_BLOB 2004L -#undef java_sql_Types_CLOB -#define java_sql_Types_CLOB 2005L -#undef java_sql_Types_REF -#define java_sql_Types_REF 2006L -#undef java_sql_Types_DATALINK -#define java_sql_Types_DATALINK 70L -#undef java_sql_Types_BOOLEAN -#define java_sql_Types_BOOLEAN 16L -#ifdef __cplusplus -} -#endif -#endif diff --git a/pljava-deploy/.classpath b/pljava-deploy/.classpath deleted file mode 100644 index fd7ad7fb..00000000 --- a/pljava-deploy/.classpath +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pljava-deploy/.gitignore b/pljava-deploy/.gitignore deleted file mode 100644 index ea8c4bf7..00000000 --- a/pljava-deploy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/pljava-deploy/.project b/pljava-deploy/.project deleted file mode 100644 index d88c05f0..00000000 --- a/pljava-deploy/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - pljava-deploy - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - diff --git a/pljava-deploy/.settings/org.eclipse.core.resources.prefs b/pljava-deploy/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index e9441bb1..00000000 --- a/pljava-deploy/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,3 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding/=UTF-8 diff --git a/pljava-deploy/.settings/org.eclipse.jdt.core.prefs b/pljava-deploy/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 60105c1b..00000000 --- a/pljava-deploy/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,5 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=1.6 diff --git a/pljava-deploy/.settings/org.eclipse.m2e.core.prefs b/pljava-deploy/.settings/org.eclipse.m2e.core.prefs deleted file mode 100644 index f897a7f1..00000000 --- a/pljava-deploy/.settings/org.eclipse.m2e.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -activeProfiles= -eclipse.preferences.version=1 -resolveWorkspaceProjects=true -version=1 diff --git a/pljava-deploy/pom.xml b/pljava-deploy/pom.xml deleted file mode 100644 index 2202e8e7..00000000 --- a/pljava-deploy/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - 4.0.0 - - org.postgresql - pljava.app - 1.6.0-SNAPSHOT - - pljava-deploy - PL/Java Deploy - - A Java standalone utility to complete the SQL steps of deploying - PL/Java into a PostgreSQL database, over a JDBC connection. - As of PL/Java 1.5.0, this utility is unnecessary and obsolescent. - For current installation instructions that do not involve the Deployer, - see "Installing into PostgreSQL" at the project web site. - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.2 - - - - org.postgresql.pljava.deploy.Deployer - - - - - - - diff --git a/pljava-deploy/src/main/java/org/postgresql/pljava/deploy/Deployer.java b/pljava-deploy/src/main/java/org/postgresql/pljava/deploy/Deployer.java deleted file mode 100644 index 356b5f36..00000000 --- a/pljava-deploy/src/main/java/org/postgresql/pljava/deploy/Deployer.java +++ /dev/null @@ -1,507 +0,0 @@ -/* - * Copyright (c) 2004-2013 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: - * Tada AB - * Purdue University - */ -package org.postgresql.pljava.deploy; - -import java.io.PrintStream; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.sql.Savepoint; -import java.sql.Statement; -import java.sql.ResultSet; -import java.util.ArrayList; - -/** - * When running the deployer, you must use a classpath that can see the - * deploy.jar found in the Pl/Java distribution and the postgresql.jar from the - * PostgreSQL distribution. The former contains the code for the deployer - * command and the second includes the PostgreSQL JDBC driver. You then run the - * deployer with the command: - *

- *

- * java -cp <your classpath> org.postgresql.pljava.deploy.Deployer [ options ] - *
- *

- * It's recommended that create a shell script or a .bat script that does this - * for you so that you don't have to do this over and over again. - *

- *

Deployer options

- *
- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Options for Deployer
OptionDescription
-installInstalls the Java language along with the sqlj procedures. The deployer - * will fail if the language is installed already.
-reinstallReinstalls the Java language and the sqlj procedures. This will - * effectively drop all jar files that have been loaded.
-removeDrops the Java language and the sqjl procedures and loaded jars
-user <user name>Name of user that connects to the database. Default is current user
-password <password>Password of user that connects to the database. Default is no password - *
-database <database>The name of the database to connect to. Default is current user
-host <hostname>Name of the host. Default is "localhost"
-windowsUse this option if the host runs on a windows platform. Affects the - * name used for the Pl/Java dynamic library
- * @deprecated See - * Installing PL/Java - * for the current installation instructions that do not require this code. - * This subproject will eventually be outdated and removed. - * @author Thomas Hallgren - */ -public class Deployer -{ - private static final int CMD_AMBIGUOUS = -2; - private static final int CMD_UNKNOWN = -1; - private static final int CMD_UNINSTALL = 0; - private static final int CMD_INSTALL = 1; - private static final int CMD_REINSTALL = 2; - private static final int CMD_USER = 3; - private static final int CMD_PASSWORD = 4; - private static final int CMD_DATABASE = 5; - private static final int CMD_HOSTNAME = 6; - private static final int CMD_PORT = 7; - - private final Connection m_connection; - - private static final ArrayList s_commands = new ArrayList(); - - static - { - s_commands.add(CMD_UNINSTALL, "uninstall"); - s_commands.add(CMD_INSTALL, "install"); - s_commands.add(CMD_REINSTALL, "reinstall"); - s_commands.add(CMD_USER, "user"); - s_commands.add(CMD_PASSWORD, "password"); - s_commands.add(CMD_DATABASE, "database"); - s_commands.add(CMD_HOSTNAME, "host"); - s_commands.add(CMD_PORT, "port"); - } - - private static final int getCommand(String arg) - { - int top = s_commands.size(); - int candidateCmd = CMD_UNKNOWN; - for(int idx = 0; idx < top; ++idx) - { - if(((String)s_commands.get(idx)).startsWith(arg)) - { - if(candidateCmd != CMD_UNKNOWN) - return CMD_AMBIGUOUS; - candidateCmd = idx; - } - } - return candidateCmd; - } - public static void printUsage() - { - PrintStream out = System.err; - out.println("usage: java org.postgresql.pljava.deploy.Deployer"); - out.println(" {-install | -uninstall | -reinstall}"); - out.println(" [ -host ] # default is localhost"); - out.println(" [ -port ] # default is blank"); - out.println(" [ -database ] # default is name of current user"); - out.println(" [ -user ] # default is name of current user"); - out.println(" [ -password ] # default is no password"); - } - - public static void main(String[] argv) - { - String driverClass = "org.postgresql.Driver"; - String hostName = "localhost"; - String userName = System.getProperty("user.name", "postgres"); - String database = userName; - String subsystem = "postgresql"; - String password = null; - String portNumber = null; - int cmd = CMD_UNKNOWN; - - int top = argv.length; - for(int idx = 0; idx < top; ++idx) - { - String arg = argv[idx]; - if(arg.length() < 2) - { - printUsage(); - return; - } - - if(arg.charAt(0) == '-') - { - int optCmd = getCommand(arg.substring(1)); - switch(optCmd) - { - case CMD_INSTALL: - case CMD_UNINSTALL: - case CMD_REINSTALL: - if(cmd != CMD_UNKNOWN) - { - printUsage(); - return; - } - cmd = optCmd; - break; - - case CMD_USER: - if(++idx < top) - { - userName = argv[idx]; - if(userName.length() > 0 - && userName.charAt(0) != '-') - break; - } - printUsage(); - return; - - case CMD_PASSWORD: - if(++idx < top) - { - password = argv[idx]; - if(password.length() > 0 - && password.charAt(0) != '-') - break; - } - printUsage(); - return; - - case CMD_DATABASE: - if(++idx < top) - { - database = argv[idx]; - if(database.length() > 0 - && database.charAt(0) != '-') - break; - } - printUsage(); - return; - - case CMD_HOSTNAME: - if(++idx < top) - { - hostName = argv[idx]; - if(hostName.length() > 0 - && hostName.charAt(0) != '-') - break; - } - printUsage(); - return; - - case CMD_PORT: - if(++idx < top) - { - portNumber = argv[idx]; - if(portNumber.length() > 0 - && portNumber.charAt(0) != '-') - break; - } - printUsage(); - return; - - default: - printUsage(); - return; - } - } - } - if(cmd == CMD_UNKNOWN) - { - printUsage(); - return; - } - - try - { - Class.forName(driverClass); - - StringBuffer cc = new StringBuffer(); - cc.append("jdbc:"); - cc.append(subsystem); - cc.append("://"); - cc.append(hostName); - if(portNumber != null) - { - cc.append(':'); - cc.append(portNumber); - } - cc.append('/'); - cc.append(database); - Connection c = DriverManager.getConnection( - cc.toString(), - userName, - password); - - checkIfConnectedAsSuperuser(c); - c.setAutoCommit(false); - Deployer deployer = new Deployer(c); - - if(cmd == CMD_UNINSTALL || cmd == CMD_REINSTALL) - { - deployer.dropSQLJSchema(); - } - - if(cmd == CMD_INSTALL || cmd == CMD_REINSTALL) - { - deployer.createSQLJSchema(); - deployer.initJavaHandlers(); - deployer.initializeSQLJSchema(); - } - c.commit(); - c.close(); - } - catch(Exception e) - { - e.printStackTrace(); - } - } - - public Deployer(Connection c) - { - m_connection = c; - } - - public static void checkIfConnectedAsSuperuser(Connection conn) - throws SQLException - { - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SHOW IS_SUPERUSER"); - - try - { - if(rs.next() && rs.getString(1).equals("on")) - return; - } - finally - { - rs.close(); - stmt.close(); - } - - throw new SQLException( - "You must be a superuser to deploy/undeploy pl/Java."); - } - - public void dropSQLJSchema() - throws SQLException - { - Statement stmt = m_connection.createStatement(); - Savepoint p = null; - - try - { - if (m_connection.getMetaData().supportsSavepoints()) - p = m_connection.setSavepoint(); - stmt.execute("DROP LANGUAGE java CASCADE"); - stmt.execute("DROP LANGUAGE javaU CASCADE"); - } - catch(SQLException e) - { - /* roll back to savepoint (if available) - * or restart the transaction (if no savepoint is available) - * & ignore the exception */ - - if (p != null) - m_connection.rollback(p); - else - /* Assuming that the dropSQLJSchema is the - * first method called in a transaction, - * we can afford to restart the transaction. - * - * This solution is designed for PostgreSQL < 8 (no savepoints available) - */ - m_connection.rollback(); - } - finally - { - if (p != null) - m_connection.releaseSavepoint(p); - } - - stmt.execute("DROP SCHEMA sqlj CASCADE"); - stmt.close(); - } - - public void createSQLJSchema() - throws SQLException - { - Statement stmt = m_connection.createStatement(); - stmt.execute("CREATE SCHEMA sqlj"); - stmt.execute("GRANT USAGE ON SCHEMA sqlj TO public"); - stmt.close(); - } - - public void initializeSQLJSchema() - throws SQLException - { - Statement stmt = m_connection.createStatement(); - - stmt.execute( - "CREATE TABLE sqlj.jar_repository(" + - " jarId SERIAL PRIMARY KEY," + - " jarName VARCHAR(100) UNIQUE NOT NULL," + - " jarOrigin VARCHAR(500) NOT NULL," + - " jarOwner NAME NOT NULL," + - " jarManifest TEXT" + - ")"); - stmt.execute("GRANT SELECT ON sqlj.jar_repository TO public"); - - stmt.execute( - "CREATE TABLE sqlj.jar_entry(" + - " entryId SERIAL PRIMARY KEY," + - " entryName VARCHAR(200) NOT NULL," + - " jarId INT NOT NULL REFERENCES sqlj.jar_repository ON DELETE CASCADE," + - " entryImage BYTEA NOT NULL," + - " UNIQUE(jarId, entryName)" + - ")"); - - stmt.execute("GRANT SELECT ON sqlj.jar_entry TO public"); - - stmt.execute( - "CREATE TABLE sqlj.jar_descriptor(" + - " jarId INT REFERENCES sqlj.jar_repository ON DELETE CASCADE," + - " ordinal INT2," + - " PRIMARY KEY (jarId, ordinal)," + - " entryId INT NOT NULL REFERENCES sqlj.jar_entry ON DELETE CASCADE" + - ")"); - - stmt.execute("GRANT SELECT ON sqlj.jar_descriptor TO public"); - - // Create the table maintaining the class path. - // - stmt.execute( - "CREATE TABLE sqlj.classpath_entry(" + - " schemaName VARCHAR(30) NOT NULL," + - " ordinal INT2 NOT NULL," + // Ordinal in class path - " jarId INT NOT NULL REFERENCES sqlj.jar_repository ON DELETE CASCADE," + - " PRIMARY KEY(schemaName, ordinal)" + - ")"); - stmt.execute("GRANT SELECT ON sqlj.classpath_entry TO public"); - - // Create the table maintaining the SQL to Java type mappings - // - stmt.execute( - "CREATE TABLE sqlj.typemap_entry(" + - " mapId SERIAL PRIMARY KEY," + - " javaName VARCHAR(200) NOT NULL," + - " sqlName NAME NOT NULL" + - ")"); - stmt.execute("GRANT SELECT ON sqlj.typemap_entry TO public"); - - // These are the proposed SQL standard methods. - // - stmt.execute( - "CREATE FUNCTION sqlj.install_jar(VARCHAR, VARCHAR, BOOLEAN) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.installJar'" + - " LANGUAGE java SECURITY DEFINER"); - - stmt.execute( - "CREATE FUNCTION sqlj.replace_jar(VARCHAR, VARCHAR, BOOLEAN) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.replaceJar'" + - " LANGUAGE java SECURITY DEFINER"); - - stmt.execute( - "CREATE FUNCTION sqlj.remove_jar(VARCHAR, BOOLEAN) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.removeJar'" + - " LANGUAGE java SECURITY DEFINER"); - - // Not proposed, but very useful if you want to send the image over - // your JDBC connection. - // - stmt.execute( - "CREATE FUNCTION sqlj.install_jar(BYTEA, VARCHAR, BOOLEAN) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.installJar'" + - " LANGUAGE java SECURITY DEFINER"); - - stmt.execute( - "CREATE FUNCTION sqlj.replace_jar(BYTEA, VARCHAR, BOOLEAN) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.replaceJar'" + - " LANGUAGE java SECURITY DEFINER"); - - // This function is not as proposed. It's more Java'ish. The proposal - // using sqlj.alter_jar_path is in my opinion bloated and will not be - // well received in the Java community. Luckily, the support is suggested - // to be optional. - // - stmt.execute( - "CREATE FUNCTION sqlj.set_classpath(VARCHAR, VARCHAR) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.setClassPath'" + - " LANGUAGE java SECURITY DEFINER"); - - stmt.execute( - "CREATE FUNCTION sqlj.get_classpath(VARCHAR) RETURNS VARCHAR" + - " AS 'org.postgresql.pljava.management.Commands.getClassPath'" + - " LANGUAGE java STABLE SECURITY DEFINER"); - - // The following functions are not included in the standard. Type mapping - // is radically different in SQL 2003 and requires a lot of additions to - // the PostgreSQL dialect. - // - stmt.execute( - "CREATE FUNCTION sqlj.add_type_mapping(VARCHAR, VARCHAR) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.addTypeMapping'" + - " LANGUAGE java SECURITY DEFINER"); - - stmt.execute( - "CREATE FUNCTION sqlj.drop_type_mapping(VARCHAR) RETURNS void" + - " AS 'org.postgresql.pljava.management.Commands.dropTypeMapping'" + - " LANGUAGE java SECURITY DEFINER"); - - stmt.close(); - } - - public void initJavaHandlers() - throws SQLException - { - Statement stmt = m_connection.createStatement(); - stmt.execute( - "CREATE FUNCTION sqlj.java_call_handler()" + - " RETURNS language_handler" + - " AS 'pljava'" + - " LANGUAGE C"); - - stmt.execute("CREATE TRUSTED LANGUAGE java HANDLER sqlj.java_call_handler"); - - stmt.execute( - "CREATE FUNCTION sqlj.javau_call_handler()" + - " RETURNS language_handler" + - " AS 'pljava'" + - " LANGUAGE C"); - - stmt.execute("CREATE LANGUAGE javaU HANDLER sqlj.javau_call_handler"); - stmt.close(); - } -} diff --git a/pljava-deploy/src/main/java/org/postgresql/pljava/deploy/package-info.java b/pljava-deploy/src/main/java/org/postgresql/pljava/deploy/package-info.java deleted file mode 100644 index a9a390e0..00000000 --- a/pljava-deploy/src/main/java/org/postgresql/pljava/deploy/package-info.java +++ /dev/null @@ -1,13 +0,0 @@ -/** - *

Note: this package is obsolescent as of PL/Java 1.5.0. - * See Installing PL/Java - * for the current installation instructions that do not require this code. - * This subproject will eventually be outdated and removed. - *

- * One approach to completing the SQL steps of PL/Java installation; will build - * a normal, standalone Java app to connect to the database over a vanilla - * JDBC connection and install, remove, or reinstall the necessary objects. - * See {@link org.postgresql.pljava.deploy.Deployer Deployer} for usage. - * @author Thomas Hallgren - */ -package org.postgresql.pljava.deploy; diff --git a/pom.xml b/pom.xml index 100d00d5..0693e125 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,6 @@ pljava-api pljava pljava-so - pljava-deploy pljava-ant pljava-examples pljava-packaging diff --git a/src/sql/install.sql b/src/sql/install.sql deleted file mode 100644 index 75dd732f..00000000 --- a/src/sql/install.sql +++ /dev/null @@ -1,100 +0,0 @@ --- Note: as of PL/Java 1.5.0, this file is obsolescent and not needed to install --- PL/Java. See "Installing into PostgreSQL" at the project web site for current --- installation instructions that do not involve this file. This will eventually --- grow out of date and be removed. --- --- You can generate a local copy of the web site with 'mvn site site:stage' and --- point a web browser at target/site/staging/install/install.html for the --- installation instructions offline. - -CREATE SCHEMA sqlj; -GRANT USAGE ON SCHEMA sqlj TO public; - -CREATE FUNCTION sqlj.java_call_handler() - RETURNS language_handler AS 'pljava' - LANGUAGE C; - -CREATE TRUSTED LANGUAGE java HANDLER sqlj.java_call_handler; - -CREATE FUNCTION sqlj.javau_call_handler() - RETURNS language_handler AS 'pljava' - LANGUAGE C; - -CREATE LANGUAGE javaU HANDLER sqlj.javau_call_handler; - -CREATE TABLE sqlj.jar_repository( - jarId SERIAL PRIMARY KEY, - jarName VARCHAR(100) UNIQUE NOT NULL, - jarOrigin VARCHAR(500) NOT NULL, - jarOwner NAME NOT NULL, - jarManifest TEXT -); -GRANT SELECT ON sqlj.jar_repository TO public; - -CREATE TABLE sqlj.jar_entry( - entryId SERIAL PRIMARY KEY, - entryName VARCHAR(200) NOT NULL, - jarId INT NOT NULL REFERENCES sqlj.jar_repository ON DELETE CASCADE, - entryImage BYTEA NOT NULL, - UNIQUE(jarId, entryName) -); -GRANT SELECT ON sqlj.jar_entry TO public; - -CREATE TABLE sqlj.jar_descriptor( - jarId INT REFERENCES sqlj.jar_repository ON DELETE CASCADE, - ordinal INT2, - PRIMARY KEY (jarId, ordinal), - entryId INT NOT NULL REFERENCES sqlj.jar_entry ON DELETE CASCADE -); -GRANT SELECT ON sqlj.jar_descriptor TO public; - -CREATE TABLE sqlj.classpath_entry( - schemaName VARCHAR(30) NOT NULL, - ordinal INT2 NOT NULL, - jarId INT NOT NULL REFERENCES sqlj.jar_repository ON DELETE CASCADE, - PRIMARY KEY(schemaName, ordinal) -); -GRANT SELECT ON sqlj.classpath_entry TO public; - -CREATE TABLE sqlj.typemap_entry( - mapId SERIAL PRIMARY KEY, - javaName VARCHAR(200) NOT NULL, - sqlName NAME NOT NULL -); -GRANT SELECT ON sqlj.typemap_entry TO public; - -CREATE FUNCTION sqlj.install_jar(VARCHAR, VARCHAR, BOOLEAN) RETURNS void - AS 'org.postgresql.pljava.management.Commands.installJar' - LANGUAGE java SECURITY DEFINER; - -CREATE FUNCTION sqlj.install_jar(BYTEA, VARCHAR, BOOLEAN) RETURNS void - AS 'org.postgresql.pljava.management.Commands.installJar' - LANGUAGE java SECURITY DEFINER; - -CREATE FUNCTION sqlj.replace_jar(VARCHAR, VARCHAR, BOOLEAN) RETURNS void - AS 'org.postgresql.pljava.management.Commands.replaceJar' - LANGUAGE java SECURITY DEFINER; - -CREATE FUNCTION sqlj.replace_jar(BYTEA, VARCHAR, BOOLEAN) RETURNS void - AS 'org.postgresql.pljava.management.Commands.replaceJar' - LANGUAGE java SECURITY DEFINER; - -CREATE FUNCTION sqlj.remove_jar(VARCHAR, BOOLEAN) RETURNS void - AS 'org.postgresql.pljava.management.Commands.removeJar' - LANGUAGE java SECURITY DEFINER; - -CREATE FUNCTION sqlj.set_classpath(VARCHAR, VARCHAR) RETURNS void - AS 'org.postgresql.pljava.management.Commands.setClassPath' - LANGUAGE java SECURITY DEFINER; - -CREATE FUNCTION sqlj.get_classpath(VARCHAR) RETURNS VARCHAR - AS 'org.postgresql.pljava.management.Commands.getClassPath' - LANGUAGE java STABLE SECURITY DEFINER; - -CREATE FUNCTION sqlj.add_type_mapping(VARCHAR, VARCHAR) RETURNS void - AS 'org.postgresql.pljava.management.Commands.addTypeMapping' - LANGUAGE java SECURITY DEFINER; - -CREATE FUNCTION sqlj.drop_type_mapping(VARCHAR) RETURNS void - AS 'org.postgresql.pljava.management.Commands.dropTypeMapping' - LANGUAGE java SECURITY DEFINER; diff --git a/src/sql/uninstall.sql b/src/sql/uninstall.sql deleted file mode 100644 index 8e9d25f1..00000000 --- a/src/sql/uninstall.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP LANGUAGE java CASCADE; -DROP LANGUAGE javaU CASCADE; -DROP SCHEMA sqlj CASCADE; From b62fb57f592993b49c8232861623e124d587937c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 31 Mar 2016 01:29:34 -0400 Subject: [PATCH 0003/1087] Post-1.5.0-vacuuming oversight. --- pljava-packaging/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index d52e45d2..4e49faf1 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -22,11 +22,6 @@ pljava ${project.version} - - org.postgresql - pljava-deploy - ${project.version} - org.postgresql pljava-examples From b30d21802f8912378a90f5faa4c1da613e6bda7e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 30 Mar 2016 22:31:47 -0400 Subject: [PATCH 0004/1087] Move Java minimum version to 7. --- pom.xml | 4 ++-- src/site/markdown/build/versions.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 0693e125..35e68912 100644 --- a/pom.xml +++ b/pom.xml @@ -72,8 +72,8 @@ maven-compiler-plugin 2.5.1 - 1.6 - 1.6 + 1.7 + 1.7 ${project.build.sourceEncoding} ${env.JAVA_HOME}/jre/lib/rt.jar diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index 0bf0b591..1e5943b4 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -1,11 +1,11 @@ # Versions of external packages needed to build and use PL/Java -As of November 2015, the following version constraints are known. +As of March 2016, the following version constraints are known. ## Java -No version of Java before 1.6 ("Java 6") is supported. The PL/Java code -makes use of Java features first appearing in Java 6. +No version of Java before 1.7 ("Java 7") is supported. The PL/Java code +makes use of Java features first appearing in Java 7. As for later versions of Java, backward compatibility in the language is generally good. The most likely problem areas with a new Java version will From ebd314bb244ceba042859a4575536603b593f743 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 31 Mar 2016 00:50:55 -0400 Subject: [PATCH 0005/1087] More regexps for common lexicals, a la jdk7. --- .../postgresql/pljava/sqlgen/Lexicals.java | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 10b36ae9..58ae87d3 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2016 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 @@ -45,7 +45,7 @@ public interface Lexicals { /** A complete regular identifier as allowed by ISO. */ Pattern ISO_REGULAR_IDENTIFIER = Pattern.compile(String.format( - "%1$s%2$s*+", + "%1$s%2$s{0,127}+", ISO_REGULAR_IDENTIFIER_START.pattern(), ISO_REGULAR_IDENTIFIER_PART.pattern() )); @@ -56,6 +56,54 @@ public interface Lexicals { "(%1$s)", ISO_REGULAR_IDENTIFIER.pattern() )); + /** A complete delimited identifier as allowed by ISO. As it happens, this + * is also the form PostgreSQL uses for elements of a LIST_QUOTE-typed GUC. + */ + Pattern ISO_DELIMITED_IDENTIFIER = Pattern.compile( + "\"(?:[^\"]|\"\"){1,128}+\"" + ); + + /** An ISO delimited identifier with a single capturing group that captures + * the content (which still needs to have "" replaced with " throughout). + * The capturing group is named {@code xd}. + */ + Pattern ISO_DELIMITED_IDENTIFIER_CAPTURING = Pattern.compile(String.format( + "\"(?(?:[^\"]|\"\"){1,128}+)\"" + )); + + /** The escape-specifier part of a Unicode delimited identifier or string. + * The escape character itself is in the capturing group named {@code uec}. + * The group can be absent, in which case \ should be used as the uec. + */ + Pattern ISO_UNICODE_ESCAPE_SPECIFIER = Pattern.compile( + "(?:\\p{IsWhite_Space}*+[Uu][Ee][Ss][Cc][Aa][Pp][Ee]"+ + "\\p{IsWhite_Space}*+'(?[^0-9A-Fa-f+'\"\\p{IsWhite_Space}])')?+" + ); + + /** A Unicode delimited identifier. The body is in capturing group + * {@code xui} and the escape character in group {@code uec}. The body + * still needs to have "" replaced with ", and {@code Unicode escape value}s + * decoded and replaced, and then it has to be verified to be no longer + * than 128 codepoints. + */ + Pattern ISO_UNICODE_IDENTIFIER = Pattern.compile(String.format( + "[Uu]&\"(?[^\"]++)\"%1$s", + ISO_UNICODE_ESCAPE_SPECIFIER.pattern() + )); + + /** A compilable pattern to match a {@code Unicode escape value}. + * A match should have one of three named capturing groups. If {@code cev}, + * substitute the {@code uec} itself. If {@code u4d} or {@code u6d}, + * substitute the codepoint represented by the hex digits. A match with none + * of those capturing groups indicates an ill-formed string. + *

+ * Maka a Pattern from this by supplying the right {@code uec}, so: + * {@code Pattern.compile(String.format(ISO_UNICODE_REPLACER, + * Pattern.quote(uec)));} + */ + String ISO_UNICODE_REPLACER = + "%1$s(?:(?%1$s)|(?[0-9A-Fa-f]{4})|\\+(?[0-9A-Fa-f]{6}))"; + /** Allowed as the first character of a regular identifier by PostgreSQL * (PG 7.4 -). */ @@ -95,12 +143,26 @@ public interface Lexicals { )); /** A regular identifier that satisfies both ISO and PostgreSQL rules, - * in a single capturing group. + * in a single capturing group named {@code i}. */ Pattern ISO_AND_PG_REGULAR_IDENTIFIER_CAPTURING = Pattern.compile( - String.format( "(%1$s)", ISO_AND_PG_REGULAR_IDENTIFIER.pattern()) + String.format( "(?%1$s)", ISO_AND_PG_REGULAR_IDENTIFIER.pattern()) ); + /** Pattern that matches any identifier valid by both ISO and PG rules, + * with the presence of named capturing groups indicating which kind it is: + * {@code i} for a regular identifier, {@code xd} for a delimited identifier + * (still needing "" replaced with "), or {@code xui} (with or without an + * explicit {@code uec} for a Unicode identifier (still needing "" to " and + * decoding of {@code Unicode escape value}s). + */ + Pattern ISO_AND_PG_IDENTIFIER_CAPTURING = Pattern.compile(String.format( + "%1$s|(?:%2$s)|(?:%3$s)", + ISO_AND_PG_REGULAR_IDENTIFIER_CAPTURING.pattern(), + ISO_DELIMITED_IDENTIFIER.pattern(), + ISO_UNICODE_IDENTIFIER.pattern() + )); + /** An identifier by ISO SQL, PostgreSQL, and Java (not SQL at all) * rules. (Not called {@code REGULAR} because Java allows no other form of * identifier.) This restrictive form is the safest for identifiers being From 389d6b36be229c90786309954810224101f6e714 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 31 Mar 2016 01:23:24 -0400 Subject: [PATCH 0006/1087] Make more quantifiers possessive. --- .../java/org/postgresql/pljava/sqlgen/DDRProcessor.java | 2 +- .../java/org/postgresql/pljava/management/Commands.java | 8 ++++---- .../org/postgresql/pljava/management/DDRExecutor.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 96c3d035..fda31ef5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -2382,7 +2382,7 @@ else if ( ! array ) // expression intended to match SQL types that are arrays static final Pattern arrayish = - Pattern.compile( "(?si:(?:\\[\\s*\\d*\\s*\\]|ARRAY)\\s*)$"); + Pattern.compile( "(?si:(?:\\[\\s*+\\d*+\\s*+\\]|ARRAY)\\s*+)$"); /** * Work around bizarre javac behavior that silently supplies an Error diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 97f74a6e..a344b9c6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -440,13 +440,13 @@ public static void addClassImages(int jarId, InputStream urlStream, int sz) } private final static Pattern ddrSection = Pattern.compile( - "(?<=[\\r\\n])Name: ((?:.|(?:\\r\\n?|\\n) )+)(?:(?:\\r\\n?|\\n))" + - "(?:[^\\r\\n]+(?:\\r\\n?|\\n)(?![\\r\\n]))*" + - "SQLJDeploymentDescriptor: (?:(?:\\r\\n?|\\r) )*TRUE(?!\\S)", + "(?<=[\\r\\n])Name: ((?:.|(?:\\r\\n?+|\\n) )++)(?:(?:\\r\\n?+|\\n))" + + "(?:[^\\r\\n]++(?:\\r\\n?+|\\n)(?![\\r\\n]))*" + + "SQLJDeploymentDescriptor: (?:(?:\\r\\n?+|\\r) )*+TRUE(?!\\S)", Pattern.CASE_INSENSITIVE ); - private final static Pattern mfCont = Pattern.compile( "(?:\\r\\n?|\\n) "); + private final static Pattern mfCont = Pattern.compile( "(?:\\r\\n?+|\\n) "); /** * Read and return a manifest, rewinding the buffered input stream. diff --git a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java index fee752c9..9238e876 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java @@ -79,7 +79,7 @@ protected DDRExecutor() { } * whitespace) indicates whether to parse more. */ private static final Pattern settingsRx = Pattern.compile(String.format( - "\\G(%1$s)(,\\s*)?", ISO_PG_JAVA_IDENTIFIER + "\\G(%1$s)(,\\s*+)?+", ISO_PG_JAVA_IDENTIFIER )); /** From 9d250c41e1b70f1072a95f76947514bb03fa5b42 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 31 Mar 2016 21:26:48 -0400 Subject: [PATCH 0007/1087] No more standard charset lookups by name. Rethink use of java.util.Scanner to slurp data ... it's very convenient, but part of the reason is it swallows IOExceptions. If you add the extra lines of code to hang onto the reference and call its ioException() method to see if something went wrong, the brevity advantage slips away. --- .../example/annotation/UDTScalarIOTest.java | 4 ++-- pljava-so/src/main/c/type/String.c | 12 +++++----- .../pljava/internal/InstallHelper.java | 23 +++++++++++++++---- .../pljava/jdbc/SQLInputFromChunk.java | 7 ++---- .../pljava/jdbc/SQLOutputToChunk.java | 9 +++----- .../pljava/management/Commands.java | 22 +++++++++--------- 6 files changed, 42 insertions(+), 35 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java index 7d970b41..3cd83200 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java @@ -25,7 +25,7 @@ import java.io.StringReader; import java.nio.ByteBuffer; import java.nio.CharBuffer; -import java.nio.charset.Charset; +import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; @@ -96,7 +96,7 @@ public class UDTScalarIOTest implements SQLData s_gedicht = s_gedicht + s_gedicht + s_gedicht; // x3 s_gedicht = s_gedicht + s_gedicht + s_gedicht; // x9 - ByteBuffer bb = Charset.forName("UTF-8").newEncoder().encode( + ByteBuffer bb = UTF_8.newEncoder().encode( CharBuffer.wrap(s_gedicht)); s_utfgedicht = new byte[bb.limit()]; bb.get(s_utfgedicht); diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index 094f257a..e095485e 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -380,16 +380,16 @@ static void String_initialize_codec() jmethodID string_intern = PgObject_getJavaMethod(s_String_class, "intern", "()Ljava/lang/String;"); jstring empty = JNI_newStringUTF( ""); - jstring u8Name = JNI_newStringUTF( "UTF-8"); - jclass charset_class = PgObject_getJavaClass("java/nio/charset/Charset"); - jmethodID charset_forName = PgObject_getStaticJavaMethod(charset_class, - "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;"); + jclass scharset_class = + PgObject_getJavaClass("java/nio/charset/StandardCharsets"); + jfieldID scharset_UTF_8 = PgObject_getStaticJavaField(scharset_class, + "UTF_8", "Ljava/nio/charset/Charset;"); + jobject u8cs = JNI_getStaticObjectField(scharset_class, scharset_UTF_8); + jclass charset_class = JNI_getObjectClass(u8cs); jmethodID charset_newDecoder = PgObject_getJavaMethod(charset_class, "newDecoder", "()Ljava/nio/charset/CharsetDecoder;"); jmethodID charset_newEncoder = PgObject_getJavaMethod(charset_class, "newEncoder", "()Ljava/nio/charset/CharsetEncoder;"); - jobject u8cs = JNI_callStaticObjectMethod(charset_class, charset_forName, - u8Name); jclass decoder_class = PgObject_getJavaClass("java/nio/charset/CharsetDecoder"); jclass encoder_class = diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 19def09b..64cf6b6d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -12,6 +12,8 @@ package org.postgresql.pljava.internal; import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; @@ -24,6 +26,7 @@ import java.text.ParseException; import java.util.Scanner; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.sql.Types.VARCHAR; import org.postgresql.pljava.jdbc.SQLUtils; @@ -131,7 +134,7 @@ public static String hello( public static void groundwork( String module_pathname, String loadpath_tbl, String loadpath_tbl_quoted, boolean asExtension, boolean exNihilo) - throws SQLException, ParseException + throws SQLException, ParseException, IOException { Connection c = null; Statement s = null; @@ -293,7 +296,7 @@ private static void languages( Connection c, Statement s) * schema variant is detected, attempt to migrate to the current one. */ private static void deployment( Connection c, Statement s, SchemaVariant sv) - throws SQLException, ParseException + throws SQLException, ParseException, IOException { if ( currentSchema == sv ) return; // assume (optimistically) that means there's nothing to do @@ -304,9 +307,19 @@ private static void deployment( Connection c, Statement s, SchemaVariant sv) return; } - InputStream is = InstallHelper.class.getResourceAsStream("/pljava.ddr"); - String raw = new Scanner(is, "utf-8").useDelimiter("\\A").next(); - SQLDeploymentDescriptor sdd = new SQLDeploymentDescriptor(raw); + StringBuilder sb; + try(InputStream is = + InstallHelper.class.getResourceAsStream("/pljava.ddr"); + InputStreamReader isr = + new InputStreamReader(is, UTF_8.newDecoder())) + { + sb = new StringBuilder(); + char[] buf = new char[512]; + for ( int got; -1 != (got = isr.read(buf)); ) + sb.append(buf, 0, got); + } + SQLDeploymentDescriptor sdd = + new SQLDeploymentDescriptor(sb.toString()); sdd.install(c); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromChunk.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromChunk.java index d668da36..571ab951 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromChunk.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromChunk.java @@ -22,7 +22,7 @@ import java.net.URL; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.charset.Charset; +import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetDecoder; import java.sql.Array; import java.sql.Blob; @@ -57,9 +57,6 @@ public class SQLInputFromChunk implements SQLInput { private ByteBuffer m_bb; - /* get rid of this once no longer supporting back to Java 6 */ - private static final Charset UTF8 = Charset.forName("UTF-8"); - private static ByteOrder scalarOrder; private static ByteOrder mirrorOrder; @@ -282,7 +279,7 @@ public String readString() throws SQLException int len = m_bb.getShort() & 0xffff; ByteBuffer bytes = (ByteBuffer)m_bb.slice().limit(len); m_bb.position(m_bb.position() + len); - return UTF8.newDecoder().decode(bytes).toString(); + return UTF_8.newDecoder().decode(bytes).toString(); } catch ( Exception e ) { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToChunk.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToChunk.java index b2c2db09..1c4f12c2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToChunk.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToChunk.java @@ -23,7 +23,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.CharBuffer; -import java.nio.charset.Charset; +import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.MalformedInputException; @@ -64,9 +64,6 @@ public class SQLOutputToChunk implements SQLOutput { private static final byte[] s_byteBuffer = new byte[8]; - /* get rid of this once no longer supporting back to Java 6 */ - private static final Charset UTF8 = Charset.forName("UTF-8"); - private long m_handle; private ByteBuffer m_bb; @@ -193,7 +190,7 @@ public void writeCharacterStream(Reader value) throws SQLException { ByteBuffer bb = ByteBuffer.allocate(65535); CharBuffer cb = CharBuffer.allocate(1024); - CharsetEncoder enc = UTF8.newEncoder(); + CharsetEncoder enc = UTF_8.newEncoder(); CoderResult cr; try @@ -331,7 +328,7 @@ public void writeString(String value) throws SQLException CharBuffer cb = CharBuffer.wrap(value); try { - CharsetEncoder enc = UTF8.newEncoder(); + CharsetEncoder enc = UTF_8.newEncoder(); ByteBuffer bb = enc.encode(cb); int len = bb.limit(); if ( 65535 < len ) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index a344b9c6..f30ba2e1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -18,12 +18,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.charset.Charset; +import java.nio.ByteBuffer; +import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetDecoder; -import java.io.UnsupportedEncodingException; +import java.nio.charset.CharacterCodingException; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLData; +import java.sql.SQLDataException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLNonTransientException; @@ -482,8 +484,7 @@ private static String rawManifest( BufferedInputStream bis, int markLimit) if ( "META-INF/MANIFEST.MF".equals( ze.getName()) ) { StringBuilder sb = new StringBuilder(); - // I'll take my chances on a required charset not being there! - CharsetDecoder u8 = Charset.forName( "UTF-8").newDecoder(); + CharsetDecoder u8 = UTF_8.newDecoder(); InputStreamReader isr = new InputStreamReader( zis, u8); char[] b = new char[512]; for ( int got; -1 != (got = isr.read(b)); ) @@ -993,20 +994,19 @@ private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) new ArrayList(); while(rs.next()) { - byte[] bytes = rs.getBytes(1); + ByteBuffer bytes = ByteBuffer.wrap(rs.getBytes(1)); // According to the SQLJ standard, this entry must be // UTF8 encoded. // - sdds.add( - new SQLDeploymentDescriptor(new String(bytes, "UTF8"))); + sdds.add( new SQLDeploymentDescriptor( + UTF_8.newDecoder().decode(bytes).toString())); } return sdds.toArray( new SQLDeploymentDescriptor[sdds.size()]); } - catch(UnsupportedEncodingException e) + catch(CharacterCodingException e) { - // Excuse me? No UTF8 encoding? - // - throw new SQLException("JVM does not support UTF8!!"); + throw new SQLDataException( + "deployment descriptor is not well-formed UTF-8", "22021", e); } catch(ParseException e) { From 21b380ed0eaa1cd3ca9ff1c3720a07b8791f043b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 31 Mar 2016 22:16:43 -0400 Subject: [PATCH 0008/1087] Prefer try-with-resources in some easy places. --- .../pljava/internal/InstallHelper.java | 13 +- .../pljava/management/Commands.java | 167 ++++++------------ 2 files changed, 59 insertions(+), 121 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 64cf6b6d..c4c319a3 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -136,13 +136,9 @@ public static void groundwork( boolean asExtension, boolean exNihilo) throws SQLException, ParseException, IOException { - Connection c = null; - Statement s = null; - try + try(Connection c = SQLUtils.getDefaultConnection(); + Statement s = c.createStatement()) { - c = SQLUtils.getDefaultConnection(); - s = c.createStatement(); - schema(c, s); SchemaVariant sv = recognizeSchema(c, s, loadpath_tbl); @@ -171,11 +167,6 @@ public static void groundwork( */ s.execute("DROP TABLE sqlj." + loadpath_tbl_quoted); } - finally - { - SQLUtils.close(s); - SQLUtils.close(c); - } } private static void schema( Connection c, Statement s) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index f30ba2e1..f5de4295 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -515,8 +515,10 @@ private static String rawManifest( BufferedInputStream bis, int markLimit) public static void addTypeMapping(String sqlTypeName, String javaClassName) throws SQLException { - PreparedStatement stmt = null; - try + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + .prepareStatement( + "INSERT INTO sqlj.typemap_entry(javaName, sqlName)" + + " VALUES(?,?)")) { ClassLoader loader = Loader.getCurrentLoader(); Class cls = loader.loadClass(javaClassName); @@ -525,10 +527,6 @@ public static void addTypeMapping(String sqlTypeName, String javaClassName) + " does not implement java.sql.SQLData"); sqlTypeName = getFullSqlNameOwned(sqlTypeName); - stmt = SQLUtils - .getDefaultConnection() - .prepareStatement( - "INSERT INTO sqlj.typemap_entry(javaName, sqlName) VALUES(?,?)"); stmt.setString(1, javaClassName); stmt.setString(2, sqlTypeName); stmt.executeUpdate(); @@ -538,10 +536,6 @@ public static void addTypeMapping(String sqlTypeName, String javaClassName) throw new SQLException( "No such class: " + javaClassName, "46103", e); } - finally - { - SQLUtils.close(stmt); - } Loader.clearSchemaLoaders(); } @@ -558,19 +552,14 @@ public static void addTypeMapping(String sqlTypeName, String javaClassName) @Function(schema="sqlj", name="drop_type_mapping", security=DEFINER) public static void dropTypeMapping(String sqlTypeName) throws SQLException { - PreparedStatement stmt = null; - try + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + .prepareStatement( + "DELETE FROM sqlj.typemap_entry WHERE sqlName = ?")) { sqlTypeName = getFullSqlNameOwned(sqlTypeName); - stmt = SQLUtils.getDefaultConnection().prepareStatement( - "DELETE FROM sqlj.typemap_entry WHERE sqlName = ?"); stmt.setString(1, sqlTypeName); stmt.executeUpdate(); } - finally - { - SQLUtils.close(stmt); - } Loader.clearSchemaLoaders(); } @@ -587,40 +576,32 @@ public static void dropTypeMapping(String sqlTypeName) throws SQLException @Function(schema="sqlj", name="get_classpath", security=DEFINER) public static String getClassPath(String schemaName) throws SQLException { - ResultSet rs = null; - PreparedStatement stmt = null; - try + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + .prepareStatement( + "SELECT r.jarName"+ + " FROM sqlj.jar_repository r INNER JOIN sqlj.classpath_entry c"+ + " ON r.jarId = c.jarId"+ + " WHERE c.schemaName = ? ORDER BY c.ordinal")) { if(schemaName == null || schemaName.length() == 0) schemaName = "public"; else schemaName = schemaName.toLowerCase(); - - stmt = SQLUtils - .getDefaultConnection() - .prepareStatement( - "SELECT r.jarName" - + " FROM sqlj.jar_repository r INNER JOIN sqlj.classpath_entry c ON r.jarId = c.jarId" - + " WHERE c.schemaName = ? ORDER BY c.ordinal"); - stmt.setString(1, schemaName); - rs = stmt.executeQuery(); StringBuffer buf = null; - while(rs.next()) + try(ResultSet rs = stmt.executeQuery()) { - if(buf == null) - buf = new StringBuffer(); - else - buf.append(':'); - buf.append(rs.getString(1)); + while(rs.next()) + { + if(buf == null) + buf = new StringBuffer(); + else + buf.append(':'); + buf.append(rs.getString(1)); + } } return (buf == null) ? null : buf.toString(); } - finally - { - SQLUtils.close(rs); - SQLUtils.close(stmt); - } } public static String getCurrentSchema() throws SQLException @@ -802,7 +783,6 @@ public static void setClassPath(String schemaName, String path) "the target schema in order to set the classpath", "42501"); } - PreparedStatement stmt; ArrayList entries = null; if(path != null && path.length() > 0) { @@ -810,9 +790,9 @@ public static void setClassPath(String schemaName, String path) // valid jar // entries = new ArrayList(); - stmt = SQLUtils.getDefaultConnection().prepareStatement( - "SELECT jarId FROM sqlj.jar_repository WHERE jarName = ?"); - try + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + .prepareStatement( + "SELECT jarId FROM sqlj.jar_repository WHERE jarName = ?")) { for(;;) { @@ -836,35 +816,27 @@ public static void setClassPath(String schemaName, String path) break; } } - finally - { - SQLUtils.close(stmt); - } } // Delete the old classpath // - stmt = SQLUtils.getDefaultConnection().prepareStatement( - "DELETE FROM sqlj.classpath_entry WHERE schemaName = ?"); - try + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + .prepareStatement( + "DELETE FROM sqlj.classpath_entry WHERE schemaName = ?")) { stmt.setString(1, schemaName); stmt.executeUpdate(); } - finally - { - SQLUtils.close(stmt); - } if(entries != null) { // Insert the new path. // - stmt = SQLUtils - .getDefaultConnection() + ; + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( - "INSERT INTO sqlj.classpath_entry(schemaName, ordinal, jarId) VALUES(?, ?, ?)"); - try + "INSERT INTO sqlj.classpath_entry("+ + " schemaName, ordinal, jarId) VALUES(?, ?, ?)")) { int top = entries.size(); for(int idx = 0; idx < top; ++idx) @@ -876,10 +848,6 @@ public static void setClassPath(String schemaName, String path) stmt.executeUpdate(); } } - finally - { - SQLUtils.close(stmt); - } } Loader.clearSchemaLoaders(); } @@ -1032,34 +1000,28 @@ private static String getFullSqlNameOwned(String sqlTypeName) AclId invoker = AclId.getOuterUser(); - ResultSet rs = null; - PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT n.nspname, t.typname," + " pg_catalog.pg_has_role(?, t.typowner, 'USAGE')" + " FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n" - + " WHERE t.oid = ? AND n.oid = t.typnamespace"); - - try + + " WHERE t.oid = ? AND n.oid = t.typnamespace")) { stmt.setObject(1, invoker); stmt.setObject(2, typeId); - rs = stmt.executeQuery(); - if(!rs.next()) - throw new SQLException("Unable to obtain type info for " - + typeId); + try(ResultSet rs = stmt.executeQuery()) + { + if(!rs.next()) + throw new SQLException("Unable to obtain type info for " + + typeId); - if ( ! rs.getBoolean(3) ) - throw new SQLSyntaxErrorException( // yeah, for 42501, really - "Permission denied. Only superuser or type's owner " + - "may add or drop a type mapping.", "42501"); + if ( ! rs.getBoolean(3) ) + throw new SQLSyntaxErrorException( // yes, for 42501, really + "Permission denied. Only superuser or type's owner " + + "may add or drop a type mapping.", "42501"); - return rs.getString(1) + '.' + rs.getString(2); - } - finally - { - SQLUtils.close(rs); - SQLUtils.close(stmt); + return rs.getString(1) + '.' + rs.getString(2); + } } } @@ -1067,8 +1029,7 @@ private static int getJarId(PreparedStatement stmt, String jarName, AclId[] ownerRet) throws SQLException { stmt.setString(1, jarName); - ResultSet rs = stmt.executeQuery(); - try + try(ResultSet rs = stmt.executeQuery()) { if(!rs.next()) return -1; @@ -1080,10 +1041,6 @@ private static int getJarId(PreparedStatement stmt, String jarName, } return id; } - finally - { - SQLUtils.close(rs); - } } /** @@ -1099,18 +1056,13 @@ private static int getJarId(PreparedStatement stmt, String jarName, private static int getJarId(String jarName, AclId[] ownerRet) throws SQLException { - PreparedStatement stmt = SQLUtils - .getDefaultConnection() + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( - "SELECT jarId, jarOwner FROM sqlj.jar_repository WHERE jarName = ?"); - try + "SELECT jarId, jarOwner FROM sqlj.jar_repository"+ + " WHERE jarName = ?")) { return getJarId(stmt, jarName, ownerRet); } - finally - { - SQLUtils.close(stmt); - } } /** @@ -1123,22 +1075,17 @@ private static int getJarId(String jarName, AclId[] ownerRet) */ private static Oid getSchemaId(String schemaName) throws SQLException { - ResultSet rs = null; - PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( - "SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = ?"); - try + "SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = ?")) { stmt.setString(1, schemaName); - rs = stmt.executeQuery(); - if(!rs.next()) - return null; - return (Oid)rs.getObject(1); - } - finally - { - SQLUtils.close(rs); - SQLUtils.close(stmt); + try(ResultSet rs = stmt.executeQuery()) + { + if(!rs.next()) + return null; + return (Oid)rs.getObject(1); + } } } From 6604a7f2a7ac870ea3bd076ea43618841c481880 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 31 Mar 2016 22:55:24 -0400 Subject: [PATCH 0009/1087] Adopt the diamond operator. --- .../pljava/sqlgen/DDRProcessor.java | 40 +++++++++---------- .../pljava/example/MetaDataBooleans.java | 4 +- .../pljava/example/MetaDataInts.java | 4 +- .../pljava/example/MetaDataStrings.java | 4 +- .../pljava/example/MetaDataTest.java | 10 ++--- .../pljava/example/ResultSetTest.java | 2 +- .../example/UsingPropertiesAsScalarSet.java | 2 +- .../UsingPropertiesAsScalarSet.java | 4 +- .../postgresql/pljava/jdbc/SPIConnection.java | 34 ++++++++++------ .../pljava/management/Commands.java | 5 +-- .../pljava/management/DDRExecutor.java | 4 +- .../management/SQLDeploymentDescriptor.java | 20 ++++++---- 12 files changed, 70 insertions(+), 63 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index fda31ef5..1ebc8a8b 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2016 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 @@ -318,7 +318,7 @@ public int hashCode() * one round), keyed by the object for which each snippet has been * generated. */ - Map snippets = new HashMap(); + Map snippets = new HashMap<>(); S getSnippet(Object o, Class c) { @@ -340,21 +340,20 @@ void putSnippet( Object o, Snippet s) * generateDescriptor, any errors reported were being shown with no source * location info, because it had been thrown away. */ - Queue> snippetQueue = new LinkedList>(); + Queue> snippetQueue = new LinkedList<>(); /** * Map from each arbitrary provides/requires label to the snippet * that 'provides' it. Has to be out here as an instance field for the * same reason {@code snippetQueue} does. */ - Map> provider = - new HashMap>(); + Map> provider = new HashMap<>(); /** * Set of provides/requires labels for which at least one consumer has * been seen. An instance field for the same reason as {@code provider}. */ - Set consumer = new HashSet(); + Set consumer = new HashSet<>(); /** * Find the elements in each round that carry any of the annotations of @@ -440,7 +439,7 @@ void defensiveEarlyCharacterize() { if ( ! snip.characterize() ) continue; - Vertex v = new Vertex( snip); + Vertex v = new Vertex<>( snip); snippetQueue.add( v); for ( String s : snip.provides() ) if ( null != provider.put( s, v) ) @@ -490,7 +489,7 @@ else if ( s == v.payload.implementor() ) // yes == if from impl Snippet[] snips = new Snippet [ snippetQueue.size() ]; - Queue> q = new LinkedList>(); + Queue> q = new LinkedList<>(); for ( Iterator> it = snippetQueue.iterator() ; it.hasNext() ; ) { @@ -1329,7 +1328,7 @@ public boolean characterize() else if ( typu.isAssignable( typu.erasure( ret), TY_ITERATOR) ) { setof = true; - List pending = new LinkedList(); + List pending = new LinkedList<>(); pending.add( ret); while ( ! pending.isEmpty() ) { @@ -1457,7 +1456,7 @@ void appendAS( StringBuilder sb) public String[] deployStrings() { - ArrayList al = new ArrayList(); + ArrayList al = new ArrayList<>(); StringBuilder sb = new StringBuilder(); sb.append( "CREATE OR REPLACE FUNCTION "); appendNameAndParams( sb, true); @@ -1767,7 +1766,7 @@ public boolean characterize() public String[] deployStrings() { - ArrayList al = new ArrayList(); + ArrayList al = new ArrayList<>(); if ( null != structure() ) { StringBuilder sb = new StringBuilder(); @@ -1788,7 +1787,7 @@ public String[] deployStrings() public String[] undeployStrings() { - ArrayList al = new ArrayList(); + ArrayList al = new ArrayList<>(); al.add( "SELECT sqlj.drop_type_mapping(" + DDRWriter.eQuote( qname) + ')'); if ( null != structure() ) @@ -1966,7 +1965,7 @@ public boolean characterize() public String[] deployStrings() { - ArrayList al = new ArrayList(); + ArrayList al = new ArrayList<>(); al.add( "CREATE TYPE " + qname); al.addAll( Arrays.asList( in.deployStrings())); @@ -2052,7 +2051,7 @@ class TypeMapper TypeMapper() { - protoMappings = new ArrayList, String>>(); + protoMappings = new ArrayList<>(); // Primitives // @@ -2122,12 +2121,11 @@ private void workAroundJava7Breakage() // assignable to by widening reference conversions, so a // topological sort is in order. // - List, String>>> vs = - new ArrayList, String>>>( + List, String>>> vs = new ArrayList<>( protoMappings.size()); for ( Map.Entry, String> me : protoMappings ) - vs.add( new Vertex, String>>( me)); + vs.add( new Vertex<>( me)); for ( int i = vs.size(); i --> 1; ) { @@ -2148,14 +2146,12 @@ private void workAroundJava7Breakage() } } - Queue, String>>> q = - new LinkedList, String>>>(); + Queue, String>>> q = new LinkedList<>(); for ( Vertex, String>> v : vs ) if ( 0 == v.indegree ) q.add( v); - finalMappings = new ArrayList>( - protoMappings.size()); + finalMappings = new ArrayList<>( protoMappings.size()); protoMappings.clear(); while ( ! q.isEmpty() ) @@ -2477,7 +2473,7 @@ class Vertex

{ this.payload = payload; indegree = 0; - adj = new ArrayList>(); + adj = new ArrayList<>(); } void precede( Vertex

v) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataBooleans.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataBooleans.java index e70c4da8..647c26f0 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataBooleans.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataBooleans.java @@ -64,8 +64,8 @@ public int compare(Method a, Method b) { Class returntype; Object[] args = new Object[0]; Boolean result = null; - ArrayList mn = new ArrayList(); - ArrayList mr = new ArrayList(); + ArrayList mn = new ArrayList<>(); + ArrayList mr = new ArrayList<>(); for (int i = 0; i < m.length; i++) { prototype = m[i].getParameterTypes(); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataInts.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataInts.java index 3dd72fff..4142d950 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataInts.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataInts.java @@ -65,8 +65,8 @@ public int compare(Method a, Method b) { Class returntype; Object[] args = new Object[0]; Integer result = null; - ArrayList mn = new ArrayList(); - ArrayList mr = new ArrayList(); + ArrayList mn = new ArrayList<>(); + ArrayList mr = new ArrayList<>(); for (int i = 0; i < m.length; i++) { prototype = m[i].getParameterTypes(); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataStrings.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataStrings.java index 9ee4b4b9..4c3efb27 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataStrings.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataStrings.java @@ -64,8 +64,8 @@ public int compare(Method a, Method b) { Class returntype; Object[] args = new Object[0]; String result = null; - ArrayList mn = new ArrayList(); - ArrayList mr = new ArrayList(); + ArrayList mn = new ArrayList<>(); + ArrayList mr = new ArrayList<>(); for (int i = 0; i < m.length; i++) { prototype = m[i].getParameterTypes(); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataTest.java index 227d5bc2..b66f45b0 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/MetaDataTest.java @@ -49,7 +49,7 @@ public MetaDataTest(String methodCall) throws SQLException { .getConnection("jdbc:default:connection"); DatabaseMetaData md = conn.getMetaData(); ResultSet rs; - m_results = new ArrayList(); + m_results = new ArrayList<>(); StringBuffer result; parseMethodCall(methodCall); @@ -136,8 +136,8 @@ private void parseMethodCall(String methodCall) throws SQLException { String paramString; String auxParamString; String param; - ArrayList objects = new ArrayList(); - ArrayList> types = new ArrayList>(); + ArrayList objects = new ArrayList<>(); + ArrayList> types = new ArrayList<>(); if (m.matches()) { m_methodName = m.group(1); @@ -182,7 +182,7 @@ private void parseMethodCall(String methodCall) throws SQLException { .compile("^\\s*\"((?:[^\\\\\"]|\\\\.)*)\"\\s*(?:,|$)(.*)$"); Matcher marr; String auxParamArr = param.trim(); - ArrayList strList = new ArrayList(); + ArrayList strList = new ArrayList<>(); while (!auxParamArr.equals("")) { marr = parr.matcher(auxParamArr); @@ -238,7 +238,7 @@ private void parseMethodCall(String methodCall) throws SQLException { .compile("^\\s*(\\d+)\\s*(?:,|$)(.*)$"); Matcher marr; String auxParamArr = param.trim(); - ArrayList intList = new ArrayList(); + ArrayList intList = new ArrayList<>(); while (!auxParamArr.equals("")) { marr = parr.matcher(auxParamArr); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/ResultSetTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/ResultSetTest.java index 833b4da2..9e70aaaa 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/ResultSetTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/ResultSetTest.java @@ -42,7 +42,7 @@ public static Iterator executeSelect(String selectSQL) throws SQLExcepti public ResultSetTest(String selectSQL) throws SQLException { Connection conn = DriverManager .getConnection("jdbc:default:connection"); - m_results = new ArrayList(); + m_results = new ArrayList<>(); StringBuffer result; Statement stmt = conn.createStatement(); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingPropertiesAsScalarSet.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingPropertiesAsScalarSet.java index 8670e7db..d486c2a6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingPropertiesAsScalarSet.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingPropertiesAsScalarSet.java @@ -30,7 +30,7 @@ public class UsingPropertiesAsScalarSet { public static Iterator getProperties() throws SQLException { StringBuffer bld = new StringBuffer(); - ArrayList list = new ArrayList(); + ArrayList list = new ArrayList<>(); Connection conn = DriverManager .getConnection("jdbc:default:connection"); Statement stmt = conn.createStatement(); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingPropertiesAsScalarSet.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingPropertiesAsScalarSet.java index 0e8a2582..3d5226a8 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingPropertiesAsScalarSet.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingPropertiesAsScalarSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2016 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 @@ -37,7 +37,7 @@ public static Iterator getProperties() throws SQLException { StringBuilder bld = new StringBuilder(); - ArrayList list = new ArrayList(); + ArrayList list = new ArrayList<>(); Connection conn = DriverManager.getConnection("jdbc:default:connection"); Statement stmt = conn.createStatement(); try diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index c7a120a6..7d56c1a1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2009, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * 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: + * Tada AB + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -993,12 +998,14 @@ public T unwrap(Class iface) "0A000" ); } - public void setClientInfo(String name, String value) throws SQLClientInfoException - { - Map failures = new HashMap(); - failures.put(name, ClientInfoStatus.REASON_UNKNOWN_PROPERTY); - throw new SQLClientInfoException("ClientInfo property not supported.", failures); - } + public void setClientInfo(String name, String value) + throws SQLClientInfoException + { + Map failures = new HashMap<>(); + failures.put(name, ClientInfoStatus.REASON_UNKNOWN_PROPERTY); + throw new SQLClientInfoException( + "ClientInfo property not supported.", failures); + } public void setClientInfo(Properties properties) @@ -1007,13 +1014,14 @@ public void setClientInfo(Properties properties) if (properties == null || properties.size() == 0) return; - Map failures = new HashMap(); + Map failures = new HashMap<>(); Iterator i = properties.stringPropertyNames().iterator(); while (i.hasNext()) { failures.put(i.next(), ClientInfoStatus.REASON_UNKNOWN_PROPERTY); } - throw new SQLClientInfoException("ClientInfo property not supported.", failures); + throw new SQLClientInfoException( + "ClientInfo property not supported.", failures); } public String getClientInfo(String name) throws SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index f5de4295..7a6564f2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2016 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 @@ -958,8 +958,7 @@ private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) { stmt.setInt(1, jarId); rs = stmt.executeQuery(); - ArrayList sdds = - new ArrayList(); + ArrayList sdds = new ArrayList<>(); while(rs.next()) { ByteBuffer bytes = ByteBuffer.wrap(rs.getBytes(1)); diff --git a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java index 9238e876..75c1f1a0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2016 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 @@ -123,7 +123,7 @@ public static DDRExecutor forImplementor( String name) private static String[] implementors() throws SQLException { String settingString = Backend.getConfigOption( "pljava.implementors"); - ArrayList al = new ArrayList(); + ArrayList al = new ArrayList<>(); Matcher m = settingsRx.matcher( settingString); while ( m.find() ) { diff --git a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java index afc67904..69f86f66 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.management; @@ -87,10 +93,8 @@ */ public class SQLDeploymentDescriptor { - private final ArrayList m_installCommands = - new ArrayList(); - private final ArrayList m_removeCommands = - new ArrayList(); + private final ArrayList m_installCommands = new ArrayList<>(); + private final ArrayList m_removeCommands = new ArrayList<>(); private final StringBuffer m_buffer = new StringBuffer(); private final char[] m_image; From a4c29cf1efd87cf780c13df961cb095e8e8f795b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Apr 2016 10:06:05 -0400 Subject: [PATCH 0010/1087] Lexicals.identifierFrom. Fixed a typo in ISO_UNICODE_IDENTIFIER and added JUnit test (not necessarily in that order). This changes Lexicals from an interface to an abstract class. That should not affect any client code unless it was written to inherit from Lexicals, which would have been an example of the "constant interface antipattern" so with any luck will not have been common. --- .../postgresql/pljava/sqlgen/Lexicals.java | 100 ++++++++++++++---- pljava-api/src/test/java/LexicalsTest.java | 44 ++++++++ pom.xml | 9 ++ 3 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 pljava-api/src/test/java/LexicalsTest.java diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 58ae87d3..ca7fcdaf 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -11,6 +11,7 @@ */ package org.postgresql.pljava.sqlgen; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -27,24 +28,26 @@ * present. Of course backend code such as {@code SQLDeploymentDescriptor} * can also refer to these. */ -public interface Lexicals { +public abstract class Lexicals { /** Allowed as the first character of a regular identifier by ISO. */ - Pattern ISO_REGULAR_IDENTIFIER_START = Pattern.compile( + public static final Pattern ISO_REGULAR_IDENTIFIER_START = Pattern.compile( "[\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{Lo}\\p{Nl}]" ); /** Allowed as any non-first character of a regular identifier by ISO. */ - Pattern ISO_REGULAR_IDENTIFIER_PART = Pattern.compile(String.format( + public static final Pattern ISO_REGULAR_IDENTIFIER_PART = + Pattern.compile(String.format( "[\\xb7\\p{Mn}\\p{Mc}\\p{Nd}\\p{Pc}\\p{Cf}%1$s]", ISO_REGULAR_IDENTIFIER_START.pattern() )); /** A complete regular identifier as allowed by ISO. */ - Pattern ISO_REGULAR_IDENTIFIER = Pattern.compile(String.format( + public static final Pattern ISO_REGULAR_IDENTIFIER = + Pattern.compile(String.format( "%1$s%2$s{0,127}+", ISO_REGULAR_IDENTIFIER_START.pattern(), ISO_REGULAR_IDENTIFIER_PART.pattern() @@ -52,14 +55,15 @@ public interface Lexicals { /** A complete ISO regular identifier in a single capturing group. */ - Pattern ISO_REGULAR_IDENTIFIER_CAPTURING = Pattern.compile(String.format( + public static final Pattern ISO_REGULAR_IDENTIFIER_CAPTURING = + Pattern.compile(String.format( "(%1$s)", ISO_REGULAR_IDENTIFIER.pattern() )); /** A complete delimited identifier as allowed by ISO. As it happens, this * is also the form PostgreSQL uses for elements of a LIST_QUOTE-typed GUC. */ - Pattern ISO_DELIMITED_IDENTIFIER = Pattern.compile( + public static final Pattern ISO_DELIMITED_IDENTIFIER = Pattern.compile( "\"(?:[^\"]|\"\"){1,128}+\"" ); @@ -67,7 +71,8 @@ public interface Lexicals { * the content (which still needs to have "" replaced with " throughout). * The capturing group is named {@code xd}. */ - Pattern ISO_DELIMITED_IDENTIFIER_CAPTURING = Pattern.compile(String.format( + public static final Pattern ISO_DELIMITED_IDENTIFIER_CAPTURING = + Pattern.compile(String.format( "\"(?(?:[^\"]|\"\"){1,128}+)\"" )); @@ -75,7 +80,8 @@ public interface Lexicals { * The escape character itself is in the capturing group named {@code uec}. * The group can be absent, in which case \ should be used as the uec. */ - Pattern ISO_UNICODE_ESCAPE_SPECIFIER = Pattern.compile( + public static final Pattern ISO_UNICODE_ESCAPE_SPECIFIER = + Pattern.compile( "(?:\\p{IsWhite_Space}*+[Uu][Ee][Ss][Cc][Aa][Pp][Ee]"+ "\\p{IsWhite_Space}*+'(?[^0-9A-Fa-f+'\"\\p{IsWhite_Space}])')?+" ); @@ -86,8 +92,9 @@ public interface Lexicals { * decoded and replaced, and then it has to be verified to be no longer * than 128 codepoints. */ - Pattern ISO_UNICODE_IDENTIFIER = Pattern.compile(String.format( - "[Uu]&\"(?[^\"]++)\"%1$s", + public static final Pattern ISO_UNICODE_IDENTIFIER = + Pattern.compile(String.format( + "[Uu]&\"(?(?:[^\"]|\"\")++)\"%1$s", ISO_UNICODE_ESCAPE_SPECIFIER.pattern() )); @@ -101,26 +108,28 @@ public interface Lexicals { * {@code Pattern.compile(String.format(ISO_UNICODE_REPLACER, * Pattern.quote(uec)));} */ - String ISO_UNICODE_REPLACER = + public static final String ISO_UNICODE_REPLACER = "%1$s(?:(?%1$s)|(?[0-9A-Fa-f]{4})|\\+(?[0-9A-Fa-f]{6}))"; /** Allowed as the first character of a regular identifier by PostgreSQL * (PG 7.4 -). */ - Pattern PG_REGULAR_IDENTIFIER_START = Pattern.compile( + public static final Pattern PG_REGULAR_IDENTIFIER_START = Pattern.compile( "[A-Za-z\\P{ASCII}_]" // hasn't seen a change since PG 7.4 ); /** Allowed as any non-first character of a regular identifier by PostgreSQL * (PG 7.4 -). */ - Pattern PG_REGULAR_IDENTIFIER_PART = Pattern.compile(String.format( + public static final Pattern PG_REGULAR_IDENTIFIER_PART = + Pattern.compile(String.format( "[0-9$%1$s]", PG_REGULAR_IDENTIFIER_START.pattern() )); /** A complete regular identifier as allowed by PostgreSQL (PG 7.4 -). */ - Pattern PG_REGULAR_IDENTIFIER = Pattern.compile(String.format( + public static final Pattern PG_REGULAR_IDENTIFIER = + Pattern.compile(String.format( "%1$s%2$s*+", PG_REGULAR_IDENTIFIER_START.pattern(), PG_REGULAR_IDENTIFIER_PART.pattern() @@ -128,13 +137,15 @@ public interface Lexicals { /** A complete PostgreSQL regular identifier in a single capturing group. */ - Pattern PG_REGULAR_IDENTIFIER_CAPTURING = Pattern.compile(String.format( + public static final Pattern PG_REGULAR_IDENTIFIER_CAPTURING = + Pattern.compile(String.format( "(%1$s)", PG_REGULAR_IDENTIFIER.pattern() )); /** A regular identifier that satisfies both ISO and PostgreSQL rules. */ - Pattern ISO_AND_PG_REGULAR_IDENTIFIER = Pattern.compile(String.format( + public static final Pattern ISO_AND_PG_REGULAR_IDENTIFIER = + Pattern.compile(String.format( "(?:(?=%1$s)%2$s)(?:(?=%3$s)%4$s)*+", ISO_REGULAR_IDENTIFIER_START.pattern(), PG_REGULAR_IDENTIFIER_START.pattern(), @@ -145,7 +156,8 @@ public interface Lexicals { /** A regular identifier that satisfies both ISO and PostgreSQL rules, * in a single capturing group named {@code i}. */ - Pattern ISO_AND_PG_REGULAR_IDENTIFIER_CAPTURING = Pattern.compile( + public static final Pattern ISO_AND_PG_REGULAR_IDENTIFIER_CAPTURING = + Pattern.compile( String.format( "(?%1$s)", ISO_AND_PG_REGULAR_IDENTIFIER.pattern()) ); @@ -156,10 +168,11 @@ public interface Lexicals { * explicit {@code uec} for a Unicode identifier (still needing "" to " and * decoding of {@code Unicode escape value}s). */ - Pattern ISO_AND_PG_IDENTIFIER_CAPTURING = Pattern.compile(String.format( + public static final Pattern ISO_AND_PG_IDENTIFIER_CAPTURING = + Pattern.compile(String.format( "%1$s|(?:%2$s)|(?:%3$s)", ISO_AND_PG_REGULAR_IDENTIFIER_CAPTURING.pattern(), - ISO_DELIMITED_IDENTIFIER.pattern(), + ISO_DELIMITED_IDENTIFIER_CAPTURING.pattern(), ISO_UNICODE_IDENTIFIER.pattern() )); @@ -170,7 +183,8 @@ public interface Lexicals { * PL/Java might load, because through 1.4.3 PL/Java used the Java * identifier rules to recognize identifiers in deployment descriptors. */ - Pattern ISO_PG_JAVA_IDENTIFIER = Pattern.compile(String.format( + public static final Pattern ISO_PG_JAVA_IDENTIFIER = + Pattern.compile(String.format( "(?:(?=%1$s)(?=\\p{%5$sStart})%2$s)(?:(?=%3$s)(?=\\p{%5$sPart})%4$s)*+", ISO_REGULAR_IDENTIFIER_START.pattern(), PG_REGULAR_IDENTIFIER_START.pattern(), @@ -178,4 +192,50 @@ public interface Lexicals { PG_REGULAR_IDENTIFIER_PART.pattern(), "javaJavaIdentifier" )); + + /** + * Return an identifier, given a {@code Matcher} that has matched an + * ISO_AND_PG_IDENTIFIER_CAPTURING. Will determine from the matching named + * groups which type of identifier it was, process the matched sequence + * appropriately, and return it. + * @param m A {@code Matcher} known to have matched an identifier. + * @return the recovered identifier string. + */ + public static String identifierFrom(Matcher m) + { + String s = m.group("i"); + if ( null != s ) + return s; + s = m.group("xd"); + if ( null != s ) + return s.replace("\"\"", "\""); + s = m.group("xui"); + if ( null == s ) + return null; // XXX? + s = s.replace("\"\"", "\""); + String uec = m.group("uec"); + if ( null == uec ) + uec = "\\"; + int uecp = uec.codePointAt(0); + Matcher replacer = + Pattern.compile( + String.format(ISO_UNICODE_REPLACER, Pattern.quote(uec))) + .matcher(s); + StringBuffer sb = new StringBuffer(); + while ( replacer.find() ) + { + replacer.appendReplacement(sb, ""); + int cp; + String uev = replacer.group("u4d"); + if ( null == uev ) + uev = replacer.group("u6d"); + if ( null != uev ) + cp = Integer.parseInt(uev, 16); + else + cp = uecp; + // XXX check validity + sb.appendCodePoint(cp); + } + return replacer.appendTail(sb).toString(); + } } diff --git a/pljava-api/src/test/java/LexicalsTest.java b/pljava-api/src/test/java/LexicalsTest.java new file mode 100644 index 00000000..6a56c682 --- /dev/null +++ b/pljava-api/src/test/java/LexicalsTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016 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; + +import java.util.regex.Matcher; + +import junit.framework.TestCase; + +import static + org.postgresql.pljava.sqlgen.Lexicals.ISO_AND_PG_IDENTIFIER_CAPTURING; +import static org.postgresql.pljava.sqlgen.Lexicals.identifierFrom; + +public class LexicalsTest extends TestCase +{ + public LexicalsTest(String name) { super(name); } + + public void testIdentifierFrom() throws Exception + { + Matcher m = ISO_AND_PG_IDENTIFIER_CAPTURING.matcher("anIdentifier"); + assertTrue("i", m.matches()); + assertEquals("anIdentifier", identifierFrom(m)); + + m.reset("\"an\"\"Identifier\"\"\""); + assertTrue("xd", m.matches()); + assertEquals("an\"Identifier\"", identifierFrom(m)); + + m.reset("u&\"an\\0049dent\"\"if\\+000069er\""); + assertTrue("xui2", m.matches()); + assertEquals("anIdent\"ifier", identifierFrom(m)); + + m.reset("u&\"an@@\"\"@0049dent@+000069fier\"\"\" uescape '@'"); + assertTrue("xui3", m.matches()); + assertEquals("an@\"Identifier\"", identifierFrom(m)); + } +} diff --git a/pom.xml b/pom.xml index 35e68912..81e1f69e 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,15 @@ + + + junit + junit + 4.12 + test + + + From c6b2072d44984b8332170fec34d6d7b17452d2b3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Apr 2016 00:13:34 -0400 Subject: [PATCH 0011/1087] Add an Identifier class. A start on encapsulating the strange and wonderful SQL rules for matching quoted and non-quoted identifiers. Note there is still no consultation of a list of SQL keywords, so the rules for regular identifiers can't be called complete yet. --- .../postgresql/pljava/sqlgen/Lexicals.java | 319 +++++++++++++++++- pljava-api/src/test/java/LexicalsTest.java | 59 +++- 2 files changed, 364 insertions(+), 14 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index ca7fcdaf..168b2f73 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -14,22 +14,25 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.processing.Messager; +import javax.tools.Diagnostic.Kind; + /** * A few useful SQL lexical definitions supplied as {@link Pattern} objects. * * The idea is not to go overboard and reimplement an SQL lexer, but to * capture in one place the rules for those bits of SQL snippets that are * likely to be human-supplied in annotations and need to be checked for - * correctness when emitted into deployment descriptors. For starters, that - * means regular (not quoted, not Unicode escaped) identifiers. + * correctness when emitted into deployment descriptors. Identifiers, for a + * start. * * Supplied in the API module so they are available to {@code javac} to * compile and generate DDR when the rest of PL/Java is not necessarily * present. Of course backend code such as {@code SQLDeploymentDescriptor} * can also refer to these. */ -public abstract class Lexicals { - +public abstract class Lexicals +{ /** Allowed as the first character of a regular identifier by ISO. */ public static final Pattern ISO_REGULAR_IDENTIFIER_START = Pattern.compile( @@ -194,21 +197,21 @@ public abstract class Lexicals { )); /** - * Return an identifier, given a {@code Matcher} that has matched an + * Return an Identifier, given a {@code Matcher} that has matched an * ISO_AND_PG_IDENTIFIER_CAPTURING. Will determine from the matching named * groups which type of identifier it was, process the matched sequence * appropriately, and return it. * @param m A {@code Matcher} known to have matched an identifier. - * @return the recovered identifier string. + * @return Identifier made from the recovered string. */ - public static String identifierFrom(Matcher m) + public static Identifier identifierFrom(Matcher m) { String s = m.group("i"); if ( null != s ) - return s; + return Identifier.from(s, false); s = m.group("xd"); if ( null != s ) - return s.replace("\"\"", "\""); + return Identifier.from(s.replace("\"\"", "\""), true); s = m.group("xui"); if ( null == s ) return null; // XXX? @@ -236,6 +239,302 @@ public static String identifierFrom(Matcher m) // XXX check validity sb.appendCodePoint(cp); } - return replacer.appendTail(sb).toString(); + return Identifier.from(replacer.appendTail(sb).toString(), true); + } + + /** + * Class representing a SQL identifier. These have wild and wooly behavior + * depending on whether they were represented in the source in quoted form + * or not. Quoted ones are case-sensitive, + * and {@link #equals(Object) equals} will only recognize exact matches. + * Non-quoted ones match case-insensitively; just to make this interesting, + * ISO SQL has one set of case-folding rules, while PostgreSQL has another. + * Also, a non-quoted identifier can match a quoted one, if the quoted one's + * exact spelling matches the non-quoted one's case-folded form. + *

+ * For even more fun, the PostgreSQL rules depend on the server encoding. + * For any multibyte encoding, only the 26 ASCII uppercase letters + * are folded to lower, leaving all other characters alone. In single-byte + * encodings, more letters can be touched. But this code has to run in a + * javac annotation processor without knowledge of any particular database's + * server encoding. The recommended encoding, UTF-8, is multibyte, so the + * PostgreSQL rule will be taken to be: only the 26 ASCII letters, always. + */ + public static class Identifier + { + protected final String m_nonFolded; + + /** + * Whether this Identifier case-folds. + * @return true if this Identifier was non-quoted in the source, + * false if it was quoted. + */ + public boolean folds() + { + return false; + } + + /** + * This Identifier's original spelling. + * @return The spelling as seen in the source, with no case folding. + */ + public String nonFolded() + { + return m_nonFolded; + } + + /** + * This Identifier as PostgreSQL would case-fold it (or the same as + * nonFolded if this was quoted and does not fold). + * @return The spelling with ASCII letters (only) folded to lowercase, + * if this Identifier folds. + */ + public String pgFolded() + { + return m_nonFolded; + } + + /** + * This Identifier as ISO SQL would case-fold it (or the same as + * nonFolded if this was quoted and does not fold). + * @return The spelling with lowercase and titlecase letters folded to + * (possibly length-changing) uppercase equivalents, + * if this Identifier folds. + */ + public String isoFolded() + { + return m_nonFolded; + } + + /** + * Create an Identifier given its original, non-folded spelling, + * and whether it represents a quoted identifier. + * @param s The exact, internal, non-folded spelling of the identifier + * (unwrapped from any quoting in its external form). + * @param quoted Pass {@code true} if this was parsed from any quoted + * external form, false if non-quoted. + * @return A corresponding Identifier + * @throws IllegalArgumentException if {@code quoted} is {@code false} + * but {@code s} cannot be a non-quoted identifier, or {@code s} is + * empty or longer than the ISO SQL maximum 128 codepoints. + */ + public static Identifier from(String s, boolean quoted) + { + boolean foldable = + ISO_AND_PG_REGULAR_IDENTIFIER.matcher(s).matches(); + if ( ! quoted ) + { + if ( ! foldable ) + throw new IllegalArgumentException(String.format( + "impossible for \"%1$s\" to be a non-quoted identifier", + s)); + return new Folding(s); + } + if ( foldable ) + return new Foldable(s); + return new Identifier(s); + } + + @Override + public String toString() + { + return m_nonFolded; + } + + /** + * For a quoted identifier that could not match any non-quoted one, + * the hash code of its non-folded spelling is good enough. In other + * cases, the code must be derived more carefully. + */ + @Override + public int hashCode() + { + return m_nonFolded.hashCode(); + } + + @Override + public boolean equals(Object other) + { + return equals(other, null); + } + + /** + * For use in an annotation processor, a version of {@code equals} that + * can take a {@link Messager} and use it to emit warnings. It will + * emit a warning whenever it compares two Identifiers that are equal + * by one or the other of PostgreSQL's or ISO SQL's rules but not both. + * @param other Object to compare to + * @param msgr a Messager to use for warnings; if {@code null}, no + * warnings will be generated. + * @return true if two quoted Identifiers match exactly, or two + * non-quoted ones match in either the PostgreSQL or ISO SQL folded + * form, or a quoted one exactly matches either folded form of a + * non-quoted one. + */ + public boolean equals(Object other, Messager msgr) + { + if ( ! (other instanceof Identifier) ) + return false; + Identifier oi = (Identifier)other; + if ( oi.folds() ) + return oi.equals(this); + return m_nonFolded.equals(oi.nonFolded()); + } + + protected Identifier(String nonFolded) + { + m_nonFolded = nonFolded; + int cpc = nonFolded.codePointCount(0, nonFolded.length()); + if ( 0 == cpc || cpc > 128 ) + throw new IllegalArgumentException(String.format( + "identifier empty or longer than 128 codepoints: \"%s\"", + nonFolded)); + } + + /** + * Class representing an Identifier that was quoted, therefore does + * not case-fold, but satisfies {@code ISO_AND_PG_REGULAR_IDENTIFIER} + * and so could conceivably be matched by a non-quoted identifier. + */ + static class Foldable extends Identifier + { + private final int m_hashCode; + + protected Foldable(String nonFolded) + { + this(nonFolded, isoFold(nonFolded)); + } + + protected Foldable(String nonFolded, String isoFolded) + { + super(nonFolded); + m_hashCode = isoFolded.hashCode(); + } + + /** + * For any identifier that case-folds, or even could be matched by + * another identifier that case-folds, the hash code is tricky. + * Hash codes are required to be equal for any instances that are + * equal (but not required to be different for instances that are + * unequal). In this case, the hash codes need to be equal whenever + * the PostgreSQL or ISO SQL folded forms match. + *

+ * This hash code will be derived from the ISO-folded spelling of + * the identifier. As long as the PostgreSQL rules only affect the + * 26 ASCII letters, all of which are also folded (albeit in the + * other direction) by the ISO rules, hash codes will also match for + * identifiers equal under PostgreSQL rules. + */ + @Override + public int hashCode() + { + return m_hashCode; + } + + /** + * The characters that ISO SQL rules will fold: anything that is + * lowercase or titlecase. + */ + private static final Pattern s_isoFolded = + Pattern.compile("[\\p{javaLowerCase}\\p{javaTitleCase}]"); + + /** + * Case-fold a string by the ISO SQL rules, where any lowercase or + * titlecase character gets replaced by its uppercase form (the + * generalized, possibly length-changing one, requiring + * {@link String#toUpperCase} and not + * {@link Character#toUpperCase}. + * @param s The non-folded value. + * @return The folded value. + */ + protected static String isoFold(String s) + { + Matcher m = s_isoFolded.matcher(s); + StringBuffer sb = new StringBuffer(); + while ( m.find() ) + m.appendReplacement(sb, m.group().toUpperCase()); + return m.appendTail(sb).toString(); + } + } + + /** + * Class representing an Identifier that was not quoted, and therefore + * has case-folded forms. + */ + static class Folding extends Foldable + { + private final String m_pgFolded; + private final String m_isoFolded; + + protected Folding(String nonFolded) + { + this(nonFolded, isoFold(nonFolded)); + } + + protected Folding(String nonFolded, String isoFolded) + { + super(nonFolded, isoFolded); + m_pgFolded = pgFold(nonFolded); + m_isoFolded = isoFolded; + } + + @Override + public String pgFolded() + { + return m_pgFolded; + } + + @Override + public String isoFolded() + { + return m_isoFolded; + } + + @Override + public boolean folds() + { + return true; + } + + @Override + public boolean equals(Object other, Messager msgr) + { + if ( ! (other instanceof Identifier) ) + return false; + Identifier oi = (Identifier)other; + boolean eqPG = m_pgFolded.equals(oi.pgFolded()); + boolean eqISO = m_isoFolded.equals(oi.isoFolded()); + if ( eqPG != eqISO && oi.folds() && null != msgr ) + { + msgr.printMessage(Kind.WARNING, String.format( + "identifiers \"%1$s\" and \"%2$s\" are equal by ISO " + + "or PostgreSQL case-insensitivity rules but not both", + m_nonFolded, oi.nonFolded())); + } + return eqPG || eqISO; + } + + /** + * The characters that PostgreSQL rules will fold: only the 26 + * uppercase ASCII letters. + */ + private static final Pattern s_pgFolded = Pattern.compile("[A-Z]"); + + /** + * Case-fold a string by the PostgreSQL rules (assuming a + * multibyte server encoding, where only the 26 uppercase ASCII + * letters fold to lowercase). + * @param s The non-folded value. + * @return The folded value. + */ + private String pgFold(String s) + { + Matcher m = s_pgFolded.matcher(s); + StringBuffer sb = new StringBuffer(); + while ( m.find() ) + m.appendReplacement(sb, m.group().toLowerCase()); + return m.appendTail(sb).toString(); + } + } } } diff --git a/pljava-api/src/test/java/LexicalsTest.java b/pljava-api/src/test/java/LexicalsTest.java index 6a56c682..5a688ed6 100644 --- a/pljava-api/src/test/java/LexicalsTest.java +++ b/pljava-api/src/test/java/LexicalsTest.java @@ -15,10 +15,15 @@ import junit.framework.TestCase; +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.*; + import static org.postgresql.pljava.sqlgen.Lexicals.ISO_AND_PG_IDENTIFIER_CAPTURING; import static org.postgresql.pljava.sqlgen.Lexicals.identifierFrom; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; + public class LexicalsTest extends TestCase { public LexicalsTest(String name) { super(name); } @@ -27,18 +32,64 @@ public void testIdentifierFrom() throws Exception { Matcher m = ISO_AND_PG_IDENTIFIER_CAPTURING.matcher("anIdentifier"); assertTrue("i", m.matches()); - assertEquals("anIdentifier", identifierFrom(m)); + assertEquals("anIdentifier", identifierFrom(m).nonFolded()); m.reset("\"an\"\"Identifier\"\"\""); assertTrue("xd", m.matches()); - assertEquals("an\"Identifier\"", identifierFrom(m)); + assertEquals("an\"Identifier\"", identifierFrom(m).nonFolded()); m.reset("u&\"an\\0049dent\"\"if\\+000069er\""); assertTrue("xui2", m.matches()); - assertEquals("anIdent\"ifier", identifierFrom(m)); + assertEquals("anIdent\"ifier", identifierFrom(m).nonFolded()); m.reset("u&\"an@@\"\"@0049dent@+000069fier\"\"\" uescape '@'"); assertTrue("xui3", m.matches()); - assertEquals("an@\"Identifier\"", identifierFrom(m)); + assertEquals("an@\"Identifier\"", identifierFrom(m).nonFolded()); + } + + public void testIdentifierEquivalence() throws Exception + { + Identifier baß = Identifier.from("baß", false); + Identifier Baß = Identifier.from("Baß", false); + Identifier bass = Identifier.from("bass", false); + Identifier BASS = Identifier.from("BASS", false); + + Identifier qbaß = Identifier.from("baß", true); + Identifier qBaß = Identifier.from("Baß", true); + Identifier qbass = Identifier.from("bass", true); + Identifier qBASS = Identifier.from("BASS", true); + + Identifier sab = Identifier.from("sopran alt baß", true); + Identifier SAB = Identifier.from("Sopran Alt Baß", true); + + /* DESERET SMALL LETTER OW */ + Identifier ow = Identifier.from("\uD801\uDC35", false); + /* DESERET CAPITAL LETTER OW */ + Identifier OW = Identifier.from("\uD801\uDC0D", false); + + assertEquals("hash1", baß.hashCode(), Baß.hashCode()); + assertEquals("hash2", baß.hashCode(), bass.hashCode()); + assertEquals("hash3", baß.hashCode(), BASS.hashCode()); + + assertEquals("hash4", baß.hashCode(), qbaß.hashCode()); + assertEquals("hash5", baß.hashCode(), qBaß.hashCode()); + assertEquals("hash6", baß.hashCode(), qbass.hashCode()); + assertEquals("hash7", baß.hashCode(), qBASS.hashCode()); + + assertEquals("hash8", ow.hashCode(), OW.hashCode()); + + assertEquals("eq1", baß, Baß); + assertEquals("eq2", baß, bass); + assertEquals("eq3", baß, BASS); + + assertEquals("eq4", Baß, qbaß); + assertEquals("eq5", Baß, qBASS); + + assertEquals("eq6", ow, OW); + + assertThat("ne1", Baß, not(equalTo(qBaß))); + assertThat("ne2", Baß, not(equalTo(qbass))); + + assertThat("ne3", sab, not(equalTo(SAB))); } } From b5915630819b2e7ebbb6778b01bc4e971e306cbd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Apr 2016 12:16:07 -0400 Subject: [PATCH 0012/1087] Add Backend.getListConfigOption. There is a limitation, because PostgreSQL uses *identifier* quoting (that is, double quotes) for list GUC members, and getListConfigOption really does use the Lexicals identifier patterns to match them, and ISO SQL restricts an identifier to no more than 128 code points, so that is a current per-member length limit. --- .../postgresql/pljava/internal/Backend.java | 32 +++++++++ .../pljava/management/Commands.java | 4 +- .../pljava/management/DDRExecutor.java | 40 ++--------- .../management/SQLDeploymentDescriptor.java | 68 ++++++++++--------- 4 files changed, 77 insertions(+), 67 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index 39fc6e70..cf3365a9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -20,12 +20,22 @@ import java.net.URLConnection; import java.security.Permission; import java.sql.SQLException; +import java.sql.SQLDataException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.PropertyPermission; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.postgresql.pljava.management.Commands; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; +import static org.postgresql.pljava.sqlgen.Lexicals.identifierFrom; +import static + org.postgresql.pljava.sqlgen.Lexicals.ISO_AND_PG_IDENTIFIER_CAPTURING; /** * Provides access to some useful routines in the PostgreSQL server. @@ -40,6 +50,9 @@ public class Backend private static Session s_session; + private static final Pattern s_gucList = Pattern.compile(String.format( + "\\G(?:%1$s)(?,\\s*+)?+", ISO_AND_PG_IDENTIFIER_CAPTURING)); + public static synchronized Session getSession() { if(s_session == null) @@ -61,6 +74,25 @@ public static String getConfigOption(String key) } } + public static List getListConfigOption(String key) + throws SQLException + { + final Matcher m = s_gucList.matcher(getConfigOption(key)); + ArrayList al = new ArrayList<>(); + while ( m.find() ) + { + al.add(identifierFrom(m)); + if ( null != m.group("more") ) + continue; + if ( ! m.hitEnd() ) + throw new SQLDataException(String.format( + "configuration option \"%1$s\" improper list syntax", + key), "22P02"); + } + al.trimToSize(); + return Collections.unmodifiableList(al); + } + /** * Returns the size of the statement cache. * @return the size of the statement cache. diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 7a6564f2..660aab2e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -977,7 +977,9 @@ private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) } catch(ParseException e) { - throw new SQLException(e.getMessage() + " at " + e.getErrorOffset()); + throw new SQLSyntaxErrorException(String.format( + "%1$s at %2$s", e.getMessage(), e.getErrorOffset()), + "42601", e); } finally { diff --git a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java index 75c1f1a0..62acf1cd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java @@ -14,17 +14,12 @@ import java.sql.Connection; import java.sql.SQLException; -import java.util.ArrayList; - -import java.util.regex.Pattern; -import java.util.regex.Matcher; - import org.postgresql.pljava.Session; import org.postgresql.pljava.SessionManager; import org.postgresql.pljava.internal.Backend; -import static org.postgresql.pljava.sqlgen.Lexicals.ISO_PG_JAVA_IDENTIFIER; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; /** * Abstract class for executing one deployment descriptor {@code } @@ -74,14 +69,6 @@ protected DDRExecutor() { } private static final DDRExecutor NOOP = new Noop(); - /* - * Capture group 1 is an identifier. Presence/absence of group 2 (comma- - * whitespace) indicates whether to parse more. - */ - private static final Pattern settingsRx = Pattern.compile(String.format( - "\\G(%1$s)(,\\s*+)?+", ISO_PG_JAVA_IDENTIFIER - )); - /** * Execute the command {@code sql} using the connection {@code conn}, * according to whatever meaning of "execute" the target {@code DDRExecutor} @@ -105,37 +92,22 @@ public abstract void execute( String sql, Connection conn) * an unadorned {@code } instead of an * {@code }. */ - public static DDRExecutor forImplementor( String name) + public static DDRExecutor forImplementor( Identifier name) throws SQLException { if ( null == name ) return PLAIN; - String[] imps = implementors(); + Iterable imps = + Backend.getListConfigOption( "pljava.implementors"); - for ( String i : imps ) - if ( name.equalsIgnoreCase( i) ) + for ( Identifier i : imps ) + if ( name.equals( i) ) return PLAIN; return NOOP; } - private static String[] implementors() throws SQLException - { - String settingString = Backend.getConfigOption( "pljava.implementors"); - ArrayList al = new ArrayList<>(); - Matcher m = settingsRx.matcher( settingString); - while ( m.find() ) - { - al.add( m.group( 1)); - if ( -1 != m.start( 2) ) - continue; - if ( m.hitEnd() ) - return al.toArray( new String [ al.size() ]); - } - throw new SQLException("Failed to parse current pljava.implementors"); - } - static class Noop extends DDRExecutor { public void execute( String sql, Connection conn) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java index 69f86f66..85c1c7ad 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java @@ -17,6 +17,13 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; +import static org.postgresql.pljava.sqlgen.Lexicals.identifierFrom; +import static + org.postgresql.pljava.sqlgen.Lexicals.ISO_AND_PG_IDENTIFIER_CAPTURING; /** * This class deals with parsing and executing the deployment descriptor as @@ -102,6 +109,12 @@ public class SQLDeploymentDescriptor private int m_position = 0; + private static final Pattern s_beginImpl = Pattern.compile(String.format( + "^(?i:BEGIN)\\s++(?:%1$s)\\s*+", ISO_AND_PG_IDENTIFIER_CAPTURING)); + + private static final Pattern s_endImpl = Pattern.compile(String.format( + "(?descImage into a series of * {@code Command} objects each having an SQL command and, if present, an @@ -168,7 +181,7 @@ private void readDescriptor() { m_logger.entering("org.postgresql.pljava.management.SQLDeploymentDescriptor", "readDescriptor"); if(!"SQLACTIONS".equals(this.readIdentifier())) - throw this.parseError("Excpected keyword 'SQLActions'"); + throw this.parseError("Expected keyword 'SQLActions'"); this.readToken('['); this.readToken(']'); @@ -217,38 +230,29 @@ else if("REMOVE".equals(actionType)) // ::= // BEGIN ... END // - // If it is, and if the implementor name corresponds to the one - // defined for this deployment, then extract the SQL token stream. + // If it is, keep track of the with the cmd. // - String implementorName; - int top = cmd.length(); - if(top >= 15 - && "BEGIN ".equalsIgnoreCase(cmd.substring(0, 6)) - && Character.isJavaIdentifierStart(cmd.charAt(6))) + Identifier implementorName = null; + if(cmd.length() >= 15) { - int pos; - for(pos = 7; pos < top; ++pos) - if(!Character.isJavaIdentifierPart(cmd.charAt(pos))) - break; - - if(cmd.charAt(pos) != ' ') - throw this.parseError( - "Expected whitespace after "); - - implementorName = cmd.substring(6, pos); - int iLen = implementorName.length(); - - int endNamePos = top - iLen; - int endPos = endNamePos - 4; - if(!implementorName.equalsIgnoreCase(cmd.substring(endNamePos)) - || !"END ".equalsIgnoreCase(cmd.substring(endPos, endNamePos))) - throw this.parseError( - "Implementor block must end with END "); - - cmd = cmd.substring(pos+1, endPos); + Matcher m = s_beginImpl.matcher(cmd); + if ( m.find() ) + { + Identifier begIdent = identifierFrom(m); + int pos = m.end(); + m = s_endImpl.matcher(cmd); + if ( ! m.find(pos) ) + throw this.parseError( + "BEGIN without matching END"); + Identifier endIdent = identifierFrom(m); + if ( ! endIdent.equals(begIdent) ) + throw this.parseError(String.format( + "BEGIN \"%1$s\" and END \"%2$s\" do not match", + begIdent, endIdent)); + implementorName = begIdent; + cmd = cmd.substring(pos, m.start()); + } } - else - implementorName = null; commands.add(new Command(cmd.trim(), implementorName)); @@ -515,7 +519,7 @@ class Command /** The sql to execute (if this command is not suppressed). Never null. */ final String sql; - private final String tag; + private final Identifier tag; /** * Execute this {@code Command} using a {@code DDRExecutor} chosen @@ -527,7 +531,7 @@ void execute( Connection conn) throws SQLException ddre.execute( sql, conn); } - Command(String sql, String tag) + Command(String sql, Identifier tag) { this.sql = sql.trim(); this.tag = tag; From 6123936d05202dcf4b4296d77b57464749d672a5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Apr 2016 17:35:13 -0400 Subject: [PATCH 0013/1087] Adopt JDBC4+ ServiceLoader conventions. Add a META-INF/services/java.sql.Driver file (probably unnecessary as the driver will always be loaded explicitly through SQLUtils before anyone else needs it, but for completeness...), and get rid of the Class.forName("...SPIDriver") that was previously in, of all places, Oid. --- .../java/org/postgresql/pljava/internal/Oid.java | 14 -------------- .../org/postgresql/pljava/jdbc/SPIConnection.java | 5 +++-- .../resources/META-INF/services/java.sql.Driver | 1 + 3 files changed, 4 insertions(+), 16 deletions(-) create mode 100644 pljava/src/main/resources/META-INF/services/java.sql.Driver diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java index f3d0df32..006c398b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java @@ -21,20 +21,6 @@ public class Oid extends Number private static final HashMap s_class2typeId = new HashMap(); private static final HashMap s_typeId2class = new HashMap(); - static - { - try - { - // Ensure that the SPI JDBC driver is loaded and registered - // with the java.sql.DriverManager. - // - Class.forName("org.postgresql.pljava.jdbc.SPIDriver"); - } - catch(ClassNotFoundException e) - { - throw new ExceptionInInitializerError(e); - } - } /** * Finds the PostgreSQL well known Oid for the given class. diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 7d56c1a1..4e2cb269 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -114,8 +114,9 @@ private static final void addType(Class clazz, int sqlType) } /** - * Returns a default connection instance. It is the callers responsability - * to close this instance. + * Returns a default connection instance. It is normally the caller's + * responsibility to close this instance, but as {@code close} is a no-op + * for this connection, that isn't critical. */ public static Connection getDefault() throws SQLException diff --git a/pljava/src/main/resources/META-INF/services/java.sql.Driver b/pljava/src/main/resources/META-INF/services/java.sql.Driver new file mode 100644 index 00000000..0141474d --- /dev/null +++ b/pljava/src/main/resources/META-INF/services/java.sql.Driver @@ -0,0 +1 @@ +org.postgresql.pljava.jdbc.SPIDriver From f087234608bbb2dd2f7de8bd0ca958f36c24072a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 25 Oct 2016 00:06:57 -0400 Subject: [PATCH 0014/1087] Advance version to 1.5.1-SNAPSHOT. Also add to pljava-packaging/build.xml to create the expected extension control file marking 1.5.0 as a version that can be updated from. --- pljava-ant/pom.xml | 2 +- pljava-api/pom.xml | 2 +- pljava-deploy/pom.xml | 2 +- pljava-examples/pom.xml | 2 +- pljava-packaging/build.xml | 4 ++++ pljava-packaging/pom.xml | 2 +- pljava-so/pom.xml | 2 +- pljava/pom.xml | 2 +- pom.xml | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index 8875ec85..ac3014bd 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pljava-ant PL/Java Ant tasks diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 05898acf..2d639b84 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pljava-api PL/Java API diff --git a/pljava-deploy/pom.xml b/pljava-deploy/pom.xml index a87705b6..9bdcc801 100644 --- a/pljava-deploy/pom.xml +++ b/pljava-deploy/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pljava-deploy PL/Java Deploy diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index e8267cfa..19bd0b43 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pljava-examples PL/Java examples diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 452c73bf..759693fb 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pljava-packaging PL/Java packaging diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 0d89888d..1bdd2deb 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pljava-so PL/Java backend native code diff --git a/pljava/pom.xml b/pljava/pom.xml index 8e778d74..0d78c08e 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pljava PL/Java backend Java code diff --git a/pom.xml b/pom.xml index 4ed0347d..9c74a07d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.postgresql pljava.app - 1.5.0-SNAPSHOT + 1.5.1-SNAPSHOT pom PostgreSQL PL/Java https://tada.github.io/pljava/ From 2493fc532a3b7e832b2e837f317998d8111483bc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Oct 2016 04:37:00 -0400 Subject: [PATCH 0015/1087] Use heap_form_tuple, not heap_formtuple. Both have existed back to PG 8.2, so no need for version conditionals. The preferred one uses a boolean nulls array instead of a char array of ' ' or 'n', and the deprecated one goes away in 9.6. memset(nulls, true, count * sizeof(bool)) is a bit off if bool is ever wider than a byte (values will be nonzero but not exactly equal to true). But it's an idiom Tom Lane has used in multiple places: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blobdiff;f=src/backend/utils/cache/catcache.c;h=d5f237bc;hp=83a12776f;hb=902d1cb35;hpb=492059d https://git.postgresql.org/gitweb/?p=postgresql.git;a=blobdiff;f=src/backend/utils/adt/arrayfuncs.c;h=3818b181;hp=5304d47fa;hb=cecb6075;hpb=c859308a which makes it good enough for me. --- pljava-so/src/main/c/type/TupleDesc.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index 3c5cdf6c..a7172eb0 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -218,11 +218,11 @@ Java_org_postgresql_pljava_internal_TupleDesc__1formTuple(JNIEnv* env, jclass cl TupleDesc self = (TupleDesc)p2l.ptrVal; int count = self->natts; Datum* values = (Datum*)palloc(count * sizeof(Datum)); - char* nulls = palloc(count); + bool* nulls = palloc(count * sizeof(bool)); jobject typeMap = Invocation_getTypeMap(); memset(values, 0, count * sizeof(Datum)); - memset(nulls, 'n', count); /* all values null initially */ + memset(nulls, true, count * sizeof(bool));/*all values null initially*/ for(idx = 0; idx < count; ++idx) { @@ -231,12 +231,12 @@ Java_org_postgresql_pljava_internal_TupleDesc__1formTuple(JNIEnv* env, jclass cl { Type type = Type_fromOid(SPI_gettypeid(self, idx + 1), typeMap); values[idx] = Type_coerceObject(type, value); - nulls[idx] = ' '; + nulls[idx] = false; } } curr = MemoryContextSwitchTo(JavaMemoryContext); - tuple = heap_formtuple(self, values, nulls); + tuple = heap_form_tuple(self, values, nulls); result = Tuple_internalCreate(tuple, false); MemoryContextSwitchTo(curr); pfree(values); From c32911532cdb9243e15c61d36fc81da95d264fea Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Oct 2016 20:02:01 -0400 Subject: [PATCH 0016/1087] Handle the PG 9.6 widening of SPI_processed. Includes a start on some Java 8 JDBC additions: getLargeUpdateCount() and executeLargeBatch(). --- pljava-so/src/main/c/SPI.c | 8 ++--- pljava-so/src/main/c/type/Portal.c | 29 ++++++++--------- .../postgresql/pljava/internal/Portal.java | 27 +++++++++++----- .../org/postgresql/pljava/internal/SPI.java | 10 ++++-- .../pljava/jdbc/SPIPreparedStatement.java | 6 ++-- .../postgresql/pljava/jdbc/SPIResultSet.java | 8 ++--- .../postgresql/pljava/jdbc/SPIStatement.java | 31 +++++++++++++++++-- 7 files changed, 81 insertions(+), 38 deletions(-) diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index 21a25372..b247cee0 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -32,7 +32,7 @@ void SPI_initialize(void) }, { "_getProcessed", - "()I", + "()J", Java_org_postgresql_pljava_internal_SPI__1getProcessed }, { @@ -97,12 +97,12 @@ Java_org_postgresql_pljava_internal_SPI__1exec(JNIEnv* env, jclass cls, jlong th /* * Class: org_postgresql_pljava_internal_SPI * Method: _getProcessed - * Signature: ()I + * Signature: ()J */ -JNIEXPORT jint JNICALL +JNIEXPORT jlong JNICALL Java_org_postgresql_pljava_internal_SPI__1getProcessed(JNIEnv* env, jclass cls) { - return (jint)SPI_processed; + return (jlong)SPI_processed; } /* diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index cdf8a430..b7a28e92 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -116,7 +116,7 @@ void Portal_initialize(void) }, { "_fetch", - "(JJZI)I", + "(JJZJ)J", Java_org_postgresql_pljava_internal_Portal__1fetch }, { @@ -141,7 +141,7 @@ void Portal_initialize(void) }, { "_move", - "(JJZI)I", + "(JJZJ)J", Java_org_postgresql_pljava_internal_Portal__1move }, { 0, 0, 0 } @@ -179,12 +179,12 @@ Java_org_postgresql_pljava_internal_Portal__1getPortalPos(JNIEnv* env, jclass cl /* * Class: org_postgresql_pljava_internal_Portal * Method: _fetch - * Signature: (JJZI)I + * Signature: (JJZJ)J */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jboolean forward, jint count) +JNIEXPORT jlong JNICALL +Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jboolean forward, jlong count) { - jint result = 0; + jlong result = 0; if(_this != 0) { BEGIN_NATIVE @@ -195,8 +195,9 @@ Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jl p2l.longVal = _this; PG_TRY(); { - SPI_cursor_fetch((Portal)p2l.ptrVal, forward == JNI_TRUE, (int)count); - result = (jint)SPI_processed; + SPI_cursor_fetch((Portal)p2l.ptrVal, forward == JNI_TRUE, + (long)count); + result = (jlong)SPI_processed; } PG_CATCH(); { @@ -343,12 +344,12 @@ Java_org_postgresql_pljava_internal_Portal__1isPosOverflow(JNIEnv* env, jclass c /* * Class: org_postgresql_pljava_internal_Portal * Method: _move - * Signature: (JJZI)I + * Signature: (JJZJ)J */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_Portal__1move(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jboolean forward, jint count) +JNIEXPORT jlong JNICALL +Java_org_postgresql_pljava_internal_Portal__1move(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jboolean forward, jlong count) { - jint result = 0; + jlong result = 0; if(_this != 0) { BEGIN_NATIVE @@ -359,8 +360,8 @@ Java_org_postgresql_pljava_internal_Portal__1move(JNIEnv* env, jclass clazz, jlo p2l.longVal = _this; PG_TRY(); { - SPI_cursor_move((Portal)p2l.ptrVal, forward == JNI_TRUE, (int)count); - result = (jint)SPI_processed; + SPI_cursor_move((Portal)p2l.ptrVal, forward == JNI_TRUE, (long)count); + result = (jlong)SPI_processed; } PG_CATCH(); { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java index eefeee0c..031a5087 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -83,12 +83,19 @@ public TupleDesc getTupleDesc() * @return The actual number of fetched rows. * @throws SQLException if the handle to the native structur is stale. */ - public int fetch(boolean forward, int count) + public long fetch(boolean forward, long count) throws SQLException { synchronized(Backend.THREADLOCK) { - return _fetch(m_pointer, System.identityHashCode(Thread.currentThread()), forward, count); + long fetched = + _fetch(m_pointer, + System.identityHashCode(Thread.currentThread()), + forward, count); + if ( fetched < 0 ) + throw new ArithmeticException( + "fetched too many rows to report in a Java signed long"); + return fetched; } } @@ -145,15 +152,21 @@ public boolean isValid() * Performs an SPI_cursor_move. * @param forward Set to true for forward, false for backward. * @param count Maximum number of rows to fetch. - * @return The value of the global variable SPI_result. + * @return The actual number of rows moved. * @throws SQLException if the handle to the native structur is stale. */ - public int move(boolean forward, int count) + public long move(boolean forward, long count) throws SQLException { synchronized(Backend.THREADLOCK) { - return _move(m_pointer, System.identityHashCode(Thread.currentThread()), forward, count); + long moved = _move(m_pointer, + System.identityHashCode(Thread.currentThread()), + forward, count); + if ( moved < 0 ) + throw new ArithmeticException( + "moved too many rows to report in a Java signed long"); + return moved; } } @@ -166,7 +179,7 @@ private static native int _getPortalPos(long pointer) private static native TupleDesc _getTupleDesc(long pointer) throws SQLException; - private static native int _fetch(long pointer, long threadId, boolean forward, int count) + private static native long _fetch(long pointer, long threadId, boolean forward, long count) throws SQLException; private static native void _close(long pointer); @@ -180,6 +193,6 @@ private static native boolean _isAtStart(long pointer) private static native boolean _isPosOverflow(long pointer) throws SQLException; - private static native int _move(long pointer, long threadId, boolean forward, int count) + private static native long _move(long pointer, long threadId, boolean forward, long count) throws SQLException; } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java index c33370b7..29fa55e2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -64,11 +64,15 @@ public static void freeTupTable() /** * Returns the value of the global variable SPI_processed. */ - public static int getProcessed() + public static long getProcessed() { synchronized(Backend.THREADLOCK) { - return _getProcessed(); + long count = _getProcessed(); + if ( count < 0 ) + throw new ArithmeticException( + "too many rows processed to count in a Java signed long"); + return count; } } @@ -172,7 +176,7 @@ public static String getResultText(int resultCode) } private native static int _exec(long threadId, String command, int rowCount); - private native static int _getProcessed(); + private native static long _getProcessed(); private native static int _getResult(); private native static void _freeTupTable(); private native static TupleTable _getTupTable(TupleDesc known); diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index f61c04aa..874140fa 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -389,10 +389,10 @@ public ParameterMetaData getParameterMetaData() return new SPIParameterMetaData(this.getSqlTypes()); } - protected int executeBatchEntry(Object batchEntry) + protected long executeBatchEntry(Object batchEntry) throws SQLException { - int ret = SUCCESS_NO_INFO; + long ret = SUCCESS_NO_INFO; Object batchParams[] = (Object[])batchEntry; Object batchValues = batchParams[0]; Object batchSqlTypes = batchParams[1]; @@ -422,7 +422,7 @@ protected int executeBatchEntry(Object batchEntry) this.getResultSet().close(); else { - int updCount = this.getUpdateCount(); + long updCount = this.getUpdateCount(); if(updCount >= 0) ret = updCount; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java index 75441f79..5d3b6002 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java @@ -29,7 +29,7 @@ public class SPIResultSet extends ResultSetBase private final SPIStatement m_statement; private final Portal m_portal; private final TupleDesc m_tupleDesc; - private final int m_maxRows; + private final long m_maxRows; private Tuple m_currentRow; private Tuple m_nextRow; @@ -37,7 +37,7 @@ public class SPIResultSet extends ResultSetBase private TupleTable m_table; private int m_tableRow; - SPIResultSet(SPIStatement statement, Portal portal, int maxRows) + SPIResultSet(SPIStatement statement, Portal portal, long maxRows) throws SQLException { super(statement.getFetchSize()); @@ -119,7 +119,7 @@ protected final TupleTable getTupleTable() if(portal.isAtEnd()) return null; - int mx; + long mx; int fetchSize = this.getFetchSize(); if(m_maxRows > 0) { @@ -134,7 +134,7 @@ protected final TupleTable getTupleTable() try { - int result = portal.fetch(true, mx); + long result = portal.fetch(true, mx); if(result > 0) m_table = SPI.getTupTable(m_tupleDesc); m_tableRow = -1; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java index 7ea241a9..3c6086fc 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java @@ -35,7 +35,7 @@ public class SPIStatement implements Statement private int m_fetchSize = 1000; private int m_maxRows = 0; private ResultSet m_resultSet = null; - private int m_updateCount = 0; + private long m_updateCount = 0; private ArrayList m_batch = null; private boolean m_closed = false; @@ -179,7 +179,23 @@ public int[] executeBatch() int numBatches = (m_batch == null) ? 0 : m_batch.size(); int[] result = new int[numBatches]; for(int idx = 0; idx < numBatches; ++idx) + { + long count = this.executeBatchEntry(m_batch.get(idx)); + result[idx] = (count > Integer.MAX_VALUE) + ? SUCCESS_NO_INFO : (int)count; + } + return result; + } + + public long[] executeLargeBatch() + throws SQLException + { + int numBatches = (m_batch == null) ? 0 : m_batch.size(); + long[] result = new long[numBatches]; + for(int idx = 0; idx < numBatches; ++idx) + { result[idx] = this.executeBatchEntry(m_batch.get(idx)); + } return result; } @@ -313,6 +329,15 @@ public int getResultSetType() public int getUpdateCount() throws SQLException + { + if ( m_updateCount > Integer.MAX_VALUE ) + throw new ArithmeticException( + "too many rows updated to report in a Java signed int"); + return (int)m_updateCount; + } + + public long getLargeUpdateCount() + throws SQLException { return m_updateCount; } @@ -384,10 +409,10 @@ protected void internalAddBatch(Object batch) m_batch.add(batch); } - protected int executeBatchEntry(Object batchEntry) + protected long executeBatchEntry(Object batchEntry) throws SQLException { - int ret = SUCCESS_NO_INFO; + long ret = SUCCESS_NO_INFO; if(this.execute(m_connection.nativeSQL((String)batchEntry))) this.getResultSet().close(); else if(m_updateCount >= 0) From 4024bac74507ebb08b2825b24c9923a7b2b1480e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Oct 2016 20:22:17 -0400 Subject: [PATCH 0017/1087] Handle widening of portalPos and demise of posOverflow. No need for version conditionals around isPosOverflow: it isn't in an API class, and nothing in this code base uses it, so out it goes. This eliminates the other compile-time error that was blocking compilation for PG 9.6, but it's not the last thing to have had its width changed. --- pljava-so/src/main/c/type/Portal.c | 33 +++---------------- .../postgresql/pljava/internal/Portal.java | 27 +++++---------- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index b7a28e92..bbac1ad2 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -106,7 +106,7 @@ void Portal_initialize(void) }, { "_getPortalPos", - "(J)I", + "(J)J", Java_org_postgresql_pljava_internal_Portal__1getPortalPos }, { @@ -135,11 +135,6 @@ void Portal_initialize(void) Java_org_postgresql_pljava_internal_Portal__1isAtStart }, { - "_isPosOverflow", - "(J)Z", - Java_org_postgresql_pljava_internal_Portal__1isPosOverflow - }, - { "_move", "(JJZJ)J", Java_org_postgresql_pljava_internal_Portal__1move @@ -161,17 +156,17 @@ void Portal_initialize(void) /* * Class: org_postgresql_pljava_internal_Portal * Method: _getPortalPos - * Signature: (J)I + * Signature: (J)J */ -JNIEXPORT jint JNICALL +JNIEXPORT jlong JNICALL Java_org_postgresql_pljava_internal_Portal__1getPortalPos(JNIEnv* env, jclass clazz, jlong _this) { - jint result = 0; + jlong result = 0; if(_this != 0) { Ptr2Long p2l; p2l.longVal = _this; - result = (jint)((Portal)p2l.ptrVal)->portalPos; + result = (jlong)((Portal)p2l.ptrVal)->portalPos; } return result; } @@ -323,24 +318,6 @@ Java_org_postgresql_pljava_internal_Portal__1isAtEnd(JNIEnv* env, jclass clazz, return result; } -/* - * Class: org_postgresql_pljava_internal_Portal - * Method: _isPosOverflow - * Signature: (J)Z - */ -JNIEXPORT jboolean JNICALL -Java_org_postgresql_pljava_internal_Portal__1isPosOverflow(JNIEnv* env, jclass clazz, jlong _this) -{ - jboolean result = JNI_FALSE; - if(_this != 0) - { - Ptr2Long p2l; - p2l.longVal = _this; - result = (jboolean)((Portal)p2l.ptrVal)->posOverflow; - } - return result; -} - /* * Class: org_postgresql_pljava_internal_Portal * Method: _move diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java index 031a5087..bb7cacdc 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -53,12 +53,17 @@ public String getName() * Returns the value of the portalPos attribute. * @throws SQLException if the handle to the native structur is stale. */ - public int getPortalPos() + public long getPortalPos() throws SQLException { synchronized(Backend.THREADLOCK) { - return _getPortalPos(m_pointer); + long pos = _getPortalPos(m_pointer); + if ( pos < 0 ) + throw new ArithmeticException( + "portal position too large to report " + + "in a Java signed long"); + return pos; } } @@ -125,19 +130,6 @@ public boolean isAtStart() } } - /** - * Returns the value of the posOverflow attribute. - * @throws SQLException if the handle to the native structur is stale. - */ - public boolean isPosOverflow() - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _isPosOverflow(m_pointer); - } - } - /** * Checks if the portal is still active. I can be closed either explicitly * using the {@link #close()} mehtod or implicitly due to a pop of invocation @@ -173,7 +165,7 @@ public long move(boolean forward, long count) private static native String _getName(long pointer) throws SQLException; - private static native int _getPortalPos(long pointer) + private static native long _getPortalPos(long pointer) throws SQLException; private static native TupleDesc _getTupleDesc(long pointer) @@ -189,9 +181,6 @@ private static native boolean _isAtEnd(long pointer) private static native boolean _isAtStart(long pointer) throws SQLException; - - private static native boolean _isPosOverflow(long pointer) - throws SQLException; private static native long _move(long pointer, long threadId, boolean forward, long count) throws SQLException; From d3113da8338b131fe06ca142bbe2becf0de67445 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Oct 2016 22:11:22 -0400 Subject: [PATCH 0018/1087] Handle the widening of FuncCallContext's call_cntr. This one is a bit stickier, because the value is passed to Java code that implements ResultSetProvider, an interface in pljava-api. The signature for assignRowValues can't just be changed. Instead, for now, a ResultSetProvider is limited to returning INT_MAX rows, but the limit is checked to ensure a predictable failure. For the future, in the Java 8 world, the ResultSetProvider interface could grow a default method largeAssignRowValues that takes a long row number; the Java 8 default implementation would just signal the same error, and classes implementing the old interface won't break. Alas, PL/Java isn't committed to supporting only Java 8 yet. In fact, the same thing could be done on the quiet: PL/Java could check whether the class implementing ResultSetProvider happens to have a largeAssignRowValues method with the right signature, and use it if present. It would simply have to be covered in documentation, and not formally declared in the interface until the Java support bar moves to Java 8. All of that is future work - this change merely checks and enforces an INT_MAX limit. --- pljava-so/src/main/c/type/Composite.c | 9 +++++++-- pljava-so/src/main/c/type/Type.c | 7 ++++--- pljava-so/src/main/include/pljava/pljava.h | 10 ++++++++++ pljava-so/src/main/include/pljava/type/Type.h | 2 +- pljava-so/src/main/include/pljava/type/Type_priv.h | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/pljava-so/src/main/c/type/Composite.c b/pljava-so/src/main/c/type/Composite.c index cdf22f88..06ce8700 100644 --- a/pljava-so/src/main/c/type/Composite.c +++ b/pljava-so/src/main/c/type/Composite.c @@ -133,15 +133,20 @@ static jobject _Composite_getSRFCollector(Type self, PG_FUNCTION_ARGS) return tmp2; } -static bool _Composite_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jint callCounter) +static bool _Composite_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jlong callCounter) { /* Obtain next row using the RowCollector as a parameter to the * ResultSetProvider.assignRowValues method. */ + if ( callCounter > PG_INT32_MAX ) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("the ResultSetProvider cannot return more than " + "INT32_MAX rows"))); return (JNI_callBooleanMethod(rowProducer, s_ResultSetProvider_assignRowValues, rowCollector, - callCounter) == JNI_TRUE); + (jint)callCounter) == JNI_TRUE); } static Datum _Composite_nextSRF(Type self, jobject rowProducer, jobject rowCollector) diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 00108719..2505d0a6 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -415,7 +415,8 @@ Datum Type_invokeSRF(Type self, jclass cls, jmethodID method, jvalue* args, PG_F currentInvocation->hasConnected = ctxData->hasConnected; currentInvocation->invocation = ctxData->invocation; - hasRow = Type_hasNextSRF(self, ctxData->rowProducer, ctxData->rowCollector, (jint)context->call_cntr); + hasRow = Type_hasNextSRF(self, ctxData->rowProducer, ctxData->rowCollector, + (jlong)context->call_cntr); ctxData->hasConnected = currentInvocation->hasConnected; ctxData->invocation = currentInvocation->invocation; @@ -629,7 +630,7 @@ static jobject _Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) return 0; } -static bool _Type_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jint callCounter) +static bool _Type_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jlong callCounter) { return (JNI_callBooleanMethod(rowProducer, s_Iterator_hasNext) == JNI_TRUE); } @@ -656,7 +657,7 @@ jobject Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) return self->typeClass->getSRFCollector(self, fcinfo); } -bool Type_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jint callCounter) +bool Type_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jlong callCounter) { return self->typeClass->hasNextSRF(self, rowProducer, rowCollector, callCounter); } diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index cf1cd408..3aecbd04 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -54,6 +54,16 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); #include "access/htup_details.h" #endif +/* + * PG_*_{MIN,MAX} macros (which happen, conveniently, to match Java's datatypes + * (the signed ones, anyway), appear in PG 9.5. Could test for them directly, + * but explicit version conditionals may be easier to find and prune when the + * back-compatibility horizon passes them. Here are only the ones being used. + */ +#if PG_VERSION_NUM < 90500 +#define PG_INT32_MAX (0x7FFFFFFF) +#endif + /* The errorOccured will be set when a call from Java into one of the * backend functions results in a elog that causes a longjmp (Levels >= ERROR) diff --git a/pljava-so/src/main/include/pljava/type/Type.h b/pljava-so/src/main/include/pljava/type/Type.h index 97aacb5c..f017d7cb 100644 --- a/pljava-so/src/main/include/pljava/type/Type.h +++ b/pljava-so/src/main/include/pljava/type/Type.h @@ -215,7 +215,7 @@ extern jobject Type_getSRFCollector(Type self, PG_FUNCTION_ARGS); /* * Called to determine if the producer will produce another row. */ -extern bool Type_hasNextSRF(Type self, jobject producer, jobject collector, jint counter); +extern bool Type_hasNextSRF(Type self, jobject producer, jobject collector, jlong counter); /* * Converts the next row into a Datum of the expected type. diff --git a/pljava-so/src/main/include/pljava/type/Type_priv.h b/pljava-so/src/main/include/pljava/type/Type_priv.h index b3f01602..f5abf4f6 100644 --- a/pljava-so/src/main/include/pljava/type/Type_priv.h +++ b/pljava-so/src/main/include/pljava/type/Type_priv.h @@ -102,7 +102,7 @@ struct TypeClass_ jobject (*getSRFProducer)(Type self, jclass clazz, jmethodID method, jvalue* args); jobject (*getSRFCollector)(Type self, PG_FUNCTION_ARGS); - bool (*hasNextSRF)(Type self, jobject producer, jobject collector, jint counter); + bool (*hasNextSRF)(Type self, jobject producer, jobject collector, jlong counter); Datum (*nextSRF)(Type self, jobject producer, jobject collector); void (*closeSRF)(Type self, jobject producer); const char* (*getJNISignature)(Type self); From 0dc6c81a17833d09b0511513371353b37cb644b3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Oct 2016 22:37:58 -0400 Subject: [PATCH 0019/1087] Handle widening of SPITupleTable .alloced and .free. Not many choices available here. Wider-than-32-bit array indices in Java seem as far off as ever, so without reimplementing TupleTable to do something else besides copying the whole megillah into a Java array, there is no choice but to check and report a suitable error if the table is too big. --- pljava-so/src/main/c/type/TupleTable.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/TupleTable.c b/pljava-so/src/main/c/type/TupleTable.c index 801eb45e..670f88a7 100644 --- a/pljava-so/src/main/c/type/TupleTable.c +++ b/pljava-so/src/main/c/type/TupleTable.c @@ -42,17 +42,25 @@ jobject TupleTable_createFromSlot(TupleTableSlot* tts) jobject TupleTable_create(SPITupleTable* tts, jobject knownTD) { jobjectArray tuples; + uint64 tupcount; MemoryContext curr; if(tts == 0) return 0; + tupcount = tts->alloced - tts->free; + if ( tupcount > PG_INT32_MAX ) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("a PL/Java TupleTable cannot represent more than " + "INT32_MAX rows"))); + curr = MemoryContextSwitchTo(JavaMemoryContext); if(knownTD == 0) knownTD = TupleDesc_internalCreate(tts->tupdesc); - tuples = Tuple_createArray(tts->vals, (jint)(tts->alloced - tts->free), true); + tuples = Tuple_createArray(tts->vals, (jint)tupcount, true); MemoryContextSwitchTo(curr); return JNI_newObject(s_TupleTable_class, s_TupleTable_init, knownTD, tuples); From 5270b7bb680af94cd9aa68438650f5384dae2ab7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 25 Oct 2016 01:21:31 -0400 Subject: [PATCH 0020/1087] Copyright notice in files just touched. Some files still have notices predating the explicit BSD-style one introduced in 2013. Update it in the files this branch has touched. --- pljava-so/src/main/c/SPI.c | 14 +++++++++----- pljava-so/src/main/c/type/Composite.c | 14 +++++++++----- pljava-so/src/main/c/type/Portal.c | 15 +++++++++------ pljava-so/src/main/c/type/TupleDesc.c | 14 +++++++++----- pljava-so/src/main/c/type/TupleTable.c | 14 +++++++++----- pljava-so/src/main/c/type/Type.c | 14 +++++++++----- pljava-so/src/main/include/pljava/pljava.h | 14 +++++++++----- pljava-so/src/main/include/pljava/type/Type.h | 14 +++++++++----- .../src/main/include/pljava/type/Type_priv.h | 14 +++++++++----- .../org/postgresql/pljava/internal/Portal.java | 14 ++++++++++---- .../java/org/postgresql/pljava/internal/SPI.java | 14 ++++++++++---- .../pljava/jdbc/SPIPreparedStatement.java | 15 ++++++++++----- .../org/postgresql/pljava/jdbc/SPIResultSet.java | 14 ++++++++++---- .../org/postgresql/pljava/jdbc/SPIStatement.java | 15 ++++++++++----- 14 files changed, 131 insertions(+), 68 deletions(-) diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index b247cee0..6018e055 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include "org_postgresql_pljava_internal_SPI.h" #include "pljava/SPI.h" diff --git a/pljava-so/src/main/c/type/Composite.c b/pljava-so/src/main/c/type/Composite.c index 06ce8700..b8d82bfc 100644 --- a/pljava-so/src/main/c/type/Composite.c +++ b/pljava-so/src/main/c/type/Composite.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include #include diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index bbac1ad2..b9ba3030 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -1,12 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2008, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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 * - * @author Thomas Hallgren + * Contributors: + * Tada AB + * PostgreSQL Global Development Group + * Chapman Flack */ #include #include diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index a7172eb0..4400eb93 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include #include diff --git a/pljava-so/src/main/c/type/TupleTable.c b/pljava-so/src/main/c/type/TupleTable.c index 670f88a7..1f710658 100644 --- a/pljava-so/src/main/c/type/TupleTable.c +++ b/pljava-so/src/main/c/type/TupleTable.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include #include diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 2505d0a6..ed1eefb1 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include #include diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index 3aecbd04..8ae361b4 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #ifndef __pljava_pljava_h #define __pljava_pljava_h diff --git a/pljava-so/src/main/include/pljava/type/Type.h b/pljava-so/src/main/include/pljava/type/Type.h index f017d7cb..c8a0bc6a 100644 --- a/pljava-so/src/main/include/pljava/type/Type.h +++ b/pljava-so/src/main/include/pljava/type/Type.h @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #ifndef __pljava_type_Type_h #define __pljava_type_Type_h diff --git a/pljava-so/src/main/include/pljava/type/Type_priv.h b/pljava-so/src/main/include/pljava/type/Type_priv.h index f5abf4f6..0a95d0db 100644 --- a/pljava-so/src/main/include/pljava/type/Type_priv.h +++ b/pljava-so/src/main/include/pljava/type/Type_priv.h @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #ifndef __pljava_type_Type_priv_h #define __pljava_type_Type_priv_h diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java index bb7cacdc..79b8e734 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java index 29fa55e2..567838c4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index 874140fa..478249aa 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2007, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java index 5d3b6002..ab27c437 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2016 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java index 3c6086fc..cf200840 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2008, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; From c8a91f78f8ef228791aa180e6c0fc2c2d49e2e72 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 25 Oct 2016 23:34:17 -0400 Subject: [PATCH 0021/1087] Teach SQL generator about PARALLEL declaration. For now, it will freely accept, and generate the right syntax for, any of parallel={UNSAFE,RESTRICTED,SAFE}. The API docs explain why anything besides UNSAFE is probably a dangerous lie, but the generator does no enforcement for now. --- .../pljava/annotation/Function.java | 27 ++++++++++++++++++- .../pljava/sqlgen/DDRProcessor.java | 7 ++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java index 28768c83..8346ff20 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2016 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 @@ -65,6 +65,14 @@ enum Effects { IMMUTABLE, STABLE, VOLATILE }; */ enum Trust { SANDBOXED, UNSANDBOXED }; + /** + * Whether the function is unsafe to use in any parallel query plan at all, + * or avoids certain operations and can appear in such a plan but must be + * executed only in the parallel group leader, or avoids an even larger + * set of operations and is safe to execute anywhere in a parallel plan. + */ + enum Parallel { UNSAFE, RESTRICTED, SAFE }; + /** * The element type in case the annotated function returns a * {@link org.postgresql.pljava.ResultSetProvider ResultSetProvider}, @@ -138,6 +146,23 @@ enum Trust { SANDBOXED, UNSANDBOXED }; * in the "untrusted" language instance. */ Trust trust() default Trust.SANDBOXED; + + /** + * Whether the function is UNSAFE to use in any parallel query plan at all + * (the default), or avoids all disqualifying operations and so is SAFE to + * execute anywhere in a parallel plan, or, by avoiding some such + * operations, may appear in parallel plans but RESTRICTED to execute only + * on the parallel group leader. The operations that must be considered are + * set out in Parallel Labeling for Functions and Aggregates in the PostgreSQL docs. + * PL/Java itself can perform such operations internally; a thorough code + * audit would be needed to learn when, if ever, a PL/Java function could + * appropriately be declared anything but the default UNSAFE. + * + * Appeared in 9.6. + */ + Parallel parallel() default Parallel.UNSAFE; /** * Whether the function can be safely pushed inside the evaluation of views diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 96c3d035..998d8a2d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2016 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 @@ -1226,6 +1226,7 @@ class FunctionImpl public Security security() { return _security; } public Effects effects() { return _effects; } public Trust trust() { return _trust; } + public Parallel parallel() { return _parallel; } public boolean leakproof() { return _leakproof; } public int cost() { return _cost; } public int rows() { return _rows; } @@ -1243,6 +1244,7 @@ class FunctionImpl public Security _security; public Effects _effects; public Trust _trust; + public Parallel _parallel; public Boolean _leakproof; int _cost; int _rows; @@ -1490,6 +1492,8 @@ else if ( setof ) sb.append( "\tRETURNS NULL ON NULL INPUT\n"); if ( Security.DEFINER.equals( security()) ) sb.append( "\tSECURITY DEFINER\n"); + if ( ! Parallel.UNSAFE.equals( parallel()) ) + sb.append( "\tPARALLEL ").append( parallel()).append( '\n'); if ( -1 != cost() ) sb.append( "\tCOST ").append( cost()).append( '\n'); if ( -1 != rows() ) @@ -1583,6 +1587,7 @@ class BaseUDTFunctionImpl extends FunctionImpl _security = Security.INVOKER; _effects = Effects.VOLATILE; _trust = Trust.SANDBOXED; + _parallel = Parallel.UNSAFE; _leakproof = false; _settings = new String[0]; _triggers = new Trigger[0]; From e5ae7fd0ac2692974f5831458b971b95c0d7f0b0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 27 Oct 2016 02:02:46 -0400 Subject: [PATCH 0022/1087] Initialize more lazily in background workers. In a background worker (in particular, one running ParallelWorkerMain), PL/Java's _PG_init can get called even if the parallel query being executed makes no reference to PL/Java functions. That's because ParallelWorkerMain makes sure the same libraries are loaded that were present in the lead process, so if it had loaded PL/Java at any time, it gets loaded in the background worker ... quite early, and _PG_init gets called before much of the state it wants to look at has been set up. Detect that case and bail from the initsequencer as soon as possible (right after defining the custom GUCs), leaving all the rest to be completed when (if!) any actual call arrives at the call handler. This prevents a baffling failure in parallel queries that make no use of PL/Java; even better, it also avoids starting JVMs unnecessarily in parallel queries that aren't going to use them. --- pljava-so/src/main/c/Backend.c | 48 +++++++++---- pljava-so/src/main/c/InstallHelper.c | 68 ++++++++++++++++++- .../src/main/include/pljava/InstallHelper.h | 11 +++ 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 5a449210..637e9b6b 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1,12 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2009, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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 * - * @author Thomas Hallgren + * Contributors: + * Tada AB - Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ #include #include @@ -177,6 +180,18 @@ static bool loadAsExtensionFailed = false; static bool seenVisualVMName; static char const visualVMprefix[] = "-Dvisualvm.display.name="; +/* + * In a background worker, _PG_init may be called very early, before much of + * the state needed during PL/Java initialization has even been set up. When + * that case is detected, initsequencer needs to go just as far as + * IS_GUCS_REGISTERED and then bail. The GUC assign hooks may then also be + * invoked as GUC values get copied from the lead process; they also need to + * return quickly (accomplished by checking this flag in ASSIGNRETURNIFNXACT). + * Further initialization is thus deferred until the first actual call arrives + * at the call handler, which resets this flag and rejoins the initsequencer. + */ +static bool deferInitInBGW = false; + static void initsequencer(enum initstage is, bool tolerant); #if PG_VERSION_NUM >= 90100 @@ -278,7 +293,7 @@ static void initsequencer(enum initstage is, bool tolerant); #define ASSIGNRETURN(thing) return (thing) #define ASSIGNRETURNIFCHECK(thing) if (doit) ; else return (thing) #define ASSIGNRETURNIFNXACT(thing) \ - if (pljavaViableXact()) ; else return (thing) + if (! deferInitInBGW && pljavaViableXact()) ; else return (thing) #define ASSIGNSTRINGHOOK(name) \ static const char * \ CppConcat(assign_,name)(const char *newval, bool doit, GucSource source); \ @@ -292,7 +307,8 @@ static void initsequencer(enum initstage is, bool tolerant); CppConcat(assign_,name)(type newval, void *extra) #define ASSIGNRETURN(thing) #define ASSIGNRETURNIFCHECK(thing) -#define ASSIGNRETURNIFNXACT(thing) if (pljavaViableXact()) ; else return +#define ASSIGNRETURNIFNXACT(thing) \ + if (! deferInitInBGW && pljavaViableXact()) ; else return #define ASSIGNSTRINGHOOK(name) ASSIGNHOOK(name, const char *) #endif @@ -302,8 +318,8 @@ ASSIGNSTRINGHOOK(libjvm_location) libjvmlocation = (char *)newval; if ( IS_FORMLESS_VOID < initstage && initstage < IS_CAND_JVMOPENED ) { - alteredSettingsWereNeeded = true; ASSIGNRETURNIFNXACT(newval); + alteredSettingsWereNeeded = true; initsequencer( initstage, true); } ASSIGNRETURN(newval); @@ -315,8 +331,8 @@ ASSIGNSTRINGHOOK(vmoptions) vmoptions = (char *)newval; if ( IS_FORMLESS_VOID < initstage && initstage < IS_JAVAVM_OPTLIST ) { - alteredSettingsWereNeeded = true; ASSIGNRETURNIFNXACT(newval); + alteredSettingsWereNeeded = true; initsequencer( initstage, true); } ASSIGNRETURN(newval); @@ -328,8 +344,8 @@ ASSIGNSTRINGHOOK(classpath) classpath = (char *)newval; if ( IS_FORMLESS_VOID < initstage && initstage < IS_JAVAVM_OPTLIST ) { - alteredSettingsWereNeeded = true; ASSIGNRETURNIFNXACT(newval); + alteredSettingsWereNeeded = true; initsequencer( initstage, true); } ASSIGNRETURN(newval); @@ -341,8 +357,8 @@ ASSIGNHOOK(enabled, bool) pljavaEnabled = newval; if ( IS_FORMLESS_VOID < initstage && initstage < IS_PLJAVA_ENABLED ) { - alteredSettingsWereNeeded = true; ASSIGNRETURNIFNXACT(true); + alteredSettingsWereNeeded = true; initsequencer( initstage, true); } ASSIGNRETURN(true); @@ -393,6 +409,8 @@ static void initsequencer(enum initstage is, bool tolerant) case IS_FORMLESS_VOID: registerGUCOptions(); initstage = IS_GUCS_REGISTERED; + if ( deferInitInBGW ) + return; case IS_GUCS_REGISTERED: if ( NULL == libjvmlocation ) @@ -744,7 +762,10 @@ void _PG_init() { if ( IS_PLJAVA_FOUND == initstage ) return; /* creating handler functions will cause recursive call */ - pljavaCheckExtension( NULL); + if ( InstallHelper_inBackgroundWorker() ) + deferInitInBGW = true; + else + pljavaCheckExtension( NULL); initsequencer( initstage, true); } @@ -1473,6 +1494,7 @@ static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS) = fcinfo->flinfo->fn_oid; if ( IS_COMPLETE != initstage ) { + deferInitInBGW = false; initsequencer( initstage, false); /* Force initial setting diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index f278f0ff..ba11ec18 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -65,6 +65,28 @@ #endif #endif +/* + * Before 9.3, there was no IsBackgroundWorker. As of 9.6.1 it still does not + * have PGDLLIMPORT, but MyBgworkerEntry != NULL can be used in MSVC instead. + * However, until 9.3.3, even that did not have PGDLLIMPORT, and there's not + * much to be done about it. BackgroundWorkerness won't be detected in MSVC + * for 9.3.0 through 9.3.2. + * + * One thing it's needed for is to avoid dereferencing MyProcPort in a + * background worker, where it's not set. Define BGW_HAS_NO_MYPROCPORT if that + * has to be (and can be) checked. + */ +#if PG_VERSION_NUM < 90300 || defined(_MSC_VER) && PG_VERSION_NUM < 90303 +#define IsBackgroundWorker false +#else +#define BGW_HAS_NO_MYPROCPORT +#include +#if defined(_MSC_VER) +#include +#define IsBackgroundWorker (MyBgworkerEntry != NULL) +#endif +#endif + #ifndef PLJAVA_SO_VERSION #error "PLJAVA_SO_VERSION needs to be defined to compile this file." #else @@ -87,6 +109,7 @@ static bool extensionExNihilo = false; static void checkLoadPath( bool *livecheck); static void getExtensionLoadPath(); +static char *origUserName(); char const *pljavaLoadPath = NULL; @@ -103,9 +126,45 @@ bool pljavaViableXact() char *pljavaDbName() { +#ifdef BGW_HAS_NO_MYPROCPORT + char *shortlived; + static char *longlived; + if ( IsBackgroundWorker ) + { + if ( NULL == longlived ) + { + shortlived = get_database_name(MyDatabaseId); + if ( NULL != shortlived ) + { + longlived = MemoryContextStrdup(TopMemoryContext, shortlived); + pfree(shortlived); + } + } + return longlived; + } +#endif return MyProcPort->database_name; } +static char *origUserName() +{ +#ifdef BGW_HAS_NO_MYPROCPORT + char *shortlived; + static char *longlived; + if ( IsBackgroundWorker ) + { + if ( NULL == longlived ) + { + shortlived = GetUserNameFromId(GetAuthenticatedUserId(), false); + longlived = MemoryContextStrdup(TopMemoryContext, shortlived); + pfree(shortlived); + } + return longlived; + } +#endif + return MyProcPort->user_name; +} + char const *pljavaClusterName() { /* @@ -327,6 +386,11 @@ char *pljavaFnOidToLibPath(Oid fnOid) return probinstring; } +bool InstallHelper_inBackgroundWorker() +{ + return IsBackgroundWorker; +} + bool InstallHelper_isPLJavaFunction(Oid fn) { char *itsPath; @@ -401,8 +465,8 @@ char *InstallHelper_hello() Invocation_pushBootContext(&ctx); nativeVer = String_createJavaStringFromNTS(SO_VERSION_STRING); - user = String_createJavaStringFromNTS(MyProcPort->user_name); - dbname = String_createJavaStringFromNTS(MyProcPort->database_name); + user = String_createJavaStringFromNTS(origUserName()); + dbname = String_createJavaStringFromNTS(pljavaDbName()); if ( '\0' == *clusternameC ) clustername = NULL; else diff --git a/pljava-so/src/main/include/pljava/InstallHelper.h b/pljava-so/src/main/include/pljava/InstallHelper.h index 8fee6ccc..754a687b 100644 --- a/pljava-so/src/main/include/pljava/InstallHelper.h +++ b/pljava-so/src/main/include/pljava/InstallHelper.h @@ -64,6 +64,8 @@ extern bool InstallHelper_isPLJavaFunction(Oid fn); /* * Return the name of the current database, from MyProcPort ... don't free it. + * In a background worker, there's no MyProcPort, and the name is found another + * way and strdup'd in TopMemoryContext, it'll keep, don't bother freeing it. */ extern char *pljavaDbName(); @@ -103,6 +105,15 @@ extern char const *InstallHelper_defaultClassPath(char *); */ extern bool pljavaViableXact(); +/* + * Backend's initsequencer needs to know whether it's being called in a 9.3+ + * background worker process (the init sequence has to change). That should be + * a simple test of IsBackgroundWorker except (wouldn't you know) for more + * version-specific Windows visibility issues, so the ugly details are in + * InstallHelper, and Backend just asks this nice function. + */ +extern bool InstallHelper_inBackgroundWorker(); + extern char *InstallHelper_hello(); extern void InstallHelper_groundwork(); From 2fb5a24b2d8aba0a13a5818867143bb03eceaad8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 29 Oct 2016 23:03:05 -0400 Subject: [PATCH 0023/1087] Out with my pessimism; PARALLEL SAFE works fine. A simple test with a PL/Java function declared PARALLEL SAFE, and used in a parallel query, did just what it says on the tin, on the first try. There may still be cases lurking in which PL/Java's inner workings would do something a SAFE or RESTRICTED function isn't supposed to do, but I had imagined there would be problems galore and there aren't, so the capability may as well be announced and documented. --- .../pljava/annotation/Function.java | 11 +-- src/site/markdown/use/parallel.md | 82 +++++++++++++++++++ src/site/markdown/use/use.md | 11 +++ 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 src/site/markdown/use/parallel.md diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java index 8346ff20..31bea59d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java @@ -150,16 +150,17 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; /** * Whether the function is UNSAFE to use in any parallel query plan at all * (the default), or avoids all disqualifying operations and so is SAFE to - * execute anywhere in a parallel plan, or, by avoiding some such + * execute anywhere in a parallel plan, or, by avoiding some such * operations, may appear in parallel plans but RESTRICTED to execute only * on the parallel group leader. The operations that must be considered are * set out in Parallel Labeling for Functions and Aggregates in the PostgreSQL docs. - * PL/Java itself can perform such operations internally; a thorough code - * audit would be needed to learn when, if ever, a PL/Java function could - * appropriately be declared anything but the default UNSAFE. - * + *

+ * For much more on the practicalities of parallel query and PL/Java, + * please see the users' guide. + *

* Appeared in 9.6. */ Parallel parallel() default Parallel.UNSAFE; diff --git a/src/site/markdown/use/parallel.md b/src/site/markdown/use/parallel.md new file mode 100644 index 00000000..627a56a2 --- /dev/null +++ b/src/site/markdown/use/parallel.md @@ -0,0 +1,82 @@ +# PL/Java in parallel query or background worker + +PL/Java can be used in some [background worker processes][bgworker] as +introduced in PostgreSQL 9.3, and in [parallel queries][parq], from +PostgreSQL 9.6, with some restrictions. + +[bgworker]: https://www.postgresql.org/docs/current/static/bgworker.html +[parq]: https://www.postgresql.org/docs/current/static/parallel-query.html + +## Background worker processes + +Because PL/Java requires access to a database containing the `sqlj` schema, +PL/Java is only usable in a worker process that initializes a database +connection, which must happen before the first use of any function that +depends on PL/Java. + +## Parallel queries + +Like any user-defined function, a PL/Java function can be +[annotated][paranno] with a level of "parallel safety", `UNSAFE` by default. + +When a function labeled `UNSAFE` is used in a query, the query cannot be +parallelized at all. If a query contains a function labeled `RESTRICTED`, parts +of the query may execute in parallel, but the part that calls the `RESTRICTED` +function will be executed only in the lead process. A function labeled `SAFE` +may be executed in every process participating in the query. + +[paranno]: ../pljava-api/apidocs/index.html?org/postgresql/pljava/annotation/Function.html#parallel() + +### Parallel setup cost + +PostgreSQL parallel query processing uses multiple operating-system processes, +and these processes are new for each parallel query. If a PL/Java function is +labeled `PARALLEL SAFE` and is pushed by the query planner to run in the +parallel worker processes, each new process will start a Java virtual machine. +The cost of doing so will reduce the expected advantage of parallel execution. + +To inform the query planner of this trade-off, the value of the PostgreSQL +configuration variable [`parallel_setup_cost`][parsetcost] should be increased. +The startup cost can be minimized with attention to the +[PL/Java VM option recommendations][vmopt], including class data sharing. + +[parsetcost]: https://www.postgresql.org/docs/current/static/runtime-config-query.html#GUC-PARALLEL-SETUP-COST +[vmopt]: ../install/vmoptions.html + +### Limits on `RESTRICTED`/`SAFE` function behavior + +There are stringent limits on what a function labeled `RESTRICTED` may do, +and even more stringent limits on what may be done in a function labeled `SAFE`. +The PostgreSQL manual describes the limits in the section +[Parallel Labeling for Functions and Aggregates][parlab]. + +[parlab]: https://www.postgresql.org/docs/current/static/parallel-safety.html#PARALLEL-LABELING + +While PostgreSQL does check for some inappropriate operations from a +`PARALLEL SAFE` or `RESTRICTED` function, for the most part it relies on +functions being labeled correctly. When in doubt, the conservative approach +is to label a function `UNSAFE`, which can't go wrong. A function mistakenly +labeled `RESTRICTED` or `SAFE` could produce unpredictable results. + +#### Internal workings of PL/Java + +While a given PL/Java function itself may clearly qualify as `RESTRICTED` or +`SAFE` by inspection, there may still be cases where a forbidden operation +results from the internal workings of PL/Java itself. This has not been seen +in testing (simple parallel queries with `RESTRICTED` or `SAFE` PL/Java +functions work fine), but to rule out the possibility would require a careful +audit of PL/Java's code. Until then, it would be prudent for any application +involving parallel query with `RESTRICTED` or `SAFE` PL/Java functions +to be first tested in a non-production environment. + +### Further reading + +A [Parallel query and PL/Java][pqwiki] page on the PL/Java wiki is provided +to collect experience and tips regarding this significant new capability +that may be gathered in between updates to this documentation. + +[README.parallel][rmp] in the PostgreSQL source, for more detail on why parallel +query works the way it does. + +[rmp]: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/README.parallel +[pqwiki]: https://github.com/tada/pljava/wiki/Parallel-query-and-PLJava diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index 66479dbd..c942434b 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -35,6 +35,17 @@ internal PL/Java classes that may change from release to release. ## Special topics +### Parallel query + +PostgreSQL 9.3 introduced [background worker processes][bgworker], +and PostgreSQL 9.6 introduced [parallel query][parq]. + +For details on PL/Java in a background worker or parallel query, see +[PL/Java in parallel query](parallel.html). + +[bgworker]: https://www.postgresql.org/docs/current/static/bgworker.html +[parq]: https://www.postgresql.org/docs/current/static/parallel-query.html + ### Character-set encodings PL/Java will work most seamlessly when the server encoding in PostgreSQL is From 97ddb5efe7e75840dbee7606e13dfa1faf455bbc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Oct 2016 18:06:48 -0400 Subject: [PATCH 0024/1087] Appease vengeful gods of back compatibility. You can have a background worker process starting in 9.3, but it can't find out who it's authenticated as until 9.5.... --- pljava-so/src/main/c/InstallHelper.c | 13 +++++++++++-- src/site/markdown/use/parallel.md | 7 ++++--- src/site/markdown/use/use.md | 3 ++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index ba11ec18..89ea0ca3 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -149,10 +149,11 @@ char *pljavaDbName() static char *origUserName() { #ifdef BGW_HAS_NO_MYPROCPORT - char *shortlived; - static char *longlived; if ( IsBackgroundWorker ) { +#if PG_VERSION_NUM >= 90500 + char *shortlived; + static char *longlived; if ( NULL == longlived ) { shortlived = GetUserNameFromId(GetAuthenticatedUserId(), false); @@ -160,6 +161,14 @@ static char *origUserName() pfree(shortlived); } return longlived; +#else + ereport(ERROR, ( + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("PL/Java in a background worker not supported " + "in this PostgreSQL version"), + errhint("PostgreSQL 9.5 is the first version to support " + "PL/Java in a background worker."))); +#endif } #endif return MyProcPort->user_name; diff --git a/src/site/markdown/use/parallel.md b/src/site/markdown/use/parallel.md index 627a56a2..083228bb 100644 --- a/src/site/markdown/use/parallel.md +++ b/src/site/markdown/use/parallel.md @@ -1,8 +1,9 @@ # PL/Java in parallel query or background worker -PL/Java can be used in some [background worker processes][bgworker] as -introduced in PostgreSQL 9.3, and in [parallel queries][parq], from -PostgreSQL 9.6, with some restrictions. +With some restrictions, PL/Java can be used in [parallel queries][parq], from +PostgreSQL 9.6, and in some [background worker processes][bgworker] (as +introduced in PostgreSQL 9.3, though 9.5 or later is needed for support +in PL/Java). [bgworker]: https://www.postgresql.org/docs/current/static/bgworker.html [parq]: https://www.postgresql.org/docs/current/static/parallel-query.html diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index c942434b..b11a1424 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -37,7 +37,8 @@ internal PL/Java classes that may change from release to release. ### Parallel query -PostgreSQL 9.3 introduced [background worker processes][bgworker], +PostgreSQL 9.3 introduced [background worker processes][bgworker] +(though at least PostgreSQL 9.5 is needed for support in PL/Java), and PostgreSQL 9.6 introduced [parallel query][parq]. For details on PL/Java in a background worker or parallel query, see From 4b053839cdacfbfed5f58e132529af9ad3ef6f5e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 19 Apr 2017 22:42:17 -0400 Subject: [PATCH 0025/1087] Clarify currentRow in ResultSetProvider. Fill in the missing explanation (reported in issue #115) of the currentRow parameter in ResultSetProvider. Trivial doc fix, pushing directly. --- .../main/java/org/postgresql/pljava/ResultSetProvider.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java index 3e5d2bed..63601c6d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2017 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 @@ -33,7 +33,8 @@ public interface ResultSetProvider * is a {@code SingleRowWriter} * writer instance that is used for capturing the data for the row. * @param receiver Receiver of values for the given row. - * @param currentRow Row number. First call will have row number 0. + * @param currentRow Row number, zero on the first call, incremented by one + * on each subsequent call. * @return true if a new row was provided, false * if not (end of data). * @throws SQLException From e05905560f9b738fec934d19ece960a0e22dc4e3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 20 Apr 2017 21:48:27 -0400 Subject: [PATCH 0026/1087] Suppress eager initialization during pg_upgrade. During pg_upgrade, dynamic libraries are loaded to confirm they exist in the new installation, but this happens early, before pg_upgrade has copied the old schema over. PL/Java should not react to the absence of its schema by creating one (as it would during a fresh install), because pg_upgrade is expecting to handle that. Reported in #117. --- pljava-so/src/main/c/Backend.c | 16 +++++++++------- pljava-so/src/main/c/InstallHelper.c | 14 ++++++++++++-- .../src/main/include/pljava/InstallHelper.h | 7 ++++--- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 637e9b6b..2d760562 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -189,8 +189,10 @@ static char const visualVMprefix[] = "-Dvisualvm.display.name="; * return quickly (accomplished by checking this flag in ASSIGNRETURNIFNXACT). * Further initialization is thus deferred until the first actual call arrives * at the call handler, which resets this flag and rejoins the initsequencer. + * The same lazy approach needs to be followed during a pg_upgrade (which test- + * loads libraries, thus calling _PG_init). This flag is set for either case. */ -static bool deferInitInBGW = false; +static bool deferInit = false; static void initsequencer(enum initstage is, bool tolerant); @@ -293,7 +295,7 @@ static void initsequencer(enum initstage is, bool tolerant); #define ASSIGNRETURN(thing) return (thing) #define ASSIGNRETURNIFCHECK(thing) if (doit) ; else return (thing) #define ASSIGNRETURNIFNXACT(thing) \ - if (! deferInitInBGW && pljavaViableXact()) ; else return (thing) + if (! deferInit && pljavaViableXact()) ; else return (thing) #define ASSIGNSTRINGHOOK(name) \ static const char * \ CppConcat(assign_,name)(const char *newval, bool doit, GucSource source); \ @@ -308,7 +310,7 @@ static void initsequencer(enum initstage is, bool tolerant); #define ASSIGNRETURN(thing) #define ASSIGNRETURNIFCHECK(thing) #define ASSIGNRETURNIFNXACT(thing) \ - if (! deferInitInBGW && pljavaViableXact()) ; else return + if (! deferInit && pljavaViableXact()) ; else return #define ASSIGNSTRINGHOOK(name) ASSIGNHOOK(name, const char *) #endif @@ -409,7 +411,7 @@ static void initsequencer(enum initstage is, bool tolerant) case IS_FORMLESS_VOID: registerGUCOptions(); initstage = IS_GUCS_REGISTERED; - if ( deferInitInBGW ) + if ( deferInit ) return; case IS_GUCS_REGISTERED: @@ -762,8 +764,8 @@ void _PG_init() { if ( IS_PLJAVA_FOUND == initstage ) return; /* creating handler functions will cause recursive call */ - if ( InstallHelper_inBackgroundWorker() ) - deferInitInBGW = true; + if ( InstallHelper_shouldDeferInit() ) + deferInit = true; else pljavaCheckExtension( NULL); initsequencer( initstage, true); @@ -1494,7 +1496,7 @@ static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS) = fcinfo->flinfo->fn_oid; if ( IS_COMPLETE != initstage ) { - deferInitInBGW = false; + deferInit = false; initsequencer( initstage, false); /* Force initial setting diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 89ea0ca3..8fb6335b 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -65,6 +65,16 @@ #endif #endif +/* + * Before 9.1, there was no IsBinaryUpgrade. Before 9.5, it did not have + * PGDLLIMPORT and so was not visible in Windows. In either case, just define + * it to be false; Windows users may have trouble using pg_upgrade to versions + * earlier than 9.5, but with the current version being 9.6 that should be rare. + */ +#if PG_VERSION_NUM < 90100 || defined(_MSC_VER) && PG_VERSION_NUM < 90500 +#define IsBinaryUpgrade false +#endif + /* * Before 9.3, there was no IsBackgroundWorker. As of 9.6.1 it still does not * have PGDLLIMPORT, but MyBgworkerEntry != NULL can be used in MSVC instead. @@ -395,9 +405,9 @@ char *pljavaFnOidToLibPath(Oid fnOid) return probinstring; } -bool InstallHelper_inBackgroundWorker() +bool InstallHelper_shouldDeferInit() { - return IsBackgroundWorker; + return IsBackgroundWorker || IsBinaryUpgrade; } bool InstallHelper_isPLJavaFunction(Oid fn) diff --git a/pljava-so/src/main/include/pljava/InstallHelper.h b/pljava-so/src/main/include/pljava/InstallHelper.h index 754a687b..efed53fb 100644 --- a/pljava-so/src/main/include/pljava/InstallHelper.h +++ b/pljava-so/src/main/include/pljava/InstallHelper.h @@ -107,12 +107,13 @@ extern bool pljavaViableXact(); /* * Backend's initsequencer needs to know whether it's being called in a 9.3+ - * background worker process (the init sequence has to change). That should be - * a simple test of IsBackgroundWorker except (wouldn't you know) for more + * background worker process, or during a pg_upgrade (in either case, the + * init sequence needs to be lazier). Those should both be simple tests of + * IsBackgroundWorker or IsBinaryUpgrade, except (wouldn't you know) for more * version-specific Windows visibility issues, so the ugly details are in * InstallHelper, and Backend just asks this nice function. */ -extern bool InstallHelper_inBackgroundWorker(); +extern bool InstallHelper_shouldDeferInit(); extern char *InstallHelper_hello(); From e6e1be219ef5aadcba233ccfa8586bab1f8b3c1f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 13 May 2017 14:51:09 -0400 Subject: [PATCH 0027/1087] Document PL/Java and pg_upgrade interactions. This is a good opportunity to break out upgrading procedures to a dedicated page. --- src/site/markdown/install/install.md.vm | 40 ++--------- src/site/markdown/install/upgrade.md | 95 +++++++++++++++++++++++++ src/site/site.xml | 2 + 3 files changed, 101 insertions(+), 36 deletions(-) create mode 100644 src/site/markdown/install/upgrade.md diff --git a/src/site/markdown/install/install.md.vm b/src/site/markdown/install/install.md.vm index cb671b00..2588db5e 100644 --- a/src/site/markdown/install/install.md.vm +++ b/src/site/markdown/install/install.md.vm @@ -143,7 +143,7 @@ things right on the first try, you might set them after, too.) For example: [sqjij]: https://github.com/tada/pljava/wiki/SQL-functions -Those two are not the only PL/Java configuration variables there are, +Those three are not the only PL/Java configuration variables there are, but it is unlikely you would have to change any others before installation succeeds. For the rest, there is a [configuration variable reference][varref] page. @@ -195,42 +195,10 @@ PL/Java performs an upgrade installation if there is already an `sqlj` schema with tables that match a known PL/Java schema from version 1.3.0 or later. It will convert, preserving data, to the current schema if necessary. -*Remember that PL/Java runs independently -in each database session where it is in use. Older PL/Java versions active in -other sessions can be disrupted by the schema change.* +A database cluster using PL/Java can be binary-upgraded using `pg_upgrade` +when certain requirements are met. -A trial installation of a PL/Java update can be done in a transaction, and -rolled back if desired, leaving the schema as it was. Any concurrent sessions -with active older PL/Java versions will not be disrupted by the altered schema -as long as the transaction remains open, *but they may block for the duration, -so whatever testing will be done within the transaction should be done quickly -if that could be an issue*. - -$h3 Upgrading, outside the extension framework - -On PostgreSQL pre-9.1, or whenever PL/Java has not been installed -with `CREATE EXTENSION`, it can be updated with a `LOAD` command just -as in a fresh installation. This must be done in a fresh session (in -which nothing has caused PL/Java to load since establishing the connection). - -$h3 Upgrading, within the extension framework - -On PostgreSQL 9.1 or later where PL/Java has been installed with -`CREATE EXTENSION`, it can be updated with -[`ALTER EXTENSION pljava UPDATE`][aeu], as long as -`SELECT * FROM pg_extension_update_paths('pljava')` shows a one-step path -from the version currently installed to the version desired. - -[aeu]: http://www.postgresql.org/docs/current/static/sql-alterextension.html - -As with the `LOAD` method, an `ALTER EXTENSION ... UPDATE` must be done -in a fresh session, before anything has loaded PL/Java; this also precludes -an update with a multi-step path in a single command, but the intent is to -always provide a one-step path between _released_ versions. - -If you will be following development (`SNAPSHOT`) versions, the installation -method using `LOAD` may be simpler, as updates between snapshots with the -same version string make no sense to the extension framework. +For more on both procedures, see [Upgrading](upgrade.html). $h2 Usage permission diff --git a/src/site/markdown/install/upgrade.md b/src/site/markdown/install/upgrade.md new file mode 100644 index 00000000..4c397c3a --- /dev/null +++ b/src/site/markdown/install/upgrade.md @@ -0,0 +1,95 @@ +# Upgrading + +## Upgrading the PL/Java version in a database + +PL/Java performs an upgrade installation if there is already an `sqlj` schema +with tables that match a known PL/Java schema from version 1.3.0 or later. It +will convert, preserving data, to the current schema if necessary. + +*Remember that PL/Java runs independently +in each database session where it is in use. Older PL/Java versions active in +other sessions can be disrupted by the schema change.* + +A trial installation of a PL/Java update can be done in a transaction, and +rolled back if desired, leaving the schema as it was. Any concurrent sessions +with active older PL/Java versions will not be disrupted by the altered schema +as long as the transaction remains open, *but they may block for the duration, +so whatever testing will be done within the transaction should be done quickly +if that could be an issue*. + +### Upgrading, outside the extension framework + +On PostgreSQL pre-9.1, or whenever PL/Java has not been installed +with `CREATE EXTENSION`, it can be updated with a `LOAD` command just +as in a fresh installation. This must be done in a fresh session (in +which nothing has caused PL/Java to load since establishing the connection). + +### Upgrading, within the extension framework + +On PostgreSQL 9.1 or later where PL/Java has been installed with +`CREATE EXTENSION`, it can be updated with +[`ALTER EXTENSION pljava UPDATE`][aeu], as long as +`SELECT * FROM pg_extension_update_paths('pljava')` shows a one-step path +from the version currently installed to the version desired. + +[aeu]: http://www.postgresql.org/docs/current/static/sql-alterextension.html + +As with the `LOAD` method, an `ALTER EXTENSION ... UPDATE` must be done +in a fresh session, before anything has loaded PL/Java; this also precludes +an update with a multi-step path in a single command, but the intent is to +always provide a one-step path between _released_ versions. + +If you will be following development (`SNAPSHOT`) versions, the installation +method using `LOAD` may be simpler, as updates between snapshots with the +same version string make no sense to the extension framework. + +## Upgrading the PostgreSQL major version with PL/Java in use + +### Binary upgrading with `pg_upgrade` + +Using the [`pg_upgrade`][pgu] tool [contributed to PostgreSQL in 9.0][pguc], +an entire PostgreSQL cluster can be upgraded to a later major version in a +more direct process than the dump to SQL and reload formerly required. +The binary upgrade is possible as long as the cluster and databases meet +certain requirements, which should be studied in the +[`pg_upgrade` manual page][pgu] version for the PostgreSQL release being +upgraded *to*. + +PL/Java adds a few additional considerations: + +* `pg_upgrade` will check in advance that every loadable module used in + the old cluster can be loaded in the new cluster, but the schema and + data will be copied over by `pg_upgrade` itself. That means that a + PL/Java build for the new PostgreSQL version must be *installed in + the directory structure* for the new cluster before running `pg_upgrade`, + but *not* installed into any databases (the new cluster should not have + had any non-system objects created yet). + +* In the steps of [Installing PL/Java](install.html), that means that the + self-extracting `java -jar ...` command must have been run (or the + equivalent package-installation command, if you are getting PL/Java + through a packaging system for your OS), but no `CREATE EXTENSION` or + `LOAD` command should have been run to configure it in any database. If + using the extracting jar, to be sure of installing it to the right cluster, + add `-Dpgconfig=`*pgconfigpath* at the end, where *pgconfigpath* is the + full path to the *new* cluster's `pg_config` executable. + +* PL/Java releases before 1.5.1 were not aware of `pg_upgrade` operation. + To avoid possible errors during the upgrade involving OID or object + clashes, the PL/Java release installed for the new cluster should be + 1.5.1 or later. + +* When `pg_upgrade` tests that all needed modules are present, it expects + the names to match. The PL/Java module name includes the PL/Java version, + so the versions installed in the old and new clusters should be the same. + Given that 1.5.1 or later should be installed in the new cluster, + if any databases in the old cluster are using an older PL/Java version, + PL/Java should be upgraded in each (as described at the top of this page) + before running `pg_upgrade`. To be sure of installing a newer PL/Java + build into the old cluster, if using the extracting jar, add + `-Dpgconfig=`*oldpgconfigpath* at the end of the `java -jar ...` command + line, with *oldpgconfigpath* the full path to the old cluster's `pg_config` + executable. + +[pgu]: https://www.postgresql.org/docs/current/static/pgupgrade.html +[pguc]: https://www.postgresql.org/docs/9.0/static/release-9-0.html#AEN103668 diff --git a/src/site/site.xml b/src/site/site.xml index cadcf4ef..4bac4893 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -24,6 +24,8 @@ href='build/build.html'/> + Date: Thu, 1 Jun 2017 20:07:33 -0400 Subject: [PATCH 0028/1087] Accommodate new version numbering in pg 10. --- pljava-so/build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/build.xml b/pljava-so/build.xml index 4bac6756..8780b69f 100644 --- a/pljava-so/build.xml +++ b/pljava-so/build.xml @@ -63,7 +63,7 @@ From e0928d7f76ff419768bd5d40e0415a73bb95956c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 3 Jun 2017 11:29:39 -0400 Subject: [PATCH 0029/1087] Accommodate PG10 demise of float datetimes. Because PostgreSQL still has an integer_datetimes GUC (it's just always true now), the obvious approach would be "do nothing", and the integer code would always be selected, and the floating code would be dead. However, the dead code now produces compiler warnings about implicit declaration of floor() and rint(); presumably, we were relying on math.h being included in some PostgreSQL header where it now is not (though I am drawing a blank on where that changed; it's not in the same upstream commit that desupported float datetimes). Given that, the cleanest solution here is to conditionally compile the float-time code only for PG < 10, and one day prune it completely once the backward-compatibility goal reaches 10. --- pljava-so/src/main/c/Backend.c | 8 ++- pljava-so/src/main/c/type/Time.c | 54 +++++++++++++-------- pljava-so/src/main/c/type/Timestamp.c | 22 ++++++--- pljava-so/src/main/include/pljava/Backend.h | 2 + 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 2d760562..deba0225 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -106,7 +106,10 @@ static bool pljavaEnabled; static bool s_currentTrust; static int s_javaLogLevel; +#if PG_VERSION_NUM < 100000 bool integerDateTimes = false; +static void checkIntTimeType(void); +#endif extern void Invocation_initialize(void); extern void Exception_initialize(void); @@ -138,7 +141,6 @@ static void JVMOptList_delete(JVMOptList*); static void JVMOptList_add(JVMOptList*, const char*, void*, bool); static void JVMOptList_addVisualVMName(JVMOptList*); static void addUserJVMOptions(JVMOptList*); -static void checkIntTimeType(void); static char* getClassPath(const char*); static jint JNICALL my_vfprintf(FILE*, const char*, va_list); static void _destroyJavaVM(int, Datum); @@ -478,7 +480,9 @@ static void initsequencer(enum initstage is, bool tolerant) case IS_CREATEVM_SYM_FOUND: s_javaLogLevel = INFO; +#if PG_VERSION_NUM < 100000 checkIntTimeType(); +#endif HashMap_initialize(); /* creates things in TopMemoryContext */ #ifdef PLJAVA_DEBUG /* Hard setting for debug. Don't forget to recompile... @@ -1264,6 +1268,7 @@ static void initJavaSession(void) } } +#if PG_VERSION_NUM < 100000 static void checkIntTimeType(void) { const char* idt = PG_GETCONFIGOPTION("integer_datetimes"); @@ -1271,6 +1276,7 @@ static void checkIntTimeType(void) integerDateTimes = (strcmp(idt, "on") == 0); elog(DEBUG2, integerDateTimes ? "Using integer_datetimes" : "Not using integer_datetimes"); } +#endif static jint initializeJavaVM(JVMOptList *optList) { diff --git a/pljava-so/src/main/c/type/Time.c b/pljava-so/src/main/c/type/Time.c index e9990927..69e6edba 100644 --- a/pljava-so/src/main/c/type/Time.c +++ b/pljava-so/src/main/c/type/Time.c @@ -31,6 +31,7 @@ static jlong msecsAtMidnight(void) return INT64CONST(1000) * (jlong)(now * 86400); } +#if PG_VERSION_NUM < 100000 static jvalue Time_coerceDatumTZ_dd(Type self, double t, bool tzAdjust) { jlong mSecs; @@ -42,6 +43,7 @@ static jvalue Time_coerceDatumTZ_dd(Type self, double t, bool tzAdjust) result.l = JNI_newObject(s_Time_class, s_Time_init, mSecs + msecsAtMidnight()); return result; } +#endif static jvalue Time_coerceDatumTZ_id(Type self, int64 t, bool tzAdjust) { @@ -62,11 +64,13 @@ static jlong Time_getMillisecsToday(Type self, jobject jt, bool tzAdjust) return mSecs; } +#if PG_VERSION_NUM < 100000 static double Time_coerceObjectTZ_dd(Type self, jobject jt, bool tzAdjust) { jlong mSecs = Time_getMillisecsToday(self, jt, tzAdjust); return ((double)mSecs) / 1000.0; /* Convert to seconds */ } +#endif static int64 Time_coerceObjectTZ_id(Type self, jobject jt, bool tzAdjust) { @@ -76,16 +80,22 @@ static int64 Time_coerceObjectTZ_id(Type self, jobject jt, bool tzAdjust) static jvalue _Time_coerceDatum(Type self, Datum arg) { - return integerDateTimes - ? Time_coerceDatumTZ_id(self, DatumGetInt64(arg), true) - : Time_coerceDatumTZ_dd(self, DatumGetFloat8(arg), true); + return +#if PG_VERSION_NUM < 100000 + (!integerDateTimes) ? + Time_coerceDatumTZ_dd(self, DatumGetFloat8(arg), true) : +#endif + Time_coerceDatumTZ_id(self, DatumGetInt64(arg), true); } static Datum _Time_coerceObject(Type self, jobject time) { - return integerDateTimes - ? Int64GetDatum(Time_coerceObjectTZ_id(self, time, true)) - : Float8GetDatum(Time_coerceObjectTZ_dd(self, time, true)); + return +#if PG_VERSION_NUM < 100000 + (!integerDateTimes) ? + Float8GetDatum(Time_coerceObjectTZ_dd(self, time, true)) : +#endif + Int64GetDatum(Time_coerceObjectTZ_id(self, time, true)); } /* @@ -96,38 +106,42 @@ static Datum _Time_coerceObject(Type self, jobject time) static jvalue _Timetz_coerceDatum(Type self, Datum arg) { jvalue val; - if(integerDateTimes) - { - TimeTzADT_id* tza = (TimeTzADT_id*)DatumGetPointer(arg); - int64 t = tza->time + (int64)tza->zone * 1000000; /* Convert to UTC */ - val = Time_coerceDatumTZ_id(self, t, false); - } - else +#if PG_VERSION_NUM < 100000 + if(!integerDateTimes) { TimeTzADT_dd* tza = (TimeTzADT_dd*)DatumGetPointer(arg); double t = tza->time + tza->zone; /* Convert to UTC */ val = Time_coerceDatumTZ_dd(self, t, false); } + else +#endif + { + TimeTzADT_id* tza = (TimeTzADT_id*)DatumGetPointer(arg); + int64 t = tza->time + (int64)tza->zone * 1000000; /* Convert to UTC */ + val = Time_coerceDatumTZ_id(self, t, false); + } return val; } static Datum _Timetz_coerceObject(Type self, jobject time) { Datum datum; - if(integerDateTimes) +#if PG_VERSION_NUM < 100000 + if(!integerDateTimes) { - TimeTzADT_id* tza = (TimeTzADT_id*)palloc(sizeof(TimeTzADT_id)); - tza->time = Time_coerceObjectTZ_id(self, time, false); + TimeTzADT_dd* tza = (TimeTzADT_dd*)palloc(sizeof(TimeTzADT_dd)); + tza->time = Time_coerceObjectTZ_dd(self, time, false); tza->zone = Timestamp_getCurrentTimeZone(); - tza->time -= (int64)tza->zone * 1000000; /* Convert UTC to local time */ + tza->time -= tza->zone; /* Convert UTC to local time */ datum = PointerGetDatum(tza); } else +#endif { - TimeTzADT_dd* tza = (TimeTzADT_dd*)palloc(sizeof(TimeTzADT_dd)); - tza->time = Time_coerceObjectTZ_dd(self, time, false); + TimeTzADT_id* tza = (TimeTzADT_id*)palloc(sizeof(TimeTzADT_id)); + tza->time = Time_coerceObjectTZ_id(self, time, false); tza->zone = Timestamp_getCurrentTimeZone(); - tza->time -= tza->zone; /* Convert UTC to local time */ + tza->time -= (int64)tza->zone * 1000000; /* Convert UTC to local time */ datum = PointerGetDatum(tza); } return datum; diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index fb4629ee..f0e638ce 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -64,6 +64,7 @@ static jvalue Timestamp_coerceDatumTZ_id(Type self, Datum arg, bool tzAdjust) return result; } +#if PG_VERSION_NUM < 100000 static jvalue Timestamp_coerceDatumTZ_dd(Type self, Datum arg, bool tzAdjust) { jlong mSecs; @@ -84,12 +85,15 @@ static jvalue Timestamp_coerceDatumTZ_dd(Type self, Datum arg, bool tzAdjust) JNI_callVoidMethod(result.l, s_Timestamp_setNanos, uSecs * 1000); return result; } +#endif static jvalue Timestamp_coerceDatumTZ(Type self, Datum arg, bool tzAdjust) { - return integerDateTimes - ? Timestamp_coerceDatumTZ_id(self, arg, tzAdjust) - : Timestamp_coerceDatumTZ_dd(self, arg, tzAdjust); + return +#if PG_VERSION_NUM < 100000 + (!integerDateTimes) ? Timestamp_coerceDatumTZ_dd(self, arg, tzAdjust) : +#endif + Timestamp_coerceDatumTZ_id(self, arg, tzAdjust); } static Datum Timestamp_coerceObjectTZ_id(Type self, jobject jts, bool tzAdjust) @@ -106,6 +110,7 @@ static Datum Timestamp_coerceObjectTZ_id(Type self, jobject jts, bool tzAdjust) return Int64GetDatum(ts); } +#if PG_VERSION_NUM < 100000 static Datum Timestamp_coerceObjectTZ_dd(Type self, jobject jts, bool tzAdjust) { double ts; @@ -119,12 +124,15 @@ static Datum Timestamp_coerceObjectTZ_dd(Type self, jobject jts, bool tzAdjust) ts -= Timestamp_getTimeZone_dd(ts); /* Adjust from UTC to local time */ return Float8GetDatum(ts); } +#endif static Datum Timestamp_coerceObjectTZ(Type self, jobject jts, bool tzAdjust) { - return integerDateTimes - ? Timestamp_coerceObjectTZ_id(self, jts, tzAdjust) - : Timestamp_coerceObjectTZ_dd(self, jts, tzAdjust); + return +#if PG_VERSION_NUM < 100000 + (!integerDateTimes) ? Timestamp_coerceObjectTZ_dd(self, jts, tzAdjust) : +#endif + Timestamp_coerceObjectTZ_id(self, jts, tzAdjust); } static jvalue _Timestamp_coerceDatum(Type self, Datum arg) @@ -179,11 +187,13 @@ int32 Timestamp_getTimeZone_id(int64 dt) return Timestamp_getTimeZone( (dt / INT64CONST(1000000) + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400)); } +#if PG_VERSION_NUM < 100000 int32 Timestamp_getTimeZone_dd(double dt) { return Timestamp_getTimeZone( (pg_time_t)rint(dt + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400)); } +#endif int32 Timestamp_getCurrentTimeZone(void) { diff --git a/pljava-so/src/main/include/pljava/Backend.h b/pljava-so/src/main/include/pljava/Backend.h index 496f3f51..fda7b67f 100644 --- a/pljava-so/src/main/include/pljava/Backend.h +++ b/pljava-so/src/main/include/pljava/Backend.h @@ -21,7 +21,9 @@ extern "C" { * * @author Thomas Hallgren *****************************************************************/ +#if PG_VERSION_NUM < 100000 extern bool integerDateTimes; +#endif void Backend_setJavaSecurity(bool trusted); From e9f1de099b4c962875f86b44e26f6cf66b1e128c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 3 Jun 2017 11:52:25 -0400 Subject: [PATCH 0030/1087] Generate SQL for trigger transition tables. To accommodate PotgreSQL 10 transition tables, allow the Trigger annotation to specify tableOld and/or tableNew, check that they are allowed (trigger must be AFTER and include events capable of populating the table), and generate the corresponding REFERENCING OLD TABLE AS ... NEW TABLE AS ... in the trigger declaration. --- .../postgresql/pljava/annotation/Trigger.java | 39 ++++++++++++++++ .../pljava/sqlgen/DDRProcessor.java | 45 ++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java index bae7e838..f8aae638 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java @@ -19,6 +19,25 @@ /** * Annotation, only used in {@link Function#triggers @Function(triggers=...)}, * to specify what trigger(s) the function will be called for. + *

+ * Transition tables ({@link #tableOld} and {@link #tableNew}) appear in + * PostgreSQL 10. If a trigger is declared with + * {@code tableOld="oo", tableNew="nn"}, then the trigger function can query + * {@code oo} and {@code nn} as if they are actual tables with the same + * columns as the table responsible for the trigger, and containing the affected + * rows before and after the changes. Only an AFTER trigger can have transition + * tables. An UPDATE will populate both tables. INSERT will not populate the + * old table, and DELETE will not populate the new table. It is an error to + * specify either table if {@code events} does not include at least one event + * that could populate that table. As long as at least one such event is + * included, the table can be specified, and will simply have no rows if the + * trigger is invoked for an event that does not populate it. + *

+ * In an after-statement trigger, the transition tables include all rows + * affected by the statement. In an after-row trigger, the same is true: + * after-row triggers are all queued until the statement completes, and then + * the function will be invoked for each row that was affected, but will see + * the complete transition tables on each invocation. * @author Thomas Hallgren */ @Target({}) @Retention(RetentionPolicy.CLASS) @@ -92,6 +111,26 @@ enum Scope { STATEMENT, ROW }; */ String[] columns() default {}; + /** + * Name to refer to "before" table of affected rows. Only usable in an AFTER + * trigger whose {@code events} include UPDATE or DELETE. The trigger + * function can issue queries as if a table by this name exists and contains + * all rows affected by the event, in their prior state. (If the trigger is + * called for an event other than UPDATE or DELETE, the function can still + * query a table by this name, which will appear to be empty.) + */ + String tableOld() default ""; + + /** + * Name to refer to "after" table of affected rows. Only usable in an AFTER + * trigger whose {@code events} include UPDATE or INSERT. The trigger + * function can issue queries as if a table by this name exists and contains + * all rows affected by the event, in their new state. (If the trigger is + * called for an event other than UPDATE or INSERT, the function can still + * query a table by this name, which will appear to be empty.) + */ + String tableNew() default ""; + /** * A comment to be associated with the trigger. If left to default, * and the Java function has a doc comment, its first sentence will be used. diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 998d8a2d..17e2872c 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1084,6 +1084,8 @@ class TriggerImpl public Called called() { return _called; } public String when() { return _when; } public String[] columns() { return _columns; } + public String tableOld() { return _tableOld; } + public String tableNew() { return _tableNew; } public String[] provides() { return new String[0]; } public String[] requires() { return new String[0]; } @@ -1098,9 +1100,14 @@ class TriggerImpl public Called _called; public String _when; public String[] _columns; + public String _tableOld; + public String _tableNew; FunctionImpl func; AnnotationMirror origin; + + boolean refOld; + boolean refNew; TriggerImpl( FunctionImpl f, AnnotationMirror am) { @@ -1139,6 +1146,33 @@ else if ( Called.INSTEAD_OF.equals( _called) ) "Column list is meaningless unless UPDATE is a trigger event"); } + refOld = ! "".equals( _tableOld); + refNew = ! "".equals( _tableNew); + + if ( ( refOld || refNew ) ) + { + if ( ! Called.AFTER.equals( _called) ) + msg( Kind.ERROR, func.func, origin, + "Only AFTER triggers can reference OLD TABLE or NEW TABLE"); + boolean badOld = refOld; + boolean badNew = refNew; + for ( Event e : _events ) + { + switch ( e ) + { + case INSERT: badNew = false; break; + case UPDATE: badOld = badNew = false; break; + case DELETE: badOld = false; break; + } + } + if ( badOld ) + msg( Kind.ERROR, func.func, origin, + "Trigger must be callable on UPDATE or DELETE to reference OLD TABLE"); + if ( badNew ) + msg( Kind.ERROR, func.func, origin, + "Trigger must be callable on UPDATE or INSERT to reference NEW TABLE"); + } + if ( "".equals( _name) ) _name = TriggerNamer.synthesizeName( this); return false; @@ -1175,7 +1209,16 @@ public String[] deployStrings() sb.append( "\n\tON "); if ( ! "".equals( schema()) ) sb.append( schema()).append( '.'); - sb.append( table()).append( "\n\tFOR EACH "); + sb.append( table()); + if ( refOld || refNew ) + { + sb.append( "\n\tREFERENCING"); + if ( refOld ) + sb.append( " OLD TABLE AS ").append( _tableOld); + if ( refNew ) + sb.append( " NEW TABLE AS ").append( _tableNew); + } + sb.append( "\n\tFOR EACH "); sb.append( scope().toString()); if ( ! "".equals( _when) ) sb.append( "\n\tWHEN ").append( _when); From adf15afd407cafbe7f0ea6d2a5da48913a1d6221 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 3 Jun 2017 11:49:56 -0400 Subject: [PATCH 0031/1087] Register a trigger's transition tables. The magic that makes tableOld/tableNew appear to exist under the chosen names, for purposes of queries in SPI, happens when SPI_register_trigger_data is called, passing the TriggerData struct that was passed to the handler function, and that struct has the chosen names and the tuplestores. PL/Java doesn't do SPI_connect unless and until the called Java function wants to use the jdbc:default:connection, at which point the connection is created in Invocation.c and remembered for the current level. So, that's a fine place to call SPI_register_trigger_data. The TriggerData struct simply needs to be stashed in the current Invocation at the time of function entry, so it can be passed to the SPI function when the time comes. --- pljava-so/src/main/c/Function.c | 6 +++++- pljava-so/src/main/c/Invocation.c | 19 ++++++++++++++++++- .../src/main/include/pljava/Invocation.h | 12 ++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index bc62be90..6086a5f1 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -838,11 +838,15 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) jvalue arg; Datum ret; - arg.l = TriggerData_create((TriggerData*)fcinfo->context); + TriggerData *td = (TriggerData*)fcinfo->context; + arg.l = TriggerData_create(td); if(arg.l == 0) return 0; currentInvocation->function = self; +#if PG_VERSION_NUM >= 100000 + currentInvocation->triggerData = td; +#endif Type_invoke(self->func.nonudt.returnType, self->clazz, self->func.nonudt.method, &arg, fcinfo); fcinfo->isnull = false; diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index c76509e5..685a21b6 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -83,9 +83,20 @@ void Invocation_initialize(void) void Invocation_assertConnect(void) { + int rslt; if(!currentInvocation->hasConnected) { - SPI_connect(); + rslt = SPI_connect(); + if ( SPI_OK_CONNECT != rslt ) + elog(ERROR, "SPI_register_trigger_data returned %d", rslt); +#if PG_VERSION_NUM >= 100000 + if ( NULL != currentInvocation->triggerData ) + { + rslt = SPI_register_trigger_data(currentInvocation->triggerData); + if ( SPI_OK_TD_REGISTER != rslt ) + elog(WARNING, "SPI_register_trigger_data returned %d", rslt); + } +#endif currentInvocation->hasConnected = true; } } @@ -116,6 +127,9 @@ void Invocation_pushBootContext(Invocation* ctx) ctx->inExprContextCB = false; ctx->previous = 0; ctx->callLocals = 0; +#if PG_VERSION_NUM >= 100000 + ctx->triggerData = 0; +#endif currentInvocation = ctx; ++s_callLevel; } @@ -138,6 +152,9 @@ void Invocation_pushInvocation(Invocation* ctx, bool trusted) ctx->inExprContextCB = false; ctx->previous = currentInvocation; ctx->callLocals = 0; +#if PG_VERSION_NUM >= 100000 + ctx->triggerData = 0; +#endif currentInvocation = ctx; Backend_setJavaSecurity(trusted); ++s_callLevel; diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index 7a67d3e3..b1ed2ba0 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -10,6 +10,9 @@ #define __pljava_Invocation_h #include +#if PG_VERSION_NUM >= 100000 +#include +#endif #include "pljava/pljava.h" #ifdef __cplusplus @@ -69,6 +72,15 @@ struct Invocation_ */ CallLocal* callLocals; +#if PG_VERSION_NUM >= 100000 + /** + * TriggerData pointer, if the function is being called as a trigger, + * so it can be passed to SPI_register_trigger_data if the function connects + * to SPI. + */ + TriggerData* triggerData; +#endif + /** * The previous call context when nested function calls * are made or 0 if this call is at the top level. From 6c8ecbaee5900866e76bf54e1810feddbbdba015 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 2 Jun 2017 00:28:42 -0400 Subject: [PATCH 0032/1087] Add example for trigger transition tables. Expand the Triggers example so the trigger actually does something, and add a new trigger to test transition table functionality in PostgreSQL 10 or later. --- .../pljava/example/annotation/Triggers.java | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index 214b8a44..62915962 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -12,7 +12,11 @@ */ package org.postgresql.pljava.example.annotation; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import org.postgresql.pljava.TriggerData; import org.postgresql.pljava.annotation.Function; @@ -21,11 +25,15 @@ import org.postgresql.pljava.annotation.Trigger; import static org.postgresql.pljava.annotation.Trigger.Called.*; import static org.postgresql.pljava.annotation.Trigger.Event.*; +import static org.postgresql.pljava.annotation.Trigger.Scope.*; import static org.postgresql.pljava.annotation.Function.Security.*; +import static org.postgresql.pljava.example.LoggerTest.logMessage; + /** * Example creating a couple of tables, and a function to be called when - * triggered by insertion into either table. + * triggered by insertion into either table. In PostgreSQL 10 or later, + * also create a function and trigger that uses transition tables. */ @SQLActions({ @SQLAction( @@ -39,9 +47,21 @@ "DROP TABLE javatest.foobar_1" } ), + @SQLAction(provides="postgresql_transitiontables", install= +" select case " + +" when 100000 <= cast(current_setting('server_version_num') as integer) " + +" then set_config('pljava.implementors', 'postgresql_transitiontables,' " + +" || current_setting('pljava.implementors'), true) " + +" end" + ), @SQLAction( requires = "foobar triggers", + provides = "foobar2_42", install = "INSERT INTO javatest.foobar_2(value) VALUES (42)" + ), + @SQLAction( + requires = { "transition triggers", "foobar2_42" }, + install = "UPDATE javatest.foobar_2 SET value = 43 WHERE value = 42" ) }) public class Triggers @@ -56,11 +76,47 @@ public class Triggers security = INVOKER, triggers = { @Trigger(called = BEFORE, table = "foobar_1", events = { INSERT } ), - @Trigger(called = BEFORE, table = "foobar_2", events = { INSERT } ) + @Trigger(called = BEFORE, scope = ROW, table = "foobar_2", + events = { INSERT } ) }) public static void insertUsername(TriggerData td) throws SQLException { + ResultSet nrs = td.getNew(); + nrs.updateString( "username", "bob"); + } + + /** + * Examine old and new rows in reponse to a trigger. + * Transition tables first became available in PostgreSQL 10. + */ + @Function( + implementor = "postgresql_transitiontables", + requires = "foobar tables", + provides = "transition triggers", + schema = "javatest", + security = INVOKER, + triggers = { + @Trigger(called = AFTER, table = "foobar_2", events = { UPDATE }, + tableOld = "oldrows", tableNew = "newrows" ) + }) + + public static void examineRows(TriggerData td) + throws SQLException + { + Connection co = DriverManager.getConnection("jdbc:default:connection"); + Statement st = co.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT o.value, n.value" + + " FROM oldrows o FULL JOIN newrows n USING (username)"); + rs.next(); + int oval = rs.getInt(1); + int nval = rs.getInt(2); + if ( 42 == oval && 43 == nval ) + logMessage( "INFO", "trigger transition table test ok"); + else + logMessage( "WARNING", String.format( + "trigger transition table oval %d nval %d", oval, nval)); } } From 114a5bda113effa6ebed1ba7f0546d52fee1cbbc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 3 Jun 2017 18:40:44 -0400 Subject: [PATCH 0033/1087] Keep making DEBUG1 quieter. These sites were missed in commit 1eb3bd8, trying to get the PL/Java-loaded-versions announcement to be the only thing at DEBUG1. --- .../main/java/org/postgresql/pljava/internal/Backend.java | 4 ++-- .../main/java/org/postgresql/pljava/jdbc/Invocation.java | 2 +- pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index 39fc6e70..2faec5f7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -266,8 +266,8 @@ private static void setTrusted(boolean trusted) try { Logger log = Logger.getAnonymousLogger(); - if(log.isLoggable(Level.FINE)) - log.fine("Using SecurityManager for " + (trusted ? "trusted" : "untrusted") + " language"); + if(log.isLoggable(Level.FINER)) + log.finer("Using SecurityManager for " + (trusted ? "trusted" : "untrusted") + " language"); System.setSecurityManager(trusted ? s_trustedSecurityManager : s_untrustedSecurityManager); } finally diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java index 3da1c84c..becb98d6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java @@ -110,7 +110,7 @@ public void onExit() while(--idx >= 0) { PreparedStatement stmt = (PreparedStatement)m_preparedStatements.get(idx); - w.fine("Closed: " + stmt); + w.finer("Closed: " + stmt); stmt.close(); } } diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index b1e08651..cf83b7b7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -212,12 +212,12 @@ public static Map getTypeMap(final String schema) throws SQLException if(typesForSchema != null) return typesForSchema; - s_logger.fine("Creating typeMappings for schema " + schema); + s_logger.finer("Creating typeMappings for schema " + schema); typesForSchema = new HashMap() { public Object get(Object key) { - s_logger.fine("Obtaining type mapping for OID " + key + " for schema " + schema); + s_logger.finer("Obtaining type mapping for OID " + key + " for schema " + schema); return super.get(key); } }; @@ -239,7 +239,7 @@ public Object get(Object key) Oid typeOid = Oid.forTypeName(sqlName); typesForSchema.put(typeOid, cls); - s_logger.fine("Adding type mapping for OID " + typeOid + " -> class " + cls.getName() + " for schema " + schema); + s_logger.finer("Adding type mapping for OID " + typeOid + " -> class " + cls.getName() + " for schema " + schema); } catch(ClassNotFoundException e) { From d77d8b21f28289de6c6fc7ef56faef0b26d1349f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Jun 2017 22:50:04 -0400 Subject: [PATCH 0034/1087] Do away with never-documented LargeObject code. In the run-up to releasing 1.5.0, the code to do with large objects was looked over in a very conservative and cautious way, as fits code that was never documented or presented as API and didn't have any test coverage, with little way of knowing whether some client code somewhere in the wild might have been developed around it. So, it got carefully updated to 64-bit offsets, and even had a vulnerability reported because it hadn't been updated to honor the object access controls added in 9.0. What all that very conservative analysis failed to notice was, thanks to a change made back in 77bfc34, coupled with the absence of test coverage, it has been about eleven years since the last chance anyone ever had of doing anything useful with a LargeObject instance. That makes the decision to do away with it much easier. In all the PostgreSQL versions PL/Java currently supports, all the functions needed to manipulate large objects are already exposed in SQL and usable through the JDBC/SPI, without any specific effort needed in PL/Java. For programming convenience, some later version (after Java 7 becomes the minimum requirement) could add a simple utility method to turn an integer LO fd from lo_open into a SeekableByteChannel that could then be used without further trips through SPI, but even that would be about optimization and convenience, not functionality. --- pljava-so/src/main/c/type/LargeObject.c | 432 ------------------ pljava-so/src/main/c/type/Type.c | 2 - .../main/include/pljava/type/LargeObject.h | 34 -- .../pljava/internal/LargeObject.java | 209 --------- 4 files changed, 677 deletions(-) delete mode 100644 pljava-so/src/main/c/type/LargeObject.c delete mode 100644 pljava-so/src/main/include/pljava/type/LargeObject.h delete mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/LargeObject.java diff --git a/pljava-so/src/main/c/type/LargeObject.c b/pljava-so/src/main/c/type/LargeObject.c deleted file mode 100644 index 896861d8..00000000 --- a/pljava-so/src/main/c/type/LargeObject.c +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (c) 2004-2016 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: - * Tada AB - * Chapman Flack - */ -#include - -#include "org_postgresql_pljava_internal_LargeObject.h" -#include "pljava/Exception.h" -#include "pljava/Invocation.h" -#include "pljava/type/Type_priv.h" -#include "pljava/type/Oid.h" -#include "pljava/type/LargeObject.h" - -static jclass s_LargeObject_class; -static jmethodID s_LargeObject_init; - -#if PG_VERSION_NUM < 90300 -#define OFFSETNARROWCAST (int) -#else -#define OFFSETNARROWCAST -#endif - -/* - * org.postgresql.pljava.type.LargeObject type. - */ -jobject LargeObject_create(LargeObjectDesc* lo) -{ - jobject jlo; - Ptr2Long loH; - - if(lo == 0) - return 0; - - loH.longVal = 0L; /* ensure that the rest is zeroed out */ - loH.ptrVal = lo; - jlo = JNI_newObject(s_LargeObject_class, s_LargeObject_init, loH.longVal); - return jlo; -} - -extern void LargeObject_initialize(void); -void LargeObject_initialize(void) -{ - TypeClass cls; - JNINativeMethod methods[] = - { - { - "_create", - "(I)Lorg/postgresql/pljava/internal/Oid;", - Java_org_postgresql_pljava_internal_LargeObject__1create - }, - { - "_drop", - "(Lorg/postgresql/pljava/internal/Oid;)I", - Java_org_postgresql_pljava_internal_LargeObject__1drop - }, - { - "_open", - "(Lorg/postgresql/pljava/internal/Oid;I)Lorg/postgresql/pljava/internal/LargeObject;", - Java_org_postgresql_pljava_internal_LargeObject__1open - }, - { - "_close", - "(J)V", - Java_org_postgresql_pljava_internal_LargeObject__1close - }, - { - "_getId", - "(J)Lorg/postgresql/pljava/internal/Oid;", - Java_org_postgresql_pljava_internal_LargeObject__1getId - }, - { - "_length", - "(J)J", - Java_org_postgresql_pljava_internal_LargeObject__1length - }, - { - "_seek", - "(JJI)J", - Java_org_postgresql_pljava_internal_LargeObject__1seek - }, - { - "_tell", - "(J)J", - Java_org_postgresql_pljava_internal_LargeObject__1tell - }, - { - "_truncate", - "(JJ)V", - Java_org_postgresql_pljava_internal_LargeObject__1truncate - }, - { - "_read", - "(J[B)I", - Java_org_postgresql_pljava_internal_LargeObject__1read - }, - { - "_write", - "(J[B)I", - Java_org_postgresql_pljava_internal_LargeObject__1write - }, - { 0, 0, 0 } - }; - - s_LargeObject_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/LargeObject")); - PgObject_registerNatives2(s_LargeObject_class, methods); - s_LargeObject_init = PgObject_getJavaMethod(s_LargeObject_class, "", "(J)V"); - - cls = TypeClass_alloc("type.LargeObject"); - cls->JNISignature = "Lorg/postgresql/pljava/internal/LargeObject;"; - cls->javaTypeName = "org.postgresql.pljava.internal.LargeObject"; - Type_registerType("org.postgresql.pljava.internal.LargeObject", TypeClass_allocInstance(cls, InvalidOid)); -} - -/**************************************** - * JNI methods - ****************************************/ -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _create - * Signature: (I)Lorg/postgresql/pljava/internal/LargeObject; - */ -JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1create(JNIEnv* env, jclass cls, jint flags) -{ - jobject result = 0; - - BEGIN_NATIVE - PG_TRY(); - { - result = Oid_create(inv_create((int)flags)); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_create"); - } - PG_END_TRY(); - END_NATIVE - - return result; -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _drop - * Signature: (Lorg/postgresql/pljava/internal/Oid;)I - */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1drop(JNIEnv* env, jclass cls, jobject oid) -{ - jint result = -1; - BEGIN_NATIVE - PG_TRY(); - { - result = inv_drop(Oid_getOid(oid)); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_drop"); - } - PG_END_TRY(); - END_NATIVE - return result; -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _open - * Signature: (Lorg/postgresql/pljava/internal/Oid;I)Lorg/postgresql/pljava/internal/LargeObject; - */ -JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1open(JNIEnv* env, jclass cls, jobject oid, jint flags) -{ - jobject result = 0; - BEGIN_NATIVE - PG_TRY(); - { - result = LargeObject_create(inv_open(Oid_getOid(oid), (int)flags, JavaMemoryContext)); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_open"); - } - PG_END_TRY(); - END_NATIVE - return result; -} - - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _close - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1close(JNIEnv* env, jclass cls, jlong _this) -{ - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - if(self != 0) - { - BEGIN_NATIVE - PG_TRY(); - { - inv_close(self); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_close"); - } - PG_END_TRY(); - END_NATIVE - } -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _getId - * Signature: (J)Lorg/postgresql/pljava/internal/Oid; - */ -JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1getId(JNIEnv* env, jclass cls, jlong _this) -{ - jobject result = 0; - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - if(self != 0) - { - BEGIN_NATIVE - result = Oid_create(self->id); - END_NATIVE - } - return result; -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _length - * Signature: (J)J - */ -JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1length(JNIEnv* env, jclass cls, jlong _this) -{ - jlong result = 0; - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - if(self != 0) - { - BEGIN_NATIVE - PG_TRY(); - { - /* There's no inv_length call so we use inv_seek on - * a temporary LargeObjectDesc. - */ - LargeObjectDesc lod; - memcpy(&lod, self, sizeof(LargeObjectDesc)); - result = (jlong)inv_seek(&lod, OFFSETNARROWCAST 0L, SEEK_END); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_seek"); - } - PG_END_TRY(); - END_NATIVE - } - return result; -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _seek - * Signature: (JJI)J - */ -JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1seek(JNIEnv* env, jclass cls, jlong _this, jlong pos, jint whence) -{ - jlong result = 0; - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - if(self != 0) - { - BEGIN_NATIVE - PG_TRY(); - { - result = (jlong)inv_seek(self, OFFSETNARROWCAST pos, (int)whence); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_seek"); - } - PG_END_TRY(); - END_NATIVE - } - return result; -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _tell - * Signature: (J)J - */ -JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1tell(JNIEnv* env, jclass cls, jlong _this) -{ - jlong result = 0; - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - if(self != 0) - { - BEGIN_NATIVE - PG_TRY(); - { - result = (jlong)inv_tell(self); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_tell"); - } - PG_END_TRY(); - END_NATIVE - } - return result; -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _truncate - * Signature: (JJ)V - */ -JNIEXPORT void JNICALL Java_org_postgresql_pljava_internal_LargeObject__1truncate - (JNIEnv *env, jclass cls, jlong _this, jlong pos) -{ -#if PG_VERSION_NUM < 80300 - Exception_featureNotSupported("truncate() for large object", "8.3"); -#else - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - if(self != 0) - { - BEGIN_NATIVE - PG_TRY(); - { - inv_truncate(self, OFFSETNARROWCAST pos); - } - PG_CATCH(); - { - Exception_throw_ERROR("inv_truncate"); - } - PG_END_TRY(); - END_NATIVE - } -#endif -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _read - * Signature: (J[B)I - */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1read(JNIEnv* env, jclass cls, jlong _this, jbyteArray buf) -{ - jint result = -1; - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - - if(self != 0 && buf != 0) - { - BEGIN_NATIVE - jint nBytes = JNI_getArrayLength(buf); - if(nBytes != 0) - { - jbyte* byteBuf = JNI_getByteArrayElements(buf, 0); - if(byteBuf != 0) - { - PG_TRY(); - { - result = (jint)inv_read(self, (char*)byteBuf, (int)nBytes); - JNI_releaseByteArrayElements(buf, byteBuf, 0); - } - PG_CATCH(); - { - JNI_releaseByteArrayElements(buf, byteBuf, JNI_ABORT); - Exception_throw_ERROR("inv_read"); - } - PG_END_TRY(); - } - } - END_NATIVE - } - return result; -} - -/* - * Class: org_postgresql_pljava_internal_LargeObject - * Method: _write - * Signature: (J[B)I - */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_LargeObject__1write(JNIEnv* env, jclass cls, jlong _this, jbyteArray buf) -{ - jint result = -1; - LargeObjectDesc* self = Invocation_getWrappedPointer(_this); - - if(self != 0 && buf != 0) - { - BEGIN_NATIVE - jint nBytes = JNI_getArrayLength(buf); - if(nBytes != 0) - { - jbyte* byteBuf = JNI_getByteArrayElements(buf, 0); - if(byteBuf != 0) - { - PG_TRY(); - { - result = (jint)inv_write(self, (char*)byteBuf, nBytes); - - /* No need to copy bytes back, hence the JNI_ABORT */ - JNI_releaseByteArrayElements(buf, byteBuf, JNI_ABORT); - } - PG_CATCH(); - { - JNI_releaseByteArrayElements(buf, byteBuf, JNI_ABORT); - Exception_throw_ERROR("inv_write"); - } - PG_END_TRY(); - } - } - END_NATIVE - } - return result; -} diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index ed1eefb1..9b12fa3f 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -721,7 +721,6 @@ extern void Timestamp_initialize(void); extern void Oid_initialize(void); extern void AclId_initialize(void); extern void ErrorData_initialize(void); -extern void LargeObject_initialize(void); extern void String_initialize(void); extern void byte_array_initialize(void); @@ -766,7 +765,6 @@ void Type_initialize(void) Oid_initialize(); AclId_initialize(); ErrorData_initialize(); - LargeObject_initialize(); byte_array_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/LargeObject.h b/pljava-so/src/main/include/pljava/type/LargeObject.h deleted file mode 100644 index bdeaf510..00000000 --- a/pljava-so/src/main/include/pljava/type/LargeObject.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html - * - * @author Thomas Hallgren - */ -#ifndef __pljava_LargeObject_h -#define __pljava_LargeObject_h - -#include "pljava/type/Type.h" -#ifdef __cplusplus -extern "C" { -#endif - -#include - -/***************************************************************** - * The LargeObject java class extends the NativeStruct and provides JNI - * access to some of the attributes of the LargeObjectDesc structure. - * - * @author Thomas Hallgren - *****************************************************************/ - -/* - * Create the org.postgresql.pljava.LargeObject instance - */ -extern jobject LargeObject_create(LargeObjectDesc* lo); - -#ifdef __cplusplus -} -#endif -#endif diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/LargeObject.java b/pljava/src/main/java/org/postgresql/pljava/internal/LargeObject.java deleted file mode 100644 index 4725ae19..00000000 --- a/pljava/src/main/java/org/postgresql/pljava/internal/LargeObject.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2004-2016 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: - * Tada AB - * Chapman Flack - */ -package org.postgresql.pljava.internal; - -import java.sql.SQLException; - -/** - * The LargeObject correspons to the internal PostgreSQL - * LargeObjectDesc. - * - * @author Thomas Hallgren - */ -public class LargeObject extends JavaWrapper -{ - /** - * Write mode flag to be passed to {@link #create} and {@link #open} - */ - public static final int INV_WRITE = 0x00020000; - - /** - * Read mode flag to be passed to {@link #create} and {@link #open} - */ - public static final int INV_READ = 0x00040000; - - /** - * Flag returned by {@link #create} and {@link #open} - */ - public static final int IFS_RDLOCK = (1 << 0); - - /** - * Flag returned by {@link #create} and {@link #open} - */ - public static final int IFS_WRLOCK = (1 << 1); - - /** - * Flag to be passed to {@link #seek} denoting that the - * offset parameter should be treated as an absolute address. - */ - public static final int SEEK_SET = 0; - - /** - * Flag to be passed to {@link #seek} denoting that the - * offset parameter should be treated relative to the current - * address. - */ - public static final int SEEK_CUR = 1; - - /** - * Flag to be passed to {@link #seek} denoting that the - * offset parameter should be treated relative to the end - * of the data. - */ - public static final int SEEK_END = 2; - - LargeObject(long nativePointer) - { - super(nativePointer); - } - - /** - * Creates a LargeObject handle and returns the {@link Oid} of - * that handle. - * @param flags Flags to use for creation. - * @return A Oid that can be used in a call to {@link #open(Oid, int)} - * or {@link #drop(Oid)}. - * @throws SQLException - */ - public static Oid create(int flags) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _create(flags); - } - } - - public static LargeObject open(Oid lobjId, int flags) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _open(lobjId, flags); - } - } - - public void close() - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - _close(this.getNativePointer()); - } - } - - public static int drop(Oid lobjId) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _drop(lobjId); - } - } - - public Oid getId() - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _getId(this.getNativePointer()); - } - } - - public long length() - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _length(this.getNativePointer()); - } - } - - public long seek(long offset, int whence) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _seek(this.getNativePointer(), offset, whence); - } - } - - public long tell() - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _tell(this.getNativePointer()); - } - } - - public void truncate(long offset) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - _truncate(this.getNativePointer(), offset); - } - } - - public int read(byte[] buf) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _read(this.getNativePointer(), buf); - } - } - - public int write(byte[] buf) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _write(this.getNativePointer(), buf); - } - } - - private static native Oid _create(int flags) - throws SQLException; - - private static native int _drop(Oid lobjId) - throws SQLException; - - private static native LargeObject _open(Oid lobjId, int flags) - throws SQLException; - - private static native void _close(long pointer) - throws SQLException; - - private static native Oid _getId(long pointer) - throws SQLException; - - private static native long _length(long pointer) - throws SQLException; - - private static native long _seek(long pointer, long offset, int whence) - throws SQLException; - - private static native long _tell(long pointer) - throws SQLException; - - private static native void _truncate(long pointer, long offset) - throws SQLException; - - private static native int _read(long pointer, byte[] buf) - throws SQLException; - - private static native int _write(long pointer, byte[] buf) - throws SQLException; -} From a6fe9b9abefc76b595b334563ed720e1c957160b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 3 Jun 2017 12:17:04 -0400 Subject: [PATCH 0035/1087] Fulfill old todo for setting a libjvm default. By passing -Dpljava.libjvmdefault=... on the mvn command line, the downstream maintainer of a packaging system for a platform where the standard Java install location is known can build a package where the default for pljava.libjvm_location is usually right. --- pljava-so/pom.xml | 30 +++++++++++++++++++++++++ pljava-so/src/main/c/Backend.c | 6 ++++- src/site/markdown/install/install.md.vm | 20 ++++++++++++----- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 1bdd2deb..b47ed345 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -230,6 +230,36 @@ + + + + haslibjvmdefault + + + pljava.libjvmdefault + + + + + + com.github.maven-nar + nar-maven-plugin + + + + PLJAVA_LIBJVMDEFAULT=${pljava.libjvmdefault} + + + + + + + diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index deba0225..8113909f 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1320,7 +1320,11 @@ static void registerGUCOptions(void) NULL, /* extended description */ &libjvmlocation, #if PG_VERSION_NUM >= 80400 - "libjvm", + #ifdef PLJAVA_LIBJVMDEFAULT + CppAsString2(PLJAVA_LIBJVMDEFAULT), + #else + "libjvm", + #endif #endif PGC_SUSET, #if PG_VERSION_NUM >= 80400 diff --git a/src/site/markdown/install/install.md.vm b/src/site/markdown/install/install.md.vm index 2588db5e..abd52806 100644 --- a/src/site/markdown/install/install.md.vm +++ b/src/site/markdown/install/install.md.vm @@ -348,14 +348,22 @@ If the `pljava-\${project.version}.jar` file is placed in the default location (typically a `pljava` subdirectory of the PostgreSQL "share" directory), then `pljava.classpath` will not need to be set. +The self-extracting jar file produced by the build, assuming it is run with +adequate permission, will extract the files into appropriate locations +determined by querying `pg_config` on the target system. If that system +may have more than one PostgreSQL installation and you wish to control +which one the files get installed into, pass the full path to that +installation's `pg_config` executable with `-Dpgconfig=` on that +`java -jar ...` command line. (In more difficult cases, each category +of file location, such as `pgconfig.sharedir`, can be separately overridden +on the command line.) + **If you are a distribution maintainer** packaging PL/Java for a certain platform, and you know or control that platform's conventions for where -the Java `libjvm` should be found, or where PostgreSQL extension files -(architecture-dependent and -independent) should go, please build your -PL/Java package with those locations as the defaults for the corresponding -PL/Java variables, and with the built files in those locations. - -_Todo: add maven build options usable by distro spinners to set those defaults._ +the Java `libjvm` should be found, please supply that full path on the `mvn` +command line with `-Dpljava.libjvmdefault=` to make it the default for +`pljava.libjvm_location`, so users on that platform can see a working PL/Java +with no need to set that variable in the usual case. $h3 PostgreSQL superuser with access as user running postgres From adda57a062c8d533bbe0c4434317e0db13272a3e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 13 May 2017 18:53:44 -0400 Subject: [PATCH 0036/1087] Select PG version to build for on mvn command line. Allow selection of which PostgreSQL version to build for, on a build host with several versions installed, by passing the full path of that version's pg_config executable on with -Dpgsql.pgconfig= on the mvn command line. This was always possible anyway, by simply prefixing the mvn command with PATH=desired-version-bin-dir:$PATH, but downstream package maintainers may have more confidence in this option, and it parallels the -Dpgconfig= option for the self-extracting jar at installation time. Also remove a bit of dead code from pom.xml, from back when less distinction between build and installation time was being made. --- pljava-so/build.xml | 13 +++++++------ pljava-so/pom.xml | 27 ++++++++++++--------------- src/site/markdown/build/build.md | 6 ++++++ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/pljava-so/build.xml b/pljava-so/build.xml index 8780b69f..d57b4554 100644 --- a/pljava-so/build.xml +++ b/pljava-so/build.xml @@ -38,22 +38,23 @@ - + - + - + - + - + - + diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index b47ed345..54146d90 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -260,6 +260,18 @@ + + + needsdefaultpgconfig + + + !pgsql.pgconfig + + + + pg_config + + @@ -287,21 +299,6 @@ - diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index 1ff13f30..93402a50 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -130,6 +130,12 @@ to [try out PL/Java in PostgreSQL][inst]. [inst]: ../install/install.html +### PostgreSQL version to build against + +If several versions of PostgreSQL are installed on the build host, select +the one to be built for by adding the full path of its `pg_config` executable +with `-Dpgsql.pgconfig=` on the `mvn` command line. + ### I know PostgreSQL and PGXS. Explain Maven! [Maven][mvn] is a widely used tool for building and maintaining projects in From 950f70fbfe81ce898843cc6bafc988674906a958 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Jun 2017 21:14:21 -0400 Subject: [PATCH 0037/1087] Add a Packaging PL/Java page. Collect in one page the variety of tips and considerations when building PL/Java for a packaging system. --- src/site/markdown/build/build.md | 2 + src/site/markdown/build/package.md | 223 ++++++++++++++++++++++++ src/site/markdown/install/install.md.vm | 4 +- src/site/site.xml | 2 + 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 src/site/markdown/build/package.md diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index 93402a50..931df1e5 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -75,6 +75,8 @@ Please review any of the following that apply to your situation: [a linker runpath](runpath.html) can help * Building on a platform that [requires PostgreSQL libraries at link time](linkpglibs.html) +* Building if you are + [making a package for a software distribution](package.html) ## Obtaining PL/Java sources diff --git a/src/site/markdown/build/package.md b/src/site/markdown/build/package.md new file mode 100644 index 00000000..29fc528f --- /dev/null +++ b/src/site/markdown/build/package.md @@ -0,0 +1,223 @@ +# Packaging PL/Java for a software distribution + +If you are responsible for creating or maintaining a PL/Java package +for a particular software distribution, thank you. PL/Java reaches a +larger community of potential users thanks to your efforts. To minimize +frustration for your users and yourself, please consider these notes +when building your package. + +## What is the default `pljava.libjvm_location`? + +Users of a PL/Java source build nearly always have to set the PostgreSQL +variable `pljava.libjvm_location` before the extension will work, because +there is too much variation in where Java gets installed across systems +for PL/Java to supply a useful default. + +When you package for a particular platform, you may have the advantage of +knowing the conventional location for Java on that platform, and you can +improve the PL/Java setup experience for users of your package by adding +`-Dpljava.libjvmdefault=...` on the `mvn` command line when building, +where the `...` is the path to the JVM library shared object where it +would be by default on your target platform. See [here][locatejvm] to find +the exact file this should refer to. + +[locatejvm]: ../install/locatejvm.html + +## What kind of a package is this? + +Your package may be for a distribution that has formal guidelines for how +to package software in certain categories, such as "Java applications", +"Java libraries", or "PostgreSQL extensions". That may force a judgment +as to which of those categories PL/Java falls in. + +### If possible: it's a PostgreSQL extension + +PL/Java has the most in common with other PostgreSQL extensions (even though +it happens to involve Java). It has nearly nothing in common with "Java +applications" or "Java libraries" as those are commonly understood. It is +neither something that can run on its own as an application, nor a library +that would be placed on the classpath in the usual fashion for other Java code +to use. It is only usable within PostgreSQL under its own distinctive rules. + +### Not recommended: Java application or library guidelines + +Formal guidelines developed for packaging Java applications or libraries +are likely to impose requirements that have no value or are inappropriate +in PL/Java's case. The necessary locations for PL/Java's components are +determined by the rules of the PostgreSQL extension mechanism, not other +platform rules that may apply to conventional Java libraries, for example. + +A packaging system's built-in treatment for Java libraries may even actively +break PL/Java. One packaging system apparently unpacks and repacks +jar files in a way that adds spurious entries. It has that "feature" to +address an obscure issue involving +[multilib conflicts for packages that use GCJ][repack], which doesn't apply +to PL/Java at all, and when the repacking silently added spurious entries +to PL/Java's self-installer jar, it took time to track down why unexpected +things were getting installed. + +If you are using that packaging system, please be sure to follow the step +shown in that link to disable the repacking of jars. + +[repack]: https://www.redhat.com/archives/fedora-devel-java-list/2008-September/msg00040.html + +### An exception: `pljava-api` + +The one part of PL/Java that could, if desired, be handled in the manner of +Java libraries is `pljava-api`. This single jar file is needed on the classpath +when compiling Java code that will be loaded into PL/Java in the database. +It is _not_ needed at the time that code will _run_. That means it could be +appropriate to treat `pljava-api` as a separate `-devel` package, if your +packaging guidelines encourage such a distinction. In that case, you would +exclude the `pljava-api` jar file from the main package, and produce a `-devel` +package that provides it. + +A `-devel` package providing `pljava-api` might appropriately follow +java library packaging guidelines to ensure it appears on a developer's +classpath when compiling code to run in PL/Java. Ideally, such a package +would also place the `pljava-api` artifact into the local Maven repository, +if any. (PL/Java's [hello world example](../use/hello.html) illustrates using +Maven to build code for use in PL/Java, which assumes the local Maven repo +contains `pljava-api`.) + +To build `pljava-api` in isolation. simply change into the `pljava-api` +subdirectory and run `mvn clean install`. It builds quickly and independently +of the rest of the project, with fewer build dependencies than the project +as a whole. + +That `mvn clean install` also puts the `pljava-api` artifact into the local +Maven repository on the build host. A `-devel` package will ideally put the +same artifact into the local Maven repository of the installation target. +(While the other subprojects in a PL/Java full build also create artifacts +in the build host's local Maven repository, they can be ignored; `pljava-api` +is the useful one to have in an installation target host's repository.) + +### PL/Java API javadocs + +The PL/Java build does not automatically build javadocs. Those that go with +`pljava-api` can be easily generated by changing into the `pljava-api` +subdirectory and running `mvn javadoc:javadoc` to build them, then collecting +the `apidocs` subtree from `target/site`. They can be included in the same +package as `pljava-api` or in a separate javadoc package, as your guidelines +may require. + +### An `examples` package? + +A full PL/Java build also builds `pljava-examples`, which typically will also +be installed into PostgreSQL's _SHAREDIR_`/pljava` directory. If the packaging +guidelines encourage placing examples into a separate package, this jar file +can be excluded from the main package and delivered in a separate one. +The examples can be built in isolation by changing into the `pljava-examples` +subdirectory and running `mvn clean package`, as long as the `pljava-api` has +been built first and installed into the build host's local Maven repository. + +Note that many of the examples do double duty as tests, as described in +_confirming the build_ below. + +## Scripting the build + +Options on the `mvn` command line may be useful in the scripted build for +the package. + +`-Dpljava.libjvmdefault=`_path/to/jvm-shared-object_ +: As suggested earlier, please use this option to build a useful default +into PL/Java for the `pljava.libjvm_location` PostgreSQL variable, users +of your package will not need to set that variable before +`CREATE EXTENSION pljava` works. + +`-Dpgsql.pgconfig=`_path/to/pg\_config_ +: If the build host may have more than one PostgreSQL version installed, +a package specific to one version can be built by using this option to point +to the `pg_config` command in the `bin` directory of the needed PostgreSQL +version. (The same effect was always possible by making sure that `bin` +directory was at the front of the `PATH` when invoking `mvn`, but this option +on the `mvn` command makes it more explicit.) + +`-Pwnosign` +: If `gcc` is used in the build, adding this option on the `mvn` command line +will suppress a large number of nuisance warnings `gcc` would otherwise +produce, keeping the build log manageable in case any actual problem needs to +be found. (The nuisance warnings can be traced upstream to PostgreSQL headers, +and have been [deemed not pressing to fix there][notgoingthere], as upstream +also builds with those warnings suppressed.) + +`-Dnar.cores=1` +: This option also helps to make the build log more intelligible in case any +problem needs to be found, by avoiding the interleaving of messages from +simultaneous compilations. PL/Java builds quickly enough that there is little +cost in wall time to do the compiles on one core. + +[notgoingthere]: https://www.postgresql.org/message-id/19039.1496353285%40sss.pgh.pa.us + +## Confirming the build + +A full build also produces a `pljava-examples` jar, containing many examples +that double as tests. Many of these are run from the deployment descriptor +if the PL/Java extension is created in a PostgreSQL instance and then the +examples jar is loaded with `install_jar` passing `deploy => true`, which +should complete with no warnings. + +Some tests involving Unicode are skipped if the `server_encoding` is not +`utf-8`, so it is best to run them in a server instance created with that +encoding. + +## Packaging the built items + +The end product of a full PL/Java source build is a jar file that functions as +a self-extracting installer when run by `java -jar`. It contains the files that +are necessary on a target system to use PL/Java with PostgreSQL, including +those needed to support `ALTER EXTENSION UPGRADE`. + +It also contains the `pljava-api` jar, needed for developing Java code to use +in a database with PL/Java, and the `pljava-examples` jar. As discussed above, +these may be omitted from a base package and supplied separately, if packaging +guidelines require. + +The self-extracting jar consults `pg_config` at the time of extraction to +determine where the files should be installed. + +Given this jar as the result of the build, there are three broad approaches +to constructing a package: + +| Approach | Pro | Con | +----|----|----| +Capture self-extracting jar in package, deliver to target system and run it as a post-install action | Simple, closest to a vanilla PL/Java build. | May not integrate well into package manager for querying, uninstalling, or verifying installed files; probably leaves the self-installing jar on the target system, where it serves no further purpose. | +Run self-extracting jar at packaging time, and package the files it installs | Still simple, captures the knowledge embedded in the installer jar; integrates better with package managers needing the list of files installed. | Slightly less space-efficient? | +Ignore the self-extracting jar and hardcode a list of the individual files resulting from the build to be captured in the package | ? | Brittle, must reverse-engineer what pljava-packaging and installer jar are doing, from release to release. Possible to miss things. | + +The sweet spot seems to be the middle approach. + +When running the self-extractor, its output can be captured for a list of the +files installed. (As always, parsing that output can get complicated if the +pathnames have newlines or other tricky characters. The names of PL/Java-related +files in the jar do not, so there is no problem as long as no tricky characters +are in the PostgreSQL installation directory names reported by `pg_config`.) + +A package specific to a PostgreSQL version can pass +`-Dpgconfig=`_path/to/pg\_config_ to Java when running the self-extractor, +to ensure the locations are obtained from the desired version's `pg_config`. +(This is the extraction-time analog of the `-Dpgsql.pgconfig` that can be +passed to `mvn` at build time.) + +If necessary to satisfy some packaging guideline, individual locations +obtained from `pg_config` can be overridden with more specific options +such as `-Dpgconfig.sharedir=...` as described in the [install][] guide. +Or, the packaging script might simply move files, or edit the paths they +will have on the target system. + +In addition to the files named in the self-extractor's output, additional +files could be included in the package (if guidelines require the README +or COPYRIGHT, for example). As discussed above, the `pljava-api` jar could +be filtered from the list if it will be delivered in a separate `-devel` +package, and the same could be done for `pljava-examples`. + +[install]: ../install/install.html + +## Late-breaking packaging news and tips + +A [Packaging Tips][tips] page on the PL/Java wiki will be created for +information on packaging issues that may be reported and resolved between +released updates to this documentation. Please be sure to check there for +any packaging issue not covered here. + +[tips]: https://github.com/tada/pljava/wiki/Packaging-tips diff --git a/src/site/markdown/install/install.md.vm b/src/site/markdown/install/install.md.vm index abd52806..aa3ba21b 100644 --- a/src/site/markdown/install/install.md.vm +++ b/src/site/markdown/install/install.md.vm @@ -363,7 +363,9 @@ platform, and you know or control that platform's conventions for where the Java `libjvm` should be found, please supply that full path on the `mvn` command line with `-Dpljava.libjvmdefault=` to make it the default for `pljava.libjvm_location`, so users on that platform can see a working PL/Java -with no need to set that variable in the usual case. +with no need to set that variable in the usual case. That tip and more are +covered +in [packaging PL/Java for a software distribution](../build/package.html). $h3 PostgreSQL superuser with access as user running postgres diff --git a/src/site/site.xml b/src/site/site.xml index 4bac4893..2b3b0d62 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -26,6 +26,8 @@ href='install/install.html'/> + Date: Sun, 18 Jun 2017 21:21:49 -0400 Subject: [PATCH 0038/1087] Add a note about patching. Ask for an issue to be opened if some need during packaging can't be met with existing options and seems to require a patch. --- src/site/markdown/build/package.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/site/markdown/build/package.md b/src/site/markdown/build/package.md index 29fc528f..342d98b2 100644 --- a/src/site/markdown/build/package.md +++ b/src/site/markdown/build/package.md @@ -149,6 +149,15 @@ cost in wall time to do the compiles on one core. [notgoingthere]: https://www.postgresql.org/message-id/19039.1496353285%40sss.pgh.pa.us +## Patching PL/Java + +If your packaging project requires patches to PL/Java, and not simply the +passing of options at build or packaging time as described on this page, +please [open an issue][issue] so that the possibility of addressing your +need without patching can be discussed. + +[issue]: https://github.com/tada/pljava/issues + ## Confirming the build A full build also produces a `pljava-examples` jar, containing many examples From a9e68d53fa4e27cbb5f3ceb5773765490c9fffcc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Jun 2017 22:38:44 -0400 Subject: [PATCH 0039/1087] Document Oid_forSqlType in coercion.md. The idea for coercion.md is to document in one place the various machinations involving PostgreSQL and Java/JDBC types and values as they are passed around, but it had not mentioned the mapping from JDBC SQL types to PostgreSQL type OIDs that is found in Oid_forSqlType. --- src/site/markdown/develop/coercion.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/site/markdown/develop/coercion.md b/src/site/markdown/develop/coercion.md index 2df11976..2674e260 100644 --- a/src/site/markdown/develop/coercion.md +++ b/src/site/markdown/develop/coercion.md @@ -152,6 +152,11 @@ if necessary, so these rules are equivalent to the first two in the "parameters and return values" case. However, see "additional JDBC coercions" below. +JDBC defines some `setObject` and `setNull` methods on `PreparedStatement` +that must be passed a `java.sql.Types` constant. The JDBC constant will be +mapped to a PostgreSQL type OID through a fixed mapping coded in +`Oid_forSqlType`. + ### Values read or written through the JDBC `ResultSet` interface This case includes not only results from SPI queries made in Java, but From 1f2dd7f7b4224994954aafc8d8af0eb9b707f40e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 19 Jun 2017 12:36:54 -0400 Subject: [PATCH 0040/1087] Draft release notes for 1.5.1-BETA1. --- src/site/markdown/releasenotes.md.vm | 133 ++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 8d8f77b2..f644d95c 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -9,6 +9,130 @@ #set($pgffeat = 'http://pgfoundry.org/tracker/?func=detail&atid=337&group_id=1000038&aid=') #set($ghbug = 'https://github.com/tada/pljava/issues/') +$h2 PL/Java 1.5.1 + +This release chiefly adds support for PostgreSQL 9.6 and 10 (beta), +and plays more nicely with `pg_upgrade`. With PostgreSQL 9.6 support +comes the ability to declare functions +`PARALLEL { UNSAFE | RESTRICTED | SAFE }`, and with PG 10 support, +transition tables are available to triggers. + +$h3 Security + +1.5.1 removes the code at issue in [CVE-2016-0768][], which pertained to +PostgreSQL large objects, but had never been documented or exposed as API. + +This is not expected to break any existing code at all, based on further +review showing the code in question had also been simply broken, since 2006, +with no reported issues in that time. That discovery would support an argument +for downgrading the severity of the reported vulnerability, but with no need +to keep that code around, it is more satisfying to remove it entirely. + +Developers wishing to manipulate large objects in PL/Java are able to do so +using the SPI JDBC interface and the large-object SQL functions already +available in every PostgreSQL version PL/Java currently supports. + +$h3 Changes + +$h4 PostgreSQL 9.6 and parallel query + +A function in PL/Java can now be [annotated][apianno] +`parallel={UNSAFE | RESTRICTED | SAFE}`, with `UNSAFE` the default. +A new [user guide section][ugparqry] explains the possibilities and +tradeoffs. (Good cases for marking a PL/Java function `SAFE` may be +rare, as pushing such a function into multiple background processes +will require them all to start JVMs. But if a practical application +arises, PostgreSQL's `parallel_setup_cost` can be tuned to help the +planner make good plans.) + +Although `RESTRICTED` and `SAFE` Java functions work in simple tests, +there has been no exhaustive audit of the code to ensure that PL/Java's +internal workings never violate the behavior constraints on such functions. +The support should be considered experimental, and could be a fruitful +area for beta testing. + +[ugparqry]: use/parallel.html + +$h4 Tuple counts widened to 64 bits with PostgreSQL 9.6 + +To accommodate the possibility of more than two billion tuples in a single +operation, the SPI implementation of the JDBC `Statement` interface now +provides the JDK 8-specified `executeLargeBatch` and `getLargeUpdateCount` +methods defined to return `long` counts. The original `executeBatch` and +`getUpdateCount` methods remain but, obviously, cannot return counts that +exceed `INT_MAX`. In case the count is too large, `getUpdateCount` will throw +an `ArithmeticException`; `executeBatch` will store `SUCCESS_NO_INFO` for +any statement in the batch that affected too many tuples to report. + +For now, a `ResultSetProvider` cannot be used to return more than `INT_MAX` +tuples, but will check that condition and throw an error to ensure predictable +behavior. + +$h4 `pg_upgrade` + +PL/Java should be upgraded to 1.5.1 in a database cluster, before that +cluster is binary-upgraded to a newer PostgreSQL version using `pg_upgrade`. +A new [Upgrading][upgrading] installation-guide section centralizes information +on both upgrading PL/Java in a database, and upgrading a database with PL/Java +in it. + +[upgrading]: install/upgrade.html + +$h4 PostgreSQL 10 and trigger transition tables + +A trigger [annotation][apianno] can now specify `tableOld="`_name1_`"` or +`tableNew="`_name2_`"`, or both, and the PL/Java function servicing the +trigger can do SPI JDBC queries and see the transition table(s) under the +given name(s). The [triggers example code][extrig] has been extended with +a demonstration. + +[extrig]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java + +$h4 Conveniences for downstream package maintainers + +The `mvn` command to build PL/Java will now accept an option to provide +a useful default for `pljava.libjvm_location`, when building a package for +a particular software environment where the likely path to Java is known. + +The `mvn` command will also accept an option to specify, by the path to +the `pg_config` executable, the PostgreSQL version to build against, in +case multiple versions exist on the build host. This was already possible +by manipulating `PATH` ahead of running `mvn`, but the option makes it more +explicit. + +A new [packaging section][packaging] in the build guide documents those +and a number of considerations for making a PL/Java package. + +[packaging]: build/package.html + +$h3 Bugs fixed + +* [Add support for PostgreSQL 9.6](${ghbug}108) +* [Clarify documentation of ResultSetProvider](${ghbug}115) +* [`pg_upgrade` (upgrade failure from 9.5 to 9.6)](${ghbug}117) + +$h3 Updated PostgreSQL APIs tracked + +* `heap_form_tuple` +* 64-bit `SPI_processed` +* 64-bit `Portal->portalPos` +* 64-bit `FuncCallContext.call_cntr` +* 64-bit `SPITupleTable.alloced` and `.free` +* `IsBackgroundWorker` +* `IsBinaryUpgrade` +* `SPI_register_trigger_data` + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + $h2 PL/Java 1.5.0 This, the first PL/Java numbered release since 1.4.3 in 2011, combines @@ -570,7 +694,14 @@ Periods in PL/Java's development have been sponsored by EnterpriseDB. In the 1.5.0 release cycle, multiple iterations of testing effort have been generously contributed by Kilobe Systems and by Pegasystems, Inc. -$h2 Earlier releases +## From this point on, the entries were reconstructed from old notes at the +## same time as the 1.5.0 notes were drafted, and they use a finer level of +## heading. So restore the 'real' values of the heading variables from here +## to the end of the file. +#set($h2 = '##') +#set($h3 = '###') +#set($h4 = '####') +#set($h5 = '#####') $h3 PL/Java 1.4.3 (15 September 2011) From 3c2de251c250048f3d99a374c181e2c0e1eaca80 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 19 Jun 2017 22:48:52 -0400 Subject: [PATCH 0041/1087] Apache Ant 1.10.0 needs Java 8; avoid it. The Ant 1.9.x series works back to Java 5, which is good enough for PL/Java 1.5.x to support Java 6 and later. --- pljava-packaging/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 6f0a5c4a..f4fdec2e 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -117,7 +117,7 @@ org.apache.ant ant - [1.8.3,) + [1.8.3,1.10.0) From 0d80a3ee9860ac561ae231cd7d16dee43a48172e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 19 Jun 2017 22:13:04 -0400 Subject: [PATCH 0042/1087] Honor client_min_messages too at PL/Java startup. Fix the easier complaint in issue #125; if Java will be summarily discarding log messages finer than a level derived from PostgreSQL's settings just once at PL/Java startup, at least derive the level from the finer of log_min_messages and client_min_messages, rather than from log_min_messages alone. --- .../pljava/internal/ELogHandler.java | 19 +++++++++++++++---- src/site/markdown/examples/examples.md.vm | 2 +- src/site/markdown/releasenotes.md.vm | 11 +++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ELogHandler.java b/pljava/src/main/java/org/postgresql/pljava/internal/ELogHandler.java index dbdad36c..91ab8ee3 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ELogHandler.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ELogHandler.java @@ -149,11 +149,15 @@ public static Level getPgLevel() // during JVM initialization and before the native method is // registered). // - String pgLevel = Backend.getConfigOption("log_min_messages"); - Level level = Level.ALL; - if(pgLevel != null) + String[] options = { "log_min_messages", "client_min_messages" }; + Level finestLevel = null; + for ( String option : options ) { + String pgLevel = Backend.getConfigOption(option); + if ( null == pgLevel ) + continue; pgLevel = pgLevel.toLowerCase().trim(); + Level level = null; if(pgLevel.equals("panic") || pgLevel.equals("fatal")) level = Level.OFF; else if(pgLevel.equals("error")) @@ -170,8 +174,15 @@ else if(pgLevel.equals("debug2")) level = Level.FINER; else if(pgLevel.equals("debug3") || pgLevel.equals("debug4") || pgLevel.equals("debug5")) level = Level.FINEST; + if ( null == level ) + continue; + if ( null == finestLevel + || finestLevel.intValue() > level.intValue() ) + finestLevel = level; } - return level; + if ( null == finestLevel ) + finestLevel = Level.ALL; + return finestLevel; } // Private method to configure an ELogHandler diff --git a/src/site/markdown/examples/examples.md.vm b/src/site/markdown/examples/examples.md.vm index 7f9de068..e0fd6a4a 100644 --- a/src/site/markdown/examples/examples.md.vm +++ b/src/site/markdown/examples/examples.md.vm @@ -58,7 +58,7 @@ descriptor goes farther, and calls several functions provided as test cases, so this use of `install_jar` may take a few extra seconds and produce some test-related output. (To see _successful_ test-related output, be sure to set `client_min_messages` to a level at least as detailed as `INFO` before -invoking `install_jar`.) +the session's first use of PL/Java.) $h2 Trying the examples diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index f644d95c..f24cbec0 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -88,6 +88,16 @@ a demonstration. [extrig]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +$h4 Logging from Java + +The way the Java logging system has historically been plumbed to PostgreSQL's, +as described in [issue 125](${ghbug}125), can be perplexing both because it +is unaffected by later changes to the PostgreSQL settings after PL/Java is +loaded in the session, and because it has honored only `log_min_messages` +and ignored `client_min_messages`. The second part is easy to fix, so in +1.5.1 the threshold where Java discards messages on the fast path is +determined by the finer of `log_min_messages` and `client_min_messages`. + $h4 Conveniences for downstream package maintainers The `mvn` command to build PL/Java will now accept an option to provide @@ -110,6 +120,7 @@ $h3 Bugs fixed * [Add support for PostgreSQL 9.6](${ghbug}108) * [Clarify documentation of ResultSetProvider](${ghbug}115) * [`pg_upgrade` (upgrade failure from 9.5 to 9.6)](${ghbug}117) +* [Java logging should honor `client_min_messages` too](${ghbug}125) $h3 Updated PostgreSQL APIs tracked From efd6af52dc1353fe4e9ef485703132f5543186f3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 19 Jun 2017 23:55:08 -0400 Subject: [PATCH 0043/1087] Poke migration-management versions for 1.5.1-BETA1. -packaging/build.xml already makes an update .sql from 1.5.0, the last released version. No change needed before this release. A change to past practice in updating the SchemaVariant list in InstallHelper.java. There has been no change to the schema, so may as well minimize changes by continuing to call it the 1.5.0 schema (the first full release to feature it), and adding 1_5_1_BETA1 only as an alias to it. --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 19def09b..71818a44 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -469,6 +469,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_5_1_BETA1 = REL_1_5_0; static final SchemaVariant REL_1_5_0_BETA3 = REL_1_5_0; static final SchemaVariant REL_1_5_0_BETA2 = REL_1_5_0_BETA3; static final SchemaVariant REL_1_5_0_BETA1 = REL_1_5_0_BETA2; From ed3e142bb7a0f29569c1786271e52720cebe5f1a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 20 Jun 2017 00:34:32 -0400 Subject: [PATCH 0044/1087] Add control file in preparation for next release. Now that 1.5.1-BETA1 is released, the next release should include an extension SQL file allowing upgrade from 1.5.1-BETA1. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 759693fb..dac3d42e 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Sat, 27 Jan 2018 19:35:27 -0500 Subject: [PATCH 0045/1087] Adapt to PG 10 changes to SPI_push et al. (#134) In postgres/postgres@1833f1a1c3b0e12b3ea40d49bf11898eedae5248, SPI_push and SPI_pop are removed. They are still defined (as no-ops), so there is no need yet to rototill PL/Java adding version checks around uses of those. They can be removed later once PG 10 is PL/Java's back-compatibility horizon. However, the same change altered some other SPI behavior too. In particular, seven SPI functions now require SPI to be connected that did not before. PL/Java only uses one of those, SPI_modifytuple, and only in one place. Ensure SPI is connected before it is called. --- pljava-so/src/main/c/Function.c | 22 ++++++++++++++++--- pljava-so/src/main/c/type/Relation.c | 10 +++++++++ .../main/include/pljava/type/TriggerData.h | 3 +++ .../postgresql/pljava/internal/Relation.java | 13 ++++++++--- .../pljava/internal/TriggerData.java | 6 +++++ 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 6086a5f1..475dfc78 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -846,6 +846,12 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) currentInvocation->function = self; #if PG_VERSION_NUM >= 100000 currentInvocation->triggerData = td; + /* Also starting in PG 10, Invocation_assertConnect must be called before + * the getTriggerReturnTuple below. That could be done right here, but at + * the risk of changing the memory context from what the invoked trigger + * function expects. More cautiously, add the assertConnect later, after + * the trigger function has returned. + */ #endif Type_invoke(self->func.nonudt.returnType, self->clazz, self->func.nonudt.method, &arg, fcinfo); @@ -854,11 +860,21 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) ret = 0; else { - /* A new Tuple may or may not be created here. If it is, ensure that - * it is created in the upper SPI context. + /* A new Tuple may or may not be created here. Ensure that, if it is, + * it is created in the upper context (even after connecting SPI, should + * that be necessary). + */ +#if PG_VERSION_NUM >= 100000 + /* If the invoked trigger function didn't connect SPI, do that here + * (getTriggerReturnTuple now needs it), but there will be no need to + * register the triggerData in that case. */ + currentInvocation->triggerData = NULL; + Invocation_assertConnect(); +#endif MemoryContext currCtx = Invocation_switchToUpperContext(); - ret = PointerGetDatum(TriggerData_getTriggerReturnTuple(arg.l, &fcinfo->isnull)); + ret = PointerGetDatum( + TriggerData_getTriggerReturnTuple(arg.l, &fcinfo->isnull)); /* Triggers are not allowed to set the fcinfo->isnull, even when * they return null. diff --git a/pljava-so/src/main/c/type/Relation.c b/pljava-so/src/main/c/type/Relation.c index 8d39f9c4..6f156db9 100644 --- a/pljava-so/src/main/c/type/Relation.c +++ b/pljava-so/src/main/c/type/Relation.c @@ -168,6 +168,16 @@ Java_org_postgresql_pljava_internal_Relation__1getTupleDesc(JNIEnv* env, jclass * Class: org_postgresql_pljava_internal_Relation * Method: _modifyTuple * Signature: (JJ[I[Ljava/lang/Object;)Lorg/postgresql/internal/pljava/Tuple; + * + * Note: starting with PostgreSQL 10, SPI_modifytuple must be run with SPI + * 'connected'. However, the caller likely wants a result living in a memory + * context longer-lived than SPI's. (At present, the only calls of this method + * originate in Function_invokeTrigger, which does switchToUpperContext() just + * for that reason.) Blindly adding Invocation_assertConnect() here would alter + * the behavior of subsequent palloc()s (not just in SPI_modifytuple, but also + * in, e.g., Tuple_create). So, given there's only one caller, let it be the + * caller's responsibility to ensure SPI is connected AND that a suitable + * memory context is selected for the result the caller wants. */ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_Relation__1modifyTuple(JNIEnv* env, jclass clazz, jlong _this, jlong _tuple, jintArray _indexes, jobjectArray _values) diff --git a/pljava-so/src/main/include/pljava/type/TriggerData.h b/pljava-so/src/main/include/pljava/type/TriggerData.h index d2b791af..b1f8676b 100644 --- a/pljava-so/src/main/include/pljava/type/TriggerData.h +++ b/pljava-so/src/main/include/pljava/type/TriggerData.h @@ -31,6 +31,9 @@ extern jobject TriggerData_create(TriggerData* triggerData); /* * Obtains the returned Tuple after trigger has been processed. + * Note: starting with PG 10, it is the caller's responsibility to ensure SPI + * is connected (and that a longer-lived memory context than SPI's is selected, + * if the caller wants the result to survive SPI_finish). */ extern HeapTuple TriggerData_getTriggerReturnTuple(jobject jtd, bool* wasNull); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java index 816fb396..397eb6e7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java @@ -67,9 +67,16 @@ public TupleDesc getTupleDesc() } /** - * Creates a new Tuple by substituting new values for selected columns - * copying the columns of the original Tuple at other positions. The - * original Tuple is not modified.
+ * Creates a new {@code Tuple} by substituting new values for selected + * columns copying the columns of the original {@code Tuple} at other + * positions. The original {@code Tuple} is not modified. + *

+ * Note: starting with PostgreSQL 10, this method can fail if SPI is not + * connected; it is the caller's responsibility in PG 10 and up + * to ensure that SPI is connected and that a longer-lived memory + * context than SPI's has been selected, if the caller wants the result of + * this call to survive {@code SPI_finish}. + * * @param original The tuple that serves as the source. * @param fieldNumbers An array of one based indexes denoting the positions that * are to receive modified values. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java index 986b065c..da1cc8bf 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java @@ -90,6 +90,12 @@ public ResultSet getOld() throws SQLException * new and returns the native pointer of new tuple. This * method is called automatically by the trigger handler and should not * be called in any other way. + *

+ * Note: starting with PostgreSQL 10, this method can fail if SPI is not + * connected; it is the caller's responsibility in PG 10 and up + * to ensure that SPI is connected and that a longer-lived memory + * context than SPI's has been selected, if the caller wants the result of + * this call to survive {@code SPI_finish}. * * @return The modified tuple, or if no modifications have been made, the * original tuple. From 73175b14bd66b64122145c6035f6a6af28644ca4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 27 Jan 2018 19:37:05 -0500 Subject: [PATCH 0046/1087] Use SPI_result_code_string more. While thinking of SPI and its error codes, report those as strings rather than numeric codes in a couple more places. --- pljava-so/src/main/c/Invocation.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 685a21b6..1af40e57 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -88,13 +88,15 @@ void Invocation_assertConnect(void) { rslt = SPI_connect(); if ( SPI_OK_CONNECT != rslt ) - elog(ERROR, "SPI_register_trigger_data returned %d", rslt); + elog(ERROR, "SPI_register_trigger_data returned %s", + SPI_result_code_string(rslt)); #if PG_VERSION_NUM >= 100000 if ( NULL != currentInvocation->triggerData ) { rslt = SPI_register_trigger_data(currentInvocation->triggerData); if ( SPI_OK_TD_REGISTER != rslt ) - elog(WARNING, "SPI_register_trigger_data returned %d", rslt); + elog(WARNING, "SPI_register_trigger_data returned %s", + SPI_result_code_string(rslt)); } #endif currentInvocation->hasConnected = true; From 7a8897a282eccea36a096342e8074ca74186fd35 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 Jan 2018 21:33:11 -0500 Subject: [PATCH 0047/1087] Document (lack of) issue #134 regression test. An obvious place to put it, in examples/Triggers.java, won't really help, because those all run with SPI already connected and would not catch the bug. This is more of a note to the future, to make such a test when there is more of a regression test infrastructure to run tests from outside PL/Java itself. --- .../postgresql/pljava/example/annotation/Triggers.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index 62915962..fcd8eed2 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -63,6 +63,15 @@ requires = { "transition triggers", "foobar2_42" }, install = "UPDATE javatest.foobar_2 SET value = 43 WHERE value = 42" ) + /* + * Note for another day: this would seem an excellent place to add a + * regression test for github issue #134 (make sure invocations of a + * trigger do not fail with SPI_ERROR_UNCONNECTED). However, any test + * here that runs from the deployment descriptor will be running when + * SPI is already connected, so a regression would not be caught. + * A proper test for it will have to wait for a proper testing harness + * invoking tests from outside PL/Java itself. + */ }) public class Triggers { From aa5419e852e29444ae7a8300ec4513e6dc252b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mich=C3=A1lek?= Date: Mon, 12 Mar 2018 20:38:40 +0100 Subject: [PATCH 0048/1087] Annotations doesn't support CREATE CONSTRAINT TRIGGER and clause FROM schema.table #138 --- .../postgresql/pljava/annotation/Trigger.java | 24 ++++++ .../pljava/sqlgen/DDRProcessor.java | 79 +++++++++++++------ 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java index f8aae638..150e50b8 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java @@ -48,6 +48,11 @@ */ enum Called { BEFORE, AFTER, INSTEAD_OF }; + /** + * Constraint trigger options. NOT_CONSTRAINT, IMMEDIATE (Is PostgreSQL default for deferrable), INITIALLY_IMMEDIATE, INITIALLY_DEFERRED + */ + enum Constraint { NOT_CONSTRAINT, IMMEDIATE, INITIALLY_IMMEDIATE, INITIALLY_DEFERRED }; + /** * Types of event that can occasion a trigger. */ @@ -64,11 +69,30 @@ enum Scope { STATEMENT, ROW }; */ String[] arguments() default {}; + /** + * Constraint trigger options. + */ + Constraint constraint() default Constraint.NOT_CONSTRAINT; + /** * The event(s) that will trigger the call. */ Event[] events(); + /** + * The schema name of another table referenced by the constraint.
+ * This option is used for foreign-key constraints and is not recommended for general use.
+ * This can only be specified for constraint triggers. + */ + String fromSchema() default ""; + + /** + * The (possibly schema-qualified) name of another table referenced by the constraint.
+ * This option is used for foreign-key constraints and is not recommended for general use.
+ * This can only be specified for constraint triggers. + */ + String fromTable() default ""; + /** * Name of the trigger. If not set, the name will * be generated. diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 17e2872c..f1030912 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1075,33 +1075,39 @@ class TriggerImpl extends AbstractAnnotationImpl implements Trigger, Snippet, Commentable { - public String[] arguments() { return _arguments; } - public Event[] events() { return _events; } - public String name() { return _name; } - public String schema() { return _schema; } - public String table() { return _table; } - public Scope scope() { return _scope; } - public Called called() { return _called; } - public String when() { return _when; } - public String[] columns() { return _columns; } - public String tableOld() { return _tableOld; } - public String tableNew() { return _tableNew; } + public String[] arguments() { return _arguments; } + public Constraint constraint() { return _constraint; } + public Event[] events() { return _events; } + public String fromSchema() { return _fromSchema; } + public String fromTable() { return _fromTable; } + public String name() { return _name; } + public String schema() { return _schema; } + public String table() { return _table; } + public Scope scope() { return _scope; } + public Called called() { return _called; } + public String when() { return _when; } + public String[] columns() { return _columns; } + public String tableOld() { return _tableOld; } + public String tableNew() { return _tableNew; } public String[] provides() { return new String[0]; } public String[] requires() { return new String[0]; } /* Trigger is a Snippet but doesn't directly participate in tsort */ - public String[] _arguments; - public Event[] _events; - public String _name; - public String _schema; - public String _table; - public Scope _scope; - public Called _called; - public String _when; - public String[] _columns; - public String _tableOld; - public String _tableNew; + public String[] _arguments; + public Constraint _constraint; + public Event[] _events; + public String _fromSchema; + public String _fromTable; + public String _name; + public String _schema; + public String _table; + public Scope _scope; + public Called _called; + public String _when; + public String[] _columns; + public String _tableOld; + public String _tableNew; FunctionImpl func; AnnotationMirror origin; @@ -1181,7 +1187,11 @@ else if ( Called.INSTEAD_OF.equals( _called) ) public String[] deployStrings() { StringBuilder sb = new StringBuilder(); - sb.append( "CREATE TRIGGER ").append( name()).append( "\n\t"); + sb.append("CREATE "); + if (Constraint.NOT_CONSTRAINT != constraint()) { + sb.append("CONSTRAINT "); + } + sb.append("TRIGGER ").append(name()).append("\n\t"); switch ( called() ) { case BEFORE: sb.append( "BEFORE " ); break; @@ -1210,7 +1220,13 @@ public String[] deployStrings() if ( ! "".equals( schema()) ) sb.append( schema()).append( '.'); sb.append( table()); - if ( refOld || refNew ) + if (!"".equals(fromSchema()) || !"".equals(fromTable())) { + sb.append("\n\t FROM "); + if (!"".equals(fromSchema())) + sb.append(fromSchema()).append('.'); + sb.append(table()); + } + if ( refOld || refNew ) { sb.append( "\n\tREFERENCING"); if ( refOld ) @@ -1218,6 +1234,21 @@ public String[] deployStrings() if ( refNew ) sb.append( " NEW TABLE AS ").append( _tableNew); } + Constraint constraint = constraint(); + if (Constraint.NOT_CONSTRAINT != constraint) { + sb.append("\n\t"); + switch (constraint) { + case INITIALLY_IMMEDIATE: + sb.append("NOT DEFERRABLE INITIALLY IMMEDIATE"); + break; + case INITIALLY_DEFERRED: + sb.append("DEFERRABLE INITIALLY DEFERRED"); + break; + default: + sb.append("DEFERRABLE"); + break; + } + } sb.append( "\n\tFOR EACH "); sb.append( scope().toString()); if ( ! "".equals( _when) ) From 1a58efbcb5d6cea8716922305ff7bd0be4562d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mich=C3=A1lek?= Date: Tue, 13 Mar 2018 00:59:23 +0100 Subject: [PATCH 0049/1087] Fix #136 SPIConnection prepareStatement doesn't recognize all parameters when SQL combines single and double quotes --- .../src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index c7a120a6..936c255c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -422,7 +422,7 @@ public String nativeSQL(String sql, int[] paramCountRet) // if(inQuote == c) inQuote = 0; - else + else if(inQuote == 0) inQuote = c; break; From 4cda8c0743f8d853b09d09f07928970caa3a2db4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Mar 2018 23:03:53 -0400 Subject: [PATCH 0050/1087] Adjust per earlier discussion. Now has enum Constraint with only the constraint-related values; its actual omission is what indicates a non-constraint trigger. The name fromSchema is kept (it has no analog in the PG syntax), but fromTable is now simply from, to match the PG syntax. Semantic errors are checked. Also some whitespace tweaks. An example/test is still needed. --- .../postgresql/pljava/annotation/Trigger.java | 55 ++++++---- .../pljava/sqlgen/DDRProcessor.java | 100 ++++++++++++------ 2 files changed, 104 insertions(+), 51 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java index 150e50b8..ab037ca6 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java @@ -48,10 +48,13 @@ */ enum Called { BEFORE, AFTER, INSTEAD_OF }; - /** - * Constraint trigger options. NOT_CONSTRAINT, IMMEDIATE (Is PostgreSQL default for deferrable), INITIALLY_IMMEDIATE, INITIALLY_DEFERRED - */ - enum Constraint { NOT_CONSTRAINT, IMMEDIATE, INITIALLY_IMMEDIATE, INITIALLY_DEFERRED }; + /** + * Deferrability (only applies to constraint triggers). + * {@code NOT_DEFERRABLE} if the constraint trigger is not deferrable + * at all; otherwise, the trigger is deferrable and this value indicates + * whether initially deferred or not. + */ + enum Constraint { NOT_DEFERRABLE, INITIALLY_IMMEDIATE, INITIALLY_DEFERRED }; /** * Types of event that can occasion a trigger. @@ -69,29 +72,39 @@ enum Scope { STATEMENT, ROW }; */ String[] arguments() default {}; - /** - * Constraint trigger options. - */ - Constraint constraint() default Constraint.NOT_CONSTRAINT; + /** + * Only for a constraint trigger, whether it is deferrable and, if so, + * initially immediate or deferred. To create a constraint trigger that is + * not deferrable, this attribute must be explicitly given with the value + * {@code NOT_DEFERRABLE}; leaving it to default is not the same. When this + * attribute is not specified, a normal trigger, not a constraint trigger, + * is created. + *

+ * A constraint trigger must have {@code called=AFTER} and + * {@code scope=ROW}. + */ + Constraint constraint() default Constraint.NOT_DEFERRABLE; /** * The event(s) that will trigger the call. */ Event[] events(); - /** - * The schema name of another table referenced by the constraint.
- * This option is used for foreign-key constraints and is not recommended for general use.
- * This can only be specified for constraint triggers. - */ - String fromSchema() default ""; - - /** - * The (possibly schema-qualified) name of another table referenced by the constraint.
- * This option is used for foreign-key constraints and is not recommended for general use.
- * This can only be specified for constraint triggers. - */ - String fromTable() default ""; + /** + * The name of another table referenced by the constraint. + * This option is used for foreign-key constraints and is not recommended + * for general use. This can only be specified for constraint triggers. + * If the name should be schema-qualified, use + * {@link #fromSchema() fromSchema} to specify the schema. + */ + String from() default ""; + + /** + * The schema containing another table referenced by the constraint. + * This can only be specified for constraint triggers, and only to name the + * schema for a table named with {@link #from() from}. + */ + String fromSchema() default ""; /** * Name of the trigger. If not set, the name will diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index f1030912..cbc5549a 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1076,10 +1076,10 @@ class TriggerImpl implements Trigger, Snippet, Commentable { public String[] arguments() { return _arguments; } - public Constraint constraint() { return _constraint; } - public Event[] events() { return _events; } - public String fromSchema() { return _fromSchema; } - public String fromTable() { return _fromTable; } + public Constraint constraint() { return _constraint; } + public Event[] events() { return _events; } + public String fromSchema() { return _fromSchema; } + public String from() { return _from; } public String name() { return _name; } public String schema() { return _schema; } public String table() { return _table; } @@ -1097,8 +1097,8 @@ class TriggerImpl public String[] _arguments; public Constraint _constraint; public Event[] _events; - public String _fromSchema; - public String _fromTable; + public String _fromSchema; + public String _from; public String _name; public String _schema; public String _table; @@ -1114,6 +1114,22 @@ class TriggerImpl boolean refOld; boolean refNew; + boolean isConstraint = false; + + /* The only values of the Constraint enum are those applicable to + * constraint triggers. To determine whether this IS a constraint + * trigger or not, use the 'explicit' parameter to distinguish whether + * the 'constraint' attribute was or wasn't seen in the annotation. + */ + public void setConstraint( Object o, boolean explicit, Element e) + { + if ( explicit ) + { + isConstraint = true; + _constraint = Constraint.valueOf( + ((VariableElement)o).getSimpleName().toString()); + } + } TriggerImpl( FunctionImpl f, AnnotationMirror am) { @@ -1179,6 +1195,28 @@ else if ( Called.INSTEAD_OF.equals( _called) ) "Trigger must be callable on UPDATE or INSERT to reference NEW TABLE"); } + if ( isConstraint ) + { + if ( ! Called.AFTER.equals( _called) ) + msg( Kind.ERROR, func.func, origin, + "A constraint trigger must be an AFTER trigger"); + if ( ! Scope.ROW.equals( _scope) ) + msg( Kind.ERROR, func.func, origin, + "A constraint trigger must be FOR EACH ROW"); + if ( "".equals( _from) && ! "".equals( _fromSchema) ) + msg( Kind.ERROR, func.func, origin, + "To use fromSchema, specify a table name with from"); + } + else + { + if ( ! "".equals( _from) ) + msg( Kind.ERROR, func.func, origin, + "Only a constraint trigger can use 'from'"); + if ( ! "".equals( _fromSchema) ) + msg( Kind.ERROR, func.func, origin, + "Only a constraint trigger can use 'fromSchema'"); + } + if ( "".equals( _name) ) _name = TriggerNamer.synthesizeName( this); return false; @@ -1188,9 +1226,10 @@ public String[] deployStrings() { StringBuilder sb = new StringBuilder(); sb.append("CREATE "); - if (Constraint.NOT_CONSTRAINT != constraint()) { - sb.append("CONSTRAINT "); - } + if ( isConstraint ) + { + sb.append("CONSTRAINT "); + } sb.append("TRIGGER ").append(name()).append("\n\t"); switch ( called() ) { @@ -1220,12 +1259,28 @@ public String[] deployStrings() if ( ! "".equals( schema()) ) sb.append( schema()).append( '.'); sb.append( table()); - if (!"".equals(fromSchema()) || !"".equals(fromTable())) { - sb.append("\n\t FROM "); - if (!"".equals(fromSchema())) - sb.append(fromSchema()).append('.'); - sb.append(table()); - } + if ( ! "".equals( from()) ) + { + sb.append("\n\tFROM "); + if ( ! "".equals( fromSchema()) ) + sb.append( fromSchema()).append( '.'); + sb.append( from()); + } + if ( isConstraint ) { + sb.append("\n\t"); + switch ( _constraint ) + { + case NOT_DEFERRABLE: + sb.append("NOT DEFERRABLE"); + break; + case INITIALLY_IMMEDIATE: + sb.append("DEFERRABLE INITIALLY IMMEDIATE"); + break; + case INITIALLY_DEFERRED: + sb.append("DEFERRABLE INITIALLY DEFERRED"); + break; + } + } if ( refOld || refNew ) { sb.append( "\n\tREFERENCING"); @@ -1234,21 +1289,6 @@ public String[] deployStrings() if ( refNew ) sb.append( " NEW TABLE AS ").append( _tableNew); } - Constraint constraint = constraint(); - if (Constraint.NOT_CONSTRAINT != constraint) { - sb.append("\n\t"); - switch (constraint) { - case INITIALLY_IMMEDIATE: - sb.append("NOT DEFERRABLE INITIALLY IMMEDIATE"); - break; - case INITIALLY_DEFERRED: - sb.append("DEFERRABLE INITIALLY DEFERRED"); - break; - default: - sb.append("DEFERRABLE"); - break; - } - } sb.append( "\n\tFOR EACH "); sb.append( scope().toString()); if ( ! "".equals( _when) ) From 2aaef63903cf0abff9ae362f345a3065e60f9749 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 13 Mar 2018 23:40:32 -0400 Subject: [PATCH 0051/1087] Enable row triggers to suppress operations. Adds to TriggerData the suppress() method suggested in issue #142. --- .../org/postgresql/pljava/TriggerData.java | 12 +++++++++-- .../pljava/example/annotation/Triggers.java | 5 ++++- .../pljava/internal/TriggerData.java | 21 ++++++++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/TriggerData.java b/pljava-api/src/main/java/org/postgresql/pljava/TriggerData.java index 126a4f50..90dd87b1 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/TriggerData.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/TriggerData.java @@ -29,7 +29,7 @@ public interface TriggerData { /** * Returns the ResultSet that represents the new row. This ResultSet will - * be null for delete triggers and for triggers that was fired for + * be null for delete triggers and for triggers that were fired for * statement.
The returned set will be updateable and positioned on a * valid row. When the trigger call returns, the trigger manager will see * the changes that has been made to this row and construct a new tuple @@ -44,7 +44,7 @@ public interface TriggerData /** * Returns the ResultSet that represents the old row. This ResultSet will - * be null for insert triggers and for triggers that was fired for + * be null for insert triggers and for triggers that were fired for * statement.
The returned set will be read-only and positioned on a * valid row. * @@ -152,4 +152,12 @@ public interface TriggerData * if the contained native buffer has gone stale. */ boolean isFiredByUpdate() throws SQLException; + + /** + * Advise PostgreSQL to silently suppress the operation on this row. + * + * @throws SQLException + * if called in an {@code AFTER} or a {@code STATEMENT} trigger + */ + void suppress() throws SQLException; } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index fcd8eed2..8a4819d6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -92,7 +92,10 @@ public class Triggers public static void insertUsername(TriggerData td) throws SQLException { - ResultSet nrs = td.getNew(); + ResultSet nrs = td.getNew(); // expect NPE in a DELETE/STATEMENT trigger + String col2asString = nrs.getString(2); + if ( "43".equals(col2asString) ) + td.suppress(); nrs.updateString( "username", "bob"); } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java index da1cc8bf..5619df7f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java @@ -9,6 +9,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import org.postgresql.pljava.TriggerException; import org.postgresql.pljava.jdbc.TriggerResultSet; /** @@ -23,12 +24,25 @@ public class TriggerData extends JavaWrapper implements org.postgresql.pljava.Tr private TriggerResultSet m_new = null; private Tuple m_newTuple; private Tuple m_triggerTuple; + private boolean m_suppress = false; TriggerData(long pointer) { super(pointer); } + @Override + public void suppress() throws SQLException + { + if ( isFiredForStatement() ) + throw new TriggerException(this, + "Attempt to suppress operation in a STATEMENT trigger"); + if ( isFiredAfter() ) + throw new TriggerException(this, + "Attempt to suppress operation in an AFTER trigger"); + m_suppress = true; + } + /** * Returns the ResultSet that represents the new row. This ResultSet will * be null for delete triggers and for triggers that was fired for @@ -102,10 +116,11 @@ public ResultSet getOld() throws SQLException */ public long getTriggerReturnTuple() throws SQLException { - if(this.isFiredForStatement() || this.isFiredAfter()) + if(this.isFiredForStatement() || this.isFiredAfter() || m_suppress) // - // Only triggers fired before each row can have a return - // value. + // Only triggers fired for each row, and not AFTER, can have a + // nonzero return value. If such a trigger does return zero, it + // tells PostgreSQL to silently suppress the row operation involved. // return 0; From b40aa730e24950b4e438d65cfc762eab6203d512 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 14 Mar 2018 00:28:03 -0400 Subject: [PATCH 0052/1087] Add a constraint-trigger example. A more interesting test would be to actually try to insert 44, and verify that the exception happens. (Oh, for the chance to use pgTAP...). --- .../pljava/example/annotation/Triggers.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index fcd8eed2..2dc1085c 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -16,6 +16,7 @@ import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLIntegrityConstraintViolationException; import java.sql.Statement; import org.postgresql.pljava.TriggerData; @@ -24,6 +25,7 @@ import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.Trigger; import static org.postgresql.pljava.annotation.Trigger.Called.*; +import static org.postgresql.pljava.annotation.Trigger.Constraint.*; import static org.postgresql.pljava.annotation.Trigger.Event.*; import static org.postgresql.pljava.annotation.Trigger.Scope.*; import static org.postgresql.pljava.annotation.Function.Security.*; @@ -54,6 +56,17 @@ " || current_setting('pljava.implementors'), true) " + " end" ), + @SQLAction(provides="postgresql_constrainttriggers", install= +" select case " + +" when 90100 <= cast(current_setting('server_version_num') as integer) " + +" then set_config('pljava.implementors', 'postgresql_constrainttriggers,' " + +" || current_setting('pljava.implementors'), true) " + +" end" + ), + @SQLAction( + requires = "constraint triggers", + install = "INSERT INTO javatest.foobar_2(value) VALUES (45)" + ), @SQLAction( requires = "foobar triggers", provides = "foobar2_42", @@ -128,4 +141,28 @@ public static void examineRows(TriggerData td) logMessage( "WARNING", String.format( "trigger transition table oval %d nval %d", oval, nval)); } + + /** + * Throw exception if value to be inserted is 44. + * Constraint triggers first became available in PostgreSQL 9.1. + */ + @Function( + implementor = "postgresql_constrainttriggers", + requires = "foobar tables", + provides = "constraint triggers", + schema = "javatest", + security = INVOKER, + triggers = { + @Trigger(called = AFTER, table = "foobar_2", events = { INSERT }, + scope = ROW, constraint = NOT_DEFERRABLE ) + }) + + public static void disallow44(TriggerData td) + throws SQLException + { + ResultSet nrs = td.getNew(); + if ( 44 == nrs.getInt( "value") ) + throw new SQLIntegrityConstraintViolationException( + "44 shall not be inserted", "23000"); + } } From 20bacb2e612205a9b0f5eb445cc562c59d0578f2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 31 Jan 2018 22:41:05 -0500 Subject: [PATCH 0053/1087] Add mvn knobs for debug or optimize of native code. Add and document -Dso.debug= and -Dso.optimize= to control the building of the native code, and also document -Dmaven.compiler.debug= (which controls building of the Java portion, and already worked). --- pljava-so/pom.xml | 2 ++ src/site/markdown/build/build.md | 1 + src/site/markdown/build/debugopt.md | 37 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/site/markdown/build/debugopt.md diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 54146d90..5b935acd 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -361,6 +361,8 @@ ${basedir}/src/main/include/ ${basedir}/target/nar/javah-include/ + ${so.debug} + ${so.optimize} diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index 931df1e5..36817563 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -77,6 +77,7 @@ Please review any of the following that apply to your situation: [requires PostgreSQL libraries at link time](linkpglibs.html) * Building if you are [making a package for a software distribution](package.html) +* Building [with debugging or optimization options](debugopt.html) ## Obtaining PL/Java sources diff --git a/src/site/markdown/build/debugopt.md b/src/site/markdown/build/debugopt.md new file mode 100644 index 00000000..bb3af83b --- /dev/null +++ b/src/site/markdown/build/debugopt.md @@ -0,0 +1,37 @@ +# Building for debugging or with optimization + +Some options can be given on the `mvn` command line to control whether +debugging information is included in the built files, or omitted to save +space at the cost of making use of a debugger less practical. It is also +possible to tailor how aggressively the C compiler will optimize the +native-code portion of PL/Java. + +## Debugging information in the Java portion of PL/Java + +`-Dmaven.compiler.debug=` with a value of `true` or `false` can be given on +the `mvn` command line. If `true`, debugging information is included so a +runtime debugger (`jdb`, Eclipse, etc.) can see local variables, source lines, +etc. The default is `true`. + +## Debugging information in the native portion of PL/Java + +`-Dso.debug=` with a value of `true` or `false` on the `mvn` command line +will control whether debugging information is included in PL/Java's native +code shared object. This is most useful when developing PL/Java itself, or, +perhaps, troubleshooting a low-level issue. The default is `false`. + +Although it is not required, debugging of PL/Java's native code can be +more comfortable when the PostgreSQL server in use was also configured +and built with `--enable-debug`. + +## Compiler optimization in the native portion of PL/Java + +`-Dso.optimize=` can be given on the `mvn` command line, with a value +chosen from `none`, `size`, `speed`, `minimal`, `full`, `aggressive`, +`extreme`, or `unsafe`. Depending on the compiler, these settings may +not all be distinct optimization levels. The default is `none`. + +Because `none` has long been the default, PL/Java has not seen extensive +testing at higher optimization levels, which should, therefore, be considered +experimental. Before reporting an issue, please make sure it is reproducible +with no optimization. From e2b5e5eacb9efe53a7956bb39823b380aba47310 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 31 Jan 2018 23:57:20 -0500 Subject: [PATCH 0054/1087] Really: Apache Ant 1.10.0 needs Java 8. Avoid it. --- pljava-ant/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index ac3014bd..1d11aadc 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -13,7 +13,7 @@ org.apache.ant ant - [1.7.0,) + [1.7.0,1.10.0) From 8c15d04e01f87d675da44b0c9c7f331851117f74 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Mar 2018 20:09:58 -0400 Subject: [PATCH 0055/1087] Update release notes for 1.5.1-BETA2. --- src/site/markdown/releasenotes.md.vm | 37 +++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index f24cbec0..d8513133 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -11,7 +11,7 @@ $h2 PL/Java 1.5.1 -This release chiefly adds support for PostgreSQL 9.6 and 10 (beta), +This release chiefly adds support for PostgreSQL 9.6 and 10, and plays more nicely with `pg_upgrade`. With PostgreSQL 9.6 support comes the ability to declare functions `PARALLEL { UNSAFE | RESTRICTED | SAFE }`, and with PG 10 support, @@ -78,6 +78,31 @@ in it. [upgrading]: install/upgrade.html +$h4 Suppressing row operations from triggers + +In PostgreSQL, a `BEFORE ROW` trigger is able to allow the proposed row +operation, allow it with modified values, or silently suppress the operation +for that row. Way back in PL/Java 1.1.0, the way to produce the 'suppress' +outcome was for the trigger method to throw an exception. Since PL/Java 1.2.0, +however, an exception thrown in a trigger method is used to signal an error +to PostgreSQL, and there has not been a way to suppress the row operation. + +The `TriggerData` interface now has a [`suppress`][tgsuppress] method that +the trigger can invoke to suppress the operation for the row. + +[tgsuppress]: pljava-api/apidocs/index.html?org/postgresql/pljava/TriggerData.html#suppress() + +$h4 Constraint triggers + +New attributes in the `@Trigger` annotation allow the SQL generator to +create constraint triggers (a type of trigger that can be created with SQL +since PostgreSQL 9.1). Such triggers will be delivered by the PL/Java runtime +(to indicate that a constraint would be violated, a constraint trigger +method should throw an informative exception). However, the trigger method +will have access, through the `TriggerData` interface, only to the properties +common to ordinary triggers; methods on that interface to retrieve properties +specific to constraint triggers have not been added for this release. + $h4 PostgreSQL 10 and trigger transition tables A trigger [annotation][apianno] can now specify `tableOld="`_name1_`"` or @@ -117,6 +142,15 @@ and a number of considerations for making a PL/Java package. $h3 Bugs fixed +$h4 Since 1.5.1-BETA1 + +* [PostgreSQL 10: SPI_modifytuple failed with SPI_ERROR_UNCONNECTED](${ghbug}134) +* [SPIConnection prepareStatement doesn't recognize all parameters](${ghbug}136) +* [Annotations don't support CREATE CONSTRAINT TRIGGER](${ghbug}138) +* [Ordinary (non-constraint) trigger has no way to suppress operation](${ghbug}142) + +$h4 In 1.5.1-BETA1 + * [Add support for PostgreSQL 9.6](${ghbug}108) * [Clarify documentation of ResultSetProvider](${ghbug}115) * [`pg_upgrade` (upgrade failure from 9.5 to 9.6)](${ghbug}117) @@ -132,6 +166,7 @@ $h3 Updated PostgreSQL APIs tracked * `IsBackgroundWorker` * `IsBinaryUpgrade` * `SPI_register_trigger_data` +* `SPI` without `SPI_push`/`SPI_pop` $h2 Earlier releases From 6db6d070197a8f445c6c1c06f0f5b34a98b06147 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 22 Mar 2018 19:59:54 -0400 Subject: [PATCH 0056/1087] Do casts in ResultSetHandle by column definition list A function declared to return RECORD or SETOF RECORD is required by SQL to be followed by a column definition list in any query using it. If the function returns SETOF RECORD using ResultSetHandle, there is a possibility that one or more corresponding columns in the definition list and the ResultSet offered by ResultSetHandle differ in type. Let the type be cast automatically, just as it would be when using ResultSetProvider and storing each value into the output ResultSet. Also convert the SetOfRecordTest example to an annotation-style example and add an SQLAction that tests it. Addresses issue #146. --- .../{ => annotation}/SetOfRecordTest.java | 25 +++++++++++- .../main/resources/deployment/examples.ddr | 5 --- .../pljava/jdbc/SingleRowWriter.java | 17 +++++--- .../org/postgresql/pljava/test/Tester.java | 40 ------------------- 4 files changed, 34 insertions(+), 53 deletions(-) rename pljava-examples/src/main/java/org/postgresql/pljava/example/{ => annotation}/SetOfRecordTest.java (55%) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/SetOfRecordTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java similarity index 55% rename from pljava-examples/src/main/java/org/postgresql/pljava/example/SetOfRecordTest.java rename to pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java index be64c5dd..583d4f07 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/SetOfRecordTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -8,8 +8,9 @@ * * Contributors: * Tada AB + * Chapman Flack */ -package org.postgresql.pljava.example; +package org.postgresql.pljava.example.annotation; import java.sql.Connection; import java.sql.DriverManager; @@ -18,13 +19,33 @@ import java.sql.SQLException; import org.postgresql.pljava.ResultSetHandle; +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; /** * Example implementing the {@code ResultSetHandle} interface, to return * the {@link ResultSet} from any SQL {@code SELECT} query passed as a string * to the {@link #executeSelect executeSelect} function. */ +@SQLAction(requires="selecttorecords fn", install= +" SELECT " + +" CASE WHEN r IS DISTINCT FROM ROW('Foo'::varchar, 1::integer, 1.5::float, " + +" 23.67::decimal(8,2), '2005-06-01'::date, '20:56'::time, " + +" '2006-02-04 23:55:10'::timestamp) " + +" THEN javatest.logmessage('WARNING', 'SetOfRecordTest not ok') " + +" ELSE javatest.logmessage('INFO', 'SetOfRecordTest ok') " + +" END " + +" FROM " + +" javatest.executeselecttorecords( " + +" 'select ''Foo'', 1, 1.5, 23.67, ''2005-06-01'', ''20:56''::time, " + +" ''2006-02-04 23:55:10''') " + +" AS r(t_varchar varchar, t_integer integer, t_float float, " + +" t_decimal decimal(8,2), t_date date, t_time time, t_timestamp timestamp)" +) public class SetOfRecordTest implements ResultSetHandle { + + @Function(schema="javatest", name="executeselecttorecords", + provides="selecttorecords fn") public static ResultSetHandle executeSelect(String selectSQL) throws SQLException { return new SetOfRecordTest(selectSQL); diff --git a/pljava-examples/src/main/resources/deployment/examples.ddr b/pljava-examples/src/main/resources/deployment/examples.ddr index 26853e09..38ad0449 100644 --- a/pljava-examples/src/main/resources/deployment/examples.ddr +++ b/pljava-examples/src/main/resources/deployment/examples.ddr @@ -343,11 +343,6 @@ SQLActions[ ] = { AS 'org.postgresql.pljava.example.ResultSetTest.executeSelect' LANGUAGE java; - CREATE FUNCTION javatest.executeSelectToRecords(varchar) - RETURNS SETOF RECORD - AS 'org.postgresql.pljava.example.SetOfRecordTest.executeSelect' - LANGUAGE java; - CREATE FUNCTION javatest.countNulls(record) RETURNS int AS 'org.postgresql.pljava.example.Parameters.countNulls' diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java index 810dc2e7..9ffca389 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -106,8 +111,8 @@ public void copyRowFrom(ResultSet rs) throws SQLException { int top = m_values.length; - for(int idx = 0; idx < top; ++idx) - m_values[idx] = rs.getObject(idx+1); + for(int idx = 1; idx <= top; ++idx) + updateObject(idx, rs.getObject(idx)); } /** diff --git a/src/java/test/org/postgresql/pljava/test/Tester.java b/src/java/test/org/postgresql/pljava/test/Tester.java index c59d5a82..857a0d71 100644 --- a/src/java/test/org/postgresql/pljava/test/Tester.java +++ b/src/java/test/org/postgresql/pljava/test/Tester.java @@ -861,44 +861,4 @@ private void executeMetaDataFunction(Statement stmt, String functionCall) } } } - - public void testResultSet() throws SQLException - { - String sql; - Statement stmt = m_connection.createStatement(); - ResultSet rs = null; - - try - { - System.out.println("*** ResultSet test:"); - sql = "SELECT * FROM javatest.executeSelect(" - + "'select ''Foo'' as t_varchar, 1::integer as t_integer, " - + "1.5::float as t_float, 23.67::decimal(8,2) as t_decimal, " - + "''2005-06-01''::date as t_date, ''20:56''::time as t_time, " - + "''2006-02-04 23:55:10''::timestamp as t_timestamp')"; - - rs = stmt.executeQuery(sql); - System.out.println("SQL = " + sql); - System.out.println("results:"); - while(rs.next()) - { - System.out.println(rs.getString(1)); - } - rs.close(); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException e) - { - } - rs = null; - } - } - } } From 847375d73c21f3fb386f25b74f0e0fd5e0977561 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 22 Mar 2018 20:34:39 -0400 Subject: [PATCH 0057/1087] Document SETOF RECORD in ResultSet{Provider|Handle} Expand the API docs to explain the interaction with the calling query's column definition list when a function is declared to return SETOF RECORD. Except for the type-casting behavior change in ResultSetHandle for issue #146, this is simply an exercise in documenting what currently happens. --- .../postgresql/pljava/ResultSetHandle.java | 17 +++++++++++++++-- .../postgresql/pljava/ResultSetProvider.java | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetHandle.java b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetHandle.java index 89b2f935..8b4a6642 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetHandle.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -21,11 +21,24 @@ * of a {@link java.sql.ResultSet}. The primary motivation for this interface is * that an implementation that returns a ResultSet must be able to close the * connection and statement when no more rows are requested. - * + *

* A function returning a SET OF a complex type generated on the * fly (rather than obtained from a query) would return * {@link ResultSetProvider} instead. One returning a SET OF a * simple type should simply return an {@link java.util.Iterator}. + *

+ * In the case of a function declared to return {@code SETOF RECORD} rather than + * of a complex type known in advance, SQL requires any query using the function + * to include a column definition list. If the number of those columns does not + * match the number in the {@code ResultSet} returned here, only as many as the + * caller expects (in index order starting with 1) will be used; an exception is + * thrown if this result set has too few columns. If the types expected by the + * caller differ, values are converted as if retrieved one by one from this + * {@code ResultSet} and stored into the caller's with + * {@link ResultSet#updateObject(int, Object) updateObject}. + *

+ * A function that needs to know the names or types of the caller's expected + * columns should implement {@link ResultSetProvider}. * @author Thomas Hallgren */ public interface ResultSetHandle diff --git a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java index 63601c6d..e19d4db8 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2017 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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,6 +23,11 @@ * it should just return {@link ResultSetHandle} instead. Functions that * return SET OF a simple type should simply return an * {@link java.util.Iterator Iterator}. + *

+ * For a function declared to return {@code SETOF RECORD} rather than a specific + * complex type known in advance, the {@code receiver} argument to + * {@link #assignRowValues(ResultSet,int) assignRowValues} can be queried to + * learn the number, names, and types of columns expected by the caller. * @author Thomas Hallgren */ public interface ResultSetProvider @@ -31,11 +36,19 @@ public interface ResultSetProvider * This method is called once for each row that should be returned from * a procedure that returns a set of rows. The receiver * is a {@code SingleRowWriter} - * writer instance that is used for capturing the data for the row. + * instance that is used for capturing the data for the row. + *

+ * If the return type is {@code RECORD} rather than a specific complex type, + * SQL requires a column definition list to follow any use of the function + * in a query. The {@link ResultSet#getMetaData() ResultSetMetaData} + * of {@code receiver} can be used here to learn the number, names, + * and types of the columns expected by the caller. (It can also be used in + * the case of a specific complex type, but in that case the names and types + * are probably already known.) * @param receiver Receiver of values for the given row. * @param currentRow Row number, zero on the first call, incremented by one * on each subsequent call. - * @return true if a new row was provided, false + * @return {@code true} if a new row was provided, {@code false} * if not (end of data). * @throws SQLException */ From 2e9db0a8a824d6da3a7c5342e52627f70046ef5b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 8 Apr 2018 13:43:34 -0400 Subject: [PATCH 0058/1087] Remove some dead declarations. Of pljavaEntryFence(), and the x64 {intVal_1, intVal_2} members of Ptr2Long, all declared in pljava.h, there is no other use anywhere in the code. Get rid of them to reduce obstacles to understanding the implementation. --- pljava-so/src/main/include/pljava/pljava.h | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index cf1cd408..87836074 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -64,7 +64,6 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); * to the original value of Warn_restart) must be made. */ extern jlong mainThreadId; -extern bool pljavaEntryFence(JNIEnv* env); extern JNIEnv* currentJNIEnv; extern MemoryContext JavaMemoryContext; @@ -164,14 +163,6 @@ typedef union { void* ptrVal; jlong longVal; /* 64 bit quantity */ - struct - { - /* Used when calculating pointer hash in systems where - * a pointer is 64 bit - */ - uint32 intVal_1; - uint32 intVal_2; - } x64; } Ptr2Long; struct Invocation_; From 61cc0a9c5f02f81bf0a7fd6286ac834c2698efcf Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Apr 2018 21:31:41 -0400 Subject: [PATCH 0059/1087] Fix dangling pointer left as GUC boot value. The clearly-intended-but-forgotten 'static' here never caused functional trouble in practice, because the value was valid at the only time it was used. It could, however, cause unexpected bytes to appear in the boot_val column of the pg_settings view, which in turn appeared as character-encoding errors in tests of XML processing with schema_to_xml('pg_catalog', ...) as the source document for testing, which ended up being how this missing 'static' came to light. --- pljava-so/src/main/c/Backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 8113909f..57a3c87d 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1312,7 +1312,7 @@ static jint initializeJavaVM(JVMOptList *optList) static void registerGUCOptions(void) { - char pathbuf[MAXPGPATH]; + static char pathbuf[MAXPGPATH]; DefineCustomStringVariable( "pljava.libjvm_location", From 1c76bab23c01afe61a3d2d514917b4bf8c126fec Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 21 Apr 2018 10:35:15 -0400 Subject: [PATCH 0060/1087] Mention JDBC 4.0 to 4.2 types in Oid.c. These are still not given any mapping other than to InvalidOid, but at least they are mentioned (conditionally, for the JDBC 4.2 ones) in the switch statement now. --- pljava-so/src/main/c/type/Oid.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index fb3972ad..2c2690b4 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -103,13 +103,28 @@ Oid Oid_forSqlType(int sqlType) case java_sql_Types_DATALINK: typeId = TEXTOID; break; -/* case java_sql_Types_NULL: + case java_sql_Types_NULL: case java_sql_Types_OTHER: case java_sql_Types_JAVA_OBJECT: case java_sql_Types_DISTINCT: case java_sql_Types_STRUCT: case java_sql_Types_ARRAY: - case java_sql_Types_REF: */ + case java_sql_Types_REF: + + /* JDBC 4.0 - present in Java 6 and later, no need to conditionalize */ + case java_sql_Types_ROWID: + case java_sql_Types_NCHAR: + case java_sql_Types_NVARCHAR: + case java_sql_Types_LONGNVARCHAR: + case java_sql_Types_NCLOB: + case java_sql_Types_SQLXML: + + /* JDBC 4.2 - conditionalize until only Java 8 and later supported */ +#ifdef java_sql_Types_REF_CURSOR + case java_sql_Types_REF_CURSOR: + case java_sql_Types_TIME_WITH_TIMEZONE: + case java_sql_Types_TIMESTAMP_WITH_TIMEZONE: +#endif default: typeId = InvalidOid; /* Not yet mapped */ break; From f3b6577a2f2878d7ea902a184ea731a90c1c7a20 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 23 Apr 2018 20:15:42 -0400 Subject: [PATCH 0061/1087] Annotate Connection methods and add more comments. Add @Override to the methods in SPIConnection that are specified by the Connection API (to make it easier to spot the ones that aren't). For those added in JDBC 4.1, the annotation is commented out, as PL/Java 1.5 still strives to be buildable with Java 6. Once the back-compatibility horizon is Java 7 or later, those @Overrides can be uncommented. It would also be fair to say this has added annotations (or commented-out annotations) through JDBC 4.2, as it didn't add any new Connection methods. The PL/Java-specific and internal methods are now easier to pick out (they're the ones without @Override annotations), and have some more extensive comments about what they're doing there. Also moved one method to be nearer the stuff it pertains to. No code changes (except to add the specified generic signature on getTypeMap/setTypeMap). Indentation adjusted in a couple contiguous areas. --- .../postgresql/pljava/jdbc/SPIConnection.java | 232 ++++++++++++++---- 1 file changed, 180 insertions(+), 52 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 936c255c..3af3ac15 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -64,11 +64,6 @@ */ public class SPIConnection implements Connection { - /** - * A map from Java classes to java.sql.Types integers. - */ - private static final HashMap s_sqlType2Class = new HashMap(30); - /** * The version number of the currently executing PostgreSQL * server. @@ -80,6 +75,15 @@ public class SPIConnection implements Connection */ private Properties _clientInfo; + /** + * A map from Java classes to java.sql.Types integers. + *

+ * This map is only used by the (non-API) getTypeForClass method, + * which, in turn, is only used for + * {@link PreparedStatement#setObject(int,Object)}. + */ + private static final HashMap s_sqlType2Class = new HashMap(30); + static { addType(String.class, Types.VARCHAR); @@ -108,6 +112,25 @@ private static final void addType(Class clazz, int sqlType) s_sqlType2Class.put(clazz, new Integer(sqlType)); } + /** + * Map a {@code Class} to a {@link Types} integer, as used in + * (and only in) {@link PreparedStatement#setObject(int,Object)}. + */ + static int getTypeForClass(Class c) + { + if(c.isArray() && !c.equals(byte[].class)) + return Types.ARRAY; + + Integer sqt = (Integer)s_sqlType2Class.get(c); + if(sqt != null) + return sqt.intValue(); + + /* + * This is not a well known JDBC type. + */ + return Types.OTHER; + } + /** * Returns a default connection instance. It is the callers responsability * to close this instance. @@ -122,6 +145,7 @@ public static Connection getDefault() * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. Cursors are actually * closed when a function returns to SQL. */ + @Override public int getHoldability() { return ResultSet.CLOSE_CURSORS_AT_COMMIT; @@ -130,6 +154,7 @@ public int getHoldability() /** * Returns {@link Connection#TRANSACTION_READ_COMMITTED}. */ + @Override public int getTransactionIsolation() { return TRANSACTION_READ_COMMITTED; @@ -139,6 +164,7 @@ public int getTransactionIsolation() * Warnings are not yet supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public void clearWarnings() throws SQLException { @@ -148,6 +174,7 @@ public void clearWarnings() /** * This is a no-op. The default connection never closes. */ + @Override public void close() { } @@ -156,6 +183,7 @@ public void close() * It's not legal to do a commit within a call from SQL. * @throws SQLException indicating that this feature is not supported. */ + @Override public void commit() throws SQLException { @@ -166,6 +194,7 @@ public void commit() * It's not legal to do a rollback within a call from SQL. * @throws SQLException indicating that this feature is not supported. */ + @Override public void rollback() throws SQLException { @@ -176,6 +205,7 @@ public void rollback() * It is assumed that an SPI call is under transaction control. This method * will always return false. */ + @Override public boolean getAutoCommit() { return false; @@ -184,6 +214,7 @@ public boolean getAutoCommit() /** * Will always return false. */ + @Override public boolean isClosed() { return false; @@ -192,6 +223,7 @@ public boolean isClosed() /** * Returns false. The SPIConnection is not real-only. */ + @Override public boolean isReadOnly() { return false; @@ -201,6 +233,7 @@ public boolean isReadOnly() * Change of holdability is not supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public void setHoldability(int holdability) throws SQLException { @@ -211,6 +244,7 @@ public void setHoldability(int holdability) * Change of transaction isolation level is not supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public void setTransactionIsolation(int level) throws SQLException { @@ -222,6 +256,7 @@ public void setTransactionIsolation(int level) * that is not supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public void setAutoCommit(boolean autoCommit) throws SQLException { @@ -233,6 +268,7 @@ public void setAutoCommit(boolean autoCommit) * SPIConnection. Changing that is not supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public void setReadOnly(boolean readOnly) throws SQLException { @@ -242,6 +278,7 @@ public void setReadOnly(boolean readOnly) /** * Returns the database in which we are running. */ + @Override public String getCatalog() throws SQLException { @@ -258,6 +295,7 @@ public String getCatalog() * The catalog name cannot be set. * @throws SQLException indicating that this feature is not supported. */ + @Override public void setCatalog(String catalog) throws SQLException { @@ -275,6 +313,7 @@ public void setCatalog(String catalog) * @return an SPIDatabaseMetaData object for this * Connection object */ + @Override public DatabaseMetaData getMetaData() { return new SPIDatabaseMetaData(this); @@ -284,12 +323,14 @@ public DatabaseMetaData getMetaData() * Warnings are not yet supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public SQLWarning getWarnings() throws SQLException { throw new UnsupportedFeatureException("Connection.getWarnings"); } + @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { if(!(savepoint instanceof PgSavepoint)) @@ -300,6 +341,7 @@ public void releaseSavepoint(Savepoint savepoint) throws SQLException forgetSavepoint(sp); } + @Override public void rollback(Savepoint savepoint) throws SQLException { if(!(savepoint instanceof PgSavepoint)) @@ -314,6 +356,7 @@ public void rollback(Savepoint savepoint) throws SQLException /** * Creates a new instance of SPIStatement. */ + @Override public Statement createStatement() throws SQLException { @@ -332,6 +375,7 @@ public Statement createStatement() * resultSetConcurrencty differs from * {@link ResultSet#CONCUR_READ_ONLY}. */ + @Override public Statement createStatement( int resultSetType, int resultSetConcurrency) @@ -354,6 +398,7 @@ public Statement createStatement( * differs from {@link ResultSet#CONCUR_READ_ONLY}, or if the * resultSetHoldability differs from {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. */ + @Override public Statement createStatement( int resultSetType, int resultSetConcurrency, @@ -369,7 +414,8 @@ public Statement createStatement( /** * Returns null. Type map is not yet imlemented. */ - public Map getTypeMap() + @Override + public Map> getTypeMap() throws SQLException { return null; @@ -379,7 +425,8 @@ public Map getTypeMap() * Type map is not yet implemented. * @throws SQLException indicating that this feature is not supported. */ - public void setTypeMap(Map map) + @Override + public void setTypeMap(Map> map) throws SQLException { throw new UnsupportedOperationException("Type map is not yet implemented"); @@ -388,12 +435,17 @@ public void setTypeMap(Map map) /** * Parse the JDBC SQL into PostgreSQL. */ + @Override public String nativeSQL(String sql) throws SQLException { return this.nativeSQL(sql, null); } + /* + * An internal nativeSQL that returns a count of substitutable parameters + * detected, used in prepareStatement(). + */ public String nativeSQL(String sql, int[] paramCountRet) { StringBuffer buf = new StringBuffer(); @@ -459,6 +511,7 @@ else if(inQuote == 0) * Procedure calls are not yet implemented. * @throws SQLException indicating that this feature is not supported. */ + @Override public CallableStatement prepareCall(String sql) throws SQLException { throw new UnsupportedOperationException("Procedure calls are not yet implemented"); @@ -468,6 +521,7 @@ public CallableStatement prepareCall(String sql) throws SQLException * Procedure calls are not yet implemented. * @throws SQLException indicating that this feature is not supported. */ + @Override public CallableStatement prepareCall( String sql, int resultSetType, @@ -481,6 +535,7 @@ public CallableStatement prepareCall( * Procedure calls are not yet implemented. * @throws SQLException indicating that this feature is not supported. */ + @Override public CallableStatement prepareCall( String sql, int resultSetType, @@ -494,6 +549,7 @@ public CallableStatement prepareCall( /** * Creates a new instance of SPIPreparedStatement. */ + @Override public PreparedStatement prepareStatement(String sql) throws SQLException { @@ -511,6 +567,7 @@ public PreparedStatement prepareStatement(String sql) * Return of auto generated keys is not yet supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { @@ -525,6 +582,7 @@ public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) * ResultSet#TYPE_FORWARD_ONLY} or if the resultSetConcurrencty * differs from {@link ResultSet#CONCUR_READ_ONLY}. */ + @Override public PreparedStatement prepareStatement( String sql, int resultSetType, @@ -548,6 +606,7 @@ public PreparedStatement prepareStatement( * differs from {@link ResultSet#CONCUR_READ_ONLY}, or if the * resultSetHoldability differs from {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. */ + @Override public PreparedStatement prepareStatement( String sql, int resultSetType, @@ -565,6 +624,7 @@ public PreparedStatement prepareStatement( * Return of auto generated keys is not yet supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { @@ -575,39 +635,31 @@ public PreparedStatement prepareStatement(String sql, int[] columnIndexes) * Return of auto generated keys is not yet supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { throw new UnsupportedFeatureException("Auto generated key support not yet implemented"); } + @Override public Savepoint setSavepoint() throws SQLException { return this.rememberSavepoint(PgSavepoint.set("anonymous_savepoint")); } + @Override public Savepoint setSavepoint(String name) throws SQLException { return this.rememberSavepoint(PgSavepoint.set(name)); } - static int getTypeForClass(Class c) - { - if(c.isArray() && !c.equals(byte[].class)) - return Types.ARRAY; - - Integer sqt = (Integer)s_sqlType2Class.get(c); - if(sqt != null) - return sqt.intValue(); - - /* - * This is not a well known JDBC type. - */ - return Types.OTHER; - } - + /* + * An implementation factor of setSavepoint() to ensure that all such + * savepoints are released when the function returns. + */ private Savepoint rememberSavepoint(PgSavepoint sp) throws SQLException { @@ -622,6 +674,10 @@ private Savepoint rememberSavepoint(PgSavepoint sp) return sp; } + /* + * An implementation factor of releaseSavepoint() and rollback(Savepoint) + * undoing the registration done by rememberSavepoint(). + */ private static void forgetSavepoint(PgSavepoint sp) throws SQLException { @@ -630,7 +686,12 @@ private static void forgetSavepoint(PgSavepoint sp) invocation.setSavepoint(null); } - public int[] getVersionNumber() throws SQLException + /** + * Return the server version number as a three-element {@code int} array + * (of which the third may be null), as used in the + * {@code getDatabase...Version} methods of {@link DatabaseMetaData}. + */ + public int[] getVersionNumber() throws SQLException { if (VERSION_NUMBER != null) return VERSION_NUMBER; @@ -674,11 +735,11 @@ public int[] getVersionNumber() throws SQLException } } - /* - * This implemetation uses the jdbc3Types array to support the jdbc3 - * datatypes. Basically jdbc2 and jdbc3 are the same, except that - * jdbc3 adds some - */ + /** + * Convert a PostgreSQL type name to a {@link Types} integer, using the + * {@code JDBC3_TYPE_NAMES}/{@code JDBC_TYPE_NUMBERS} arrays; used in + * {@link DatabaseMetaData} and {@link ResultSetMetaData}. + */ public int getSQLType(String pgTypeName) { if (pgTypeName == null) @@ -691,8 +752,10 @@ public int getSQLType(String pgTypeName) return Types.OTHER; } - /* - * This returns the java.sql.Types type for a PG type oid + /** + * This returns the {@link Types} type for a PG type oid, by mapping it + * to a name using {@link #getPGType} and then to the result via + * {@link #getSQLType(String)}. * * @param oid PostgreSQL type oid * @return the java.sql.Types type @@ -703,7 +766,12 @@ public int getSQLType(Oid oid) throws SQLException return getSQLType(getPGType(oid)); } - public String getPGType(Oid oid) throws SQLException + /** + * Map the Oid of a PostgreSQL type to its name (specifically, the + * {@code typname} attribute of {@code pg_type}. Used in + * {@link DatabaseMetaData} and {@link ResultSetMetaData}. + */ + public String getPGType(Oid oid) throws SQLException { String typeName = null; PreparedStatement query = null; @@ -735,6 +803,19 @@ public String getPGType(Oid oid) throws SQLException return typeName; } + /** + * Apply some hardwired coercions from an object to a desired class, + * where the class can be {@code String} or {@code URL}, as used in + * {@code ObjectResultSet} for retrieving values and + * {@code SingleRowWriter} for storing them, and also in + * {@code SQLInputFromTuple} for UDTs mapping composite types. + *

+ * Some review may be in order to determine just what part of JDBC's + * type mapping rules this corresponds to. It seems strangely limited, and + * the use of the same coercion in both the retrieval and storage direction + * in {@code ResultSet}s seems a bit suspect, as does its use in UDT input + * but not output with composites. + */ static Object basicCoersion(Class cls, Object value) throws SQLException { @@ -765,6 +846,20 @@ else if(cls == URL.class && value instanceof String) cls.getName() + " from an object of class " + value.getClass().getName()); } + /** + * Apply some hardwired coercions from an object to a desired class, + * one of Java's several numeric classes, when the value is an instance of + * {@code Number}, {@code String}, or {@code Boolean}, as used in + * {@code ObjectResultSet} for retrieving values and + * {@code SingleRowWriter} for storing them, and also in + * {@code SQLInputFromTuple} for UDTs mapping composite types. + *

+ * Some review may be in order to determine just what part of JDBC's + * type mapping rules this corresponds to. It seems strangely limited, and + * the use of the same coercion in both the retrieval and storage direction + * in {@code ResultSet}s seems a bit suspect, as does its use in UDT input + * but not output with composites. + */ static Number basicNumericCoersion(Class cls, Object value) throws SQLException { @@ -798,6 +893,19 @@ else if(cls == BigDecimal.class) throw new SQLException("Cannot derive a Number from an object of class " + value.getClass().getName()); } + /** + * Apply some hardwired coercions from an object to a desired class, + * where the class may be {@link Timestamp}, {@link Date}, or {@link Time} + * and the value one of those or {@code String}, as used in + * {@code ObjectResultSet} for retrieving values and + * {@code SingleRowWriter} for storing them, but not also in + * {@code SQLInputFromTuple} for UDTs mapping composite types. + *

+ * Some review may be in order to determine just what part of JDBC's + * type mapping rules this corresponds to. It seems strangely limited, and + * the use of the same coercion in both the retrieval and storage direction + * in {@code ResultSet}s seems a bit suspect. + */ static Object basicCalendricalCoersion(Class cls, Object value, Calendar cal) throws SQLException { @@ -929,6 +1037,7 @@ else if(value instanceof String) // Non-implementation of JDBC 4 methods. // ************************************************************ + @Override public Struct createStruct( String typeName, Object[] attributes ) throws SQLException { @@ -936,6 +1045,7 @@ public Struct createStruct( String typeName, Object[] attributes ) "SPIConnection.createStruct( String, Object[] ) not implemented yet.", "0A000" ); } + @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { @@ -943,6 +1053,7 @@ public Array createArrayOf(String typeName, Object[] elements) "SPIConnection.createArrayOf( String, Object[] ) not implemented yet.", "0A000" ); } + @Override public boolean isValid( int timeout ) throws SQLException { @@ -950,24 +1061,28 @@ public boolean isValid( int timeout ) // ready, right? } + @Override public SQLXML createSQLXML() throws SQLException { throw new SQLFeatureNotSupportedException( "SPIConnection.createSQLXML() not implemented yet.", "0A000" ); } + @Override public NClob createNClob() throws SQLException { throw new SQLFeatureNotSupportedException( "SPIConnection.createNClob() not implemented yet.", "0A000" ); } + @Override public Blob createBlob() throws SQLException { throw new SQLFeatureNotSupportedException( "SPIConnection.createBlob() not implemented yet.", "0A000" ); } + @Override public Clob createClob() throws SQLException { @@ -993,14 +1108,16 @@ public T unwrap(Class iface) "0A000" ); } - public void setClientInfo(String name, String value) throws SQLClientInfoException - { - Map failures = new HashMap(); - failures.put(name, ClientInfoStatus.REASON_UNKNOWN_PROPERTY); - throw new SQLClientInfoException("ClientInfo property not supported.", failures); - } + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException + { + Map failures = new HashMap(); + failures.put(name, ClientInfoStatus.REASON_UNKNOWN_PROPERTY); + throw new SQLClientInfoException("ClientInfo property not supported.", failures); + } + @Override public void setClientInfo(Properties properties) throws SQLClientInfoException { @@ -1016,11 +1133,13 @@ public void setClientInfo(Properties properties) throw new SQLClientInfoException("ClientInfo property not supported.", failures); } + @Override public String getClientInfo(String name) throws SQLException { return null; } + @Override public Properties getClientInfo() throws SQLException { if (_clientInfo == null) { @@ -1029,34 +1148,43 @@ public Properties getClientInfo() throws SQLException return _clientInfo; } - public void abort(Executor executor) throws SQLException - { + // ************************************************************ + // Non-implementation of JDBC 4.1 methods. + // ************************************************************ + + // add @Override once Java 7 is back-support limit + public void abort(Executor executor) throws SQLException + { throw new SQLFeatureNotSupportedException( "SPIConnection.abort(Executor) not implemented yet.", "0A000" ); - } + } - public int getNetworkTimeout() throws SQLException - { + // add @Override once Java 7 is back-support limit + public int getNetworkTimeout() throws SQLException + { throw new SQLFeatureNotSupportedException( "SPIConnection.getNetworkTimeout() not implemented yet.", "0A000" ); } - public void setNetworkTimeout(Executor executor, int milliseconds) - throws SQLException - { + // add @Override once Java 7 is back-support limit + public void setNetworkTimeout(Executor executor, int milliseconds) + throws SQLException + { throw new SQLFeatureNotSupportedException( "SPIConnection.setNetworkTimeout(Executor,int) not implemented yet.", "0A000" ); - } + } - public String getSchema() throws SQLException - { + // add @Override once Java 7 is back-support limit + public String getSchema() throws SQLException + { throw new SQLFeatureNotSupportedException( "SPIConnection.getSchema() not implemented yet.", "0A000" ); - } + } - public void setSchema(String schema) throws SQLException - { + // add @Override once Java 7 is back-support limit + public void setSchema(String schema) throws SQLException + { throw new SQLFeatureNotSupportedException( "SPIConnection.setSchema(String) not implemented yet.", "0A000" ); - } + } } From 9eb363f9b766562be954bcf914a5c750b2442e27 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 15:35:53 -0400 Subject: [PATCH 0062/1087] Sort implemented from unimplemented JDBC4 methods. Move those that have some JDBC-allowed behavior (other than throwing a not-supported exception) into the 'implemented' category, even when the JDBC-allowed behavior is to do nothing special. Leave in the 'unimplemented' category just the methods that ought to do something else, but currently throw the not-supported exception. In passing, just provide the natural implementation for isWrapperFor and unwrap; it's no more trouble than throwing the exception! For ResultSetMetaData, pull up their implementations into AbstractResultSetMetaData, so they don't have to be duplicated in SPIResultSetMetaData and SyntheticResultSetMetaData. Some indentation normalized, only in areas touched. --- .../pljava/jdbc/AbstractResultSet.java | 57 +++++--- .../jdbc/AbstractResultSetMetaData.java | 38 ++++- .../postgresql/pljava/jdbc/SPIConnection.java | 137 ++++++++++-------- .../pljava/jdbc/SPIDatabaseMetaData.java | 70 +++++---- .../pljava/jdbc/SPIParameterMetaData.java | 35 +++-- .../pljava/jdbc/SPIResultSetMetaData.java | 42 ++---- .../postgresql/pljava/jdbc/SPIStatement.java | 56 +++---- .../jdbc/SyntheticResultSetMetaData.java | 43 ++---- 8 files changed, 263 insertions(+), 215 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index 8e56055f..4c438b81 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -380,6 +385,30 @@ public void updateTimestamp(String columnName, Timestamp x) this.updateTimestamp(this.findColumn(columnName), x); } + // ************************************************************ + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. + // ************************************************************ + + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + public T unwrap(Class iface) + throws SQLException + { + if ( iface.isInstance(this) ) + return iface.cast(this); + throw new SQLFeatureNotSupportedException + ( this.getClass().getSimpleName() + + " does not wrap " + iface.getName(), + "0A000" ); + } + // ************************************************************ // Non-implementation of JDBC 4 methods. // ************************************************************ @@ -741,22 +770,4 @@ public RowId getRowId(String columnLabel) throw new SQLFeatureNotSupportedException( this.getClass() + ".getRowId( String ) not implemented yet.", "0A000" ); } - - public boolean isWrapperFor(Class iface) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".isWrapperFor( Class ) not implemented yet.", - "0A000" ); - } - - public T unwrap(Class iface) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".unwrapClass( Class ) not implemented yet.", - "0A000" ); - } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSetMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSetMetaData.java index 545ec34d..0fe1bb1e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSetMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSetMetaData.java @@ -1,14 +1,21 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Filip Hrbek + * Chapman Flack */ package org.postgresql.pljava.jdbc; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Connection; import java.sql.DriverManager; @@ -416,4 +423,27 @@ private Connection getDefaultConnection() throws SQLException return m_conn; } + // ************************************************************ + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. + // ************************************************************ + + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + public T unwrap(Class iface) + throws SQLException + { + if ( iface.isInstance(this) ) + return iface.cast(this); + throw new SQLFeatureNotSupportedException + ( this.getClass().getSimpleName() + + " does not wrap " + iface.getName(), + "0A000" ); + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 3af3ac15..fb76428c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2009, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * 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: + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -1034,25 +1039,12 @@ else if(value instanceof String) }; // ************************************************************ - // Non-implementation of JDBC 4 methods. + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. // ************************************************************ - @Override - public Struct createStruct( String typeName, Object[] attributes ) - throws SQLException - { - throw new SQLFeatureNotSupportedException( - "SPIConnection.createStruct( String, Object[] ) not implemented yet.", "0A000" ); - } - - @Override - public Array createArrayOf(String typeName, Object[] elements) - throws SQLException - { - throw new SQLFeatureNotSupportedException( - "SPIConnection.createArrayOf( String, Object[] ) not implemented yet.", "0A000" ); - } - @Override public boolean isValid( int timeout ) throws SQLException @@ -1062,52 +1054,31 @@ public boolean isValid( int timeout ) } @Override - public SQLXML createSQLXML() - throws SQLException - { - throw new SQLFeatureNotSupportedException( "SPIConnection.createSQLXML() not implemented yet.", - "0A000" ); - } - @Override - public NClob createNClob() - throws SQLException - { - throw new SQLFeatureNotSupportedException( "SPIConnection.createNClob() not implemented yet.", - "0A000" ); - } - @Override - public Blob createBlob() - throws SQLException - { - throw new SQLFeatureNotSupportedException( "SPIConnection.createBlob() not implemented yet.", - "0A000" ); - } - @Override - public Clob createClob() - throws SQLException - { - throw new SQLFeatureNotSupportedException( "SPIConnection.createClob() not implemented yet.", - "0A000" ); - } - public boolean isWrapperFor(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".isWrapperFor( Class ) not implemented yet.", - "0A000" ); + return iface.isInstance(this); } + @Override public T unwrap(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".unwrapClass( Class ) not implemented yet.", + if ( iface.isInstance(this) ) + return iface.cast(this); + throw new SQLFeatureNotSupportedException + ( this.getClass().getSimpleName() + + " does not wrap " + iface.getName(), "0A000" ); } + /* + * These ClientInfo implementations behave as if there are no known + * ClientInfo properties, which is an allowable implementation. However, + * there is a PostgreSQL notion corresponding to ApplicationName, so a + * later extension of these to recognize that property would not be amiss. + */ + @Override public void setClientInfo(String name, String value) throws SQLClientInfoException { @@ -1148,6 +1119,58 @@ public Properties getClientInfo() throws SQLException return _clientInfo; } + // ************************************************************ + // Non-implementation of JDBC 4 methods. + // ************************************************************ + + @Override + public Struct createStruct( String typeName, Object[] attributes ) + throws SQLException + { + throw new SQLFeatureNotSupportedException( + "SPIConnection.createStruct( String, Object[] ) not implemented yet.", "0A000" ); + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) + throws SQLException + { + throw new SQLFeatureNotSupportedException( + "SPIConnection.createArrayOf( String, Object[] ) not implemented yet.", "0A000" ); + } + + @Override + public SQLXML createSQLXML() + throws SQLException + { + throw new SQLFeatureNotSupportedException( "SPIConnection.createSQLXML() not implemented yet.", + "0A000" ); + } + + @Override + public NClob createNClob() + throws SQLException + { + throw new SQLFeatureNotSupportedException( "SPIConnection.createNClob() not implemented yet.", + "0A000" ); + } + + @Override + public Blob createBlob() + throws SQLException + { + throw new SQLFeatureNotSupportedException( "SPIConnection.createBlob() not implemented yet.", + "0A000" ); + } + + @Override + public Clob createClob() + throws SQLException + { + throw new SQLFeatureNotSupportedException( "SPIConnection.createClob() not implemented yet.", + "0A000" ); + } + // ************************************************************ // Non-implementation of JDBC 4.1 methods. // ************************************************************ diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index d478e764..76016f20 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2005, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2005-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Filip Hrbek + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -3460,6 +3465,35 @@ private ResultSet createSyntheticResultSet(ResultSetField[] f, ArrayList tuples) { return new SyntheticResultSet(f, tuples); } + + // ************************************************************ + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. + // ************************************************************ + + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + public T unwrap(java.lang.Class iface) + throws SQLException + { + if ( iface.isInstance(this) ) + return iface.cast(this); + throw new SQLFeatureNotSupportedException + ( this.getClass().getSimpleName() + + " does not wrap " + iface.getName(), + "0A000" ); + } + + public boolean generatedKeyAlwaysReturned() throws SQLException + { + return false; + } // ************************************************************ // Non-implementation of JDBC 4 methods. @@ -3530,32 +3564,12 @@ public RowIdLifetime getRowIdLifetime() "SPIDatabaseMetadata.getRowIdLifetime() not implemented yet.", "0A000" ); } - public boolean isWrapperFor(Class c) - throws SQLException - { - throw new SQLFeatureNotSupportedException( - "SPIDatabaseMetadata.isWrapperFor( Class< ? > ) not implemented yet.", "0A000" ); - } - - public T unwrap(java.lang.Class T) - throws SQLException + public ResultSet getPseudoColumns(String catalog, String schemaPattern, + String tableNamePattern, String columnNamePattern) throws SQLException { - throw new SQLFeatureNotSupportedException( - "SPIDatabaseMetadata.unwrap( Class< T > ) not implemented yet.", "0A000" ); - - } - - public boolean generatedKeyAlwaysReturned() throws SQLException - { - return false; - } - - public ResultSet getPseudoColumns(String catalog, String schemaPattern, - String tableNamePattern, String columnNamePattern) throws SQLException - { throw new SQLFeatureNotSupportedException( "SPIDatabaseMetadata.getPseudoColumns(String,String,String,String) not implemented yet.", "0A000" ); - } + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java index 38ad4654..08ed90a8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group - * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * Copyright (c) 2004-2018 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: + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -95,24 +100,26 @@ public int getParameterMode(int paramIndex) throws SQLException } // ************************************************************ - // Non-implementation of JDBC 4 methods. + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. // ************************************************************ public boolean isWrapperFor(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".isWrapperFor( Class ) not implemented yet.", - "0A000" ); + return iface.isInstance(this); } public T unwrap(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".unwrapClass( Class ) not implemented yet.", + if ( iface.isInstance(this) ) + return iface.cast(this); + throw new SQLFeatureNotSupportedException + ( this.getClass().getSimpleName() + + " does not wrap " + iface.getName(), "0A000" ); } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSetMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSetMetaData.java index dcc3cefa..8788149a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSetMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSetMetaData.java @@ -1,16 +1,20 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group - * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * Copyright (c) 2004-2018 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: + * Filip Hrbek + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; import org.postgresql.pljava.internal.Oid; import org.postgresql.pljava.internal.TupleDesc; @@ -135,28 +139,4 @@ protected final int getFieldLength(int column) throws SQLException { return 0; } - - // ************************************************************ - // Non-implementation of JDBC 4 methods. - // ************************************************************ - - - public boolean isWrapperFor(Class iface) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".isWrapperFor( Class ) not implemented yet.", - "0A000" ); - } - - public T unwrap(Class iface) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".unwrapClass( Class ) not implemented yet.", - "0A000" ); - } - } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java index cf200840..c8249a5f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -432,65 +432,71 @@ void resultSetClosed(ResultSet rs) } // ************************************************************ - // Non-implementation of JDBC 4 methods. + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. // ************************************************************ - public void setPoolable(boolean poolable) + public boolean isWrapperFor(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".setPoolable( boolean ) not implemented yet.", - "0A000" ); + return iface.isInstance(this); } - public boolean isPoolable() + public T unwrap(Class iface) throws SQLException { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".isPoolable() not implemented yet.", + if ( iface.isInstance(this) ) + return iface.cast(this); + throw new SQLFeatureNotSupportedException + ( this.getClass().getSimpleName() + + " does not wrap " + iface.getName(), "0A000" ); } - public boolean isClosed() + public boolean isCloseOnCompletion() throws SQLException + { + return false; + } + + // ************************************************************ + // Non-implementation of JDBC 4 methods. + // ************************************************************ + + public void setPoolable(boolean poolable) throws SQLException { throw new SQLFeatureNotSupportedException ( this.getClass() - + ".isClosed() not implemented yet.", + + ".setPoolable( boolean ) not implemented yet.", "0A000" ); } - public boolean isWrapperFor(Class iface) + public boolean isPoolable() throws SQLException { throw new SQLFeatureNotSupportedException ( this.getClass() - + ".isWrapperFor( Class ) not implemented yet.", + + ".isPoolable() not implemented yet.", "0A000" ); } - public T unwrap(Class iface) + public boolean isClosed() throws SQLException { throw new SQLFeatureNotSupportedException ( this.getClass() - + ".unwrapClass( Class ) not implemented yet.", + + ".isClosed() not implemented yet.", "0A000" ); } - public void closeOnCompletion() throws SQLException - { + public void closeOnCompletion() throws SQLException + { throw new SQLFeatureNotSupportedException ( this.getClass() + ".closeOneCompletion() not implemented yet.", "0A000" ); - } - - public boolean isCloseOnCompletion() throws SQLException - { - return false; - } + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSetMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSetMetaData.java index e941fd29..e85e9f54 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSetMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSetMetaData.java @@ -1,16 +1,20 @@ /* - * Copyright (c) 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2005, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2005-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Filip Hrbek + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; import org.postgresql.pljava.internal.Oid; @@ -132,31 +136,4 @@ protected final int getFieldLength(int column) throws SQLException { return m_fields[column-1].getLength(); } - - // ************************************************************ - // Non-implementation of JDBC 4 methods. - // ************************************************************ - - public boolean isWrapperFor(Class iface) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".isWrapperFor( Class ) not implemented yet.", - "0A000" ); - } - - public T unwrap(Class iface) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".unwrapClass( Class ) not implemented yet.", - "0A000" ); - } - - // ************************************************************ - // End of non-implementation of JDBC 4 methods. - // ************************************************************ - } From 141efe2264553a2300af425a7f3d4b5a76d02924 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 1 Jun 2018 13:07:58 -0400 Subject: [PATCH 0063/1087] Use getSQLTypeName, and add a test. Provide one of the short-term solutions suggested in issue #149. As the current PreparedStatement implementation does not get the inferred parameter types from PostgreSQL (which became possible with SPI only as recently as PostgreSQL 9.0), its setObject method must make a best effort to map in the other direction, finding the PostgreSQL type that corresponds to the Java parameter value. In one case, this is easily made much more reliable: when the Java parameter value is an SQLData instance (a UDT), and therefore has a getSQLTypeName method, unused until now. Add a test method (in the ComplexTuple UDT example) to confirm that the type is properly mapped when passed as a parameter to a PreparedStatement. --- .../example/annotation/ComplexTuple.java | 35 +++++++++++++++++-- .../org/postgresql/pljava/internal/Oid.java | 25 ++++++++----- .../pljava/jdbc/SPIPreparedStatement.java | 4 +-- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java index 54a04197..e02dac25 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java @@ -12,6 +12,10 @@ */ package org.postgresql.pljava.example.annotation; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLData; import java.sql.SQLException; import java.sql.SQLInput; @@ -31,9 +35,12 @@ * Complex (re and im parts are doubles) implemented in Java as a mapped UDT. */ @SQLAction(requires={ - "complextuple type", "complextuple assertHasValues"}, install= + "complextuple type", "complextuple assertHasValues", + "complextuple setParameter"}, install={ "SELECT javatest.assertHasValues(" + - " CAST('(1,2)' AS javatest.complextuple), 1, 2)" + " CAST('(1,2)' AS javatest.complextuple), 1, 2)", + "SELECT javatest.setParameter()" + } ) @MappedUDT(schema="javatest", name="complextuple", provides="complextuple type", structure={ @@ -79,6 +86,30 @@ public static void assertHasValues( throw new SQLException("assertHasValues fails"); } + /** + * Pass a 'complextuple' UDT as a parameter to a PreparedStatement + * that returns it, and verify that it makes the trip intact. + */ + @Function(schema="javatest", + requires="complextuple type", provides="complextuple setParameter", + effects=IMMUTABLE, onNullInput=RETURNS_NULL) + public static void setParameter() throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + PreparedStatement ps = + c.prepareStatement("SELECT CAST(? AS javatest.complextuple)"); + ComplexTuple ct = new ComplexTuple(); + ct.m_x = 1.5; + ct.m_y = 2.5; + ct.m_typeName = "javatest.complextuple"; + ps.setObject(1, ct); + ResultSet rs = ps.executeQuery(); + rs.next(); + ct = (ComplexTuple)rs.getObject(1); + ps.close(); + assertHasValues(ct, 1.5, 2.5); + } + private double m_x; private double m_y; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java index f3d0df32..7d3a9778 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java @@ -1,11 +1,18 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; +import java.sql.SQLData; import java.sql.SQLException; import java.util.HashMap; @@ -37,13 +44,15 @@ public class Oid extends Number } /** - * Finds the PostgreSQL well known Oid for the given class. - * @param clazz The class. + * Finds the PostgreSQL well known Oid for the given Java object. + * @param obj The object. * @return The well known Oid or null if no such Oid could be found. */ - public static Oid forJavaClass(Class clazz) + public static Oid forJavaObject(Object obj) throws SQLException { - return (Oid)s_class2typeId.get(clazz); + if ( obj instanceof SQLData ) + return forTypeName(((SQLData)obj).getSQLTypeName()); + return (Oid)s_class2typeId.get(obj.getClass()); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index 478249aa..f78c40cc 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -209,7 +209,7 @@ public void setObject(int columnIndex, Object value, int sqlType) throw new SQLException("Illegal parameter index"); Oid id = (sqlType == Types.OTHER) - ? Oid.forJavaClass(value.getClass()) + ? Oid.forJavaObject(value) : Oid.forSqlType(sqlType); // Default to String. From 27916e15603be5a482dce739bc3d9256fce9758f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 1 Jun 2018 23:50:51 -0400 Subject: [PATCH 0064/1087] Fix omitted link in install docs. --- src/site/markdown/install/vmoptions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/site/markdown/install/vmoptions.md b/src/site/markdown/install/vmoptions.md index efca87c5..129b15f9 100644 --- a/src/site/markdown/install/vmoptions.md +++ b/src/site/markdown/install/vmoptions.md @@ -157,6 +157,8 @@ The `G1` collector, favored as `ConcMarkSweep`'s replacement, uses slightly more space to work, while `Parallel` and `ParallelOld` will occupy more than double the space of any of these. +[gcchoice]: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html + ### Plausible settings The optimal memory settings and garbage collector for a specific PL/Java From 5e4bf0be03a1203ce7df880e2f4e937741f5d6ed Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 16 Jun 2018 17:13:53 -0400 Subject: [PATCH 0065/1087] Let annotations give row-type parameters defaults. The @SQLType annotation can now supply an array of strings as defaultValue for a parameter that has a row type (either the RECORD type, whose structure is unknown, or a named row type). In the case of RECORD, the only default that will be accepted is {} (the array of no strings). While a bit limiting, that is still just the ticket when a RECORD-typed parameter is being used to supply a function with an optional, arbitrary sequence of named, typed parameters. In the case of a named row type, the default should be an array of as many strings as the components of the type, and the strings should be castable to the corresponding component types. The DDR generator has no way to check that at compile time, but PostgreSQL will report the error at jar install time if there is a mismatch. --- .../postgresql/pljava/annotation/SQLType.java | 11 +- .../pljava/sqlgen/DDRProcessor.java | 34 ++++- .../annotation/RecordParameterDefaults.java | 144 ++++++++++++++++++ 3 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java index ce6608f0..051f1eb1 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java @@ -41,13 +41,22 @@ String value() default ""; /** - * Default value for the parameter. Parameters of array type can have + * Default value for the parameter. Parameters of row or array type can have * defaults too, so this element accepts an array. For a scalar type, * just supply one value. Values given here go into the descriptor file * as properly-escaped string literals explicitly cast to the parameter * type, which covers the typical case of defaults that are simple * literals or can be computed as Java String-typed constant expressions * (e.g. ""+Math.PI) and ensures the parsability of the descriptor file. + *

+ * For a row type of unknown structure (PostgreSQL type {@code RECORD}), the + * only default that can be specified is {@code {}}, which can be useful for + * functions that use a {@code RECORD} parameter to accept an arbitrary + * sequence of named, typed parameters from the caller. For a named row type + * (not {@code RECORD}), an array of nonzero length will be accepted. It + * needs to match the number and order of components of the row type (which + * cannot be checked at compile time, but will cause the deployment + * descriptor code to fail at jar install time if it does not). */ String[] defaultValue() default {}; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index cbc5549a..023dde70 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -2362,6 +2362,7 @@ String getSQLType(TypeMirror tm, Element e, boolean contravariant, boolean withDefault) { boolean array = false; + boolean row = false; String rslt = null; String[] defaults = null; @@ -2387,9 +2388,13 @@ String getSQLType(TypeMirror tm, Element e, // only for bytea[] should this ever still be an array } } + + if ( ! array && typu.isSameType( tm, TY_RESULTSET) ) + row = true; if ( null != rslt ) - return typeWithDefault( rslt, array, defaults, withDefault); + return typeWithDefault( + e, rslt, array, row, defaults, withDefault); if ( tm.getKind().equals( TypeKind.VOID) ) return "void"; // can't be a parameter type so no defaults apply @@ -2449,7 +2454,7 @@ String getSQLType(TypeMirror tm, Element e, if ( array ) rslt += "[]"; - return typeWithDefault( rslt, array, defaults, withDefault); + return typeWithDefault( e, rslt, array, row, defaults, withDefault); } /** @@ -2462,29 +2467,42 @@ String getSQLType(TypeMirror tm, Element e, * cases of simple literals and even anything that can be computed as * a Java String constant expression (e.g. ""+Math.PI). * + * @param e Annotated element (chiefly for use as a location hint in + * diagnostic messages). * @param rslt The bare SQL type string already determined * @param array Whether the Java type was determined to be an array + * @param row Whether the Java type was ResultSet, indicating an SQL + * record or row type. * @param defaults Array (null if not present) of default value strings * @param withDefault Whether to append the default information to the * type. */ String typeWithDefault( - String rslt, boolean array, String[] defaults, boolean withDefault) + Element e, String rslt, boolean array, boolean row, + String[] defaults, boolean withDefault) { if ( null == defaults || ! withDefault ) return rslt; int n = defaults.length; - if ( n != 1 ) + if ( row ) + { + assert ! array; + if ( n > 0 && rslt.equalsIgnoreCase("record") ) + msg( Kind.ERROR, e, + "Only supported default for unknown RECORD type is {}"); + } + else if ( n != 1 ) array = true; else if ( ! array ) array = arrayish.matcher( rslt).matches(); StringBuilder sb = new StringBuilder( rslt); - sb.append( " DEFAULT CAST("); + sb.append( " DEFAULT "); + sb.append( row ? "ROW(" : "CAST("); if ( array ) sb.append( "ARRAY["); - if ( n != 1 ) + if ( n > 1 ) sb.append( "\n\t"); for ( String s : defaults ) { @@ -2494,7 +2512,9 @@ else if ( ! array ) } if ( array ) sb.append( ']'); - sb.append( " AS ").append( rslt).append( ')'); + if ( ! row ) + sb.append( " AS ").append( rslt); + sb.append( ')'); return sb.toString(); } } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java new file mode 100644 index 00000000..02f0795e --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2018- 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.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import static java.util.Arrays.fill; + +import org.postgresql.pljava.ResultSetProvider; +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLActions; +import org.postgresql.pljava.annotation.SQLType; + +/** + * Example demonstrating the use of a {@code RECORD} parameter as a way to + * supply an arbitrary sequence of named, typed parameters to a PL/Java + * function. + *

+ * Also tests the proper DDR generation of defaults for such parameters. + */ +@SQLActions({ + @SQLAction( + provides = "paramtypeinfo type", // created in Triggers.java + install = { + "CREATE TYPE javatest.paramtypeinfo AS (" + + "name text, pgtypename text, javaclass text, tostring text" + + ")" + }, + remove = { + "DROP TYPE javatest.paramtypeinfo" + } + ), + @SQLAction( + requires = "foobar tables", // created in Triggers.java + install = { + }, + remove = { + } + ), +}) +public class RecordParameterDefaults implements ResultSetProvider +{ + /** + * Return the names, types, and values of parameters supplied as a single + * anonymous RECORD type; the parameter is given an empty-record default, + * allowing it to be omitted in calls, or used with the named-parameter + * call syntax. + *

+ * For example, this function could be called as: + *

+	 * SELECT (paramDefaultsRecord()).*;
+	 *
+ * or as: + *
+	 * SELECT (paramDefaultsRecord(params => s)).*
+	 * FROM (SELECT 42 AS a, '42' AS b, 42.0 AS c) AS s;
+	 *
+ */ + @Function( + requires = "paramtypeinfo type", + schema = "javatest", + type = "javatest.paramtypeinfo" + ) + public static ResultSetProvider paramDefaultsRecord( + @SQLType(defaultValue={})ResultSet params) + throws SQLException + { + return new RecordParameterDefaults(params); + } + + /** + * Like paramDefaultsRecord but illustrating the use of a named row type + * with known structure, and supplying a default for the function + * parameter. + *

+ *

+	 * SELECT paramDefaultsNamedRow();
+	 *
+	 * SELECT paramDefaultsNamedRow(userWithNum => ('fred', 3.14));
+	 *
+ */ + @Function( + requires = "foobar tables", // created in Triggers.java + schema = "javatest" + ) + public static String paramDefaultsNamedRow( + @SQLType(value="javatest.foobar_2", defaultValue={"bob", "42"}) + ResultSet userWithNum) + throws SQLException + { + return String.format("username is %s and value is %s", + userWithNum.getObject("username"), userWithNum.getObject("value")); + } + + + + private final ResultSetMetaData m_paramrsmd; + private final Object[] m_values; + + RecordParameterDefaults(ResultSet paramrs) throws SQLException + { + m_paramrsmd = paramrs.getMetaData(); + /* + * Grab the values from the parameter SingleRowResultSet now; it isn't + * guaranteed to stay valid for the life of the set-returning function. + */ + m_values = new Object [ m_paramrsmd.getColumnCount() ]; + for ( int i = 0; i < m_values.length; ++ i ) + m_values[i] = paramrs.getObject( 1 + i); + } + + @Override + public boolean assignRowValues(ResultSet receiver, int currentRow) + throws SQLException + { + int col = 1 + currentRow; + if ( col > m_paramrsmd.getColumnCount() ) + return false; + receiver.updateString("name", m_paramrsmd.getColumnLabel(col)); + receiver.updateString("pgtypename", m_paramrsmd.getColumnTypeName(col)); + Object o = m_values[col - 1]; + receiver.updateString("javaclass", o.getClass().getName()); + receiver.updateString("tostring", o.toString()); + return true; + } + + @Override + public void close() throws SQLException + { + fill(m_values, null); + } +} From 9da65f8b7e4f1e73d3a1643d3497f74b7e491dc2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 16 Jun 2018 17:32:05 -0400 Subject: [PATCH 0066/1087] Remove an overlooked copy/paste artifact. --- .../pljava/example/annotation/RecordParameterDefaults.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index 02f0795e..cb6055c4 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -42,13 +42,6 @@ "DROP TYPE javatest.paramtypeinfo" } ), - @SQLAction( - requires = "foobar tables", // created in Triggers.java - install = { - }, - remove = { - } - ), }) public class RecordParameterDefaults implements ResultSetProvider { From 5efb2b57e1aae6a96c50dabe66de4e04dc6dccfd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 11 May 2018 21:09:42 -0400 Subject: [PATCH 0067/1087] Add SQLXMLImpl abstract class. Concrete subclasses will implement the readable flavor of SQLXML instance (as would be received as a function parameter or retrieved from a ResultSet) and the writable flavor (as would be obtained empty from Connection.createSQLXML, populated, then used as a query parameter, return value, etc.). --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java new file mode 100644 index 00000000..96c8e219 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2018 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.jdbc; + +/* Imports for API */ + +import java.sql.SQLXML; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import javax.xml.transform.Source; +import javax.xml.transform.Result; + +import java.sql.SQLException; + +/* Supplemental imports for SQLXMLImpl */ + +import java.io.Closeable; +import java.io.IOException; + +import java.util.concurrent.atomic.AtomicReference; + +import java.sql.SQLNonTransientException; + +public abstract class SQLXMLImpl implements SQLXML +{ + protected AtomicReference m_backing; + + protected SQLXMLImpl(V backing) + { + m_backing = new AtomicReference(backing); + } + + @Override + public void free() throws SQLException + { + V backing = m_backing.getAndSet(null); + if ( null == backing ) + return; + try + { + backing.close(); + } + catch ( IOException e ) + { + throw normalizedException(e); + } + } + + @Override + public InputStream getBinaryStream() throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of getBinaryStream on an unreadable SQLXML object", + "55000"); + } + + @Override + public OutputStream setBinaryStream() throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of setBinaryStream on an unwritable SQLXML object", + "55000"); + } + + @Override + public Reader getCharacterStream() throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of getCharacterStream on an unreadable " + + "SQLXML object", "55000"); + } + + @Override + public Writer setCharacterStream() throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of setCharacterStream on an unwritable " + + "SQLXML object", "55000"); + } + + @Override + public String getString() throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of getString on an unreadable SQLXML object", + "55000"); + } + + @Override + public void setString(String value) throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of setString on an unwritable SQLXML object", + "55000"); + } + + @Override + public T getSource(Class sourceClass) + throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of getSource on an unreadable SQLXML object", + "55000"); + } + + @Override + public T setResult(Class resultClass) + throws SQLException + { + throw new SQLNonTransientException( + "Attempted use of setResult on an unwritable SQLXML object", + "55000"); + } + + protected V backingIfNotFreed() throws SQLException + { + V backing = m_backing.get(); + if ( null == backing ) + throw new SQLNonTransientException( + "Attempted use of already-freed SQLXML object", "55000"); + return backing; + } + + /** + * Wrap other checked exceptions in SQLException for methods specified to + * throw only that. + */ + protected SQLException normalizedException(Exception e) + { + if ( e instanceof SQLException ) + return (SQLException) e; + if ( e instanceof RuntimeException ) + throw (RuntimeException) e; + + if ( e instanceof IOException ) + { + Throwable cause = e.getCause(); + if ( cause instanceof SQLException ) + return (SQLException)cause; + } + + return new SQLException( + "Exception in XML processing, not otherwise provided for", + "XX000", e); + } +} From 6df786639138a572232ecaf0ef4bfbb254fdb2a3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 11 May 2018 19:51:43 -0400 Subject: [PATCH 0068/1087] Introduce DualState class. Yes, it is *another* pattern for objects that have Java and native state. For now, the existing ones are being left alone. The existing patterns do not seem to include anything for objects that are spec'd to live for the whole transaction (as JDBC specs SQLXML to do). This pattern hooks into PostgreSQL ResourceOwner mechanisms (and there is a TopTransaction ResourceOwner) in order to be usable for that. If nothing blows up, it may be possible to migrate other existing patterns to it later. --- pljava-so/src/main/c/Backend.c | 2 + pljava-so/src/main/c/DualState.c | 80 +++++ pljava-so/src/main/c/Invocation.c | 6 + pljava-so/src/main/c/JNICalls.c | 34 ++ pljava-so/src/main/include/pljava/DualState.h | 33 ++ pljava-so/src/main/include/pljava/JNICalls.h | 6 + .../postgresql/pljava/internal/DualState.java | 304 ++++++++++++++++++ 7 files changed, 465 insertions(+) create mode 100644 pljava-so/src/main/c/DualState.c create mode 100644 pljava-so/src/main/include/pljava/DualState.h create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/DualState.java diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 57a3c87d..1a8409e7 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -34,6 +34,7 @@ #include #include "org_postgresql_pljava_internal_Backend.h" +#include "pljava/DualState.h" #include "pljava/Invocation.h" #include "pljava/InstallHelper.h" #include "pljava/Function.h" @@ -832,6 +833,7 @@ static void initPLJavaClasses(void) Invocation_initialize(); Exception_initialize2(); SPI_initialize(); + pljava_DualState_initialize(); Type_initialize(); Function_initialize(); Session_initialize(); diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c new file mode 100644 index 00000000..2cbe6a44 --- /dev/null +++ b/pljava-so/src/main/c/DualState.c @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2018 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 + */ + +#include "pljava/DualState.h" + +#include "pljava/PgObject.h" +#include "pljava/JNICalls.h" +static jclass s_DualState_class; + +static jmethodID s_DualState_resourceOwnerRelease; +static jmethodID s_DualState_cleanEnqueuedInstances; + +static jobject s_DualState_key; + +static void resourceReleaseCB(ResourceReleasePhase phase, + bool isCommit, bool isTopLevel, void *arg); + +jobject pljava_DualState_key(void) +{ + return s_DualState_key; +} + +void pljava_DualState_cleanEnqueuedInstances(void) +{ + JNI_callStaticVoidMethodLocked(s_DualState_class, + s_DualState_cleanEnqueuedInstances); +} + +void pljava_DualState_initialize(void) +{ + jclass clazz; + jmethodID ctor; + + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState")); + + s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( + s_DualState_class, "resourceOwnerRelease", "(J)V"); + s_DualState_cleanEnqueuedInstances = PgObject_getStaticJavaMethod( + s_DualState_class, "cleanEnqueuedInstances", "()V"); + + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$Key"); + ctor = PgObject_getJavaMethod(clazz, "", "()V"); + s_DualState_key = JNI_newGlobalRef(JNI_newObject(clazz, ctor)); + JNI_deleteLocalRef(clazz); + + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); +} + +static void resourceReleaseCB(ResourceReleasePhase phase, + bool isCommit, bool isTopLevel, void *arg) +{ + Ptr2Long p2l; + + /* + * This static assertion does not need to be in every file + * that uses Ptr2Long, but it should be somewhere once, so here it is. + */ + StaticAssertStmt(sizeof p2l.ptrVal <= sizeof p2l.longVal, + "Pointer will not fit in long on this platform"); + + if ( RESOURCE_RELEASE_AFTER_LOCKS != phase ) + return; + + p2l.longVal = 0L; + p2l.ptrVal = CurrentResourceOwner; + JNI_callStaticVoidMethodLocked(s_DualState_class, + s_DualState_resourceOwnerRelease, + p2l.longVal); +} diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 1af40e57..26880f0d 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -15,6 +15,7 @@ #include "pljava/PgObject.h" #include "pljava/JNICalls.h" #include "pljava/Backend.h" +#include "pljava/DualState.h" #define LOCAL_FRAME_SIZE 128 @@ -174,6 +175,11 @@ void Invocation_popInvocation(bool wasException) JNI_deleteGlobalRef(currentInvocation->invocation); } + /* + * Check for any DualState objects that became unreachable and can be freed. + */ + pljava_DualState_cleanEnqueuedInstances(); + if(currentInvocation->hasConnected) SPI_finish(); diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 71c14550..e6b1bd8a 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -529,6 +529,21 @@ void JNI_callStaticVoidMethodV(jclass clazz, jmethodID methodID, va_list args) END_CALL } +void JNI_callStaticVoidMethodLocked(jclass clazz, jmethodID methodID, ...) +{ + va_list args; + va_start(args, methodID); + JNI_callStaticVoidMethodLockedV(clazz, methodID, args); + va_end(args); +} + +void JNI_callStaticVoidMethodLockedV(jclass clazz, jmethodID methodID, va_list args) +{ + BEGIN_CALL_MONITOR_HELD + (*env)->CallStaticVoidMethodV(env, clazz, methodID, args); + END_CALL_MONITOR_HELD +} + void JNI_callVoidMethod(jobject object, jmethodID methodID, ...) { va_list args; @@ -1076,6 +1091,25 @@ jobject JNI_newObjectV(jclass clazz, jmethodID ctor, va_list args) return result; } +jobject JNI_newObjectLocked(jclass clazz, jmethodID ctor, ...) +{ + jobject result; + va_list args; + va_start(args, ctor); + result = JNI_newObjectLockedV(clazz, ctor, args); + va_end(args); + return result; +} + +jobject JNI_newObjectLockedV(jclass clazz, jmethodID ctor, va_list args) +{ + jobject result; + BEGIN_CALL_MONITOR_HELD + result = (*env)->NewObjectV(env, clazz, ctor, args); + END_CALL_MONITOR_HELD + return result; +} + void JNI_releaseByteArrayElements(jbyteArray array, jbyte* elems, jint mode) { BEGIN_JAVA diff --git a/pljava-so/src/main/include/pljava/DualState.h b/pljava-so/src/main/include/pljava/DualState.h new file mode 100644 index 00000000..6148f1a5 --- /dev/null +++ b/pljava-so/src/main/include/pljava/DualState.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018 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 + */ +#ifndef __pljava_DualState_h +#define __pljava_DualState_h + +#include +#include + +#include "pljava/pljava.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern jobject pljava_DualState_key(void); + +extern void pljava_DualState_cleanEnqueuedInstances(void); + +extern void pljava_DualState_initialize(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index a9252946..0a4678e5 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -68,15 +68,21 @@ extern jclass NoSuchMethodError_class; * whole time. They are used in String.c for character set coding conversions, * which may frequently call Java methods that are never expected to have any * reason to block or reenter the backend. + * Also, they can be used with DualState and related objects, to be sure certain + * methods or constructors are called on a thread that holds the native lock. */ extern jobject JNI_callObjectMethodLocked(jobject object, jmethodID methodID, ...); extern jobject JNI_callObjectMethodLockedV(jobject object, jmethodID methodID, va_list args); extern jobject JNI_callStaticObjectMethodLocked(jclass clazz, jmethodID methodID, ...); extern jobject JNI_callStaticObjectMethodLockedV(jclass clazz, jmethodID methodID, va_list args); +extern void JNI_callStaticVoidMethodLocked(jclass clazz, jmethodID methodID, ...); +extern void JNI_callStaticVoidMethodLockedV(jclass clazz, jmethodID methodID, va_list args); extern jint JNI_callIntMethodLocked(jobject object, jmethodID methodID, ...); extern jint JNI_callIntMethodLockedV(jobject object, jmethodID methodID, va_list args); extern void JNI_callVoidMethodLocked(jobject object, jmethodID methodID, ...); extern void JNI_callVoidMethodLockedV(jobject object, jmethodID methodID, va_list args); +extern jobject JNI_newObjectLocked(jclass clazz, jmethodID ctor, ...); +extern jobject JNI_newObjectLockedV(jclass clazz, jmethodID ctor, va_list args); /* * Misc JNIEnv mappings. See for more info. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java new file mode 100644 index 00000000..8433ab83 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2018 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.internal; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +import java.sql.SQLException; + +import java.util.Deque; +import java.util.Iterator; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * Base class for object state with corresponding Java and native components. + *

+ * A {@code DualState} object connects some state that exists in the JVM + * as well as some native/PostgreSQL resources. It will 'belong' to some Java + * object that holds a strong reference to it, and this state object is, + * in turn, a {@link WeakReference} to that object. Java state may be held in + * that object (if it needs only to be freed by the garbage collector when + * unreachable), or in this object if it needs some more specific cleanup. + * Native state will be referred to by this object. + *

+ * These interesting events are possible in the life cycle of a + * {@code DualState} object: + *

    + *
  • It is explicitly closed by the Java code using it. It, and any associated + * native state, should be released.
  • + *
  • It is found unreachable by the Java garbage collector. Again, any + * associated state should be released.
  • + *
  • Its associated native state is released or invalidated (such as by exit + * of a corresponding context). If the object is still reachable from Java, it + * must be marked to throw an exception for any future attempted access to its + * native state.
  • + *
+ *

+ * The introduction of this class represents yet another pattern + * within PL/Java for objects that combine Java and native state. It is meant + * to be general (and documented) enough to support future gradual migration of + * other existing patterns to it. + *

+ * A parameter to the {@code DualState} constructor is a {@code ResourceOwner}, + * a PostgreSQL implementation concept introduced in PG 8.0. Instances will be + * called at their {@link #nativeStateReleased nativeStateReleased} methods + * when the corresponding {@code ResourceOwner} is released in PostgreSQL. + *

+ * Instances will be enqueued on a {@link ReferenceQueue} when found by the Java + * garbage collector to be unreachable. The + * {@link #cleanEnqueuedInstances cleanEnqueuedInstances} static method will + * call those instances at their + * {@link #javaStateUnreachable javaStateUnreachable} methods if the weak + * reference has already been cleared by the garbage collector. The method + * should clean up any lingering native state. + *

+ * As the native cleanup is likely to involve calls into PostgreSQL, to reduce + * thread contention, {@code cleanEnqueuedInstances} should be called in one or + * more likely places from a thread already known to be entering/exiting Java + * from/to PostgreSQL. + *

+ * If convenient, explicit close actions from Java can be handled similarly, + * by having the close method call {@link #enqueue enqueue}, and providing a + * {@link #javaStateReleased javaStateReleased} method, which will be called by + * {@code cleanEnqueuedInstances} if the weak reference is nonnull, indicating + * the instance was enqueued explicitly rather than by the garbage collector. + */ +public abstract class DualState extends WeakReference +{ + /** + * {@code DualState} objects Java no longer needs. + *

+ * They will turn up on this queue (with referent already set null) if + * the garbage collector has determined them to be unreachable. They can + * also arrive here (with referent not yet set null) if some Java + * method (such as a {@code close} or {@code free} has called + * {@code enqueue}; whether the referent is null allows the cases to be + * distinguished. + *

+ * The queue is only processed by a private method called from native code + * in selected places where it makes sense to do so. + */ + private static ReferenceQueue s_releasedInstances = + new ReferenceQueue(); + + /** + * All instances are added to this collection upon creation. + */ + private static Deque s_liveInstances = + new LinkedBlockingDeque(); + + /** + * Pointer value of the {@code ResourceOwner} this instance belongs to, + * if any. + */ + private final long m_resourceOwner; + + /** + * Construct a {@code DualState} instance with a reference to the Java + * object whose state it represents. + *

+ * Subclass constructors must accept a cookie parameter from the + * native caller, and pass it along to superclass constructors. That allows + * some confidence that constructor parameters representing native values + * are for real, and also that the construction is taking place on a thread + * holding the native lock, keeping the concurrency story simple. + * @param cookie Capability held by native code to invoke {@code DualState} + * constructors. + * @param referent The Java object whose state this instance represents. + * @param resourceOwner Pointer value of the native {@code ResourceOwner} + * whose release callback will indicate that this object's native state is + * no longer valid. + */ + protected DualState(Key cookie, T referent, long resourceOwner) + { + super(referent, s_releasedInstances); + + if ( ! (cookie instanceof Key) ) + throw new UnsupportedOperationException( + "Constructing DualState instance without cookie"); + + m_resourceOwner = resourceOwner; + s_liveInstances.add(this); + } + + /** + * Return {@code true} if the native state is still valid. An abstract + * method so it can be tailored to whatever native state is maintained + * by an implementing class. + */ + protected abstract boolean nativeStateIsValid(); + + /** + * Method that will be called when the associated {@code ResourceOwner} + * is released, indicating that the native portion of the state + * is no longer valid. The implementing class should clean up + * whatever is appropriate to that event, and must ensure that + * {@code nativeStateIsValid} will thereafter return {@code false}. + *

+ * This object's monitor will always be held when this method is called + * during resource owner release. The class whose state this is must + * synchronize and hold this object's monitor for the duration of any + * operation that could refer to the native state. + */ + protected abstract void nativeStateReleased(); + + /** + * Method that will be called when the Java garbage collector has determined + * the referent object is no longer strongly reachable. This default + * implementation does nothing; a subclass should override it to do any + * cleanup, or release of native resources, that may be required. + *

+ * It is not necessary for this method to remove the instance from the + * {@code liveInstances} collection; that will have been done just before + * this method is called. + */ + protected void javaStateUnreachable() + { + } + + /** + * Method that can be called to indicate that Java code has explicitly + * released the instance (for example, through calling a {@code close} + * method on the referent object). This can be handled two ways: + *

    + *
  • A {@code close} or similar method calls this directly. This instance + * must be removed from the {@code liveInstances} collection. This default + * implementation does so. + *
  • A {@code close} or similar method simply calls + * {@link #enqueue enqueue} instead of this method. This method will be + * called when the queue is processed, the next time native code calls + * {@link #clearEnqueuedInstances clearEnqueuedInstances}. For that case, + * this method should be overridden to do whatever other cleanup is in + * order, but not remove the instance from {@code liveInstances}, + * which will have happened just before this method is called. + *
+ */ + protected void javaStateReleased() + { + s_liveInstances.remove(this); + } + + /** + * Throw an {@code SQLException} with a specified message and SQLSTATE code + * if {@code nativeStateIsValid} returns {@code false}. + */ + public void assertNativeStateIsValid(String message, String sqlState) + throws SQLException + { + if ( ! nativeStateIsValid() ) + throw new SQLException(message, sqlState); + } + + /** + * Throw an {@code SQLException} with a specified message and SQLSTATE code + * of 55000 "object not in prerequisite state" if {@code nativeStateIsValid} + * returns {@code false}. + */ + public void assertNativeStateIsValid(String message) + throws SQLException + { + assertNativeStateIsValid(message, "55000"); + } + + /** + * Throw an {@code SQLException} with a default message and SQLSTATE code + * of 55000 "object not in prerequisite state" if {@code nativeStateIsValid} + * returns {@code false}. + */ + public void assertNativeStateIsValid() + throws SQLException + { + if ( ! nativeStateIsValid() ) + { + Object referent = get(); + String message; + if ( null != referent ) + message = referent.getClass().getName(); + else + message = getClass().getName(); + message += " used beyond its PostgreSQL lifetime"; + throw new SQLException(message, "55000"); + } + } + + /** + * Called only from native code by the {@code ResourceOwner} callback when a + * resource owner is being released. Must identify the live instances that + * have been registered to that owner, if any, and call their + * {@link #nativeStateReleased nativeStateReleased} methods. + * @param resourceOwner Pointer value identifying the resource owner being + * released. Calls can be received for resource owners to which no instances + * here have been registered. + */ + private static void resourceOwnerRelease(long resourceOwner) + { + for ( Iterator i = s_liveInstances.iterator(); + i.hasNext(); ) + { + DualState s = i.next(); + if ( s.m_resourceOwner == resourceOwner ) + { + i.remove(); + synchronized ( s ) + { + s.nativeStateReleased(); + } + } + } + } + + /** + * Called only from native code, at points where checking the + * freed/unreachable objects queue would be useful. Calls the + * {@link #javaStateUnreachable javaStateUnreachable} method for instances + * that were cleared and enqueued by the garbage collector; calls the + * {@link #javaStateReleased javaStateReleased} method for instances that + * have not yet been garbage collected, but were enqueued by Java code + * explicitly calling {@link #enqueue enqueue}. + */ + private static void cleanEnqueuedInstances() + { + DualState s; + while ( null != (s = (DualState)s_releasedInstances.poll()) ) + { + s_liveInstances.remove(s); + try + { + if ( null == s.get() ) + s.javaStateUnreachable(); + else + s.javaStateReleased(); + } + catch ( Throwable t ) { } /* JDK 9 Cleaner ignores exceptions, so */ + } + } + + /** + * Magic cookie needed as a constructor parameter to confirm that + * {@code DualState} subclass instances are being constructed from + * native code. + */ + public static final class Key + { + private static boolean constructed = false; + private Key() + { + synchronized ( Key.class ) + { + if ( constructed ) + throw new IllegalStateException("Duplicate DualState.Key"); + constructed = true; + } + } + } +} From 498f5dff57f2818c3e3063a2516db8918a982e82 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 9 Apr 2018 00:23:21 -0400 Subject: [PATCH 0069/1087] Add a SinglePfree subclass of DualState. This should cover many cases where the native component of the state is a single pointer that only wants to be pfree'd when no longer needed. --- pljava-so/src/main/c/DualState.c | 38 +++++++- .../postgresql/pljava/internal/DualState.java | 86 +++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 2cbe6a44..56d63573 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -7,9 +7,11 @@ * http://opensource.org/licenses/BSD-3-Clause * * Contributors: + * Thomas Hallgren * Chapman Flack */ +#include "org_postgresql_pljava_internal_DualState_SinglePfree.h" #include "pljava/DualState.h" #include "pljava/PgObject.h" @@ -40,9 +42,18 @@ void pljava_DualState_initialize(void) jclass clazz; jmethodID ctor; + JNINativeMethod methods[] = + { + { + "_pfree", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SinglePfree__1pfree + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); - s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( s_DualState_class, "resourceOwnerRelease", "(J)V"); s_DualState_cleanEnqueuedInstances = PgObject_getStaticJavaMethod( @@ -54,6 +65,11 @@ void pljava_DualState_initialize(void) s_DualState_key = JNI_newGlobalRef(JNI_newObject(clazz, ctor)); JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SinglePfree"); + PgObject_registerNatives2(clazz, methods); + JNI_deleteLocalRef(clazz); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); } @@ -78,3 +94,23 @@ static void resourceReleaseCB(ResourceReleasePhase phase, s_DualState_resourceOwnerRelease, p2l.longVal); } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_SinglePfree + * Method: _pfree + * Signature: (J)V + * + * Cadged from JavaWrapper.c + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SinglePfree__1pfree( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + pfree(p2l.ptrVal); + END_NATIVE +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 8433ab83..606f3f3d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -301,4 +301,90 @@ private Key() } } } + + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code pfree} of a single pointer. + */ + public static abstract class SinglePfree extends DualState + { + private volatile long m_pointer; + + protected SinglePfree( + Key cookie, T referent, long resourceOwner, long pfreeTarget) + { + super(cookie, referent, resourceOwner); + m_pointer = pfreeTarget; + } + + /** + * For this class, the native state is valid whenever the wrapped + * pointer is not null. + */ + @Override + protected boolean nativeStateIsValid() + { + return 0 != m_pointer; + } + + /** + * When the native state is released, the wrapped pointer is nulled + * to indicate the state is no longer valid; no {@code pfree} call is + * made, on the assumption that the resource owner's release will be + * followed by wholesale release of the containing memory context + * anyway. + */ + @Override + protected void nativeStateReleased() + { + m_pointer = 0; + } + + /** + * When the Java state is released, the wrapped pointer is nulled to + * indicate the state is no longer valid, and a {@code pfree} + * call is made so the native memory is released without having to wait + * for release of its containing context. + *

+ * This overrides the inherited default, which would have removed this + * instance from the live instances collection. Users of this class + * should not call this method directly, but simply call + * {@link #enqueue enqueue}, and let the reclamation happen when the + * queue is processed. + */ + @Override + protected void javaStateReleased() + { + synchronized(Backend.THREADLOCK) + { + long p = m_pointer; + m_pointer = 0; + if ( 0 != p ) + _pfree(p); + } + } + + /** + * This override simply calls + * {@link #javaStateReleased javaStateReleased}, so there is no + * difference in the effect of the Java object being explicitly + * released, or found unreachable by the garbage collector. + */ + @Override + protected void javaStateUnreachable() + { + javaStateReleased(); + } + + /** + * Allows a subclass to obtain the wrapped pointer value. + */ + protected long getPointer() throws SQLException + { + assertNativeStateIsValid(); + return m_pointer; + } + + private native void _pfree(long pointer); + } } From 2f9844c0ab4d18fcf397178c2ec7acc72a5e9899 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 17:50:33 -0400 Subject: [PATCH 0070/1087] Add VarlenaWrapper. A VarlenaWrapper.Input presents a readable view of a detoasted datum residing in native memory. It is constructed from native code, specifying the memory context to (always copy or) detoast into, and a ResourceOwner bounding the life of the native copy (which must be released no later than the specified memory context is reset). The available() method returns the actual detoasted size. It would also be easy to support mark/reset, but nothing urgently needs them at the moment. --- pljava-so/src/main/c/DualState.c | 11 ++ pljava-so/src/main/c/VarlenaWrapper.c | 63 +++++++ .../src/main/include/pljava/VarlenaWrapper.h | 32 ++++ .../pljava/internal/VarlenaWrapper.java | 166 ++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 pljava-so/src/main/c/VarlenaWrapper.c create mode 100644 pljava-so/src/main/include/pljava/VarlenaWrapper.h create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 56d63573..6fae486c 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -16,6 +16,12 @@ #include "pljava/PgObject.h" #include "pljava/JNICalls.h" + +/* + * Includes for objects dependent on DualState, so they can be initialized here + */ +#include "pljava/VarlenaWrapper.h" + static jclass s_DualState_class; static jmethodID s_DualState_resourceOwnerRelease; @@ -71,6 +77,11 @@ void pljava_DualState_initialize(void) JNI_deleteLocalRef(clazz); RegisterResourceReleaseCallback(resourceReleaseCB, NULL); + + /* + * Call initialize() methods of known classes built upon DualState. + */ + pljava_VarlenaWrapper_initialize(); } static void resourceReleaseCB(ResourceReleasePhase phase, diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c new file mode 100644 index 00000000..b0c5afd7 --- /dev/null +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018 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: + * Thomas Hallgren + * Chapman Flack + */ + +#include "pljava/VarlenaWrapper.h" +#include "pljava/DualState.h" + +#include "pljava/PgObject.h" +#include "pljava/JNICalls.h" + +static jclass s_VarlenaWrapper_Input_class; + +static jmethodID s_VarlenaWrapper_Input_init; + +jobject pljava_VarlenaWrapper_Input(Datum d, MemoryContext mc, ResourceOwner ro) +{ + jobject vr; + jobject dbb; + MemoryContext prevcxt; + struct varlena *copy; + Ptr2Long p2lro; + Ptr2Long p2ldatum; + + prevcxt = MemoryContextSwitchTo(mc); + copy = PG_DETOAST_DATUM_COPY(d); + MemoryContextSwitchTo(prevcxt); + + p2lro.longVal = 0L; + p2ldatum.longVal = 0L; + + p2lro.ptrVal = ro; + p2ldatum.ptrVal = copy; + + dbb = JNI_newDirectByteBuffer(VARDATA(copy), VARSIZE_ANY_EXHDR(copy)); + + vr = JNI_newObjectLocked(s_VarlenaWrapper_Input_class, + s_VarlenaWrapper_Input_init, pljava_DualState_key(), + p2lro.longVal, p2ldatum.longVal, dbb); + JNI_deleteLocalRef(dbb); + + return vr; +} + +void pljava_VarlenaWrapper_initialize(void) +{ + s_VarlenaWrapper_Input_class = + (jclass)JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/VarlenaWrapper$Input")); + + s_VarlenaWrapper_Input_init = PgObject_getJavaMethod( + s_VarlenaWrapper_Input_class, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;" + "JJLjava/nio/ByteBuffer;)V"); +} diff --git a/pljava-so/src/main/include/pljava/VarlenaWrapper.h b/pljava-so/src/main/include/pljava/VarlenaWrapper.h new file mode 100644 index 00000000..8fc71bb2 --- /dev/null +++ b/pljava-so/src/main/include/pljava/VarlenaWrapper.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018 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 + */ +#ifndef __pljava_VarlenaWrapper_h +#define __pljava_VarlenaWrapper_h + +#include +#include + +#include "pljava/pljava.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern jobject pljava_VarlenaWrapper_Input( + Datum d, MemoryContext mc, ResourceOwner ro); + +extern void pljava_VarlenaWrapper_initialize(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java new file mode 100644 index 00000000..62322b6d --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2018 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.internal; + +import java.io.IOException; +import java.io.InputStream; + +import java.nio.ByteBuffer; + +import java.sql.SQLException; + +public interface VarlenaWrapper +{ + /** + * A class by which Java reads the content of a varlena as an InputStream. + * + * Associated with a {@code ResourceOwner} to bound the lifetime of + * the native reference; the chosen resource owner must be one that will be + * released no later than the memory context containing the varlena. + */ + public static class Input extends InputStream + { + private State m_state; + private boolean m_open = true; + + /** + * Construct a {@code VarlenaWrapper.Input}. + * @param cookie Capability held by native code. + * @param resourceOwner Resource owner whose release will indicate that the + * underlying varlena is no longer valid. + * @param varlenaPtr Pointer value to the underlying varlena, to be + * {@code pfree}d when Java code closes or reclaims this object. + * @param buf Readable direct {@code ByteBuffer} constructed over the + * varlena's data bytes. + */ + private Input(DualState.Key cookie, long resourceOwner, + long varlenaPtr, ByteBuffer buf) + { + m_state = new State( + cookie, this, resourceOwner, varlenaPtr, buf.asReadOnlyBuffer()); + } + + private ByteBuffer buf() throws IOException + { + if ( ! m_open ) + throw new IOException("Read from closed VarlenaWrapper"); + try + { + return m_state.buffer(); + } + catch ( SQLException sqe ) + { + throw new IOException("Read from varlena failed", sqe); + } + } + + @Override + public int read() throws IOException + { + synchronized ( m_state ) + { + ByteBuffer src = buf(); + if ( 0 < src.remaining() ) + return src.get(); + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException + { + synchronized ( m_state ) + { + ByteBuffer src = buf(); + int has = src.remaining(); + if ( len > has ) + { + if ( 0 == has ) + return -1; + len = has; + } + src.get(b, off, len); + return len; + } + } + + @Override + public long skip(long n) throws IOException + { + synchronized ( m_state ) + { + ByteBuffer src = buf(); + int has = src.remaining(); + if ( n > has ) + n = has; + src.position(src.position() + (int)n); + return n; + } + } + + @Override + public int available() throws IOException + { + synchronized ( m_state ) + { + return buf().remaining(); + } + } + + @Override + public void close() throws IOException + { + synchronized ( m_state ) + { + if ( ! m_open ) + return; + m_open = false; + m_state.enqueue(); + } + } + + + + private static class State extends DualState.SinglePfree + { + private ByteBuffer m_buf; + + private State( + DualState.Key cookie, Input vr, long resourceOwner, + long varlenaPtr, ByteBuffer buf) + { + super(cookie, vr, resourceOwner, varlenaPtr); + m_buf = buf; + } + + private ByteBuffer buffer() throws SQLException + { + assertNativeStateIsValid(); + return m_buf; + } + + @Override + protected void nativeStateReleased() + { + m_buf = null; + super.nativeStateReleased(); + } + + @Override + protected void javaStateReleased() + { + m_buf = null; + super.javaStateReleased(); + } + } + } +} From f0bb8b5c86bbe4b1f20591caa9645c5b692c2a9a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 22:18:11 -0400 Subject: [PATCH 0071/1087] Add SQLXMLImpl.Readable. This implements the reading (get...) operations, so it is the kind of SQLXML instance that would be passed by PostgreSQL to a Java function as a parameter, or retrieved from the ResultSet of a query. --- pljava-so/src/main/c/type/SQLXMLImpl.c | 52 +++++++ pljava-so/src/main/c/type/Type.c | 3 + .../pljava/internal/InstallHelper.java | 16 ++ .../postgresql/pljava/internal/Session.java | 27 ++++ .../postgresql/pljava/jdbc/SQLXMLImpl.java | 145 ++++++++++++++++++ 5 files changed, 243 insertions(+) create mode 100644 pljava-so/src/main/c/type/SQLXMLImpl.c diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c new file mode 100644 index 00000000..4154ace8 --- /dev/null +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018 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 + */ +#include + +#include "pljava/type/Type_priv.h" +#include "pljava/VarlenaWrapper.h" + +static jclass s_SQLXML_Readable_class; +static jmethodID s_SQLXML_Readable_init; + +static jvalue _SQLXML_coerceDatum(Type self, Datum arg) +{ + jvalue result; + jobject vwi = pljava_VarlenaWrapper_Input( + arg, TopTransactionContext, TopTransactionResourceOwner); + result.l = JNI_newObject( + s_SQLXML_Readable_class, s_SQLXML_Readable_init, vwi); + JNI_deleteLocalRef(vwi); + return result; +} + +static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) +{ + return 0; /* haven't got that direction yet */ +} + +/* Make this datatype available to the postgres system. + */ +extern void pljava_SQLXMLImpl_initialize(void); +void pljava_SQLXMLImpl_initialize(void) +{ + TypeClass cls = TypeClass_alloc("type.SQLXML"); + cls->JNISignature = "Ljava/sql/SQLXML;"; + cls->javaTypeName = "java.sql.SQLXML"; + cls->coerceDatum = _SQLXML_coerceDatum; + cls->coerceObject = _SQLXML_coerceObject; + Type_registerType("java.sql.SQLXML", TypeClass_allocInstance(cls, XMLOID)); + + s_SQLXML_Readable_class = JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/jdbc/SQLXMLImpl$Readable")); + s_SQLXML_Readable_init = PgObject_getJavaMethod(s_SQLXML_Readable_class, + "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Input;)V"); +} diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 9b12fa3f..bad72926 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -736,6 +736,8 @@ extern void TupleTable_initialize(void); extern void Composite_initialize(void); +extern void pljava_SQLXMLImpl_initialize(void); + extern void Type_initialize(void); void Type_initialize(void) { @@ -778,6 +780,7 @@ void Type_initialize(void) TupleTable_initialize(); Composite_initialize(); + pljava_SQLXMLImpl_initialize(); s_Map_class = JNI_newGlobalRef(PgObject_getJavaClass("java/util/Map")); s_Map_get = PgObject_getJavaMethod(s_Map_class, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 71818a44..970d7d3c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -12,6 +12,7 @@ package org.postgresql.pljava.internal; import java.io.InputStream; +import java.nio.charset.Charset; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; @@ -101,6 +102,21 @@ public static String hello( System.clearProperty(orderKey + ".scalar"); System.clearProperty(orderKey + ".mirror"); + String encodingKey = "org.postgresql.server.encoding"; + String encName = System.getProperty(encodingKey); + if ( null == encName ) + encName = Backend.getConfigOption( "server_encoding"); + try + { + Charset cs = Charset.forName(encName); + org.postgresql.pljava.internal.Session.s_serverCharset = cs; // poke + System.setProperty(encodingKey, cs.name()); + } + catch ( IllegalArgumentException iae ) + { + System.clearProperty(encodingKey); + } + /* * Construct the strings announcing the versions in use. */ diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java index 0f55cdad..60103ff2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java @@ -6,6 +6,8 @@ */ package org.postgresql.pljava.internal; +import java.nio.charset.Charset; + import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -28,6 +30,31 @@ public class Session implements org.postgresql.pljava.Session { private final TransactionalMap m_attributes = new TransactionalMap(new HashMap()); + /** + * The Java charset corresponding to the server encoding, or null if none + * such was found. Put here by InstallHelper via package access at startup. + */ + static Charset s_serverCharset; + + /** + * A static method (not part of the API-exposed Session interface) by which + * pljava implementation classes can get hold of the server charset without + * the indirection of getting a Session instance. If there turns out to be + * demand for client code to obtain it through the API, an interface method + * {@code serverCharset} can easily be added later. + * @return The Java Charset corresponding to the server's encoding, or null + * if no matching Java charset was found. That can happen if a corresponding + * Java charset really does exist but is not successfully found using the + * name reported by PostgreSQL. That can be worked around by giving the + * right name explicitly as the system property + * {@code org.postgresql.server.encoding} in {@code pljava.vmoptions} for + * the affected database (or cluster-wide, if the same encoding is used). + */ + public static Charset implServerCharset() + { + return s_serverCharset; + } + /** * Adds the specified listener to the list of listeners that will * receive transactional events. diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 96c8e219..0d693ef7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -33,6 +33,32 @@ import java.sql.SQLNonTransientException; +/* Supplemental imports for SQLXMLImpl.Readable */ + +import java.io.InputStreamReader; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.xml.transform.stream.StreamSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stax.StAXSource; +import javax.xml.transform.dom.DOMSource; + +import org.xml.sax.InputSource; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import static org.postgresql.pljava.internal.Session.implServerCharset; +import org.postgresql.pljava.internal.VarlenaWrapper; + +import java.sql.SQLFeatureNotSupportedException; + public abstract class SQLXMLImpl implements SQLXML { protected AtomicReference m_backing; @@ -155,4 +181,123 @@ protected SQLException normalizedException(Exception e) "Exception in XML processing, not otherwise provided for", "XX000", e); } + + static class Readable extends SQLXMLImpl + { + private AtomicBoolean m_readable = new AtomicBoolean(true); + private Charset m_serverCS = implServerCharset(); + + private Readable(VarlenaWrapper.Input vwi) throws SQLException + { + super(vwi); + if ( null == m_serverCS ) + { + try + { + vwi.close(); + } + catch ( IOException ioe ) { } + throw new SQLFeatureNotSupportedException("SQLXML: no Java " + + "Charset found to match server encoding; perhaps set " + + "org.postgresql.server.encoding system property to a " + + "valid Java charset name for the same encoding?", "0A000"); + } + } + + private InputStream backingAndClearReadable() throws SQLException + { + InputStream backing = backingIfNotFreed(); + return m_readable.getAndSet(false) ? backing : null; + } + + @Override + public InputStream getBinaryStream() throws SQLException + { + InputStream is = backingAndClearReadable(); + if ( null == is ) + return super.getBinaryStream(); + return is; + } + + @Override + public Reader getCharacterStream() throws SQLException + { + InputStream is = backingAndClearReadable(); + if ( null == is ) + return super.getCharacterStream(); + return new InputStreamReader(is, m_serverCS.newDecoder()); + } + + @Override + public String getString() throws SQLException + { + InputStream is = backingAndClearReadable(); + if ( null == is ) + return super.getString(); + + Reader r = new InputStreamReader(is, m_serverCS.newDecoder()); + CharBuffer cb = CharBuffer.allocate(32768); + StringBuilder sb = new StringBuilder(); + try { + while ( -1 != r.read(cb) ) + { + sb.append((CharBuffer)cb.flip()); + cb.clear(); + } + r.close(); + return sb.toString(); + } + catch ( Exception e ) + { + throw normalizedException(e); + } + } + + @Override + public T getSource(Class sourceClass) + throws SQLException + { + InputStream is = backingAndClearReadable(); + if ( null == is ) + return super.getSource(sourceClass); + + if ( null == sourceClass || Source.class == sourceClass ) + sourceClass = (Class)StreamSource.class; // trust me on this + + try + { + if ( sourceClass.isAssignableFrom(StreamSource.class) ) + return sourceClass.cast( + new StreamSource(is)); + + if ( sourceClass.isAssignableFrom(SAXSource.class) ) + return sourceClass.cast( + new SAXSource(new InputSource(is))); + + if ( sourceClass.isAssignableFrom(StAXSource.class) ) + { + XMLInputFactory xif = XMLInputFactory.newFactory(); + XMLStreamReader xsr = + xif.createXMLStreamReader(is); + return sourceClass.cast(new StAXSource(xsr)); + } + + if ( sourceClass.isAssignableFrom(DOMSource.class) ) + { + DocumentBuilderFactory dbf = + DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + return sourceClass.cast(new DOMSource(db.parse(is))); + } + } + catch ( Exception e ) + { + throw normalizedException(e); + } + + throw new SQLFeatureNotSupportedException( + "No support for SQLXML.getSource(" + + sourceClass.getName() + ".class)", "0A000"); + } + } } From ae412dbd7c00a88f3b09a309852d378a36b6083a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 18:35:15 -0400 Subject: [PATCH 0072/1087] Recognize type SQLXML in the annotation processor. --- .../src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 023dde70..6bb62ce9 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -2200,6 +2200,7 @@ class TypeMapper this.addMap(Timestamp.class, "timestamp"); this.addMap(Time.class, "time"); this.addMap(java.sql.Date.class, "date"); + this.addMap(java.sql.SQLXML.class, "xml"); this.addMap(BigInteger.class, "numeric"); this.addMap(BigDecimal.class, "numeric"); this.addMap(ResultSet.class, "record"); From 2950d96cfa4b5f2de2402cae37924270317dafaa Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 12 May 2018 20:39:18 -0400 Subject: [PATCH 0073/1087] Add a rudimentary example. --- .../pljava/example/annotation/PassXML.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java 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 new file mode 100644 index 00000000..500b3b98 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018- 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.sql.SQLDataException; +import java.sql.SQLException; +import java.sql.SQLXML; + +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; + +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stax.StAXSource; + +import org.postgresql.pljava.annotation.Function; + +public class PassXML +{ + static SQLXML s_sx; + + static TransformerFactory s_tf = TransformerFactory.newInstance(); + + /** + * Echo an XML parameter back as a string, exercising seven different ways + * (how => 1-7) of reading an SQLXML object. + *

+ * If how => 0, the XML parameter is simply saved in a static. It can be + * read in a subsequent call with sx => null, but only in the same + * transaction. + */ + @Function(schema="javatest") + public static String echoXMLParameter(SQLXML sx, int how) + throws SQLException + { + if ( null == sx ) + sx = s_sx; + if ( 0 == how ) + { + s_sx = sx; + return "(saved)"; + } + return echoSQLXML(sx, how); + } + + private static String echoSQLXML(SQLXML sx, int how) throws SQLException + { + Source src; + + switch ( how ) + { + case 1: + src = new StreamSource(sx.getBinaryStream()); + break; + case 2: + src = new StreamSource(sx.getCharacterStream()); + break; + case 3: + src = new StreamSource(new StringReader(sx.getString())); + break; + case 4: + src = sx.getSource(DOMSource.class); + break; + case 5: + src = sx.getSource(SAXSource.class); + break; + case 6: + src = sx.getSource(StAXSource.class); + break; + case 7: + src = sx.getSource(StreamSource.class); + break; + default: + throw new SQLDataException("how should be 1-7", "22003"); + } + + StringWriter sw = new StringWriter(); + Result rlt = new StreamResult(sw); + + try + { + Transformer t = s_tf.newTransformer(); + t.transform(src, rlt); + } + catch ( TransformerException te ) + { + throw new SQLException("XML transformation failed", te); + } + + return sw.toString(); + } +} From 593e30d1aac9016ecea2bd81cdeaf814eef56892 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 18:45:44 -0400 Subject: [PATCH 0074/1087] Honor API spec for namespaces with getSource(). "Sources for XML parsers will have namespace processing on by default." --- .../java/org/postgresql/pljava/jdbc/SQLXMLImpl.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 0d693ef7..780dcb90 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -47,6 +47,8 @@ import javax.xml.transform.dom.DOMSource; import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamReader; @@ -271,12 +273,18 @@ public T getSource(Class sourceClass) new StreamSource(is)); if ( sourceClass.isAssignableFrom(SAXSource.class) ) + { + XMLReader xr = XMLReaderFactory.createXMLReader(); + xr.setFeature("http://xml.org/sax/features/namespaces", + true); return sourceClass.cast( - new SAXSource(new InputSource(is))); + new SAXSource(xr, new InputSource(is))); + } if ( sourceClass.isAssignableFrom(StAXSource.class) ) { XMLInputFactory xif = XMLInputFactory.newFactory(); + xif.setProperty(xif.IS_NAMESPACE_AWARE, true); XMLStreamReader xsr = xif.createXMLStreamReader(is); return sourceClass.cast(new StAXSource(xsr)); @@ -286,6 +294,7 @@ public T getSource(Class sourceClass) { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); return sourceClass.cast(new DOMSource(db.parse(is))); } From c03b4e6203a6ca0888f4c8068b266e31c530a72e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 22:22:51 -0400 Subject: [PATCH 0075/1087] Ensure declaration, if any, is accurate. The current PostgreSQL stored form for the XML type is a serialized character string in the server encoding, still preceded by any XMLDecl or TextDecl that preceded it, even if wrong (about the encoding, anyway). PG's xml_out and xml_send deal with that by replacing the decl, if any, with a newly-constructed one, and have their own problems, but that's the basic technique reproduced here. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 559 +++++++++++++++++- 1 file changed, 550 insertions(+), 9 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 780dcb90..2063fa2e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -24,7 +24,7 @@ import java.sql.SQLException; -/* Supplemental imports for SQLXMLImpl */ +/* ... for SQLXMLImpl */ import java.io.Closeable; import java.io.IOException; @@ -33,7 +33,7 @@ import java.sql.SQLNonTransientException; -/* Supplemental imports for SQLXMLImpl.Readable */ +/* ... for SQLXMLImpl.Readable */ import java.io.InputStreamReader; import java.nio.CharBuffer; @@ -61,6 +61,16 @@ import java.sql.SQLFeatureNotSupportedException; +/* ... for SQLXMLImpl.DeclProbe */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.SequenceInputStream; + +import java.util.Arrays; + +import java.sql.SQLDataException; + public abstract class SQLXMLImpl implements SQLXML { protected AtomicReference m_backing; @@ -218,7 +228,14 @@ public InputStream getBinaryStream() throws SQLException InputStream is = backingAndClearReadable(); if ( null == is ) return super.getBinaryStream(); - return is; + try + { + return correctedDeclStream(is); + } + catch ( IOException e ) + { + throw normalizedException(e); + } } @Override @@ -227,7 +244,15 @@ public Reader getCharacterStream() throws SQLException InputStream is = backingAndClearReadable(); if ( null == is ) return super.getCharacterStream(); - return new InputStreamReader(is, m_serverCS.newDecoder()); + try + { + is = correctedDeclStream(is); + return new InputStreamReader(is, m_serverCS.newDecoder()); + } + catch ( IOException e ) + { + throw normalizedException(e); + } } @Override @@ -237,10 +262,12 @@ public String getString() throws SQLException if ( null == is ) return super.getString(); - Reader r = new InputStreamReader(is, m_serverCS.newDecoder()); CharBuffer cb = CharBuffer.allocate(32768); StringBuilder sb = new StringBuilder(); - try { + try + { + is = correctedDeclStream(is); + Reader r = new InputStreamReader(is, m_serverCS.newDecoder()); while ( -1 != r.read(cb) ) { sb.append((CharBuffer)cb.flip()); @@ -270,7 +297,7 @@ public T getSource(Class sourceClass) { if ( sourceClass.isAssignableFrom(StreamSource.class) ) return sourceClass.cast( - new StreamSource(is)); + new StreamSource(correctedDeclStream(is))); if ( sourceClass.isAssignableFrom(SAXSource.class) ) { @@ -278,7 +305,8 @@ public T getSource(Class sourceClass) xr.setFeature("http://xml.org/sax/features/namespaces", true); return sourceClass.cast( - new SAXSource(xr, new InputSource(is))); + new SAXSource(xr, + new InputSource(correctedDeclStream(is)))); } if ( sourceClass.isAssignableFrom(StAXSource.class) ) @@ -286,7 +314,7 @@ public T getSource(Class sourceClass) XMLInputFactory xif = XMLInputFactory.newFactory(); xif.setProperty(xif.IS_NAMESPACE_AWARE, true); XMLStreamReader xsr = - xif.createXMLStreamReader(is); + xif.createXMLStreamReader(correctedDeclStream(is)); return sourceClass.cast(new StAXSource(xsr)); } @@ -296,6 +324,7 @@ public T getSource(Class sourceClass) DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); + is = correctedDeclStream(is); return sourceClass.cast(new DOMSource(db.parse(is))); } } @@ -308,5 +337,517 @@ public T getSource(Class sourceClass) "No support for SQLXML.getSource(" + sourceClass.getName() + ".class)", "0A000"); } + + /** + * Return an InputStream presenting the contents of the underlying + * varlena, but with the leading declaration corrected if need be. + *

+ * The current stored form in PG for the XML type is a character string + * in server encoding, which may or may not still include a declaration + * left over from an input or cast operation, which declaration may or + * may not be correct (about the encoding, anyway). + * @return An InputStream with its original decl, if any, replaced with + * a new one known to be correct, or none if the defaults are correct. + */ + private InputStream correctedDeclStream(InputStream is) + throws IOException, SQLException + { + byte[] buf = new byte[40]; + int got; + boolean needMore = false; + DeclProbe probe = new DeclProbe(); + + while ( -1 != ( got = is.read(buf) ) ) + { + for ( int i = 0 ; i < got ; ++ i ) + needMore = probe.take(buf[i]); + if ( ! needMore ) + break; + } + + /* + * At this point, for better or worse, the loop is done. There may + * or may not be more of m_vwi left to read; the probe may or may + * not have found a decl. If it didn't, prefix() will treat whatever + * had been read as readahead and hand it all back, so it suffices + * here to create a SequenceInputStream of the prefix and whatever + * is or isn't left of m_vwi. + * A bonus is that the SequenceInputStream closes each underlying + * stream as it reaches EOF. After the last stream is used up, the + * SequenceInputStream remains open-at-EOF until explicitly closed, + * providing the expected input-stream behavior, but the underlying + * resources don't have to stick around for that. + */ + InputStream pfx = + new ByteArrayInputStream(probe.prefix(m_serverCS)); + return new SequenceInputStream(pfx, is); + } + } + + /** + * A class to parse and, if necessary, check or correct, the + * possibly-erroneous XMLDecl or TextDecl syntax found in the stored form + * of PG's XML datatype. + *

+ * This implementation depends heavily on the (currently dependable) fact + * that, in all PG-supported server encodings, the characters that matter + * for decls are encoded as in ASCII. + */ + static class DeclProbe + { + /* + * In Python 3, they've achieved a very nice symmetry where they provide + * regular expressions with comparable functionality for both character + * streams and byte streams. Will Java ever follow suit? It's 2018, I + * can ask my phone spoken questions, and I'm writing a DFA by hand. + */ + private static enum State + { + START, + MAYBEVER, + VER, VEQ, VQ, VVAL, VVALTAIL, + MAYBEENC, + ENC, EEQ, EQ, EVAL, EVALTAIL, + MAYBESA, + SA , SEQ, SQ, SVAL, SVALTAIL, + TRAILING, END, MATCHED, UNMATCHED, ABANDONED + }; + private State m_state = State.START; + private int m_idx = 0; + private byte m_q = 0; + private ByteArrayOutputStream m_save = new ByteArrayOutputStream(); + private static final byte[] s_tpl = { + '<', '?', 'x', 'm', 'l', 0, // 0 - 5 + 'v', 'e', 'r', 's', 'i', 'o', 'n', 0, // 6 - 13 + '1', '.', 0, // 14 - 16 + 'e', 'n', 'c', 'o', 'd', 'i', 'n', 'g', 0, // 17 - 25 + 's', 't', 'a', 'n', 'd', 'a', 'l', 'o', 'n', 'e', 0, // 26 - 36 + 'y', 'e', 's', 0, // 37 - 40 + 'n', 'o', 0 // 41 - 43 + }; + + private boolean m_saving = true; + private int m_pos = 0; + private boolean m_mustBeDecl = false; + private boolean m_mustBeXmlDecl = false; + private boolean m_mustBeTextDecl = false; + private boolean m_xml1_0 = false; + private Boolean m_standalone = null; + + private int m_versionStart, m_versionEnd; + private int m_encodingStart, m_encodingEnd; + private int m_readaheadStart; + + /** + * Parse for an initial declaration (XMLDecl or TextDecl) in a stream + * made available a byte at a time. + *

+ * Pass bytes in as long as this method keeps returning {@code true}; + * once it returns {@code false}, it has either parsed a declaration + * successfully or determined none to be present. The results of parsing + * are remembered in the instance and available to the + * {@link #prefix prefix()} method to generate a suitable decl with the + * encoding corrected as needed. + *

+ * It is not an error to pass some more bytes after the method has + * returned {@code false}; they will simply be buffered as readahead + * and included in the result of {@code prefix()}. If no decl + * was found, the readahead will include all bytes passed in. If a + * partial or malformed decl was found, an exception is thrown. + * @param b The next byte of the stream. + * @return True if more input is needed to fully parse a decl or be sure + * that none is present; false when enough input has been seen. + * @throws SQLDataException If a partial or malformed decl is found. + */ + boolean take(byte b) throws SQLException + { + if ( m_saving ) + { + m_save.write(b); + ++ m_pos; + } + byte tpl = s_tpl[m_idx]; + switch ( m_state ) + { + case START: + if ( 0 == tpl && isSpace(b) ) + { + m_mustBeDecl = true; + m_saving = false; + m_state = State.MAYBEVER; + return true; + } + if ( tpl != b ) + { + m_state = State.UNMATCHED; + return false; + } + ++ m_idx; + return true; + case MAYBEVER: + if ( isSpace(b) ) + return true; + switch ( b ) + { + case 'v': + m_state = State.VER; + m_idx = 7; + return true; + case 'e': + m_mustBeTextDecl = true; + m_state = State.ENC; + m_idx = 18; + return true; + default: + } + break; + case VER: + if ( 0 == tpl ) + { + if ( isSpace(b) ) + { + m_state = State.VEQ; + return true; + } + if ( '=' == b ) + { + m_state = State.VQ; + return true; + } + } + if ( tpl != b ) + break; + ++ m_idx; + return true; + case VEQ: + if ( isSpace(b) ) + return true; + if ( '=' != b ) + break; + m_state = State.VQ; + return true; + case VQ: + if ( isSpace(b) ) + return true; + if ( '\'' != b && '"' != b) + break; + m_q = b; + m_state = State.VVAL; + m_idx = 14; + m_saving = true; + m_versionStart = m_pos; + return true; + case VVAL: + if ( 0 == tpl ) + { + if ( '0' > b || b > '9' ) + break; + if ( '0' == b ) + m_xml1_0 = true; + m_state = State.VVALTAIL; + return true; + } + if ( tpl != b ) + break; + ++ m_idx; + return true; + case VVALTAIL: + if ( '0' <= b && b <= '9' ) + { + m_xml1_0 = false; + return true; + } + if ( m_q != b ) + break; + m_state = State.MAYBEENC; + m_saving = false; + m_versionEnd = m_pos - 1; + return true; + case MAYBEENC: + if ( isSpace(b) ) + return true; + if ( 'e' == b ) + { + m_state = State.ENC; + m_idx = 18; + return true; + } + if ( m_mustBeTextDecl ) + break; + m_mustBeXmlDecl = true; + if ( 's' == b ) + { + m_state = State.SA; + m_idx = 27; + return true; + } + if ( '?' != b ) + break; + m_state = State.END; + return true; + case ENC: + if ( 0 == tpl ) + { + if ( isSpace(b) ) + { + m_state = State.EEQ; + return true; + } + if ( '=' == b ) + { + m_state = State.EQ; + return true; + } + } + if ( tpl != b ) + break; + ++ m_idx; + return true; + case EEQ: + if ( isSpace(b) ) + return true; + if ( '=' != b ) + break; + m_state = State.EQ; + return true; + case EQ: + if ( isSpace(b) ) + return true; + if ( '\'' != b && '"' != b) + break; + m_q = b; + m_state = State.EVAL; + m_saving = true; + m_encodingStart = m_pos; + return true; + case EVAL: + if ( ( 'A' > b || b > 'Z' ) && ( 'a' > b || b > 'z' ) ) + break; + m_state = State.EVALTAIL; + return true; + case EVALTAIL: + if ( ( 'A' <= b && b <= 'Z' ) || ( 'a' <= b && b <= 'z' ) || + ( '0' <= b && b <= '9' ) || ( '.' == b ) || ( '_' == b ) || + ( '-' == b ) ) + return true; + if ( m_q != b ) + break; + m_state = m_mustBeTextDecl ? State.TRAILING : State.MAYBESA; + m_saving = false; + m_encodingEnd = m_pos - 1; + return true; + case MAYBESA: + if ( isSpace(b) ) + return true; + switch ( b ) + { + case 's': + m_mustBeXmlDecl = true; + m_state = State.SA; + m_idx = 27; + return true; + case '?': + m_state = State.END; + return true; + default: + } + break; + case SA: + if ( 0 == tpl ) + { + if ( isSpace(b) ) + { + m_state = State.SEQ; + return true; + } + if ( '=' == b ) + { + m_state = State.SQ; + return true; + } + } + if ( tpl != b ) + break; + ++ m_idx; + return true; + case SEQ: + if ( isSpace(b) ) + return true; + if ( '=' != b ) + break; + m_state = State.SQ; + return true; + case SQ: + if ( isSpace(b) ) + return true; + if ( '\'' != b && '"' != b) + break; + m_q = b; + m_state = State.SVAL; + return true; + case SVAL: + if ( 'y' == b ) + { + m_idx = 38; + m_standalone = Boolean.TRUE; + } + else if ( 'n' == b ) + { + m_idx = 42; + m_standalone = Boolean.FALSE; + } + else + break; + m_state = State.SVALTAIL; + return true; + case SVALTAIL: + if ( 0 == tpl ) + { + if ( m_q != b ) + break; + m_state = State.TRAILING; + return true; + } + if ( tpl != b ) + break; + ++ m_idx; + return true; + case TRAILING: + if ( isSpace(b) ) + return true; + if ( '?' != b ) + break; + m_state = State.END; + return true; + case END: + if ( '>' != b ) + break; + m_state = State.MATCHED; + m_readaheadStart = m_pos; + m_saving = true; + return false; + case MATCHED: // no more input needed for a determination; + case UNMATCHED: // whatever more is provided, just buffer it + return false; // as readahead + case ABANDONED: + } + m_state = State.ABANDONED; + String m = "Invalid XML/Text declaration"; + if ( m_mustBeXmlDecl ) + m = "Invalid XML declaration"; + else if ( m_mustBeTextDecl ) + m = "Invalid text declaration"; + throw new SQLDataException(m, "2200N"); + } + + private boolean isSpace(byte b) + { + return (0x20 == b) || (0x09 == b) || (0x0D == b) || (0x0A == b); + } + + /** + * Generate a declaration, if necessary, with the XML version and + * standalone status determined in declaration parsing and the name of + * the server encoding, followed always by any readahead buffered during + * a nonmatching parse or following a matching one. + * @param serverCharset The encoding to be named in the declaration if + * one is generated (which is forced if the encoding isn't UTF-8). + * @return A byte array representing the declaration if any, followed + * by any readahead. + */ + byte[] prefix(Charset serverCharset) throws IOException + { + /* + * Will this be DOCUMENT or CONTENT ? + * Without some out-of-band indication, we just don't know yet. + * For now, get DOCUMENT working. + */ + // boolean mightBeDocument = true; + // boolean mightBeContent = true; + + /* + * Defaults for when no declaration was matched: + */ + boolean canOmitVersion = true; // no declaration => 1.0 + byte[] version = new byte[] { '1', '.', '0' }; + boolean canOmitEncoding = "UTF-8".equals(serverCharset.name()); + boolean canOmitStandalone = true; + + byte[] parseResult = m_save.toByteArray(); + + if ( State.MATCHED == m_state ) + { + /* + * Parsing the decl could have turned up a non-1.0 XML version, + * which would mean we can't neglect to declare it, As for any + * encoding found in the varlena, the value doesn't matter (PG + * always uses the server encoding, whatever the stored decl + * might say). Its presence or absence in the decl can influence + * m_mustBeXmlDecl: the grammar productions XMLDecl and TextDecl + * are slightly different, which in a better world could help + * distinguish DOCUMENT from CONTENT, but PG doesn't preserve + * the distinction, instead always omitting the encoding in + * xml_out, which only XMLDecl can match. This code isn't + * reading from xml_out ... but if the value has ever been put + * through PG expressions that involved casting, xml_out may + * have eaten the encoding at that time. + * So, for now, all that can be done here is refinement of + * canOmitVersion and canOmitStandalone. Also, PG's hand-laid + * parse_xml_decl always insists on the version being present, + * so if we produce a decl at all, it had better not have either + * the version or the encoding omitted. + */ + canOmitVersion = m_xml1_0; // && ! m_mustBeXmlDecl; + // canOmitEncoding &&= ! m_mustBeTextDecl; + canOmitStandalone = null == m_standalone; + if ( ! m_xml1_0 && m_versionEnd > m_versionStart ) + version = Arrays.copyOfRange(parseResult, + m_versionStart, m_versionEnd); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if ( ! ( canOmitVersion && canOmitEncoding && canOmitStandalone ) ) + { + baos.write(s_tpl, 0, 5); // '); + } + + baos.write(parseResult, + m_readaheadStart, parseResult.length - m_readaheadStart); + + return baos.toByteArray(); + } } } From 3ce8cbdf7c76d2f1b9c4439b24d2f025bd4816a7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 19:13:52 -0400 Subject: [PATCH 0076/1087] Add an example that applies a transform. --- .../pljava/example/annotation/PassXML.java | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) 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 500b3b98..e5dd1787 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 @@ -19,8 +19,12 @@ import java.io.StringReader; import java.io.StringWriter; +import java.util.Map; +import java.util.HashMap; + import javax.xml.transform.Result; import javax.xml.transform.Source; +import javax.xml.transform.Templates; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; @@ -39,6 +43,8 @@ public class PassXML static TransformerFactory s_tf = TransformerFactory.newInstance(); + static Map s_tpls = new HashMap(); + /** * Echo an XML parameter back as a string, exercising seven different ways * (how => 1-7) of reading an SQLXML object. @@ -61,37 +67,73 @@ public static String echoXMLParameter(SQLXML sx, int how) return echoSQLXML(sx, how); } - private static String echoSQLXML(SQLXML sx, int how) throws SQLException + /** + * Precompile an XSL transform {@code source} and save it (for the + * current session) as {@code name}. + *

+ * Each value of {@code how}, 1-7, selects a different way of presenting + * the {@code SQLXML} object to the XSL processor. + *

+ * Preparing a transform with + * {@link TransformerFactoory.newTemplates newTemplates()} seems to require + * {@link Function.Trust.UNSANDBOXED Trust.UNSANDBOXED}, at least for the + * XSLTC transform compiler in newer JREs. + */ + @Function(schema="javatest", trust=Function.Trust.UNSANDBOXED) + public static void prepareXMLTransform(String name, SQLXML source, int how) + throws SQLException + { + try + { + s_tpls.put(name, s_tf.newTemplates(sxToSource(source, how))); + } + catch ( TransformerException te ) + { + throw new SQLException("XML transformation failed", te); + } + } + + @Function(schema="javatest") + public static String transformXML( + String transformName, SQLXML source, int how) + throws SQLException { - Source src; + Templates tpl = s_tpls.get(transformName); + Source src = sxToSource(source, how); + StringWriter sw = new StringWriter(); + Result rlt = new StreamResult(sw); + try + { + Transformer t = tpl.newTransformer(); + t.transform(src, rlt); + } + catch ( TransformerException te ) + { + throw new SQLException("XML transformation failed", te); + } + + return sw.toString(); + } + + private static Source sxToSource(SQLXML sx, int how) throws SQLException + { switch ( how ) { - case 1: - src = new StreamSource(sx.getBinaryStream()); - break; - case 2: - src = new StreamSource(sx.getCharacterStream()); - break; - case 3: - src = new StreamSource(new StringReader(sx.getString())); - break; - case 4: - src = sx.getSource(DOMSource.class); - break; - case 5: - src = sx.getSource(SAXSource.class); - break; - case 6: - src = sx.getSource(StAXSource.class); - break; - case 7: - src = sx.getSource(StreamSource.class); - break; - default: - throw new SQLDataException("how should be 1-7", "22003"); + case 1: return new StreamSource(sx.getBinaryStream()); + case 2: return new StreamSource(sx.getCharacterStream()); + case 3: return new StreamSource(new StringReader(sx.getString())); + case 4: return sx.getSource(DOMSource.class); + case 5: return sx.getSource(SAXSource.class); + case 6: return sx.getSource(StAXSource.class); + case 7: return sx.getSource(StreamSource.class); + default: throw new SQLDataException("how should be 1-7", "22003"); } + } + private static String echoSQLXML(SQLXML sx, int how) throws SQLException + { + Source src = sxToSource(sx, how); StringWriter sw = new StringWriter(); Result rlt = new StreamResult(sw); From 93fb395cc39f3b31c49118f63c8f022a1ef8104e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 19:34:07 -0400 Subject: [PATCH 0077/1087] Make toString more useful for DualState. --- .../org/postgresql/pljava/internal/DualState.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 606f3f3d..eef2e901 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -231,6 +231,13 @@ public void assertNativeStateIsValid() } } + @Override + public String toString() + { + return String.format("DualState owner:%x %s", m_resourceOwner, + nativeStateIsValid() ? "fresh" : "stale"); + } + /** * Called only from native code by the {@code ResourceOwner} callback when a * resource owner is being released. Must identify the live instances that @@ -317,6 +324,12 @@ protected SinglePfree( m_pointer = pfreeTarget; } + @Override + public String toString() + { + return String.format("%s pfree(%x)", super.toString(), m_pointer); + } + /** * For this class, the native state is valid whenever the wrapped * pointer is not null. From c5992c2bc63dd6571722b9ced5a8e2c0376b1b15 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 21 Apr 2018 11:45:41 -0400 Subject: [PATCH 0078/1087] Add DualState.SingleMemContextDelete. A subclass of DualState that encapsulates a reference to a single PostgreSQL memory context, and will delete it if the Java reference is released or garbage collected. --- pljava-so/src/main/c/DualState.c | 38 ++++++- .../postgresql/pljava/internal/DualState.java | 101 ++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 6fae486c..9e0851dc 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -12,6 +12,7 @@ */ #include "org_postgresql_pljava_internal_DualState_SinglePfree.h" +#include "org_postgresql_pljava_internal_DualState_SingleMemContextDelete.h" #include "pljava/DualState.h" #include "pljava/PgObject.h" @@ -48,7 +49,7 @@ void pljava_DualState_initialize(void) jclass clazz; jmethodID ctor; - JNINativeMethod methods[] = + JNINativeMethod singlePfreeMethods[] = { { "_pfree", @@ -58,6 +59,16 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod singleMemContextDeleteMethods[] = + { + { + "_memContextDelete", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SingleMemContextDelete__1memContextDelete + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( @@ -73,7 +84,12 @@ void pljava_DualState_initialize(void) clazz = (jclass)PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState$SinglePfree"); - PgObject_registerNatives2(clazz, methods); + PgObject_registerNatives2(clazz, singlePfreeMethods); + JNI_deleteLocalRef(clazz); + + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SingleMemContextDelete"); + PgObject_registerNatives2(clazz, singleMemContextDeleteMethods); JNI_deleteLocalRef(clazz); RegisterResourceReleaseCallback(resourceReleaseCB, NULL); @@ -125,3 +141,21 @@ Java_org_postgresql_pljava_internal_DualState_00024SinglePfree__1pfree( pfree(p2l.ptrVal); END_NATIVE } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_SingleMemContextDelete + * Method: _memContextDelete + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SingleMemContextDelete__1memContextDelete( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + MemoryContextDelete(p2l.ptrVal); + END_NATIVE +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index eef2e901..aa04cd74 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -400,4 +400,105 @@ protected long getPointer() throws SQLException private native void _pfree(long pointer); } + + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code MemoryContextDelete} of a single context. + *

+ * This class may get called at the {@code nativeStateReleased) entry, not + * only if the native state is actually being released, but if it is being + * 'claimed' by native code for its own purposes. The effect is the same + * as far as Java is concerned; the object is no longer accessible, and the + * native code is responsible for whatever happens to it next. + */ + public static abstract class SingleMemContextDelete extends DualState + { + private volatile long m_context; + + protected SingleMemContextDelete( + Key cookie, T referent, long resourceOwner, long memoryContext) + { + super(cookie, referent, resourceOwner); + m_context = memoryContext; + } + + @Override + public String toString() + { + return String.format("%s MemoryContextDelete(%x)", super.toString(), + m_context); + } + + /** + * For this class, the native state is valid whenever the wrapped + * context pointer is not null. + */ + @Override + protected boolean nativeStateIsValid() + { + return 0 != m_context; + } + + /** + * When the native state is released, the wrapped pointer is nulled + * to indicate the state is no longer valid. No + * {@code MemoryContextDelete} call is + * made; this is important, as the native code may have other plans for + * the memory context, such as to relink it under a different parent + * context, etc. + */ + @Override + protected void nativeStateReleased() + { + m_context = 0; + } + + /** + * When the Java state is released, the wrapped pointer is nulled to + * indicate the state is no longer valid, and a + * {@code MemoryContextDelete} + * call is made so the native memory is released without having to wait + * for release of its parent context. + *

+ * This overrides the inherited default, which would have removed this + * instance from the live instances collection. Users of this class + * should not call this method directly, but simply call + * {@link #enqueue enqueue}, and let the reclamation happen when the + * queue is processed. + */ + @Override + protected void javaStateReleased() + { + synchronized(Backend.THREADLOCK) + { + long p = m_context; + m_context = 0; + if ( 0 != p ) + _memContextDelete(p); + } + } + + /** + * This override simply calls + * {@link #javaStateReleased javaStateReleased}, so there is no + * difference in the effect of the Java object being explicitly + * released, or found unreachable by the garbage collector. + */ + @Override + protected void javaStateUnreachable() + { + javaStateReleased(); + } + + /** + * Allows a subclass to obtain the wrapped pointer value. + */ + protected long getMemoryContext() throws SQLException + { + assertNativeStateIsValid(); + return m_context; + } + + private native void _memContextDelete(long pointer); + } } From 3a7a0145da0550dc11f762b92ae979403e06b471 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 21:11:00 -0400 Subject: [PATCH 0079/1087] Add VarlenaWrapper.Output. --- pljava-so/src/main/c/JNICalls.c | 19 ++ pljava-so/src/main/c/VarlenaWrapper.c | 229 ++++++++++++++++++ pljava-so/src/main/include/pljava/JNICalls.h | 2 + .../src/main/include/pljava/VarlenaWrapper.h | 4 + .../pljava/internal/VarlenaWrapper.java | 203 ++++++++++++++++ 5 files changed, 457 insertions(+) diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index e6b1bd8a..2f536d69 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -321,6 +321,25 @@ jlong JNI_callLongMethodV(jobject object, jmethodID methodID, va_list args) return result; } +jlong JNI_callLongMethodLocked(jobject object, jmethodID methodID, ...) +{ + jlong result; + va_list args; + va_start(args, methodID); + result = JNI_callLongMethodLockedV(object, methodID, args); + va_end(args); + return result; +} + +jlong JNI_callLongMethodLockedV(jobject object, jmethodID methodID, va_list args) +{ + jlong result; + BEGIN_CALL_MONITOR_HELD + result = (*env)->CallLongMethodV(env, object, methodID, args); + END_CALL_MONITOR_HELD + return result; +} + jshort JNI_callShortMethod(jobject object, jmethodID methodID, ...) { jshort result; diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index b0c5afd7..90bd969c 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -11,16 +11,70 @@ * Chapman Flack */ +#include "org_postgresql_pljava_internal_VarlenaWrapper_Output_State.h" #include "pljava/VarlenaWrapper.h" #include "pljava/DualState.h" #include "pljava/PgObject.h" #include "pljava/JNICalls.h" +#define INITIALSIZE 1024 + static jclass s_VarlenaWrapper_Input_class; +static jclass s_VarlenaWrapper_Output_class; static jmethodID s_VarlenaWrapper_Input_init; +static jmethodID s_VarlenaWrapper_Output_init; +static jmethodID s_VarlenaWrapper_Output_adopt; + +/* + * For VarlenaWrapper.Output, define a dead-simple "expanded object" format + * consisting of linked allocated blocks, so if a long value is being written, + * it does not have to get repeatedly reallocated and copied. The "expanded + * object" form is a valid sort of PostgreSQL Datum, and can be passed around + * in that form, and reparented between memory contexts with different + * lifetimes; when a time comes that PostgreSQL needs it in a 'flattened' + * form, it will use these 'methods' to flatten it, and that's when the one + * final reallocation and copy will happen. + */ +static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr); +static void VOS_flatten_into(ExpandedObjectHeader *eohptr, + void *result, Size allocated_size); + +static const ExpandedObjectMethods VOS_methods = +{ + VOS_get_flat_size, + VOS_flatten_into +}; + +typedef struct ExpandedVarlenaOutputStreamNode ExpandedVarlenaOutputStreamNode; + +struct ExpandedVarlenaOutputStreamNode +{ + ExpandedVarlenaOutputStreamNode *next; + Size size; +}; + +typedef struct ExpandedVarlenaOutputStreamHeader +{ + ExpandedObjectHeader hdr; + ExpandedVarlenaOutputStreamNode *tail; + Size total_size; +} ExpandedVarlenaOutputStreamHeader; + + + +/* + * Create and return a VarlenaWrapper.Input allowing Java to read the content + * of an existing Datum d, which must be a varlena type (assumed, not checked + * here). + * + * The datum will be copied (detoasting if need be) into the memory context mc, + * and the VarlenaWrapper will be associated with the ResourceOwner ro, which + * determines its lifespan. The ResourceOwner needs to be one that will be + * released no later than the memory context itself. + */ jobject pljava_VarlenaWrapper_Input(Datum d, MemoryContext mc, ResourceOwner ro) { jobject vr; @@ -50,14 +104,189 @@ jobject pljava_VarlenaWrapper_Input(Datum d, MemoryContext mc, ResourceOwner ro) return vr; } +/* + * Create and return a VarlenaWrapper.Output, initially empty, into which Java + * can write. + * + * The datum will be assembled in the memory context mc, and the VarlenaWrapper + * will be associated with the ResourceOwner ro, which determines its lifespan. + * The ResourceOwner needs to be one that will be released no later than + * the memory context itself. + * + * After Java has written the content, native code can obtain the Datum by + * calling pljava_VarlenaWrapper_Output_adopt(). + */ +jobject pljava_VarlenaWrapper_Output(MemoryContext parent, ResourceOwner ro) +{ + ExpandedVarlenaOutputStreamHeader *evosh; + jobject vos; + jobject dbb; + MemoryContext mc; + Ptr2Long p2lro; + Ptr2Long p2lcxt; + Ptr2Long p2ldatum; + + mc = AllocSetContextCreate(parent, "PL/Java VarlenaWrapper.Output", + ALLOCSET_START_SMALL_SIZES); + /* + * Allocate an initial chunk sized to contain the expanded V.O.S. header, + * plus the header and data for one node to hold INITIALSIZE data bytes. + */ + evosh = MemoryContextAlloc(mc, + sizeof *evosh + sizeof *(evosh->tail) + INITIALSIZE); + /* + * Initialize the expanded object header and its pointer to the first node. + */ + EOH_init_header(&(evosh->hdr), &VOS_methods, mc); + evosh->total_size = VARHDRSZ; + evosh->tail = (ExpandedVarlenaOutputStreamNode *)(evosh + 1); + /* + * Initialize that first node. + */ + evosh->tail->next = evosh->tail; + /* evosh->tail->size will be filled in by _nextBuffer() later */ + + p2lro.longVal = 0L; + p2lcxt.longVal = 0L; + p2ldatum.longVal = 0L; + + p2lro.ptrVal = ro; + p2lcxt.ptrVal = mc; + p2ldatum.ptrVal = DatumGetPointer(EOHPGetRWDatum(&(evosh->hdr))); + + /* + * The data bytes begin right after the node header struct. + */ + dbb = JNI_newDirectByteBuffer(evosh->tail + 1, INITIALSIZE); + + vos = JNI_newObjectLocked(s_VarlenaWrapper_Output_class, + s_VarlenaWrapper_Output_init, pljava_DualState_key(), + p2lro.longVal, p2lcxt.longVal, p2ldatum.longVal, dbb); + JNI_deleteLocalRef(dbb); + + return vos; +} + +/* + * Adopt a VarlenaWrapper.Output after Java code has written and closed it. + * The wrapper will no longer be accessible from Java. + */ +Datum pljava_VarlenaWrapper_Output_adopt(jobject vlos) +{ + Ptr2Long p2l; + + p2l.longVal = JNI_callLongMethodLocked(vlos, s_VarlenaWrapper_Output_adopt); + return PointerGetDatum(p2l.ptrVal); +} + +static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr) +{ + ExpandedVarlenaOutputStreamHeader *evosh = + (ExpandedVarlenaOutputStreamHeader *)eohptr; + return evosh->total_size; +} + +static void VOS_flatten_into(ExpandedObjectHeader *eohptr, + void *result, Size allocated_size) +{ + ExpandedVarlenaOutputStreamHeader *evosh = + (ExpandedVarlenaOutputStreamHeader *)eohptr; + ExpandedVarlenaOutputStreamNode *node = evosh->tail; + + Assert(allocated_size == evosh->total_size); + SET_VARSIZE(result, allocated_size); + result = VARDATA(result); + + do + { + node = node->next; + memcpy(result, node + 1, node->size); + result = (char *)result + node->size; + } + while ( node != evosh->tail ); +} + void pljava_VarlenaWrapper_initialize(void) { + jclass clazz; + JNINativeMethod methods[] = + { + { + "_nextBuffer", + "(JII)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Output_00024State__1nextBuffer + }, + { 0, 0, 0 } + }; + s_VarlenaWrapper_Input_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/VarlenaWrapper$Input")); + s_VarlenaWrapper_Output_class = + (jclass)JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/VarlenaWrapper$Output")); s_VarlenaWrapper_Input_init = PgObject_getJavaMethod( s_VarlenaWrapper_Input_class, "", "(Lorg/postgresql/pljava/internal/DualState$Key;" "JJLjava/nio/ByteBuffer;)V"); + + s_VarlenaWrapper_Output_init = PgObject_getJavaMethod( + s_VarlenaWrapper_Output_class, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;" + "JJJLjava/nio/ByteBuffer;)V"); + + s_VarlenaWrapper_Output_adopt = PgObject_getJavaMethod( + s_VarlenaWrapper_Output_class, "adopt", "()J"); + + clazz = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/VarlenaWrapper$Output$State")); + PgObject_registerNatives2(clazz, methods); + JNI_deleteLocalRef(clazz); +} + +/* + * Class: org_postgresql_pljava_internal_VarlenaWrapper_Output_State + * Method: _nextBuffer + * Signature: (JII)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Output_00024State__1nextBuffer + (JNIEnv *env, jobject _this, + jlong varlenaPtr, jint currentBufPosition, jint desiredCapacity) +{ + ExpandedVarlenaOutputStreamHeader *evosh; + ExpandedVarlenaOutputStreamNode *node; + Ptr2Long p2l; + Datum d; + jobject dbb; + + p2l.longVal = varlenaPtr; + d = PointerGetDatum(p2l.ptrVal); + + evosh = (ExpandedVarlenaOutputStreamHeader *)DatumGetEOHP(d); + evosh->tail->size = currentBufPosition; + evosh->total_size += currentBufPosition; + + if ( 0 == desiredCapacity ) + return NULL; + + BEGIN_NATIVE + /* + * This adjustment of desiredCapacity is arbitrary and amenable to + * performance experimentation. For initial signs of life, ignore the + * desiredCapacity hint completely and use a hardwired size. + */ + desiredCapacity = 8180; + + node = (ExpandedVarlenaOutputStreamNode *) + MemoryContextAlloc(evosh->hdr.eoh_context, desiredCapacity); + node->next = evosh->tail->next; + evosh->tail->next = node; + evosh->tail = node; + + dbb = JNI_newDirectByteBuffer(node + 1, desiredCapacity - sizeof *node); + END_NATIVE + + return dbb; } diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index 0a4678e5..cab3d6ff 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -79,6 +79,8 @@ extern void JNI_callStaticVoidMethodLocked(jclass clazz, jmethodID metho extern void JNI_callStaticVoidMethodLockedV(jclass clazz, jmethodID methodID, va_list args); extern jint JNI_callIntMethodLocked(jobject object, jmethodID methodID, ...); extern jint JNI_callIntMethodLockedV(jobject object, jmethodID methodID, va_list args); +extern jlong JNI_callLongMethodLocked(jobject object, jmethodID methodID, ...); +extern jlong JNI_callLongMethodLockedV(jobject object, jmethodID methodID, va_list args); extern void JNI_callVoidMethodLocked(jobject object, jmethodID methodID, ...); extern void JNI_callVoidMethodLockedV(jobject object, jmethodID methodID, va_list args); extern jobject JNI_newObjectLocked(jclass clazz, jmethodID ctor, ...); diff --git a/pljava-so/src/main/include/pljava/VarlenaWrapper.h b/pljava-so/src/main/include/pljava/VarlenaWrapper.h index 8fc71bb2..08f56222 100644 --- a/pljava-so/src/main/include/pljava/VarlenaWrapper.h +++ b/pljava-so/src/main/include/pljava/VarlenaWrapper.h @@ -24,6 +24,10 @@ extern "C" { extern jobject pljava_VarlenaWrapper_Input( Datum d, MemoryContext mc, ResourceOwner ro); +extern jobject pljava_VarlenaWrapper_Output(MemoryContext mc, ResourceOwner ro); + +extern Datum pljava_VarlenaWrapper_Output_adopt(jobject vlos); + extern void pljava_VarlenaWrapper_initialize(void); #ifdef __cplusplus diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 62322b6d..ab47c8fc 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.ByteBuffer; @@ -163,4 +164,206 @@ protected void javaStateReleased() } } } + + /** + * A class by which Java writes the content of a varlena as an OutputStream. + * + * Associated with a {@code ResourceOwner} to bound the lifetime of + * the native reference; the chosen resource owner must be one that will be + * released no later than the memory context containing the varlena. + */ + public class Output extends OutputStream + { + private State m_state; + private boolean m_open = true; + + /** + * Construct a {@code VarlenaWrapper.Output}. + * @param cookie Capability held by native code. + * @param resourceOwner Resource owner whose release will indicate that + * the underlying varlena is no longer valid. + * @param context Pointer to memory context containing the underlying + * varlena; subject to {@code MemoryContextDelete} if Java code frees or + * reclaims this object. + * @param varlenaPtr Pointer value to the underlying varlena. + * @param buf Writable direct {@code ByteBuffer} constructed over (an + * initial region of) the varlena's data bytes. + */ + private Output(DualState.Key cookie, long resourceOwner, + long context, long varlenaPtr, ByteBuffer buf) + { + m_state = new State( + cookie, this, resourceOwner, context, varlenaPtr, buf); + } + + /** + * Return a ByteBuffer to write into. + *

+ * It will be the existing buffer if it has any remaining capacity to + * write into (even if it is less than desiredCapacity), otherwise a new + * buffer allocated with desiredCapacity as a hint (it may still be + * smaller than the hint, or larger). Call with desiredCapacity zero to + * indicate that writing is finished and make the varlena available for + * native code to adopt. + */ + private ByteBuffer buf(int desiredCapacity) throws IOException + { + if ( ! m_open ) + throw new IOException("Write on closed VarlenaWrapper.Output"); + try + { + ByteBuffer buf = m_state.buffer(); + if ( 0 < buf.remaining() && 0 < desiredCapacity ) + return buf; + return m_state.nextBuffer(desiredCapacity); + } + catch ( SQLException sqe ) + { + throw new IOException("Write on varlena failed", sqe); + } + } + + @Override + public void write(int b) throws IOException + { + synchronized ( m_state ) + { + ByteBuffer dst = buf(1); + dst.put((byte)(b & 0xff)); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + synchronized ( m_state ) + { + while ( 0 < len ) + { + ByteBuffer dst = buf(len); + int can = dst.remaining(); + if ( can > len ) + can = len; + dst.put(b, off, can); + off += can; + len -= can; + } + } + } + + @Override + public void close() throws IOException + { + synchronized ( m_state ) + { + if ( ! m_open ) + return; + buf(0); + m_open = false; + } + } + + /** + * Actually free a {@code VarlenaWrapper.Output}. + *

+ * {@code close()} does not do so, because the typical use of this class + * is to write to an instance, close it, then let some native code adopt + * it. If it turns out one won't be adopted and must be freed, use this + * method. + */ + public void free() throws IOException + { + close(); + m_state.javaStateReleased(); + } + + /* + * Called only from native code to retrieve the varlena after writing. + */ + private long adopt() throws SQLException + { + synchronized ( m_state ) + { + if ( m_open ) + throw new SQLException( + "Writing of VarlenaWrapper.Output not yet complete", + "55000"); + return m_state.adopt(); + } + } + + + + private static class State + extends DualState.SingleMemContextDelete + { + private ByteBuffer m_buf; + private long m_varlena; + + private State( + DualState.Key cookie, Output vr, + long resourceOwner, long memContext, long varlenaPtr, + ByteBuffer buf) + { + super(cookie, vr, resourceOwner, memContext); + m_varlena = varlenaPtr; + m_buf = buf; + } + + private ByteBuffer buffer() throws SQLException + { + assertNativeStateIsValid(); + return m_buf; + } + + private ByteBuffer nextBuffer(int desiredCapacity) + throws SQLException + { + assertNativeStateIsValid(); + if ( 0 < m_buf.remaining() && 0 < desiredCapacity ) + throw new SQLException( + "VarlenaWrapper.Output buffer management error", + "XX000"); + synchronized ( Backend.THREADLOCK ) + { + m_buf = _nextBuffer(m_varlena, m_buf.position(), + desiredCapacity); + } + return m_buf; + } + + private long adopt() throws SQLException + { + assertNativeStateIsValid(); + long varlena = m_varlena; + nativeStateReleased(); + return varlena; + } + + @Override + public String toString() + { + return String.format("%s VarlenaWrapper.Output (%x) %s", + super.toString(), m_varlena, + String.valueOf(m_buf).replace("java.nio.", "")); + } + + @Override + protected void nativeStateReleased() + { + m_buf = null; + super.nativeStateReleased(); + } + + @Override + protected void javaStateReleased() + { + m_buf = null; + super.javaStateReleased(); + } + + private native ByteBuffer _nextBuffer( + long varlenaPtr, int currentBufPosition, int desiredCapacity); + } + } } From ad1ebc8dc0a10ae7258a924d4dd09339169339a7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 21:32:38 -0400 Subject: [PATCH 0080/1087] Add SQLXMLImpl.Writable. --- pljava-so/src/main/c/type/SQLXMLImpl.c | 7 + .../postgresql/pljava/jdbc/SQLXMLImpl.java | 329 ++++++++++++++++++ 2 files changed, 336 insertions(+) diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 4154ace8..ece9d811 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -16,6 +16,8 @@ static jclass s_SQLXML_Readable_class; static jmethodID s_SQLXML_Readable_init; +static jclass s_SQLXML_Writable_class; +static jmethodID s_SQLXML_Writable_init; static jvalue _SQLXML_coerceDatum(Type self, Datum arg) { @@ -49,4 +51,9 @@ void pljava_SQLXMLImpl_initialize(void) "org/postgresql/pljava/jdbc/SQLXMLImpl$Readable")); s_SQLXML_Readable_init = PgObject_getJavaMethod(s_SQLXML_Readable_class, "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Input;)V"); + + s_SQLXML_Writable_class = JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/jdbc/SQLXMLImpl$Writable")); + s_SQLXML_Writable_init = PgObject_getJavaMethod(s_SQLXML_Writable_class, + "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Output;)V"); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 2063fa2e..33cedd25 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -71,6 +71,24 @@ import java.sql.SQLDataException; +/* ... for SQLXMLImpl.Writable */ + +import java.io.FilterOutputStream; +import java.io.OutputStreamWriter; + +import static javax.xml.transform.OutputKeys.ENCODING; + +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.stax.StAXResult; +import javax.xml.transform.dom.DOMResult; + +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; + public abstract class SQLXMLImpl implements SQLXML { protected AtomicReference m_backing; @@ -384,6 +402,265 @@ private InputStream correctedDeclStream(InputStream is) } } + static class Writable extends SQLXMLImpl + { + private AtomicBoolean m_writable = new AtomicBoolean(true); + private Charset m_serverCS = implServerCharset(); + + private Writable(VarlenaWrapper.Output vwo) throws SQLException + { + super(vwo); + if ( null == m_serverCS ) + { + try + { + vwo.free(); + } + catch ( IOException ioe ) { } + throw new SQLFeatureNotSupportedException("SQLXML: no Java " + + "Charset found to match server encoding; perhaps set " + + "org.postgresql.server.encoding system property to a " + + "valid Java charset name for the same encoding?", "0A000"); + } + } + + private OutputStream backingAndClearWritable() + throws SQLException + { + OutputStream backing = backingIfNotFreed(); + return m_writable.getAndSet(false) ? backing : null; + } + + @Override + public void free() throws SQLException + { + VarlenaWrapper.Output vwo = m_backing.getAndSet(null); + if ( null == vwo ) + return; + try + { + vwo.free(); + } + catch ( Exception e ) + { + throw normalizedException(e); + } + } + + @Override + public OutputStream setBinaryStream() throws SQLException + { + OutputStream os = backingAndClearWritable(); + if ( null == os ) + return super.setBinaryStream(); + try + { + return new DeclCheckedOutputStream(os, m_serverCS); + } + catch ( IOException e ) + { + throw normalizedException(e); + } + } + + @Override + public Writer setCharacterStream() throws SQLException + { + OutputStream os = backingAndClearWritable(); + if ( null == os ) + return super.setCharacterStream(); + try + { + os = new DeclCheckedOutputStream(os, m_serverCS); + return new OutputStreamWriter(os, m_serverCS.newEncoder()); + } + catch ( IOException e ) + { + throw normalizedException(e); + } + } + + @Override + public void setString(String value) throws SQLException + { + OutputStream os = backingAndClearWritable(); + if ( null == os ) + super.setString(value); + try + { + os = new DeclCheckedOutputStream(os, m_serverCS); + Writer w = new OutputStreamWriter(os, m_serverCS.newEncoder()); + w.write(value); + w.close(); + } + catch ( Exception e ) + { + throw normalizedException(e); + } + } + + @Override + public T setResult(Class resultClass) + throws SQLException + { + OutputStream os = backingAndClearWritable(); + if ( null == os ) + return super.setResult(resultClass); + + if ( null == resultClass || Result.class == resultClass ) + resultClass = (Class)StreamResult.class; // trust me on this + + try + { + if ( resultClass.isAssignableFrom(StreamResult.class) ) + return resultClass.cast( + new StreamResult(new DeclCheckedOutputStream( + os, m_serverCS))); + + if ( resultClass.isAssignableFrom(SAXResult.class) ) + { + SAXTransformerFactory saxtf = (SAXTransformerFactory) + SAXTransformerFactory.newInstance(); + TransformerHandler th = saxtf.newTransformerHandler(); + th.getTransformer().setOutputProperty( + ENCODING, m_serverCS.name()); + th.setResult(new StreamResult(new DeclCheckedOutputStream( + os, m_serverCS))); + return resultClass.cast(new SAXResult(th)); + } + + if ( resultClass.isAssignableFrom(StAXResult.class) ) + { + XMLOutputFactory xof = XMLOutputFactory.newFactory(); + XMLStreamWriter xsw = xof.createXMLStreamWriter( + new DeclCheckedOutputStream(os, m_serverCS), + m_serverCS.name()); + return resultClass.cast(new StAXResult(xsw)); + } + + if ( resultClass.isAssignableFrom(DOMResult.class) ) + { + /* leave this grody case to be implemented later */ + } + } + catch ( Exception e ) + { + throw normalizedException(e); + } + + throw new SQLFeatureNotSupportedException( + "No support for SQLXML.setResult(" + + resultClass.getName() + ".class)", "0A000"); + } + } + + static class DeclCheckedOutputStream extends FilterOutputStream + { + private Charset m_serverCS; + private DeclProbe m_probe; + + private DeclCheckedOutputStream(OutputStream os, Charset cs) + throws IOException + { + super(os); + os.write(new byte[0]); // is the VarlenaWrapper.Output still alive? + m_serverCS = cs; + m_probe = new DeclProbe(); + } + + @Override + public void write(int b) throws IOException + { + synchronized ( out ) + { + if ( null == m_probe ) + { + out.write(b); + return; + } + try + { + if ( ! m_probe.take((byte)(b & 0xff)) ) + check(); + } + catch ( SQLException sqe ) + { + throw normalizedException(sqe); + } + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + synchronized ( out ) + { + if ( null != m_probe ) + { + try + { + while ( 0 < len -- ) + { + if ( ! m_probe.take(b[off ++]) ) + break; + } + check(); + } + catch ( SQLException sqe ) + { + throw normalizedException(sqe); + } + } + out.write(b, off, len); + } + } + + @Override + public void flush() throws IOException + { + } + + @Override + public void close() throws IOException + { + synchronized ( out ) + { + try + { + check(); + } + catch ( SQLException sqe ) + { + throw normalizedException(sqe); + } + out.close(); + } + } + + private void check() throws IOException, SQLException + { + if ( null == m_probe ) + return; + m_probe.checkEncoding(m_serverCS, false); + byte[] prefix = m_probe.prefix(m_serverCS); + m_probe = null; // Do not check more than once. + out.write(prefix); + } + + /** + * Wrap other checked exceptions in IOException for methods specified to + * throw only that. + */ + private IOException normalizedException(Exception e) + { + if ( e instanceof IOException ) + return (IOException)e; + if ( e instanceof RuntimeException ) + throw (RuntimeException)e; + return new IOException("Malformed XML", e); + } + } + /** * A class to parse and, if necessary, check or correct, the * possibly-erroneous XMLDecl or TextDecl syntax found in the stored form @@ -849,5 +1126,57 @@ byte[] prefix(Charset serverCharset) throws IOException return baos.toByteArray(); } + + /** + * Throw an exception if a decl was matched and specified an encoding + * that isn't the server encoding, or if a decl was malformed, or if + * strict is specified, no encoding was declared, and the server + * encoding is not UTF-8. + * @param serverCharset The encoding used by the server; any encoding + * specified in the stream must resolve (possibly as an alias) to this + * encoding. + * @param strict if true, a decl may only be absent, or lack encoding + * information, if the server charset is UTF-8. If false, the check + * passes regardless of server encoding if the stream contains no decl + * or the decl does not declare an encoding. + */ + void checkEncoding(Charset serverCharset, boolean strict) + throws SQLException + { + if ( State.MATCHED == m_state ) + { + if ( m_encodingEnd > m_encodingStart ) + { + byte[] parseResult = m_save.toByteArray(); + /* + * The assumption that the serverCharset can be used in + * constructing this String rests again on all supported + * server charsets matching on the characters used in decls. + */ + String encName = new String(parseResult, + m_encodingStart, m_encodingEnd - m_encodingStart, + serverCharset); + try + { + Charset cs = Charset.forName(encName); + if ( serverCharset.equals(cs) ) + return; + } + catch ( IllegalArgumentException iae ) { } + throw new SQLDataException( + "XML declares character set \"" + encName + + "\" which does not match server encoding", "2200N"); + } + } + else if ( State.UNMATCHED != m_state ) + throw new SQLDataException( + "XML begins with an incomplete declaration", "2200N"); + + if ( ! strict || "UTF-8".equals(serverCharset.name()) ) + return; + throw new SQLDataException( + "XML does not declare a character set, and server encoding " + + "is not UTF-8", "2200N"); + } } } From dedd2c7ccc4ab9ac08093cbc4505ba155a8fe2d2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 11 May 2018 21:17:08 -0400 Subject: [PATCH 0081/1087] Implement adopt() for Writable. --- pljava-so/src/main/c/type/SQLXMLImpl.c | 8 +++++++- .../java/org/postgresql/pljava/jdbc/SQLXMLImpl.java | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index ece9d811..1ed536b4 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -18,6 +18,7 @@ static jclass s_SQLXML_Readable_class; static jmethodID s_SQLXML_Readable_init; static jclass s_SQLXML_Writable_class; static jmethodID s_SQLXML_Writable_init; +static jmethodID s_SQLXML_Writable_adopt; static jvalue _SQLXML_coerceDatum(Type self, Datum arg) { @@ -32,7 +33,10 @@ static jvalue _SQLXML_coerceDatum(Type self, Datum arg) static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) { - return 0; /* haven't got that direction yet */ + jobject vwo = JNI_callObjectMethodLocked(sqlxml, s_SQLXML_Writable_adopt); + Datum d = pljava_VarlenaWrapper_Output_adopt(vwo); + JNI_deleteLocalRef(vwo); + return TransferExpandedObject(d, CurrentMemoryContext); } /* Make this datatype available to the postgres system. @@ -56,4 +60,6 @@ void pljava_SQLXMLImpl_initialize(void) "org/postgresql/pljava/jdbc/SQLXMLImpl$Writable")); s_SQLXML_Writable_init = PgObject_getJavaMethod(s_SQLXML_Writable_class, "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Output;)V"); + s_SQLXML_Writable_adopt = PgObject_getJavaMethod(s_SQLXML_Writable_class, + "adopt", "()Lorg/postgresql/pljava/internal/VarlenaWrapper$Output;"); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 33cedd25..3e28c042 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -552,6 +552,17 @@ public T setResult(Class resultClass) "No support for SQLXML.setResult(" + resultClass.getName() + ".class)", "0A000"); } + + private VarlenaWrapper.Output adopt() throws SQLException + { + VarlenaWrapper.Output vwo = m_backing.getAndSet(null); + if ( m_writable.get() ) + throw new SQLNonTransientException( + "Writable SQLXML object has not been written yet", "55000"); + if ( null == vwo ) + backingIfNotFreed(); /* shorthand way to throw the exception */ + return vwo; + } } static class DeclCheckedOutputStream extends FilterOutputStream From 4c738695e8098dc0f5f227ae0c37c6ff53114584 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 22:25:43 -0400 Subject: [PATCH 0082/1087] Implement Connection.createSQLXML() --- pljava-so/src/main/c/type/SQLXMLImpl.c | 37 +++++++++++++++++++ .../postgresql/pljava/jdbc/SPIConnection.java | 15 ++++---- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 12 ++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 1ed536b4..6e8054ab 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -11,6 +11,8 @@ */ #include +#include "org_postgresql_pljava_jdbc_SQLXMLImpl.h" + #include "pljava/type/Type_priv.h" #include "pljava/VarlenaWrapper.h" @@ -44,6 +46,17 @@ static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) extern void pljava_SQLXMLImpl_initialize(void); void pljava_SQLXMLImpl_initialize(void) { + jclass clazz; + JNINativeMethod methods[] = + { + { + "_newWritable", + "()Ljava/sql/SQLXML;", + Java_org_postgresql_pljava_jdbc_SQLXMLImpl__1newWritable + }, + { 0, 0, 0 } + }; + TypeClass cls = TypeClass_alloc("type.SQLXML"); cls->JNISignature = "Ljava/sql/SQLXML;"; cls->javaTypeName = "java.sql.SQLXML"; @@ -62,4 +75,28 @@ void pljava_SQLXMLImpl_initialize(void) "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Output;)V"); s_SQLXML_Writable_adopt = PgObject_getJavaMethod(s_SQLXML_Writable_class, "adopt", "()Lorg/postgresql/pljava/internal/VarlenaWrapper$Output;"); + + clazz = PgObject_getJavaClass("org/postgresql/pljava/jdbc/SQLXMLImpl"); + PgObject_registerNatives2(clazz, methods); + JNI_deleteLocalRef(clazz); +} + +/* + * Class: org_postgresql_pljava_jdbc_SQLXMLImpl + * Method: _newWritable + * Signature: ()Ljava/sql/SQLXML; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_jdbc_SQLXMLImpl__1newWritable + (JNIEnv *env, jclass sqlxml_class) +{ + jobject sqlxml; + jobject vwo; + BEGIN_NATIVE + vwo = pljava_VarlenaWrapper_Output( + TopTransactionContext, TopTransactionResourceOwner); + sqlxml = JNI_newObjectLocked( + s_SQLXML_Writable_class, s_SQLXML_Writable_init, vwo); + END_NATIVE + return sqlxml; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index fb76428c..5f6897d7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1119,6 +1119,13 @@ public Properties getClientInfo() throws SQLException return _clientInfo; } + @Override + public SQLXML createSQLXML() + throws SQLException + { + return SQLXMLImpl.newWritable(); + } + // ************************************************************ // Non-implementation of JDBC 4 methods. // ************************************************************ @@ -1139,14 +1146,6 @@ public Array createArrayOf(String typeName, Object[] elements) "SPIConnection.createArrayOf( String, Object[] ) not implemented yet.", "0A000" ); } - @Override - public SQLXML createSQLXML() - throws SQLException - { - throw new SQLFeatureNotSupportedException( "SPIConnection.createSQLXML() not implemented yet.", - "0A000" ); - } - @Override public NClob createNClob() throws SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 3e28c042..92447aba 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -31,6 +31,8 @@ import java.util.concurrent.atomic.AtomicReference; +import org.postgresql.pljava.internal.Backend; + import java.sql.SQLNonTransientException; /* ... for SQLXMLImpl.Readable */ @@ -212,6 +214,16 @@ protected SQLException normalizedException(Exception e) "XX000", e); } + static SQLXML newWritable() + { + synchronized ( Backend.THREADLOCK ) + { + return _newWritable(); + } + } + + private static native SQLXML _newWritable(); + static class Readable extends SQLXMLImpl { private AtomicBoolean m_readable = new AtomicBoolean(true); From f9f7b53d0da9b84aa94573f9e636c00fc45bbfa7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 22:06:36 -0400 Subject: [PATCH 0083/1087] Extend example to echo via writable SQLXML. --- .../pljava/example/annotation/PassXML.java | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) 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 e5dd1787..9a4ee412 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 @@ -11,6 +11,8 @@ */ package org.postgresql.pljava.example.annotation; +import java.sql.Connection; +import java.sql.DriverManager; import java.sql.SQLDataException; import java.sql.SQLException; import java.sql.SQLXML; @@ -31,8 +33,11 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXResult; import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stax.StAXResult; import javax.xml.transform.stax.StAXSource; import org.postgresql.pljava.annotation.Function; @@ -46,25 +51,26 @@ public class PassXML static Map s_tpls = new HashMap(); /** - * Echo an XML parameter back as a string, exercising seven different ways - * (how => 1-7) of reading an SQLXML object. + * Echo an XML parameter back, exercising seven different ways + * (howin => 1-7) of reading an SQLXML object, and six (howout => 1-6) + * of returning one. *

- * If how => 0, the XML parameter is simply saved in a static. It can be + * If howin => 0, the XML parameter is simply saved in a static. It can be * read in a subsequent call with sx => null, but only in the same * transaction. */ @Function(schema="javatest") - public static String echoXMLParameter(SQLXML sx, int how) + public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) throws SQLException { if ( null == sx ) sx = s_sx; - if ( 0 == how ) + if ( 0 == howin ) { s_sx = sx; - return "(saved)"; + return null; } - return echoSQLXML(sx, how); + return echoSQLXML(sx, howin, howout); } /** @@ -123,19 +129,36 @@ private static Source sxToSource(SQLXML sx, int how) throws SQLException case 1: return new StreamSource(sx.getBinaryStream()); case 2: return new StreamSource(sx.getCharacterStream()); case 3: return new StreamSource(new StringReader(sx.getString())); - case 4: return sx.getSource(DOMSource.class); + case 4: return sx.getSource(StreamSource.class); case 5: return sx.getSource(SAXSource.class); case 6: return sx.getSource(StAXSource.class); - case 7: return sx.getSource(StreamSource.class); + case 7: return sx.getSource(DOMSource.class); default: throw new SQLDataException("how should be 1-7", "22003"); } } - private static String echoSQLXML(SQLXML sx, int how) throws SQLException + private static Result sxToResult(SQLXML sx, int how) throws SQLException { - Source src = sxToSource(sx, how); - StringWriter sw = new StringWriter(); - Result rlt = new StreamResult(sw); + switch ( how ) + { + case 1: return new StreamResult(sx.setBinaryStream()); + case 2: return new StreamResult(sx.setCharacterStream()); + case 3: return new StreamResult(new StringWriter()); + case 4: return sx.setResult(StreamResult.class); + case 5: return sx.setResult(SAXResult.class); + case 6: return sx.setResult(StAXResult.class); + case 7: return sx.setResult(DOMResult.class); + default: throw new SQLDataException("how should be 1-7", "22003"); + } + } + + private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) + throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + SQLXML rx = c.createSQLXML(); + Source src = sxToSource(sx, howin); + Result rlt = sxToResult(rx, howout); try { @@ -147,6 +170,13 @@ private static String echoSQLXML(SQLXML sx, int how) throws SQLException throw new SQLException("XML transformation failed", te); } - return sw.toString(); + if ( 3 == howout ) + { + StringWriter sw = (StringWriter)((StreamResult)rlt).getWriter(); + String s = sw.toString(); + rx.setString(s); + } + + return rx; } } From 7d9718e49f5e3cc5538c8c374a2b101ca897e82d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Apr 2018 23:39:30 -0400 Subject: [PATCH 0084/1087] Close SAXResult upon endDocument(). To comply with the API spec, SQLXML has to throw an exception if passed to the database to be stored before being closed. However, a SAXResult exposes no way for the caller to explicitly close it, so wrap it in an adapter that will do so after the endDocument() is processed. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 135 +++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 92447aba..287ead72 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -91,6 +91,12 @@ import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamWriter; +/* ... for SQLXMLImpl.SAXResultAdapter */ + +import javax.xml.transform.Transformer; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.XMLFilterImpl; + public abstract class SQLXMLImpl implements SQLXML { protected AtomicReference m_backing; @@ -536,8 +542,9 @@ public T setResult(Class resultClass) TransformerHandler th = saxtf.newTransformerHandler(); th.getTransformer().setOutputProperty( ENCODING, m_serverCS.name()); - th.setResult(new StreamResult(new DeclCheckedOutputStream( - os, m_serverCS))); + os = new DeclCheckedOutputStream(os, m_serverCS); + th.setResult(new StreamResult(os)); + th = SAXResultAdapter.newInstance(th, os); return resultClass.cast(new SAXResult(th)); } @@ -684,6 +691,130 @@ private IOException normalizedException(Exception e) } } + /** + * Class to wrap a SAX {@code TransformerHandler} and hook the + * {@code endDocument} callback to also close the underlying output stream, + * making the {@code SQLXML} object ready to use for storing or returning + * the value. + */ + static class SAXResultAdapter + extends XMLFilterImpl implements TransformerHandler + { + private OutputStream m_os; + private TransformerHandler m_th; + private SAXResultAdapter(TransformerHandler th, OutputStream os) + { + m_os = os; + m_th = th; + setContentHandler(th); + setDTDHandler(th); + } + + static TransformerHandler newInstance( + TransformerHandler th, OutputStream os) + { + return new SAXResultAdapter(th, os); + } + + /** + * Version of {@code endDocument} that also closes the underlying + * stream. + */ + @Override + public void endDocument() throws SAXException + { + super.endDocument(); + try + { + m_os.close(); + } + catch ( IOException ioe ) + { + throw new SAXException("Failure closing SQLXML SAXResult", ioe); + } + m_os = null; + } + + /* + * XMLFilterImpl provides default pass-through methods for most of the + * superinterfaces of TransformerHandler, but not for those of + * LexicalHandler, so here goes. + */ + + @Override + public void startDTD(String name, String publicId, String systemId) + throws SAXException + { + m_th.startDTD(name, publicId, systemId); + } + + @Override + public void endDTD() + throws SAXException + { + m_th.endDTD(); + } + + @Override + public void startEntity(String name) + throws SAXException + { + m_th.startEntity(name); + } + + @Override + public void endEntity(String name) + throws SAXException + { + m_th.endEntity(name); + } + + @Override + public void startCDATA() + throws SAXException + { + m_th.startCDATA(); + } + + @Override + public void endCDATA() + throws SAXException + { + m_th.endCDATA(); + } + + @Override + public void comment(char[] ch, int start, int length) + throws SAXException + { + m_th.comment(ch, start, length); + } + + @Override + public void setResult(Result result) + { + throw new IllegalArgumentException("Result already set"); + } + + @Override + public void setSystemId(String systemId) + { + m_th.setSystemId(systemId); + } + + @Override + public String getSystemId() + { + return m_th.getSystemId(); + } + + @Override + public Transformer getTransformer() + { + return m_th.getTransformer(); + } + } + /** * A class to parse and, if necessary, check or correct, the * possibly-erroneous XMLDecl or TextDecl syntax found in the stored form From 8247c3adee3953d491ce12d3e30cac198344405f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Apr 2018 00:31:31 -0400 Subject: [PATCH 0085/1087] Close StAXResult upon endDocument. To comply with the API spec, SQLXML has to throw an exception if passed to the database to be stored before being closed. However, a StAXResult exposes no way for the caller to explicitly close it (XMLStreamWriter has a close() method, but its documentation explicitly forbids it to close the underlying stream!). So, wrap it in an adapter that does so after the endDocument is processed. This still leaves the oddity that serializing with StAX does not reproduce self-closing empty elements. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 124 +++++++++++++++++- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 287ead72..003a21c5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -89,7 +89,7 @@ import javax.xml.transform.sax.TransformerHandler; import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamWriter; +import javax.xml.stream.XMLEventWriter; /* ... for SQLXMLImpl.SAXResultAdapter */ @@ -97,6 +97,14 @@ import org.xml.sax.SAXException; import org.xml.sax.helpers.XMLFilterImpl; +/* ... for SQLXMLImpl.StAXResultAdapter */ + +import javax.xml.namespace.NamespaceContext; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.events.XMLEvent; + +import javax.xml.stream.XMLStreamException; + public abstract class SQLXMLImpl implements SQLXML { protected AtomicReference m_backing; @@ -551,9 +559,10 @@ public T setResult(Class resultClass) if ( resultClass.isAssignableFrom(StAXResult.class) ) { XMLOutputFactory xof = XMLOutputFactory.newFactory(); - XMLStreamWriter xsw = xof.createXMLStreamWriter( - new DeclCheckedOutputStream(os, m_serverCS), - m_serverCS.name()); + os = new DeclCheckedOutputStream(os, m_serverCS); + XMLEventWriter xsw = xof.createXMLEventWriter( + os, m_serverCS.name()); + xsw = new StAXResultAdapter(xsw, os); return resultClass.cast(new StAXResult(xsw)); } @@ -815,6 +824,113 @@ public Transformer getTransformer() } } + /** + * Class to wrap a StAX {@code XMLEventWriter} and hook the + * {@code endDocument} event to also close the underlying output stream, + * making the {@code SQLXML} object ready to use for storing or returning + * the value. + */ + static class StAXResultAdapter implements XMLEventWriter + { + /* + * TODO: it seems there is a practical difference between the + * XMLEventWriter and XMLStreamWriter APIs, as the former cannot + * distinguish between self-closed empty elements and the start-and-end + * tag form, while the latter can. This should probably be replaced + * by a class that implements XMLStreamWriter; that's just more tedious + * and longwinded, with so many more methods to implement. + */ + private XMLEventWriter m_xew; + private OutputStream m_os; + + StAXResultAdapter(XMLEventWriter xew, OutputStream os) + { + m_xew = xew; + m_os = os; + } + + @Override + public void flush() throws XMLStreamException + { + m_xew.flush(); + } + + @Override + public void close() throws XMLStreamException + { + m_xew.close(); + } + + /** + * Version of {@code add} that also closes the underlying stream after + * handling an {@code endDocument} event. + *

+ * Note it does not call this class's own close; a + * calling transformer may emit a warning if that is done. + */ + @Override + public void add(XMLEvent event) throws XMLStreamException + { + m_xew.add(event); + if ( ! event.isEndDocument() ) + return; + try + { + m_os.close(); + } + catch ( Exception ioe ) + { + throw new XMLStreamException( + "Failure closing SQLXML StAXResult", ioe); + } + } + + /** + * Include a whole content fragment by supplying another reader. + * That is passed directly to the underlying class, so the + * {@code endDocument} event ending that content will not close + * the stream. + */ + @Override + public void add(XMLEventReader reader) throws XMLStreamException + { + m_xew.add(reader); + } + + @Override + public String getPrefix(String uri) throws XMLStreamException + { + return m_xew.getPrefix(uri); + } + + @Override + public void setPrefix(String prefix, String uri) + throws XMLStreamException + { + m_xew.setPrefix(prefix, uri); + } + + @Override + public void setDefaultNamespace(String uri) + throws XMLStreamException + { + m_xew.setDefaultNamespace(uri); + } + + @Override + public void setNamespaceContext(NamespaceContext context) + throws XMLStreamException + { + m_xew.setNamespaceContext(context); + } + + @Override + public NamespaceContext getNamespaceContext() + { + return m_xew.getNamespaceContext(); + } + } + /** * A class to parse and, if necessary, check or correct, the * possibly-erroneous XMLDecl or TextDecl syntax found in the stored form From b5ed64501c45efdb330040cdcda43f9664319643 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Apr 2018 01:06:28 -0400 Subject: [PATCH 0086/1087] Illustrate the must-close requirements. --- .../pljava/example/annotation/PassXML.java | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) 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 9a4ee412..6bd20449 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 @@ -17,9 +17,12 @@ import java.sql.SQLException; import java.sql.SQLXML; +import java.io.IOException; +import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; +import java.io.Writer; import java.util.Map; import java.util.HashMap; @@ -170,11 +173,55 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) throw new SQLException("XML transformation failed", te); } - if ( 3 == howout ) + /* + * Before a SQLXML object that has been written to can be used by + * PostgreSQL (returned as a function result, plugged in as a prepared + * statement parameter or into a ResultSet, etc.), the method used for + * writing it must be "closed" to ensure the writing is complete. + * If it is set with setString(), nothing more is needed; setString + * obviously sets the whole value at once. Any OutputStream or Writer + * obtained from setBinaryStream() or setCharacterStream(), or from + * setResult(StreamResult.class), has to be explicitly closed (a + * Transformer does not close its Result when the transformation is + * complete!). Those are cases 1, 2, and 4 here. + * Cases 5 (SAXResult) and 6 (StAXResult) need no special attention; + * though the Transformer does not close them, the ones returned by + * this SQLXML implementation are set up to close themselves when the + * endDocument event is written. + */ + switch ( howout ) { + case 1: + case 2: + case 4: + StreamResult sr = (StreamResult)rlt; + OutputStream os = sr.getOutputStream(); + Writer w = sr.getWriter(); + try + { + if ( null != os ) + os.close(); + if ( null != w ) + w.close(); + } + catch ( IOException ioe ) + { + throw new SQLException( + "Failure closing SQLXML result", "XX000"); + } + break; + case 3: + /* + * This case is just here as a roundabout way to test setString. + * There is no StringResult.class, so to keep case 3 parallel to + * the others, sxToSource returned a StreamResult over a + * StringWriter; here, we retrieve the string from that, and test + * setString(). + */ StringWriter sw = (StringWriter)((StreamResult)rlt).getWriter(); String s = sw.toString(); rx.setString(s); + break; } return rx; From c9cf11cbf6a1722a7ef72dd30647d4a324001728 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Apr 2018 12:25:05 -0400 Subject: [PATCH 0087/1087] Use SQLXML for transformXML's result. A quick test with a transform that produces a fragment ("CONTENT", in SQL/XML-speak) establishes that the Java serializers have no objection (by default?) to serializing one, despite the parsers being peevish about parsing one. --- .../pljava/example/annotation/PassXML.java | 141 +++++++++++------- 1 file changed, 90 insertions(+), 51 deletions(-) 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 6bd20449..c6025bb8 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 @@ -103,14 +103,15 @@ public static void prepareXMLTransform(String name, SQLXML source, int how) } @Function(schema="javatest") - public static String transformXML( - String transformName, SQLXML source, int how) + public static SQLXML transformXML( + String transformName, SQLXML source, int howin, int howout) throws SQLException { Templates tpl = s_tpls.get(transformName); - Source src = sxToSource(source, how); - StringWriter sw = new StringWriter(); - Result rlt = new StreamResult(sw); + Source src = sxToSource(source, howin); + Connection c = DriverManager.getConnection("jdbc:default:connection"); + SQLXML result = c.createSQLXML(); + Result rlt = sxToResult(result, howout); try { @@ -122,7 +123,28 @@ public static String transformXML( throw new SQLException("XML transformation failed", te); } - return sw.toString(); + return ensureClosed(rlt, result, howout); + } + + private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) + throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + SQLXML rx = c.createSQLXML(); + Source src = sxToSource(sx, howin); + Result rlt = sxToResult(rx, howout); + + try + { + Transformer t = s_tf.newTransformer(); + t.transform(src, rlt); + } + catch ( TransformerException te ) + { + throw new SQLException("XML transformation failed", te); + } + + return ensureClosed(rlt, rx, howout); } private static Source sxToSource(SQLXML sx, int how) throws SQLException @@ -140,6 +162,19 @@ private static Source sxToSource(SQLXML sx, int how) throws SQLException } } + /** + * Return some instance of {@code Result} for writing an {@code SQLXML} + * object, depending on the parameter {@code how}. + *

+ * Note that this method always returns a {@code Result}, even for cases + * 1 and 2 (obtaining writable streams directly from the {@code SQLXML} + * object; this method wraps them in {@code Result}), and case 3 + * ({@code setString}; this method creates a {@code StringWriter} and + * returns it wrapped in a {@code Result}. + *

+ * In case 3, it will be necessary, after writing, to get the {@code String} + * from the {@code StringWriter}, and call {@code setString} with it. + */ private static Result sxToResult(SQLXML sx, int how) throws SQLException { switch ( how ) @@ -155,46 +190,58 @@ private static Result sxToResult(SQLXML sx, int how) throws SQLException } } - private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) + /** + * Ensure the closing of whatever method was used to add content to + * an {@code SQLXML} object. + *

+ * Before a {@code SQLXML} object that has been written to can be used by + * PostgreSQL (returned as a function result, plugged in as a prepared + * statement parameter or into a {@code ResultSet}, etc.), the method used + * for writing it must be "closed" to ensure the writing is complete. + *

+ * If it is set with {@link SQLXML#setString setString}, nothing more is + * needed; {@code setString} obviously sets the whole value at once. Any + * {@code OutputStream} or {@code Writer} obtained from + * {@link SQLXML#setBinaryStream setBinaryStream} or + * {@link SQLXML#setCharacterStream setCharacterStream}, or from + * {@link SQLXML#setResult setResult}{@code (StreamResult.class)}, has to be + * explicitly closed (a {@link Transformer} does not close its + * {@link Result} when the transformation is complete!). + * Those are cases 1, 2, and 4 here. + *

+ * Cases 5 ({@code SAXResult}) and 6 ({@code StAXResult}) need no special + * attention; though the {@code Transformer} does not close them, the ones + * returned by this {@code SQLXML} implementation are set up to close + * themselves when the {@code endDocument} event is written. + *

+ * Case 3 (test of {@code setString} is handled specially here. As this + * class allows testing of all techniques for writing the {@code SQLXML} + * object, and most of those involve a {@code Result}, case 3 is handled + * by also constructing a {@code Result} over a {@link StringWriter} and + * having the content written into that; this method then extracts the + * content from the {@code StringWriter} and passes it to {@code setString}. + * For cases 1 and 2, likewise, the stream obtained with + * {@code getBinaryStream} or {@code getCharacterStream} has been wrapped in + * a {@code Result} for generality in this example. + *

+ * A typical application will not need the generality seen here; it + * will usually know which technique it is using to write the {@code SQLXML} + * object, and only needs to know how to close that if it needs closing. + * @param r The {@code Result} onto which writing was done. + * @param sx The {@code SQLXML} object being written. + * @param how The integer used in this example class to select which method + * of writing the {@code SQLXML} object was to be tested. + * @return The {@code SQLXML} object {@code sx}, because why not? + */ + public static SQLXML ensureClosed(Result r, SQLXML sx, int how) throws SQLException { - Connection c = DriverManager.getConnection("jdbc:default:connection"); - SQLXML rx = c.createSQLXML(); - Source src = sxToSource(sx, howin); - Result rlt = sxToResult(rx, howout); - - try - { - Transformer t = s_tf.newTransformer(); - t.transform(src, rlt); - } - catch ( TransformerException te ) - { - throw new SQLException("XML transformation failed", te); - } - - /* - * Before a SQLXML object that has been written to can be used by - * PostgreSQL (returned as a function result, plugged in as a prepared - * statement parameter or into a ResultSet, etc.), the method used for - * writing it must be "closed" to ensure the writing is complete. - * If it is set with setString(), nothing more is needed; setString - * obviously sets the whole value at once. Any OutputStream or Writer - * obtained from setBinaryStream() or setCharacterStream(), or from - * setResult(StreamResult.class), has to be explicitly closed (a - * Transformer does not close its Result when the transformation is - * complete!). Those are cases 1, 2, and 4 here. - * Cases 5 (SAXResult) and 6 (StAXResult) need no special attention; - * though the Transformer does not close them, the ones returned by - * this SQLXML implementation are set up to close themselves when the - * endDocument event is written. - */ - switch ( howout ) + switch ( how ) { case 1: case 2: case 4: - StreamResult sr = (StreamResult)rlt; + StreamResult sr = (StreamResult)r; OutputStream os = sr.getOutputStream(); Writer w = sr.getWriter(); try @@ -211,19 +258,11 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) } break; case 3: - /* - * This case is just here as a roundabout way to test setString. - * There is no StringResult.class, so to keep case 3 parallel to - * the others, sxToSource returned a StreamResult over a - * StringWriter; here, we retrieve the string from that, and test - * setString(). - */ - StringWriter sw = (StringWriter)((StreamResult)rlt).getWriter(); + StringWriter sw = (StringWriter)((StreamResult)r).getWriter(); String s = sw.toString(); - rx.setString(s); + sx.setString(s); break; } - - return rx; + return sx; } } From 0bb474d20c67faa7ed17a4b777db04aedd5b8ea5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 11 May 2018 22:10:14 -0400 Subject: [PATCH 0088/1087] Implement SQLXML.setResult(DOMResult.class) ... the one flavor of Result originally left for later (= now). It turns out that, alone among the setResult flavors, a DOMResult does object to being used as the result of a transformation that produces a document fragment (CONTENT, in SQL/XMLese). That's because, unless setNode() is called first with a DocumentFragment node, the transform will default to installing a Document node there instead. Add an example presetting the DOMResult with an empty DocumentFragment. --- .../pljava/example/annotation/PassXML.java | 50 ++++++++++++++++++- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 34 ++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) 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 c6025bb8..705b5307 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 @@ -27,6 +27,9 @@ import java.util.Map; import java.util.HashMap; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Templates; @@ -45,6 +48,9 @@ import org.postgresql.pljava.annotation.Function; +/** + * Class illustrating use of {@link SQLXML} to operate on XML data. + */ public class PassXML { static SQLXML s_sx; @@ -55,7 +61,7 @@ public class PassXML /** * Echo an XML parameter back, exercising seven different ways - * (howin => 1-7) of reading an SQLXML object, and six (howout => 1-6) + * (howin => 1-7) of reading an SQLXML object, and seven (howout => 1-7) * of returning one. *

* If howin => 0, the XML parameter is simply saved in a static. It can be @@ -102,6 +108,10 @@ public static void prepareXMLTransform(String name, SQLXML source, int how) } } + /** + * Transform some XML according to a named transform prepared with + * {@code prepareXMLTransform}. + */ @Function(schema="javatest") public static SQLXML transformXML( String transformName, SQLXML source, int howin, int howout) @@ -185,7 +195,10 @@ private static Result sxToResult(SQLXML sx, int how) throws SQLException case 4: return sx.setResult(StreamResult.class); case 5: return sx.setResult(SAXResult.class); case 6: return sx.setResult(StAXResult.class); - case 7: return sx.setResult(DOMResult.class); + case 7: + DOMResult r = sx.setResult(DOMResult.class); + allowFragment(r); // else it'll accept only DOCUMENT form + return r; default: throw new SQLDataException("how should be 1-7", "22003"); } } @@ -265,4 +278,37 @@ public static SQLXML ensureClosed(Result r, SQLXML sx, int how) } return sx; } + + /** + * Configure a {@code DOMResult} to accept {@code CONTENT} (a/k/a + * document fragment), not only the more restrictive {@code DOCUMENT}. + *

+ * The other forms of {@code Result} that can be requested will happily + * accept {@code XML(CONTENT)} and not just {@code XML(DOCUMENT)}. + * The {@code DOMResult} is pickier, however: if you first call + * {@link DOMResult#setNode setNode} with a {@code DocumentFragment}, it + * will accept either form, but if you leave the node unset when passing the + * {@code DOMResult} to a transformer, the transformer will default to + * putting a {@code Document} node there, and then it will not accept a + * fragment. + *

+ * If you need to handle fragments, this method illustrates how to pre-load + * the {@code DOMResult} with an empty {@code DocumentFragment}. Note that + * if you use some XML processing package that supplies its own classes + * implementing DOM nodes, you may need to use a {@code DocumentFragment} + * instance obtained from that package. + */ + public static void allowFragment(DOMResult r) throws SQLException + { + try + { + r.setNode(DocumentBuilderFactory.newInstance() + .newDocumentBuilder().newDocument() + .createDocumentFragment()); + } + catch ( ParserConfigurationException pce ) + { + throw new SQLException("Failed initializing DOMResult", pce); + } + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 003a21c5..711a9bfd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -79,6 +79,8 @@ import java.io.OutputStreamWriter; import static javax.xml.transform.OutputKeys.ENCODING; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.sax.SAXResult; @@ -432,6 +434,7 @@ static class Writable extends SQLXMLImpl { private AtomicBoolean m_writable = new AtomicBoolean(true); private Charset m_serverCS = implServerCharset(); + private DOMResult m_domResult; private Writable(VarlenaWrapper.Output vwo) throws SQLException { @@ -568,7 +571,7 @@ public T setResult(Class resultClass) if ( resultClass.isAssignableFrom(DOMResult.class) ) { - /* leave this grody case to be implemented later */ + return resultClass.cast(m_domResult = new DOMResult()); } } catch ( Exception e ) @@ -581,6 +584,30 @@ public T setResult(Class resultClass) resultClass.getName() + ".class)", "0A000"); } + /** + * Serialize a {@code DOMResult} to an {@code OutputStream} + * and close it. + */ + private void serializeDOM(DOMResult r, OutputStream os) + throws SQLException + { + DOMSource src = new DOMSource(r.getNode()); + try + { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer t = tf.newTransformer(); + t.setOutputProperty(ENCODING, m_serverCS.name()); + os = new DeclCheckedOutputStream(os, m_serverCS); + StreamResult rlt = new StreamResult(os); + t.transform(src, rlt); + os.close(); + } + catch ( Exception e ) + { + throw normalizedException(e); + } + } + private VarlenaWrapper.Output adopt() throws SQLException { VarlenaWrapper.Output vwo = m_backing.getAndSet(null); @@ -589,6 +616,11 @@ private VarlenaWrapper.Output adopt() throws SQLException "Writable SQLXML object has not been written yet", "55000"); if ( null == vwo ) backingIfNotFreed(); /* shorthand way to throw the exception */ + if ( null != m_domResult ) + { + serializeDOM(m_domResult, vwo); + m_domResult = null; + } return vwo; } } From 1d023b8343f1dc3426520baea276cbcd234adc7a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 1 May 2018 23:56:35 -0400 Subject: [PATCH 0089/1087] Work around JRE bug in reporting entity boundaries. When being written as a Result by a transformer built from the JRE-bundled Transformer implementation, it is possible to get called from an implementation class com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler that overrides startEntity, and so passes those along, but neglects to override endEntity, and so never reports those ... leaving our underlying serializer thinking it is stuck in an entity forever. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 711a9bfd..ec42b7aa 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -800,14 +800,29 @@ public void endDTD() public void startEntity(String name) throws SAXException { - m_th.startEntity(name); + /* + * For the time being, do NOT pass through startEntity/endEntity. + * When we are the result of a transform using the JRE-bundled + * Transformer implementation, we may get called by a class + * com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler + * that overrides startEntity and gives us those, but neglects to + * override endEntity and never gives us those, leaving our + * serializer thinking it's stuck in an entity forever. (Insert + * Java bug number here if assigned.) Can revisit later if a fixed + * Java version is known, or could use a simple test to check for + * presence of the bug. + */ + //m_th.startEntity(name); } @Override public void endEntity(String name) throws SAXException { - m_th.endEntity(name); + /* + * See startEntity. + */ + // m_th.endEntity(name); } @Override From fd8ab228f1c2548e58302cc81f3624eb6c5d3533 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 2 May 2018 22:36:57 -0400 Subject: [PATCH 0090/1087] Choose the Source/Result types to be default. The spec allows getSource and setResult to be called with null as the desired source or result class, and to return one that is chosen with some driver-specific knowledge of what's most natural/ efficient. It was easy to assume that byte streams would be the natural choice, as they are what's going on under the hood. However, some testing shows no time cost and even a slight memory advantage when presenting input in the form of a SAXSource--at least when presenting it to a JRE-bundled Transformer, which is likely to be a typical case where null is passed as the desired class. On the output side, there is a slight time advantage to StreamResult, but SAXResult is pretty comparable, and saves the caller the burden of getting the encoding right, closing the stream at the end, etc. Interestingly, StAX for output was a little more sparing of memory than SAX, but slower. It's possible that slowness could turn out to be something simple (adding a BufferedWriter to the pipeline somewhere, for example); if such an improvement to StAX timing is possible, it might make a reasonable default on memory grounds. There's still a point against StAX on fidelity grounds: when driven by a default JRE identity transform, it can render empty elements with start and end tags where they were self-closed in the original. This commit only changes defaults for when the caller has no preferred class. It is still possible to request any of the choices. DOM is quite profligate with memory, and can balloon the Java heap to something like 20 times the size of the serialized XML value; it should probably be avoided except when it's exactly what the calling code is built for. --- .../src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index ec42b7aa..7bed56cb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -337,7 +337,7 @@ public T getSource(Class sourceClass) return super.getSource(sourceClass); if ( null == sourceClass || Source.class == sourceClass ) - sourceClass = (Class)StreamSource.class; // trust me on this + sourceClass = (Class)SAXSource.class; // trust me on this try { @@ -537,7 +537,7 @@ public T setResult(Class resultClass) return super.setResult(resultClass); if ( null == resultClass || Result.class == resultClass ) - resultClass = (Class)StreamResult.class; // trust me on this + resultClass = (Class)SAXResult.class; // trust me on this try { From 542ac0cd1938358cce0e8f39f29ec7dab5c079c8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 3 May 2018 18:57:21 -0400 Subject: [PATCH 0091/1087] Support mark/reset in VarlenaWrapper.Input. As remarked earlier, it's straightforward. It will end up being handy after all. --- .../pljava/internal/VarlenaWrapper.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index ab47c8fc..800626d4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -16,6 +16,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; +import java.nio.InvalidMarkException; import java.sql.SQLException; @@ -129,6 +130,53 @@ public void close() throws IOException } } + @Override + public void mark(int readlimit) + { + synchronized ( m_state ) + { + if ( ! m_open ) + return; + try + { + buf().mark(); + } + catch ( IOException e ) + { + /* + * The contract is for mark to throw no checked exception. + * An exception caught here would mean the state's no longer + * live, which will be signaled to the caller if another, + * throwing, method is then called. If not, no harm no foul. + */ + } + } + } + + @Override + public void reset() throws IOException + { + synchronized ( m_state ) + { + if ( ! m_open ) + return; + try + { + buf().reset(); + } + catch ( InvalidMarkException e ) + { + throw new IOException("reset attempted when mark not set"); + } + } + } + + @Override + public boolean markSupported() + { + return true; + } + private static class State extends DualState.SinglePfree From b9910f30d61c42e9d17326c9bc95c885a6033b87 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 9 May 2018 15:14:04 -0400 Subject: [PATCH 0092/1087] Test imperfectly for DOCUMENT/CONTENT in Readable. The current stored form of XML in PostgreSQL does not save any indication of whether the XML is in DOCUMENT or CONTENT form (which, to determine correctly, requires fully reparsing it). Java's bundled XML parsers do not accept the CONTENT form, requiring a synthetic document root element to be wrapped around the stream content first, then filtered out of the parse result. This, of course, is not necessary if the input is in DOCUMENT form, but is harmless unless the input is in DOCUMENT form with a DTD, in which case such wrapping must be avoided. An imperfect test is added here that uses an input stream supporting mark/reset, and pulls a few initial parse events before resetting the stream for the application to parse. Those initial events only need to be pulled until a DTD is parsed (meaning the input is definitely DOCUMENT), or a disallowed-in-prolog production is parsed (meaning it definitely isn't), or an element-start is parsed (meaning it could be either, barring a full reparse to settle the question). The last two cases can both be handled by the wrapping technique, so don't need to be distinguished further. The JDBC spec is mute on how SQLXML should handle DOCUMENT vs. CONTENT. For the forms of Source that expose parse results, wrapping and filtering is effective, but it would seem a POLA violation to include synthetic wrapper elements via getBinaryStream, getCharacterStream, or getString (or getSource with StreamSource.class). So, for those cases, no wrapping. The application code will need to know what it's doing if the XML value might be of CONTENT form. Because the JRE-supplied SequenceInputStream doesn't support mark/reset, adds a (possibly general-purpose) MarkableSequenceInputStream that does, as long as its constituent input streams do. --- .../jdbc/MarkableSequenceInputStream.java | 302 ++++++++++++++++++ .../postgresql/pljava/jdbc/SQLXMLImpl.java | 178 ++++++++++- 2 files changed, 466 insertions(+), 14 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java new file mode 100644 index 00000000..38947cb1 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java @@ -0,0 +1,302 @@ +package org.postgresql.pljava.jdbc; + +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.io.IOException; + +/** + * Version of {@link SequenceInputStream} that supports + * {@code mark} and {@code reset}, to the extent its constituent input streams + * do. + *

+ * This class implements {@code mark} and {@code reset} by calling the + * corresponding methods on the underlying streams; it does not add buffering + * or have any means of providing {@code mark} and {@code reset} support if + * the underlying streams do not. + *

+ * As with {@code SequenceInputStream}, each underlying stream, when completely + * read and no longer needed, is closed to free resources. This instance itself + * will remain in "open at EOF" condition until explicitly closed, but does not + * prevent reclamation of the underlying streams. + *

+ * Unlike {@code SequenceInputStream}, this class can keep underlying streams + * open, after fully reading them, if a {@code mark} has been set, so that + * {@code reset} will be possible. When a mark is no longer needed, it can be + * canceled (by calling {@code mark} with a {@code readlimit} of 0) to again + * allow the underlying streams to be reclaimed as soon as possible. + */ +public class MarkableSequenceInputStream extends InputStream +{ + private InputStream[] m_streams; + private int m_activeStream; + private int m_markedStream; + private int m_readlimit_orig; + private int m_readlimit_curr; + private boolean m_markSupported; + private boolean m_markSupported_determined; + + /** + * Create a {@code MarkableSequenceInputStream} from one or more existing + * input streams. + * @param streams Sequence of {@code InputStream}s that will be read from + * in order. + * @throws NullPointerException if {@code streams} is {@code null}, or + * contains {@code null} for any stream. + */ + public MarkableSequenceInputStream(InputStream... streams) + { + if ( null == streams ) + throw new NullPointerException("MarkableSequenceInputStream(null)"); + for ( InputStream s : streams ) + if ( null == s ) + throw new NullPointerException( + "MarkableSequenceInputStream(..., null, ...)"); + + m_streams = streams; + m_activeStream = 0; /* -1 will mean closed */ + m_markedStream = -1; /* -1 will mean no mark has been set */ + } + + private InputStream currentStream() throws IOException + { + if ( -1 == m_activeStream ) + throw new IOException("I/O on closed InputStream"); + if ( m_streams.length == m_activeStream ) + return null; + return m_streams[m_activeStream]; + } + + private InputStream nextStream() throws IOException + { + assert m_streams.length > m_activeStream; + assert -1 == m_streams[m_activeStream].read(); + + if ( -1 == m_markedStream ) + { + m_streams[m_activeStream].close(); + m_streams[m_activeStream++] = null; + } + else if ( m_streams.length > ++m_activeStream ) + m_streams[m_activeStream].mark(m_readlimit_curr); + + if ( m_streams.length == m_activeStream ) + return null; + return m_streams[m_activeStream]; + } + + private void decrementLimit(long bytes) + { + assert 0 < bytes; + if ( -1 == m_markedStream ) + return; + if ( bytes < m_readlimit_curr ) + { + m_readlimit_curr -= bytes; + return; + } + mark(0); /* undo markage of underlying streams */ + } + + @Override + public int read() throws IOException + { + synchronized ( this ) + { + for ( InputStream s = currentStream(); null != s; s = nextStream() ) + { + int c = s.read(); + if ( -1 != c ) + { + decrementLimit(1); + return c; + } + } + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException + { + synchronized ( this ) + { + for ( InputStream s = currentStream(); null != s; s = nextStream() ) + { + int rslt = s.read(b, off, len); + if ( -1 != rslt ) + { + decrementLimit(rslt); + return rslt; + } + } + return -1; + } + } + + @Override + public long skip(long n) throws IOException + { + synchronized ( this ) + { + long skipped; + long totalSkipped = 0; + InputStream s = currentStream(); + while ( null != s ) + { + skipped = s.skip(n); + n -= skipped; + decrementLimit(skipped); + totalSkipped += skipped; + if ( 0 >= n ) + break; + /* + * A short count from skip doesn't have to mean EOF was reached. + * A read() will settle that question, though. + */ + if ( -1 != s.read() ) + { + n -= 1; + decrementLimit(1); + totalSkipped += 1; + continue; + } + /* + * Ok, it was EOF on that underlying stream. + */ + s = nextStream(); + } + return totalSkipped; + } + } + + @Override + public int available() throws IOException + { + synchronized ( this ) + { + if ( -1 == m_activeStream ) + return 0; + InputStream s = currentStream(); + return null == s ? 0 : s.available(); + } + } + + @Override + public void close() throws IOException + { + synchronized ( this ) + { + if ( -1 == m_activeStream ) + return; + if ( -1 != m_markedStream ) + m_activeStream = m_markedStream; + for ( ; m_streams.length > m_activeStream ; ++ m_activeStream ) + m_streams[m_activeStream].close(); + m_activeStream = -1; + m_streams = null; + } + } + + /** + * Marks the current position in this input stream. In this implementation, + * it is possible to 'cancel' a mark, by passing this method a + * {@code readlimit} of zero, returning the stream immediately to the state + * of having no mark. + */ + @Override + public void mark(int readlimit) + { + synchronized ( this ) + { + if ( -1 == m_activeStream ) + return; + + if ( -1 == m_markedStream ) + m_markedStream = m_activeStream; + else + { + for ( ; m_markedStream < m_activeStream ; ++ m_markedStream ) + { + try + { + m_streams[m_markedStream].close(); + } + catch ( IOException e ) + { + } + m_streams[m_markedStream] = null; + } + } + + if ( 0 >= readlimit ) /* setting instantly-invalid mark */ + { + m_readlimit_curr = m_readlimit_orig = 0; + m_markedStream = -1; + return; + } + m_readlimit_curr = m_readlimit_orig = readlimit; + + if ( m_streams.length == m_markedStream ) /* setting mark at EOF */ + return; + + m_streams[m_markedStream].mark(readlimit); + } + } + + @Override + public void reset() throws IOException + { + synchronized ( this ) + { + if ( -1 == m_activeStream ) + throw new IOException("reset on closed InputStream"); + if ( -1 == m_markedStream ) + throw new IOException("reset without mark"); + while ( true ) + { + if ( m_streams.length > m_activeStream ) + m_streams[m_activeStream].reset(); + if ( m_activeStream == m_markedStream ) + break; + -- m_activeStream; + } + m_readlimit_curr = m_readlimit_orig; + } + } + + /** + * Tests if this input stream supports the mark and reset methods. + *

+ * By the API spec, this method's return is "an invariant property of a + * particular input stream instance." For any instance of this class, the + * result is determined by the first call to this method, and does not + * change thereafter. At the first call, the result is determined only by + * the underlying input streams remaining to be read (or, if a mark has been + * set, which is possible before checking this method, then by the + * underlying input streams including and following the one that was current + * when the mark was set). The result will be {@code true} unless any of + * those underlying streams reports it as {@code false}. + */ + @Override + public boolean markSupported() + { + synchronized ( this ) + { + if ( m_markSupported_determined ) + return m_markSupported; + int i = m_markedStream; + if ( -1 == i ) + i = m_activeStream; + if ( -1 == i ) + return false; + m_markSupported = true; + for ( ; m_streams.length > i ; ++ i ) + { + if ( ! m_streams[i].markSupported() ) + m_markSupported = false; + } + m_markSupported_determined = true; + return m_markSupported; + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 7bed56cb..6870d61e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -58,6 +58,13 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import static javax.xml.stream.XMLStreamConstants.COMMENT; +import static javax.xml.stream.XMLStreamConstants.DTD; +import static javax.xml.stream.XMLStreamConstants.PROCESSING_INSTRUCTION; +import static javax.xml.stream.XMLStreamConstants.SPACE; +import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; +import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; + import static org.postgresql.pljava.internal.Session.implServerCharset; import org.postgresql.pljava.internal.VarlenaWrapper; @@ -67,7 +74,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.SequenceInputStream; import java.util.Arrays; @@ -244,6 +250,7 @@ static class Readable extends SQLXMLImpl { private AtomicBoolean m_readable = new AtomicBoolean(true); private Charset m_serverCS = implServerCharset(); + private boolean m_wrapped = false; private Readable(VarlenaWrapper.Input vwi) throws SQLException { @@ -276,7 +283,7 @@ public InputStream getBinaryStream() throws SQLException return super.getBinaryStream(); try { - return correctedDeclStream(is); + return correctedDeclStream(is, true); } catch ( IOException e ) { @@ -292,7 +299,7 @@ public Reader getCharacterStream() throws SQLException return super.getCharacterStream(); try { - is = correctedDeclStream(is); + is = correctedDeclStream(is, true); return new InputStreamReader(is, m_serverCS.newDecoder()); } catch ( IOException e ) @@ -312,7 +319,7 @@ public String getString() throws SQLException StringBuilder sb = new StringBuilder(); try { - is = correctedDeclStream(is); + is = correctedDeclStream(is, true); Reader r = new InputStreamReader(is, m_serverCS.newDecoder()); while ( -1 != r.read(cb) ) { @@ -343,7 +350,7 @@ public T getSource(Class sourceClass) { if ( sourceClass.isAssignableFrom(StreamSource.class) ) return sourceClass.cast( - new StreamSource(correctedDeclStream(is))); + new StreamSource(correctedDeclStream(is, true))); if ( sourceClass.isAssignableFrom(SAXSource.class) ) { @@ -352,7 +359,7 @@ public T getSource(Class sourceClass) true); return sourceClass.cast( new SAXSource(xr, - new InputSource(correctedDeclStream(is)))); + new InputSource(correctedDeclStream(is, false)))); } if ( sourceClass.isAssignableFrom(StAXSource.class) ) @@ -360,7 +367,8 @@ public T getSource(Class sourceClass) XMLInputFactory xif = XMLInputFactory.newFactory(); xif.setProperty(xif.IS_NAMESPACE_AWARE, true); XMLStreamReader xsr = - xif.createXMLStreamReader(correctedDeclStream(is)); + xif.createXMLStreamReader( + correctedDeclStream(is, false)); return sourceClass.cast(new StAXSource(xsr)); } @@ -370,7 +378,7 @@ public T getSource(Class sourceClass) DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); - is = correctedDeclStream(is); + is = correctedDeclStream(is, false); return sourceClass.cast(new DOMSource(db.parse(is))); } } @@ -391,11 +399,41 @@ public T getSource(Class sourceClass) * The current stored form in PG for the XML type is a character string * in server encoding, which may or may not still include a declaration * left over from an input or cast operation, which declaration may or - * may not be correct (about the encoding, anyway). + * may not be correct (about the encoding, anyway). Nothing is stored + * to distinguish whether the value is of the {@code DOCUMENT} or + * {@code CONTENT} form, to determine which requires a full reparse in + * the general case. + *

+ * This method only peeks at early parse events in the stream, to see + * if a {@code DOCTYPE} is present (must be {@code DOCUMENT}, or there + * is any other content before the first element (cannot be + * {@code DOCUMENT}). The input will not have a synthetic root element + * wrapped around it if a {@code DOCTYPE} is present, as that would + * break validation; otherwise (whether the check concluded it can't be + * {@code DOCUMENT}, or was simply inconclusive}, a synthetic wrapper + * will be added, as it will not break anything. + *

+ * As a side effect, this method sets {@code m_wrapped} tp {@code true} + * if it applies a wrapper element. When returning a type of + * {@code Source} that presents parsed results, it will be configured + * to present them with the wrapper element filtered out. + *

+ * However, when using the API that exposes the serialized form + * directly ({@code getBinaryStream}, {@code getCharacterStream}, + * {@code getString}), this method is passed {@code true} for + * {@code neverWrap}, and no wrapping is done. The application code must + * then handle the possibility that the stream may fail to parse as a + * {@code DOCUMENT}. (The JDBC spec gives no guidance in this area.) + * @param neverWrap When {@code true}, suppresses the wrapping described + * above. * @return An InputStream with its original decl, if any, replaced with - * a new one known to be correct, or none if the defaults are correct. + * a new one known to be correct, or none if the defaults are correct, + * and with the remaining content wrapped in a synthetic root element, + * unless the input is known early (by having a {@code DOCTYPE}) not to + * need one. */ - private InputStream correctedDeclStream(InputStream is) + private InputStream correctedDeclStream( + InputStream is, boolean neverWrap) throws IOException, SQLException { byte[] buf = new byte[40]; @@ -424,9 +462,112 @@ private InputStream correctedDeclStream(InputStream is) * providing the expected input-stream behavior, but the underlying * resources don't have to stick around for that. */ - InputStream pfx = - new ByteArrayInputStream(probe.prefix(m_serverCS)); - return new SequenceInputStream(pfx, is); + byte[] pfx = probe.prefix(m_serverCS); + int raLen = probe.readaheadLength(); + int raOff = pfx.length - raLen; + InputStream pfis = new ByteArrayInputStream(pfx, 0, raOff); + InputStream rais = new ByteArrayInputStream(pfx, raOff, raLen); + InputStream msis = new MarkableSequenceInputStream(pfis, rais, is); + + if ( neverWrap || ! useWrappingElement(msis) ) + return msis; + + m_wrapped = true; + InputStream elemStart = new ByteArrayInputStream( + "".getBytes(m_serverCS)); + InputStream elemEnd = new ByteArrayInputStream( + "".getBytes(m_serverCS)); + msis = new MarkableSequenceInputStream( + pfis, elemStart, rais, is, elemEnd); + return msis; + } + + /** + * Check (incompletely!) whether an {@code InputStream} is in XML + * {@code DOCUMENT} form (which Java XML parsers will accept) or + * {@code CONTENT} form, (which they won't, unless enclosed in a + * wrapping element). + *

+ * Proceed by requiring the input stream to support {@code mark} and + * {@code reset}, marking it, creating a StAX parser, and pulling some + * initial parse events. + *

+ * A possible {@code START_DOCUMENT} along with possible {@code SPACE}, + * {@code COMMENT}, and {@code PROCESSING_INSTRUCTION} events could + * allowably begin either the {@code DOCUMENT} or the {@code CONTENT} + * form. + *

+ * If a {@code DTD} is seen, the input must be in {@code DOCUMENT} form, + * and must not have a wrapper element added. + *

+ * If anything else is seen before the first {@code START_ELEMENT}, the + * input must be in {@code CONTENT} form, and must have + * a wrapper element added. + *

+ * If a {@code START_ELEMENT} is seen before either of those conclusions + * can be reached, this check is inconclusive. The conclusive check + * would be to finish parsing that element to see what, if anything, + * follows it. But that would often amount to parsing the whole stream + * just to determine how to parse it. Instead, just return @code true} + * anyway, as without a DTD, the wrapping trick is usable and won't + * break anything, even if it may not be necessary. + * @param is An {@code InputStream} that must be markable, will be + * marked on entry, and reset upon return. + * @return {@code true} if a wrapping element should be used. + */ + private boolean useWrappingElement(InputStream is) throws IOException + { + is.mark(Integer.MAX_VALUE); + XMLInputFactory xif = XMLInputFactory.newFactory(); + xif.setProperty(xif.IS_NAMESPACE_AWARE, true); + + boolean mustBeDocument = false; + boolean cantBeDocument = false; + + XMLStreamReader xsr = null; + try + { + xsr = xif.createXMLStreamReader(is); + while ( xsr.hasNext() ) + { + int evt = xsr.next(); + + if ( COMMENT == evt || PROCESSING_INSTRUCTION == evt + || SPACE == evt || START_DOCUMENT == evt ) + continue; + + if ( DTD == evt ) + { + mustBeDocument = true; + break; + } + + if ( START_ELEMENT == evt ) // could be DOCUMENT or CONTENT + break; + + cantBeDocument = true; + break; + } + } + catch ( XMLStreamException e ) + { + cantBeDocument = true; + } + + if ( null != xsr ) + { + try + { + xsr.close(); + } + catch ( XMLStreamException e ) + { + } + } + is.reset(); + is.mark(0); // relax any reset-buffer requirement + + return ! mustBeDocument; } } @@ -1444,6 +1585,15 @@ byte[] prefix(Charset serverCharset) throws IOException return baos.toByteArray(); } + /** + * Return the number of bytes at the end of the {@code prefix} result + * that represent readahead, rather than being part of the decl. + */ + int readaheadLength() + { + return m_save.size() - m_readaheadStart; + } + /** * Throw an exception if a decl was matched and specified an encoding * that isn't the server encoding, or if a decl was malformed, or if From 064f7cbed968426c6673cd4eb04c5ef3c2feb79a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 12 May 2018 20:59:09 -0400 Subject: [PATCH 0093/1087] Don't let stream get closed in useWrappingElement. The JRE XMLStreamReader seems to end up closing it, if it happens to parse to the end, under some circumstances only, such as if there's nothing (after the possible decl) but whitespace. I suspect a bug (XMLDocumentScannerImpl$PrologDriver seems to use XMLEntityScanner.skipSpaces to do that work, and maybe an /entity/ scanner would assume it can close its stream when it's done); in any case, make sure that doesn't actually close the original stream. --- .../org/postgresql/pljava/jdbc/SQLXMLImpl.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 6870d61e..a8b592f7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -37,6 +37,7 @@ /* ... for SQLXMLImpl.Readable */ +import java.io.FilterInputStream; import java.io.InputStreamReader; import java.nio.CharBuffer; import java.nio.charset.Charset; @@ -524,10 +525,21 @@ private boolean useWrappingElement(InputStream is) throws IOException boolean mustBeDocument = false; boolean cantBeDocument = false; + /* + * The XMLStreamReader may actually close the input stream if it + * reaches the end skipping only whitespace. That is probably a bug; + * in any case, protect the original input stream from being closed. + */ + InputStream tmpis = new FilterInputStream(is) + { + @Override + public void close() throws IOException { } + }; + XMLStreamReader xsr = null; try { - xsr = xif.createXMLStreamReader(is); + xsr = xif.createXMLStreamReader(tmpis); while ( xsr.hasNext() ) { int evt = xsr.next(); From b3281090d7a2e16e9dcd45833149d3bbabba6f73 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 8 May 2018 21:37:24 -0400 Subject: [PATCH 0094/1087] Unwrappage for SAX. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index a8b592f7..d2cffe42 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -100,9 +100,10 @@ import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLEventWriter; -/* ... for SQLXMLImpl.SAXResultAdapter */ +/* ... for SQLXMLImpl.SAXResultAdapter and .SAXUnwrapFilter */ import javax.xml.transform.Transformer; +import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.XMLFilterImpl; @@ -358,9 +359,11 @@ public T getSource(Class sourceClass) XMLReader xr = XMLReaderFactory.createXMLReader(); xr.setFeature("http://xml.org/sax/features/namespaces", true); + is = correctedDeclStream(is, false); + if ( m_wrapped ) + xr = new SAXUnwrapFilter(xr); return sourceClass.cast( - new SAXSource(xr, - new InputSource(correctedDeclStream(is, false)))); + new SAXSource(xr, new InputSource(is))); } if ( sourceClass.isAssignableFrom(StAXSource.class) ) @@ -885,6 +888,45 @@ private IOException normalizedException(Exception e) } } + /** + * Class to wrap an {@code XMLReader} and pass all of the parse events + * except the outermost ("document root") element, in effect producing + * {@code XML(CONTENT)} when the underlying stream has had a synthetic + * root element wrapped around it to satisfy a JRE-bundled parser that + * only accepts {@code XML(DOCUMENT)}. + *

+ * The result may be surprising to code consuming the SAX stream, depending + * on what it expects, but testing has showed the JRE-bundled identity + * transformer, at least, to accept the input and faithfully reproduce the + * non-document content. + */ + static class SAXUnwrapFilter extends XMLFilterImpl + { + private int m_nestLevel = 0; + + SAXUnwrapFilter(XMLReader parent) + { + super(parent); + } + + @Override + public void startElement( + String uri, String localName, String qName, Attributes atts) + throws SAXException + { + if ( 0 < m_nestLevel++ ) + super.startElement(uri, localName, qName, atts); + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException + { + if ( 0 < --m_nestLevel ) + super.endElement(uri, localName, qName); + } + } + /** * Class to wrap a SAX {@code TransformerHandler} and hook the * {@code endDocument} callback to also close the underlying output stream, From 395512d829f4b6751b97de94813e0b4ab8521c0b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 9 May 2018 13:28:24 -0400 Subject: [PATCH 0095/1087] Unwrappage for StAX. The XMLStreamReader created here returns the expected sequence of things with no complaints or surprises when called directly. However, a JRE default identity transformer using it as a source will ignore or lose things, even while the 'same' transformer reading the same sequence of things through the SAXUnwrapFilter reproduces them faithfully. Odd, but for now it has to be enough that the filter itself behaves as expected. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 124 +++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index d2cffe42..d8c34f96 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -59,6 +59,8 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import static javax.xml.stream.XMLStreamConstants.CDATA; +import static javax.xml.stream.XMLStreamConstants.CHARACTERS; import static javax.xml.stream.XMLStreamConstants.COMMENT; import static javax.xml.stream.XMLStreamConstants.DTD; import static javax.xml.stream.XMLStreamConstants.PROCESSING_INSTRUCTION; @@ -107,11 +109,14 @@ import org.xml.sax.SAXException; import org.xml.sax.helpers.XMLFilterImpl; -/* ... for SQLXMLImpl.StAXResultAdapter */ +/* ... for SQLXMLImpl.StAXResultAdapter and .StAXUnwrapFilter */ + +import java.util.NoSuchElementException; import javax.xml.namespace.NamespaceContext; import javax.xml.stream.XMLEventReader; import javax.xml.stream.events.XMLEvent; +import javax.xml.stream.util.StreamReaderDelegate; import javax.xml.stream.XMLStreamException; @@ -373,6 +378,8 @@ public T getSource(Class sourceClass) XMLStreamReader xsr = xif.createXMLStreamReader( correctedDeclStream(is, false)); + if ( m_wrapped ) + xsr = new StAXUnwrapFilter(xsr); return sourceClass.cast(new StAXSource(xsr)); } @@ -1066,6 +1073,121 @@ public Transformer getTransformer() } } + /** + * Class to wrap an {@code XMLStreamReader} and pass all of the parse events + * except the outermost ("document root") element, in effect producing + * {@code XML(CONTENT)} when the underlying stream has had a synthetic + * root element wrapped around it to satisfy a JRE-bundled parser that + * only accepts {@code XML(DOCUMENT)}. + *

+ * The result may be surprising to code consuming the StAX stream, depending + * on what it expects; testing has shown the JRE-bundled identity + * transformer does not faithfully reproduce such input (though, oddly, the + * 'same' identity transformer reading the 'same' content through the + * {@code SAXUnwrapFilter} does). Code that will be expected to handle + * {@code XML(CONTENT)} and not just {@code XML(DOCUMENT)} using this + * interface should be tested for correct behavior. + */ + static class StAXUnwrapFilter extends StreamReaderDelegate + { + private boolean m_hasPeeked; + private int m_nestLevel = 0; + + StAXUnwrapFilter(XMLStreamReader reader) + { + super(reader); + } + + @Override + public boolean hasNext() throws XMLStreamException + { + if ( m_hasPeeked ) + return true; + if ( ! super.hasNext() ) + return false; + int evt = super.next(); + + if ( START_ELEMENT == evt ) + { + if ( 0 < m_nestLevel++ ) + { + m_hasPeeked = true; + return true; + } + if ( ! super.hasNext() ) + return false; + evt = super.next(); + } + + /* + * If the above if() matched, we saw a START_ELEMENT, and if it + * wasn't the hidden one, we returned and are not here. If the if() + * matched and we're here, it was the hidden one, and we are looking + * at the next event. It could also be a START_ELEMENT, but it can't + * be the hidden one, so needs no special treatment other than to + * increment nestLevel. It could be an END_ELEMENT, checked next. + */ + + if ( START_ELEMENT == evt ) + ++ m_nestLevel; + else if ( END_ELEMENT == evt ) + { + if ( 0 < --m_nestLevel ) + { + m_hasPeeked = true; + return true; + } + if ( ! super.hasNext() ) + return false; + evt = super.next(); + } + + /* + * If the above if() matched, we saw an END_ELEMENT, and if it + * wasn't the hidden one, we returned and are not here. If the if() + * matched and we're here, it was the hidden one, and we are looking + * at the next event. It can't really be an END_ELEMENT (the hidden + * one had better be the last one) at all, much less the hidden one. + * It also can't really be a START_ELEMENT. So, no more bookkeeping, + * other than to set hasPeeked. + */ + + m_hasPeeked = true; + return true; + } + + @Override + public int next() throws XMLStreamException + { + if ( ! hasNext() ) + throw new NoSuchElementException(); + m_hasPeeked = false; + return getEventType(); + } + + @Override + public int nextTag() throws XMLStreamException + { + int evt; + while ( true ) + { + if ( ! hasNext() ) + throw new NoSuchElementException(); + evt = next(); + if ( ( CHARACTERS == evt || CDATA == evt ) && isWhiteSpace() ) + continue; + if ( SPACE != evt && PROCESSING_INSTRUCTION != evt + && COMMENT != evt ) + break; + } + /* if NoSuchElement wasn't thrown, evt is definitely assigned */ + if ( START_ELEMENT != evt && END_ELEMENT != evt ) + throw new XMLStreamException( + "expected start or end tag", getLocation()); + return evt; + } + } + /** * Class to wrap a StAX {@code XMLEventWriter} and hook the * {@code endDocument} event to also close the underlying output stream, From 27b80ca98b3ee706d9e672a00a60a52b70db3e8b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 11 May 2018 20:30:46 -0400 Subject: [PATCH 0096/1087] Unwrappage for DOM. The DOMResult will now come back with getNode() returning a Document node in the DOCUMENT case, a DocumentFragment node in the CONTENT case. The JRE-bundled identity transformer seems to reproduce either faithfully, without complaint. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index d8c34f96..fa444d89 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -68,6 +68,12 @@ import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; +import org.w3c.dom.Document; +import org.w3c.dom.DocumentFragment; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.Text; + import static org.postgresql.pljava.internal.Session.implServerCharset; import org.postgresql.pljava.internal.VarlenaWrapper; @@ -390,7 +396,10 @@ public T getSource(Class sourceClass) dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); is = correctedDeclStream(is, false); - return sourceClass.cast(new DOMSource(db.parse(is))); + DOMSource ds = new DOMSource(db.parse(is)); + if ( m_wrapped ) + domUnwrap(ds); + return sourceClass.cast(ds); } } catch ( Exception e ) @@ -591,6 +600,75 @@ public void close() throws IOException { } return ! mustBeDocument; } + + /** + * Unwrap a DOM tree parsed from input that was wrapped in a synthetic + * root element in case it had the form of {@code XML(CONTENT)}. + *

+ * Because the wrapping is applied pessimistically (it is done whenever + * a quick preparse did not conclusively prove the input was + * {@code DOCUMENT}), repeat the check here, where it requires only + * traversing one list of immediate DOM node children. Produce a + * {@code Document} node if possible, a {@code DocumentFragment} only if + * the tree really does not have {@code DOCUMENT} form. + * @param ds A {@code DOMSource} produced by parsing wrapped input. + * The parse result will be retrieved using {@code getNode()}, then + * replaced using {@code setNode()} with the unwrapped result, either a + * {@code Document} or a {@code DocumentFragment} node. + */ + private void domUnwrap(DOMSource ds) + { + Document d = (Document)ds.getNode(); + Element wrapper = d.getDocumentElement(); + /* + * Wrapping isn't done if the input has a DTD, so if we are here, + * the input does not have a DTD, and the null, null, null parameter + * list for createDocument is appropriate. + */ + Document newDoc = + d.getImplementation().createDocument(null, null, null); + DocumentFragment docFrag = newDoc.createDocumentFragment(); + boolean isDocument = true; + boolean seenElement = false; + for ( Node n = wrapper.getFirstChild(), next = null; + null != n; n = next ) + { + /* + * Grab the next sibling early, before the adoptNode() below, + * because that will unlink this node from its source Document, + * clearing its nextSibling link. + */ + next = n.getNextSibling(); + + switch ( n.getNodeType() ) + { + case Node.ELEMENT_NODE: + if ( seenElement ) + isDocument = false; + seenElement = true; + break; + case Node.COMMENT_NODE: + case Node.PROCESSING_INSTRUCTION_NODE: + break; + case Node.TEXT_NODE: + if ( ! ((Text)n).isElementContentWhitespace() ) + isDocument = false; + break; + default: + isDocument = false; + } + + docFrag.appendChild(newDoc.adoptNode(n)); + } + + if ( isDocument ) + { + newDoc.appendChild(docFrag); + ds.setNode(newDoc); + } + else + ds.setNode(docFrag); + } } static class Writable extends SQLXMLImpl From fce40fe43255b79213a7fd509a2dbd8ec385769b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 11 May 2018 23:22:16 -0400 Subject: [PATCH 0097/1087] Add an example doing "XML echo" at a lower level. While the simple use of Transformer.transform(src, rlt) in echoXMLParameter() is easy to read and covers a lot of cases without a lot of coding, lowLevelXMLEcho() shows that, for some cases, it is not hard to do without the Transformer. Comparisons of time and memory demands can be made. --- .../pljava/example/annotation/PassXML.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) 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 705b5307..a33d0382 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 @@ -18,6 +18,7 @@ import java.sql.SQLXML; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; @@ -157,6 +158,81 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) return ensureClosed(rlt, rx, howout); } + /** + * Echo the XML parameter back, using lower-level manipulations than + * {@code echoXMLParameter}. + *

+ * This illustrates how the simple use of {@code t.transform(src,rlt)} + * in {@code echoSQLXML} substitutes for a lot of fiddly case-by-case code + * (and not all the cases are even covered here!), but when coding for a + * specific case, all the generality of {@code transform} may not be needed. + * It can be interesting to compare memory use when XML values are large. + */ + @Function(schema="javatest") + public static SQLXML lowLevelXMLEcho(SQLXML sx, int how) + throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + SQLXML rx = c.createSQLXML(); + + try + { + switch ( how ) + { + case 1: + InputStream is = sx.getBinaryStream(); + OutputStream os = rx.setBinaryStream(); + shovelBytes(is, os); + break; + case 2: + Reader r = sx.getCharacterStream(); + Writer w = rx.setCharacterStream(); + shovelChars(r, w); + break; + case 3: + rx.setString(sx.getString()); + break; + case 4: + StreamSource ss = sx.getSource(StreamSource.class); + StreamResult sr = rx.setResult(StreamResult.class); + is = ss.getInputStream(); + r = ss.getReader(); + os = sr.getOutputStream(); + w = sr.getWriter(); + if ( null != is && null != os ) + { + shovelBytes(is, os); + break; + } + if ( null != r && null != r ) + { + shovelChars(r, w); + break; + } + throw new SQLDataException( + "Unimplemented combination of StreamSource/StreamResult"); + case 5: + case 6: + throw new SQLDataException( + "Unimplemented lowlevel SAX or StAX echo"); + case 7: + DOMSource ds = sx.getSource(DOMSource.class); + DOMResult dr = rx.setResult(DOMResult.class); + dr.setNode(ds.getNode()); + break; + default: + throw new SQLDataException( + "how must be 1-7 for lowLevelXMLEcho", "22003"); + } + } + catch ( IOException e ) + { + throw new SQLException( + "IOException in lowLevelXMLEcho", "58030", e); + } + return rx; + } + private static Source sxToSource(SQLXML sx, int how) throws SQLException { switch ( how ) @@ -311,4 +387,26 @@ public static void allowFragment(DOMResult r) throws SQLException throw new SQLException("Failed initializing DOMResult", pce); } } + + private static void shovelBytes(InputStream is, OutputStream os) + throws IOException + { + byte[] b = new byte[8192]; + int got; + while ( -1 != (got = is.read(b)) ) + os.write(b, 0, got); + is.close(); + os.close(); + } + + private static void shovelChars(Reader r, Writer w) + throws IOException + { + char[] b = new char[8192]; + int got; + while ( -1 != (got = r.read(b)) ) + w.write(b, 0, got); + r.close(); + w.close(); + } } From a73abee1385c22861f8b8357d28360c073bad916 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 13 May 2018 00:26:46 -0400 Subject: [PATCH 0098/1087] Allow returning/storing a 'readable' SQLXML. Relax the distinction between 'readable' and 'writable' SQLXML instances just enough to allow a readable one passed in (or, later, retrieved from a ResultSet) to be directly returned (or, later, passed as a parameter or into a ResultSet), as long as it is still readable, i.e., has not had any of the reading methods called by Java. It can be used that way exactly once, as the native code retakes ownership of it at that point. --- .../pljava/example/annotation/PassXML.java | 10 +++ pljava-so/src/main/c/VarlenaWrapper.c | 42 +++++++---- pljava-so/src/main/c/type/SQLXMLImpl.c | 22 ++++-- .../src/main/include/pljava/VarlenaWrapper.h | 2 +- .../postgresql/pljava/internal/DualState.java | 11 ++- .../pljava/internal/VarlenaWrapper.java | 73 +++++++++++++++---- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 22 +++++- 7 files changed, 140 insertions(+), 42 deletions(-) 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 a33d0382..2ba8553a 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 @@ -83,6 +83,16 @@ public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) return echoSQLXML(sx, howin, howout); } + /** + * "Echo" an XML parameter not by creating a new writable {@code SQLXML} + * object at all, but simply returning the passed-in readable one untouched. + */ + @Function(schema="javatest") + public static SQLXML bounceXMLParameter(SQLXML sx) throws SQLException + { + return sx; + } + /** * Precompile an XSL transform {@code source} and save it (for the * current session) as {@code name}. diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 90bd969c..d6f9bcd4 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -20,13 +20,15 @@ #define INITIALSIZE 1024 +static jclass s_VarlenaWrapper_class; +static jmethodID s_VarlenaWrapper_adopt; + static jclass s_VarlenaWrapper_Input_class; static jclass s_VarlenaWrapper_Output_class; static jmethodID s_VarlenaWrapper_Input_init; static jmethodID s_VarlenaWrapper_Output_init; -static jmethodID s_VarlenaWrapper_Output_adopt; /* * For VarlenaWrapper.Output, define a dead-simple "expanded object" format @@ -70,35 +72,44 @@ typedef struct ExpandedVarlenaOutputStreamHeader * of an existing Datum d, which must be a varlena type (assumed, not checked * here). * - * The datum will be copied (detoasting if need be) into the memory context mc, + * The datum will be copied (detoasting if need be) into a memory context with + * parent as its parent, so it can be efficiently reparented later if adopted, * and the VarlenaWrapper will be associated with the ResourceOwner ro, which - * determines its lifespan. The ResourceOwner needs to be one that will be - * released no later than the memory context itself. + * determines its lifespan (if not adopted). The ResourceOwner needs to be one + * that will be released no later than the memory context itself. */ -jobject pljava_VarlenaWrapper_Input(Datum d, MemoryContext mc, ResourceOwner ro) +jobject pljava_VarlenaWrapper_Input( + Datum d, MemoryContext parent, ResourceOwner ro) { jobject vr; jobject dbb; + MemoryContext mc; MemoryContext prevcxt; struct varlena *copy; Ptr2Long p2lro; + Ptr2Long p2lcxt; Ptr2Long p2ldatum; + mc = AllocSetContextCreate(parent, "PL/Java VarlenaWrapper.Input", + ALLOCSET_START_SMALL_SIZES); + prevcxt = MemoryContextSwitchTo(mc); copy = PG_DETOAST_DATUM_COPY(d); MemoryContextSwitchTo(prevcxt); p2lro.longVal = 0L; + p2lcxt.longVal = 0L; p2ldatum.longVal = 0L; p2lro.ptrVal = ro; + p2lcxt.ptrVal = mc; p2ldatum.ptrVal = copy; dbb = JNI_newDirectByteBuffer(VARDATA(copy), VARSIZE_ANY_EXHDR(copy)); vr = JNI_newObjectLocked(s_VarlenaWrapper_Input_class, s_VarlenaWrapper_Input_init, pljava_DualState_key(), - p2lro.longVal, p2ldatum.longVal, dbb); + p2lro.longVal, p2lcxt.longVal, p2ldatum.longVal, dbb); JNI_deleteLocalRef(dbb); return vr; @@ -168,14 +179,15 @@ jobject pljava_VarlenaWrapper_Output(MemoryContext parent, ResourceOwner ro) } /* - * Adopt a VarlenaWrapper.Output after Java code has written and closed it. - * The wrapper will no longer be accessible from Java. + * Adopt a VarlenaWrapper (if Output, after Java code has written and closed it) + * and leave it no longer accessible from Java. */ -Datum pljava_VarlenaWrapper_Output_adopt(jobject vlos) +Datum pljava_VarlenaWrapper_adopt(jobject vlw) { Ptr2Long p2l; - p2l.longVal = JNI_callLongMethodLocked(vlos, s_VarlenaWrapper_Output_adopt); + p2l.longVal = JNI_callLongMethodLocked(vlw, s_VarlenaWrapper_adopt, + pljava_DualState_key()); return PointerGetDatum(p2l.ptrVal); } @@ -219,6 +231,9 @@ void pljava_VarlenaWrapper_initialize(void) { 0, 0, 0 } }; + s_VarlenaWrapper_class = + (jclass)JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/VarlenaWrapper")); s_VarlenaWrapper_Input_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/VarlenaWrapper$Input")); @@ -229,15 +244,16 @@ void pljava_VarlenaWrapper_initialize(void) s_VarlenaWrapper_Input_init = PgObject_getJavaMethod( s_VarlenaWrapper_Input_class, "", "(Lorg/postgresql/pljava/internal/DualState$Key;" - "JJLjava/nio/ByteBuffer;)V"); + "JJJLjava/nio/ByteBuffer;)V"); s_VarlenaWrapper_Output_init = PgObject_getJavaMethod( s_VarlenaWrapper_Output_class, "", "(Lorg/postgresql/pljava/internal/DualState$Key;" "JJJLjava/nio/ByteBuffer;)V"); - s_VarlenaWrapper_Output_adopt = PgObject_getJavaMethod( - s_VarlenaWrapper_Output_class, "adopt", "()J"); + s_VarlenaWrapper_adopt = PgObject_getJavaMethod( + s_VarlenaWrapper_class, "adopt", + "(Lorg/postgresql/pljava/internal/DualState$Key;)J"); clazz = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/VarlenaWrapper$Output$State")); diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 6e8054ab..2ea090c0 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -16,11 +16,12 @@ #include "pljava/type/Type_priv.h" #include "pljava/VarlenaWrapper.h" +static jclass s_SQLXML_class; +static jmethodID s_SQLXML_adopt; static jclass s_SQLXML_Readable_class; static jmethodID s_SQLXML_Readable_init; static jclass s_SQLXML_Writable_class; static jmethodID s_SQLXML_Writable_init; -static jmethodID s_SQLXML_Writable_adopt; static jvalue _SQLXML_coerceDatum(Type self, Datum arg) { @@ -35,10 +36,14 @@ static jvalue _SQLXML_coerceDatum(Type self, Datum arg) static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) { - jobject vwo = JNI_callObjectMethodLocked(sqlxml, s_SQLXML_Writable_adopt); - Datum d = pljava_VarlenaWrapper_Output_adopt(vwo); - JNI_deleteLocalRef(vwo); - return TransferExpandedObject(d, CurrentMemoryContext); + jobject vw = JNI_callObjectMethodLocked(sqlxml, s_SQLXML_adopt); + Datum d = pljava_VarlenaWrapper_adopt(vw); + JNI_deleteLocalRef(vw); + if ( VARATT_IS_EXTERNAL_EXPANDED_RW(DatumGetPointer(d)) ) + return TransferExpandedObject(d, CurrentMemoryContext); + MemoryContextSetParent( + GetMemoryChunkContext(DatumGetPointer(d)), CurrentMemoryContext); + return d; } /* Make this datatype available to the postgres system. @@ -64,6 +69,11 @@ void pljava_SQLXMLImpl_initialize(void) cls->coerceObject = _SQLXML_coerceObject; Type_registerType("java.sql.SQLXML", TypeClass_allocInstance(cls, XMLOID)); + s_SQLXML_class = JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/jdbc/SQLXMLImpl")); + s_SQLXML_adopt = PgObject_getJavaMethod(s_SQLXML_class, + "adopt", "()Lorg/postgresql/pljava/internal/VarlenaWrapper;"); + s_SQLXML_Readable_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl$Readable")); s_SQLXML_Readable_init = PgObject_getJavaMethod(s_SQLXML_Readable_class, @@ -73,8 +83,6 @@ void pljava_SQLXMLImpl_initialize(void) "org/postgresql/pljava/jdbc/SQLXMLImpl$Writable")); s_SQLXML_Writable_init = PgObject_getJavaMethod(s_SQLXML_Writable_class, "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Output;)V"); - s_SQLXML_Writable_adopt = PgObject_getJavaMethod(s_SQLXML_Writable_class, - "adopt", "()Lorg/postgresql/pljava/internal/VarlenaWrapper$Output;"); clazz = PgObject_getJavaClass("org/postgresql/pljava/jdbc/SQLXMLImpl"); PgObject_registerNatives2(clazz, methods); diff --git a/pljava-so/src/main/include/pljava/VarlenaWrapper.h b/pljava-so/src/main/include/pljava/VarlenaWrapper.h index 08f56222..bbfd692f 100644 --- a/pljava-so/src/main/include/pljava/VarlenaWrapper.h +++ b/pljava-so/src/main/include/pljava/VarlenaWrapper.h @@ -26,7 +26,7 @@ extern jobject pljava_VarlenaWrapper_Input( extern jobject pljava_VarlenaWrapper_Output(MemoryContext mc, ResourceOwner ro); -extern Datum pljava_VarlenaWrapper_Output_adopt(jobject vlos); +extern Datum pljava_VarlenaWrapper_adopt(jobject vlos); extern void pljava_VarlenaWrapper_initialize(void); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index aa04cd74..a2d776cd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -103,6 +103,13 @@ public abstract class DualState extends WeakReference */ private final long m_resourceOwner; + protected static void checkCookie(Key cookie) + { + if ( ! Key.class.isInstance(cookie) ) + throw new UnsupportedOperationException( + "Operation on DualState instance without cookie"); + } + /** * Construct a {@code DualState} instance with a reference to the Java * object whose state it represents. @@ -123,9 +130,7 @@ protected DualState(Key cookie, T referent, long resourceOwner) { super(referent, s_releasedInstances); - if ( ! (cookie instanceof Key) ) - throw new UnsupportedOperationException( - "Constructing DualState instance without cookie"); + checkCookie(cookie); m_resourceOwner = resourceOwner; s_liveInstances.add(this); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 800626d4..1a8f6ca0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -11,6 +11,7 @@ */ package org.postgresql.pljava.internal; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -20,8 +21,26 @@ import java.sql.SQLException; -public interface VarlenaWrapper +/** + * Interface that wraps a PostgreSQL native variable-length ("varlena") datum; + * implementing classes present an existing one to Java as a readable + * {@code InputStream}, or allow a new one to be constructed by presenting a + * writable {@code OutputStream}. + *

+ * Common to both is a single method {@link #adopt adopt()}, allowing native + * code to reassert control over the varlena (for the writable variety, after + * Java code has written and closed it), after which it is no longer accessible + * from Java. + */ +public interface VarlenaWrapper extends Closeable { + /** + * Return the varlena address to native code and dissociate the varlena + * from Java. + * @param cookie Capability held by native code. + */ + long adopt(DualState.Key cookie) throws SQLException; + /** * A class by which Java reads the content of a varlena as an InputStream. * @@ -29,7 +48,7 @@ public interface VarlenaWrapper * the native reference; the chosen resource owner must be one that will be * released no later than the memory context containing the varlena. */ - public static class Input extends InputStream + public static class Input extends InputStream implements VarlenaWrapper { private State m_state; private boolean m_open = true; @@ -39,16 +58,18 @@ public static class Input extends InputStream * @param cookie Capability held by native code. * @param resourceOwner Resource owner whose release will indicate that the * underlying varlena is no longer valid. + * @param context Memory context in which the varlena is allocated. * @param varlenaPtr Pointer value to the underlying varlena, to be * {@code pfree}d when Java code closes or reclaims this object. * @param buf Readable direct {@code ByteBuffer} constructed over the * varlena's data bytes. */ private Input(DualState.Key cookie, long resourceOwner, - long varlenaPtr, ByteBuffer buf) + long context, long varlenaPtr, ByteBuffer buf) { m_state = new State( - cookie, this, resourceOwner, varlenaPtr, buf.asReadOnlyBuffer()); + cookie, this, resourceOwner, + context, varlenaPtr, buf.asReadOnlyBuffer()); } private ByteBuffer buf() throws IOException @@ -177,17 +198,33 @@ public boolean markSupported() return true; } + @Override + public long adopt(DualState.Key cookie) throws SQLException + { + synchronized ( m_state ) + { + if ( ! m_open ) + throw new SQLException( + "Cannot adopt VarlenaWrapper.Input after it is closed", + "55000"); + return m_state.adopt(cookie); + } + } + - private static class State extends DualState.SinglePfree + private static class State + extends DualState.SingleMemContextDelete { private ByteBuffer m_buf; + private long m_varlena; private State( DualState.Key cookie, Input vr, long resourceOwner, - long varlenaPtr, ByteBuffer buf) + long memContext, long varlenaPtr, ByteBuffer buf) { - super(cookie, vr, resourceOwner, varlenaPtr); + super(cookie, vr, resourceOwner, memContext); + m_varlena = varlenaPtr; m_buf = buf; } @@ -197,6 +234,15 @@ private ByteBuffer buffer() throws SQLException return m_buf; } + private long adopt(DualState.Key cookie) throws SQLException + { + checkCookie(cookie); + assertNativeStateIsValid(); + long varlena = m_varlena; + nativeStateReleased(); + return varlena; + } + @Override protected void nativeStateReleased() { @@ -220,7 +266,7 @@ protected void javaStateReleased() * the native reference; the chosen resource owner must be one that will be * released no later than the memory context containing the varlena. */ - public class Output extends OutputStream + public class Output extends OutputStream implements VarlenaWrapper { private State m_state; private boolean m_open = true; @@ -325,10 +371,8 @@ public void free() throws IOException m_state.javaStateReleased(); } - /* - * Called only from native code to retrieve the varlena after writing. - */ - private long adopt() throws SQLException + @Override + public long adopt(DualState.Key cookie) throws SQLException { synchronized ( m_state ) { @@ -336,7 +380,7 @@ private long adopt() throws SQLException throw new SQLException( "Writing of VarlenaWrapper.Output not yet complete", "55000"); - return m_state.adopt(); + return m_state.adopt(cookie); } } @@ -380,8 +424,9 @@ private ByteBuffer nextBuffer(int desiredCapacity) return m_buf; } - private long adopt() throws SQLException + private long adopt(DualState.Key cookie) throws SQLException { + checkCookie(cookie); assertNativeStateIsValid(); long varlena = m_varlena; nativeStateReleased(); diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index fa444d89..00d74dc8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -26,7 +26,6 @@ /* ... for SQLXMLImpl */ -import java.io.Closeable; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; @@ -126,7 +125,7 @@ import javax.xml.stream.XMLStreamException; -public abstract class SQLXMLImpl implements SQLXML +public abstract class SQLXMLImpl implements SQLXML { protected AtomicReference m_backing; @@ -257,9 +256,11 @@ static SQLXML newWritable() } } + protected abstract VarlenaWrapper adopt() throws SQLException; + private static native SQLXML _newWritable(); - static class Readable extends SQLXMLImpl + static class Readable extends SQLXMLImpl { private AtomicBoolean m_readable = new AtomicBoolean(true); private Charset m_serverCS = implServerCharset(); @@ -412,6 +413,18 @@ public T getSource(Class sourceClass) sourceClass.getName() + ".class)", "0A000"); } + @Override + protected VarlenaWrapper adopt() throws SQLException + { + VarlenaWrapper vw = m_backing.getAndSet(null); + if ( ! m_readable.get() ) + throw new SQLNonTransientException( + "SQLXML object has already been read from", "55000"); + if ( null == vw ) + backingIfNotFreed(); /* shorthand way to throw the exception */ + return vw; + } + /** * Return an InputStream presenting the contents of the underlying * varlena, but with the leading declaration corrected if need be. @@ -849,7 +862,8 @@ private void serializeDOM(DOMResult r, OutputStream os) } } - private VarlenaWrapper.Output adopt() throws SQLException + @Override + protected VarlenaWrapper adopt() throws SQLException { VarlenaWrapper.Output vwo = m_backing.getAndSet(null); if ( m_writable.get() ) From aa46edb392a2bfa254a071b11baf9d336cf2bd72 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 29 May 2018 23:42:25 -0400 Subject: [PATCH 0099/1087] SQLXML in/out of PreparedStatements / ResultSets. For PreparedStatement.setObject(col, x, Types.SQLXML) to work, it's sufficient to mention the type in Oid.c. Getting the type back from the ResultSet already works, thanks to the Type_registerType() already done in SQLXMLImpl.c. However, the test in the getObject(col, SQLXML.class) needed fixing, as it was testing exact class equality, not whether it got an implementation of the desired interface. (In passing, reorder those methods to make clearer what JDBC revision brought them in.) The setObject overload without the explicit Types.SQLXML will not work yet, and simply adding addType(SQLXML.class,Types.SQLXML) in SPIConnection is not sufficient ... because that uses a hash table and therefore also depends on exact class equality; the full grody names of the implementing classes would have to be added. Or the mechanism would have to be fixed. Wondering why the out.updateObject(col, x) works, without giving an explicit type? That result set is already typed (by the column definition list you have to include in the query calling this RECORD-typed function). That method succeeding is /almost/ enough to make a subsequent ps.setObject(col, x) work right (because in succeeding, it primed the s_class2typeId cache in Oid); how would that be for spooky action at a distance? But that doesn't quite happen, because that cache is also a strict-class-equality hash map. At this point, implementing the various (get,set,update}SQLXML methods will be an easy formality, as they can be implemented in terms of the methods taking explicit types, which now work. Alas, this current behavior won't do for a 1.5.x release, because it would actually start producing SQLXML objects by default where old code may be expecting String. Before merging, this will need a way to suppress that automatic mapping but still allow the explicit kind. A subsequent major release can then go the rest of the way. --- .../pljava/example/annotation/PassXML.java | 24 +++++++++++ pljava-so/src/main/c/type/Oid.c | 6 ++- .../pljava/jdbc/AbstractResultSet.java | 43 ++++++++++++------- 3 files changed, 56 insertions(+), 17 deletions(-) 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 2ba8553a..cf953b12 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 @@ -13,9 +13,12 @@ import java.sql.Connection; import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLDataException; import java.sql.SQLException; import java.sql.SQLXML; +import java.sql.Types; import java.io.IOException; import java.io.InputStream; @@ -243,6 +246,27 @@ public static SQLXML lowLevelXMLEcho(SQLXML sx, int how) return rx; } + /** + * Create some XML, pass it to a {@code SELECT ?} prepared statement, + * retrieve it from the result set, and return it via the out-parameter + * result set of this {@code RECORD}-returning function. + */ + @Function(schema="javatest", type="RECORD") + public static boolean xmlInStmtAndRS(ResultSet out) throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + SQLXML x = c.createSQLXML(); + x.setString(""); + PreparedStatement ps = c.prepareStatement("SELECT ?"); + ps.setObject(1, x, Types.SQLXML); + ResultSet rs = ps.executeQuery(); + rs.next(); + x = rs.getObject(1, SQLXML.class); + ps.close(); + out.updateObject(1, x); + return true; + } + private static Source sxToSource(SQLXML sx, int how) throws SQLException { switch ( how ) diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index 2c2690b4..2ef951c0 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -110,14 +110,18 @@ Oid Oid_forSqlType(int sqlType) case java_sql_Types_STRUCT: case java_sql_Types_ARRAY: case java_sql_Types_REF: + typeId = InvalidOid; /* Not yet mapped */ + break; /* JDBC 4.0 - present in Java 6 and later, no need to conditionalize */ + case java_sql_Types_SQLXML: + typeId = XMLOID; + break; case java_sql_Types_ROWID: case java_sql_Types_NCHAR: case java_sql_Types_NVARCHAR: case java_sql_Types_LONGNVARCHAR: case java_sql_Types_NCLOB: - case java_sql_Types_SQLXML: /* JDBC 4.2 - conditionalize until only Java 8 and later supported */ #ifdef java_sql_Types_REF_CURSOR diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index 4c438b81..a01d062d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -168,22 +168,6 @@ public Object getObject(String columnName, Map map) return this.getObject(this.findColumn(columnName), map); } - public T getObject(int columnIndex, Class type) - throws SQLException - { - final Object obj = getObject( columnIndex ); - if ( obj.getClass().equals( type ) ) return (T) obj; - throw new SQLException( "Cannot convert " + obj.getClass().getName() + " to " + type ); - } - - public T getObject(String columnName, Class type) - throws SQLException - { - final Object obj = getObject( columnName ); - if ( obj.getClass().equals( type ) ) return (T) obj; - throw new SQLException( "Cannot convert " + obj.getClass().getName() + " to " + type ); - } - public Ref getRef(String columnName) throws SQLException { @@ -770,4 +754,31 @@ public RowId getRowId(String columnLabel) throw new SQLFeatureNotSupportedException( this.getClass() + ".getRowId( String ) not implemented yet.", "0A000" ); } + + // ************************************************************ + // Implementation of JDBC 4.1 methods. These are half-baked at + // the moment: the type parameter isn't able to /influence/ + // what type is returned, but only to fail if what gets + // returned by default isn't that. + // ************************************************************ + + public T getObject(int columnIndex, Class type) + throws SQLException + { + final Object obj = getObject( columnIndex ); + if ( type.isInstance(obj) ) + return type.cast(obj); + throw new SQLException( "Cannot convert " + obj.getClass().getName() + + " to " + type ); + } + + public T getObject(String columnName, Class type) + throws SQLException + { + final Object obj = getObject( columnName ); + if ( type.isInstance(obj) ) + return type.cast(obj); + throw new SQLException( "Cannot convert " + obj.getClass().getName() + + " to " + type ); + } } From f8b39a6333a82ea87c0497b94332eb5dc1c65d15 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 May 2018 02:32:47 -0400 Subject: [PATCH 0100/1087] Confirm SQLXML can be read from a mapped composite. It can; nothing more needed to be done (readSQLXML still throws SQLFeatureNotSupported, but readObject returns SQLXML). On the SQLOutput side, there is no general-purpose writeObject as there is for statements and result sets. Checking that will have to wait until writeSQLXML no longer throws NotSupported. --- .../pljava/example/annotation/PassXML.java | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) 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 cf953b12..f6f8ac06 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 @@ -15,12 +15,16 @@ import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.sql.SQLDataException; -import java.sql.SQLException; +import java.sql.SQLData; +import java.sql.SQLInput; +import java.sql.SQLOutput; import java.sql.SQLXML; +import java.sql.Statement; import java.sql.Types; -import java.io.IOException; +import java.sql.SQLDataException; +import java.sql.SQLException; + import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; @@ -28,6 +32,8 @@ import java.io.StringWriter; import java.io.Writer; +import java.io.IOException; + import java.util.Map; import java.util.HashMap; @@ -38,9 +44,10 @@ import javax.xml.transform.Source; import javax.xml.transform.Templates; import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerException; + import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.transform.dom.DOMResult; @@ -51,11 +58,18 @@ import javax.xml.transform.stax.StAXSource; import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.MappedUDT; /** * Class illustrating use of {@link SQLXML} to operate on XML data. + *

+ * This class also serves as the mapping class for a composite type + * {@code javatest.onexml}, the better to verify that {@link SQLData} + * input/output works too. That's why it has to implement SQLData. */ -public class PassXML +@MappedUDT(schema="javatest", name="onexml", structure="c1 xml", + comment="A composite type mapped by the PassXML example class") +public class PassXML implements SQLData { static SQLXML s_sx; @@ -443,4 +457,44 @@ private static void shovelChars(Reader r, Writer w) r.close(); w.close(); } + + /* + * Required to serve as a MappedUDT: + */ + public PassXML() { } + + private String m_typeName; + private SQLXML m_value; + + @Override + public String getSQLTypeName() { return m_typeName; } + + @Override + public void readSQL(SQLInput stream, String typeName) throws SQLException + { + m_typeName = typeName; + m_value = (SQLXML) stream.readObject(); + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException + { + stream.writeSQLXML(m_value); // this is not expected to work yet + } + + /* + * Test the MappedUDT (in one direction anyway). + */ + @Function(schema="javatest") + public static SQLXML xmlFromComposite() throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + Statement s = c.createStatement(); + ResultSet r = s.executeQuery( + "SELECT CAST(ROW(XMLELEMENT(NAME a)) AS javatest.onexml)"); + r.next(); + PassXML obj = r.getObject(1, PassXML.class); + s.close(); + return obj.m_value; + } } From 964aba2dfaffcfe0bdbef902caa259540694b555 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 May 2018 23:40:11 -0400 Subject: [PATCH 0101/1087] Take off the training wheels. --- .../pljava/jdbc/AbstractResultSet.java | 54 +++++++++---------- .../pljava/jdbc/SPIPreparedStatement.java | 24 +++++---- .../pljava/jdbc/SQLInputFromTuple.java | 23 ++++---- .../pljava/jdbc/SQLOutputToTuple.java | 22 ++++---- 4 files changed, 65 insertions(+), 58 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index a01d062d..2071a282 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -393,6 +393,30 @@ public T unwrap(Class iface) "0A000" ); } + public void updateSQLXML(int columnIndex, SQLXML xmlObject) + throws SQLException + { + updateObject(columnIndex, xmlObject); + } + + public void updateSQLXML(String columnLabel, SQLXML xmlObject) + throws SQLException + { + updateObject(columnLabel, xmlObject); + } + + public SQLXML getSQLXML(int columnIndex) + throws SQLException + { + return (SQLXML)getObject(columnIndex); + } + + public SQLXML getSQLXML(String columnLabel) + throws SQLException + { + return (SQLXML)getObject(columnLabel); + } + // ************************************************************ // Non-implementation of JDBC 4 methods. // ************************************************************ @@ -666,36 +690,6 @@ public String getNString(String columnLabel) "0A000" ); } - public void updateSQLXML(int columnIndex, SQLXML xmlObject) - throws SQLException - { - throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateSQLXML( int, SQLXML ) not implemented yet.", - "0A000" ); - } - - public void updateSQLXML(String columnLabel, SQLXML xmlObject) - throws SQLException - { - throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateSQLXML( String, SQLXML ) not implemented yet.", - "0A000" ); - } - - public SQLXML getSQLXML(int columnIndex) - throws SQLException - { - throw new SQLFeatureNotSupportedException( this.getClass() + - ".getSQLXML( int ) not implemented yet.", "0A000" ); - } - - public SQLXML getSQLXML(String columnLabel) - throws SQLException - { - throw new SQLFeatureNotSupportedException( this.getClass() + - ".getSQLXML( String ) not implemented yet.", "0A000" ); - } - public NClob getNClob(String columnLabel) throws SQLException { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index f78c40cc..ab5f771a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -434,6 +434,20 @@ protected long executeBatchEntry(Object batchEntry) return ret; } + // ************************************************************ + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. + // ************************************************************ + + public void setSQLXML(int parameterIndex, + SQLXML xmlObject) + throws SQLException + { + setObject(parameterIndex, xmlObject, Types.SQLXML); + } + // ************************************************************ // Non-implementation of JDBC 4 methods. // ************************************************************ @@ -611,16 +625,6 @@ public void setAsciiStream(int parameterIndex, "0A000" ); } - - public void setSQLXML(int parameterIndex, - SQLXML xmlObject) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".setSQLXML( int, SQLXML ) not implemented yet.", - "0A000" ); - } public void setNString(int parameterIndex, String value) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java index 070f3eb9..38bc18ab 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java @@ -199,6 +199,20 @@ private Object readValue(Class valueClass) throws SQLException { return SPIConnection.basicCoersion(valueClass, this.readObject()); } + + // ************************************************************ + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. + // ************************************************************ + + public SQLXML readSQLXML() + throws SQLException + { + return (SQLXML)this.readValue(SQLXML.class); + } + // ************************************************************ // Non-implementation of JDBC 4 methods. // ************************************************************ @@ -213,15 +227,6 @@ public RowId readRowId() "0A000" ); } - public SQLXML readSQLXML() - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".readSQLXML() not implemented yet.", - "0A000" ); - } - public String readNString() throws SQLException { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java index ddce3499..79086c6d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java @@ -200,6 +200,19 @@ public void writeURL(URL value) throws SQLException this.writeValue(value.toString()); } + // ************************************************************ + // Implementation of JDBC 4 methods. Methods go here if they + // don't throw SQLFeatureNotSupportedException; they can be + // considered implemented even if they do nothing useful, as + // long as that's an allowed behavior by the JDBC spec. + // ************************************************************ + + public void writeSQLXML(SQLXML x) + throws SQLException + { + this.writeValue(x); + } + // ************************************************************ // Non-implementation of JDBC 4 methods. // ************************************************************ @@ -230,15 +243,6 @@ public void writeRowId(RowId x) + ".writeRowId( RowId ) not implemented yet.", "0A000" ); } - - public void writeSQLXML(SQLXML x) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".writeSQLXML( SQLXML ) not implemented yet.", - "0A000" ); - } // ************************************************************ // End of non-implementation of JDBC 4 methods. From b795a760aab046fe5e805398af7ea28569c6fc4b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 May 2018 23:53:44 -0400 Subject: [PATCH 0102/1087] Complete the mapped composite example. --- .../pljava/example/annotation/PassXML.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 f6f8ac06..105620d2 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 @@ -479,7 +479,7 @@ public void readSQL(SQLInput stream, String typeName) throws SQLException @Override public void writeSQL(SQLOutput stream) throws SQLException { - stream.writeSQLXML(m_value); // this is not expected to work yet + stream.writeSQLXML(m_value); } /* @@ -489,12 +489,18 @@ public void writeSQL(SQLOutput stream) throws SQLException public static SQLXML xmlFromComposite() throws SQLException { Connection c = DriverManager.getConnection("jdbc:default:connection"); - Statement s = c.createStatement(); - ResultSet r = s.executeQuery( - "SELECT CAST(ROW(XMLELEMENT(NAME a)) AS javatest.onexml)"); + PreparedStatement ps = + c.prepareStatement("SELECT CAST(? AS javatest.onexml)"); + SQLXML x = c.createSQLXML(); + x.setString(""); + PassXML obj = new PassXML(); + obj.m_value = x; + obj.m_typeName = "javatest.onexml"; + ps.setObject(1, obj); + ResultSet r = ps.executeQuery(); r.next(); - PassXML obj = r.getObject(1, PassXML.class); - s.close(); + obj = r.getObject(1, PassXML.class); + ps.close(); return obj.m_value; } } From 2d850c64d1285f161643e57543820ee1f8d143d7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 16 Jun 2018 18:21:34 -0400 Subject: [PATCH 0103/1087] Enter type into yet one more mapping. There are too many places in this code base where type mappings are represented. Without this one, ResultSetMetaData will still return Types.OTHER from getColumnType(), even though ResultSet.getObject() correctly returns a SQLXML object. Add comments in favor of future consolidation of the various places where type mappings have to be kept up to date. --- .../pljava/example/annotation/PassXML.java | 5 ++++ .../postgresql/pljava/jdbc/SPIConnection.java | 24 ++++++++++++++----- .../pljava/jdbc/SPIDatabaseMetaData.java | 2 +- 3 files changed, 24 insertions(+), 7 deletions(-) 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 105620d2..f6fd45aa 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 @@ -60,6 +60,8 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; +import static org.postgresql.pljava.example.LoggerTest.logMessage; + /** * Class illustrating use of {@link SQLXML} to operate on XML data. *

@@ -275,6 +277,9 @@ public static boolean xmlInStmtAndRS(ResultSet out) throws SQLException ps.setObject(1, x, Types.SQLXML); ResultSet rs = ps.executeQuery(); rs.next(); + if ( Types.SQLXML != rs.getMetaData().getColumnType(1) ) + logMessage("WARNING", + "ResultSetMetaData.getColumnType() misreports SQLXML"); x = rs.getObject(1, SQLXML.class); ps.close(); out.updateObject(1, x); diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 5f6897d7..9828513b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -742,16 +742,16 @@ public int[] getVersionNumber() throws SQLException /** * Convert a PostgreSQL type name to a {@link Types} integer, using the - * {@code JDBC3_TYPE_NAMES}/{@code JDBC_TYPE_NUMBERS} arrays; used in - * {@link DatabaseMetaData} and {@link ResultSetMetaData}. + * {@code JDBC_TYPE_NAMES}/{@code JDBC_TYPE_NUMBERS} arrays; used in + * two places in {@link DatabaseMetaData}. */ public int getSQLType(String pgTypeName) { if (pgTypeName == null) return Types.OTHER; - for (int i = 0;i < JDBC3_TYPE_NAMES.length;i++) - if (pgTypeName.equals(JDBC3_TYPE_NAMES[i])) + for (int i = 0;i < JDBC_TYPE_NAMES.length;i++) + if (pgTypeName.equals(JDBC_TYPE_NAMES[i])) return JDBC_TYPE_NUMBERS[i]; return Types.OTHER; @@ -760,7 +760,14 @@ public int getSQLType(String pgTypeName) /** * This returns the {@link Types} type for a PG type oid, by mapping it * to a name using {@link #getPGType} and then to the result via - * {@link #getSQLType(String)}. + * {@link #getSQLType(String)}; used in {@link ResultSetMetaData} and + * five places in {@link DatabaseMetaData}. + *

+ * This method is a bit goofy, as it first maps from Oid to type name, and + * then from name to JDBC type, all to accomplish the inverse of the JDBC + * type / Oid mapping that already exists in Oid.c, and so the mapping + * arrays in this file have to be updated in sync with that. Look into + * future consolidation.... * * @param oid PostgreSQL type oid * @return the java.sql.Types type @@ -983,10 +990,13 @@ else if(value instanceof String) * They default automatically to Types.OTHER * * Note: This must be in the same order as below. + * + * These arrays are not only used by getSQLType() in this file, but also + * directly accessed by getUDTs() in DatabaseMetaData. * * Tip: keep these grouped together by the Types. value */ - public static final String JDBC3_TYPE_NAMES[] = { + public static final String JDBC_TYPE_NAMES[] = { "int2", "int4", "oid", "int8", @@ -1002,6 +1012,7 @@ else if(value instanceof String) "date", "time", "timetz", "abstime", "timestamp", "timestamptz", + "xml", "_bool", "_char", "_int2", "_int4", "_text", "_oid", "_varchar", "_int8", "_float4", "_float8", "_abstime", "_date", "_time", "_timestamp", "_numeric", @@ -1032,6 +1043,7 @@ else if(value instanceof String) Types.DATE, Types.TIME, Types.TIME, Types.TIMESTAMP, Types.TIMESTAMP, Types.TIMESTAMP, + Types.SQLXML, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index 76016f20..2a5b72b6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -2996,7 +2996,7 @@ public java.sql.ResultSet getUDTs(String catalog, String schemaPattern, + " end as data_type, pg_catalog.obj_description(t.oid, 'pg_type') " + "as remarks, CASE WHEN t.typtype = 'd' then (select CASE"; - for(int i = 0; i < SPIConnection.JDBC3_TYPE_NAMES.length; i++) + for(int i = 0; i < SPIConnection.JDBC_TYPE_NAMES.length; i++) { sql += " when typname = '" + SPIConnection.JDBC_TYPE_NUMBERS[i] + "' then " + SPIConnection.JDBC_TYPE_NUMBERS[i]; From f4f9c28257a70b0ff2264f50e645f6f8a3527319 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Jun 2018 21:38:23 -0400 Subject: [PATCH 0104/1087] A largely-working XMLQUERY using s9api. Known limitations: Maps all INTERVALs to xs:duration, rather than to yearMonthDuration or dayTimeDuration as called for in the spec (PL/Java ResultSetMetaData not exposing adequate information from the typmod). Maps all NUMERIC / DECIMAL to xs:decimal, not to xs:integer when scale is zero as called for in the spec (PL/Java ResultSetMetaData returning hardcoded value for scale). Surely needs more work around time zones for types that may or may not have them. --- pljava-examples/pom.xml | 5 + .../pljava/example/annotation/S9.java | 795 ++++++++++++++++++ 2 files changed, 800 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 19bd0b43..5070544d 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -15,6 +15,11 @@ pljava-api ${project.version} + + net.sf.saxon + Saxon-HE + 9.8.0-12 + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java new file mode 100644 index 00000000..23153fa6 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java @@ -0,0 +1,795 @@ +/* + * Copyright (c) 2018- 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.io.StringReader; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import static java.sql.ResultSetMetaData.columnNoNulls; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; + +import java.sql.SQLException; +import java.sql.SQLDataException; +import java.sql.SQLNonTransientException; +import java.sql.SQLSyntaxErrorException; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.transform.stream.StreamSource; + +import net.sf.saxon.om.SequenceIterator; + +import net.sf.saxon.query.QueryResult; +import net.sf.saxon.query.StaticQueryContext; + +import net.sf.saxon.tree.iter.LookaheadIterator; + +import net.sf.saxon.s9api.DocumentBuilder; +import net.sf.saxon.s9api.ItemType; +import net.sf.saxon.s9api.ItemTypeFactory; +import net.sf.saxon.s9api.OccurrenceIndicator; +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.s9api.QName; +import net.sf.saxon.s9api.SequenceType; +import net.sf.saxon.s9api.XdmAtomicValue; +import static net.sf.saxon.s9api.XdmAtomicValue.makeAtomicValue; +import net.sf.saxon.s9api.XdmEmptySequence; +import static net.sf.saxon.s9api.XdmNodeKind.DOCUMENT; +import net.sf.saxon.s9api.XdmValue; +import net.sf.saxon.s9api.XQueryCompiler; +import net.sf.saxon.s9api.XQueryEvaluator; +import net.sf.saxon.s9api.XQueryExecutable; + +import net.sf.saxon.s9api.SaxonApiException; + +import net.sf.saxon.trans.XPathException; + +import net.sf.saxon.value.Base64BinaryValue; +import net.sf.saxon.value.HexBinaryValue; + +import org.postgresql.pljava.annotation.Function; +import static org.postgresql.pljava.annotation.Function.OnNullInput.CALLED; + +/** + * Class illustrating use of XQuery with Saxon as the + * implementation, using its native "s9api". + *

+ * Supplies alternative, XML Query-based (as the SQL/XML standard dictates) + * implementation of some of SQL/XML, where the implementation in core + * PostgreSQL is limited to the capabilities of XPath (and XPath 1.0, at that). + *

+ * Without the syntatic sugar built into the core PostgreSQL parser, calls to + * a function in this class can look a bit more verbose in SQL, but reflect a + * straightforward rewriting from the standard syntax. For example, suppose + * there is a table {@code catalog_as_xml} with a single row whose {@code x} + * column is a (respectably sized) XML document recording the stuff in + * {@code pg_catalog}. It could be created like this: + *

+ * CREATE TABLE catalog_as_xml(x) AS
+ *   SELECT schema_to_xml('pg_catalog', false, true, '');
+ *
+ *

+ * In the syntax of the SQL/XML standard, here is a query that would return + * an XML element representing the declaration of a function with a specified + * name: + *

+ * SELECT XMLQUERY('/pg_catalog/pg_proc[proname eq $NAME]'
+ *                 PASSING BY VALUE x, 'numeric_avg' AS NAME
+ *                 RETURNING CONTENT EMPTY ON EMPTY)
+ * FROM catalog_as_xml;
+ *
+ *

+ * It binds the 'context item' of the query to {@code x}, and the {@code NAME} + * parameter to the given value, then evaluates the query and returns XML + * "CONTENT" (a tree structure with a document node at the root, but not + * necessarily meeting all the requirements of an XML "DOCUMENT"). It can be + * rewritten as this call to the {@link #xq_ret_content xq_ret_content} method: + *

+ * SELECT javatest.xq_ret_content('/pg_catalog/pg_proc[proname eq $NAME]',
+ *                                passing, nullOnEmpty => false)
+ * FROM catalog_as_xml,
+ * LATERAL (SELECT x AS ".", 'numeric_avg' AS NAME) AS passing;
+ *
+ *

+ * In the rewritten form, the type of value returned is determined by which + * function is called, and the parameters to pass to the query are moved out to + * a separate {@code SELECT} that supplies their values, types, and names (with + * the context item now given the name ".") and is passed by its alias into the + * query function. + *

+ * In the standard, parameters and results (of XML types) can be passed + * {@code BY VALUE} or {@code BY REF}, where the latter means that the same + * nodes will retain their XQuery node identities over calls (note that this is + * a meaning unrelated to what "by value" and "by reference" usually mean in + * PostgreSQL's documentation). PostgreSQL's implementation of the XML type + * provides no way for {@code BY REF} semantics to be implemented, so everything + * happening here happens {@code BY VALUE} implicitly, and does not need to be + * specified. + * @author Chapman Flack + */ +public class S9 +{ + private S9() { } + + /** For verifying clauses "the shall be an XML 1.1 NCName." */ + static final Pattern s_NCName_pattern; + static final Connection s_dbc; + static final Processor s_s9p = new Processor(false); + static final ItemTypeFactory s_itf = new ItemTypeFactory(s_s9p); + + enum XMLBinary { HEX, BASE64 }; + enum Nulls { ABSENT, NIL }; + + static + { + // https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-NameStartChar + String noColonNameStartChar = "A-Z" + "_" + "a-z" + + "\\x{C0}-\\x{D6}" + "\\x{D8}-\\x{F6}" + "\\x{F8}-\\x{2FF}" + + "\\x{370}-\\x{37D}" + "\\x{37F}-\\x{1FFF}"+ "\\x{200C}-\\x{200D}" + + "\\x{2070}-\\x{218F}" + "\\x{2C00}-\\x{2FEF}" + + "\\x{3001}-\\x{D7FF}" + "\\x{F900}-\\x{FDCF}" + + "\\x{FDF0}-\\x{FFFD}" + "\\x{10000}-\\x{EFFFF}"; + s_NCName_pattern = Pattern.compile( + "[" + noColonNameStartChar + "]"+ + "[" + "-.0-9\\x{B7}\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}" + + noColonNameStartChar + "]*+"); + + try + { + s_dbc = DriverManager.getConnection("jdbc:default:connection"); + } + catch ( SQLException e ) + { + throw new ExceptionInInitializerError(e); + } + } + + /** + * A simple example corresponding to {@code XMLQUERY(expression + * PASSING BY VALUE passing RETURNING CONTENT {NULL|EMPTY} ON EMPTY). + * @param expression An XQuery expression. Must not be {@code null} (in the + * SQL standard {@code XMLQUERY} syntax, it is not even allowed to be an + * expression at all, only a string literal). + * @param passing A row value whose columns will be supplied to the query + * as parameters. Columns with names (typically supplied with {@code AS}) + * appear as predeclared external variables with matching names (in no + * namespace) in the query, with types derived from the SQL types of the + * row value's columns. There may be one (and no more than one) + * column with {@code AS "."} which, if present, will be bound as the + * context item. (The name {@code ?column?}, which PostgreSQL uses for an + * otherwise-unnamed column, is also accepted, which will often allow the + * context item to be specified with no {@code AS} at all. Beware, though, + * that PostgreSQL likes to invent column names from any function or type + * name that may appear in the value expression, so this shorthand will not + * always work, while {@code AS "."} will.) JDBC uppercases all column + * names, so any uses of the corresponding variables in the query must have + * the names in upper case. + * @param nullOnEmpty pass {@code true} to get a null return in place of + * an empty sequence, or {@code false} to just get the empty sequence. + */ + @Function( + schema="javatest", + onNullInput=CALLED, + settings="IntervalStyle TO iso_8601" + ) + public static SQLXML xq_ret_content( + String expression, ResultSet passing, boolean nullOnEmpty) + throws SQLException + { + try + { + return _xq_ret_content(expression, passing, nullOnEmpty); + } + catch ( SaxonApiException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } + catch ( XPathException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } + } + + private static SQLXML _xq_ret_content( + String expression, ResultSet passing, boolean nullOnEmpty) + throws SQLException, SaxonApiException, XPathException + { + /* + * The expression itself may not be null (in the standard, it isn't + * even allowed to be dynamic, and can only be a string literal!). + */ + if ( null == expression ) + throw new SQLDataException( + "XMLQUERY expression may not be null", "22004"); + + /* + * Get an XQueryCompiler with the static context properly set up. + */ + Map nameToIdx = new HashMap(); + XQueryCompiler xqc = + createStaticContextWithPassedTypes(passing, nameToIdx); + + XQueryExecutable xqx = xqc.compile(expression); + + /* + * This method implements the RETURNING CONTENT case. + * + * Now for the General Rules. + */ + XQueryEvaluator xqe = xqx.load(); + DocumentBuilder dBuilder = s_s9p.newDocumentBuilder(); + + /* + * Is there or is there not a context item? + */ + if ( ! nameToIdx.containsKey(null) ) + { + /* "... there is no context item in XDC." */ + } + else + { + Object cve = passing.getObject(nameToIdx.remove(null)); + if ( null == cve ) + return null; + XdmValue ci; + if ( cve instanceof SQLXML ) // XXX support SEQUENCE input someday + ci = dBuilder.build(((SQLXML)cve).getSource(null)); + else + ci = xmlCastAsSequence( + cve, XMLBinary.HEX, xqc.getRequiredContextItemType()); + switch ( ci.size() ) + { + case 0: + /* "... there is no context item in XDC." */ + break; + case 1: + xqe.setContextItem(ci.itemAt(0)); + break; + default: + throw new SQLDataException( + "invalid XQuery context item", "2200V"); + } + } + + /* + * For each XQV: + * (the ResultSetMetaData is only needed here in a quick'n'dirty + * workaround of the difficulty of retrieving the already-computed + * static types of the variables. sigh.) + */ + ResultSetMetaData rsmd = passing.getMetaData(); + for ( Map.Entry e : nameToIdx.entrySet() ) + { + String name = e.getKey(); + int i = e.getValue(); + Object v = passing.getObject(i); + XdmValue vv; + if ( null == v ) + vv = XdmEmptySequence.getInstance(); + else if ( v instanceof SQLXML ) // XXX support SEQUENCE someday + vv = dBuilder.build(((SQLXML)v).getSource(null)); + else + { + /* + * The SequenceType that was determined for the variable has + * been set in the (below s9api, underlying) static query + * context, but there's no easy way to retrieve it from there + * (it is of an underlying, non-s9api class). The method to + * determineXQueryFormalType isn't very costly in this + * implementation (as it doesn't bother generating the Schema + * snippets), so for now, quick 'n' dirty, just do it again. + */ + ItemType it = determineXQueryFormalType(rsmd, i).getItemType(); + vv = xmlCastAsSequence(v, XMLBinary.HEX, it); + } + xqe.setExternalVariable(new QName(name), vv); + } + + /* + * For now, punt on whether the is evaluated + * with XML 1.1 or 1.0 lexical rules.... XXX + */ + XdmValue x1 = xqe.evaluate(); + SequenceIterator x1s = x1.getUnderlyingValue().iterate(); + if ( nullOnEmpty ) + { + if ( 0 == ( SequenceIterator.LOOKAHEAD & x1s.getProperties() ) ) + throw new SQLException( + "nullOnEmpty requested and result sequence lacks lookahead", + "XX000"); + if ( ! ((LookaheadIterator)x1s).hasNext() ) + { + x1s.close(); + return null; + } + } + + SQLXML rsx = s_dbc.createSQLXML(); + QueryResult.serializeSequence( + x1s, s_s9p.getUnderlyingConfiguration(), rsx.setResult(null), + new Properties()); + return rsx; + } + + /** + * Return a s9api {@link XQueryCompiler XQueryCompiler} with static context + * preconfigured as the Syntax Rules dictate. + * @param pt The single-row ResultSet representing the passed parameters + * and context item, if any. + * @param nameToIndex A Map, supplied empty, that on return will map + * variable names for the dynamic context to column indices in {@code pt}. + * If a context item was supplied, its index will be entered in the map + * with the null key. + */ + private static XQueryCompiler createStaticContextWithPassedTypes( + ResultSet pt, Map nameToIndex) + throws SQLException, XPathException + { + ResultSetMetaData rsmd = pt.getMetaData(); + int nParams = rsmd.getColumnCount(); + + /* + * Apply syntax rules to the names. + */ + Matcher ncName = s_NCName_pattern.matcher(""); + int contextItemIdx = 0; + + for ( int i = 1; i <= nParams; ++i ) + { + String label = rsmd.getColumnLabel(i); + if ( "?COLUMN?".equals(label) || ".".equals(label) ) + { + if ( 0 != contextItemIdx ) + throw new SQLSyntaxErrorException( + "Context item supplied more than once (at " + + contextItemIdx + " and " + i + ')', "42712"); + contextItemIdx = i; + continue; + } + if ( ! ncName.reset(label).matches() ) + throw new SQLSyntaxErrorException( + "Not an XML NCname: \"" + label + '"', "42602"); + Integer was = nameToIndex.put(label, i); + if ( null != was ) + throw new SQLSyntaxErrorException( + "Name \"" + label + "\" duplicated at positions " + was + + " and " + i, "42712"); + } + + /* + * Apply syntax rules to the parameter types. This includes (it's weird + * what SQL standards call 'syntax') adding their names and static types + * to the static context. + */ + XQueryCompiler xqc = s_s9p.newXQueryCompiler(); + xqc.declareNamespace( + "sqlxml", "http://standards.iso.org/iso9075/2003/sqlxml"); + // https://sourceforge.net/p/saxon/mailman/message/20318550/ : + xqc.declareNamespace("xdt", "http://www.w3.org/2001/XMLSchema"); + + /* + * This business of predeclaring global external named variables + * is not an s9api-level advertised ability in Saxon, hence the + * various getUnderlying.../getStructured... methods here to access + * the things that make it happen. + */ + StaticQueryContext sqc = xqc.getUnderlyingStaticContext(); + + for ( Map.Entry e : nameToIndex.entrySet() ) + { + String name = e.getKey(); + int i = e.getValue(); + int ct = rsmd.getColumnType(i); + assertCanCastAsXmlSequence(ct, name); + SequenceType st = determineXQueryFormalType(rsmd, i); + sqc.declareGlobalVariable( + new QName(name).getStructuredQName(), + st.getUnderlyingSequenceType(), null, true); + } + + /* + * Apply syntax rules to the context item, if any. + */ + if ( 0 != contextItemIdx ) + { + nameToIndex.put(null, contextItemIdx); + int ct = rsmd.getColumnType(contextItemIdx); + assertCanCastAsXmlSequence(ct, "(context item)"); + ItemType it = determineXQueryFormalTypeContextItem( + rsmd, contextItemIdx); + xqc.setRequiredContextItemType(it); + } + + return xqc; + } + + /** + * Check that something's type is "convertible to XML(SEQUENCE) + * according to the Syntax Rules of ... ." + * That turns out not to be a very high bar; not much is excluded + * by those rules except collection, row, structured, or + * reference typed s. + * @param jdbcType The {@link Types JDBC type} to be checked. + * @param what A string to include in the exception message if the + * check fails. + * @throws SQLException if {@code jdbcType} is one of the prohibited types. + */ + private static void assertCanCastAsXmlSequence(int jdbcType, String what) + throws SQLException + { + if ( Types.ARRAY == jdbcType || Types.STRUCT == jdbcType + || Types.REF == jdbcType ) + throw new SQLSyntaxErrorException( + "The type of \"" + what + "\" is not suitable for " + + "XMLCAST to XML(SEQUENCE).", "42804"); + } + + private static SequenceType determineXQueryFormalType( + ResultSetMetaData rsmd, int columnIndex) + throws SQLException + { + return determineXQueryFormalType(rsmd, columnIndex, false); + } + + private static ItemType determineXQueryFormalTypeContextItem( + ResultSetMetaData rsmd, int columnIndex) + throws SQLException + { + SequenceType st = determineXQueryFormalType(rsmd, columnIndex, true); + assert OccurrenceIndicator.ONE == st.getOccurrenceIndicator(); + return st.getItemType(); + } + + private static SequenceType determineXQueryFormalType( + ResultSetMetaData rsmd, int columnIndex, boolean forContextItem) + throws SQLException + { + int sd = rsmd.getColumnType(columnIndex); + OccurrenceIndicator suffix; + /* + * The SQL/XML standard uses a formal type notation straight out of + * the XQuery 1.0 and XPath 2.0 Formal Semantics document, and that is + * strictly more fine-grained and expressive than anything you can + * actually say in the form of XQuery SequenceTypes. This method will + * simply return the nearest approximation in the form of a sequence + * type; some of the standard's distinct formal type notations will + * collapse into the same SequenceType. + * That also means the various cases laid out in the standard will, + * here, all simply assign some ItemType to 'it', and therefore the + * tacking on of the occurrence suffix can be factored out for the + * very end. + */ + ItemType it; + + if ( forContextItem ) + suffix = OccurrenceIndicator.ONE; + // else if sd is XML(SEQUENCE) - we don't have this type yet + // suffix = OccurrenceIndicator.ZERO_OR_MORE; + /* + * Go through the motions of checking isNullable, though PL/Java's JDBC + * currently hardcodes columnNullableUnknown. Maybe someday it won't. + */ + else if ( columnNoNulls == rsmd.isNullable(columnIndex) ) + suffix = OccurrenceIndicator.ONE; + else + suffix = OccurrenceIndicator.ZERO_OR_ONE; + + // Define ET... for {DOCUMENT|CONTENT}(XMLSCHEMA) case ... not supported + + // if SD is XML(DOCUMENT(UNTYPED)) - not currently tracked, can't tell + // it = s_itf.getDocumentTest(item type for xdt:untyped); + // else if SD is XML(DOCUMENT(ANY)) - not currently tracked, can't tell + // it = s_itf.getDocumentTest(item type for xs:anyType); + // else if SD is XML(DOCUMENT(XMLSCHEMA)) - unsupported and can't tell + // it = s_itf.getDocumentTest(the ET... we didn't define earlier) + // else if SD is XML(CONTENT(UNTYPED)) - which we're not tracking ... + // at s9api granularity, there's no test for this that's not same as: + // else if SD is XML(CONTENT(ANY)) - which we must assume for ANY XML + if ( Types.SQLXML == sd ) + it = s_itf.getNodeKindTest(DOCUMENT); + // else if SD is XML(CONTENT(XMLSCHEMA)) - we don't track and can't tell + // at s9api granularity, there's no test that means this anyway. + // else if SD is XML(SEQUENCE) - we really should have this type, but no + // it = it.ANY_ITEM + else // it ain't XML, it's some SQL type + { + ItemType xmlt = mapSQLDataTypeToXMLSchemaDataType( + sd, rsmd, columnIndex, XMLBinary.HEX, Nulls.ABSENT); + // ItemType pt = xmlt.getUnderlyingItemType().getPrimitiveType() + // .somehowGetFromUnderlyingPTBackToS9apiPT() - ugh, the hard part + /* + * The intention here is to replace any derived type with the + * primitive type it is based on, *except* for three types that are + * technically derived: integer (from decimal), yearMonthDuration + * and dayTimeDuration (from duration). Those are not replaced, so + * they stand, as if they were honorary primitive types. + * + * For now, it's simplified greatly by mapSQLDataType... skipping + * the construction of a whole derived XML Schema snippet, and just + * returning the type we want anyway. Also, no need to dive under + * the s9api layer to try to make getPrimitiveType work. + */ + it = xmlt; + } + + SequenceType xftn = SequenceType.makeSequenceType(it, suffix); + return xftn; + } + + private static ItemType mapSQLDataTypeToXMLSchemaDataType( + int sd, ResultSetMetaData rsmd, int columnIndex, + XMLBinary xmlbinary, Nulls nulls) + throws SQLException + { + /* + * Nearly all of the fussing about specified in the standard + * for this method is to create XML Schema derived types that + * accurately reflect the typmod information for the SQL type + * in question. Then, in determineXQueryFormalType (the only + * client of this method so far!), all of that is thrown away + * and our painstakingly specified derived type is replaced with + * the primitive type we based it on. That simplifies a lot. :) + * For now, forget the derived XML Schema declarations, and just + * return the primitive types thwy would be based on. + * + * The need for the nulls parameter vanishes if no XML Schema snippets + * are to be generated. + * + * If the full XML Schema snippet generation ever proves to be + * needed, one hacky way to get it would be with a SELECT + * query_to_xmlschema('SELECT null::type-in-question', false, false, + * '') where the same derivations are already implemented (though it + * produces some different results; that work may have been done from + * an earlier version of the standard). + */ + switch ( sd ) + { + case Types.CHAR: + case Types.VARCHAR: + case Types.CLOB: + return ItemType.STRING; + + case Types.BINARY: + case Types.VARBINARY: + case Types.BLOB: + return XMLBinary.HEX == xmlbinary ? + ItemType.HEX_BINARY : ItemType.BASE64_BINARY; + + case Types.NUMERIC: + case Types.DECIMAL: + /* + * Go through the motions to get the scale and do this right, + * though PL/Java's getScale currently hardcodes a -1 return. + * Maybe someday it won't. + */ + int scale = rsmd.getScale(columnIndex); + return 0 == scale ? ItemType.INTEGER : ItemType.DECIMAL; + + case Types.INTEGER: + return ItemType.INT; + case Types.SMALLINT: + return ItemType.SHORT; + case Types.BIGINT: + return ItemType.LONG; + + case Types.FLOAT: + assert false; // PG should always report either REAL or DOUBLE + case Types.REAL: + return ItemType.FLOAT; // could check P, MINEXP, MAXEXP here. + case Types.DOUBLE: + return ItemType.DOUBLE; + + case Types.BOOLEAN: + return ItemType.BOOLEAN; + + case Types.DATE: + return ItemType.DATE; + + case Types.TIME: + return ItemType.TIME; + + case Types.TIME_WITH_TIMEZONE: + return ItemType.TIME; // restrictive facet would make sense here + + case Types.TIMESTAMP: + return ItemType.DATE_TIME; + + case Types.TIMESTAMP_WITH_TIMEZONE: + return ItemType.DATE_TIME_STAMP; // xsd 1.1 equivalent of facet! + + // There's no JDBC Types.INTERVAL; handle it after switch + + // Good luck finding out from JDBC if it's a domain + + // PG doesn't have DISTINCT types per se + + // PL/Java's JDBC doesn't support PostgreSQL's arrays as ARRAY + + // PG doesn't seem to have multisets (JDBC doesn't grok them either) + + // Types.SQLXML we could recognize, but for determineFormalTypes it has + // been handled already, and it's not yet clear what would be + // appropriate to return (short of the specified XMLSchema snippet), + // probably just document. + + // So punt all these for now; what hasn't been handled in this switch + // can be handled specially after the switch falls through, and what + // isn't, isn't supported just now. + } + + String typeName = rsmd.getColumnTypeName(columnIndex); + if ( "interval".equals(typeName) ) + { + /* + * XXX This isn't right yet; it needs to be refined to a + * YEAR_MONTH_DURATION or a DAY_TIME_DURATION in the appropriate + * cases, and for that it needs access to the typmod information + * for the type, which getColumnTypeName doesn't now provide. + */ + return ItemType.DURATION; + } + + throw new SQLNonTransientException(String.format( + "Mapping SQL type \"%s\" to XML type not supported", typeName), + "0N000"); + } + + /** + * Implement that portion of the {@code } specification where + * the target data type is sequence, and (for now, anyway) the source is + * not an XML type; the only caller, so far, handles that case separately. + * @param v The SQL value to be cast (in the form of an Object from JDBC). + * @param enc Whether binary values should be encoded in hex or base 64. + * @param xst The formal static XS type derived from the SQL type of v. + * @return An {@code XdmValue}, {@code null} if {@code v} is null. + */ + private static XdmValue xmlCastAsSequence( + Object v, XMLBinary enc, ItemType xst) + throws SQLException + { + if ( null == v ) + return null; + /* + * What happens next in the standard is one of the most breathtaking + * feats of obscurantism in the whole document. It begins, plausibly + * enough, by using mapValuesOfSQLTypesToValuesOfXSTypes to produce + * the lexical form of the XS type (but with XML metacharacters escaped, + * if it's a string type). Then: + * 1. That lexical form is to be fed to an XML parser, producing an + * XQuery document node that NEVER can be a well-formed document (it + * is expected to satisfy document { text ? } where the text node is + * just the lexical value form we started with, now with the escaped + * metacharacters unescaped again as a consequence of parsing). For + * some source types, mapValuesOfSQLTypesToValuesOfXSTypes can + * produce a string that parses to XML with element content: row + * types, arrays, multisets, XML. Clearly, those cases can't satisfy + * the formal type assumed here, and they are cases this routine + * won't be expected to handle: XML handled separately by the caller, + * arrays/structs/etc. being ruled out by assertCanCastAsXmlSequence. + * 2. That document node is made the $TEMP parameter of an XML Query, + * '$TEMP cast as XSBTN' (where XSBTN is a QName for the result type + * chosen according to the rules) and the sequence resulting from + * that query is the result of the cast. + * + * Step (1) can only succeed if the XML parser doesn't insist on well- + * formed documents, as the stock JRE parser emphatically does. And the + * ultimate effect of that whole dance is that the cast in (2) casts a + * document node to the target type, which means the document node gets + * atomized, which, for a document node, means everything is thrown away + * save the concatenated values of its descendant text nodes (or node, + * in this case; haven't we seen that value somewhere before?), assigned + * the type xs:untypedAtomic, and then that's operated on by the cast. + * + * Because this implementation's in PL/Java, the value v received here + * has already been mapped from an SQL type to a Java type according to + * JDBC's rules as PL/Java implements them, so there's one degree of + * removal from the specified algorithm anyway. And the s9api + * XdmAtomicValue already has constructors from several of the expected + * Java types, as well as one taking a lexical form and explicit type. + * Beause this is /example/ code, rather than slavishly implementing the + * specified algorithm, it will assume that that is either roughly or + * exactly equivalent to what these s9api constructors in fact do, and + * just use them; conformance-testing code could then check for exact + * equivalence if there's enough interest to write it. + * + * So, we will NOT start with this: + * + * String xmlv = mapValuesOfSQLTypesToValuesOfXSTypes( + * v, enc, Nulls.ABSENT, true); + * + * Instead, we'll derive this type first ... + */ + ItemType xsbt; + // year-month interval type => xsbt = YEAR_MONTH_DURATION + // day-time interval type => xsbt = DAY_TIME_DURATION + xsbt = xst; // we have a winner! + // xs non-built-in atomic type => xsbt = getPrimitiveType(ugh). + + /* + * ... and then use this method instead: + */ + try + { + return mapJDBCofSQLvalueToXdmAtomicValue(v, enc, xsbt); + } + catch ( SaxonApiException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } + } + + private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( + Object dv, XMLBinary encoding, ItemType xst) + throws SQLException, SaxonApiException + { + if ( ItemType.STRING.equals(xst) ) + return new XdmAtomicValue((String)dv); + + if ( ItemType.HEX_BINARY.equals(xst) ) + return makeAtomicValue(new HexBinaryValue((byte[])dv)); + if ( ItemType.BASE64_BINARY.equals(xst) ) + return makeAtomicValue(new Base64BinaryValue((byte[])dv)); + + if ( ItemType.INTEGER.equals(xst) ) + return new XdmAtomicValue(((BigInteger)dv).toString(), xst); + if ( ItemType.DECIMAL.equals(xst) ) + return new XdmAtomicValue((BigDecimal)dv); + if ( ItemType.INT.equals(xst) ) + return new XdmAtomicValue((Integer)dv); + if ( ItemType.SHORT.equals(xst) ) + return new XdmAtomicValue((Short)dv); + if ( ItemType.LONG.equals(xst) ) + return new XdmAtomicValue((Long)dv); + if ( ItemType.FLOAT.equals(xst) ) + return new XdmAtomicValue((Float)dv); + if ( ItemType.DOUBLE.equals(xst) ) + return new XdmAtomicValue((Double)dv); + + if ( ItemType.BOOLEAN.equals(xst) ) + return new XdmAtomicValue((Boolean)dv); + + if ( ItemType.DATE.equals(xst) ) + return new XdmAtomicValue(((Date)dv).toString(), xst); + + if ( ItemType.TIME.equals(xst) ) // XXX with/without tz matters here... + return new XdmAtomicValue(((Time)dv).toString(), xst); + + if ( ItemType.DATE_TIME.equals(xst) ) + return new XdmAtomicValue( // trust me, there's only one space + ((Timestamp)dv).toString().replace(' ', 'T'), xst); + + if ( ItemType.DATE_TIME_STAMP.equals(xst) ) // XXX here too... + return new XdmAtomicValue( + ((Timestamp)dv).toString().replace(' ', 'T'), xst); + + if ( ItemType.DURATION.equals(xst) ) + return new XdmAtomicValue((String)dv, xst); + + throw new SQLNonTransientException(String.format( + "Mapping SQL value to XML type \"%s\" not supported", xst), + "0N000"); + } +} From d97261f20979fbee6c26a2cbe4ad34394fad2667 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 16 Jun 2018 23:48:53 -0400 Subject: [PATCH 0105/1087] Tidy, improve internal interfaces. Factor the main works of XMLQUERY into an xmlquery_internal method that is easier to call, and move the result serialization as CONTENT back out into xq_ret_content (where the form of result is enforced). Also create a Binding class to abstract the bindings of context item and parameters away from ResultSet specifics; BindingsFromResultSet is now just one implementation. There turns out to be a handy isValidNCName() provided in Saxon. --- .../pljava/example/annotation/S9.java | 454 ++++++++++++------ 1 file changed, 313 insertions(+), 141 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java index 23153fa6..28521ba4 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java @@ -32,14 +32,20 @@ import java.sql.SQLNonTransientException; import java.sql.SQLSyntaxErrorException; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + +import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI; import javax.xml.transform.stream.StreamSource; +import static net.sf.saxon.om.NameChecker.isValidNCName; import net.sf.saxon.om.SequenceIterator; import net.sf.saxon.query.QueryResult; @@ -71,6 +77,7 @@ import net.sf.saxon.value.HexBinaryValue; import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.annotation.Function.OnNullInput.CALLED; /** @@ -134,8 +141,6 @@ public class S9 { private S9() { } - /** For verifying clauses "the shall be an XML 1.1 NCName." */ - static final Pattern s_NCName_pattern; static final Connection s_dbc; static final Processor s_s9p = new Processor(false); static final ItemTypeFactory s_itf = new ItemTypeFactory(s_s9p); @@ -145,18 +150,6 @@ enum Nulls { ABSENT, NIL }; static { - // https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-NameStartChar - String noColonNameStartChar = "A-Z" + "_" + "a-z" + - "\\x{C0}-\\x{D6}" + "\\x{D8}-\\x{F6}" + "\\x{F8}-\\x{2FF}" + - "\\x{370}-\\x{37D}" + "\\x{37F}-\\x{1FFF}"+ "\\x{200C}-\\x{200D}" + - "\\x{2070}-\\x{218F}" + "\\x{2C00}-\\x{2FEF}" + - "\\x{3001}-\\x{D7FF}" + "\\x{F900}-\\x{FDCF}" + - "\\x{FDF0}-\\x{FFFD}" + "\\x{10000}-\\x{EFFFF}"; - s_NCName_pattern = Pattern.compile( - "[" + noColonNameStartChar + "]"+ - "[" + "-.0-9\\x{B7}\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}" + - noColonNameStartChar + "]*+"); - try { s_dbc = DriverManager.getConnection("jdbc:default:connection"); @@ -173,6 +166,8 @@ enum Nulls { ABSENT, NIL }; * @param expression An XQuery expression. Must not be {@code null} (in the * SQL standard {@code XMLQUERY} syntax, it is not even allowed to be an * expression at all, only a string literal). + * @param nullOnEmpty pass {@code true} to get a null return in place of + * an empty sequence, or {@code false} to just get the empty sequence. * @param passing A row value whose columns will be supplied to the query * as parameters. Columns with names (typically supplied with {@code AS}) * appear as predeclared external variables with matching names (in no @@ -187,8 +182,6 @@ enum Nulls { ABSENT, NIL }; * always work, while {@code AS "."} will.) JDBC uppercases all column * names, so any uses of the corresponding variables in the query must have * the names in upper case. - * @param nullOnEmpty pass {@code true} to get a null return in place of - * an empty sequence, or {@code false} to just get the empty sequence. */ @Function( schema="javatest", @@ -196,12 +189,38 @@ enum Nulls { ABSENT, NIL }; settings="IntervalStyle TO iso_8601" ) public static SQLXML xq_ret_content( - String expression, ResultSet passing, boolean nullOnEmpty) + String expression, Boolean nullOnEmpty, + @SQLType(defaultValue={}) ResultSet passing) throws SQLException { + if ( null == nullOnEmpty ) + throw new SQLDataException( + "XMLQUERY nullOnEmpty may not be null", "22004"); + try { - return _xq_ret_content(expression, passing, nullOnEmpty); + XdmValue x1 = xmlquery_internal( + expression, new BindingsFromResultSet(passing)); + + SequenceIterator x1s = x1.getUnderlyingValue().iterate(); + if ( nullOnEmpty.booleanValue() ) + { + if ( 0 == ( SequenceIterator.LOOKAHEAD & x1s.getProperties() ) ) + throw new SQLException( + "nullOnEmpty requested and result sequence lacks lookahead", + "XX000"); + if ( ! ((LookaheadIterator)x1s).hasNext() ) + { + x1s.close(); + return null; + } + } + + SQLXML rsx = s_dbc.createSQLXML(); + QueryResult.serializeSequence( + x1s, s_s9p.getUnderlyingConfiguration(), rsx.setResult(null), + new Properties()); + return rsx; } catch ( SaxonApiException e ) { @@ -213,8 +232,8 @@ public static SQLXML xq_ret_content( } } - private static SQLXML _xq_ret_content( - String expression, ResultSet passing, boolean nullOnEmpty) + private static XdmValue xmlquery_internal( + String expression, Binding.Assemblage passing) throws SQLException, SaxonApiException, XPathException { /* @@ -228,9 +247,7 @@ private static SQLXML _xq_ret_content( /* * Get an XQueryCompiler with the static context properly set up. */ - Map nameToIdx = new HashMap(); - XQueryCompiler xqc = - createStaticContextWithPassedTypes(passing, nameToIdx); + XQueryCompiler xqc = createStaticContextWithPassedTypes(passing); XQueryExecutable xqx = xqc.compile(expression); @@ -245,13 +262,13 @@ private static SQLXML _xq_ret_content( /* * Is there or is there not a context item? */ - if ( ! nameToIdx.containsKey(null) ) + if ( null == passing.contextItem() ) { /* "... there is no context item in XDC." */ } else { - Object cve = passing.getObject(nameToIdx.remove(null)); + Object cve = passing.contextItem().valueJDBC(); if ( null == cve ) return null; XdmValue ci; @@ -276,35 +293,19 @@ private static SQLXML _xq_ret_content( /* * For each XQV: - * (the ResultSetMetaData is only needed here in a quick'n'dirty - * workaround of the difficulty of retrieving the already-computed - * static types of the variables. sigh.) */ - ResultSetMetaData rsmd = passing.getMetaData(); - for ( Map.Entry e : nameToIdx.entrySet() ) + for ( Binding.Parameter p : passing ) { - String name = e.getKey(); - int i = e.getValue(); - Object v = passing.getObject(i); + String name = p.name(); + Object v = p.valueJDBC(); XdmValue vv; if ( null == v ) vv = XdmEmptySequence.getInstance(); else if ( v instanceof SQLXML ) // XXX support SEQUENCE someday vv = dBuilder.build(((SQLXML)v).getSource(null)); else - { - /* - * The SequenceType that was determined for the variable has - * been set in the (below s9api, underlying) static query - * context, but there's no easy way to retrieve it from there - * (it is of an underlying, non-s9api class). The method to - * determineXQueryFormalType isn't very costly in this - * implementation (as it doesn't bother generating the Schema - * snippets), so for now, quick 'n' dirty, just do it again. - */ - ItemType it = determineXQueryFormalType(rsmd, i).getItemType(); - vv = xmlCastAsSequence(v, XMLBinary.HEX, it); - } + vv = xmlCastAsSequence( + v, XMLBinary.HEX, p.typeXS().getItemType()); xqe.setExternalVariable(new QName(name), vv); } @@ -312,26 +313,7 @@ else if ( v instanceof SQLXML ) // XXX support SEQUENCE someday * For now, punt on whether the is evaluated * with XML 1.1 or 1.0 lexical rules.... XXX */ - XdmValue x1 = xqe.evaluate(); - SequenceIterator x1s = x1.getUnderlyingValue().iterate(); - if ( nullOnEmpty ) - { - if ( 0 == ( SequenceIterator.LOOKAHEAD & x1s.getProperties() ) ) - throw new SQLException( - "nullOnEmpty requested and result sequence lacks lookahead", - "XX000"); - if ( ! ((LookaheadIterator)x1s).hasNext() ) - { - x1s.close(); - return null; - } - } - - SQLXML rsx = s_dbc.createSQLXML(); - QueryResult.serializeSequence( - x1s, s_s9p.getUnderlyingConfiguration(), rsx.setResult(null), - new Properties()); - return rsx; + return xqe.evaluate(); } /** @@ -345,50 +327,14 @@ else if ( v instanceof SQLXML ) // XXX support SEQUENCE someday * with the null key. */ private static XQueryCompiler createStaticContextWithPassedTypes( - ResultSet pt, Map nameToIndex) + Binding.Assemblage pt) throws SQLException, XPathException { - ResultSetMetaData rsmd = pt.getMetaData(); - int nParams = rsmd.getColumnCount(); - - /* - * Apply syntax rules to the names. - */ - Matcher ncName = s_NCName_pattern.matcher(""); - int contextItemIdx = 0; - - for ( int i = 1; i <= nParams; ++i ) - { - String label = rsmd.getColumnLabel(i); - if ( "?COLUMN?".equals(label) || ".".equals(label) ) - { - if ( 0 != contextItemIdx ) - throw new SQLSyntaxErrorException( - "Context item supplied more than once (at " + - contextItemIdx + " and " + i + ')', "42712"); - contextItemIdx = i; - continue; - } - if ( ! ncName.reset(label).matches() ) - throw new SQLSyntaxErrorException( - "Not an XML NCname: \"" + label + '"', "42602"); - Integer was = nameToIndex.put(label, i); - if ( null != was ) - throw new SQLSyntaxErrorException( - "Name \"" + label + "\" duplicated at positions " + was + - " and " + i, "42712"); - } - - /* - * Apply syntax rules to the parameter types. This includes (it's weird - * what SQL standards call 'syntax') adding their names and static types - * to the static context. - */ XQueryCompiler xqc = s_s9p.newXQueryCompiler(); xqc.declareNamespace( "sqlxml", "http://standards.iso.org/iso9075/2003/sqlxml"); // https://sourceforge.net/p/saxon/mailman/message/20318550/ : - xqc.declareNamespace("xdt", "http://www.w3.org/2001/XMLSchema"); + xqc.declareNamespace("xdt", W3C_XML_SCHEMA_NS_URI); /* * This business of predeclaring global external named variables @@ -398,13 +344,12 @@ private static XQueryCompiler createStaticContextWithPassedTypes( */ StaticQueryContext sqc = xqc.getUnderlyingStaticContext(); - for ( Map.Entry e : nameToIndex.entrySet() ) + for ( Binding.Parameter p : pt ) { - String name = e.getKey(); - int i = e.getValue(); - int ct = rsmd.getColumnType(i); + String name = p.name(); + int ct = p.typeJDBC(); assertCanCastAsXmlSequence(ct, name); - SequenceType st = determineXQueryFormalType(rsmd, i); + SequenceType st = p.typeXS(); sqc.declareGlobalVariable( new QName(name).getStructuredQName(), st.getUnderlyingSequenceType(), null, true); @@ -413,13 +358,12 @@ private static XQueryCompiler createStaticContextWithPassedTypes( /* * Apply syntax rules to the context item, if any. */ - if ( 0 != contextItemIdx ) + Binding.ContextItem ci = pt.contextItem(); + if ( null != ci ) { - nameToIndex.put(null, contextItemIdx); - int ct = rsmd.getColumnType(contextItemIdx); + int ct = ci.typeJDBC(); assertCanCastAsXmlSequence(ct, "(context item)"); - ItemType it = determineXQueryFormalTypeContextItem( - rsmd, contextItemIdx); + ItemType it = ci.typeXS(); xqc.setRequiredContextItemType(it); } @@ -448,26 +392,10 @@ private static void assertCanCastAsXmlSequence(int jdbcType, String what) } private static SequenceType determineXQueryFormalType( - ResultSetMetaData rsmd, int columnIndex) - throws SQLException - { - return determineXQueryFormalType(rsmd, columnIndex, false); - } - - private static ItemType determineXQueryFormalTypeContextItem( - ResultSetMetaData rsmd, int columnIndex) + Binding b, boolean forContextItem) throws SQLException { - SequenceType st = determineXQueryFormalType(rsmd, columnIndex, true); - assert OccurrenceIndicator.ONE == st.getOccurrenceIndicator(); - return st.getItemType(); - } - - private static SequenceType determineXQueryFormalType( - ResultSetMetaData rsmd, int columnIndex, boolean forContextItem) - throws SQLException - { - int sd = rsmd.getColumnType(columnIndex); + int sd = b.typeJDBC(); OccurrenceIndicator suffix; /* * The SQL/XML standard uses a formal type notation straight out of @@ -492,7 +420,7 @@ private static SequenceType determineXQueryFormalType( * Go through the motions of checking isNullable, though PL/Java's JDBC * currently hardcodes columnNullableUnknown. Maybe someday it won't. */ - else if ( columnNoNulls == rsmd.isNullable(columnIndex) ) + else if ( b.knownNonNull() ) suffix = OccurrenceIndicator.ONE; else suffix = OccurrenceIndicator.ZERO_OR_ONE; @@ -517,7 +445,7 @@ else if ( columnNoNulls == rsmd.isNullable(columnIndex) ) else // it ain't XML, it's some SQL type { ItemType xmlt = mapSQLDataTypeToXMLSchemaDataType( - sd, rsmd, columnIndex, XMLBinary.HEX, Nulls.ABSENT); + b, XMLBinary.HEX, Nulls.ABSENT); // ItemType pt = xmlt.getUnderlyingItemType().getPrimitiveType() // .somehowGetFromUnderlyingPTBackToS9apiPT() - ugh, the hard part /* @@ -540,8 +468,7 @@ else if ( columnNoNulls == rsmd.isNullable(columnIndex) ) } private static ItemType mapSQLDataTypeToXMLSchemaDataType( - int sd, ResultSetMetaData rsmd, int columnIndex, - XMLBinary xmlbinary, Nulls nulls) + Binding b, XMLBinary xmlbinary, Nulls nulls) throws SQLException { /* @@ -565,7 +492,7 @@ private static ItemType mapSQLDataTypeToXMLSchemaDataType( * produces some different results; that work may have been done from * an earlier version of the standard). */ - switch ( sd ) + switch ( b.typeJDBC() ) { case Types.CHAR: case Types.VARCHAR: @@ -585,7 +512,7 @@ private static ItemType mapSQLDataTypeToXMLSchemaDataType( * though PL/Java's getScale currently hardcodes a -1 return. * Maybe someday it won't. */ - int scale = rsmd.getScale(columnIndex); + int scale = b.scale(); return 0 == scale ? ItemType.INTEGER : ItemType.DECIMAL; case Types.INTEGER: @@ -640,7 +567,7 @@ private static ItemType mapSQLDataTypeToXMLSchemaDataType( // isn't, isn't supported just now. } - String typeName = rsmd.getColumnTypeName(columnIndex); + String typeName = b.typePG(); if ( "interval".equals(typeName) ) { /* @@ -792,4 +719,249 @@ private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( "Mapping SQL value to XML type \"%s\" not supported", xst), "0N000"); } + + static class Binding + { + String typePG() throws SQLException + { + if ( null != m_typePG ) + return m_typePG; + return m_typePG = implTypePG(); + } + + int typeJDBC() throws SQLException + { + if ( null != m_typeJDBC ) + return m_typeJDBC; + return m_typeJDBC = implTypeJDBC(); + } + + Object valueJDBC() throws SQLException + { + if ( m_valueJDBCValid ) + return m_valueJDBC; + return setValueJDBC(implValueJDBC()); + } + + boolean knownNonNull() throws SQLException + { + if ( null != m_knownNonNull ) + return m_knownNonNull; + return m_knownNonNull = implKnownNonNull(); + } + + int scale() throws SQLException + { + if ( null != m_scale ) + return m_scale; + return m_scale = implScale(); + } + + static class ContextItem extends Binding + { + ItemType typeXS() throws SQLException + { + if ( null != m_typeXS ) + return m_typeXS; + SequenceType st = implTypeXS(true); + assert OccurrenceIndicator.ONE == st.getOccurrenceIndicator(); + return m_typeXS = st.getItemType(); + } + + protected ItemType m_typeXS; + } + + static class Parameter extends Binding + { + String name() + { + return m_name; + } + + SequenceType typeXS() throws SQLException + { + if ( null != m_typeXS ) + return m_typeXS; + return m_typeXS = implTypeXS(false); + } + + protected SequenceType m_typeXS; + + private final String m_name; + + protected Parameter(String name) throws SQLException + { + if ( ! isValidNCName(name) ) + throw new SQLSyntaxErrorException( + "Not an XML NCname: \"" + name + '"', "42602"); + m_name = name; + } + } + + protected String m_typePG; + protected Integer m_typeJDBC; + protected Boolean m_knownNonNull; + protected Integer m_scale; + private Object m_valueJDBC; + private boolean m_valueJDBCValid; + protected Object setValueJDBC(Object v) + { + m_valueJDBCValid = true; + return m_valueJDBC = v; + } + + protected String implTypePG() throws SQLException + { + throw new UnsupportedOperationException( + "typePG() on synthetic binding"); + } + + protected int implTypeJDBC() throws SQLException + { + throw new UnsupportedOperationException( + "typeJDBC() on synthetic binding"); + } + + protected boolean implKnownNonNull() throws SQLException + { + throw new UnsupportedOperationException( + "knownNonNull() on synthetic binding"); + } + + protected int implScale() throws SQLException + { + throw new UnsupportedOperationException( + "scale() on synthetic binding"); + } + + protected Object implValueJDBC() throws SQLException + { + throw new UnsupportedOperationException( + "valueJDBC() on synthetic binding"); + } + + protected SequenceType implTypeXS(boolean forContextItem) + throws SQLException + { + return determineXQueryFormalType(this, forContextItem); + } + + static class Assemblage implements Iterable + { + ContextItem contextItem() { return m_contextItem; } + + @Override + public Iterator iterator() + { + return m_params.iterator(); + } + + protected ContextItem m_contextItem; + protected Collection m_params = Collections.emptyList(); + } + } + + static class BindingsFromResultSet extends Binding.Assemblage + { + BindingsFromResultSet(ResultSet rs) throws SQLException + { + m_resultSet = rs; + m_rsmd = rs.getMetaData(); + + int nParams = m_rsmd.getColumnCount(); + ContextItem contextItem = null; + Map n2b = + new HashMap(); + + for ( int i = 1; i <= nParams; ++i ) + { + String label = m_rsmd.getColumnLabel(i); + if ( "?COLUMN?".equals(label) || ".".equals(label) ) + { + if ( null != contextItem ) + throw new SQLSyntaxErrorException( + "Context item supplied more than once (at " + + contextItem.m_idx + " and " + i + ')', "42712"); + contextItem = new ContextItem(i); + continue; + } + + Parameter was = + (Parameter)n2b.put(label, new Parameter(label, i)); + if ( null != was ) + throw new SQLSyntaxErrorException( + "Name \"" + label + "\" duplicated at positions " + + was.m_idx + " and " + i, "42712"); + } + + m_contextItem = contextItem; + m_params = n2b.values(); + } + + private ResultSet m_resultSet; + private ResultSetMetaData m_rsmd; + + class ContextItem extends Binding.ContextItem + { + final int m_idx; + + ContextItem(int index) { m_idx = index; } + + protected String implTypePG() throws SQLException + { + return m_rsmd.getColumnTypeName(m_idx); + } + + protected int implTypeJDBC() throws SQLException + { + return m_rsmd.getColumnType(m_idx); + } + + protected int implScale() throws SQLException + { + return m_rsmd.getScale(m_idx); + } + + protected Object implValueJDBC() throws SQLException + { + return m_resultSet.getObject(m_idx); + } + } + + class Parameter extends Binding.Parameter + { + final int m_idx; + + Parameter(String name, int index) throws SQLException + { + super(name); + m_idx = index; + } + + protected String implTypePG() throws SQLException + { + return m_rsmd.getColumnTypeName(m_idx); + } + + protected int implTypeJDBC() throws SQLException + { + return m_rsmd.getColumnType(m_idx); + } + + protected boolean implKnownNonNull() throws SQLException + { + return columnNoNulls == m_rsmd.isNullable(m_idx); + } + + protected int implScale() throws SQLException + { + return m_rsmd.getScale(m_idx); + } + + protected Object implValueJDBC() throws SQLException + { + return m_resultSet.getObject(m_idx); + } + } + } } From 82eb4954e4e2e070540e1c268119ac3f2876e94d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 16 Jun 2018 23:55:25 -0400 Subject: [PATCH 0106/1087] Support namespace declarations. --- .../pljava/example/annotation/S9.java | 77 +++++++++++++++++-- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java index 28521ba4..bca56c19 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java @@ -42,6 +42,10 @@ import java.util.Properties; import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI; +import static javax.xml.XMLConstants.XML_NS_URI; +import static javax.xml.XMLConstants.XML_NS_PREFIX; +import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI; +import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE; import javax.xml.transform.stream.StreamSource; @@ -182,6 +186,12 @@ enum Nulls { ABSENT, NIL }; * always work, while {@code AS "."} will.) JDBC uppercases all column * names, so any uses of the corresponding variables in the query must have * the names in upper case. + * @param namespaces An even-length String array where, of each pair of + * consecutive entries, the first is a namespace prefix and the second is + * to URI to which to bind it. The zero-length prefix sets the default + * element and type namespace; if the prefix has zero length, the URI may + * also have zero length, to declare that unprefixed elements are in no + * namespace. */ @Function( schema="javatest", @@ -190,7 +200,8 @@ enum Nulls { ABSENT, NIL }; ) public static SQLXML xq_ret_content( String expression, Boolean nullOnEmpty, - @SQLType(defaultValue={}) ResultSet passing) + @SQLType(defaultValue={}) ResultSet passing, + @SQLType(defaultValue={}) String[] namespaces) throws SQLException { if ( null == nullOnEmpty ) @@ -199,8 +210,9 @@ public static SQLXML xq_ret_content( try { - XdmValue x1 = xmlquery_internal( - expression, new BindingsFromResultSet(passing)); + XdmValue x1 = xmlquery_internal(expression, + new BindingsFromResultSet(passing), + namespaceBindings(namespaces)); SequenceIterator x1s = x1.getUnderlyingValue().iterate(); if ( nullOnEmpty.booleanValue() ) @@ -233,7 +245,8 @@ public static SQLXML xq_ret_content( } private static XdmValue xmlquery_internal( - String expression, Binding.Assemblage passing) + String expression, Binding.Assemblage passing, + Iterable> namespaces) throws SQLException, SaxonApiException, XPathException { /* @@ -247,7 +260,8 @@ private static XdmValue xmlquery_internal( /* * Get an XQueryCompiler with the static context properly set up. */ - XQueryCompiler xqc = createStaticContextWithPassedTypes(passing); + XQueryCompiler xqc = createStaticContextWithPassedTypes( + passing, namespaces); XQueryExecutable xqx = xqc.compile(expression); @@ -327,7 +341,7 @@ else if ( v instanceof SQLXML ) // XXX support SEQUENCE someday * with the null key. */ private static XQueryCompiler createStaticContextWithPassedTypes( - Binding.Assemblage pt) + Binding.Assemblage pt, Iterable> namespaces) throws SQLException, XPathException { XQueryCompiler xqc = s_s9p.newXQueryCompiler(); @@ -336,6 +350,9 @@ private static XQueryCompiler createStaticContextWithPassedTypes( // https://sourceforge.net/p/saxon/mailman/message/20318550/ : xqc.declareNamespace("xdt", W3C_XML_SCHEMA_NS_URI); + for ( Map.Entry e : namespaces ) + xqc.declareNamespace(e.getKey(), e.getValue()); + /* * This business of predeclaring global external named variables * is not an s9api-level advertised ability in Saxon, hence the @@ -720,6 +737,54 @@ private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( "0N000"); } + static Iterable> namespaceBindings(String[] nbs) + throws SQLException + { + if ( 1 == nbs.length % 2 ) + throw new SQLSyntaxErrorException( + "Namespace binding array must have even length", "42000"); + Map m = new HashMap(); + + for ( int i = 0; i < nbs.length; i += 2 ) + { + String prefix = nbs[i]; + String uri = nbs[1 + i]; + + if ( null == prefix || null == uri ) + throw new SQLSyntaxErrorException( + "Namespace binding array elements must not be null", + "42000"); + + if ( ! "".equals(prefix) ) + { + if ( ! isValidNCName(prefix) ) + throw new SQLSyntaxErrorException( + "Not an XML NCname: \"" + prefix + '"', "42602"); + if ( XML_NS_PREFIX.equals(prefix) + || XMLNS_ATTRIBUTE.equals(prefix) ) + throw new SQLSyntaxErrorException( + "Namespace prefix may not be xml or xmlns", "42939"); + if ( XML_NS_URI.equals(uri) + || XMLNS_ATTRIBUTE_NS_URI.equals(uri) ) + throw new SQLSyntaxErrorException( + "Namespace URI has a disallowed value", "42P17"); + if ( "".equals(uri) ) + throw new SQLSyntaxErrorException( + "URI for non-default namespace may not be zero-length", + "42P17"); + } + + String was = m.put(prefix.intern(), uri.intern()); + + if ( null != was ) + throw new SQLSyntaxErrorException( + "Namespace prefix \"" + prefix + "\" multiply bound (" + + "to \"" + was + "\" and \"" + uri + "\")", "42712"); + } + + return Collections.unmodifiableSet(m.entrySet()); + } + static class Binding { String typePG() throws SQLException From 8064557349d62be64733f226bfe11904dfed0ecb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 18 Jun 2018 02:09:35 -0400 Subject: [PATCH 0107/1087] A first cut at XMLTABLE. Correctly returns atomic types matching atomic values passed in, except times and timestamps aren't right yet, to the great surprise of no one in particular. Is limited, so far, to output columns of non-XML types, where each column expression produces an atomic value that's an instance of the atomic type the rules determine from the SQL output column type. The automatic casting of some other atomic type result value AV to the value BV of the determined type isn't implemented yet (but that is no blocker; XQuery "cast as" can be written into the column expression to achieve exactly that effect). This is perhaps most noticeable with the output column type bytea, for which the rules always determine an XQuery type of xs:hexBinary. A binary value in the form xs:base64Binary can't be retrieved as a bytea, without writing "cast as xs:hexBinary" in the column expression. That limitation will go away when the AV-to-BV casting rules are in place. --- .../pljava/example/annotation/S9.java | 545 ++++++++++++++++-- 1 file changed, 497 insertions(+), 48 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java index bca56c19..204e1c25 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java @@ -32,6 +32,7 @@ import java.sql.SQLNonTransientException; import java.sql.SQLSyntaxErrorException; +import static java.util.Arrays.asList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -55,8 +56,6 @@ import net.sf.saxon.query.QueryResult; import net.sf.saxon.query.StaticQueryContext; -import net.sf.saxon.tree.iter.LookaheadIterator; - import net.sf.saxon.s9api.DocumentBuilder; import net.sf.saxon.s9api.ItemType; import net.sf.saxon.s9api.ItemTypeFactory; @@ -64,11 +63,14 @@ import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.QName; import net.sf.saxon.s9api.SequenceType; +import static net.sf.saxon.s9api.SequenceType.makeSequenceType; import net.sf.saxon.s9api.XdmAtomicValue; import static net.sf.saxon.s9api.XdmAtomicValue.makeAtomicValue; import net.sf.saxon.s9api.XdmEmptySequence; +import net.sf.saxon.s9api.XdmItem; import static net.sf.saxon.s9api.XdmNodeKind.DOCUMENT; import net.sf.saxon.s9api.XdmValue; +import net.sf.saxon.s9api.XdmSequenceIterator; import net.sf.saxon.s9api.XQueryCompiler; import net.sf.saxon.s9api.XQueryEvaluator; import net.sf.saxon.s9api.XQueryExecutable; @@ -77,9 +79,13 @@ import net.sf.saxon.trans.XPathException; +import net.sf.saxon.tree.iter.LookaheadIterator; + import net.sf.saxon.value.Base64BinaryValue; import net.sf.saxon.value.HexBinaryValue; +import org.postgresql.pljava.ResultSetProvider; + import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.annotation.Function.OnNullInput.CALLED; @@ -120,9 +126,9 @@ * rewritten as this call to the {@link #xq_ret_content xq_ret_content} method: *

  * SELECT javatest.xq_ret_content('/pg_catalog/pg_proc[proname eq $NAME]',
- *                                passing, nullOnEmpty => false)
+ *                                PASSING => p, nullOnEmpty => false)
  * FROM catalog_as_xml,
- * LATERAL (SELECT x AS ".", 'numeric_avg' AS NAME) AS passing;
+ * LATERAL (SELECT x AS ".", 'numeric_avg' AS NAME) AS p;
  *
*

* In the rewritten form, the type of value returned is determined by which @@ -139,11 +145,53 @@ * provides no way for {@code BY REF} semantics to be implemented, so everything * happening here happens {@code BY VALUE} implicitly, and does not need to be * specified. + *

+ * The function {@link #xmltable xmltable} here implements (much of) the + * standard function of the same name. Because it is the same name, it has to + * be either schema-qualified or double-quoted in a call to avoid confusion + * with the reserved word. A rewritten form of the first example in the PostgreSQL manual could be: + *

+ * SELECT xmltable.*
+ * FROM
+ *	xmldata,
+ *
+ *	LATERAL (SELECT data AS ".", 'not specified'::text AS DPREMIER) AS p,
+ *
+ *	"xmltable"('//ROWS/ROW', PASSING => p, COLUMNS => ARRAY[
+ *	 'xs:int(@id)', null, 'string(COUNTRY_NAME)',
+ *	 'string(COUNTRY_ID)', 'xs:double(SIZE[@unit eq "sq_km"])',
+ *	 'concat(SIZE[@unit ne "sq_km"], " ", SIZE[@unit ne "sq_km"]/@unit)',
+ *	 'let $e := zero-or-one(PREMIER_NAME)/string()
+ *	  return if ( empty($e) )then $DPREMIER else $e'
+ *	]) AS "xmltable" (
+ *	 id int, ordinality int, "COUNTRY_NAME" text, country_id text,
+ *	 size_sq_km float, size_other text, premier_name text
+ *	);
+ *
+ *

+ * The explicit casts and {@code zero-or-one} will not be needed once the + * full automatic casting rules (for now only partially implemented) are + * in place. * @author Chapman Flack */ -public class S9 +public class S9 implements ResultSetProvider { - private S9() { } + private S9( + XdmSequenceIterator xsi, + XQueryEvaluator[] columnXQEs, + SequenceType[] columnStaticTypes) + { + m_sequenceIterator = xsi; + m_columnXQEs = columnXQEs; + m_columnStaticTypes = columnStaticTypes; + } + + final XdmSequenceIterator m_sequenceIterator; + final XQueryEvaluator[] m_columnXQEs; + final SequenceType[] m_columnStaticTypes; + Binding.Assemblage m_outBindings; static final Connection s_dbc; static final Processor s_s9p = new Processor(false); @@ -204,15 +252,35 @@ public static SQLXML xq_ret_content( @SQLType(defaultValue={}) String[] namespaces) throws SQLException { + /* + * The expression itself may not be null (in the standard, it isn't + * even allowed to be dynamic, and can only be a string literal!). + */ + if ( null == expression ) + throw new SQLDataException( + "XMLQUERY expression may not be null", "22004"); + if ( null == nullOnEmpty ) throw new SQLDataException( "XMLQUERY nullOnEmpty may not be null", "22004"); + Binding.Assemblage bindings = new BindingsFromResultSet(passing); + try { - XdmValue x1 = xmlquery_internal(expression, - new BindingsFromResultSet(passing), - namespaceBindings(namespaces)); + XQueryCompiler xqc = createStaticContextWithPassedTypes( + bindings, namespaceBindings(namespaces)); + + XQueryEvaluator xqe = xqc.compile(expression).load(); + + if ( storePassedValuesInDynamicContext(xqe, bindings, true) ) + return null; + + /* + * For now, punt on whether the is evaluated + * with XML 1.1 or 1.0 lexical rules.... XXX + */ + XdmValue x1 = xqe.evaluate(); SequenceIterator x1s = x1.getUnderlyingValue().iterate(); if ( nullOnEmpty.booleanValue() ) @@ -244,39 +312,241 @@ public static SQLXML xq_ret_content( } } - private static XdmValue xmlquery_internal( - String expression, Binding.Assemblage passing, - Iterable> namespaces) - throws SQLException, SaxonApiException, XPathException + /** + * An implementation of (much of) XMLTABLE, using genuine XML Query. + *

+ * The {@code columns} array must supply a valid XML Query expression for + * every column in the column definition list that follows the call of this + * function in SQL, except that the column for ordinality, if wanted, is + * identified by a {@code null} entry in {@code columns}. Syntax sugar in + * the standard allows an omitted column expression to imply an element test + * for an element with the same name as the column; that doesn't work here. + *

+ * For now, this implementation lacks the ability to specify defaults for + * when a column expression produces an empty sequence. It is possible to + * do defaults explicitly by rewriting a query expression expr as + * {@code let $e := }expr{@code return if(empty($e))then $D else $e} + * and supplying the default D as another query parameter, though + * such defaults will be evaluated only once when {@code xmltable} is called + * and will not be able to refer to other values in an output row. + *

+ * Output columns of XML type are not yet supported. + * @param rows The single XQuery expression whose result sequence generates + * the rows of the resulting table. Must not be null. + * @param columns Array of XQuery expressions, exactly as many as result + * columns in the column definition list that follows the SQL call to this + * function. This array must not be null. It is allowed for one element (and + * no more than one) to be null, marking the corresponding column to be + * "FOR ORDINALITY" (the column must be of integer, or, ahem, "exact numeric + * with scale zero", type). + * @param passing A row value whose columns will be supplied to the query + * as parameters, just as described for + * {@link #xq_ret_content xq_ret_content()}. If a context item is supplied, + * it is the context item for the {@code rows} query (the {@code columns} + * queries get their context item from the {@code rows} query's result). Any + * named parameters supplied here are available both in the {@code rows} + * expression and (though this goes beyond the standard) in every expression + * of {@code columns}, with their values unchanging from row to row. + * @param namespaces An even-length String array where, of each pair of + * consecutive entries, the first is a namespace prefix and the second is + * to URI to which to bind it, just as described for + * {@link #xq_ret_content xq_ret_content()}. + */ + @Function( + schema="javatest", + onNullInput=CALLED, + settings="IntervalStyle TO iso_8601" + ) + public static ResultSetProvider xmltable( + String rows, String[] columns, + @SQLType(defaultValue={}) ResultSet passing, + @SQLType(defaultValue={}) String[] namespaces) + throws SQLException { - /* - * The expression itself may not be null (in the standard, it isn't - * even allowed to be dynamic, and can only be a string literal!). - */ - if ( null == expression ) + if ( null == rows ) throw new SQLDataException( - "XMLQUERY expression may not be null", "22004"); + "XMLTABLE row expression may not be null", "22004"); - /* - * Get an XQueryCompiler with the static context properly set up. - */ - XQueryCompiler xqc = createStaticContextWithPassedTypes( - passing, namespaces); + if ( null == columns ) + throw new SQLDataException( + "XMLTABLE columns expression array may not be null", "22004"); - XQueryExecutable xqx = xqc.compile(expression); + Binding.Assemblage rowBindings = new BindingsFromResultSet(passing); - /* - * This method implements the RETURNING CONTENT case. - * - * Now for the General Rules. - */ - XQueryEvaluator xqe = xqx.load(); + Iterable> namespacepairs = + namespaceBindings(namespaces); + + XQueryEvaluator[] columnXQEs = new XQueryEvaluator[ columns.length ]; + SequenceType[] columnStaticTypes = new SequenceType[ columns.length ]; + + try + { + XQueryCompiler rowXQC = createStaticContextWithPassedTypes( + rowBindings, namespacepairs); + + XQueryExecutable rowXQX = rowXQC.compile(rows); + + Binding.Assemblage columnBindings = + new BindingsFromXQX(rowXQX, rowBindings); + + XQueryCompiler columnXQC = createStaticContextWithPassedTypes( + columnBindings, namespacepairs); + + boolean ordinalitySeen = false; + for ( int i = 0; i < columns.length; ++ i ) + { + String expr = columns[i]; + if ( null == expr ) + { + if ( ordinalitySeen ) + throw new SQLSyntaxErrorException( + "No more than one column expression may be null " + + "(=> \"for ordinality\")", "42611"); + ordinalitySeen = true; + continue; + } + XQueryExecutable columnXQX = columnXQC.compile(expr); + columnStaticTypes[i] = makeSequenceType( + columnXQX.getResultItemType(), + columnXQX.getResultCardinality()); + columnXQEs[i] = columnXQX.load(); + storePassedValuesInDynamicContext( + columnXQEs[i], columnBindings, false); + } + + XQueryEvaluator rowXQE = rowXQX.load(); + XdmSequenceIterator rowIterator; + if ( storePassedValuesInDynamicContext(rowXQE, rowBindings, true) ) + rowIterator = XdmEmptySequence.getInstance().iterator(); + else + rowIterator = rowXQE.iterator(); + return new S9(rowIterator, columnXQEs, columnStaticTypes); + } + catch ( SaxonApiException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } + catch ( XPathException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } + } + + @Override + public void close() + { + m_sequenceIterator.close(); + } + + @Override + public boolean assignRowValues(ResultSet receive, int currentRow) + throws SQLException + { + if ( 0 == currentRow ) + m_outBindings = new BindingsFromResultSet(receive, m_columnXQEs); + + if ( ! m_sequenceIterator.hasNext() ) + return false; + + ++ currentRow; // for use as 1-based ordinality column + + XdmItem it = m_sequenceIterator.next(); + + int i = 0; + for ( Binding.Parameter p : m_outBindings ) + { + XQueryEvaluator xqe = m_columnXQEs [ i ]; + SequenceType staticType = m_columnStaticTypes [ i++ ]; + + if ( null == xqe ) + { + receive.updateInt( i, currentRow); + continue; + } + + xqe.setContextItem(it); + + try + { + XdmValue x1 = xqe.evaluate(); + + /* + * The fully general rules, outlining how x1 gets from here to + * the end goal of a value of the SQL result column, are a + * real caution. They begin by pumping x1 through all the + * effects of XMLDOCUMENT( ... RETURNING CONTENT) (text nodes + * built from atomic values, errors raised for attribute nodes, + * everything wrapped in a document node, etc.,) and then + * hitting that on the head with XMLCAST. In the case where the + * SQL result column has a non-XML type, that starts right off + * by document-node unwrapping followed by atomization (you get + * your dog back, you get your truck back, you get your wife + * back, only now as lexical strings with types all erased + * to xs:untypedAtomic), and finally (ring a little bell here) + * putting those through XML Query "cast as" to the XQuery type + * determined according to the output column SQL type, and then + * converting and stashing that value into the result column. + * + * In cases where the XQuery compiler was statically certain + * the result will be exactly one, or zero-or-one, atomic value, + * it seems sensible to skip right ahead to the little ringing + * bell ... both for efficiency's sake, and because it gets a + * very useful subset of XMLTABLE working before all that other + * jazz is in place. + */ + if ( staticType.getOccurrenceIndicator().allowsMany() + || ! ItemType.ANY_ATOMIC_VALUE.subsumes( + staticType.getItemType()) ) + throw new UnsupportedOperationException( + "The fully general rules for returning XMLTABLE " + + "output are not yet implemented (column " + i + ")."); + + /* + * The value is statically known to be atomic and either exactly + * one or zero-or-one. May as well just use size() to see if + * it's empty. + */ + if ( 0 == x1.size() ) + { + receive.updateNull(i); // XXX Handle defaults some day + continue; + } + XdmAtomicValue av = (XdmAtomicValue)x1.itemAt(0); + xmlCastAsNonXML(av, p, receive, i); + } + catch ( SaxonApiException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } + } + return true; + } + + /** + * Store the values of any passed parameters and/or context item into the + * dynamic context, returning true if the overall query should + * short-circuit and return null. + *

+ * The specification requires the overall query to return null if a + * context item is specified in the bindings and its value is null. + * @param xqe XQuery evaluator into which to store the values. + * @param passing The bindings whose values should be installed. + * @param setContextItem True to handle the context item, if present in the + * bindings. False to skip any processing of the context item, in cases + * where the caller will handle that. + * @return True if the overall query's return should be null, false if the + * query should proceed to evaluation. + */ + private static boolean storePassedValuesInDynamicContext( + XQueryEvaluator xqe, Binding.Assemblage passing, boolean setContextItem) + throws SQLException, SaxonApiException + { DocumentBuilder dBuilder = s_s9p.newDocumentBuilder(); /* * Is there or is there not a context item? */ - if ( null == passing.contextItem() ) + if ( ! setContextItem || null == passing.contextItem() ) { /* "... there is no context item in XDC." */ } @@ -284,13 +554,13 @@ private static XdmValue xmlquery_internal( { Object cve = passing.contextItem().valueJDBC(); if ( null == cve ) - return null; + return true; XdmValue ci; if ( cve instanceof SQLXML ) // XXX support SEQUENCE input someday ci = dBuilder.build(((SQLXML)cve).getSource(null)); else ci = xmlCastAsSequence( - cve, XMLBinary.HEX, xqc.getRequiredContextItemType()); + cve, XMLBinary.HEX, passing.contextItem().typeXS()); switch ( ci.size() ) { case 0: @@ -323,11 +593,7 @@ else if ( v instanceof SQLXML ) // XXX support SEQUENCE someday xqe.setExternalVariable(new QName(name), vv); } - /* - * For now, punt on whether the is evaluated - * with XML 1.1 or 1.0 lexical rules.... XXX - */ - return xqe.evaluate(); + return false; } /** @@ -480,7 +746,7 @@ else if ( b.knownNonNull() ) it = xmlt; } - SequenceType xftn = SequenceType.makeSequenceType(it, suffix); + SequenceType xftn = makeSequenceType(it, suffix); return xftn; } @@ -497,7 +763,7 @@ private static ItemType mapSQLDataTypeToXMLSchemaDataType( * and our painstakingly specified derived type is replaced with * the primitive type we based it on. That simplifies a lot. :) * For now, forget the derived XML Schema declarations, and just - * return the primitive types thwy would be based on. + * return the primitive types they would be based on. * * The need for the nulls parameter vanishes if no XML Schema snippets * are to be generated. @@ -685,6 +951,74 @@ private static XdmValue xmlCastAsSequence( } } + /** + * Handle the case of XMLCAST to a non-XML target type when the cast operand + * is already a single atomic value. + * @param av The atomic operand value + * @param p The parameter binding, recording the needed type information + * @param rs ResultSet into which the value will be stored + * @param col Index of the result column + */ + private static void xmlCastAsNonXML( + XdmAtomicValue av, Binding.Parameter p, ResultSet rs, int col) + throws SQLException + { + XdmAtomicValue bv; + ItemType xt = p.typeXS().getItemType(); + if ( xt.matches(av) ) // perhaps we can skip one bout of casting + bv = av; + else + throw new UnsupportedOperationException( + "Casting AV to BV is not yet implemented (column " + col + ")"); + + if ( ItemType.STRING.subsumes(xt) ) + rs.updateString(col, bv.getStringValue()); + + else if ( ItemType.HEX_BINARY.subsumes(xt) ) + rs.updateBytes(col, + ((HexBinaryValue)bv.getUnderlyingValue()).getBinaryValue()); + else if ( ItemType.BASE64_BINARY.subsumes(xt) ) + rs.updateBytes(col, + ((Base64BinaryValue)bv.getUnderlyingValue()).getBinaryValue()); + + else if ( ItemType.DECIMAL.subsumes(xt) ) + rs.updateObject(col, bv.getValue()); + + /* + * The standard calls for throwing "data exception - numeric value out + * of range" rather than forwarding a float or double inf, -inf, or nan + * to SQL, but PostgreSQL supports those values, and these conversions + * preserve them. + */ + else if ( ItemType.FLOAT.subsumes(xt) ) + rs.updateObject(col, bv.getValue()); + else if ( ItemType.DOUBLE.subsumes(xt) ) + rs.updateObject(col, bv.getValue()); + + else if ( ItemType.DATE.subsumes(xt) ) // try it, see what goes wrong + rs.updateString(col, bv.getStringValue()); + else if ( ItemType.DATE_TIME.subsumes(xt) ) // likewise (no tz case) + rs.updateString(col, bv.getStringValue()); + else if ( ItemType.DATE_TIME_STAMP.subsumes(xt) ) // likewise (tz case) + rs.updateString(col, bv.getStringValue()); + else if ( ItemType.TIME.subsumes(xt) ) // no handy tz/notz distinction + rs.updateString(col, bv.getStringValue()); + + else if ( ItemType.YEAR_MONTH_DURATION.subsumes(xt) ) + rs.updateString(col, bv.getStringValue()); + else if ( ItemType.DAY_TIME_DURATION.subsumes(xt) ) + rs.updateString(col, bv.getStringValue()); + else if ( ItemType.DURATION.subsumes(xt) ) // need this case for now + rs.updateString(col, bv.getStringValue()); + + else if ( ItemType.BOOLEAN.subsumes(xt) ) + rs.updateObject(col, bv.getValue()); + else + throw new SQLNonTransientException(String.format( + "Mapping XML type \"%s\" to SQL value not supported", xt), + "0N000"); + } + private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( Object dv, XMLBinary encoding, ItemType xst) throws SQLException, SaxonApiException @@ -751,9 +1085,9 @@ static Iterable> namespaceBindings(String[] nbs) String uri = nbs[1 + i]; if ( null == prefix || null == uri ) - throw new SQLSyntaxErrorException( + throw new SQLDataException( "Namespace binding array elements must not be null", - "42000"); + "22004"); if ( ! "".equals(prefix) ) { @@ -854,9 +1188,20 @@ SequenceType typeXS() throws SQLException private final String m_name; - protected Parameter(String name) throws SQLException + /** + * @param name The SQL name of the parameter + * @param isInput True if this is an input parameter from SQL to the + * XML query context, or false if it describes a result (as in the + * ouput of XMLTABLE). If an input parameter, the name must be a + * valid NCName; for an output parameter, the name doesn't matter + * and isn't checked. + * @throws SQLException if the name of an input parameter isn't a + * valid NCName. + */ + protected Parameter(String name, boolean isInput) + throws SQLException { - if ( ! isValidNCName(name) ) + if ( isInput && ! isValidNCName(name) ) throw new SQLSyntaxErrorException( "Not an XML NCname: \"" + name + '"', "42602"); m_name = name; @@ -928,6 +1273,16 @@ public Iterator iterator() static class BindingsFromResultSet extends Binding.Assemblage { + /** + * Construct the bindings from a ResultSet representing input parameters + * to an XML query. + * @param rs ResultSet representing the input parameters. Column names + * "." and "?COLUMN?" are treated specially, and used to supply the + * query's context item; every other column name must be a valid NCName, + * and neither any named parameter nor the context item may be mentioned + * more than once. + * @throws SQLException if names are duplicated or invalid. + */ BindingsFromResultSet(ResultSet rs) throws SQLException { m_resultSet = rs; @@ -952,7 +1307,7 @@ static class BindingsFromResultSet extends Binding.Assemblage } Parameter was = - (Parameter)n2b.put(label, new Parameter(label, i)); + (Parameter)n2b.put(label, new Parameter(label, i, true)); if ( null != was ) throw new SQLSyntaxErrorException( "Name \"" + label + "\" duplicated at positions " + @@ -963,6 +1318,63 @@ static class BindingsFromResultSet extends Binding.Assemblage m_params = n2b.values(); } + /** + * Construct the bindings from a ResultSet representing output + * parameters (as from XMLTABLE). + * @param rs ResultSet representing the result parameters. Names have + * no particular significance and are not subject to any checks. + * @param exprs Compiled evaluators for the supplied column expressions. + * The number of these must match the number of columns in {@code rs}. + * One of these (and no more than one; the caller will have enforced + * that) is allowed to be null, making the corresponding column + * "FOR ORDINALITY". An ordinality column will be checked to ensure it + * has an SQL type that is (ahem) "exact numeric with scale 0 (zero)." + * @throws SQLException if numbers of columns and expressions don't + * match, or there is an ordinality column and its type is not suitable. + */ + BindingsFromResultSet(ResultSet rs, XQueryEvaluator[] exprs) + throws SQLException + { + m_resultSet = rs; + m_rsmd = rs.getMetaData(); + + int nParams = m_rsmd.getColumnCount(); + if ( nParams != exprs.length ) + throw new SQLSyntaxErrorException( + "Not as many supplied column expressions as output columns", + "42611"); + + Binding.Parameter[] ps = new Binding.Parameter[ nParams ]; + + for ( int i = 1; i <= nParams; ++i ) + { + String label = m_rsmd.getColumnLabel(i); + Parameter p = new Parameter(label, i, false); + ps [ i - 1 ] = p; + if ( null == exprs [ i - 1 ] ) + { + switch ( p.typeJDBC() ) + { + case Types.INTEGER: + case Types.SMALLINT: + case Types.BIGINT: + break; + case Types.NUMERIC: + case Types.DECIMAL: + int scale = p.scale(); + if ( 0 == scale || -1 == scale ) + break; + default: + throw new SQLSyntaxErrorException( + "Column FOR ORDINALITY must have an exact numeric" + + " type with scale zero.", "42611"); + } + } + } + + m_params = asList(ps); + } + private ResultSet m_resultSet; private ResultSetMetaData m_rsmd; @@ -997,9 +1409,10 @@ class Parameter extends Binding.Parameter { final int m_idx; - Parameter(String name, int index) throws SQLException + Parameter(String name, int index, boolean isInput) + throws SQLException { - super(name); + super(name, isInput); m_idx = index; } @@ -1029,4 +1442,40 @@ protected Object implValueJDBC() throws SQLException } } } + + static class BindingsFromXQX extends Binding.Assemblage + { + /** + * Construct a new assemblage of bindings for the static context of an + * XMLTABLE column expression. It will have the same named-parameter + * bindings passed to the row expression, but the static type of the + * context item will be the result type of the row expression. The + * {@code ContextItem} in this assemblage will have no associated value; + * the caller is responsible for retrieving that from the row evaluator + * and storing it in the column expression context every iteration. + * @param xqx The result of compiling the row expression; its + * compiler-determined static result type will be used as the static + * context item type. + * @param params The bindings supplied to the row expression. Its named + * parameters will be copied as the named parameters here. + */ + BindingsFromXQX(XQueryExecutable xqx, Binding.Assemblage params) + { + m_params = params.m_params; + m_contextItem = new ContextItem(xqx.getResultItemType()); + } + + static class ContextItem extends Binding.ContextItem + { + ContextItem(ItemType it) + { + m_typeXS = it; + } + + protected int implTypeJDBC() throws SQLException + { + return Types.OTHER; // anything canCastAsXmlSequence won't toss + } + } + } } From 522053898ea085e71a99c0beb49f4ada43158196 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 18 Jun 2018 23:43:38 -0400 Subject: [PATCH 0108/1087] Get negative intervals working. Both XML Schema and PostgreSQL supposedly accept the ISO 8601 PdddYddMddDTddHddMdd.dddS form, but they could hardly disagree more on what to do if the value is negative. XML Schema wants -PdddYddMddDTddHddMdd.dddS which PG roundly rejects, wanting to see instead P-dddY-ddM-ddDT-ddH-ddM-dd.dddS which is anathema to XS. For this datatype there is no real help to be had from JDBC or Saxon methods, so it boils down to shuffling the '-' characters around in the lexical string. --- .../pljava/example/annotation/S9.java | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java index 204e1c25..cf08374a 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java @@ -42,6 +42,9 @@ import java.util.NoSuchElementException; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI; import static javax.xml.XMLConstants.XML_NS_URI; import static javax.xml.XMLConstants.XML_NS_PREFIX; @@ -197,6 +200,9 @@ private S9( static final Processor s_s9p = new Processor(false); static final ItemTypeFactory s_itf = new ItemTypeFactory(s_s9p); + static final Pattern s_intervalSigns; + static final Pattern s_intervalSignSite; + enum XMLBinary { HEX, BASE64 }; enum Nulls { ABSENT, NIL }; @@ -205,6 +211,27 @@ enum Nulls { ABSENT, NIL }; try { s_dbc = DriverManager.getConnection("jdbc:default:connection"); + + /* + * XML Schema thinks an ISO 8601 duration must have no sign + * anywhere but at the very beginning before the P. PostgreSQL + * thinks that's the one place a sign must never be, and instead + * it should appear in front of every numeric field. (It will + * accept input where the signs vary, but not produce such output.) + * So, here's a regex with a capturing group for a leading -, and + * one for any field-leading -, and one for the absence of a field- + * leading -. Any PostgreSQL or XS duration ought to match overall, + * but the capturing group matches should be either (f,f,t) or + * (f,t,f) for a PostgreSQL duration, or either (f,f,t) or (t,f,t) + * for an XS duration. + */ + s_intervalSigns = Pattern.compile( + "(-)?+(?:[PYMWDTH](?:(?:(-)|())\\d++)?+)++(?:(?:[.,]\\d*+)?+S)?+"); + /* + * To convert from the leading-sign form, need to find every spot + * where a digit follows a [PYMWDTH] to insert a - there. + */ + s_intervalSignSite = Pattern.compile("(?<=[PYMWDTH])(?=\\d)"); } catch ( SQLException e ) { @@ -1005,11 +1032,11 @@ else if ( ItemType.TIME.subsumes(xt) ) // no handy tz/notz distinction rs.updateString(col, bv.getStringValue()); else if ( ItemType.YEAR_MONTH_DURATION.subsumes(xt) ) - rs.updateString(col, bv.getStringValue()); + rs.updateString(col, toggleIntervalRepr(bv.getStringValue())); else if ( ItemType.DAY_TIME_DURATION.subsumes(xt) ) - rs.updateString(col, bv.getStringValue()); + rs.updateString(col, toggleIntervalRepr(bv.getStringValue())); else if ( ItemType.DURATION.subsumes(xt) ) // need this case for now - rs.updateString(col, bv.getStringValue()); + rs.updateString(col, toggleIntervalRepr(bv.getStringValue())); else if ( ItemType.BOOLEAN.subsumes(xt) ) rs.updateObject(col, bv.getValue()); @@ -1064,13 +1091,36 @@ private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( ((Timestamp)dv).toString().replace(' ', 'T'), xst); if ( ItemType.DURATION.equals(xst) ) - return new XdmAtomicValue((String)dv, xst); + return new XdmAtomicValue(toggleIntervalRepr((String)dv), xst); throw new SQLNonTransientException(String.format( "Mapping SQL value to XML type \"%s\" not supported", xst), "0N000"); } + /* + * Toggle the lexical representation of an interval/duration between the + * form PostgreSQL likes and the form XML Schema likes. Only negative values + * are affected. Positive values are returned unchanged, as are those that + * don't fit any expected form; those will probably be reported as malformed + * by whatever tries to consume them. + */ + static String toggleIntervalRepr(String lex) + { + Matcher m = s_intervalSigns.matcher(lex); + if ( ! m.matches() ) + return lex; // it's weird, just don't touch it + if ( -1 == m.start(1) ) + { + if ( -1 != m.start(2) && -1 == m.start(3) ) // it's PG negative + return '-' + lex.replace("-", ""); // make it XS negative + } + else if ( -1 == m.start(2) && -1 != m.start(3) )// it's XS negative + return m.usePattern(s_intervalSignSite) // make it PG negative + .reset(lex.substring(1)).replaceAll("-"); + return lex; // it's either positive, or weird, just don't touch it + } + static Iterable> namespaceBindings(String[] nbs) throws SQLException { From 5d9374b731ddde98c3d6a2edcb0aee102988b01e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 25 Jun 2018 22:31:36 -0400 Subject: [PATCH 0109/1087] Add TypeRoundTripper example class. This class simplifies testing of PL/Java's mappings between PostgreSQL types and Java/JDBC types. It accepts and returns RECORD, so values of any type can be passed and returned. --- .../example/annotation/TypeRoundTripper.java | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java new file mode 100644 index 00000000..1ad8640c --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2018- 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.reflect.Field; +import java.lang.reflect.Modifier; +import static java.lang.reflect.Modifier.isPublic; +import static java.lang.reflect.Modifier.isStatic; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Types; +import static java.sql.Types.VARCHAR; + +import java.sql.SQLException; +import java.sql.SQLDataException; + +import org.postgresql.pljava.annotation.Function; + +/** + * A class to simplify testing of PL/Java's mappings between PostgreSQL and + * Java/JDBC types. + *

+ * Provides one function, {@link #roundTrip roundTrip()}. Its single input + * parameter is an unspecified row type, so you can pass it a row that has + * exactly one column of any type. + *

+ * Its return type is also an unspecified row type, so you need to follow the + * function call with a column definition list of up to six columns. Each + * requested output column must have its name (case-insensitively) and type + * drawn from this table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Column nameColumn typeWhat is returned
TYPEPGany text/varcharThe PostgreSQL type name
TYPEJDBCany text/varcharThe JDBC Types constant
CLASSJDBCany text/varcharName of the Java class JDBC claims (in metadata) it will instantiate
CLASSany text/varcharName of the Java class JDBC did instantiate
TOSTRINGany text/varcharResult of {@code toString()} on the object returned by + * {@code ResultSet.getObject()}
ROUNDTRIPPEDsame as input columnResult of passing the object returned by {@code ResultSet.getObject()} + * directly to {@code ResultSet.updateObject()}
+ *

+ * Serving suggestion: + *

+ *SELECT
+ *  orig = roundtripped AS good, *
+ *FROM
+ *  (VALUES (timestamptz '2017-08-21 18:25:29.900005Z')) AS p(orig),
+ *  roundtrip(p) AS r(roundtripped timestamptz);
+ *
+ */ +public class TypeRoundTripper +{ + private TypeRoundTripper() { } + + /** + * Function accepting one parameter of row type (one column, any type) + * and returning a row with up to six columns (use a column definition list + * after the function call, choose column names from TYPEPG, TYPEJDBC, + * CLASSJDBC, CLASS, TOSTRING, ROUNDTRIPPED where any of the first five + * must have text/varchar type, while ROUNDTRIPPED must match the type of + * the input column). + * @param in The input row value (required to have exactly one column). + * @param out The output row (supplied by PL/Java, representing the column + * definition list that follows the call of this function in SQL). + * @throws SQLException if {@code in} does not have exactly one column, if + * {@code out} has more than six, if a requested column name in {@code out} + * is not among those recognized, if a column of {@code out} is not of its + * required type, or if other stuff goes wrong. + */ + @Function( + schema = "javatest", + type = "RECORD" + ) + public static boolean roundTrip(ResultSet in, ResultSet out) + throws SQLException + { + ResultSetMetaData inmd = in.getMetaData(); + ResultSetMetaData outmd = out.getMetaData(); + + if ( 1 != inmd.getColumnCount() ) + throw new SQLDataException( + "in parameter must be a one-column row type", "22000"); + + int outcols = outmd.getColumnCount(); + if ( 6 < outcols ) + throw new SQLDataException( + "result description may have no more than six columns", + "22000"); + + String inTypePG = inmd.getColumnTypeName(1); + int inTypeJDBC = inmd.getColumnType(1); + Object val = in.getObject(1); + + for ( int i = 1; i <= outcols; ++ i ) + { + String what = outmd.getColumnLabel(i); + + if ( "TYPEPG".equals(what) ) + { + assertTypeJDBC(outmd, i, VARCHAR); + out.updateObject(i, inTypePG); + } + else if ( "TYPEJDBC".equals(what) ) + { + assertTypeJDBC(outmd, i, VARCHAR); + out.updateObject(i, typeNameJDBC(inTypeJDBC)); + } + else if ( "CLASSJDBC".equals(what) ) + { + assertTypeJDBC(outmd, i, VARCHAR); + out.updateObject(i, inmd.getColumnClassName(1)); + } + else if ( "CLASS".equals(what) ) + { + assertTypeJDBC(outmd, i, VARCHAR); + out.updateObject(i, val.getClass().getName()); + } + else if ( "TOSTRING".equals(what) ) + { + assertTypeJDBC(outmd, i, VARCHAR); + out.updateObject(i, val.toString()); + } + else if ( "ROUNDTRIPPED".equals(what) ) + { + if ( ! inTypePG.equals(outmd.getColumnTypeName(i)) ) + throw new SQLDataException( + "Result ROUNDTRIPPED column must have same type as input", + "22000"); + out.updateObject(i, val); + } + else + throw new SQLDataException( + "Output column label \""+ what + "\" should be one of: " + + "TYPEPG, TYPEJDBC, CLASSJDBC, CLASS, TOSTRING, VALUE", + "22000"); + } + + return true; + } + + static void assertTypeJDBC(ResultSetMetaData md, int i, int t) + throws SQLException + { + if ( md.getColumnType(i) != t ) + throw new SQLDataException( + "Result column " + i + " must be of JDBC type " + + typeNameJDBC(t)); + } + + static String typeNameJDBC(int t) + { + for ( Field f : Types.class.getFields() ) + { + int m = f.getModifiers(); + if ( isPublic(m) && isStatic(m) && int.class == f.getType() ) + try + { + if ( f.getInt(null) == t ) + return f.getName(); + } + catch ( IllegalAccessException e ) { } + } + return String.valueOf(t); + } +} From c4e162a7f956413e5e2729c9e307570b32319b13 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 26 Jun 2018 00:42:11 -0400 Subject: [PATCH 0110/1087] Correct errors in Timestamp (issue #155). Review the arithmetic to make sure 'floored' / and % are always done to preserve the constraint that the 'nanos' component of the Java Timestamp class is always nonnegative, and to avoid double-counting the part of the fractional second that is represented in both the millis and the nanos components. Add SQLActions in the TypeRoundTripper to test this behavior. In passing, add conditionals for access in MSVC to the session_timezone GUC, for those PG versions where it has become possible, and avoid the work of computing tz in coerceDatum unless it is needed. --- .../example/annotation/TypeRoundTripper.java | 31 ++++++++++++++++++- pljava-so/src/main/c/type/Timestamp.c | 25 ++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index 1ad8640c..aad76cfd 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -25,6 +25,7 @@ import java.sql.SQLDataException; import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; /** * A class to simplify testing of PL/Java's mappings between PostgreSQL and @@ -81,6 +82,33 @@ * roundtrip(p) AS r(roundtripped timestamptz); * */ +@SQLAction(requires = "TypeRoundTripper.roundTrip", install = { +" SELECT "+ +" CASE WHEN every(orig = roundtripped) "+ +" THEN javatest.logmessage('INFO', 'timestamp roundtrip passes') "+ +" ELSE javatest.logmessage('WARNING', 'timestamp roundtrip fails')"+ +" END "+ +" FROM "+ +" (values "+ +" (timestamp '2017-08-21 18:25:29.900005'), "+ +" (timestamp '1970-03-07 17:37:49.300009'), "+ +" (timestamp '1919-05-29 13:08:33.600001') "+ +" ) as p(orig), "+ +" roundtrip(p) as r(roundtripped timestamp) ", + +" SELECT "+ +" CASE WHEN every(orig = roundtripped) "+ +" THEN javatest.logmessage('INFO', 'timestamptz roundtrip passes') "+ +" ELSE javatest.logmessage('WARNING', 'timestamptz roundtrip fails') "+ +" END "+ +" FROM "+ +" (values "+ +" (timestamptz '2017-08-21 18:25:29.900005Z'), "+ +" (timestamptz '1970-03-07 17:37:49.300009Z'), "+ +" (timestamptz '1919-05-29 13:08:33.600001Z') "+ +" ) as p(orig), "+ +" roundtrip(p) as r(roundtripped timestamptz) ", +}) public class TypeRoundTripper { private TypeRoundTripper() { } @@ -102,7 +130,8 @@ private TypeRoundTripper() { } */ @Function( schema = "javatest", - type = "RECORD" + type = "RECORD", + provides = "TypeRoundTripper.roundTrip" ) public static boolean roundTrip(ResultSet in, ResultSet out) throws SQLException diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index f0e638ce..6c43ae71 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -45,15 +45,19 @@ static jvalue Timestamp_coerceDatumTZ_id(Type self, Datum arg, bool tzAdjust) { jvalue result; int64 ts = DatumGetInt64(arg); - int tz = Timestamp_getTimeZone_id(ts); - /* Expect number of microseconds since 01 Jan 2000 + /* Expect number of microseconds since 01 Jan 2000. Separate out a positive + * sub-second microseconds value (whether this C compiler's signed % + * has trunc or floor behavior). */ - jlong mSecs = ts / 1000; /* Convert to millisecs */ - jint uSecs = (jint)(ts % 1000000); /* preserve microsecs */ + jint uSecs = (jint)(((ts % 1000000) + 1000000) % 1000000); + jlong mSecs = (ts - uSecs) / 1000; /* Convert to millisecs */ if(tzAdjust) + { + int tz = Timestamp_getTimeZone_id(ts); mSecs += tz * 1000; /* Adjust from local time to UTC */ + } /* Adjust for diff between Postgres and Java (Unix) */ mSecs += ((jlong)EPOCH_DIFF) * 1000L; @@ -101,6 +105,12 @@ static Datum Timestamp_coerceObjectTZ_id(Type self, jobject jts, bool tzAdjust) int64 ts; jlong mSecs = JNI_callLongMethod(jts, s_Timestamp_getTime); jint nSecs = JNI_callIntMethod(jts, s_Timestamp_getNanos); + /* + * getNanos() should have supplied positive nSecs, whether mSecs is positive + * or negative. So mSecs needs to be floor()ed to a multiple of 1000 ms, + * whether this C compiler does signed integer division with floor or trunc. + */ + mSecs -= ((mSecs % 1000) + 1000) % 1000; mSecs -= ((jlong)EPOCH_DIFF) * 1000L; ts = mSecs * 1000L; /* Convert millisecs to microsecs */ if(nSecs != 0) @@ -167,7 +177,12 @@ static Datum _Timestamptz_coerceObject(Type self, jobject ts) static int32 Timestamp_getTimeZone(pg_time_t time) { -#ifdef _MSC_VER +#if defined(_MSC_VER) && ( \ + 100000<=PG_VERSION_NUM && PG_VERSION_NUM<102000 || \ + 90600<=PG_VERSION_NUM && PG_VERSION_NUM< 90607 || \ + 90500<=PG_VERSION_NUM && PG_VERSION_NUM< 90511 || \ + 90400<=PG_VERSION_NUM && PG_VERSION_NUM< 90416 || \ + PG_VERSION_NUM < 90321 ) /* This is gross, but pg_tzset has a cache, so not as gross as you think. * There is some renewed interest on pgsql-hackers to find a good answer for * the MSVC PGDLLIMPORT nonsense, so this may not have to stay gross. From e1fcce8f7836ecd1c4907ee429e23ad643c6240b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 26 Jun 2018 01:03:50 -0400 Subject: [PATCH 0111/1087] Improve comments. One always pushes to a public repository first, then sees the comments that need improvement. --- pljava-so/src/main/c/type/Timestamp.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 6c43ae71..0d0febe7 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -1,12 +1,16 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2007, 2008, 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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 * - * @author Thomas Hallgren + * Contributors: + * Tada AB + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ #include #include @@ -46,7 +50,7 @@ static jvalue Timestamp_coerceDatumTZ_id(Type self, Datum arg, bool tzAdjust) jvalue result; int64 ts = DatumGetInt64(arg); - /* Expect number of microseconds since 01 Jan 2000. Separate out a positive + /* Expect number of microseconds since 01 Jan 2000. Tease out a non-negative * sub-second microseconds value (whether this C compiler's signed % * has trunc or floor behavior). */ @@ -106,9 +110,10 @@ static Datum Timestamp_coerceObjectTZ_id(Type self, jobject jts, bool tzAdjust) jlong mSecs = JNI_callLongMethod(jts, s_Timestamp_getTime); jint nSecs = JNI_callIntMethod(jts, s_Timestamp_getNanos); /* - * getNanos() should have supplied positive nSecs, whether mSecs is positive - * or negative. So mSecs needs to be floor()ed to a multiple of 1000 ms, - * whether this C compiler does signed integer division with floor or trunc. + * getNanos() should have supplied non-negative nSecs, whether mSecs is + * positive or negative. So mSecs needs to be floor()ed to a multiple of + * 1000 ms, whether this C compiler does signed integer division with floor + * or trunc. */ mSecs -= ((mSecs % 1000) + 1000) % 1000; mSecs -= ((jlong)EPOCH_DIFF) * 1000L; From c4e4c54754f4f7aea5703aaad6bce67e8b2b0758 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 25 Jun 2018 01:35:17 -0400 Subject: [PATCH 0112/1087] Timestamp (w/wo tz) good with + and - values. There may still be particular values to test that could reveal remaining flaws, but with a selection of positive and negative (from the Java epoch) values, and with the corrections to Timestamp.c, successful round trips are now seen. --- .../pljava/example/annotation/S9.java | 234 ++++++++++++++++-- 1 file changed, 210 insertions(+), 24 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java index cf08374a..f77b33b3 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java @@ -23,6 +23,7 @@ import java.sql.ResultSetMetaData; import static java.sql.ResultSetMetaData.columnNoNulls; import java.sql.SQLXML; +import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; @@ -35,12 +36,17 @@ import static java.util.Arrays.asList; import java.util.Collection; import java.util.Collections; +import java.util.GregorianCalendar; +import static java.util.GregorianCalendar.DST_OFFSET; +import static java.util.GregorianCalendar.ZONE_OFFSET; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Properties; +import java.util.TimeZone; +import static java.util.TimeZone.getTimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -84,7 +90,12 @@ import net.sf.saxon.tree.iter.LookaheadIterator; +import net.sf.saxon.type.BuiltInAtomicType; +import net.sf.saxon.type.ValidationFailure; + import net.sf.saxon.value.Base64BinaryValue; +import static net.sf.saxon.value.CalendarValue.NO_TIMEZONE; +import net.sf.saxon.value.DateTimeValue; import net.sf.saxon.value.HexBinaryValue; import org.postgresql.pljava.ResultSetProvider; @@ -500,19 +511,21 @@ public boolean assignRowValues(ResultSet receive, int currentRow) /* * The fully general rules, outlining how x1 gets from here to * the end goal of a value of the SQL result column, are a - * real caution. They begin by pumping x1 through all the - * effects of XMLDOCUMENT( ... RETURNING CONTENT) (text nodes - * built from atomic values, errors raised for attribute nodes, - * everything wrapped in a document node, etc.,) and then - * hitting that on the head with XMLCAST. In the case where the - * SQL result column has a non-XML type, that starts right off - * by document-node unwrapping followed by atomization (you get - * your dog back, you get your truck back, you get your wife - * back, only now as lexical strings with types all erased - * to xs:untypedAtomic), and finally (ring a little bell here) - * putting those through XML Query "cast as" to the XQuery type - * determined according to the output column SQL type, and then - * converting and stashing that value into the result column. + * real caution. Except when the wanted SQL data type is + * XML(SEQUENCE)--which PG doesn't support--they begin by + * pumping x1 through all the effects of XMLDOCUMENT( ... + * RETURNING CONTENT) (text nodes built from atomic values, + * errors raised for attribute nodes, everything wrapped in a + * document node, etc.,) and then hitting that on the head with + * XMLCAST. In the case where the SQL result column has a + * non-XML type, that starts right off by document-node + * unwrapping followed by atomization (you get your house back, + * you get your dog back, you get your truck back, only now as + * lexical strings with types all erased to xs:untypedAtomic), + * and finally (ring a little bell right here) putting those + * through XML Query "cast as" to the XQuery type determined + * according to the output column SQL type, and then converting + * and stashing that value into the result column. * * In cases where the XQuery compiler was statically certain * the result will be exactly one, or zero-or-one, atomic value, @@ -545,6 +558,10 @@ public boolean assignRowValues(ResultSet receive, int currentRow) { throw new SQLException(e.getMessage(), "10000", e); } + catch ( XPathException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } } return true; } @@ -976,6 +993,10 @@ private static XdmValue xmlCastAsSequence( { throw new SQLException(e.getMessage(), "10000", e); } + catch ( XPathException e ) + { + throw new SQLException(e.getMessage(), "10000", e); + } } /** @@ -988,7 +1009,7 @@ private static XdmValue xmlCastAsSequence( */ private static void xmlCastAsNonXML( XdmAtomicValue av, Binding.Parameter p, ResultSet rs, int col) - throws SQLException + throws SQLException, XPathException { XdmAtomicValue bv; ItemType xt = p.typeXS().getItemType(); @@ -1024,10 +1045,10 @@ else if ( ItemType.DOUBLE.subsumes(xt) ) else if ( ItemType.DATE.subsumes(xt) ) // try it, see what goes wrong rs.updateString(col, bv.getStringValue()); - else if ( ItemType.DATE_TIME.subsumes(xt) ) // likewise (no tz case) - rs.updateString(col, bv.getStringValue()); - else if ( ItemType.DATE_TIME_STAMP.subsumes(xt) ) // likewise (tz case) - rs.updateString(col, bv.getStringValue()); + else if ( ItemType.DATE_TIME_STAMP.subsumes(xt) ) + rs.updateObject(col, xdmTimestampToJDBC(bv, true)); + else if ( ItemType.DATE_TIME.subsumes(xt) ) + rs.updateObject(col, xdmTimestampToJDBC(bv, false)); else if ( ItemType.TIME.subsumes(xt) ) // no handy tz/notz distinction rs.updateString(col, bv.getStringValue()); @@ -1048,7 +1069,7 @@ else if ( ItemType.BOOLEAN.subsumes(xt) ) private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( Object dv, XMLBinary encoding, ItemType xst) - throws SQLException, SaxonApiException + throws SQLException, SaxonApiException, XPathException { if ( ItemType.STRING.equals(xst) ) return new XdmAtomicValue((String)dv); @@ -1083,12 +1104,10 @@ private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( return new XdmAtomicValue(((Time)dv).toString(), xst); if ( ItemType.DATE_TIME.equals(xst) ) - return new XdmAtomicValue( // trust me, there's only one space - ((Timestamp)dv).toString().replace(' ', 'T'), xst); + return jdbcTimestampToXdm(dv, false); if ( ItemType.DATE_TIME_STAMP.equals(xst) ) // XXX here too... - return new XdmAtomicValue( - ((Timestamp)dv).toString().replace(' ', 'T'), xst); + return jdbcTimestampToXdm(dv, true); if ( ItemType.DURATION.equals(xst) ) return new XdmAtomicValue(toggleIntervalRepr((String)dv), xst); @@ -1098,6 +1117,153 @@ private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( "0N000"); } + /* + * The current (pre-JSR 310) behavior of PL/Java type mapping is to + * unconditionally return a java.sql.Timestamp instance from getObject, + * for both PostgreSQL types timestamp and timestamptz. + * + * In the timestamptz case, the instance directly reflects what PostgreSQL + * has stored, which is the originally-entered value adjusted to zone Z, + * and the most-faithful way to map that to xdm is as a DATE_TIME_STAMP + * with exactly those properties (there is no information available from + * which its original time zone of entry could be reconstructed). + * + * In the non-tz case, PL/Java has done something you might not expect, + * and adjusted the stored value to Z time by assuming it is a local time + * stored in PostgreSQL's current (as of this moment) session_timezone. + * (It does that because there's no such thing as a "local" + * java.sql.Timestamp. When PL/Java's JDBC implementation catches up to + * JSR 310, that'll provide an alternative that does less violence to the + * principle of least astonishment). + * + * Until then, the game here can only be to get PostgreSQL's + * session_timezone and adjust the JDBC value back! + */ + static XdmAtomicValue jdbcTimestampToXdm(Object v, boolean withTimeZone) + throws SQLException, XPathException + { + Timestamp ts = (Timestamp)v; + long millisFromEpoch = ts.getTime(); + int nanos = ts.getNanos(); + + /* + * The fractional second, down to milliseconds, is redundantly + * represented in millisFromEpoch and the high part of nanos. The nanos + * value is always positive, so needs to be (scaled and) subtracted from + * millisFromEpoch, leaving the latter (which is signed) a multiple of + * 1000, so the / 1000 will not have to round. + */ + long secsFromEpoch = (millisFromEpoch - nanos / 1000000) / 1000; + + DateTimeValue dtv; + + if ( withTimeZone ) + { + dtv = DateTimeValue.fromJavaInstant(secsFromEpoch, nanos); + /* fromJavaInstant implicitly assigns a time zone of Z */ + ValidationFailure vf = + dtv.convertToSubType(BuiltInAtomicType.DATE_TIME_STAMP); + if ( null != vf ) + throw vf.makeException(); + } + else + { + TimeZone tz = getSessionTimeZone(); + int offsetInMillis = tz.getOffset(millisFromEpoch); + secsFromEpoch += offsetInMillis / 1000; + // the following line brought to you by OCD + nanos += 1000000 * (offsetInMillis % 1000); + dtv = DateTimeValue.fromJavaInstant(secsFromEpoch, nanos); + dtv = (DateTimeValue)dtv.removeTimezone(); + } + + return makeAtomicValue(dtv); + } + + /* + * Undo what jdbcTimestampToXdm does, for the same variously astonishing + * reasons. + * + * There is an earlier step of the XMLCAST specification that (once it is + * implemented) will lead to surprising results here if the original xdm + * value being cast was with-time-zone but we are casting it to without. + * In that case, it will have been converted to UTC already before its + * timezone was dropped. The without-time-zone case here has to apply + * another "to-UTC" conversion, in this case from the PG session time zone, + * only because the JDBC driver will reverse that conversion. Such a value + * will end up in UTC, as the SQL/XML spec requires for a value that is + * cast from with time zone to without, and so is not really so surprising + * after all. If the value being cast is originally without time zone, that + * earlier step will not have altered it, and what is done here will be + * reversed by the JDBC driver, leaving the value as it appeared in xdm. + * + * In any case, because of that earlier step (once it's been implemented), + * it can be assumed that the value presented here has hasTimezone() if and + * only if withTimeZone is true. + * + * One wrinkle: because DATE_TIME_STAMP is a subtype of DATE_TIME, the + * current test to short-circuit that casting step can actually send us a + * DATE_TIME_STAMP here when expecting a DATE_TIME (withTimeZone false). + * We'll produce (if asserts are disabled ;) the right result if the thing's + * zone is Z, but not in general, so that special case will have to be + * handled when the earlier cast step gets implemented. + */ + static Object xdmTimestampToJDBC(XdmAtomicValue v, boolean withTimeZone) + throws SQLException, XPathException + { + DateTimeValue dtv = (DateTimeValue)v.getUnderlyingValue(); + assert dtv.hasTimezone() == withTimeZone; + + GregorianCalendar gc; + + if ( withTimeZone ) + { + dtv = dtv.adjustToUTC(NO_TIMEZONE); + gc = dtv.getCalendar(); + } + else + { + gc = dtv.getCalendar(); + gc.setTimeZone(getSessionTimeZone()); + gc.clear(DST_OFFSET); + gc.clear(ZONE_OFFSET); + } + + long millis = gc.getTimeInMillis(); + int micros = dtv.getMicrosecond(); + /* + * The fractional-second part, down to milliseconds, is redundantly + * represented in millis and the high digits of micros. That doesn't + * require special attention, because ts.setNanos() appears to be + * an overwrite. + */ + Timestamp ts = new Timestamp(millis); + ts.setNanos(1000 * micros); + return ts; + } + + /* + * Current PL/Java behavior is *not* to set Java's implicit time zone to + * match the PostgreSQL session timezone ... but to use it implicitly in the + * course of some value conversions. If we need to know it, we need to ask + * for it. For now, do this in the simplest way, and hope that at least the + * TimeZone class is doing some useful caching.... + */ + static TimeZone getSessionTimeZone() throws SQLException + { + Statement s = s_dbc.createStatement(); + try + { + ResultSet rs = s.executeQuery("SHOW TIME ZONE"); + rs.next(); + return getTimeZone(rs.getString(1)); + } + finally + { + s.close(); + } + } + /* * Toggle the lexical representation of an interval/duration between the * form PostgreSQL likes and the form XML Schema likes. Only negative values @@ -1182,7 +1348,27 @@ int typeJDBC() throws SQLException { if ( null != m_typeJDBC ) return m_typeJDBC; - return m_typeJDBC = implTypeJDBC(); + int tj = implTypeJDBC(); + /* + * The JDBC types TIME_WITH_TIMEZONE and TIMESTAMP_WITH_TIMEZONE + * first appear in JDBC 4.2 / Java 8. PL/Java's JDBC driver does + * not yet return those values. As a workaround until it does, + * recheck here using the PG type name string, if TIME or TIMESTAMP + * is the JDBC type that the driver returned. + */ + switch ( tj ) + { + case Types.TIME: + if ( "timetz".equals(typePG()) ) + tj = Types.TIME_WITH_TIMEZONE; + break; + case Types.TIMESTAMP: + if ( "timestamptz".equals(typePG()) ) + tj = Types.TIMESTAMP_WITH_TIMEZONE; + break; + default: + } + return m_typeJDBC = tj; } Object valueJDBC() throws SQLException From af28366a81d1f1733745e301040c361212cc8ad9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 17 Jul 2018 22:58:28 -0400 Subject: [PATCH 0113/1087] Adapt the Parameters example to annotation style. This example checks many of the mappings between SQL and Java parameter and return types, so, as an annotation example, it also exercises those mappings in the SQL generator. It is sufficient to expose issue #157 (indeed, with this commit only, compilation will fail on the inability to map byte[] to bytea). The former, hand-crafted deployment descriptor did not contain a declaration for every public static method in the Parameters class. Those that were not declared then have been left unannotated now. The SQL generator includes parameter names in the function declarations it emits. That makes it possible to use named- parameter notation when calling the functions. However, it has a consequence that parameter names should not be SQL type keywords, as parameter names are optional in declarations so if the name could be a type, PostgreSQL assumes it is, then fails to parse the actual type that follows it. A few Java method parameter names, accordingly, have been changed here. --- .../example/{ => annotation}/Parameters.java | 91 ++++++++++++--- .../main/resources/deployment/examples.ddr | 110 ------------------ 2 files changed, 78 insertions(+), 123 deletions(-) rename pljava-examples/src/main/java/org/postgresql/pljava/example/{ => annotation}/Parameters.java (62%) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/Parameters.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Parameters.java similarity index 62% rename from pljava-examples/src/main/java/org/postgresql/pljava/example/Parameters.java rename to pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Parameters.java index 8919520f..04a52a79 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/Parameters.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Parameters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -8,8 +8,9 @@ * * Contributors: * Tada AB + * Chapman Flack */ -package org.postgresql.pljava.example; +package org.postgresql.pljava.example.annotation; import java.math.BigDecimal; import java.sql.Date; @@ -22,12 +23,33 @@ import java.util.TimeZone; import java.util.logging.Logger; +import org.postgresql.pljava.annotation.Function; +import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; +import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLType; + /** * Some methods used for testing parameter and return value coersion and * resolution of overloaded methods. - * + *

+ * About the {@code @SQLAction} here: the original, hand-crafted deployment + * descriptor declared two SQL functions both implemented by the same + * {@link #getTimestamp() getTimestamp} method here. Only one declaration can be + * automatically generated from a {@code @Function} annotation on the method + * itself. This {@code @SQLAction} takes care of the other declaration. + * Of course, there is now a burden on the author to get this declaration right + * and to keep it up to date if the method evolves, but at least it is here in + * the same file, rather than in a separate hand-maintained DDR file. * @author Thomas Hallgren */ +@SQLAction(install = { + "CREATE OR REPLACE FUNCTION javatest.java_getTimestamptz()" + + " RETURNS timestamptz" + + " AS 'org.postgresql.pljava.example.annotation.Parameters.getTimestamp'" + + " LANGUAGE java" + }, + remove = "DROP FUNCTION javatest.java_getTimestamptz()" +) public class Parameters { public static double addNumbers(short a, int b, long c, BigDecimal d, BigDecimal e, float f, double g) { @@ -38,6 +60,7 @@ public static int addOne(int value) { return value + 1; } + @Function(schema = "javatest", name = "java_addOne", effects = IMMUTABLE) public static int addOne(Integer value) { return value.intValue() + 1; } @@ -46,6 +69,7 @@ public static int addOneLong(long value) { return (int) value + 1; } + @Function(schema = "javatest") public static int countNulls(Integer[] intArray) throws SQLException { int nullCount = 0; int top = intArray.length; @@ -56,6 +80,7 @@ public static int countNulls(Integer[] intArray) throws SQLException { return nullCount; } + @Function(schema = "javatest") public static int countNulls(ResultSet input) throws SQLException { int nullCount = 0; int top = input.getMetaData().getColumnCount(); @@ -75,6 +100,7 @@ public static Time getTime() { return new Time(System.currentTimeMillis()); } + @Function(schema = "javatest", name = "java_getTimestamp") public static Timestamp getTimestamp() { return new Timestamp(System.currentTimeMillis()); } @@ -89,15 +115,30 @@ static void log(String msg) { Logger.getAnonymousLogger().info(msg); } + @Function(schema = "javatest", effects = IMMUTABLE) public static Integer nullOnEven(int value) { return (value % 2) == 0 ? null : new Integer(value); } - public static byte print(byte value) { + /* + * Declare parameter and return type as the PostgreSQL-specific "char" + * (the quoted one, not SQL CHAR) type ... that's how it was declared + * in the original hand-generated deployment descriptor. PL/Java's SQL + * generator would otherwise have emitted smallint by default for the + * Java byte type. + * + * Note that the SQL rules for quoted vs. regular identifiers are complex, + * and PL/Java has not yet precisely specified how the identifiers given in + * annotations are to be treated. A future release may lay down more precise + * rules, which may affect code supplying quoted identifiers like this. + */ + @Function(schema = "javatest", type = "\"char\"") + public static byte print(@SQLType("\"char\"") byte value) { log("byte " + value); return value; } + @Function(schema = "javatest") public static byte[] print(byte[] byteArray) { StringBuffer buf = new StringBuffer(); int top = byteArray.length; @@ -115,19 +156,22 @@ public static byte[] print(byte[] byteArray) { return byteArray; } - public static void print(Date time) { + @Function(schema = "javatest") + public static void print(Date value) { DateFormat p = DateFormat.getDateInstance(DateFormat.FULL); - log("Local Date is " + p.format(time)); + log("Local Date is " + p.format(value)); p.setTimeZone(TimeZone.getTimeZone("UTC")); - log("UTC Date is " + p.format(time)); + log("UTC Date is " + p.format(value)); log("TZ = " + TimeZone.getDefault().getDisplayName()); } + @Function(schema = "javatest") public static double print(double value) { log("double " + value); return value; } + @Function(schema = "javatest") public static double[] print(double[] doubleArray) { StringBuffer buf = new StringBuffer(); int top = doubleArray.length; @@ -145,11 +189,13 @@ public static double[] print(double[] doubleArray) { return doubleArray; } + @Function(schema = "javatest") public static float print(float value) { log("float " + value); return value; } + @Function(schema = "javatest") public static float[] print(float[] floatArray) { StringBuffer buf = new StringBuffer(); int top = floatArray.length; @@ -167,11 +213,13 @@ public static float[] print(float[] floatArray) { return floatArray; } + @Function(schema = "javatest") public static int print(int value) { log("int " + value); return value; } + @Function(schema = "javatest") public static int[] print(int[] intArray) { StringBuffer buf = new StringBuffer(); int top = intArray.length; @@ -189,6 +237,7 @@ public static int[] print(int[] intArray) { return intArray; } + @Function(schema = "javatest", name = "printObj") public static Integer[] print(Integer[] intArray) { StringBuffer buf = new StringBuffer(); int top = intArray.length; @@ -206,11 +255,13 @@ public static Integer[] print(Integer[] intArray) { return intArray; } + @Function(schema = "javatest") public static long print(long value) { log("long " + value); return value; } + @Function(schema = "javatest") public static long[] print(long[] longArray) { StringBuffer buf = new StringBuffer(); int top = longArray.length; @@ -228,11 +279,13 @@ public static long[] print(long[] longArray) { return longArray; } + @Function(schema = "javatest") public static short print(short value) { log("short " + value); return value; } + @Function(schema = "javatest") public static short[] print(short[] shortArray) { StringBuffer buf = new StringBuffer(); int top = shortArray.length; @@ -250,20 +303,32 @@ public static short[] print(short[] shortArray) { return shortArray; } - public static void print(Time time) { + /* + * Declare the parameter type to be timetz in SQL, to match what the + * original hand-crafted deployment descriptor did. The SQL generator + * would otherwise assume time (without time zone). + */ + @Function(schema = "javatest") + public static void print(@SQLType("timetz") Time value) { DateFormat p = new SimpleDateFormat("HH:mm:ss z Z"); - log("Local Time is " + p.format(time)); + log("Local Time is " + p.format(value)); p.setTimeZone(TimeZone.getTimeZone("UTC")); - log("UTC Time is " + p.format(time)); + log("UTC Time is " + p.format(value)); log("TZ = " + TimeZone.getDefault().getDisplayName()); } - public static void print(Timestamp time) { + /* + * Declare the parameter type to be timestamptz in SQL, to match what the + * original hand-crafted deployment descriptor did. The SQL generator + * would otherwise assume timestamp (without time zone). + */ + @Function(schema = "javatest") + public static void print(@SQLType("timestamptz") Timestamp value) { DateFormat p = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); - log("Local Timestamp is " + p.format(time)); + log("Local Timestamp is " + p.format(value)); p.setTimeZone(TimeZone.getTimeZone("UTC")); - log("UTC Timestamp is " + p.format(time)); + log("UTC Timestamp is " + p.format(value)); log("TZ = " + TimeZone.getDefault().getDisplayName()); } } diff --git a/pljava-examples/src/main/resources/deployment/examples.ddr b/pljava-examples/src/main/resources/deployment/examples.ddr index 38ad0449..ff7a464f 100644 --- a/pljava-examples/src/main/resources/deployment/examples.ddr +++ b/pljava-examples/src/main/resources/deployment/examples.ddr @@ -3,106 +3,6 @@ SQLActions[ ] = { CREATE SCHEMA javatest; BEGIN PostgreSQL SET search_path TO javatest,public ENd postgreSQL; - CREATE FUNCTION javatest.java_getTimestamp() - RETURNS timestamp - AS 'org.postgresql.pljava.example.Parameters.getTimestamp' - LANGUAGE java; - - CREATE FUNCTION javatest.java_getTimestamptz() - RETURNS timestamptz - AS 'org.postgresql.pljava.example.Parameters.getTimestamp' - LANGUAGE java; - - CREATE FUNCTION javatest.print(date) - RETURNS void - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(timetz) - RETURNS void - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(timestamptz) - RETURNS void - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print("char") - RETURNS "char" - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(bytea) - RETURNS bytea - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(int2) - RETURNS int2 - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(int2[]) - RETURNS int2[] - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(int4) - RETURNS int4 - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(int4[]) - RETURNS int4[] - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(int8) - RETURNS int8 - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(int8[]) - RETURNS int8[] - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(real) - RETURNS real - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(real[]) - RETURNS real[] - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(double precision) - RETURNS double precision - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.print(double precision[]) - RETURNS double precision[] - AS 'org.postgresql.pljava.example.Parameters.print' - LANGUAGE java; - - CREATE FUNCTION javatest.printObj(int[]) - RETURNS int[] - AS 'org.postgresql.pljava.example.Parameters.print(java.lang.Integer[])' - LANGUAGE java; - - CREATE FUNCTION javatest.java_addOne(int) - RETURNS int - AS 'org.postgresql.pljava.example.Parameters.addOne(java.lang.Integer)' - IMMUTABLE LANGUAGE java; - - CREATE FUNCTION javatest.nullOnEven(int) - RETURNS int - AS 'org.postgresql.pljava.example.Parameters.nullOnEven' - IMMUTABLE LANGUAGE java; - CREATE FUNCTION javatest.java_getSystemProperty(varchar) RETURNS varchar AS 'java.lang.System.getProperty' @@ -343,16 +243,6 @@ SQLActions[ ] = { AS 'org.postgresql.pljava.example.ResultSetTest.executeSelect' LANGUAGE java; - CREATE FUNCTION javatest.countNulls(record) - RETURNS int - AS 'org.postgresql.pljava.example.Parameters.countNulls' - LANGUAGE java; - - CREATE FUNCTION javatest.countNulls(int[]) - RETURNS int - AS 'org.postgresql.pljava.example.Parameters.countNulls(java.lang.Integer[])' - LANGUAGE java; - /* * An example using the ANY type */ From 8adc01b8293b88d30d48666690ecf2e2d9a38519 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Jul 2018 23:18:24 -0400 Subject: [PATCH 0114/1087] This fixes a problem but not the problem. The name passed to getTypeElement is expected to be a canonical name, but that isn't what Class.getName() returns (or there wouldn't be a Class.getCanonicalName()). However, the problem seems to be deeper; arrays are not listed among the things TypeElements exist to represent. It may be necessary to drill down to the component type and then construct the TypeMirror using getArrayType(). --- .../postgresql/pljava/sqlgen/DDRProcessor.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 023dde70..549ffd61 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -2291,12 +2291,19 @@ private void workAroundJava7Breakage() } else { - TypeElement te = - elmu.getTypeElement( k.getName()); - if ( null == te ) // can't find it -> not used in code? + String cname = k.getCanonicalName(); + if ( null == cname ) { msg( Kind.WARNING, - "Found no TypeElement for %s", k.getName()); + "Cannot register type mapping for class %s" + + "that lacks a canonical name", k.getName()); + continue; + } + TypeElement te = elmu.getTypeElement( cname); + if ( null == te ) + { + msg( Kind.WARNING, + "Found no TypeElement for %s", cname); continue; // hope it wasn't one we'll need! } ktm = te.asType(); From 12ec013be94c3b256f2d2d184b3a2979932f83b7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Jul 2018 23:59:30 -0400 Subject: [PATCH 0115/1087] Types.getArrayType() does the trick. --- .../pljava/sqlgen/DDRProcessor.java | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 549ffd61..e541b204 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -2282,38 +2282,47 @@ private void workAroundJava7Breakage() Vertex, String>> v = q.remove(); v.use( q); Class k = v.payload.getKey(); - TypeMirror ktm; - if ( k.isPrimitive() ) - { - TypeKind tk = - TypeKind.valueOf( k.getName().toUpperCase()); - ktm = typu.getPrimitiveType( tk); - } - else - { - String cname = k.getCanonicalName(); - if ( null == cname ) - { - msg( Kind.WARNING, - "Cannot register type mapping for class %s" + - "that lacks a canonical name", k.getName()); - continue; - } - TypeElement te = elmu.getTypeElement( cname); - if ( null == te ) - { - msg( Kind.WARNING, - "Found no TypeElement for %s", cname); - continue; // hope it wasn't one we'll need! - } - ktm = te.asType(); - } + TypeMirror ktm = typeMirrorFromClass( k); + if ( null == ktm ) + continue; // typeMirrorFromClass already emitted warning finalMappings.add( new AbstractMap.SimpleImmutableEntry( ktm, v.payload.getValue())); } } + private TypeMirror typeMirrorFromClass( Class k) + { + if ( k.isArray() ) + { + TypeMirror ctm = typeMirrorFromClass( k.getComponentType()); + return typu.getArrayType( ctm); + } + + if ( k.isPrimitive() ) + { + TypeKind tk = TypeKind.valueOf( k.getName().toUpperCase()); + return typu.getPrimitiveType( tk); + } + + String cname = k.getCanonicalName(); + if ( null == cname ) + { + msg( Kind.WARNING, + "Cannot register type mapping for class %s" + + "that lacks a canonical name", k.getName()); + return null; + } + + TypeElement te = elmu.getTypeElement( cname); + if ( null == te ) + { + msg( Kind.WARNING, "Found no TypeElement for %s", cname); + return null; // hope it wasn't one we'll need! + } + return te.asType(); + } + /** * Add a custom mapping from a Java class to an SQL type. * From 269437b15c358cd9b021439f420d5742f5a6376b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 17 Jul 2018 23:06:55 -0400 Subject: [PATCH 0116/1087] Remove a vestige of GCJ support. --- .../postgresql/pljava/example/annotation/Parameters.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Parameters.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Parameters.java index 04a52a79..51321e57 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Parameters.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Parameters.java @@ -106,13 +106,7 @@ public static Timestamp getTimestamp() { } static void log(String msg) { - // GCJ has a somewhat serious bug (reported) - // - if ("GNU libgcj".equals(System.getProperty("java.vm.name"))) { - System.out.print("INFO: "); - System.out.println(msg); - } else - Logger.getAnonymousLogger().info(msg); + Logger.getAnonymousLogger().info(msg); } @Function(schema = "javatest", effects = IMMUTABLE) From f3f7544bf442c9c5e14d138d45fc4912750094f5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 19 Jul 2018 21:27:42 -0400 Subject: [PATCH 0117/1087] Restore javadoc buildability with Java 8+ Fix a few recently-touched javadoc comments that do not prevent doc building in Java 7, but do under the stricter rules introduced in javadoc in Java 8. --- .../pljava/example/annotation/RecordParameterDefaults.java | 4 ++-- .../pljava/example/annotation/TypeRoundTripper.java | 1 + .../main/java/org/postgresql/pljava/jdbc/SPIConnection.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index cb6055c4..210064b0 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -57,7 +57,7 @@ public class RecordParameterDefaults implements ResultSetProvider * * or as: *

-	 * SELECT (paramDefaultsRecord(params => s)).*
+	 * SELECT (paramDefaultsRecord(params => s)).*
 	 * FROM (SELECT 42 AS a, '42' AS b, 42.0 AS c) AS s;
 	 *
*/ @@ -81,7 +81,7 @@ public static ResultSetProvider paramDefaultsRecord( *
 	 * SELECT paramDefaultsNamedRow();
 	 *
-	 * SELECT paramDefaultsNamedRow(userWithNum => ('fred', 3.14));
+	 * SELECT paramDefaultsNamedRow(userWithNum => ('fred', 3.14));
 	 *
*/ @Function( diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index aad76cfd..ea077cba 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -40,6 +40,7 @@ * requested output column must have its name (case-insensitively) and type * drawn from this table: * + * * * * diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index fb76428c..4b2d0d22 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -28,6 +28,7 @@ import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; // for javadoc link import java.sql.SQLClientInfoException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; From 50e2e95a936f551067770c79ecec600c727f95aa Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 20 Jul 2018 22:06:06 -0400 Subject: [PATCH 0118/1087] Don't repeat yourself with UDT mappings. It was never satisfying to define a UDT in Java through annotations and still have the DDR generator give "no mapping to an SQL type" errors for functions accepting or returning that type. It was easy enough (if tedious) to work around by adding explicit type= or @SQLType annotations, but the DDR generator had all the necessary information to figure that out without such manual help. Now it does, eliminating the tedium illustrated in jcflack/pljava-udt-type-extension@bcb5734. To accommodate mappings added from the source being compiled, the protoMappings collection is now of TypeMirror instances rather than of Class instances. Still future work: also add implied ordering dependencies for uses of the new type (such as functions that have it as parameter or return types and have to follow the type declaration, as illustrated in jcflack/pljava-udt-type-extension@225ef5c, or that are named in it as, for example, typmod in/out functions, and have to precede it). --- .../pljava/sqlgen/DDRProcessor.java | 112 ++++++++++++------ .../example/annotation/ComplexScalar.java | 9 +- .../example/annotation/ComplexTuple.java | 10 +- .../pljava/example/annotation/IntWithMod.java | 9 +- .../pljava/example/annotation/Point.java | 9 +- 5 files changed, 91 insertions(+), 58 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index e541b204..e96440a6 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -675,6 +675,7 @@ void processUDT( Element e, UDTKind k) if ( am.getAnnotationType().asElement().equals( AN_MAPPEDUDT) ) populateAnnotationImpl( mu, e, am); } + mu.registerMapping(); break; } } @@ -1844,6 +1845,9 @@ protected void setQname() qname = _name; else qname = _schema + "." + _name; + + if ( ! tmpr.mappingsFrozen() ) + tmpr.addMap( tclass.asType(), qname); } protected void addComment( ArrayList al) @@ -1875,10 +1879,13 @@ public void setStructure( Object o, boolean explicit, Element e) super( e); } - public boolean characterize() + public void registerMapping() { setQname(); + } + public boolean characterize() + { _requires = augmentRequires( _requires, implementor()); return true; @@ -2166,12 +2173,12 @@ public String[] undeployStrings() */ class TypeMapper { - ArrayList, String>> protoMappings; + ArrayList> protoMappings; ArrayList> finalMappings; TypeMapper() { - protoMappings = new ArrayList, String>>(); + protoMappings = new ArrayList>(); // Primitives // @@ -2208,6 +2215,11 @@ class TypeMapper this.addMap(byte[].class, "bytea"); } + private boolean mappingsFrozen() + { + return null != finalMappings; + } + /* * What worked in Java 6 was to keep a list of Class -> sqltype * mappings, and get TypeMirrors from the Classes at the time of trying @@ -2234,60 +2246,54 @@ class TypeMapper */ private void workAroundJava7Breakage() { - if ( null != finalMappings ) + if ( mappingsFrozen() ) return; // after the first round, it's too late! // Need to check more specific types before those they are // assignable to by widening reference conversions, so a // topological sort is in order. // - List, String>>> vs = - new ArrayList, String>>>( + List>> vs = + new ArrayList>>( protoMappings.size()); - for ( Map.Entry, String> me : protoMappings ) - vs.add( new Vertex, String>>( me)); + for ( Map.Entry me : protoMappings ) + vs.add( new Vertex>( me)); for ( int i = vs.size(); i --> 1; ) { - Vertex, String>> vi = vs.get( i); - Class ci = vi.payload.getKey(); + Vertex> vi = vs.get( i); + TypeMirror ci = vi.payload.getKey(); for ( int j = i; j --> 0; ) { - Vertex, String>> vj = vs.get( j); - Class cj = vj.payload.getKey(); - boolean oij = ci.isAssignableFrom( cj); - boolean oji = cj.isAssignableFrom( ci); + Vertex> vj = vs.get( j); + TypeMirror cj = vj.payload.getKey(); + boolean oij = typu.isAssignable( ci, cj); + boolean oji = typu.isAssignable( cj, ci); if ( oji == oij ) continue; // no precedence constraint between these two if ( oij ) - vj.precede( vi); - else vi.precede( vj); + else + vj.precede( vi); } } - Queue, String>>> q = - new LinkedList, String>>>(); - for ( Vertex, String>> v : vs ) + Queue>> q = + new LinkedList>>(); + for ( Vertex> v : vs ) if ( 0 == v.indegree ) q.add( v); - finalMappings = new ArrayList>( - protoMappings.size()); protoMappings.clear(); + finalMappings = protoMappings; + protoMappings = null; while ( ! q.isEmpty() ) { - Vertex, String>> v = q.remove(); + Vertex> v = q.remove(); v.use( q); - Class k = v.payload.getKey(); - TypeMirror ktm = typeMirrorFromClass( k); - if ( null == ktm ) - continue; // typeMirrorFromClass already emitted warning - finalMappings.add( - new AbstractMap.SimpleImmutableEntry( - ktm, v.payload.getValue())); + finalMappings.add( v.payload); } } @@ -2331,15 +2337,28 @@ private TypeMirror typeMirrorFromClass( Class k) */ void addMap(Class k, String v) { - if ( null != finalMappings ) + addMap( typeMirrorFromClass( k), v); + } + + /** + * Add a custom mapping from a Java class (represented as a TypeMirror) + * to an SQL type. + * + * @param tm TypeMirror representing the Java type + * @param v String representing the SQL type to be used + */ + void addMap(TypeMirror tm, String v) + { + if ( mappingsFrozen() ) { msg( Kind.ERROR, "addMap(%s, %s)\n" + - "called after workAroundJava7Breakage", k.getName(), v); + "called after workAroundJava7Breakage", tm.toString(), v); return; } protoMappings.add( - new AbstractMap.SimpleImmutableEntry, String>( k, v)); + new AbstractMap.SimpleImmutableEntry( tm, v) + ); } /** @@ -2602,10 +2621,12 @@ interface Snippet */ public String[] requires(); /** - * Method to be called on the final round, after all annotations' + * Method to be called after all annotations' * element/value pairs have been filled in, to compute any additional * information derived from those values before deployStrings() or - * undeployStrings() can be called. + * undeployStrings() can be called. May also check for and report semantic + * errors that are not easily checked earlier while populating the + * element/value pairs. * @return true if this Snippet is standalone and should be scheduled and * emitted based on provides/requires; false if something else will emit it. */ @@ -2628,6 +2649,11 @@ class Vertex

int indegree; List> adj; + /** + * Construct a new vertex with the supplied payload, indegree zero, and an + * empty out-adjacency list. + * @param payload Object to be associated with this vertex. + */ Vertex( P payload) { this.payload = payload; @@ -2635,12 +2661,22 @@ class Vertex

adj = new ArrayList>(); } + /** + * Record that this vertex must precede the specified vertex. + * @param v a Vertex that this Vertex must precede. + */ void precede( Vertex

v) { ++ v.indegree; adj.add( v); } + /** + * Record that this vertex has been 'used'. Decrement the indegree of any + * in its adjacency list, and add to the supplied queue any of those whose + * indegree becomes zero. + * @param q A queue of vertices that are ready (have indegree zero). + */ void use( Collection> q) { for ( Vertex

v : adj ) @@ -2648,6 +2684,14 @@ void use( Collection> q) q.add( v); } + /** + * Record that this vertex has been 'used'. Decrement the indegree of any + * in its adjacency list; any of those whose indegree becomes zero should be + * both added to the ready queue {@code q} and removed from the collection + * {@code vs}. + * @param q A queue of vertices that are ready (have indegree zero). + * @param vs A collection of vertices not yet ready. + */ void use( Collection> q, Collection> vs) { for ( Vertex

v : adj ) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index d2a8a45e..215deed4 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -23,7 +23,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLType; import org.postgresql.pljava.annotation.BaseUDT; import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; @@ -51,11 +50,10 @@ public class ComplexScalar implements SQLData { * @param cpl any instance of this UDT * @return the same instance passed in */ - @Function(requires="scalar complex type", type="javatest.complex", + @Function(requires="scalar complex type", schema="javatest", name="logcomplex", effects=IMMUTABLE, onNullInput=RETURNS_NULL) - public static ComplexScalar logAndReturn( - @SQLType("javatest.complex") ComplexScalar cpl) { + public static ComplexScalar logAndReturn(ComplexScalar cpl) { s_logger.info(cpl.getSQLTypeName() + cpl); return cpl; } @@ -71,8 +69,7 @@ public static ComplexScalar logAndReturn( @Function(schema="javatest", requires="scalar complex type", provides="complex assertHasValues", effects=IMMUTABLE, onNullInput=RETURNS_NULL) - public static void assertHasValues( - @SQLType("javatest.complex") ComplexScalar cpl, double re, double im) + public static void assertHasValues(ComplexScalar cpl, double re, double im) throws SQLException { if ( cpl.m_x != re || cpl.m_y != im ) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java index e02dac25..5382a207 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexTuple.java @@ -25,7 +25,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; import static @@ -58,9 +57,8 @@ public class ComplexTuple implements SQLData { */ @Function(schema="javatest", name="logcomplex", effects=IMMUTABLE, onNullInput=RETURNS_NULL, - type="javatest.complextuple", requires="complextuple type") - public static ComplexTuple logAndReturn( - @SQLType("javatest.complextuple") ComplexTuple cpl) { + requires="complextuple type") + public static ComplexTuple logAndReturn(ComplexTuple cpl) { s_logger.info(cpl.getSQLTypeName() + "(" + cpl.m_x + ", " + cpl.m_y + ")"); return cpl; @@ -77,9 +75,7 @@ public static ComplexTuple logAndReturn( @Function(schema="javatest", requires="complextuple type", provides="complextuple assertHasValues", effects=IMMUTABLE, onNullInput=RETURNS_NULL) - public static void assertHasValues( - @SQLType("javatest.complextuple") ComplexTuple cpl, - double re, double im) + public static void assertHasValues(ComplexTuple cpl, double re, double im) throws SQLException { if ( cpl.m_x != re || cpl.m_y != im ) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java index ae02eb8d..51ae33d1 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java @@ -156,11 +156,10 @@ public static String modOut(int mod) throws SQLException { */ @Function(schema="javatest", name="intwithmod_typmodapply", requires="IntWithMod type", provides="IntWithMod modApply", - type="javatest.IntWithMod", effects=IMMUTABLE, onNullInput=RETURNS_NULL) - public static IntWithMod modApply( - @SQLType("javatest.IntWithMod") IntWithMod iwm, - int mod, boolean explicit) throws SQLException { - + effects=IMMUTABLE, onNullInput=RETURNS_NULL) + public static IntWithMod modApply(IntWithMod iwm, int mod, boolean explicit) + throws SQLException + { if ( -1 == mod ) return iwm; if ( (iwm.m_value & 1) != mod ) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Point.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Point.java index bc6d9372..6ef06f27 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Point.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Point.java @@ -21,7 +21,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; import static @@ -46,9 +45,9 @@ public class Point implements SQLData { * @param pt any instance of the type this UDT mirrors * @return the same instance passed in */ - @Function(schema="javatest", type="point", requires="point mirror type", + @Function(schema="javatest", requires="point mirror type", effects=IMMUTABLE, onNullInput=RETURNS_NULL) - public static Point logAndReturn(@SQLType("point") Point pt) { + public static Point logAndReturn(Point pt) { s_logger.info(pt.getSQLTypeName() + pt); return pt; } @@ -64,9 +63,7 @@ public static Point logAndReturn(@SQLType("point") Point pt) { @Function(schema="javatest", requires="point mirror type", provides="point assertHasValues", effects=IMMUTABLE, onNullInput=RETURNS_NULL) - public static void assertHasValues( - @SQLType("point") Point pt, - double x, double y) + public static void assertHasValues(Point pt, double x, double y) throws SQLException { if ( pt.m_x != x || pt.m_y != y ) From 3b67999e1b152132f725f9a3f33c88b237fc3d1a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 23 Jul 2018 21:39:02 -0400 Subject: [PATCH 0119/1087] Use ALLOCSET_*_SIZES macros to create contexts. These macros became available in 9.6 (ea268cd, with a "back-patch" appearing in 9.6.0, funnily enough). It becomes /necessary/ to use them in PG 11 (9fa6f00), at least when using AllocSetContextCreate rather than the PG11-new AllocSetContextCreateExtended. Here, just define the macros locally for PG < 9.6, and use them unconditionally. --- pljava-so/src/main/c/type/JavaWrapper.c | 4 +--- pljava-so/src/main/c/type/Type.c | 4 +--- pljava-so/src/main/include/pljava/pljava.h | 14 ++++++++++++++ src/site/markdown/releasenotes.md.vm | 1 + 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/type/JavaWrapper.c b/pljava-so/src/main/c/type/JavaWrapper.c index 2b0c4358..2300d9a4 100644 --- a/pljava-so/src/main/c/type/JavaWrapper.c +++ b/pljava-so/src/main/c/type/JavaWrapper.c @@ -61,9 +61,7 @@ void JavaWrapper_initialize(void) JavaMemoryContext = AllocSetContextCreate(TopMemoryContext, "PL/Java", - ALLOCSET_DEFAULT_MINSIZE, - ALLOCSET_DEFAULT_INITSIZE, - ALLOCSET_DEFAULT_MAXSIZE); + ALLOCSET_DEFAULT_SIZES); } /* diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 9b12fa3f..ed3c36fe 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -402,9 +402,7 @@ Datum Type_invokeSRF(Type self, jclass cls, jmethodID method, jvalue* args, PG_F ctxData->rowContext = AllocSetContextCreate(context->multi_call_memory_ctx, "PL/Java row context", - ALLOCSET_DEFAULT_MINSIZE, - ALLOCSET_DEFAULT_INITSIZE, - ALLOCSET_DEFAULT_MAXSIZE); + ALLOCSET_DEFAULT_SIZES); /* Register callback to be called when the function ends */ diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index 8ae361b4..e132360a 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -50,6 +50,20 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); #define StaticAssertStmt(condition, errmessage) #endif +/* + * AllocSetContextCreate sprouted these macros for common combinations of + * size parameters in PG 9.6. It becomes /necessary/ to use them in PG 11 + * when using AllocSetContextCreate (which becomes a macro in that version) + * and not the (new in that version) AllocSetContextCreateExtended. + */ +#if PG_VERSION_NUM < 90600 +#define ALLOCSET_DEFAULT_SIZES \ + ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE +#define ALLOCSET_SMALL_SIZES \ + ALLOCSET_SMALL_MINSIZE, ALLOCSET_SMALL_INITSIZE, ALLOCSET_SMALL_MAXSIZE +#define ALLOCSET_START_SMALL_SIZES \ + ALLOCSET_SMALL_MINSIZE, ALLOCSET_SMALL_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE +#endif /* * GETSTRUCT require "access/htup_details.h" to be included in PG9.3 diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index d8513133..f1f4c802 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -167,6 +167,7 @@ $h3 Updated PostgreSQL APIs tracked * `IsBinaryUpgrade` * `SPI_register_trigger_data` * `SPI` without `SPI_push`/`SPI_pop` +* `AllocSetContextCreate` $h2 Earlier releases From fdd2cdd285ff5417b730ef734330990bfc10c48e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 23 Jul 2018 22:26:09 -0400 Subject: [PATCH 0120/1087] Drop GUC_LIST_QUOTE flag on pljava.implementors. It becomes disallowed for extensions to define GUC_LIST_QUOTE variables as of PG 11 (846b5a5). --- pljava-so/src/main/c/Backend.c | 6 +++++- src/site/markdown/releasenotes.md.vm | 1 + src/site/markdown/use/variables.md | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 57a3c87d..2c104adb 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1457,7 +1457,11 @@ static void registerGUCOptions(void) #endif PGC_USERSET, #if PG_VERSION_NUM >= 80400 - GUC_LIST_INPUT | GUC_LIST_QUOTE, + GUC_LIST_INPUT + #if PG_VERSION_NUM < 110000 + | GUC_LIST_QUOTE + #endif + , #endif #if PG_VERSION_NUM >= 90100 NULL, /* check hook */ diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index f1f4c802..4164ec58 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -168,6 +168,7 @@ $h3 Updated PostgreSQL APIs tracked * `SPI_register_trigger_data` * `SPI` without `SPI_push`/`SPI_pop` * `AllocSetContextCreate` +* `DefineCustom...Variable` (no `GUC_LIST_QUOTE` in extensions) $h2 Earlier releases diff --git a/src/site/markdown/use/variables.md b/src/site/markdown/use/variables.md index e404bf8b..08d782b1 100644 --- a/src/site/markdown/use/variables.md +++ b/src/site/markdown/use/variables.md @@ -46,7 +46,11 @@ These PostgreSQL configuration variables can influence PL/Java's operation: only on a system recognizing that name. By default, this list contains only the entry `postgresql`. A deployment descriptor that contains commands with other implementor names can achieve a rudimentary kind of conditional - execution if earlier commands adjust this list of names. + execution if earlier commands adjust this list of names. _Commas separate + elements of this list. Elements that are not regular identifiers need to be + surrounded by double-quotes; prior to PostgreSQL 11, that syntax can be used + directly in a `SET` command, while in 11 and after, such a value needs to be + a (single-quoted) string explicitly containing the double quotes._ `pljava.libjvm_location` : Used by PL/Java to load the Java runtime. The full path to a `libjvm` shared From c58975fc98fbffc8d67db9d8cbc1ebbfa48a9e85 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 25 Jul 2018 02:11:07 -0400 Subject: [PATCH 0121/1087] Back-compat: fix Backend.c for PG < 8.4. Among nits to fix, it pleasantly turns out that GUCs had boot values even pre-8.4. They just had to be assigned in a different way. This both tidies up the code in Backend and improves the experience of setting up PL/Java in the oldest PG versions it supports. --- pljava-so/src/main/c/Backend.c | 177 +++++++++--------- pljava-so/src/main/c/InstallHelper.c | 8 - .../src/main/include/pljava/InstallHelper.h | 9 + 3 files changed, 101 insertions(+), 93 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 2c104adb..47f55cfe 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -727,7 +727,11 @@ static void reLogWithChangedLevel(int level) FreeErrorData(edata); #else if (!errstart(level, edata->filename, edata->lineno, - edata->funcname, NULL)) + edata->funcname +#if PG_VERSION_NUM >= 80400 + , NULL +#endif + )) { FreeErrorData(edata); return; @@ -738,8 +742,10 @@ static void reLogWithChangedLevel(int level) errmsg("%s", edata->message); if (edata->detail) errdetail("%s", edata->detail); +#if PG_VERSION_NUM >= 80400 if (edata->detail_log) errdetail_log("%s", edata->detail_log); +#endif if (edata->hint) errhint("%s", edata->hint); if (edata->context) @@ -1310,122 +1316,125 @@ static jint initializeJavaVM(JVMOptList *optList) return jstat; } +#if PG_VERSION_NUM >= 80400 +#define GUCBOOTVAL(v) (v), +#define GUCBOOTASSIGN(a, v) +#define GUCFLAGS(f) (f), +#else +#define GUCBOOTVAL(v) +#define GUCBOOTASSIGN(a, v) \ + StaticAssertStmt(NULL != (valueAddr), "NULL valueAddr for GUC"); \ + *(a) = (v); +#define GUCFLAGS(f) +#endif + +#if PG_VERSION_NUM >= 90100 +#define GUCCHECK(h) (h), +#else +#define GUCCHECK(h) +#endif + +#define BOOL_GUC(name, short_desc, long_desc, valueAddr, bootValue, context, \ + flags, check_hook, assign_hook, show_hook) \ + GUCBOOTASSIGN((valueAddr), (bootValue)) \ + DefineCustomBoolVariable((name), (short_desc), (long_desc), (valueAddr), \ + GUCBOOTVAL(bootValue) (context), GUCFLAGS(flags) GUCCHECK(check_hook) \ + assign_hook, show_hook) + +#define INT_GUC(name, short_desc, long_desc, valueAddr, bootValue, minValue, \ + maxValue, context, flags, check_hook, assign_hook, show_hook) \ + GUCBOOTASSIGN((valueAddr), (bootValue)) \ + DefineCustomIntVariable((name), (short_desc), (long_desc), (valueAddr), \ + GUCBOOTVAL(bootValue) (minValue), (maxValue), (context), \ + GUCFLAGS(flags) GUCCHECK(check_hook) assign_hook, show_hook) + +#define STRING_GUC(name, short_desc, long_desc, valueAddr, bootValue, context, \ + flags, check_hook, assign_hook, show_hook) \ + GUCBOOTASSIGN((char const **)(valueAddr), (bootValue)) \ + DefineCustomStringVariable((name), (short_desc), (long_desc), (valueAddr), \ + GUCBOOTVAL(bootValue) (context), GUCFLAGS(flags) GUCCHECK(check_hook) \ + assign_hook, show_hook) + static void registerGUCOptions(void) { static char pathbuf[MAXPGPATH]; - DefineCustomStringVariable( + STRING_GUC( "pljava.libjvm_location", "Path to the libjvm (.so, .dll, etc.) file in Java's jre/lib area", NULL, /* extended description */ &libjvmlocation, - #if PG_VERSION_NUM >= 80400 - #ifdef PLJAVA_LIBJVMDEFAULT - CppAsString2(PLJAVA_LIBJVMDEFAULT), - #else - "libjvm", - #endif + #ifdef PLJAVA_LIBJVMDEFAULT + CppAsString2(PLJAVA_LIBJVMDEFAULT), + #else + "libjvm", #endif PGC_SUSET, - #if PG_VERSION_NUM >= 80400 - 0, /* flags */ - #endif - #if PG_VERSION_NUM >= 90100 - check_libjvm_location, - #endif + 0, /* flags */ + check_libjvm_location, assign_libjvm_location, NULL); /* show hook */ - DefineCustomStringVariable( + STRING_GUC( "pljava.vmoptions", "Options sent to the JVM when it is created", NULL, /* extended description */ &vmoptions, - #if PG_VERSION_NUM >= 80400 - NULL, /* boot value */ - #endif + NULL, /* boot value */ PGC_SUSET, - #if PG_VERSION_NUM >= 80400 - 0, /* flags */ - #endif - #if PG_VERSION_NUM >= 90100 - check_vmoptions, - #endif + 0, /* flags */ + check_vmoptions, assign_vmoptions, NULL); /* show hook */ - DefineCustomStringVariable( + STRING_GUC( "pljava.classpath", "Classpath used by the JVM", NULL, /* extended description */ &classpath, - #if PG_VERSION_NUM >= 80400 - InstallHelper_defaultClassPath(pathbuf), /* boot value */ - #endif + InstallHelper_defaultClassPath(pathbuf), /* boot value */ PGC_SUSET, - #if PG_VERSION_NUM >= 80400 - 0, /* flags */ - #endif - #if PG_VERSION_NUM >= 90100 - check_classpath, - #endif + 0, /* flags */ + check_classpath, assign_classpath, NULL); /* show hook */ - DefineCustomBoolVariable( + BOOL_GUC( "pljava.debug", "Stop the backend to attach a debugger", NULL, /* extended description */ &pljavaDebug, - #if PG_VERSION_NUM >= 80400 - false, /* boot value */ - #endif + false, /* boot value */ PGC_USERSET, - #if PG_VERSION_NUM >= 80400 - 0, /* flags */ - #endif - #if PG_VERSION_NUM >= 90100 - NULL, /* check hook */ - #endif + 0, /* flags */ + NULL, /* check hook */ NULL, NULL); /* assign hook, show hook */ - DefineCustomIntVariable( + INT_GUC( "pljava.statement_cache_size", "Size of the prepared statement MRU cache", NULL, /* extended description */ &statementCacheSize, - #if PG_VERSION_NUM >= 80400 - 11, /* boot value */ - #endif + 11, /* boot value */ 0, 512, /* min, max values */ PGC_USERSET, - #if PG_VERSION_NUM >= 80400 - 0, /* flags */ - #endif - #if PG_VERSION_NUM >= 90100 - NULL, /* check hook */ - #endif + 0, /* flags */ + NULL, /* check hook */ NULL, NULL); /* assign hook, show hook */ - DefineCustomBoolVariable( + BOOL_GUC( "pljava.release_lingering_savepoints", "If true, lingering savepoints will be released on function exit. " "If false, they will be rolled back", NULL, /* extended description */ &pljavaReleaseLingeringSavepoints, - #if PG_VERSION_NUM >= 80400 - false, /* boot value */ - #endif + false, /* boot value */ PGC_USERSET, - #if PG_VERSION_NUM >= 80400 - 0, /* flags */ - #endif - #if PG_VERSION_NUM >= 90100 - NULL, /* check hook */ - #endif + 0, /* flags */ + NULL, /* check hook */ NULL, NULL); /* assign hook, show hook */ - DefineCustomBoolVariable( + BOOL_GUC( "pljava.enable", "If off, the Java virtual machine will not be started until set on.", "This is mostly of use on PostgreSQL versions < 9.2, where option " @@ -1434,43 +1443,41 @@ static void registerGUCOptions(void) &pljavaEnabled, #if PG_VERSION_NUM >= 90200 true, /* boot value */ - #elif PG_VERSION_NUM >= 80400 + #else false, /* boot value */ #endif PGC_USERSET, - #if PG_VERSION_NUM >= 80400 - 0, /* flags */ - #endif - #if PG_VERSION_NUM >= 90100 - check_enabled, /* check hook */ - #endif + 0, /* flags */ + check_enabled, /* check hook */ assign_enabled, NULL); /* show hook */ - DefineCustomStringVariable( + STRING_GUC( "pljava.implementors", "Implementor names recognized in deployment descriptors", NULL, /* extended description */ &implementors, - #if PG_VERSION_NUM >= 80400 - "postgresql", /* boot value */ - #endif + "postgresql", /* boot value */ PGC_USERSET, - #if PG_VERSION_NUM >= 80400 - GUC_LIST_INPUT - #if PG_VERSION_NUM < 110000 - | GUC_LIST_QUOTE - #endif - , - #endif - #if PG_VERSION_NUM >= 90100 - NULL, /* check hook */ + GUC_LIST_INPUT + #if PG_VERSION_NUM < 110000 + | GUC_LIST_QUOTE #endif + , + NULL, /* check hook */ NULL, NULL); /* assign hook, show hook */ EmitWarningsOnPlaceholders("pljava"); } +#undef GUCBOOTVAL +#undef GUCBOOTASSIGN +#undef GUCFLAGS +#undef GUCCHECK +#undef BOOL_GUC +#undef INT_GUC +#undef STRING_GUC + static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS); extern PLJAVADLLEXPORT Datum javau_call_handler(PG_FUNCTION_ARGS); diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 8fb6335b..9ec194dd 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -44,14 +44,6 @@ #include "pljava/PgObject.h" #include "pljava/type/String.h" -/* - * CppAsString2 first appears in PG8.4. Once the compatibility target reaches - * 8.4, this fallback will not be needed. - */ -#ifndef CppAsString2 -#define CppAsString2(x) CppAsString(x) -#endif - /* * Before 9.1, there was no creating_extension. Before 9.5, it did not have * PGDLLIMPORT and so was not visible in Windows. In either case, just define diff --git a/pljava-so/src/main/include/pljava/InstallHelper.h b/pljava-so/src/main/include/pljava/InstallHelper.h index efed53fb..c80cb8c8 100644 --- a/pljava-so/src/main/include/pljava/InstallHelper.h +++ b/pljava-so/src/main/include/pljava/InstallHelper.h @@ -18,6 +18,15 @@ * included only in InstallHelper.c and will not clutter most other code. */ +/* + * CppAsString2 first appears in PG8.4. Once the compatibility target reaches + * 8.4, this fallback will not be needed. Used in InstallHelper and Backend, + * both of which include this file. + */ +#ifndef CppAsString2 +#define CppAsString2(x) CppAsString(x) +#endif + /* * The path from which this library is being loaded, which is surprisingly * tricky to find (and wouldn't be, if PostgreSQL called _PG_init functions From 7b3f8746aa21355f2144f9511239ccc92ef3beec Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 25 Jul 2018 22:11:21 -0400 Subject: [PATCH 0122/1087] Make sure InstallHelper_groundwork has a snapshot. The check in pg_plan_query() was added for 8.4 (backpatched to 8.3.6.and 8.2.12), and can prevent loading in some cases (seen in 9.1 and earlier), at least when doing the 'guided' sort of load with interactive setting of GUCs. In passing, fix a plain old typo in Type that has been there for a while, but only is exposed when building for 8.2. --- pljava-so/src/main/c/InstallHelper.c | 31 ++++++++++++++++++++++++++++ pljava-so/src/main/c/type/Type.c | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 9ec194dd..93db6eb5 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -29,6 +29,9 @@ #include #include #include +#if PG_VERSION_NUM >= 80400 +#include +#endif #include #if PG_VERSION_NUM < 90000 @@ -516,8 +519,20 @@ char *InstallHelper_hello() void InstallHelper_groundwork() { Invocation ctx; + bool snapshot_set = false; Invocation_pushInvocation(&ctx, false); ctx.function = Function_INIT_WRITER; +#if PG_VERSION_NUM >= 80400 + if ( ! ActiveSnapshotSet() ) + { + PushActiveSnapshot(GetTransactionSnapshot()); +#else + if ( NULL == ActiveSnapshot ) + { + ActiveSnapshot = CopySnapshot(GetTransactionSnapshot()); +#endif + snapshot_set = true; + } PG_TRY(); { char const *lpt = LOADPATH_TBL_NAME; @@ -535,10 +550,26 @@ void InstallHelper_groundwork() JNI_deleteLocalRef(pljlp); JNI_deleteLocalRef(jlpt); JNI_deleteLocalRef(jlptq); + if ( snapshot_set ) + { +#if PG_VERSION_NUM >= 80400 + PopActiveSnapshot(); +#else + ActiveSnapshot = NULL; +#endif + } Invocation_popInvocation(false); } PG_CATCH(); { + if ( snapshot_set ) + { +#if PG_VERSION_NUM >= 80400 + PopActiveSnapshot(); +#else + ActiveSnapshot = NULL; +#endif + } Invocation_popInvocation(true); PG_RE_THROW(); } diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index ed3c36fe..b6d63e9d 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -46,7 +46,7 @@ static CoercionPathType fcp(Oid targetTypeId, Oid sourceTypeId, CoercionContext ccontext, Oid *funcid) { if ( find_coercion_pathway(targetTypeId, sourceTypeId, ccontext, funcid) ) - return *funcId != InvalidOid ? + return *funcid != InvalidOid ? COERCION_PATH_FUNC : COERCION_PATH_RELABELTYPE; else return COERCION_PATH_NONE; From 36b628dc3d13bc051bf7008aa83293657977d685 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 26 Jul 2018 20:38:04 -0400 Subject: [PATCH 0123/1087] Preserve LOAD usability over supported PG versions In PG versions where CREATE EXTENSION is available, it will surely be the most common installation method, but the method using LOAD should not be allowed to bitrot. It can still be preferable, whether for quick testing purposes, or cases where write access isn't available to the PG installation's libdir and sharedir to install the PL/Java files there. In both the most recent supported PG versions, 10 and later, and the very earliest, 8.2, there are slight differences in the path of things to dereference while finding the LoadStmt to get the library name. In 10 and later, there can be an extra PlannedStmt to unwrap on the way, and 8.2 had a list of query trees on the Portal instead of Stmts. --- pljava-so/src/main/c/InstallHelper.c | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 93db6eb5..e66f636a 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -225,6 +225,11 @@ static void checkLoadPath( bool *livecheck) List *l; Node *ut; LoadStmt *ls; +#if PG_VERSION_NUM >= 80300 + PlannedStmt *ps; +#else + Query *ps; +#endif #ifndef CREATING_EXTENSION_HACK if ( NULL != livecheck ) @@ -232,7 +237,12 @@ static void checkLoadPath( bool *livecheck) #endif if ( NULL == ActivePortal ) return; - l = ActivePortal->stmts; + l = ActivePortal-> +#if PG_VERSION_NUM >= 80300 + stmts; +#else + parseTrees; +#endif if ( NULL == l ) return; if ( 1 < list_length( l) ) @@ -243,6 +253,28 @@ static void checkLoadPath( bool *livecheck) elog(DEBUG2, "got null for first statement from ActivePortal"); return; } +#if PG_VERSION_NUM >= 80300 + if ( T_PlannedStmt == nodeTag(ut) ) + { + ps = (PlannedStmt *)ut; +#else + if ( T_Query == nodeTag(ut) ) + { + ps = (Query *)ut; +#endif + if ( CMD_UTILITY != ps->commandType ) + { + elog(DEBUG2, "ActivePortal has PlannedStmt command type %u", + ps->commandType); + return; + } + ut = ps->utilityStmt; + if ( NULL == ut ) + { + elog(DEBUG2, "got null for utilityStmt from PlannedStmt"); + return; + } + } if ( T_LoadStmt != nodeTag(ut) ) #ifdef CREATING_EXTENSION_HACK if ( T_CreateExtensionStmt == nodeTag(ut) ) From 2ee21e77d25668c8662bfc028fba9a8f808b67b1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 28 Jul 2018 19:54:00 -0400 Subject: [PATCH 0124/1087] Conditionalize examples that depend on PG version. (Also avoid yet another conditional, by replacing a use of the format() function, which appeared in 9.0, with plain string concatenation.) Move the setting of the several version-specific "implementor tags" to one place, ConditionalDDR.java. It gets crazy to have SELECT CASE WHEN ... server_version_num ... proliferating all through the examples. --- .../example/annotation/ConditionalDDR.java | 54 ++++++++++++++++- .../example/annotation/Enumeration.java | 20 +++++-- .../pljava/example/annotation/IntWithMod.java | 11 +++- .../pljava/example/annotation/PGF1010962.java | 12 +++- .../annotation/RecordParameterDefaults.java | 9 ++- .../example/annotation/SetOfRecordTest.java | 7 ++- .../pljava/example/annotation/Triggers.java | 22 ++----- .../example/annotation/TypeRoundTripper.java | 58 ++++++++++--------- .../annotation/UnicodeRoundTripTest.java | 23 ++++---- .../example/annotation/VarlenaUDTTest.java | 5 +- 10 files changed, 155 insertions(+), 66 deletions(-) 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 0d877259..f9efe841 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 @@ -51,6 +51,10 @@ * default implementor PostgreSQL being recognized, probably not what's wanted. * The final {@code true} argument to {@code set_config} makes the setting * local, so it is reverted when the transaction completes. + *

+ * In addition to the goodness-of-life examples, this file also generates + * several statements setting PostgreSQL-version-based implementor tags that + * are relied on by various other examples in this directory. */ @SQLActions({ @SQLAction(provides={"LifeIsGood","LifeIsNotGood"}, install= @@ -69,6 +73,54 @@ @SQLAction(implementor="LifeIsNotGood", install= "SELECT javatest.logmessage('WARNING', 'This should not be executed')" - ) + ), + + @SQLAction(provides="postgresql_ge_80300", install= + "SELECT CASE WHEN" + + " 80300 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_80300,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction(provides="postgresql_ge_80400", install= + "SELECT CASE WHEN" + + " 80400 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_80400,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction(provides="postgresql_ge_90000", install= + "SELECT CASE WHEN" + + " 90000 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_90000,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction(provides="postgresql_ge_90100", install= + "SELECT CASE WHEN" + + " 90100 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_90100,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction(provides="postgresql_ge_90300", install= + "SELECT CASE WHEN" + + " 90300 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_90300,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction(provides="postgresql_ge_100000", install= + "SELECT CASE WHEN" + + " 100000 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_100000,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), }) public class ConditionalDDR { } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java index 17d1fa4c..29805053 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java @@ -22,13 +22,17 @@ /** * Confirms the mapping of PG enum and Java String, and arrays of each, as * parameter and return types. + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. PostgreSQL before 8.3 + * did not have enum types. */ @SQLActions({ - @SQLAction(provides="mood type", + @SQLAction(provides="mood type", implementor="postgresql_ge_80300", install="CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", remove="DROP TYPE mood" ), - @SQLAction( + @SQLAction(implementor="postgresql_ge_80300", requires={"textToMood", "moodToText", "textsToMoods", "moodsToTexts"}, install={ "SELECT textToMood('happy')", @@ -40,22 +44,26 @@ }) public class Enumeration { - @Function(requires="mood type", provides="textToMood", type="mood") + @Function(requires="mood type", provides="textToMood", type="mood", + implementor="postgresql_ge_80300") public static String textToMood(String s) { return s; } - @Function(requires="mood type", provides="moodToText") + @Function(requires="mood type", provides="moodToText", + implementor="postgresql_ge_80300") public static String moodToText(@SQLType("mood")String s) { return s; } - @Function(requires="mood type", provides="textsToMoods", type="mood") + @Function(requires="mood type", provides="textsToMoods", type="mood", + implementor="postgresql_ge_80300") public static Iterator textsToMoods(String[] ss) { return Arrays.asList(ss).iterator(); } - @Function(requires="mood type", provides="moodsToTexts") + @Function(requires="mood type", provides="moodsToTexts", + implementor="postgresql_ge_80300") public static Iterator moodsToTexts(@SQLType("mood[]")String[] ss) { return Arrays.asList(ss).iterator(); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java index 51ae33d1..85921755 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java @@ -51,8 +51,15 @@ *

* Certainly, it would be less tedious with some more annotation support and * autogeneration of the ordering dependencies that are now added by hand here. + *

+ * Most of this must be suppressed (using conditional implementor tags) if the + * PostgreSQL instance is older than 8.3, because it won't have the cstring[] + * type, so the typeModifierInput function can't be declared, and so neither + * can the type, or functions that accept or return it. See the + * {@link ConditionalDDR} example for where the implementor tag is set up. */ @SQLAction(requires={"IntWithMod type", "IntWithMod modApply"}, + implementor="postgresql_ge_80300", remove="DROP CAST (javatest.IntWithMod AS javatest.IntWithMod)", install={ "CREATE CAST (javatest.IntWithMod AS javatest.IntWithMod)" + @@ -64,6 +71,7 @@ } ) @BaseUDT(schema="javatest", provides="IntWithMod type", + implementor="postgresql_ge_80300", requires={"IntWithMod modIn", "IntWithMod modOut"}, typeModifierInput="javatest.intwithmod_typmodin", typeModifierOutput="javatest.intwithmod_typmodout", @@ -121,7 +129,7 @@ public void writeSQL(SQLOutput stream) throws SQLException { * "even" or "odd". The modifier value is 0 for even or 1 for odd. */ @Function(schema="javatest", name="intwithmod_typmodin", - provides="IntWithMod modIn", + provides="IntWithMod modIn", implementor="postgresql_ge_80300", effects=IMMUTABLE, onNullInput=RETURNS_NULL) public static int modIn(@SQLType("cstring[]") String[] toks) throws SQLException { @@ -155,6 +163,7 @@ public static String modOut(int mod) throws SQLException { * Function backing the type-modifier application cast for IntWithMod type. */ @Function(schema="javatest", name="intwithmod_typmodapply", + implementor="postgresql_ge_80300", requires="IntWithMod type", provides="IntWithMod modApply", effects=IMMUTABLE, onNullInput=RETURNS_NULL) public static IntWithMod modApply(IntWithMod iwm, int mod, boolean explicit) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java index b30c8ea7..2b06f481 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java @@ -10,16 +10,23 @@ /** * A gnarly test of TupleDesc reference management, crafted by Johann Oskarsson * for bug report 1010962 on pgFoundry. + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. Before PostgreSQL 8.4, + * there is no array of {@code RECORD}, which this test requires. */ -@SQLAction(requires="1010962 func", +@SQLAction(requires="1010962 func", implementor="postgresql_ge_80400", install={ "CREATE TYPE javatest.B1010962 AS ( b1_val float8, b2_val int)", + "CREATE TYPE javatest.C1010962 AS ( c1_val float8, c2_val float8)", + "CREATE TYPE javatest.A1010962 as (" + " b B1010962," + " c C1010962," + " a_val int" + ")", + "SELECT javatest.complexParam(array_agg(" + " CAST(" + " (" + @@ -44,7 +51,8 @@ public class PGF1010962 * @param receiver Looks polymorphic, but expects an array of A1010962 * @return 0 */ - @Function(schema="javatest", provides="1010962 func") + @Function(schema="javatest", provides="1010962 func", + implementor="postgresql_ge_80400") public static int complexParam( ResultSet receiver[] ) throws SQLException { diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index 210064b0..d4f240ce 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -29,19 +29,22 @@ * function. *

* Also tests the proper DDR generation of defaults for such parameters. + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. */ @SQLActions({ @SQLAction( provides = "paramtypeinfo type", // created in Triggers.java install = { "CREATE TYPE javatest.paramtypeinfo AS (" + - "name text, pgtypename text, javaclass text, tostring text" + + " name text, pgtypename text, javaclass text, tostring text" + ")" }, remove = { "DROP TYPE javatest.paramtypeinfo" } - ), + ) }) public class RecordParameterDefaults implements ResultSetProvider { @@ -64,6 +67,7 @@ public class RecordParameterDefaults implements ResultSetProvider @Function( requires = "paramtypeinfo type", schema = "javatest", + implementor = "postgresql_ge_80400", // supports function param DEFAULTs type = "javatest.paramtypeinfo" ) public static ResultSetProvider paramDefaultsRecord( @@ -86,6 +90,7 @@ public static ResultSetProvider paramDefaultsRecord( */ @Function( requires = "foobar tables", // created in Triggers.java + implementor = "postgresql_ge_80400", // supports function param DEFAULTs schema = "javatest" ) public static String paramDefaultsNamedRow( diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java index 583d4f07..976c8cab 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java @@ -26,8 +26,13 @@ * Example implementing the {@code ResultSetHandle} interface, to return * the {@link ResultSet} from any SQL {@code SELECT} query passed as a string * to the {@link #executeSelect executeSelect} function. + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. Before PostgreSQL 8.4, + * there was no {@code =} or {@code DISTINCT FROM} operator between row types. */ -@SQLAction(requires="selecttorecords fn", install= +@SQLAction(requires="selecttorecords fn", implementor="postgresql_ge_80400", +install= " SELECT " + " CASE WHEN r IS DISTINCT FROM ROW('Foo'::varchar, 1::integer, 1.5::float, " + " 23.67::decimal(8,2), '2005-06-01'::date, '20:56'::time, " + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index 8ab7df80..f950a602 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -36,6 +36,10 @@ * Example creating a couple of tables, and a function to be called when * triggered by insertion into either table. In PostgreSQL 10 or later, * also create a function and trigger that uses transition tables. + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. Constraint triggers + * appear in PG 9.1, transition tables in PG 10. */ @SQLActions({ @SQLAction( @@ -49,20 +53,6 @@ "DROP TABLE javatest.foobar_1" } ), - @SQLAction(provides="postgresql_transitiontables", install= -" select case " + -" when 100000 <= cast(current_setting('server_version_num') as integer) " + -" then set_config('pljava.implementors', 'postgresql_transitiontables,' " + -" || current_setting('pljava.implementors'), true) " + -" end" - ), - @SQLAction(provides="postgresql_constrainttriggers", install= -" select case " + -" when 90100 <= cast(current_setting('server_version_num') as integer) " + -" then set_config('pljava.implementors', 'postgresql_constrainttriggers,' " + -" || current_setting('pljava.implementors'), true) " + -" end" - ), @SQLAction( requires = "constraint triggers", install = "INSERT INTO javatest.foobar_2(value) VALUES (45)" @@ -117,7 +107,7 @@ public static void insertUsername(TriggerData td) * Transition tables first became available in PostgreSQL 10. */ @Function( - implementor = "postgresql_transitiontables", + implementor = "postgresql_ge_100000", requires = "foobar tables", provides = "transition triggers", schema = "javatest", @@ -150,7 +140,7 @@ public static void examineRows(TriggerData td) * Constraint triggers first became available in PostgreSQL 9.1. */ @Function( - implementor = "postgresql_constrainttriggers", + implementor = "postgresql_ge_90100", requires = "foobar tables", provides = "constraint triggers", schema = "javatest", diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index ea077cba..875f2fe4 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -82,34 +82,40 @@ * (VALUES (timestamptz '2017-08-21 18:25:29.900005Z')) AS p(orig), * roundtrip(p) AS r(roundtripped timestamptz); * + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. */ -@SQLAction(requires = "TypeRoundTripper.roundTrip", install = { -" SELECT "+ -" CASE WHEN every(orig = roundtripped) "+ -" THEN javatest.logmessage('INFO', 'timestamp roundtrip passes') "+ -" ELSE javatest.logmessage('WARNING', 'timestamp roundtrip fails')"+ -" END "+ -" FROM "+ -" (values "+ -" (timestamp '2017-08-21 18:25:29.900005'), "+ -" (timestamp '1970-03-07 17:37:49.300009'), "+ -" (timestamp '1919-05-29 13:08:33.600001') "+ -" ) as p(orig), "+ -" roundtrip(p) as r(roundtripped timestamp) ", +@SQLAction(implementor = "postgresql_ge_90300", // funcs see earlier FROM items + requires = "TypeRoundTripper.roundTrip", + install = { + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'timestamp roundtrip passes')" + + " ELSE javatest.logmessage('WARNING', 'timestamp roundtrip fails')" + + " END" + + " FROM" + + " (VALUES" + + " (timestamp '2017-08-21 18:25:29.900005')," + + " (timestamp '1970-03-07 17:37:49.300009')," + + " (timestamp '1919-05-29 13:08:33.600001')" + + " ) AS p(orig)," + + " roundtrip(p) AS r(roundtripped timestamp)", -" SELECT "+ -" CASE WHEN every(orig = roundtripped) "+ -" THEN javatest.logmessage('INFO', 'timestamptz roundtrip passes') "+ -" ELSE javatest.logmessage('WARNING', 'timestamptz roundtrip fails') "+ -" END "+ -" FROM "+ -" (values "+ -" (timestamptz '2017-08-21 18:25:29.900005Z'), "+ -" (timestamptz '1970-03-07 17:37:49.300009Z'), "+ -" (timestamptz '1919-05-29 13:08:33.600001Z') "+ -" ) as p(orig), "+ -" roundtrip(p) as r(roundtripped timestamptz) ", -}) + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'timestamptz roundtrip passes')" + + " ELSE javatest.logmessage('WARNING', 'timestamptz roundtrip fails')" + + " END" + + " FROM" + + " (VALUES" + + " (timestamptz '2017-08-21 18:25:29.900005Z')," + + " (timestamptz '1970-03-07 17:37:49.300009Z')," + + " (timestamptz '1919-05-29 13:08:33.600001Z')" + + " ) AS p(orig)," + + " roundtrip(p) AS r(roundtripped timestamptz)", + } +) public class TypeRoundTripper { private TypeRoundTripper() { } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 1911a760..568d6401 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -35,15 +35,18 @@ * calls this function on each (1k array, 1k string) pair, and counts a failure * if {@code matched} is false or the original and returned arrays or strings * do not match as seen in SQL. + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example, and also sets its own. */ @SQLActions({ - @SQLAction(provides="postgresql_unicodetest", install= -" select case " + -" when 90000 <= cast(current_setting('server_version_num') as integer) " + -" and 'UTF8' = current_setting('server_encoding') " + -" then set_config('pljava.implementors', 'postgresql_unicodetest,' || " + -" current_setting('pljava.implementors'), true) " + -" end" + @SQLAction(provides="postgresql_unicodetest", + implementor="postgresql_ge_90000", install= + "SELECT CASE" + + " WHEN 'UTF8' = current_setting('server_encoding')" + + " THEN set_config('pljava.implementors', 'postgresql_unicodetest,' ||" + + " current_setting('pljava.implementors'), true) " + + "END" ), @SQLAction(requires="unicodetest fn", implementor="postgresql_unicodetest", @@ -77,9 +80,9 @@ " ) " + " select " + " case when n_failing_groups > 0 then " + -" javatest.logmessage('WARNING', format( " + -" '%s 1k codepoint ranges had mismatches, first is block starting 0x%s', " + -" n_failing_groups, to_hex(1024 * first_failing_group))) " + +" javatest.logmessage('WARNING', n_failing_groups || " + +" ' 1k codepoint ranges had mismatches, first is block starting 0x' || " + +" to_hex(1024 * first_failing_group)) " + " else " + " javatest.logmessage('INFO', " + " 'all Unicode codepoint ranges roundtripped successfully.') " + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java index e2526f00..db1cb76a 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java @@ -27,8 +27,11 @@ * characters. That makes it easy to test how big a value gets correctly stored * and retrieved. It should be about a GB, but in issue 52 was failing at 32768 * because of a narrowing assignment in the native code. + *

+ * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. */ -@SQLAction(requires="varlena UDT", install= +@SQLAction(requires="varlena UDT", implementor="postgresql_ge_80300", install= " SELECT CASE v::text = v::javatest.VarlenaUDTTest::text " + " WHEN true THEN javatest.logmessage('INFO', 'works for ' || v) " + " ELSE javatest.logmessage('WARNING', 'fails for ' || v) " + From b1b19c1778cede4c969d0978ee4ac1be912285de Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Jul 2018 00:27:27 -0400 Subject: [PATCH 0125/1087] Emit REMOVE section of DDR in right order (#163). The implicitly-added provides/requires relationships between implementor tags and the snippets that test the environment to activate or suppress them need to be treated specially. Unlike other dependency relationships, which have their sense reversed between the INSTALL and REMOVE action groups, these implicit ones need to keep the same sense. It always works better to test a condition before needing to use the result. The old augmentRequires method, which created the implied dependencies by outright adding them to the snippet's other requires, is now gone; the implied relationship is simply handled within the ordering code. Two DAGs are created, one for install and one for remove, and an order is found independently for each. It's also necessary to use the deployStrings of tag-testing snippets at undeploy time, at least in the case they don't have explicit undeployStrings supplied, which is the usual case, because the conditions to be tested are the same deploying and undeploying. --- .../pljava/sqlgen/DDRProcessor.java | 223 ++++++++++++------ .../postgresql/pljava/sqlgen/DDRWriter.java | 27 ++- 2 files changed, 172 insertions(+), 78 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index e96440a6..1c4dc394 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -340,21 +340,16 @@ void putSnippet( Object o, Snippet s) * generateDescriptor, any errors reported were being shown with no source * location info, because it had been thrown away. */ - Queue> snippetQueue = new LinkedList>(); + List> snippetVPairs = + new ArrayList>(); /** * Map from each arbitrary provides/requires label to the snippet * that 'provides' it. Has to be out here as an instance field for the * same reason {@code snippetQueue} does. */ - Map> provider = - new HashMap>(); - - /** - * Set of provides/requires labels for which at least one consumer has - * been seen. An instance field for the same reason as {@code provider}. - */ - Set consumer = new HashSet(); + Map> provider = + new HashMap>(); /** * Find the elements in each round that carry any of the annotations of @@ -440,13 +435,11 @@ void defensiveEarlyCharacterize() { if ( ! snip.characterize() ) continue; - Vertex v = new Vertex( snip); - snippetQueue.add( v); + VertexPair v = new VertexPair( snip); + snippetVPairs.add( v); for ( String s : snip.provides() ) if ( null != provider.put( s, v) ) msg( Kind.ERROR, "tag %s has more than one provider", s); - for ( String s : snip.requires() ) - consumer.add( s); } snippets.clear(); } @@ -459,23 +452,64 @@ void defensiveEarlyCharacterize() void generateDescriptor() { boolean errorRaised = false; + Set fwdConsumers = new HashSet(); + Set revConsumers = new HashSet(); + + for ( VertexPair v : snippetVPairs ) + { + VertexPair p; - for ( Vertex v : snippetQueue ) - for ( String s : v.payload.requires() ) + /* + * First handle the implicit requires(implementor()). This is unlike + * the typical provides/requires relationship, in that it does not + * reverse when generating the 'remove' actions. Conditions that + * determined what got installed must also be evaluated early and + * determine what gets removed. + */ + String imp = v.payload().implementor(); + if ( null != imp ) { - Vertex p = provider.get( s); + fwdConsumers.add( imp); + revConsumers.add( imp); + + p = provider.get( imp); if ( null != p ) - p.precede( v); - else if ( s == v.payload.implementor() ) // yes == if from impl + { + p.fwd.precede( v.fwd); + p.rev.precede( v.rev); + + /* + * A snippet providing an implementor tag probably has no + * undeployStrings, because its deployStrings should be used + * on both occasions; if so, replace it with a proxy that + * returns deployStrings for undeployStrings. + */ + if ( 0 == p.rev.payload.undeployStrings().length ) + p.rev.payload = new ImpProvider( p.rev.payload); + } + else if ( ! defaultImplementor.equals( imp) ) { /* - * It's the implicit requires(implementor()). Bump the - * indegree anyway so the snippet won't be emitted until - * the cycle breaker code (see below) sets it free after - * any others that can be handled first. + * Don't insist that every implementor tag have a provider + * somewhere in the code. Perhaps the environment will + * provide it at load time. If this is not the default + * implementor, bump the relying vertices' indegree anyway + * so the snippet won't be emitted until the cycle-breaker + * code (see below) sets it free after any others that + * can be handled first. */ - if ( ! defaultImplementor.equals( s) ) - ++ v.indegree; + ++ v.fwd.indegree; + ++ v.rev.indegree; + } + } + for ( String s : v.payload().requires() ) + { + fwdConsumers.add( s); + p = provider.get( s); + if ( null != p ) + { + p.fwd.precede( v.fwd); + v.rev.precede( p.rev); // these relationships do reverse } else { @@ -484,35 +518,75 @@ else if ( s == v.payload.implementor() ) // yes == if from impl errorRaised = true; } } + for ( String s : v.payload().requires() ) + revConsumers.add( s); + } if ( errorRaised ) return; - Snippet[] snips = new Snippet [ snippetQueue.size() ]; + Queue> fwdBlocked = new LinkedList>(); + Queue> revBlocked = new LinkedList>(); + + Queue> fwdReady = new LinkedList>(); + Queue> revReady = new LinkedList>(); Queue> q = new LinkedList>(); - for ( Iterator> it = snippetQueue.iterator() ; - it.hasNext() ; ) + + for ( VertexPair vp : snippetVPairs ) { - Vertex v = it.next(); + Vertex v = vp.fwd; if ( 0 == v.indegree ) - { - q.add( v); - it.remove(); - } + fwdReady.add( v); + else + fwdBlocked.add( v); + v = vp.rev; + if ( 0 == v.indegree ) + revReady.add( v); + else + revBlocked.add( v); } -queuerunning: for ( int i = 0 ; ; ) + Snippet[] fwdSnips = order( fwdReady, fwdBlocked, fwdConsumers); + Snippet[] revSnips = order( revReady, revBlocked, revConsumers); + + if ( null == fwdSnips || null == revSnips ) + return; // error already reported + + try { - while ( ! q.isEmpty() ) + DDRWriter.emit( fwdSnips, revSnips, this); + } + catch ( IOException ioe ) + { + msg( Kind.ERROR, "while writing %s: %s", output, ioe.getMessage()); + } + } + + /** + * Given a Snippet DAG, either the forward or reverse one, return the + * snippets in a workable order. + * @return Array of snippets in order, or null if no suitable order could + * be found. + */ + Snippet[] order( + Queue> ready, Queue> blocked, + Set consumer) + { + Snippet[] snips = new Snippet [ ready.size() + blocked.size() ]; + +queuerunning: + for ( int i = 0 ; ; ) + { + while ( ! ready.isEmpty() ) { - Vertex v = q.remove(); + Vertex v = ready.remove(); snips[i++] = v.payload; - v.use( q, snippetQueue); + v.use( ready, blocked); for ( String p : v.payload.provides() ) consumer.remove(p); } - if ( snippetQueue.isEmpty() ) + if ( blocked.isEmpty() ) break; // all done /* * There are snippets remaining to output but they all have @@ -523,7 +597,7 @@ else if ( s == v.payload.implementor() ) // yes == if from impl * be "providing" that tag anyway), so set one such snippet free * and see how much farther we get. */ - for ( Iterator> it = snippetQueue.iterator(); + for ( Iterator> it = blocked.iterator(); it.hasNext(); ) { Vertex v = it.next(); @@ -533,7 +607,7 @@ else if ( s == v.payload.implementor() ) // yes == if from impl continue; -- v.indegree; it.remove(); - q.add( v); + ready.add( v); continue queuerunning; } /* @@ -541,17 +615,9 @@ else if ( s == v.payload.implementor() ) // yes == if from impl */ for ( String s : consumer ) msg( Kind.ERROR, "requirement in a cycle: %s", s); - return; - } - - try - { - DDRWriter.emit( snips, this); - } - catch ( IOException ioe ) - { - msg( Kind.ERROR, "while writing %s: %s", output, ioe.getMessage()); + return null; } + return snips; } /** @@ -831,19 +897,6 @@ public void setImplementor( Object o, boolean explicit, Element e) _implementor = "".equals( o) ? null : (String)o; } - /** - * Use from characterize() in any subclass implementing Snippet. - */ - protected String[] augmentRequires( String req[], String imp) - { - if ( null == imp ) - return req; - String[] newreq = new String [ 1 + req.length ]; - System.arraycopy( req, 0, newreq, 0, req.length); - newreq[req.length] = imp; - return newreq; - } - public String comment() { return _comment; } public void setComment( Object o, boolean explicit, Element e) @@ -1067,7 +1120,6 @@ class SQLActionImpl public boolean characterize() { - _requires = augmentRequires( _requires, implementor()); return true; } } @@ -1514,8 +1566,6 @@ else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) */ deployStrings(); - _requires = augmentRequires( _requires, implementor()); - for ( Trigger t : triggers() ) ((TriggerImpl)t).characterize(); return true; @@ -1886,8 +1936,6 @@ public void registerMapping() public boolean characterize() { - _requires = augmentRequires( _requires, implementor()); - return true; } @@ -2085,8 +2133,6 @@ public boolean characterize() msg( Kind.ERROR, tclass, "UDT category must be a printable ASCII character"); - _requires = augmentRequires( _requires, implementor()); - return true; } @@ -2702,3 +2748,42 @@ void use( Collection> q, Collection> vs) } } } + +/** + * A pair of Vertex instances for the same payload, for use when two directions + * of topological ordering must be computed. + */ +class VertexPair

+{ + Vertex

fwd; + Vertex

rev; + + VertexPair( P payload) + { + fwd = new Vertex( payload); + rev = new Vertex( payload); + } + + P payload() + { + return rev.payload; + } +} + +/** + * Proxy a snippet that 'provides' an implementor tag and has no + * undeployStrings, returning its deployStrings in their place. + */ +class ImpProvider implements Snippet +{ + Snippet s; + + ImpProvider( Snippet s) { this.s = s; } + + @Override public String implementor() { return s.implementor(); } + @Override public String[] deployStrings() { return s.deployStrings(); } + @Override public String[] undeployStrings() { return s.deployStrings(); } + @Override public String[] provides() { return s.provides(); } + @Override public String[] requires() { return s.requires(); } + @Override public boolean characterize() { return s.characterize(); } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRWriter.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRWriter.java index 02adf67d..7cb06d1d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRWriter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRWriter.java @@ -36,16 +36,25 @@ public class DDRWriter { /** * Generate the deployment descriptor file. + *

+ * Important: it is assumed that {@code fwdSnips} and + * {@code revSnips} contain exactly the same snippets and differ only in + * their ordering. * - * @param snips Code snippets to include in the file, in a workable order - * for the install actions group. The remove actions group will be generated - * by processing this array in reverse order. + * @param fwdSnips Code snippets to include in the file, in a workable order + * for the install actions group. + * @param revSnips The same snippets in a workable order for the remove + * actions group. Not necessarily simply fwdSnips back to front, as the + * implied dependencies on implementor tags have the same sense for both + * install and remove: the tag conditions have to be evaluated before the + * snippets that depend on them. * @param p Reference to the calling object, used to obtain the Filer * object and desired output file name, and for diagnostic messages. */ - static void emit( Snippet[] snips, DDRProcessorImpl p) throws IOException + static void emit( Snippet[] fwdSnips, Snippet[] revSnips, + DDRProcessorImpl p) throws IOException { - if ( ! ensureLexable( snips, p) ) + if ( ! ensureLexable( fwdSnips, p) ) // assume same members as revSnips! return; Writer w = @@ -53,15 +62,15 @@ static void emit( Snippet[] snips, DDRProcessorImpl p) throws IOException w.write( "SQLActions[]={\n\"BEGIN INSTALL\n"); - for ( Snippet snip : snips ) + for ( Snippet snip : fwdSnips ) for ( String s : snip.deployStrings() ) writeCommand( w, s, snip.implementor()); w.write( "END INSTALL\",\n\"BEGIN REMOVE\n"); - for ( int i = snips.length; i --> 0; ) - for ( String s : snips[i].undeployStrings() ) - writeCommand( w, s, snips[i].implementor()); + for ( Snippet snip : revSnips ) + for ( String s : snip.undeployStrings() ) + writeCommand( w, s, snip.implementor()); w.write( "END REMOVE\"\n}\n"); From bd08bd31a62162a13e4d199113998b8dc9ec0c2c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Jul 2018 14:07:46 -0400 Subject: [PATCH 0126/1087] Add PostgreSQL versions to greeting on load. Some troubleshooting exercise somewhere is bound to be simplified by reporting, at load time, the PostgreSQL version that was built against and the version currently running. --- pljava-so/src/main/c/InstallHelper.c | 24 +++++++++++++++++-- .../pljava/internal/InstallHelper.java | 5 +++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index e66f636a..06b79553 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -498,6 +498,10 @@ char *InstallHelper_hello() char pathbuf[MAXPGPATH]; Invocation ctx; jstring nativeVer; + jstring serverBuiltVer; + jstring serverRunningVer; + FunctionCallInfoData fcinfo; + text *runningVer; jstring user; jstring dbname; jstring clustername; @@ -511,6 +515,17 @@ char *InstallHelper_hello() Invocation_pushBootContext(&ctx); nativeVer = String_createJavaStringFromNTS(SO_VERSION_STRING); + serverBuiltVer = String_createJavaStringFromNTS(PG_VERSION_STR); + + InitFunctionCallInfoData(fcinfo, NULL, 0, +#if PG_VERSION_NUM >= 90100 + InvalidOid, /* collation */ +#endif + NULL, NULL); + runningVer = DatumGetTextP(pgsql_version(&fcinfo)); + serverRunningVer = String_createJavaString(runningVer); + pfree(runningVer); + user = String_createJavaStringFromNTS(origUserName()); dbname = String_createJavaStringFromNTS(pljavaDbName()); if ( '\0' == *clusternameC ) @@ -531,9 +546,13 @@ char *InstallHelper_hello() greeting = JNI_callStaticObjectMethod( s_InstallHelper_class, s_InstallHelper_hello, - nativeVer, user, dbname, clustername, ddir, ldir, sdir, edir); + nativeVer, serverBuiltVer, serverRunningVer, + user, dbname, clustername, + ddir, ldir, sdir, edir); JNI_deleteLocalRef(nativeVer); + JNI_deleteLocalRef(serverBuiltVer); + JNI_deleteLocalRef(serverRunningVer); JNI_deleteLocalRef(user); JNI_deleteLocalRef(dbname); if ( NULL != clustername ) @@ -616,7 +635,8 @@ void InstallHelper_initialize() "hello", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" - "Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); + "Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)Ljava/lang/String;"); s_InstallHelper_groundwork = PgObject_getStaticJavaMethod( s_InstallHelper_class, "groundwork", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZ)V"); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 71818a44..a265864c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -45,7 +45,8 @@ private static void setPropertyIfNull( String property, String value) } public static String hello( - String nativeVer, String user, String dbname, String clustername, + String nativeVer, String serverBuiltVer, String serverRunningVer, + String user, String dbname, String clustername, String datadir, String libdir, String sharedir, String etcdir) { String implVersion = @@ -120,6 +121,8 @@ public static String hello( StringBuilder sb = new StringBuilder(); sb.append( "PL/Java native code (").append( nativeVer).append( ")\n"); sb.append( "PL/Java common code (").append( implVersion).append( ")\n"); + sb.append( "Built for (").append( serverBuiltVer).append( ")\n"); + sb.append( "Loaded in (").append( serverRunningVer).append( ")\n"); sb.append( jreName).append( " (").append( jreVer).append( ")\n"); sb.append( vmName).append( " (").append( vmVer); if ( null != vmInfo ) From 55ae99b2b16e4ca32e63aba383c145a05d172f5a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Jul 2018 15:25:36 -0400 Subject: [PATCH 0127/1087] Do not clobber timeouts when loaded (#166). The call to InitializeTimeouts here was probably added based on a quick reading of the code comment in PG's timeout.c, where it says "This must be called in every process that wants to use timeouts." But it has been called already in a backend process (PostgresMain did that), and calling it again here is not necessary or correct. --- pljava-so/src/main/c/Backend.c | 1 - 1 file changed, 1 deletion(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 2c104adb..520cc802 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1084,7 +1084,6 @@ static void _destroyJavaVM(int status, Datum dummy) } #if PG_VERSION_NUM >= 90300 - InitializeTimeouts(); /* establishes SIGALRM handler */ tid = RegisterTimeout(USER_TIMEOUT, terminationTimeoutHandler); #else saveSigAlrm = pqsignal(SIGALRM, terminationTimeoutHandler); From 13a6f387f559edd52a42dc9c69cab57e9ed7fc78 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Jul 2018 17:26:26 -0400 Subject: [PATCH 0128/1087] Tighten some conditional-DDR documentation. The javadocs of the ConditionalDDR example are perhaps the most complete written description of the feature, so keep them up to date. --- .../pljava/example/annotation/ConditionalDDR.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 0d877259..df47ae1b 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 @@ -46,6 +46,18 @@ * placed as late in the generated DDR as other dependencies allow, in case * something in the preceding actions will be setting those implementor tags. *

+ * 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. + *

+ * An {@code SQLAction} setting an implementor tag 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 * default implementor PostgreSQL being recognized, probably not what's wanted. From 2e11fc415ea8a17a008bddd4161d61fedeade140 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 24 Jul 2018 22:43:40 -0400 Subject: [PATCH 0129/1087] Back-compat: let tweaks for old PG versions begin. Before PG 9.5, "expanded" TOASTed forms were not a thing. Before 9.2, there was no MemoryContextSetParent, and before 8.3, no VARSIZE_ANY_EXHDR or SET_VARSIZE. --- pljava-so/src/main/c/VarlenaWrapper.c | 79 +++++++++++++++++++++++++- pljava-so/src/main/c/type/SQLXMLImpl.c | 7 +++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index d6f9bcd4..69dea01a 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -18,6 +18,11 @@ #include "pljava/PgObject.h" #include "pljava/JNICalls.h" +#if PG_VERSION_NUM < 80300 +#define VARSIZE_ANY_EXHDR(PTR) (VARSIZE(PTR) - VARHDRSZ) +#define SET_VARSIZE(PTR, len) VARATT_SIZEP(PTR) = len & VARATT_MASK_SIZE +#endif + #define INITIALSIZE 1024 static jclass s_VarlenaWrapper_class; @@ -40,6 +45,37 @@ static jmethodID s_VarlenaWrapper_Output_init; * form, it will use these 'methods' to flatten it, and that's when the one * final reallocation and copy will happen. */ + +#if PG_VERSION_NUM < 90500 +/* + * There aren't 'expanded' varlenas yet. Copy some defs (in simplified form) + * and pretend there are. + */ +typedef struct ExpandedObjectHeader ExpandedObjectHeader; + +typedef Size (*EOM_get_flat_size_method) (ExpandedObjectHeader *eohptr); +typedef void (*EOM_flatten_into_method) (ExpandedObjectHeader *eohptr, + void *result, Size allocated_size); + +typedef struct ExpandedObjectMethods +{ + EOM_get_flat_size_method get_flat_size; + EOM_flatten_into_method flatten_into; +} ExpandedObjectMethods; + +struct ExpandedObjectHeader +{ + int32 magic; + MemoryContext eoh_context; +}; + +#define EOH_init_header(eohptr, methods, obj_context) \ + do {(eohptr)->magic = -1; (eohptr)->eoh_context = (obj_context);} while (0) + +#define EOHPGetRWDatum(eohptr) (eohptr) +#define DatumGetEOHP(d) (d) +#endif + static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr); static void VOS_flatten_into(ExpandedObjectHeader *eohptr, void *result, Size allocated_size); @@ -180,15 +216,34 @@ jobject pljava_VarlenaWrapper_Output(MemoryContext parent, ResourceOwner ro) /* * Adopt a VarlenaWrapper (if Output, after Java code has written and closed it) - * and leave it no longer accessible from Java. + * and leave it no longer accessible from Java. It may be an 'expanded' datum, + * in PG 9.5+ where there are such things. Otherwise, it will be an ordinary + * flat one (the ersatz 'expanded' form used internally here then being only an + * implementation detail, not exposed to the caller); its memory context is + * unchanged. */ Datum pljava_VarlenaWrapper_adopt(jobject vlw) { Ptr2Long p2l; +#if PG_VERSION_NUM < 90500 + ExpandedObjectHeader *eohptr; + Size final_size; + void *final_result; +#endif p2l.longVal = JNI_callLongMethodLocked(vlw, s_VarlenaWrapper_adopt, pljava_DualState_key()); +#if PG_VERSION_NUM >= 90500 return PointerGetDatum(p2l.ptrVal); +#else + eohptr = p2l.ptrVal; + if ( -1 != eohptr->magic ) + return PointerGetDatum(eohptr); + final_size = VOS_get_flat_size(eohptr); + final_result = MemoryContextAlloc(eohptr->eoh_context, final_size); + VOS_flatten_into(eohptr, final_result, final_size); + return PointerGetDatum(final_result); +#endif } static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr) @@ -204,6 +259,9 @@ static void VOS_flatten_into(ExpandedObjectHeader *eohptr, ExpandedVarlenaOutputStreamHeader *evosh = (ExpandedVarlenaOutputStreamHeader *)eohptr; ExpandedVarlenaOutputStreamNode *node = evosh->tail; +#if PG_VERSION_NUM < 90500 + ExpandedVarlenaOutputStreamNode *next; +#endif Assert(allocated_size == evosh->total_size); SET_VARSIZE(result, allocated_size); @@ -216,6 +274,25 @@ static void VOS_flatten_into(ExpandedObjectHeader *eohptr, result = (char *)result + node->size; } while ( node != evosh->tail ); + +#if PG_VERSION_NUM < 90500 + /* + * It's been flattened into the same context; the original nodes can be + * freed so the 2x memory usage doesn't last longer than necessary. Freeing + * them retail isn't ideal, but this is back-compatibility code. Remember + * the first one wasn't a separate allocation. + */ + node = node->next; /* this is the head, the one that can't be pfreed */ + evosh->tail = node; /* tail is now head, the non-pfreeable node */ + node = node->next; + while ( node != evosh->tail ) + { + next = node->next; + pfree(node); + node = next; + } + pfree(evosh); +#endif } void pljava_VarlenaWrapper_initialize(void) diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 2ea090c0..daf03406 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -39,10 +39,17 @@ static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) jobject vw = JNI_callObjectMethodLocked(sqlxml, s_SQLXML_adopt); Datum d = pljava_VarlenaWrapper_adopt(vw); JNI_deleteLocalRef(vw); +#if PG_VERSION_NUM >= 90500 if ( VARATT_IS_EXTERNAL_EXPANDED_RW(DatumGetPointer(d)) ) return TransferExpandedObject(d, CurrentMemoryContext); +#endif +#if PG_VERSION_NUM >= 90200 MemoryContextSetParent( GetMemoryChunkContext(DatumGetPointer(d)), CurrentMemoryContext); +#else + if ( CurrentMemoryContext != GetMemoryChunkContext(DatumGetPointer(d)) ) + d = PointerGetDatum(PG_DETOAST_DATUM_COPY(d)); +#endif return d; } From f1526584c5fd8a7d74917995024263b7084e23a7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 26 Jul 2018 22:56:36 -0400 Subject: [PATCH 0130/1087] Let PL/Java build where PG lacks XML type. In Java, SQLXML will still be available, and can even be used as function parameter and return types, as long as these are mapped in the function declaration to the PostgreSQL type text, as the type xml will not be present). --- .../pljava/example/annotation/PassXML.java | 24 ++++++++++++----- pljava-so/src/main/c/type/Oid.c | 4 +++ pljava-so/src/main/c/type/SQLXMLImpl.c | 27 ++++++++++++++++++- 3 files changed, 48 insertions(+), 7 deletions(-) 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 f6fd45aa..5effc1a1 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 @@ -59,6 +59,7 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; +import org.postgresql.pljava.annotation.SQLAction; import static org.postgresql.pljava.example.LoggerTest.logMessage; @@ -68,8 +69,18 @@ * This class also serves as the mapping class for a composite type * {@code javatest.onexml}, the better to verify that {@link SQLData} * input/output works too. That's why it has to implement SQLData. + *

+ * Everything mentioning the type XML here needs a conditional implementor tag + * in case of being loaded into a PostgreSQL instance built without that type. */ +@SQLAction(provides="postgresql_xml", install= + "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + + " THEN set_config('pljava.implementors', 'postgresql_xml,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) @MappedUDT(schema="javatest", name="onexml", structure="c1 xml", + implementor="postgresql_xml", comment="A composite type mapped by the PassXML example class") public class PassXML implements SQLData { @@ -88,7 +99,7 @@ public class PassXML implements SQLData * read in a subsequent call with sx => null, but only in the same * transaction. */ - @Function(schema="javatest") + @Function(schema="javatest", implementor="postgresql_xml") public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) throws SQLException { @@ -106,7 +117,7 @@ public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) * "Echo" an XML parameter not by creating a new writable {@code SQLXML} * object at all, but simply returning the passed-in readable one untouched. */ - @Function(schema="javatest") + @Function(schema="javatest", implementor="postgresql_xml") public static SQLXML bounceXMLParameter(SQLXML sx) throws SQLException { return sx; @@ -124,7 +135,8 @@ public static SQLXML bounceXMLParameter(SQLXML sx) throws SQLException * {@link Function.Trust.UNSANDBOXED Trust.UNSANDBOXED}, at least for the * XSLTC transform compiler in newer JREs. */ - @Function(schema="javatest", trust=Function.Trust.UNSANDBOXED) + @Function(schema="javatest", trust=Function.Trust.UNSANDBOXED, + implementor="postgresql_xml") public static void prepareXMLTransform(String name, SQLXML source, int how) throws SQLException { @@ -142,7 +154,7 @@ public static void prepareXMLTransform(String name, SQLXML source, int how) * Transform some XML according to a named transform prepared with * {@code prepareXMLTransform}. */ - @Function(schema="javatest") + @Function(schema="javatest", implementor="postgresql_xml") public static SQLXML transformXML( String transformName, SQLXML source, int howin, int howout) throws SQLException @@ -197,7 +209,7 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) * specific case, all the generality of {@code transform} may not be needed. * It can be interesting to compare memory use when XML values are large. */ - @Function(schema="javatest") + @Function(schema="javatest", implementor="postgresql_xml") public static SQLXML lowLevelXMLEcho(SQLXML sx, int how) throws SQLException { @@ -490,7 +502,7 @@ public void writeSQL(SQLOutput stream) throws SQLException /* * Test the MappedUDT (in one direction anyway). */ - @Function(schema="javatest") + @Function(schema="javatest", implementor="postgresql_xml") public static SQLXML xmlFromComposite() throws SQLException { Connection c = DriverManager.getConnection("jdbc:default:connection"); diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index 2ef951c0..ce7cad11 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -115,7 +115,11 @@ Oid Oid_forSqlType(int sqlType) /* JDBC 4.0 - present in Java 6 and later, no need to conditionalize */ case java_sql_Types_SQLXML: +#ifdef XMLOID /* but PG can have been built without libxml */ typeId = XMLOID; +#else + typeId = InvalidOid; +#endif break; case java_sql_Types_ROWID: case java_sql_Types_NCHAR: diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index daf03406..720166c7 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -23,6 +23,21 @@ static jmethodID s_SQLXML_Readable_init; static jclass s_SQLXML_Writable_class; static jmethodID s_SQLXML_Writable_init; +/* + * It is possible to install PL/Java in a PostgreSQL instance that was built + * without libxml and the native XML data type. It could even be useful for + * SQLXML to be usable in those circumstances, so the canReplaceType method + * will return true if the native type is text. (An exact match on TEXTOID is + * required, for now at least, because over in String.c, canReplaceType answers + * true for any native type that has text in/out conversions, and we do NOT want + * SQLXML to willy/nilly expose the internals of just any of those. + */ +static bool _SQLXML_canReplaceType(Type self, Type other) +{ + TypeClass cls = Type_getClass(other); + return Type_getClass(self) == cls || Type_getOid(other) == TEXTOID; +} + static jvalue _SQLXML_coerceDatum(Type self, Datum arg) { jvalue result; @@ -72,9 +87,19 @@ void pljava_SQLXMLImpl_initialize(void) TypeClass cls = TypeClass_alloc("type.SQLXML"); cls->JNISignature = "Ljava/sql/SQLXML;"; cls->javaTypeName = "java.sql.SQLXML"; + cls->canReplaceType = _SQLXML_canReplaceType; cls->coerceDatum = _SQLXML_coerceDatum; cls->coerceObject = _SQLXML_coerceObject; - Type_registerType("java.sql.SQLXML", TypeClass_allocInstance(cls, XMLOID)); + Type_registerType( + "java.sql.SQLXML", + TypeClass_allocInstance( + cls, +#ifdef XMLOID /* it is possible to build PG without libxml */ + XMLOID +#else + InvalidOid +#endif + )); s_SQLXML_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl")); From 3632b76457659dd8bdb7ca8d70d7b7132e8a63da Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 30 Jul 2018 02:03:26 -0400 Subject: [PATCH 0131/1087] Include return types in most DDR method signatures The 'AS' syntax for PL/Java function declarations already accepts explicit return types as well as the parameter types, but the DDR generator has not been emitting them. That means the magic doesn't happen when, say, you have a Java method returning SQLXML but give it type="text" in the @Function annotation. For PL/Java to notice the automatic coercion needed there, it has to see that the Java return type is not the one that naturally corresponds to the SQL type ... so the Java return type needs to be included in the signature. Avoid, however, including the return type in the case of trigger functions, or those that return sets or composite types. Those already get special treatment in the function parsing code, and the explicit return types would get in the way. --- .../pljava/sqlgen/DDRProcessor.java | 2 ++ .../pljava/example/annotation/PassXML.java | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index b11c5297..e8207bec 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1613,6 +1613,8 @@ void appendParams( StringBuilder sb, boolean dflts) void appendAS( StringBuilder sb) { + if ( ! ( complexViaInOut || setof || trigger ) ) + sb.append( func.getReturnType()).append( '='); Element e = func.getEnclosingElement(); if ( ! e.getKind().equals( ElementKind.CLASS) ) msg( Kind.ERROR, func, 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 5effc1a1..fe6d483e 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 @@ -60,6 +60,7 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.example.LoggerTest.logMessage; @@ -113,6 +114,32 @@ public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) return echoSQLXML(sx, howin, howout); } + /** + * Echo an XML parameter back, but with parameter and return types of + * PostgreSQL {@code text}. + *

+ * The other version of this method needs a conditional implementor tag + * because it cannot be declared in a PostgreSQL instance that was built + * without [@code libxml} support and the PostgreSQL {@code XML} type. + * But this version can, simply by mapping the {@code SQLXML} parameter + * and return types to the SQL {@code text} type. The Java code is no + * different. + *

+ * Note that it's possible for both declarations to coexist in PostgreSQL + * (because as far as it is concerned, their signatures are different), but + * these two Java methods cannot have the same name (because they differ + * only in annotations, not in the declared Java types). So, this one needs + * a slightly tweaked name, and a {@code name} attribute in the annotation + * so PostgreSQL sees the right name. + */ + @Function(schema="javatest", name="echoXMLParameter", type="text") + public static SQLXML echoXMLParameter_( + @SQLType("text") SQLXML sx, int howin, int howout) + throws SQLException + { + return echoXMLParameter(sx, howin, howout); + } + /** * "Echo" an XML parameter not by creating a new writable {@code SQLXML} * object at all, but simply returning the passed-in readable one untouched. From af22f60ef6f7b8b40a85d597cbe078d0b1369942 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 29 Jul 2018 23:35:35 -0400 Subject: [PATCH 0132/1087] Javadoc 8 nits, and other tidying. Flesh out the existing toString methods to produce more useful information for debugging. --- .../pljava/example/annotation/PassXML.java | 68 +++++++++++-------- .../postgresql/pljava/internal/DualState.java | 56 ++++++++++++--- .../pljava/internal/VarlenaWrapper.java | 65 ++++++++++++++++-- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 53 +++++++++++++++ 4 files changed, 202 insertions(+), 40 deletions(-) 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 fe6d483e..9c6d9be7 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 @@ -93,11 +93,11 @@ public class PassXML implements SQLData /** * Echo an XML parameter back, exercising seven different ways - * (howin => 1-7) of reading an SQLXML object, and seven (howout => 1-7) - * of returning one. + * (howin => 1-7) of reading an SQLXML object, and seven + * (howout => 1-7) of returning one. *

- * If howin => 0, the XML parameter is simply saved in a static. It can be - * read in a subsequent call with sx => null, but only in the same + * If howin => 0, the XML parameter is simply saved in a static. It can + * be read in a subsequent call with sx => null, but only in the same * transaction. */ @Function(schema="javatest", implementor="postgresql_xml") @@ -158,8 +158,8 @@ public static SQLXML bounceXMLParameter(SQLXML sx) throws SQLException * the {@code SQLXML} object to the XSL processor. *

* Preparing a transform with - * {@link TransformerFactoory.newTemplates newTemplates()} seems to require - * {@link Function.Trust.UNSANDBOXED Trust.UNSANDBOXED}, at least for the + * {@link TransformerFactory#newTemplates newTemplates()} seems to require + * {@link Function.Trust#UNSANDBOXED Trust.UNSANDBOXED}, at least for the * XSLTC transform compiler in newer JREs. */ @Function(schema="javatest", trust=Function.Trust.UNSANDBOXED, @@ -502,9 +502,43 @@ private static void shovelChars(Reader r, Writer w) w.close(); } + /** + * Test the MappedUDT (in one direction anyway). + *

+ * Creates a {@code PassXML} object, the Java class that maps the + * {@code javatest.onexml} composite type, which has one member, of XML + * type. Stores a {@code SQLXML} value in that field of the {@code PassXML} + * object, and passes that to an SQL query that expects and returns + * {@code javatest.onexml}. Retrieves the XML from the value field of the + * {@code PassXML} object created to map the result of the query. + * @return The original XML value, if all goes well. + */ + @Function(schema="javatest", implementor="postgresql_xml") + public static SQLXML xmlFromComposite() throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + PreparedStatement ps = + c.prepareStatement("SELECT CAST(? AS javatest.onexml)"); + SQLXML x = c.createSQLXML(); + x.setString(""); + PassXML obj = new PassXML(); + obj.m_value = x; + obj.m_typeName = "javatest.onexml"; + ps.setObject(1, obj); + ResultSet r = ps.executeQuery(); + r.next(); + obj = r.getObject(1, PassXML.class); + ps.close(); + return obj.m_value; + } + /* * Required to serve as a MappedUDT: */ + /** + * No-arg constructor required of objects that will implement + * {@link SQLData}. + */ public PassXML() { } private String m_typeName; @@ -525,26 +559,4 @@ public void writeSQL(SQLOutput stream) throws SQLException { stream.writeSQLXML(m_value); } - - /* - * Test the MappedUDT (in one direction anyway). - */ - @Function(schema="javatest", implementor="postgresql_xml") - public static SQLXML xmlFromComposite() throws SQLException - { - Connection c = DriverManager.getConnection("jdbc:default:connection"); - PreparedStatement ps = - c.prepareStatement("SELECT CAST(? AS javatest.onexml)"); - SQLXML x = c.createSQLXML(); - x.setString(""); - PassXML obj = new PassXML(); - obj.m_value = x; - obj.m_typeName = "javatest.onexml"; - ps.setObject(1, obj); - ResultSet r = ps.executeQuery(); - r.next(); - obj = r.getObject(1, PassXML.class); - ps.close(); - return obj.m_value; - } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index a2d776cd..8c23980c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -103,6 +103,9 @@ public abstract class DualState extends WeakReference */ private final long m_resourceOwner; + /** + * Check that a cookie is valid, throwing an unchecked exception otherwise. + */ protected static void checkCookie(Key cookie) { if ( ! Key.class.isInstance(cookie) ) @@ -182,7 +185,7 @@ protected void javaStateUnreachable() *

  • A {@code close} or similar method simply calls * {@link #enqueue enqueue} instead of this method. This method will be * called when the queue is processed, the next time native code calls - * {@link #clearEnqueuedInstances clearEnqueuedInstances}. For that case, + * {@link #cleanEnqueuedInstances cleanEnqueuedInstances}. For that case, * this method should be overridden to do whatever other cleanup is in * order, but not remove the instance from {@code liveInstances}, * which will have happened just before this method is called. @@ -236,10 +239,47 @@ public void assertNativeStateIsValid() } } + /** + * Produce a string describing this state object in a way useful for + * debugging, with such information as the associated {@code ResourceOwner} + * and whether the state is fresh or stale. + *

    + * This method calls {@link #toString(Object)} passing {@code this}. + * Subclasses are encouraged to override that method with versions that add + * subclass-specific details. + * @return Description of this state object. + */ @Override public String toString() { - return String.format("DualState owner:%x %s", m_resourceOwner, + return toString(this); + } + + /** + * Produce a string with such details of this object as might be useful for + * debugging, starting with an abbreviated form of the class name of the + * supplied object. + *

    + * Subclasses are encouraged to override this and then call it, via super, + * passing the object unchanged, and then append additional + * subclass-specific details to the result. + *

    + * Because the recursion ends here, this one actually does construct the + * abbreviated form of the class name of the object, and use it at the start + * of the returned string. + * @param o An object whose class name (abbreviated by stripping the package + * prefix) will be used at the start of the string. Passing {@code null} is + * the same as passing {@code this}. + * @return Description of this state object, prefixed with the abbreviated + * class name of the passed object. + */ + public String toString(Object o) + { + Class c = (null == o ? this : o).getClass(); + String cn = c.getCanonicalName(); + int pnl = c.getPackage().getName().length(); + return String.format("%s owner:%x %s", + cn.substring(1 + pnl), m_resourceOwner, nativeStateIsValid() ? "fresh" : "stale"); } @@ -330,9 +370,9 @@ protected SinglePfree( } @Override - public String toString() + public String toString(Object o) { - return String.format("%s pfree(%x)", super.toString(), m_pointer); + return String.format("%s pfree(%x)", super.toString(o), m_pointer); } /** @@ -410,7 +450,7 @@ protected long getPointer() throws SQLException * A {@code DualState} subclass whose only native resource releasing action * needed is {@code MemoryContextDelete} of a single context. *

    - * This class may get called at the {@code nativeStateReleased) entry, not + * This class may get called at the {@code nativeStateReleased} entry, not * only if the native state is actually being released, but if it is being * 'claimed' by native code for its own purposes. The effect is the same * as far as Java is concerned; the object is no longer accessible, and the @@ -428,10 +468,10 @@ protected SingleMemContextDelete( } @Override - public String toString() + public String toString(Object o) { - return String.format("%s MemoryContextDelete(%x)", super.toString(), - m_context); + return String.format("%s MemoryContextDelete(%x)", + super.toString(o), m_context); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 1a8f6ca0..605da63f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -27,7 +27,7 @@ * {@code InputStream}, or allow a new one to be constructed by presenting a * writable {@code OutputStream}. *

    - * Common to both is a single method {@link #adopt adopt()}, allowing native + * Common to both is a method {@link #adopt adopt()}, allowing native * code to reassert control over the varlena (for the writable variety, after * Java code has written and closed it), after which it is no longer accessible * from Java. @@ -41,6 +41,25 @@ public interface VarlenaWrapper extends Closeable */ long adopt(DualState.Key cookie) throws SQLException; + /** + * Return a string describing this object in a way useful for debugging, + * prefixed with the name (abbreviated for comfort) of the class of the + * object passed in (the normal Java {@code toString()} method should pass + * {@code this}). + *

    + * Subclasses or consumers are encouraged to call this method and append + * further details specific to the subclass or consumer. The convention + * should be that the recursion will stop at some class that will actually + * construct the abbreviated class name of {@code o} and use it to prefix + * the returned value. + * @param o An object whose class name (possibly abbreviated) should be used + * to prefix the returned string. + * @return Description of this object. + */ + String toString(Object o); + + + /** * A class by which Java reads the content of a varlena as an InputStream. * @@ -192,6 +211,10 @@ public void reset() throws IOException } } + /** + * Return {@code true}; this class does support {@code mark} and + * {@code reset}. + */ @Override public boolean markSupported() { @@ -211,6 +234,19 @@ public long adopt(DualState.Key cookie) throws SQLException } } + @Override + public String toString() + { + return toString(this); + } + + @Override + public String toString(Object o) + { + return String.format("%s %s", m_state.toString(o), + m_open ? "open" : "closed"); + } + private static class State @@ -256,6 +292,14 @@ protected void javaStateReleased() m_buf = null; super.javaStateReleased(); } + + @Override + public String toString(Object o) + { + return String.format("%s varlena:%x %s", + super.toString(o), m_varlena, + String.valueOf(m_buf).replace("java.nio.", "")); + } } } @@ -384,6 +428,19 @@ public long adopt(DualState.Key cookie) throws SQLException } } + @Override + public String toString() + { + return toString(this); + } + + @Override + public String toString(Object o) + { + return String.format("%s %s", m_state.toString(o), + m_open ? "open" : "closed"); + } + private static class State @@ -434,10 +491,10 @@ private long adopt(DualState.Key cookie) throws SQLException } @Override - public String toString() + public String toString(Object o) { - return String.format("%s VarlenaWrapper.Output (%x) %s", - super.toString(), m_varlena, + return String.format("%s varlena:%x %s", + super.toString(o), m_varlena, String.valueOf(m_buf).replace("java.nio.", "")); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 00d74dc8..9fb48739 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -258,8 +258,44 @@ static SQLXML newWritable() protected abstract VarlenaWrapper adopt() throws SQLException; + /** + * Return a description of this object useful for debugging (not the raw + * XML content). + */ + @Override + public String toString() + { + return toString(this); + } + + /** + * Return information about this object useful for debugging, prefixed with + * a possibly shortened form of the class name of the passed object + * {@code o}; the normal Java {@code toString()} will pass {@code this}. + *

    + * Subclasses are encouraged to override, call the super method and append + * subclass-specific detail. + * @param o Object whose class name should be used to prefix the returned + * string. Passing {@code null} is the same as passing {@code this}. + * @return Description of this object for debugging convenience. + */ + protected String toString(Object o) + { + if ( null == o ) + o = this; + V backing = m_backing.get(); + if ( null != backing ) + return backing.toString(o); + Class c = o.getClass(); + String cn = c.getCanonicalName(); + int pnl = c.getPackage().getName().length(); + return cn.substring(1 + pnl) + " defunct"; + } + private static native SQLXML _newWritable(); + + static class Readable extends SQLXMLImpl { private AtomicBoolean m_readable = new AtomicBoolean(true); @@ -425,6 +461,14 @@ protected VarlenaWrapper adopt() throws SQLException return vw; } + @Override + protected String toString(Object o) + { + return String.format("%s %sreadable %swrapped", + super.toString(o), m_readable.get() ? "" : "not ", + m_wrapped ? "" : "not "); + } + /** * Return an InputStream presenting the contents of the underlying * varlena, but with the leading declaration corrected if need be. @@ -684,6 +728,8 @@ private void domUnwrap(DOMSource ds) } } + + static class Writable extends SQLXMLImpl { private AtomicBoolean m_writable = new AtomicBoolean(true); @@ -878,6 +924,13 @@ protected VarlenaWrapper adopt() throws SQLException } return vwo; } + + @Override + protected String toString(Object o) + { + return String.format("%s %swritable", super.toString(o), + m_writable.get() ? "" : "not "); + } } static class DeclCheckedOutputStream extends FilterOutputStream From 7d152374cf27f85bb0437d41b33f5e66193a4588 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 1 Aug 2018 22:51:16 -0400 Subject: [PATCH 0133/1087] Plant a DualState_cleanEnqueuedInstances call. Where to plant such calls may involve some arbitrary and strategic choices, but the one currently planted in Invocation_popInvocation is surely not enough in the case a PL/Java function loops through a lot of data, possibly creating and discarding DualState objects, without returning to PostgreSQL. This seems a good place to plant another. It is not called for every row processed, but for every fetchSize rows, defaulting (in SPIStatement) to 1000. Perhaps that might still be too seldom for code that churns a lot of DualState objects. But it's fairly certain not to be too often and impose an onerous overhead. --- pljava-so/src/main/c/type/Portal.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index b9ba3030..81e504e1 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -18,6 +18,7 @@ #include "org_postgresql_pljava_internal_Portal.h" #include "pljava/Backend.h" +#include "pljava/DualState.h" #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/HashMap.h" @@ -190,6 +191,16 @@ Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jl STACK_BASE_VARS STACK_BASE_PUSH(threadId) + /* + * One call to cleanEnqueued... is made in Invocation_popInvocation, + * when any PL/Java function returns to PostgreSQL. But what of a + * PL/Java function that loops through a lot of data before returning? + * It could be important to call cleanEnqueued... from some other + * strategically-chosen places, and this seems a good one. We get here + * every fetchSize (default 1000? See SPIStatement) rows retrieved. + */ + pljava_DualState_cleanEnqueuedInstances(); + p2l.longVal = _this; PG_TRY(); { From 3377c2dd544e942e50d64588590b5a217aa761f5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 6 Aug 2018 01:09:22 -0400 Subject: [PATCH 0134/1087] Let MarkableSequenceInputStream draw from a queue. Change the internal form of the sequence of constituent streams from an array to a ListIterator backed by a LinkedList, and add a constructor that accepts a BlockingQueue of constituent streams. If these constituent streams are markable/resettable, the combined stream still is also. This can then serve as an efficient markable input stream for reading along concurrently with content generated in a series of buffers that are just handed over here as they are filled. --- .../jdbc/MarkableSequenceInputStream.java | 325 +++++++++++++++--- 1 file changed, 270 insertions(+), 55 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java index 38947cb1..9975f5fb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java @@ -4,6 +4,15 @@ import java.io.SequenceInputStream; import java.io.IOException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CancellationException; + /** * Version of {@link SequenceInputStream} that supports * {@code mark} and {@code reset}, to the extent its constituent input streams @@ -27,9 +36,19 @@ */ public class MarkableSequenceInputStream extends InputStream { - private InputStream[] m_streams; - private int m_activeStream; - private int m_markedStream; + /** + * A sentinel value, needed because a {@code BlockingQueue} does not allow + * a null value to be enqueued. + */ + public static final InputStream NO_MORE = new InputStream() + { + @Override public int read() { return -1; } + }; + + private boolean m_closed = false; + private InputStream m_markedStream = null; + + private ListIterator m_streams; private int m_readlimit_orig; private int m_readlimit_curr; private boolean m_markSupported; @@ -47,47 +66,77 @@ public MarkableSequenceInputStream(InputStream... streams) { if ( null == streams ) throw new NullPointerException("MarkableSequenceInputStream(null)"); + LinkedList isl = new LinkedList(); for ( InputStream s : streams ) + { if ( null == s ) throw new NullPointerException( "MarkableSequenceInputStream(..., null, ...)"); + isl.add(s); + } + m_streams = isl.listIterator(); + } - m_streams = streams; - m_activeStream = 0; /* -1 will mean closed */ - m_markedStream = -1; /* -1 will mean no mark has been set */ + /** + * A {@code MarkableSequenceInputStream} that will receive streams to read, + * in order, over a {@code BlockingQueue}. + *

    + * The thread supplying the queue should enqueue the value {@link #NO_MORE} + * following the last actual {@code InputStream} to read. (The sentinel is + * needed because a {@code BlockingQueue} does not allow null values.) + * @param queue Source of input streams to read. + * @throws NullPointerException if {@code queue} is {@code null}. + */ + public MarkableSequenceInputStream(BlockingQueue queue) + { + m_streams = + new FetchingListIterator( + new LinkedList(), queue, NO_MORE); } + /* + * This method depends on an invariant: the iterator's next() method, when + * called here, will return the current, active stream. Each consumer method + * is responsible for preserving that invariant by calling previous() once + * after obtaining, but not hitting EOF on, a stream from next(). + */ private InputStream currentStream() throws IOException { - if ( -1 == m_activeStream ) + if ( m_closed ) throw new IOException("I/O on closed InputStream"); - if ( m_streams.length == m_activeStream ) - return null; - return m_streams[m_activeStream]; + if ( m_streams.hasNext() ) + return m_streams.next(); + return null; } + /* + * The invariant here is that a "current" stream was recently obtained, and + * can be re-obtained with previous(). This should not be called unless + * there is nothing left to read from that stream. + */ private InputStream nextStream() throws IOException { - assert m_streams.length > m_activeStream; - assert -1 == m_streams[m_activeStream].read(); - - if ( -1 == m_markedStream ) + if ( null == m_markedStream ) { - m_streams[m_activeStream].close(); - m_streams[m_activeStream++] = null; + m_streams.previous().close(); + assert ! m_streams.hasPrevious(); + m_streams.remove(); + if ( m_streams.hasNext() ) + return m_streams.next(); } - else if ( m_streams.length > ++m_activeStream ) - m_streams[m_activeStream].mark(m_readlimit_curr); - - if ( m_streams.length == m_activeStream ) - return null; - return m_streams[m_activeStream]; + else if ( m_streams.hasNext() ) + { + InputStream is = m_streams.next(); + is.mark(m_readlimit_curr); + return is; + } + return null; } private void decrementLimit(long bytes) { assert 0 < bytes; - if ( -1 == m_markedStream ) + if ( null == m_markedStream ) return; if ( bytes < m_readlimit_curr ) { @@ -108,6 +157,7 @@ public int read() throws IOException if ( -1 != c ) { decrementLimit(1); + m_streams.previous(); /* maintain "current" invariant */ return c; } } @@ -126,6 +176,7 @@ public int read(byte[] b, int off, int len) throws IOException if ( -1 != rslt ) { decrementLimit(rslt); + m_streams.previous(); /* maintain "current" invariant */ return rslt; } } @@ -148,7 +199,10 @@ public long skip(long n) throws IOException decrementLimit(skipped); totalSkipped += skipped; if ( 0 >= n ) + { + m_streams.previous(); /* maintain "current" invariant */ break; + } /* * A short count from skip doesn't have to mean EOF was reached. * A read() will settle that question, though. @@ -174,10 +228,13 @@ public int available() throws IOException { synchronized ( this ) { - if ( -1 == m_activeStream ) + if ( m_closed ) return 0; InputStream s = currentStream(); - return null == s ? 0 : s.available(); + if ( null == s ) + return 0; + m_streams.previous(); /* maintain "current" invariant */ + return s.available(); } } @@ -186,14 +243,14 @@ public void close() throws IOException { synchronized ( this ) { - if ( -1 == m_activeStream ) + if ( m_closed ) return; - if ( -1 != m_markedStream ) - m_activeStream = m_markedStream; - for ( ; m_streams.length > m_activeStream ; ++ m_activeStream ) - m_streams[m_activeStream].close(); - m_activeStream = -1; + while ( m_streams.hasPrevious() ) + m_streams.previous(); + while ( m_streams.hasNext() ) + m_streams.next().close(); m_streams = null; + m_closed = true; } } @@ -208,38 +265,61 @@ public void mark(int readlimit) { synchronized ( this ) { - if ( -1 == m_activeStream ) + if ( m_closed ) return; - if ( -1 == m_markedStream ) - m_markedStream = m_activeStream; - else + InputStream activeStream = null; + if ( m_streams.hasNext() ) { - for ( ; m_markedStream < m_activeStream ; ++ m_markedStream ) + m_streams.next(); + activeStream = m_streams.previous(); + } + + if ( null != m_markedStream ) + { + for ( InputStream is = activeStream; is != m_markedStream; ) + is = m_streams.previous(); + /* + * Whether the above loop executed zero or more times, the last + * event on m_streams was a previous(), and returned the marked + * stream, and the next next() will also. + */ + m_markedStream = null; // so nextStream() will close things + /* + * It is safe to start off this loop with next(), because it + * will return the formerly marked stream, known to exist. + */ + for ( InputStream is = m_streams.next(); is != activeStream; ) { try { - m_streams[m_markedStream].close(); + is = nextStream(); // will close stream and return next } catch ( IOException e ) { + throw new UndeclaredThrowableException(e); } - m_streams[m_markedStream] = null; } + /* + * Leave the invariant the same whether this if block was taken + * or not. + */ + if ( null != activeStream ) + m_streams.previous(); } if ( 0 >= readlimit ) /* setting instantly-invalid mark */ { m_readlimit_curr = m_readlimit_orig = 0; - m_markedStream = -1; return; } m_readlimit_curr = m_readlimit_orig = readlimit; - if ( m_streams.length == m_markedStream ) /* setting mark at EOF */ + if ( null == activeStream ) /* setting mark at EOF */ return; - m_streams[m_markedStream].mark(readlimit); + m_markedStream = activeStream; + activeStream.mark(readlimit); } } @@ -248,19 +328,38 @@ public void reset() throws IOException { synchronized ( this ) { - if ( -1 == m_activeStream ) + if ( m_closed ) throw new IOException("reset on closed InputStream"); - if ( -1 == m_markedStream ) + + if ( null == m_markedStream ) + { + if ( 0 < m_readlimit_orig ) + return; // the mark-at-EOF case; reset allowed, no effect throw new IOException("reset without mark"); + } + + InputStream is = currentStream(); + /* + * 'is' right now is the active stream, or null if we are at EOF; + * either way the first call to previous() coming up below will + * return an existing stream, the one we need (in reverse order) + * to reset first. + */ + while ( true ) { - if ( m_streams.length > m_activeStream ) - m_streams[m_activeStream].reset(); - if ( m_activeStream == m_markedStream ) + is = m_streams.previous(); + is.reset(); + if ( is == m_markedStream ) break; - -- m_activeStream; + is.mark(0); // release possible resources } m_readlimit_curr = m_readlimit_orig; + /* + * The invariant (that the next next() will return the stream we + * just touched) is already satisfied, as we obtained it with + * previous() above. + */ } } @@ -284,19 +383,135 @@ public boolean markSupported() { if ( m_markSupported_determined ) return m_markSupported; - int i = m_markedStream; - if ( -1 == i ) - i = m_activeStream; - if ( -1 == i ) + if ( m_closed ) return false; - m_markSupported = true; - for ( ; m_streams.length > i ; ++ i ) + + InputStream activeStream = null; + if ( m_streams.hasNext() ) { - if ( ! m_streams[i].markSupported() ) - m_markSupported = false; + m_streams.next(); + activeStream = m_streams.previous(); + } + + if ( null != m_markedStream ) + { + for ( InputStream is = activeStream; is != m_markedStream; ) + is = m_streams.previous(); } + + /* + * The next next() returns the marked stream (if there is one), or + * the active stream (if there is one). + */ + m_markSupported = true; + while ( m_streams.hasNext() ) + if ( ! m_streams.next().markSupported() ) + m_markSupported = false; + /* + * We've run to the end of the streams list. Back up to the active + * one. + */ + for ( InputStream is = null; is != activeStream; ) + is = m_streams.previous(); + + /* + * The "current" invariant is satisfied. + */ m_markSupported_determined = true; return m_markSupported; } } + + /** + * A {@code ListIterator} that will fetch an element from a + * {@code BlockingQueue} whenever {@code hasNext} would (otherwise) + * return {@code false}, adding it to the end of the list where the next + * {@code next()} will retrieve it.' + *

    + * It is possible for the {@code hasNext}, {@code next}, and + * {@code nextIndex} methods to throw {@link CancellationException}, if the + * thread is interrupted while they await something on the queue. + */ + public static class FetchingListIterator implements ListIterator + { + private final ListIterator m_li; + private BlockingQueue m_q; + private final E m_sentinel; + + /** + * Construct a {@code FetchingListIterator} given an existing list, + * a {@code BlockingQueue}, and a particular instance of the list's + * element type to use as an end-of-queue sentinel (it is not possible + * to enqueue a null value on a {@code BlockingQueue}). + * @param list An existing list. + * @param queue A blocking queue whose elements will be taken in order + * following any existing elements in the original list. + * @param sentinel A value that the supplier, feeding the blocking + * queue, will enqueue when no more actual values will be forthcoming. + * When an element is dequeued that matches this sentinel (per reference + * equality), it is not added to the list, and nothing more will be + * fetched from the queue. + * @throws NullPointerException if any parameter is null. + */ + public FetchingListIterator( + List list, BlockingQueue queue, E sentinel) + { + if ( null == list || null == queue || null == sentinel ) + throw new NullPointerException( + "a null parameter was passed to FetchingListIterator"); + + m_li = list.listIterator(); + m_q = queue; + m_sentinel = sentinel; + } + + @Override + public boolean hasNext() + { + boolean has = m_li.hasNext(); + E e; + if ( has || null == m_q ) + return has; + try + { + e = m_q.take(); + } + catch ( InterruptedException ex ) + { + m_q = null; + throw (CancellationException) + new CancellationException("Interrupted waiting for input") + .initCause(ex); + } + if ( m_sentinel == e ) + { + m_q = null; + return has; + } + m_li.add(e); + m_li.previous(); + return true; + } + + @Override + public E next() + { + hasNext(); + return m_li.next(); + } + + @Override + public int nextIndex() + { + hasNext(); + return m_li.nextIndex(); + } + + @Override public void add(E e) { m_li.add(e); } + @Override public boolean hasPrevious() { return m_li.hasPrevious(); } + @Override public E previous() { return m_li.previous(); } + @Override public int previousIndex() { return m_li.previousIndex(); } + @Override public void remove() { m_li.remove(); } + @Override public void set(E e) { m_li.set(e); } + } } From 8e79cc3e55d1755dde20589243e38df25b2dfe5d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 6 Aug 2018 01:10:40 -0400 Subject: [PATCH 0135/1087] Re{factor|package} M.S.I.Stream and VarlenaWrapper There is nothing very JDBC-specific about MarkableSequenceInputStream, so move it from o.p.p.jdbc to o.p.p.internal. VarlenaWrapper.Input is mostly made up of what could be a more general ByteBufferInputStream, so factor that out into its own class. --- .../internal/ByteBufferInputStream.java | 196 ++++++++++++++++++ .../MarkableSequenceInputStream.java | 2 +- .../pljava/internal/VarlenaWrapper.java | 126 +---------- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 1 + 4 files changed, 207 insertions(+), 118 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java rename pljava/src/main/java/org/postgresql/pljava/{jdbc => internal}/MarkableSequenceInputStream.java (99%) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java b/pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java new file mode 100644 index 00000000..0dd602db --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2018 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.internal; + +import java.io.InputStream; +import java.io.IOException; + +import java.nio.ByteBuffer; +import java.nio.InvalidMarkException; + +/** + * Wrap a readable {@link ByteBuffer} as an {@link InputStream}. + *

    + * An implementing class must provide a {@link #buffer} method that returns the + * {@code ByteBuffer}, and the method is responsible for knowing when the memory + * region windowed by the {@code ByteBuffer} is no longer to be accessed, and + * throwing an exception in that case. + *

    + * The implementing class must supply an object that the {@code InputStream} + * operations will be {@code synchronized} on, and that must be the same object + * on which any operations that affect the byte buffer's accessibility will + * synchronize. + */ +public abstract class ByteBufferInputStream extends InputStream +{ + /** + * The object on which the {@code InputStream} operations will synchronize. + *

    + * Must be the same object, if any, that operations affecting the byte + * buffer's accessibility synchronize on, and may be of any type useful to + * the implementing class. + * Every implementing subclass must assign something to + * {@code m_state} when created, and then leave it alone as if it were + * {@code final}. + */ + protected Object m_state; + + /** + * Whether this stream is open; initially true. + */ + protected boolean m_open; + + /** + * Construct an instance, given an object on which to synchronize. + *

    + * Does not require a parameter to initialize {@link m_state} (because if an + * implementing subclass needs to create a state object with a reference to + * this, Java's restriction on referencing this prior to calling a + * superclass constructor could be triggered). + *

    + * Every implementing subclass must assign something to + * {@code m_state} when created, and then leave it alone as if it were + * {@code final}. + */ + protected ByteBufferInputStream() + { + m_open = true; + } + + /** + * Return the {@link ByteBuffer} being wrapped, or throw an exception if the + * memory windowed by the buffer should no longer be accessed. + *

    + * The monitor on {@link #m_state} is held when this method is called. + *

    + * This method also should throw an exception if {@link #m_open} is false. + * It is called everywhere that should happen, so it is the perfect place + * for the test, and allows the implementing class to use a customized + * message in the exception. + */ + protected abstract ByteBuffer buffer() throws IOException; + + @Override + public int read() throws IOException + { + synchronized ( m_state ) + { + ByteBuffer src = buffer(); + if ( 0 < src.remaining() ) + return src.get(); + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException + { + synchronized ( m_state ) + { + ByteBuffer src = buffer(); + int has = src.remaining(); + if ( len > has ) + { + if ( 0 == has ) + return -1; + len = has; + } + src.get(b, off, len); + return len; + } + } + + @Override + public long skip(long n) throws IOException + { + synchronized ( m_state ) + { + ByteBuffer src = buffer(); + int has = src.remaining(); + if ( n > has ) + n = has; + src.position(src.position() + (int)n); + return n; + } + } + + @Override + public int available() throws IOException + { + synchronized ( m_state ) + { + return buffer().remaining(); + } + } + + @Override + public void close() throws IOException + { + synchronized ( m_state ) + { + if ( ! m_open ) + return; + m_open = false; + } + } + + @Override + public void mark(int readlimit) + { + synchronized ( m_state ) + { + if ( ! m_open ) + return; + try + { + buffer().mark(); + } + catch ( IOException e ) + { + /* + * The contract is for mark to throw no checked exception. + * An exception caught here probably means the state's no longer + * live, which will be signaled to the caller if another, + * throwing, method is then called. If not, no harm no foul. + */ + } + } + } + + @Override + public void reset() throws IOException + { + synchronized ( m_state ) + { + if ( ! m_open ) + return; + try + { + buffer().reset(); + } + catch ( InvalidMarkException e ) + { + throw new IOException("reset attempted when mark not set"); + } + } + } + + /** + * Return {@code true}; this class does support {@code mark} and + * {@code reset}. + */ + @Override + public boolean markSupported() + { + return true; + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java b/pljava/src/main/java/org/postgresql/pljava/internal/MarkableSequenceInputStream.java similarity index 99% rename from pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java rename to pljava/src/main/java/org/postgresql/pljava/internal/MarkableSequenceInputStream.java index 9975f5fb..4319b198 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/MarkableSequenceInputStream.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/MarkableSequenceInputStream.java @@ -1,4 +1,4 @@ -package org.postgresql.pljava.jdbc; +package org.postgresql.pljava.internal; import java.io.InputStream; import java.io.SequenceInputStream; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 605da63f..cb40dc20 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -17,7 +17,6 @@ import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.InvalidMarkException; import java.sql.SQLException; @@ -67,11 +66,9 @@ public interface VarlenaWrapper extends Closeable * the native reference; the chosen resource owner must be one that will be * released no later than the memory context containing the varlena. */ - public static class Input extends InputStream implements VarlenaWrapper + public static class Input + extends ByteBufferInputStream implements VarlenaWrapper { - private State m_state; - private boolean m_open = true; - /** * Construct a {@code VarlenaWrapper.Input}. * @param cookie Capability held by native code. @@ -91,13 +88,14 @@ private Input(DualState.Key cookie, long resourceOwner, context, varlenaPtr, buf.asReadOnlyBuffer()); } - private ByteBuffer buf() throws IOException + @Override + protected ByteBuffer buffer() throws IOException { if ( ! m_open ) throw new IOException("Read from closed VarlenaWrapper"); try { - return m_state.buffer(); + return ((State)m_state).buffer(); } catch ( SQLException sqe ) { @@ -105,122 +103,16 @@ private ByteBuffer buf() throws IOException } } - @Override - public int read() throws IOException - { - synchronized ( m_state ) - { - ByteBuffer src = buf(); - if ( 0 < src.remaining() ) - return src.get(); - return -1; - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException - { - synchronized ( m_state ) - { - ByteBuffer src = buf(); - int has = src.remaining(); - if ( len > has ) - { - if ( 0 == has ) - return -1; - len = has; - } - src.get(b, off, len); - return len; - } - } - - @Override - public long skip(long n) throws IOException - { - synchronized ( m_state ) - { - ByteBuffer src = buf(); - int has = src.remaining(); - if ( n > has ) - n = has; - src.position(src.position() + (int)n); - return n; - } - } - - @Override - public int available() throws IOException - { - synchronized ( m_state ) - { - return buf().remaining(); - } - } - @Override public void close() throws IOException { synchronized ( m_state ) { - if ( ! m_open ) - return; - m_open = false; - m_state.enqueue(); - } - } - - @Override - public void mark(int readlimit) - { - synchronized ( m_state ) - { - if ( ! m_open ) - return; - try - { - buf().mark(); - } - catch ( IOException e ) - { - /* - * The contract is for mark to throw no checked exception. - * An exception caught here would mean the state's no longer - * live, which will be signaled to the caller if another, - * throwing, method is then called. If not, no harm no foul. - */ - } + super.close(); + ((State)m_state).enqueue(); } } - @Override - public void reset() throws IOException - { - synchronized ( m_state ) - { - if ( ! m_open ) - return; - try - { - buf().reset(); - } - catch ( InvalidMarkException e ) - { - throw new IOException("reset attempted when mark not set"); - } - } - } - - /** - * Return {@code true}; this class does support {@code mark} and - * {@code reset}. - */ - @Override - public boolean markSupported() - { - return true; - } - @Override public long adopt(DualState.Key cookie) throws SQLException { @@ -230,7 +122,7 @@ public long adopt(DualState.Key cookie) throws SQLException throw new SQLException( "Cannot adopt VarlenaWrapper.Input after it is closed", "55000"); - return m_state.adopt(cookie); + return ((State)m_state).adopt(cookie); } } @@ -243,7 +135,7 @@ public String toString() @Override public String toString(Object o) { - return String.format("%s %s", m_state.toString(o), + return String.format("%s %s", ((State)m_state).toString(o), m_open ? "open" : "closed"); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 9fb48739..c42d157e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -31,6 +31,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.postgresql.pljava.internal.Backend; +import org.postgresql.pljava.internal.MarkableSequenceInputStream; import java.sql.SQLNonTransientException; From f4b87bede67f63602ea666763deeb840c7a11154 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 6 Aug 2018 00:57:40 -0400 Subject: [PATCH 0136/1087] Add VarlenaWrapper.Verifier. A Verifier can be set on a VarlenaWrapper.Output to read along as bytes are written to the varlena, and throw an exception if they do not make a well-formed representation of the expected type. This is not necessary if the PL/Java code for a given type retains complete control over what is written to the varlena, but is, if an API requires exposing the VarlenaWrapper.Output more or less directly to client code that could write arbitrary content on it. The code that implements a specific data type over varlena can create a subclass of Verifier.Base, whose verify(InputStream) method simply reads the whole stream and returns successfully if nothing is wrong, otherwise throws an exception. VarlenaWrapper.Output will take care of submitting that verify method to a thread pool, feeding it buffers of written data as they are filled, and collecting its success or exception status when the stream is closed (or canceling the task when appropriate). --- .../pljava/internal/VarlenaWrapper.java | 505 +++++++++++++++++- 1 file changed, 490 insertions(+), 15 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index cb40dc20..7ece0e3e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -20,6 +20,20 @@ import java.sql.SQLException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; + +import java.util.concurrent.atomic.AtomicReference; + /** * Interface that wraps a PostgreSQL native variable-length ("varlena") datum; * implementing classes present an existing one to Java as a readable @@ -226,6 +240,28 @@ private Output(DualState.Key cookie, long resourceOwner, cookie, this, resourceOwner, context, varlenaPtr, buf); } + /** + * Set the {@link Verifier Verifier} to be used on content written to + * this varlena. + *

    + * A verifier must be set, either to {@link Verifier.NoOp NoOp} or a + * datatype-specific subclass of {@link Verifier.Base Base}, before + * writing can succeed. + *

    + * On construction, no verifier is set, so the datatype-specific code + * can determine whether the {@code NoOp} or a specific verifier will be + * needed. This method can only be called once, so that this class could + * then be exposed to client code as an {@code OutputStream} without + * allowing the verifier to be changed. + */ + public void setVerifier(Verifier v) throws IOException + { + if ( ! m_open ) + throw new IOException( + "I/O operation on closed VarlenaWrapper.Output"); + m_state.setVerifier(v); + } + /** * Return a ByteBuffer to write into. *

    @@ -242,10 +278,7 @@ private ByteBuffer buf(int desiredCapacity) throws IOException throw new IOException("Write on closed VarlenaWrapper.Output"); try { - ByteBuffer buf = m_state.buffer(); - if ( 0 < buf.remaining() && 0 < desiredCapacity ) - return buf; - return m_state.nextBuffer(desiredCapacity); + return m_state.buffer(desiredCapacity); } catch ( SQLException sqe ) { @@ -291,6 +324,10 @@ public void close() throws IOException buf(0); m_open = false; } + /* + * Outside of synchronized block to avoid deadlock with verifier. + */ + m_state.verify(); } /** @@ -340,6 +377,7 @@ private static class State { private ByteBuffer m_buf; private long m_varlena; + private Verifier m_verifier; private State( DualState.Key cookie, Output vr, @@ -351,25 +389,20 @@ private State( m_buf = buf; } - private ByteBuffer buffer() throws SQLException - { - assertNativeStateIsValid(); - return m_buf; - } - - private ByteBuffer nextBuffer(int desiredCapacity) - throws SQLException + private ByteBuffer buffer(int desiredCapacity) throws SQLException { assertNativeStateIsValid(); if ( 0 < m_buf.remaining() && 0 < desiredCapacity ) - throw new SQLException( - "VarlenaWrapper.Output buffer management error", - "XX000"); + return m_buf; + ByteBuffer filledBuf = m_buf; synchronized ( Backend.THREADLOCK ) { m_buf = _nextBuffer(m_varlena, m_buf.position(), desiredCapacity); } + m_verifier.update(this, filledBuf); + if ( 0 == desiredCapacity ) + m_verifier.update(MarkableSequenceInputStream.NO_MORE); return m_buf; } @@ -382,6 +415,45 @@ private long adopt(DualState.Key cookie) throws SQLException return varlena; } + private void setVerifier(Verifier v) + { + if ( null != m_verifier ) + throw new IllegalStateException( + "setVerifier when already set"); + if ( null == v ) + throw new NullPointerException("Null Verifier parameter"); + m_verifier = v.schedule(); + } + + private void cancelVerifier() + { + try + { + m_verifier.cancel(); + } + catch ( Exception e ) + { + } + } + + /* + * Caller must NOT be in synchronized block, to make sure verifier + * thread can complete. + */ + private void verify() throws IOException // because called in close + { + try + { + m_verifier.finish(); + } + catch ( SQLException e ) + { + throw new IOException( + "Variable-length PostgreSQL data written failed " + + "verification", e); + } + } + @Override public String toString(Object o) { @@ -394,6 +466,7 @@ public String toString(Object o) protected void nativeStateReleased() { m_buf = null; + cancelVerifier(); super.nativeStateReleased(); } @@ -401,6 +474,7 @@ protected void nativeStateReleased() protected void javaStateReleased() { m_buf = null; + cancelVerifier(); super.javaStateReleased(); } @@ -408,4 +482,405 @@ private native ByteBuffer _nextBuffer( long varlenaPtr, int currentBufPosition, int desiredCapacity); } } + + /** + * A {@code Verifier} verifies the proper form of content written to a + * {@code VarlenaWrapper.Output}. + *

    + * This is necessary only when the correctness of the written stream may be + * doubtful, as when an API spec requires exposing a method for client code + * to write arbitrary bytes. If a type implementation exposes only + * type-appropriate operations to client code, and always controls the byte + * stream written to the varlena, the {@code NoOp} verifier can be used. + *

    + * {@code Verifier} itself cannot be instantiated or extended, except by its + * two immediate subclasses, {@link NoOp NoOp} and {@link Base Base}. + * Type-specific verifiers must extend {@code Base}. Exactly one instance of + * {@code NoOp}, {@link NoOp#INSTANCE NoOp.INSTANCE}, exists. + *

    + * A type-specific verifier must supply a {@link #verify} method that reads + * its input stream argument (which may be assumed to support + * {@link InputStream#mark mark} and {@link InputStream#reset reset} + * efficiently), and complete normally if the full stream is a complete and + * well-formed representation of the type. Otherwise, it must throw an + * exception. + *

    + * In use, a verifier is instantiated and {@link #schedule schedule()}d, + * which sets the {@code verify} method running in a separate thread. + * The {@code verify} method must not interact with PostgreSQL. + * The varlena wrapper code then passes buffers to it via {@link #update + * update()} as they are filled. A final call to {@link #finish}, in the + * thread interacting with PostgreSQL, waits for the verify task to + * complete and then rethrows the exception, if it threw one. It is possible + * to {@link #cancel} a {@code Verifier}. + *

    + * As an optimization, all those methods are no-ops in the {@code NoOp} + * class; no other thread is used, and no work is done. The {@code Base} + * class, unextended, also serves as a verifier that accepts anything, but + * goes through all the motions to do it. + */ + public static abstract class Verifier implements Callable + { + private final BlockingQueue m_queue; + private final AtomicReference m_latch; + private volatile Future m_future; + + /* + * The design of Java's FutureTask strikes me as bizarre. One might + * think that the most natural way to use it for a task that does a + * certain thing and returns a result would be to extend it, override + * a particular method to do that thing, and submit it, ending up with + * one object that serves both as the task to be run and the Future. + * And that seems to be the exact design approach that its API + * /precludes/, because its only available constructors require passing + * a Runnable or Callable /that is some other object/. + * + * Can the constructor create a Callable that simply calls back to the + * verify method of this object, and pass that callable to the + * FutureTask constructor? No, because referring to 'this' before the + * supertype constructor has been called is a compile-time error. + * + * I really want one Verifier object that has all of: the verify() + * method being run in the executor, the update() method used to feed it + * stuff, and the Future-inspired methods for dealing with its status + * and result. And the only way I am seeing to get there is to have it + * submit() itself (in the schedule method) and then hold a reference to + * the Future created for it in that operation. Then it can have some + * Future-inspired methods that are more or less proxies to the methods + * on the Future itself, but then those have to deal with the chance + * that the Future reference hasn't been stored yet, so another whole + * synchronization puzzle crops up around the convenient synchronization + * tool. :( + * + * So, this method returns the Future, if we have it yet, or throws an + * IllegalStateException if we shouldn't have it yet because schedule() + * hasn't been called (or hasn't installed the latch yet; it's races all + * the way down), or waits for the latch and /then/ returns the Future. + */ + private Future future() throws SQLException + { + Future f = m_future; + if ( null != f ) + return f; + CountDownLatch cll = m_latch.get(); + if ( null == cll ) + throw new IllegalStateException("Verifier not yet scheduled"); + try + { + cll.await(); + } + catch ( InterruptedException e ) + { + throw new SQLException("Waiting thread interrupted", e); + } + return m_future; + } + + /* + * Private constructor. The nested class NoOp can call it passing nulls. + */ + private Verifier( + BlockingQueue queue, + AtomicReference latch) + { + m_queue = queue; + m_latch = latch; + } + + /* + * The nested class Base can call this one. Otherwise it's private, + * so no other direct subclasses are possible. + */ + private Verifier() + { + this(new LinkedBlockingQueue(), + new AtomicReference()); + } + + protected void verify(InputStream is) throws Exception + { + do + { + is.skip(Long.MAX_VALUE); + } + while ( -1 != is.read() ); + } + + @Override + public final Void call() throws Exception + { + InputStream is = null; + try + { + is = new MarkableSequenceInputStream(m_queue); + verify(is); + } + finally + { + if ( null != is ) + is.close(); + } + return null; + } + + /** + * A Verifier that accepts any content, cheaply. + */ + public static final class NoOp extends Verifier + { + private NoOp() { super(null, null); } + + public static final Verifier INSTANCE = new NoOp(); + + public Verifier schedule() + { + return this; + } + + public void update(InputStream is) throws SQLException + { + } + + public void update(Output.State state, ByteBuffer bb) + throws SQLException + { + } + + public void finish() throws SQLException + { + } + + public void cancel() throws SQLException + { + } + } + + /** + * Verifier to be extended to verify byte streams for specific types. + *

    + * A subclass should override {@link verify} with a method that reads + * the InputStream and throws an exception unless the entire stream was + * successfully read and represented a well-formed instance of the type. + */ + public static class Base extends Verifier + { + protected Base() { } + + @Override + public final Verifier schedule() + { + return super.schedule(); + } + + @Override + public final void update(InputStream is) throws SQLException + { + super.update(is); + } + + public final void update(Output.State state, ByteBuffer bb) + throws SQLException + { + super.update(state, bb); + } + + @Override + public final void finish() throws SQLException + { + super.finish(); + } + + @Override + public final void cancel() throws SQLException + { + super.cancel(); + } + } + + /** + * Set up the {@link #verify verify} method to be executed + * in another thread. + * @return This {@code Verifier} object. + */ + public Verifier schedule() + { + CountDownLatch cll = new CountDownLatch(1); + if ( m_latch.compareAndSet(null, cll) ) + { + m_future = LazyExecutorService.INSTANCE.submit(this); + cll.countDown(); + } + return this; + } + + /** + * Send the next {@code InputStream} of content to be verified. + *

    + * It is assumed, but not checked here, that any + * {@code InputStream} supplied to this method supports + * {@link InputStream#mark mark} and {@link InputStream#reset reset} + * efficiently. + *

    + * If the verifier has already thrown an exception, it will be rethrown + * here in the current thread. + * @param is InputStream representing the next range of bytes to be + * verified. + * @throws SQLException if a verification error has already been + * detected, the verifier has been cancelled, etc. + */ + public void update(InputStream is) throws SQLException + { + Future f = future(); + if ( f.isDone() ) + { + finish(); + throw new SQLException("Verifier finished prematurely"); + } + try + { + m_queue.put(is); + } + catch ( InterruptedException e ) + { + f.cancel(true); + throw (CancellationException) + new CancellationException("Waiting thread interrupted") + .initCause(e); + } + } + + /** + * Convenience method that calls {@link ByteBuffer#flip flip()} on a + * byte buffer, wraps it in a {@link BufferWrapper BufferWrapper}, and + * passes it to {@link update(InputStream)}. + *

    + * Note that the {@link NoOp NoOp} version of this method does none of + * that; in particular, the byte buffer will not have been flipped. This + * should not be a problem, as the thread passing the buffer to this + * method had better make no further use of it anyway. + * @param state The state object protecting the native memory. + * @param bb Byte buffer containing next range of content to verify. + * @throws SQLException if a verification error has already been + * detected, the verifier has been cancelled, etc. + */ + public void update(Output.State state, ByteBuffer bb) + throws SQLException + { + bb.flip(); + update(new BufferWrapper(state, bb)); + } + + /** + * Cancel this verifier. + */ + public void cancel() throws SQLException + { + Future f = future(); + f.cancel(true); + } + + /** + * Wait for the verify task and rethrow any exception it might + * have thrown. + * @throws SQLException any exception thrown by the verify method, or + * for unexpected conditions such as interruption while waiting. + */ + public void finish() throws SQLException + { + Future f = future(); + + try + { + f.get(); + } + catch ( InterruptedException inte ) + { + f.cancel(true); + throw (CancellationException) + new CancellationException("Waiting thread interrupted") + .initCause(inte); + } + catch ( ExecutionException exce ) + { + Throwable t = exce.getCause(); + if ( t instanceof SQLException ) + throw (SQLException) t; + if ( t instanceof RuntimeException ) + throw (RuntimeException) t; + throw new SQLException( + "Exception verifying variable-length data, not " + + "otherwise provided for", "XX000"); + } + + if ( ! m_queue.isEmpty() ) + throw new SQLException("Verifier finished prematurely"); + } + + /** + * Lazy holder for a singleton instance of a thread-pool + * {@link ExecutorService}. + *

    + * If it ever happens later that other PL/Java components could have use + * for a thread pool, this could certainly be moved out of + * {@code VarlenaWrapper} to a more common place. + */ + static class LazyExecutorService + { + static final ExecutorService INSTANCE; + + static + { + final ThreadFactory dflttf = Executors.defaultThreadFactory(); + ThreadFactory daemtf = new ThreadFactory() + { + @Override + public Thread newThread(Runnable r) + { + Thread t = dflttf.newThread(r); + if ( null != t ) + { + t.setDaemon(true); + t.setName( + "varlenaVerify-" + t.getName().substring(5)); + } + return t; + } + }; + INSTANCE = Executors.newCachedThreadPool(daemtf); + } + } + + /** + * {@link ByteBufferInputStream ByteBufferInputStream} subclass that + * wraps a {@code ByteBuffer} and the {@link Output.State Output.State} + * that protects it. + */ + static class BufferWrapper extends ByteBufferInputStream + { + private ByteBuffer m_buf; + + BufferWrapper(Output.State state, ByteBuffer buf) + { + m_state = state; + m_buf = buf; + } + + @Override + protected ByteBuffer buffer() throws IOException + { + if ( ! m_open ) + throw new IOException( + "I/O operation on closed VarlenaWrapper.Verifier"); + try + { + ((Output.State)m_state).assertNativeStateIsValid(); + } + catch ( SQLException e ) + { + throw new IOException(e.getMessage(), e); + } + return m_buf; + } + } + } } From c0fe9f7e11927fbc0ecdf02669b478833262823b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 6 Aug 2018 00:28:20 -0400 Subject: [PATCH 0137/1087] Add a Verifier for SQLXMLImpl.Writable. Because the stored form in PostgreSQL of the XML data type is just its character serialization, of all the methods offered by java.sql.SQLXML for reading/writing the content, the ones based on strings, character streams, and byte streams are the quickest ones to get working in a lazy sort of way. But the lazy way isn't appropriate for production, at least for the writing methods, because type safety precludes letting client code write arbitrary bytes into a varlena that PostgreSQL believes to be of XML type. So the quick, lazy methods ultimately become the ones that need the complexity of a Verifier making sure what is written is XML. The other writing methods, where the serialization is done via Java XML APIs, are left with the NoOp verifier, trusting the APIs to produce XML. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 475 ++++++++++-------- 1 file changed, 265 insertions(+), 210 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index c42d157e..d3b0330d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -26,8 +26,12 @@ /* ... for SQLXMLImpl */ +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; import java.io.IOException; +import java.nio.charset.Charset; + import java.util.concurrent.atomic.AtomicReference; import org.postgresql.pljava.internal.Backend; @@ -35,12 +39,24 @@ import java.sql.SQLNonTransientException; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; + +import static javax.xml.stream.XMLStreamConstants.CDATA; +import static javax.xml.stream.XMLStreamConstants.CHARACTERS; +import static javax.xml.stream.XMLStreamConstants.COMMENT; +import static javax.xml.stream.XMLStreamConstants.DTD; +import static javax.xml.stream.XMLStreamConstants.PROCESSING_INSTRUCTION; +import static javax.xml.stream.XMLStreamConstants.SPACE; +import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; +import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; + +import javax.xml.stream.XMLStreamException; + /* ... for SQLXMLImpl.Readable */ -import java.io.FilterInputStream; import java.io.InputStreamReader; import java.nio.CharBuffer; -import java.nio.charset.Charset; import java.util.concurrent.atomic.AtomicBoolean; @@ -53,21 +69,9 @@ import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamReader; - import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; -import static javax.xml.stream.XMLStreamConstants.CDATA; -import static javax.xml.stream.XMLStreamConstants.CHARACTERS; -import static javax.xml.stream.XMLStreamConstants.COMMENT; -import static javax.xml.stream.XMLStreamConstants.DTD; -import static javax.xml.stream.XMLStreamConstants.PROCESSING_INSTRUCTION; -import static javax.xml.stream.XMLStreamConstants.SPACE; -import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; -import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; - import org.w3c.dom.Document; import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; @@ -81,7 +85,6 @@ /* ... for SQLXMLImpl.DeclProbe */ -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Arrays; @@ -124,8 +127,6 @@ import javax.xml.stream.events.XMLEvent; import javax.xml.stream.util.StreamReaderDelegate; -import javax.xml.stream.XMLStreamException; - public abstract class SQLXMLImpl implements SQLXML { protected AtomicReference m_backing; @@ -295,6 +296,202 @@ protected String toString(Object o) private static native SQLXML _newWritable(); + /** + * Return an InputStream presenting the contents of the underlying + * varlena, but with the leading declaration corrected if need be. + *

    + * The current stored form in PG for the XML type is a character string + * in server encoding, which may or may not still include a declaration + * left over from an input or cast operation, which declaration may or + * may not be correct (about the encoding, anyway). Nothing is stored + * to distinguish whether the value is of the {@code DOCUMENT} or + * {@code CONTENT} form, to determine which requires a full reparse in + * the general case. + *

    + * This method only peeks at early parse events in the stream, to see + * if a {@code DOCTYPE} is present (must be {@code DOCUMENT}, or there + * is any other content before the first element (cannot be + * {@code DOCUMENT}). The input will not have a synthetic root element + * wrapped around it if a {@code DOCTYPE} is present, as that would + * break validation; otherwise (whether the check concluded it can't be + * {@code DOCUMENT}, or was simply inconclusive}, a synthetic wrapper + * will be added, as it will not break anything. + *

    + * As a side effect, this method sets {@code m_wrapped} tp {@code true} + * if it applies a wrapper element. When returning a type of + * {@code Source} that presents parsed results, it will be configured + * to present them with the wrapper element filtered out. + *

    + * However, when using the API that exposes the serialized form + * directly ({@code getBinaryStream}, {@code getCharacterStream}, + * {@code getString}), this method is passed {@code true} for + * {@code neverWrap}, and no wrapping is done. The application code must + * then handle the possibility that the stream may fail to parse as a + * {@code DOCUMENT}. (The JDBC spec gives no guidance in this area.) + * @param is The InputStream to be corrected. + * @param neverWrap When {@code true}, suppresses the wrapping described + * above. + * @param wrapping An array of one boolean, which will be set true if + * the returned stream has had a wrapping document element applied that + * will have to be filtered away after parsing. + * @return An InputStream with its original decl, if any, replaced with + * a new one known to be correct, or none if the defaults are correct, + * and with the remaining content wrapped in a synthetic root element, + * unless the input is known early (by having a {@code DOCTYPE}) not to + * need one. + */ + static InputStream correctedDeclStream( + InputStream is, boolean neverWrap, Charset serverCS, boolean[] wrapping) + throws IOException, SQLException + { + assert null != wrapping && 1 == wrapping.length; + + byte[] buf = new byte[40]; + int got; + boolean needMore = false; + DeclProbe probe = new DeclProbe(); + + while ( -1 != ( got = is.read(buf) ) ) + { + for ( int i = 0 ; i < got ; ++ i ) + needMore = probe.take(buf[i]); + if ( ! needMore ) + break; + } + + /* + * At this point, for better or worse, the loop is done. There may + * or may not be more of m_vwi left to read; the probe may or may + * not have found a decl. If it didn't, prefix() will treat whatever + * had been read as readahead and hand it all back, so it suffices + * here to create a SequenceInputStream of the prefix and whatever + * is or isn't left of m_vwi. + * A bonus is that the SequenceInputStream closes each underlying + * stream as it reaches EOF. After the last stream is used up, the + * SequenceInputStream remains open-at-EOF until explicitly closed, + * providing the expected input-stream behavior, but the underlying + * resources don't have to stick around for that. + */ + byte[] pfx = probe.prefix(serverCS); + int raLen = probe.readaheadLength(); + int raOff = pfx.length - raLen; + InputStream pfis = new ByteArrayInputStream(pfx, 0, raOff); + InputStream rais = new ByteArrayInputStream(pfx, raOff, raLen); + InputStream msis = new MarkableSequenceInputStream(pfis, rais, is); + + if ( neverWrap || ! useWrappingElement(msis) ) + return msis; + + wrapping[0] = true; + InputStream elemStart = new ByteArrayInputStream( + "".getBytes(serverCS)); + InputStream elemEnd = new ByteArrayInputStream( + "".getBytes(serverCS)); + msis = new MarkableSequenceInputStream( + pfis, elemStart, rais, is, elemEnd); + return msis; + } + + /** + * Check (incompletely!) whether an {@code InputStream} is in XML + * {@code DOCUMENT} form (which Java XML parsers will accept) or + * {@code CONTENT} form, (which they won't, unless enclosed in a + * wrapping element). + *

    + * Proceed by requiring the input stream to support {@code mark} and + * {@code reset}, marking it, creating a StAX parser, and pulling some + * initial parse events. + *

    + * A possible {@code START_DOCUMENT} along with possible {@code SPACE}, + * {@code COMMENT}, and {@code PROCESSING_INSTRUCTION} events could + * allowably begin either the {@code DOCUMENT} or the {@code CONTENT} + * form. + *

    + * If a {@code DTD} is seen, the input must be in {@code DOCUMENT} form, + * and must not have a wrapper element added. + *

    + * If anything else is seen before the first {@code START_ELEMENT}, the + * input must be in {@code CONTENT} form, and must have + * a wrapper element added. + *

    + * If a {@code START_ELEMENT} is seen before either of those conclusions + * can be reached, this check is inconclusive. The conclusive check + * would be to finish parsing that element to see what, if anything, + * follows it. But that would often amount to parsing the whole stream + * just to determine how to parse it. Instead, just return @code true} + * anyway, as without a DTD, the wrapping trick is usable and won't + * break anything, even if it may not be necessary. + * @param is An {@code InputStream} that must be markable, will be + * marked on entry, and reset upon return. + * @return {@code true} if a wrapping element should be used. + */ + static boolean useWrappingElement(InputStream is) + throws IOException + { + is.mark(Integer.MAX_VALUE); + XMLInputFactory xif = XMLInputFactory.newFactory(); + xif.setProperty(xif.IS_NAMESPACE_AWARE, true); + + boolean mustBeDocument = false; + boolean cantBeDocument = false; + + /* + * The XMLStreamReader may actually close the input stream if it + * reaches the end skipping only whitespace. That is probably a bug; + * in any case, protect the original input stream from being closed. + */ + InputStream tmpis = new FilterInputStream(is) + { + @Override + public void close() throws IOException { } + }; + + XMLStreamReader xsr = null; + try + { + xsr = xif.createXMLStreamReader(tmpis); + while ( xsr.hasNext() ) + { + int evt = xsr.next(); + + if ( COMMENT == evt || PROCESSING_INSTRUCTION == evt + || SPACE == evt || START_DOCUMENT == evt ) + continue; + + if ( DTD == evt ) + { + mustBeDocument = true; + break; + } + + if ( START_ELEMENT == evt ) // could be DOCUMENT or CONTENT + break; + + cantBeDocument = true; + break; + } + } + catch ( XMLStreamException e ) + { + cantBeDocument = true; + } + + if ( null != xsr ) + { + try + { + xsr.close(); + } + catch ( XMLStreamException e ) + { + } + } + is.reset(); + is.mark(0); // relax any reset-buffer requirement + + return ! mustBeDocument; + } + static class Readable extends SQLXMLImpl @@ -471,192 +668,19 @@ protected String toString(Object o) } /** - * Return an InputStream presenting the contents of the underlying - * varlena, but with the leading declaration corrected if need be. - *

    - * The current stored form in PG for the XML type is a character string - * in server encoding, which may or may not still include a declaration - * left over from an input or cast operation, which declaration may or - * may not be correct (about the encoding, anyway). Nothing is stored - * to distinguish whether the value is of the {@code DOCUMENT} or - * {@code CONTENT} form, to determine which requires a full reparse in - * the general case. - *

    - * This method only peeks at early parse events in the stream, to see - * if a {@code DOCTYPE} is present (must be {@code DOCUMENT}, or there - * is any other content before the first element (cannot be - * {@code DOCUMENT}). The input will not have a synthetic root element - * wrapped around it if a {@code DOCTYPE} is present, as that would - * break validation; otherwise (whether the check concluded it can't be - * {@code DOCUMENT}, or was simply inconclusive}, a synthetic wrapper - * will be added, as it will not break anything. - *

    - * As a side effect, this method sets {@code m_wrapped} tp {@code true} - * if it applies a wrapper element. When returning a type of - * {@code Source} that presents parsed results, it will be configured - * to present them with the wrapper element filtered out. - *

    - * However, when using the API that exposes the serialized form - * directly ({@code getBinaryStream}, {@code getCharacterStream}, - * {@code getString}), this method is passed {@code true} for - * {@code neverWrap}, and no wrapping is done. The application code must - * then handle the possibility that the stream may fail to parse as a - * {@code DOCUMENT}. (The JDBC spec gives no guidance in this area.) - * @param neverWrap When {@code true}, suppresses the wrapping described - * above. - * @return An InputStream with its original decl, if any, replaced with - * a new one known to be correct, or none if the defaults are correct, - * and with the remaining content wrapped in a synthetic root element, - * unless the input is known early (by having a {@code DOCTYPE}) not to - * need one. + * An instance method for calling the static {@code + * correctedDeclInputStream} and storing its {@code wrapped} indicator + * as {@code m_wrapped}. */ private InputStream correctedDeclStream( InputStream is, boolean neverWrap) throws IOException, SQLException { - byte[] buf = new byte[40]; - int got; - boolean needMore = false; - DeclProbe probe = new DeclProbe(); - - while ( -1 != ( got = is.read(buf) ) ) - { - for ( int i = 0 ; i < got ; ++ i ) - needMore = probe.take(buf[i]); - if ( ! needMore ) - break; - } - - /* - * At this point, for better or worse, the loop is done. There may - * or may not be more of m_vwi left to read; the probe may or may - * not have found a decl. If it didn't, prefix() will treat whatever - * had been read as readahead and hand it all back, so it suffices - * here to create a SequenceInputStream of the prefix and whatever - * is or isn't left of m_vwi. - * A bonus is that the SequenceInputStream closes each underlying - * stream as it reaches EOF. After the last stream is used up, the - * SequenceInputStream remains open-at-EOF until explicitly closed, - * providing the expected input-stream behavior, but the underlying - * resources don't have to stick around for that. - */ - byte[] pfx = probe.prefix(m_serverCS); - int raLen = probe.readaheadLength(); - int raOff = pfx.length - raLen; - InputStream pfis = new ByteArrayInputStream(pfx, 0, raOff); - InputStream rais = new ByteArrayInputStream(pfx, raOff, raLen); - InputStream msis = new MarkableSequenceInputStream(pfis, rais, is); - - if ( neverWrap || ! useWrappingElement(msis) ) - return msis; - - m_wrapped = true; - InputStream elemStart = new ByteArrayInputStream( - "".getBytes(m_serverCS)); - InputStream elemEnd = new ByteArrayInputStream( - "".getBytes(m_serverCS)); - msis = new MarkableSequenceInputStream( - pfis, elemStart, rais, is, elemEnd); - return msis; - } - - /** - * Check (incompletely!) whether an {@code InputStream} is in XML - * {@code DOCUMENT} form (which Java XML parsers will accept) or - * {@code CONTENT} form, (which they won't, unless enclosed in a - * wrapping element). - *

    - * Proceed by requiring the input stream to support {@code mark} and - * {@code reset}, marking it, creating a StAX parser, and pulling some - * initial parse events. - *

    - * A possible {@code START_DOCUMENT} along with possible {@code SPACE}, - * {@code COMMENT}, and {@code PROCESSING_INSTRUCTION} events could - * allowably begin either the {@code DOCUMENT} or the {@code CONTENT} - * form. - *

    - * If a {@code DTD} is seen, the input must be in {@code DOCUMENT} form, - * and must not have a wrapper element added. - *

    - * If anything else is seen before the first {@code START_ELEMENT}, the - * input must be in {@code CONTENT} form, and must have - * a wrapper element added. - *

    - * If a {@code START_ELEMENT} is seen before either of those conclusions - * can be reached, this check is inconclusive. The conclusive check - * would be to finish parsing that element to see what, if anything, - * follows it. But that would often amount to parsing the whole stream - * just to determine how to parse it. Instead, just return @code true} - * anyway, as without a DTD, the wrapping trick is usable and won't - * break anything, even if it may not be necessary. - * @param is An {@code InputStream} that must be markable, will be - * marked on entry, and reset upon return. - * @return {@code true} if a wrapping element should be used. - */ - private boolean useWrappingElement(InputStream is) throws IOException - { - is.mark(Integer.MAX_VALUE); - XMLInputFactory xif = XMLInputFactory.newFactory(); - xif.setProperty(xif.IS_NAMESPACE_AWARE, true); - - boolean mustBeDocument = false; - boolean cantBeDocument = false; - - /* - * The XMLStreamReader may actually close the input stream if it - * reaches the end skipping only whitespace. That is probably a bug; - * in any case, protect the original input stream from being closed. - */ - InputStream tmpis = new FilterInputStream(is) - { - @Override - public void close() throws IOException { } - }; - - XMLStreamReader xsr = null; - try - { - xsr = xif.createXMLStreamReader(tmpis); - while ( xsr.hasNext() ) - { - int evt = xsr.next(); - - if ( COMMENT == evt || PROCESSING_INSTRUCTION == evt - || SPACE == evt || START_DOCUMENT == evt ) - continue; - - if ( DTD == evt ) - { - mustBeDocument = true; - break; - } - - if ( START_ELEMENT == evt ) // could be DOCUMENT or CONTENT - break; - - cantBeDocument = true; - break; - } - } - catch ( XMLStreamException e ) - { - cantBeDocument = true; - } - - if ( null != xsr ) - { - try - { - xsr.close(); - } - catch ( XMLStreamException e ) - { - } - } - is.reset(); - is.mark(0); // relax any reset-buffer requirement - - return ! mustBeDocument; + boolean[] wrapped = { false }; + InputStream rslt = + correctedDeclStream(is, neverWrap, m_serverCS, wrapped); + m_wrapped = wrapped[0]; + return rslt; } /** @@ -754,10 +778,10 @@ private Writable(VarlenaWrapper.Output vwo) throws SQLException } } - private OutputStream backingAndClearWritable() + private VarlenaWrapper.Output backingAndClearWritable() throws SQLException { - OutputStream backing = backingIfNotFreed(); + VarlenaWrapper.Output backing = backingIfNotFreed(); return m_writable.getAndSet(false) ? backing : null; } @@ -780,11 +804,12 @@ public void free() throws SQLException @Override public OutputStream setBinaryStream() throws SQLException { - OutputStream os = backingAndClearWritable(); + VarlenaWrapper.Output os = backingAndClearWritable(); if ( null == os ) return super.setBinaryStream(); try { + os.setVerifier(new Verifier()); return new DeclCheckedOutputStream(os, m_serverCS); } catch ( IOException e ) @@ -796,12 +821,13 @@ public OutputStream setBinaryStream() throws SQLException @Override public Writer setCharacterStream() throws SQLException { - OutputStream os = backingAndClearWritable(); - if ( null == os ) + VarlenaWrapper.Output vwo = backingAndClearWritable(); + if ( null == vwo ) return super.setCharacterStream(); try { - os = new DeclCheckedOutputStream(os, m_serverCS); + vwo.setVerifier(new Verifier()); + OutputStream os = new DeclCheckedOutputStream(vwo, m_serverCS); return new OutputStreamWriter(os, m_serverCS.newEncoder()); } catch ( IOException e ) @@ -813,12 +839,13 @@ public Writer setCharacterStream() throws SQLException @Override public void setString(String value) throws SQLException { - OutputStream os = backingAndClearWritable(); - if ( null == os ) + VarlenaWrapper.Output vwo = backingAndClearWritable(); + if ( null == vwo ) super.setString(value); try { - os = new DeclCheckedOutputStream(os, m_serverCS); + vwo.setVerifier(new Verifier()); + OutputStream os = new DeclCheckedOutputStream(vwo, m_serverCS); Writer w = new OutputStreamWriter(os, m_serverCS.newEncoder()); w.write(value); w.close(); @@ -833,8 +860,8 @@ public void setString(String value) throws SQLException public T setResult(Class resultClass) throws SQLException { - OutputStream os = backingAndClearWritable(); - if ( null == os ) + VarlenaWrapper.Output vwo = backingAndClearWritable(); + if ( null == vwo ) return super.setResult(resultClass); if ( null == resultClass || Result.class == resultClass ) @@ -843,9 +870,18 @@ public T setResult(Class resultClass) try { if ( resultClass.isAssignableFrom(StreamResult.class) ) + { + vwo.setVerifier(new Verifier()); return resultClass.cast( new StreamResult(new DeclCheckedOutputStream( - os, m_serverCS))); + vwo, m_serverCS))); + } + + /* + * The remaining cases all can use the NoOp verifier. + */ + vwo.setVerifier(VarlenaWrapper.Verifier.NoOp.INSTANCE); + OutputStream os = vwo; if ( resultClass.isAssignableFrom(SAXResult.class) ) { @@ -932,6 +968,25 @@ protected String toString(Object o) return String.format("%s %swritable", super.toString(o), m_writable.get() ? "" : "not "); } + + static class Verifier extends VarlenaWrapper.Verifier.Base + { + @Override + protected void verify(InputStream is) throws Exception + { + boolean[] wrapped = { false }; + XMLReader xr = XMLReaderFactory.createXMLReader(); + xr.setFeature("http://xml.org/sax/features/namespaces", true); + is = correctedDeclStream( + is, false, implServerCharset(), wrapped); + /* + * What does an XMLReader do if no handlers have been set for + * content events? Parses everything and discards the events. + * Just what you'd want for a verifier. + */ + xr.parse(new InputSource(is)); + } + } } static class DeclCheckedOutputStream extends FilterOutputStream From 5716b0a0209cb2f698cda97fa39aa8a340ac80da Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 6 Aug 2018 02:21:03 -0400 Subject: [PATCH 0138/1087] Two more methods in the PassXML example. 1. a text-typed variant of lowLevelXMLEcho, which does not depend on PostgreSQL's XML type, and so can be used in PG builds without libxml, and also is useful for testing the behavior of an output verifier with input that wouldn't get past PostgreSQL's XML input. 2. a method to create SQLXML instances and leave them unused, unfreed, and unreferenced, the better to examine the cleanup behavior. Also add an SQLAction that runs a largish sample of XML--specifically, the result of table_to_xml('pg_operator', ...)--through echoXMLParameter using (almost) every combination of reading and writing protocols offered by SQLXML. Currently does not test reading as StAX(6) and writing as DOM(7), because that combination seems to make the JRE-supplied Transformer mess up, at least for me and Java 8. --- .../pljava/example/annotation/PassXML.java | 80 +++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) 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 9c6d9be7..3ce842f6 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 @@ -60,6 +60,7 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.example.LoggerTest.logMessage; @@ -74,12 +75,47 @@ * Everything mentioning the type XML here needs a conditional implementor tag * in case of being loaded into a PostgreSQL instance built without that type. */ -@SQLAction(provides="postgresql_xml", install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) +@SQLActions({ + @SQLAction(provides="postgresql_xml", install= + "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + + " THEN set_config('pljava.implementors', 'postgresql_xml,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction(implementor="postgresql_ge_80400", provides="postgresql_xml_cte", + install= + "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + + " THEN set_config('pljava.implementors', 'postgresql_xml_cte,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction(implementor="postgresql_xml_cte", requires="echoXMLParameter", + install= + "WITH" + + " s(how) AS (SELECT generate_series(1, 7))," + + " t(x) AS (" + + " SELECT table_to_xml('pg_catalog.pg_operator', true, false, '')" + + " )," + + " r(howin, howout, isdoc) AS (" + + " SELECT" + + " i.how, o.how," + + " javatest.echoxmlparameter(x, i.how, o.how) IS DOCUMENT" + + " FROM" + + " t, s AS i, s AS o" + + " WHERE" + + " NOT (i.how = 6 and o.how = 7)" + // 6->7 unreliable in some JREs + " ) " + + "SELECT" + + " CASE WHEN every(isdoc)" + + " THEN javatest.logmessage('INFO', 'SQLXML echos succeeded')" + + " ELSE javatest.logmessage('WARNING', 'SQLXML echos had problems')" + + " END " + + "FROM" + + " r" + ) +}) @MappedUDT(schema="javatest", name="onexml", structure="c1 xml", implementor="postgresql_xml", comment="A composite type mapped by the PassXML example class") @@ -100,7 +136,8 @@ public class PassXML implements SQLData * be read in a subsequent call with sx => null, but only in the same * transaction. */ - @Function(schema="javatest", implementor="postgresql_xml") + @Function(schema="javatest", implementor="postgresql_xml", + provides="echoXMLParameter") public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) throws SQLException { @@ -301,6 +338,16 @@ public static SQLXML lowLevelXMLEcho(SQLXML sx, int how) return rx; } + /** + * Text-typed variant of lowLevelXMLEcho (does not require XML type). + */ + @Function(schema="javatest", name="lowLevelXMLEcho", type="text") + public static SQLXML lowLevelXMLEcho_(@SQLType("text") SQLXML sx, int how) + throws SQLException + { + return lowLevelXMLEcho(sx, how); + } + /** * Create some XML, pass it to a {@code SELECT ?} prepared statement, * retrieve it from the result set, and return it via the out-parameter @@ -325,6 +372,25 @@ public static boolean xmlInStmtAndRS(ResultSet out) throws SQLException return true; } + /** + * Create and leave some number of SQLXML objects unclosed, unused, and + * unreferenced, as a test of reclamation. + * @param howmany Number of SQLXML instances to create. + * @param how If nonzero, the flavor of writing to request on the object + * before abandoning it; if zero, it is left in its initial, writable state. + */ + @Function(schema="javatest") + public static void unclosedSQLXML(int howmany, int how) throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + while ( howmany --> 0 ) + { + SQLXML sx = c.createSQLXML(); + if ( 0 < how ) + sxToResult(sx, how); + } + } + private static Source sxToSource(SQLXML sx, int how) throws SQLException { switch ( how ) From 925cec2d812c26ef9e0e0da2208457b5f2f182ed Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 7 Aug 2018 09:02:49 -0400 Subject: [PATCH 0139/1087] Avoid 'bouncing' SQLXML unchecked between types. Commit a73abee made it possible to take a readable SQLXML, as might come in as a function parameter, and use it directly as you could a writable one, returning it from the function or stuffing it into a query parameter. Commit f152658 made it possible to use this API even in PostgreSQL instances that were not built with the XML type, by allowing a function declaration to explicitly type an SQLXML parameter or return type as 'text'. In combination, those two features could undermine type safety: a function like: @Function public static SQLXML badcast(@SQLType("text") SQLXML sx) { return sx; } could serve as an "unchecked cast" returning non-XML text content as a PostgreSQL XML type. Solution: remember the PostgreSQL type Oid of the source when creating a readable SQLXML instance. If it is later used directly in the Java->PG direction, and the target PG Oid is different, run the verifier over the content first. That is, of course, slower than not doing it, but interestingly, the speed of the Java XML parser compares respectably to a direct PG CAST (text AS xml). I am seeing it pull ahead of the PG cast right around 32 kB of serialized XML, ultimately beating PG by a factor of two or better at 1 or 2 MB sizes. PG, of course, wins hands down on tiny values, where the invocation overhead predominates. --- .../pljava/example/annotation/PassXML.java | 30 ++++++ pljava-so/src/main/c/type/Oid.c | 8 ++ pljava-so/src/main/c/type/SQLXMLImpl.c | 54 +++++++++-- .../pljava/internal/VarlenaWrapper.java | 54 ++++++++++- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 96 +++++++++++++++---- 5 files changed, 209 insertions(+), 33 deletions(-) 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 3ce842f6..a29c355d 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 @@ -187,6 +187,36 @@ public static SQLXML bounceXMLParameter(SQLXML sx) throws SQLException return sx; } + /** + * Just like {@link bounceXMLParameter} but with parameter and return typed + * as {@code text}, and so usable on a PostgreSQL instance lacking the XML + * type. + */ + @Function(schema="javatest", type="text", name="bounceXMLParameter") + public static SQLXML bounceXMLParameter_(@SQLType("text") SQLXML sx) + throws SQLException + { + return sx; + } + + /** + * Just like {@link bounceXMLParameter} but with the parameter typed as + * {@code text} and the return type left as XML, so functions as a cast. + *

    + * Slower than the other cases, because it must verify that the input really + * is XML before blindly calling it a PostgreSQL XML type. But the speed + * compares respectably to PostgreSQL's own CAST(text AS xml), at least for + * larger values; I am seeing Java pull ahead right around 32kB of XML data + * and beat PG by a factor of 2 or better at sizes of 1 or 2 MB. + * Unsurprisingly, PG has the clear advantage when values are very short. + */ + @Function(schema="javatest", implementor="postgresql_xml") + public static SQLXML castTextXML(@SQLType("text") SQLXML sx) + throws SQLException + { + return sx; + } + /** * Precompile an XSL transform {@code source} and save it (for the * current session) as {@code name}. diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index ce7cad11..ecc7a875 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -32,6 +32,14 @@ static jobject s_OidOid; jobject Oid_create(Oid oid) { jobject joid; + /* + * This is a natural place to have a StaticAssertStmt making sure the + * ubiquitous PG type 'Oid' fits in a jint. If it is ever removed from here + * or this code goes away, it should go someplace else. If it ever produces + * an error, don't assume the only things that need fixing will be in this + * file or nearby.... + */ + StaticAssertStmt(sizeof(Oid) <= sizeof(jint), "Oid wider than jint?!"); if(OidIsValid(oid)) joid = JNI_newObject(s_Oid_class, s_Oid_init, oid); else diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 720166c7..4c3adfa5 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -16,6 +16,7 @@ #include "pljava/type/Type_priv.h" #include "pljava/VarlenaWrapper.h" +static TypeClass s_SQLXMLClass; static jclass s_SQLXML_class; static jmethodID s_SQLXML_adopt; static jclass s_SQLXML_Readable_class; @@ -23,6 +24,11 @@ static jmethodID s_SQLXML_Readable_init; static jclass s_SQLXML_Writable_class; static jmethodID s_SQLXML_Writable_init; +static bool _SQLXML_canReplaceType(Type self, Type other); +static jvalue _SQLXML_coerceDatum(Type self, Datum arg); +static Datum _SQLXML_coerceObject(Type self, jobject sqlxml); +static Type _SQLXML_obtain(Oid typeId); + /* * It is possible to install PL/Java in a PostgreSQL instance that was built * without libxml and the native XML data type. It could even be useful for @@ -43,15 +49,16 @@ static jvalue _SQLXML_coerceDatum(Type self, Datum arg) jvalue result; jobject vwi = pljava_VarlenaWrapper_Input( arg, TopTransactionContext, TopTransactionResourceOwner); - result.l = JNI_newObject( - s_SQLXML_Readable_class, s_SQLXML_Readable_init, vwi); + result.l = JNI_newObject(s_SQLXML_Readable_class, s_SQLXML_Readable_init, + vwi, Type_getOid(self)); JNI_deleteLocalRef(vwi); return result; } static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) { - jobject vw = JNI_callObjectMethodLocked(sqlxml, s_SQLXML_adopt); + jobject vw = JNI_callObjectMethodLocked( + sqlxml, s_SQLXML_adopt, Type_getOid(self)); Datum d = pljava_VarlenaWrapper_adopt(vw); JNI_deleteLocalRef(vw); #if PG_VERSION_NUM >= 90500 @@ -68,6 +75,33 @@ static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) return d; } +/* + * A Type can be 'registered' two ways. In one case, a single instance can be + * created with TypeClass_allocInstance(2)? and assigned a fixed Oid, and that + * instance then passed to Type_registerType along with the Java name. + * + * The other way is not to allocate any Type instance up front, but instead + * to call Type_registerType2, passing just the type's canonical Oid, the Java + * name, and an 'obtainer' function, like this one. + * + * The difference appears when this TypeClass has a _canReplaceType function + * that allows it to serve more than one PostgreSQL type (as, indeed, SQLXML + * now does and can). With the first registration style, the same Type instance + * will be used for any of the PostgreSQL types accepted by the _canReplaceType + * function. With the second style, the obtainer will be called to produce a + * distinct Type instance (sharing the same TypeClass) for each one, recording + * its own PostgreSQL Oid. + * + * SQLXML has a need to run a content verifier when 'bouncing' a readable + * instance back to PostgreSQL, and ideally only to do so when the Oids at + * create and adopt time are different, so it cannot make do with the singleton + * type instance, and needs to use Type_registerType2 with an obtainer. + */ +static Type _SQLXML_obtain(Oid typeId) +{ + return TypeClass_allocInstance(s_SQLXMLClass, typeId); +} + /* Make this datatype available to the postgres system. */ extern void pljava_SQLXMLImpl_initialize(void); @@ -90,26 +124,26 @@ void pljava_SQLXMLImpl_initialize(void) cls->canReplaceType = _SQLXML_canReplaceType; cls->coerceDatum = _SQLXML_coerceDatum; cls->coerceObject = _SQLXML_coerceObject; - Type_registerType( - "java.sql.SQLXML", - TypeClass_allocInstance( - cls, + s_SQLXMLClass = cls; + + Type_registerType2( #ifdef XMLOID /* it is possible to build PG without libxml */ XMLOID #else InvalidOid #endif - )); + , "java.sql.SQLXML", _SQLXML_obtain + ); s_SQLXML_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl")); s_SQLXML_adopt = PgObject_getJavaMethod(s_SQLXML_class, - "adopt", "()Lorg/postgresql/pljava/internal/VarlenaWrapper;"); + "adopt", "(I)Lorg/postgresql/pljava/internal/VarlenaWrapper;"); s_SQLXML_Readable_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl$Readable")); s_SQLXML_Readable_init = PgObject_getJavaMethod(s_SQLXML_Readable_class, - "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Input;)V"); + "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Input;I)V"); s_SQLXML_Writable_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl$Writable")); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 7ece0e3e..7218aa21 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -12,10 +12,12 @@ package org.postgresql.pljava.internal; import java.io.Closeable; -import java.io.IOException; +import java.io.FilterInputStream; import java.io.InputStream; import java.io.OutputStream; +import java.io.IOException; + import java.nio.ByteBuffer; import java.sql.SQLException; @@ -102,6 +104,54 @@ private Input(DualState.Key cookie, long resourceOwner, context, varlenaPtr, buf.asReadOnlyBuffer()); } + /** + * Apply a {@code Verifier} to the input data. + *

    + * This should only be necessary if the input wrapper is being used + * directly as an output item, and needs verification that it conforms + * to the format of the target type. + *

    + * The current position must be at the beginning of the stream. The + * verifier must leave it at the end to confirm the entire stream was + * examined. There should be no need to reset the position here, as the + * only anticipated use is during an {@code adopt}, and the native code + * will only care about the varlena's address. + */ + public void verify(Verifier v) throws SQLException + { + try + { + ByteBuffer buf = buffer(); + if ( 0 != buf.position() ) + throw new SQLException( + "Variable-length input data to be verified " + + " not positioned at start", + "55000"); + InputStream dontCloseMe = new FilterInputStream(this) + { + @Override + public void close() throws IOException { } + }; + v.verify(dontCloseMe); + if ( 0 != buf.remaining() ) + throw new SQLException("Verifier finished prematurely"); + } + catch ( SQLException sqe ) + { + throw sqe; + } + catch ( RuntimeException rte ) + { + throw rte; + } + catch ( Exception e ) + { + throw new SQLException( + "Error verifying variable-length input data, " + + "not otherwise provided for", "XX000", e); + } + } + @Override protected ByteBuffer buffer() throws IOException { @@ -809,7 +859,7 @@ public void finish() throws SQLException throw (RuntimeException) t; throw new SQLException( "Exception verifying variable-length data, not " + - "otherwise provided for", "XX000"); + "otherwise provided for", "XX000", exce); } if ( ! m_queue.isEmpty() ) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index d3b0330d..43f46266 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -250,6 +250,10 @@ protected SQLException normalizedException(Exception e) "XX000", e); } + /** + * Create a new, initially empty and writable, SQLXML instance, whose + * backing memory will in a transaction-scoped PostgreSQL memory context. + */ static SQLXML newWritable() { synchronized ( Backend.THREADLOCK ) @@ -258,7 +262,17 @@ static SQLXML newWritable() } } - protected abstract VarlenaWrapper adopt() throws SQLException; + /** + * Native code calls this method to claim complete control over the + * underlying {@code VarlenaWrapper} and dissociate it from Java. + * @param oid The PostgreSQL type ID the native code is expecting; + * see Readable.adopt for why that can matter. + * @return The underlying {@code VarlenaWrapper} (which has its own + * {@code adopt} method the native code will call next. + * @throws SQLException if this {@code SQLXML} instance is not in the + * proper state to be adoptable. + */ + protected abstract VarlenaWrapper adopt(int oid) throws SQLException; /** * Return a description of this object useful for debugging (not the raw @@ -499,10 +513,22 @@ static class Readable extends SQLXMLImpl private AtomicBoolean m_readable = new AtomicBoolean(true); private Charset m_serverCS = implServerCharset(); private boolean m_wrapped = false; + private final int m_pgTypeID; - private Readable(VarlenaWrapper.Input vwi) throws SQLException + /** + * Create a readable instance, when called by native code (the + * constructor is otherwise private, after all), passing an initialized + * {@code VarlenaWrapper} and the PostgreSQL type ID from which it has + * been created. + * @param vwi The already-created wrapper for reading the varlena from + * native memory. + * @param oid The PostgreSQL type ID from which this instance is being + * created (for why it matters, see {@code adopt}). + */ + private Readable(VarlenaWrapper.Input vwi, int oid) throws SQLException { super(vwi); + m_pgTypeID = oid; if ( null == m_serverCS ) { try @@ -647,15 +673,43 @@ public T getSource(Class sourceClass) sourceClass.getName() + ".class)", "0A000"); } + /** + * {@inheritDoc} + *

    + * This is the readable subclass, most typically used for data + * coming from PostgreSQL to Java. The only circumstance in which it can + * be {@code adopt}ed is if the Java code has left it untouched, and + * simply returned it from a function, or used it directly as a query + * parameter. + *

    + * That is a very efficient handoff with no superfluous copying of data. + * However, the backend is able to associate {@code SQLXML} instances + * with more than one PostgreSQL data type (as of this writing, it will + * allow XML or text, so that this API is usable in Java even if the + * PostgreSQL instance was not built with the XML type, or if, for some + * other reason, it is useful to apply Java XML processing to values in + * the database as text, without the overhead of a PG cast). + *

    + * It would break type safety to allow a {@code SQLXML} instance created + * from text (on which PostgreSQL does not impose any particular syntax) + * to be directly assigned to a PostgreSQL XML type without verifying + * that it is XML. For generality, the verification will be done here + * whenever the PostgreSQL oid at {@code adopt} time differs from the + * one saved at creation. Doing the verification is noticeably slower + * than not doing it, but that fast case has to be reserved for when + * there is no funny business with the PostgreSQL types. + */ @Override - protected VarlenaWrapper adopt() throws SQLException + protected VarlenaWrapper adopt(int oid) throws SQLException { - VarlenaWrapper vw = m_backing.getAndSet(null); + VarlenaWrapper.Input vw = m_backing.getAndSet(null); if ( ! m_readable.get() ) throw new SQLNonTransientException( "SQLXML object has already been read from", "55000"); if ( null == vw ) backingIfNotFreed(); /* shorthand way to throw the exception */ + if ( m_pgTypeID != oid ) + vw.verify(new Verifier()); return vw; } @@ -946,7 +1000,7 @@ private void serializeDOM(DOMResult r, OutputStream os) } @Override - protected VarlenaWrapper adopt() throws SQLException + protected VarlenaWrapper adopt(int oid) throws SQLException { VarlenaWrapper.Output vwo = m_backing.getAndSet(null); if ( m_writable.get() ) @@ -968,24 +1022,24 @@ protected String toString(Object o) return String.format("%s %swritable", super.toString(o), m_writable.get() ? "" : "not "); } + } - static class Verifier extends VarlenaWrapper.Verifier.Base + static class Verifier extends VarlenaWrapper.Verifier.Base + { + @Override + protected void verify(InputStream is) throws Exception { - @Override - protected void verify(InputStream is) throws Exception - { - boolean[] wrapped = { false }; - XMLReader xr = XMLReaderFactory.createXMLReader(); - xr.setFeature("http://xml.org/sax/features/namespaces", true); - is = correctedDeclStream( - is, false, implServerCharset(), wrapped); - /* - * What does an XMLReader do if no handlers have been set for - * content events? Parses everything and discards the events. - * Just what you'd want for a verifier. - */ - xr.parse(new InputSource(is)); - } + boolean[] wrapped = { false }; + XMLReader xr = XMLReaderFactory.createXMLReader(); + xr.setFeature("http://xml.org/sax/features/namespaces", true); + is = correctedDeclStream( + is, false, implServerCharset(), wrapped); + /* + * What does an XMLReader do if no handlers have been set for + * content events? Parses everything and discards the events. + * Just what you'd want for a verifier. + */ + xr.parse(new InputSource(is)); } } From 6851f98146342323812519cca42556bcc9f619e4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 8 Aug 2018 18:44:31 -0400 Subject: [PATCH 0140/1087] Changes belonging with issue #157 fix. These two uses of @SQLType were old workarounds of the same issue reported and fixed as #157, so are no longer needed. --- .../java/org/postgresql/pljava/management/Commands.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 97f74a6e..9964de7a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -51,7 +51,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.annotation.Function.Security.DEFINER; /** @@ -645,8 +644,7 @@ public static String getCurrentSchema() throws SQLException * @see #setClassPath */ @Function(schema="sqlj", name="install_jar", security=DEFINER) - public static void installJar( - @SQLType("bytea") byte[] image, String jarName, boolean deploy) + public static void installJar(byte[] image, String jarName, boolean deploy) throws SQLException { installJar("streamed byte image", jarName, deploy, image); @@ -735,8 +733,7 @@ public static void removeJar(String jarName, boolean undeploy) * @throws SQLException if the named jar cannot be found in the repository. */ @Function(schema="sqlj", name="replace_jar", security=DEFINER) - public static void replaceJar( - @SQLType("bytea") byte[] jarImage, String jarName, + public static void replaceJar(byte[] jarImage, String jarName, boolean redeploy) throws SQLException { replaceJar("streamed byte image", jarName, redeploy, jarImage); From e7f4dcaea536bf7b0cdc17c2afd7705338ba9047 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 8 Aug 2018 20:46:02 -0400 Subject: [PATCH 0141/1087] Belonged with commit fcde4f4. That commit fixed several regexps that used non-possessive quantifiers in places where backtracking would provably not be useful. But it missed these. --- .../java/org/postgresql/pljava/management/Commands.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 9964de7a..9132dee1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -439,13 +439,13 @@ public static void addClassImages(int jarId, InputStream urlStream, int sz) } private final static Pattern ddrSection = Pattern.compile( - "(?<=[\\r\\n])Name: ((?:.|(?:\\r\\n?|\\n) )+)(?:(?:\\r\\n?|\\n))" + - "(?:[^\\r\\n]+(?:\\r\\n?|\\n)(?![\\r\\n]))*" + - "SQLJDeploymentDescriptor: (?:(?:\\r\\n?|\\r) )*TRUE(?!\\S)", + "(?<=[\\r\\n])Name: ((?:.|(?:\\r\\n?+|\\n) )++)(?:\\r\\n?+|\\n)" + + "(?:[^\\r\\n]++(?:\\r\\n?+|\\n)(?![\\r\\n]))*" + + "SQLJDeploymentDescriptor: (?:(?:\\r\\n?+|\\r) )*+TRUE(?!\\S)", Pattern.CASE_INSENSITIVE ); - private final static Pattern mfCont = Pattern.compile( "(?:\\r\\n?|\\n) "); + private final static Pattern mfCont = Pattern.compile( "(?:\\r\\n?+|\\n) "); /** * Read and return a manifest, rewinding the buffered input stream. From 1a5caf1ced2ac53da7117292b5971c115b2e0264 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 12 Aug 2018 11:45:32 -0400 Subject: [PATCH 0142/1087] Lazy detoast, working in PG 11beta3, 10, 9.6. Introduce lazy detoasting, where if the value underlying a readable SQLXML instance is found not to be fully detoasted at the time of construction, it's allowed to stay that way until the contents are actually needed, as discussed in https://www.postgresql.org/message-id/1c64290b-b729-eeab-219e-1577a12e9b5a%40anastigmatix.net (and above and below in the same message thread), implementing the "just use the oldest one" snapshot selection strategy from https://www.postgresql.org/message-id/8ca78589-734b-f904-1cc5-007eeb5d4737%40anastigmatix.net The implementation passes the following test inspired by Andrew Gierth in https://www.postgresql.org/message-id/877eovbjc3.fsf%40news-spur.riddles.org.uk CREATE TABLE t(x xml); BEGIN READ WRITE, ISOLATION LEVEL READ COMMITTED; /* * In other session: INSERT INTO t(x) * SELECT table_to_xml('pg_operator', true, false, ''); */ SELECT javatest.echoxmlparameter(x, 0, 5) FROM t; -- 0 => stash x /* * In other session: DELETE FROM t; * VACUUM t; */ SELECT javatest.echoxmlparameter(null, 5, 5); -- null => unstash COMMIT; And, indeed, the same test is made to fail by commenting out the snapshot registrations/unregistrations in VarlenaWrapper.c, so they are performing the expected valuable service. Without them: ERROR: missing chunk number 0 for toast value 16716 in pg_toast_16708 --- pljava-so/src/main/c/DualState.c | 11 +- pljava-so/src/main/c/VarlenaWrapper.c | 204 +++++++++++++++++- .../postgresql/pljava/internal/DualState.java | 12 +- .../pljava/internal/VarlenaWrapper.java | 108 +++++++++- 4 files changed, 311 insertions(+), 24 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 9e0851dc..704a782e 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -112,7 +112,16 @@ static void resourceReleaseCB(ResourceReleasePhase phase, StaticAssertStmt(sizeof p2l.ptrVal <= sizeof p2l.longVal, "Pointer will not fit in long on this platform"); - if ( RESOURCE_RELEASE_AFTER_LOCKS != phase ) + /* + * The way ResourceOwnerRelease is implemented, callbacks to loadable + * modules (like us!) happen /after/ all of the built-in releasey actions + * for a particular phase. So, by looking for RESOURCE_RELEASE_LOCKS here, + * we actually end up executing after all the built-in lock-related stuff + * has been released, but before any of the built-in stuff released in the + * RESOURCE_RELEASE_AFTER_LOCKS phase. Which, at least for the currently + * implemented DualState subclasses, is about the right time. + */ + if ( RESOURCE_RELEASE_LOCKS != phase ) return; p2l.longVal = 0L; diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 69dea01a..b4f74512 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -11,6 +11,13 @@ * Chapman Flack */ +#include +#include +#include +#include +#include + +#include "org_postgresql_pljava_internal_VarlenaWrapper_Input_State.h" #include "org_postgresql_pljava_internal_VarlenaWrapper_Output_State.h" #include "pljava/VarlenaWrapper.h" #include "pljava/DualState.h" @@ -35,6 +42,8 @@ static jmethodID s_VarlenaWrapper_Input_init; static jmethodID s_VarlenaWrapper_Output_init; +static jfieldID s_VarlenaWrapper_Input_State_varlena; + /* * For VarlenaWrapper.Output, define a dead-simple "expanded object" format * consisting of linked allocated blocks, so if a long value is being written, @@ -121,32 +130,74 @@ jobject pljava_VarlenaWrapper_Input( jobject dbb; MemoryContext mc; MemoryContext prevcxt; - struct varlena *copy; + struct varlena *vl; Ptr2Long p2lro; Ptr2Long p2lcxt; + Ptr2Long p2lpin; Ptr2Long p2ldatum; + Size parked; + Size actual; + Snapshot pin = NULL; + + vl = (struct varlena *) DatumGetPointer(d); + if ( VARATT_IS_EXTERNAL_INDIRECT(vl) ) /* at most once; can't be nested */ + { + struct varatt_indirect redirect; + VARATT_EXTERNAL_GET_POINTER(redirect, vl); + vl = (struct varlena *)redirect.pointer; + d = PointerGetDatum(vl); + } + + parked = VARSIZE_ANY(vl); + actual = toast_raw_datum_size(d) - VARHDRSZ; mc = AllocSetContextCreate(parent, "PL/Java VarlenaWrapper.Input", ALLOCSET_START_SMALL_SIZES); prevcxt = MemoryContextSwitchTo(mc); - copy = PG_DETOAST_DATUM_COPY(d); + + if ( actual < 4096 || (actual >> 1) < parked ) + goto justDetoastEagerly; + if ( VARATT_IS_EXTERNAL_EXPANDED(vl) ) + goto justDetoastEagerly; + if ( VARATT_IS_EXTERNAL_ONDISK(vl) ) + { + pin = GetOldestSnapshot(); + if ( NULL == pin ) + goto justDetoastEagerly; + pin = RegisterSnapshotOnOwner(pin, ro); + } + +/* parkAndDetoastLazily: */ + vl = (struct varlena *) DatumGetPointer(datumCopy(d, false, -1)); + dbb = NULL; + goto constructResult; + +justDetoastEagerly: + vl = PG_DETOAST_DATUM_COPY(d); + parked = actual + VARHDRSZ; + dbb = JNI_newDirectByteBuffer(VARDATA(vl), actual); + +constructResult: MemoryContextSwitchTo(prevcxt); p2lro.longVal = 0L; p2lcxt.longVal = 0L; + p2lpin.longVal = 0L; p2ldatum.longVal = 0L; p2lro.ptrVal = ro; p2lcxt.ptrVal = mc; - p2ldatum.ptrVal = copy; - - dbb = JNI_newDirectByteBuffer(VARDATA(copy), VARSIZE_ANY_EXHDR(copy)); + p2lpin.ptrVal = pin; + p2ldatum.ptrVal = vl; vr = JNI_newObjectLocked(s_VarlenaWrapper_Input_class, s_VarlenaWrapper_Input_init, pljava_DualState_key(), - p2lro.longVal, p2lcxt.longVal, p2ldatum.longVal, dbb); - JNI_deleteLocalRef(dbb); + p2lro.longVal, p2lcxt.longVal, p2lpin.longVal, p2ldatum.longVal, + parked, actual, dbb); + + if ( NULL != dbb ) + JNI_deleteLocalRef(dbb); return vr; } @@ -298,7 +349,26 @@ static void VOS_flatten_into(ExpandedObjectHeader *eohptr, void pljava_VarlenaWrapper_initialize(void) { jclass clazz; - JNINativeMethod methods[] = + JNINativeMethod methodsIn[] = + { + { + "_unregisterSnapshot", + "(JJ)V", + Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unregisterSnapshot + }, + { + "_detoast", + "(JJJJ)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoast + }, + { + "_fetch", + "(JJ)J", + Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1fetch + }, + { 0, 0, 0 } + }; + JNINativeMethod methodsOut[] = { { "_nextBuffer", @@ -321,7 +391,7 @@ void pljava_VarlenaWrapper_initialize(void) s_VarlenaWrapper_Input_init = PgObject_getJavaMethod( s_VarlenaWrapper_Input_class, "", "(Lorg/postgresql/pljava/internal/DualState$Key;" - "JJJLjava/nio/ByteBuffer;)V"); + "JJJJJJLjava/nio/ByteBuffer;)V"); s_VarlenaWrapper_Output_init = PgObject_getJavaMethod( s_VarlenaWrapper_Output_class, "", @@ -332,12 +402,126 @@ void pljava_VarlenaWrapper_initialize(void) s_VarlenaWrapper_class, "adopt", "(Lorg/postgresql/pljava/internal/DualState$Key;)J"); + clazz = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/VarlenaWrapper$Input$State")); + + PgObject_registerNatives2(clazz, methodsIn); + + s_VarlenaWrapper_Input_State_varlena = PgObject_getJavaField( + clazz, "m_varlena", "J"); + + JNI_deleteLocalRef(clazz); + clazz = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/VarlenaWrapper$Output$State")); - PgObject_registerNatives2(clazz, methods); + + PgObject_registerNatives2(clazz, methodsOut); + JNI_deleteLocalRef(clazz); } +/* + * Class: org_postgresql_pljava_internal_VarlenaWrapper_Input_State + * Method: _unregisterSnapshot + * Signature: (JJ)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unregisterSnapshot + (JNIEnv *env, jobject _this, jlong snapshot, jlong ro) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2lsnap; + Ptr2Long p2lro; + p2lsnap.longVal = snapshot; + p2lro.longVal = ro; + UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); + END_NATIVE +} + +/* + * Class: org_postgresql_pljava_internal_VarlenaWrapper_Input_State + * Method: _detoast + * Signature: (JJJJ)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoast + (JNIEnv *env, jobject _this, jlong vl, jlong cxt, jlong snap, jlong resOwner) +{ + Ptr2Long p2lvl; + Ptr2Long p2lcxt; + Ptr2Long p2lsnap; + Ptr2Long p2lro; + Ptr2Long p2ldetoasted; + struct varlena *detoasted; + MemoryContext prevcxt; + jobject dbb; + + BEGIN_NATIVE_NO_ERRCHECK + + p2lvl.longVal = vl; + p2lcxt.longVal = cxt; + p2lsnap.longVal = snap; + p2lro.longVal = resOwner; + + prevcxt = MemoryContextSwitchTo((MemoryContext)p2lcxt.ptrVal); + + detoasted = PG_DETOAST_DATUM_COPY(PointerGetDatum(p2lvl.ptrVal)); + p2ldetoasted.longVal = 0L; + p2ldetoasted.ptrVal = detoasted; + + MemoryContextSwitchTo(prevcxt); + + JNI_setLongField(_this, + s_VarlenaWrapper_Input_State_varlena, p2ldetoasted.longVal); + pfree(p2lvl.ptrVal); + + if ( 0 != snap ) + UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); + + dbb = JNI_newDirectByteBuffer( + VARDATA(detoasted), VARSIZE_ANY_EXHDR(detoasted)); + + END_NATIVE + + return dbb; +} + +/* + * Class: org_postgresql_pljava_internal_VarlenaWrapper_Input_State + * Method: _fetch + * Signature: (JJ)J + * + * Assumption: this is only called when a snapshot has been registered (meaning + * the varlena is EXTERNAL_ONDISK) and the snapshot is soon to be unregistered. + * All that's needed is to 'fetch' the representation from disk, in case the + * toast rows could be subject to vacuuming after the snapshot is unregistered. + * A fetch is not a full detoast; if what's fetched is compressed, it stays + * compressed. This method does not need to unregister the snapshot, as that + * will happen soon anyway. It does pfree the toast pointer. + */ +JNIEXPORT jlong JNICALL Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1fetch + (JNIEnv *env, jobject _this, jlong varlena, jlong memContext) +{ + Ptr2Long p2lvl; + Ptr2Long p2lcxt; + MemoryContext prevcxt; + struct varlena *fetched; + + BEGIN_NATIVE_NO_ERRCHECK; + p2lvl.longVal = varlena; + p2lcxt.longVal = memContext; + + prevcxt = MemoryContextSwitchTo((MemoryContext) p2lcxt.ptrVal); + fetched = heap_tuple_fetch_attr((struct varlena *)p2lvl.ptrVal); + pfree(p2lvl.ptrVal); + p2lvl.longVal = 0L; + p2lvl.ptrVal = fetched; + MemoryContextSwitchTo(prevcxt); + + END_NATIVE; + return p2lvl.longVal; +} + /* * Class: org_postgresql_pljava_internal_VarlenaWrapper_Output_State * Method: _nextBuffer diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 8c23980c..5f71825f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -101,7 +101,7 @@ public abstract class DualState extends WeakReference * Pointer value of the {@code ResourceOwner} this instance belongs to, * if any. */ - private final long m_resourceOwner; + protected final long m_resourceOwner; /** * Check that a cookie is valid, throwing an unchecked exception otherwise. @@ -291,6 +291,13 @@ public String toString(Object o) * @param resourceOwner Pointer value identifying the resource owner being * released. Calls can be received for resource owners to which no instances * here have been registered. + *

    + * Some state subclasses may have their nativeStateReleased methods called + * from Java code, when it is clear the native state is no longer needed in + * Java. That doesn't remove the state instance from s_liveInstances though, + * so it will still eventually be seen by this loop and efficiently removed + * by the iterator. Hence the nativeStateIsValid test, to avoid invoking + * nativeStateReleased more than once. */ private static void resourceOwnerRelease(long resourceOwner) { @@ -303,7 +310,8 @@ private static void resourceOwnerRelease(long resourceOwner) i.remove(); synchronized ( s ) { - s.nativeStateReleased(); + if ( s.nativeStateIsValid() ) + s.nativeStateReleased(); } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 7218aa21..6945c43c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -85,23 +85,36 @@ public interface VarlenaWrapper extends Closeable public static class Input extends ByteBufferInputStream implements VarlenaWrapper { + private long m_parkedSize; + private long m_bufferSize; + /** * Construct a {@code VarlenaWrapper.Input}. * @param cookie Capability held by native code. * @param resourceOwner Resource owner whose release will indicate that the * underlying varlena is no longer valid. * @param context Memory context in which the varlena is allocated. + * @param snapshot A snapshot that has been registered in case the + * parked varlena is TOASTed on disk, to keep the toast tuples from + * being vacuumed away. * @param varlenaPtr Pointer value to the underlying varlena, to be * {@code pfree}d when Java code closes or reclaims this object. + * @param parkedSize Size occupied by this datum in memory while it is + * "parked", that is, before the first call to a reading method. + * @param bufferSize Size that is or will be occupied by the detoasted + * content once a reading method has been called. * @param buf Readable direct {@code ByteBuffer} constructed over the * varlena's data bytes. */ private Input(DualState.Key cookie, long resourceOwner, - long context, long varlenaPtr, ByteBuffer buf) + long context, long snapshot, long varlenaPtr, + long parkedSize, long bufferSize, ByteBuffer buf) { + m_parkedSize = parkedSize; + m_bufferSize = bufferSize; m_state = new State( cookie, this, resourceOwner, - context, varlenaPtr, buf.asReadOnlyBuffer()); + context, snapshot, varlenaPtr, buf); } /** @@ -199,7 +212,8 @@ public String toString() @Override public String toString(Object o) { - return String.format("%s %s", ((State)m_state).toString(o), + return String.format("%s parked:%d buffer:%d %s", + ((State)m_state).toString(o), m_parkedSize, m_bufferSize, m_open ? "open" : "closed"); } @@ -209,27 +223,46 @@ private static class State extends DualState.SingleMemContextDelete { private ByteBuffer m_buf; + private long m_snapshot; private long m_varlena; private State( DualState.Key cookie, Input vr, long resourceOwner, - long memContext, long varlenaPtr, ByteBuffer buf) + long memContext, long snapshot, long varlenaPtr, ByteBuffer buf) { super(cookie, vr, resourceOwner, memContext); + m_snapshot = snapshot; m_varlena = varlenaPtr; - m_buf = buf; + m_buf = null == buf ? buf : buf.asReadOnlyBuffer(); } private ByteBuffer buffer() throws SQLException { - assertNativeStateIsValid(); + long ctx = getMemoryContext(); + if ( null == m_buf ) + { + synchronized ( Backend.THREADLOCK ) + { + m_buf = _detoast( + m_varlena, ctx, m_snapshot, m_resourceOwner) + .asReadOnlyBuffer(); + m_snapshot = 0; + } + } return m_buf; } private long adopt(DualState.Key cookie) throws SQLException { checkCookie(cookie); - assertNativeStateIsValid(); + long ctx = getMemoryContext(); + if ( 0 != m_snapshot ) /* fetch now, before snapshot released */ + { + synchronized ( Backend.THREADLOCK ) + { + m_varlena = _fetch(m_varlena, ctx); + } + } long varlena = m_varlena; nativeStateReleased(); return varlena; @@ -238,24 +271,77 @@ private long adopt(DualState.Key cookie) throws SQLException @Override protected void nativeStateReleased() { + synchronized ( Backend.THREADLOCK ) + { + super.nativeStateReleased(); + /* + * You might not expect to have to explicitly unregister a + * snapshot from the resource owner that is at this very + * moment being released, and will happily unregister the + * snapshot itself in the course of so doing. Ah, but it + * also happily logs a warning when it does that, so we need + * to have our toys picked up before it gets the chance. + */ + if ( 0 != m_snapshot ) + _unregisterSnapshot(m_snapshot, m_resourceOwner); + m_snapshot = 0; + } m_buf = null; - super.nativeStateReleased(); } @Override protected void javaStateReleased() { + synchronized ( Backend.THREADLOCK ) + { + super.javaStateReleased(); + if ( 0 != m_snapshot ) + _unregisterSnapshot(m_snapshot, m_resourceOwner); + m_snapshot = 0; + } m_buf = null; - super.javaStateReleased(); } @Override public String toString(Object o) { - return String.format("%s varlena:%x %s", - super.toString(o), m_varlena, + return String.format("%s snap:%x varlena:%x %s", + super.toString(o), m_snapshot, m_varlena, String.valueOf(m_buf).replace("java.nio.", "")); } + + /** + * Unregister a snapshot we've been holding. + */ + private native void + _unregisterSnapshot(long snap, long resOwner); + + /** + * Detoast the parked value; called when a method needing to read it + * has been invoked. + *

    + * Detoast the passed {@code varlena} into the same + * {@code memContext}, {@code pfree} the original, update the + * {@code m_varlena} instance field to point to the detoasted copy, + * and return a direct byte buffer that windows it. + *

    + * If {@code snapshot} is nonzero, unregister the snapshot from the + * resource owner. The caller may rely on this happening, and + * confidently set {@code m_snapshot} to zero after this call. + */ + private native ByteBuffer _detoast( + long varlena, long memContext, long snapshot, long resOwner); + + /** + * Merely fetch a parked value, when it does not need to be fully + * detoasted and readable, but simply retrieved from its TOAST rows + * before loss of the snapshot that may be protecting them from + * VACUUM. The original value is {@code pfree}d. + *

    + * The result may still have an 'extended' (for example, compressed) + * form. + */ + private native long _fetch(long varlena, long memContext); } } From cfe42fc52cc31f8e230276e9535589299a44b376 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 11 Aug 2018 22:43:52 -0400 Subject: [PATCH 0143/1087] Lazy detoast for 9.5 ... 8.2. Before 9.6, there was no GetOldestSnapshot(). There may have been some more roundabout way to dig up a suitable snapshot to protect the toast rows, but instead, just give up on parking toast pointers and park the fetched-but-not-uncompressed contents instead. With XML that can still be an easy factor of 20 or so smaller. In theory, pre-8.4, it might be possible to park toast pointers again, because back then they were safe from vacuuming as long as the overall transaction remains open. But for versions that old, who has the time? --- pljava-so/src/main/c/VarlenaWrapper.c | 83 +++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index b4f74512..3f2d28a7 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -14,8 +14,13 @@ #include #include #include + +#if PG_VERSION_NUM >= 80400 #include #include +#else +#define RegisterSnapshotOnOwner(s,o) NULL +#endif #include "org_postgresql_pljava_internal_VarlenaWrapper_Input_State.h" #include "org_postgresql_pljava_internal_VarlenaWrapper_Output_State.h" @@ -25,9 +30,40 @@ #include "pljava/PgObject.h" #include "pljava/JNICalls.h" +#if PG_VERSION_NUM < 90600 +#define GetOldestSnapshot() NULL +#endif + +#if PG_VERSION_NUM < 90400 +/* + * There aren't 'indirect' varlenas yet, IS_EXTERNAL_ONDISK is just IS_EXTERNAL, + * and VARATT_EXTERNAL_GET_POINTER is private inside tuptoaster.c; copy it here. + */ +#define VARATT_IS_EXTERNAL_ONDISK VARATT_IS_EXTERNAL +#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \ +do { \ + varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \ + memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \ +} while (0) +#endif + #if PG_VERSION_NUM < 80300 +#define VARSIZE_ANY(PTR) VARSIZE(PTR) #define VARSIZE_ANY_EXHDR(PTR) (VARSIZE(PTR) - VARHDRSZ) #define SET_VARSIZE(PTR, len) VARATT_SIZEP(PTR) = len & VARATT_MASK_SIZE +struct varatt_external +{ + int32 va_extsize; /* the only piece used here */ +}; +#undef VARATT_EXTERNAL_GET_POINTER +#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \ +do { \ + (toast_pointer).va_extsize = \ + ((varattrib *)(attr))->va_content.va_external.va_extsize; \ +} while (0) +#define _VL_TYPE varattrib * +#else +#define _VL_TYPE struct varlena * #endif #define INITIALSIZE 1024 @@ -83,6 +119,7 @@ struct ExpandedObjectHeader #define EOHPGetRWDatum(eohptr) (eohptr) #define DatumGetEOHP(d) (d) +#define VARATT_IS_EXTERNAL_EXPANDED(attr) false #endif static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr); @@ -130,7 +167,7 @@ jobject pljava_VarlenaWrapper_Input( jobject dbb; MemoryContext mc; MemoryContext prevcxt; - struct varlena *vl; + _VL_TYPE vl; Ptr2Long p2lro; Ptr2Long p2lcxt; Ptr2Long p2lpin; @@ -139,14 +176,17 @@ jobject pljava_VarlenaWrapper_Input( Size actual; Snapshot pin = NULL; - vl = (struct varlena *) DatumGetPointer(d); + vl = (_VL_TYPE) DatumGetPointer(d); + +#if PG_VERSION_NUM >= 90400 if ( VARATT_IS_EXTERNAL_INDIRECT(vl) ) /* at most once; can't be nested */ { struct varatt_indirect redirect; VARATT_EXTERNAL_GET_POINTER(redirect, vl); - vl = (struct varlena *)redirect.pointer; + vl = (_VL_TYPE)redirect.pointer; d = PointerGetDatum(vl); } +#endif parked = VARSIZE_ANY(vl); actual = toast_raw_datum_size(d) - VARHDRSZ; @@ -164,17 +204,32 @@ jobject pljava_VarlenaWrapper_Input( { pin = GetOldestSnapshot(); if ( NULL == pin ) - goto justDetoastEagerly; + { + /* + * Unable to register a snapshot and just park the tiny pointer. + * If it points to compressed data, can still park that rather than + * fully detoasting. + */ + struct varatt_external toast_pointer; + VARATT_EXTERNAL_GET_POINTER(toast_pointer, vl); + parked = toast_pointer.va_extsize + VARHDRSZ; + if ( (actual >> 1) < parked ) /* not compressed enough to bother */ + goto justDetoastEagerly; + vl = heap_tuple_fetch_attr(vl); /* fetch without decompressing */ + d = PointerGetDatum(vl); + dbb = NULL; + goto constructResult; + } pin = RegisterSnapshotOnOwner(pin, ro); } /* parkAndDetoastLazily: */ - vl = (struct varlena *) DatumGetPointer(datumCopy(d, false, -1)); + vl = (_VL_TYPE) DatumGetPointer(datumCopy(d, false, -1)); dbb = NULL; goto constructResult; justDetoastEagerly: - vl = PG_DETOAST_DATUM_COPY(d); + vl = (_VL_TYPE) PG_DETOAST_DATUM_COPY(d); parked = actual + VARHDRSZ; dbb = JNI_newDirectByteBuffer(VARDATA(vl), actual); @@ -429,6 +484,7 @@ JNIEXPORT void JNICALL Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unregisterSnapshot (JNIEnv *env, jobject _this, jlong snapshot, jlong ro) { +#if PG_VERSION_NUM >= 80400 BEGIN_NATIVE_NO_ERRCHECK Ptr2Long p2lsnap; Ptr2Long p2lro; @@ -436,6 +492,7 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unreg p2lro.longVal = ro; UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); END_NATIVE +#endif } /* @@ -449,10 +506,12 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa { Ptr2Long p2lvl; Ptr2Long p2lcxt; +#if PG_VERSION_NUM >= 80400 Ptr2Long p2lsnap; Ptr2Long p2lro; +#endif Ptr2Long p2ldetoasted; - struct varlena *detoasted; + _VL_TYPE detoasted; MemoryContext prevcxt; jobject dbb; @@ -460,12 +519,14 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa p2lvl.longVal = vl; p2lcxt.longVal = cxt; +#if PG_VERSION_NUM >= 80400 p2lsnap.longVal = snap; p2lro.longVal = resOwner; +#endif prevcxt = MemoryContextSwitchTo((MemoryContext)p2lcxt.ptrVal); - detoasted = PG_DETOAST_DATUM_COPY(PointerGetDatum(p2lvl.ptrVal)); + detoasted = (_VL_TYPE) PG_DETOAST_DATUM_COPY(PointerGetDatum(p2lvl.ptrVal)); p2ldetoasted.longVal = 0L; p2ldetoasted.ptrVal = detoasted; @@ -475,8 +536,10 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa s_VarlenaWrapper_Input_State_varlena, p2ldetoasted.longVal); pfree(p2lvl.ptrVal); +#if PG_VERSION_NUM >= 80400 if ( 0 != snap ) UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); +#endif dbb = JNI_newDirectByteBuffer( VARDATA(detoasted), VARSIZE_ANY_EXHDR(detoasted)); @@ -505,14 +568,14 @@ JNIEXPORT jlong JNICALL Java_org_postgresql_pljava_internal_VarlenaWrapper_00024 Ptr2Long p2lvl; Ptr2Long p2lcxt; MemoryContext prevcxt; - struct varlena *fetched; + _VL_TYPE fetched; BEGIN_NATIVE_NO_ERRCHECK; p2lvl.longVal = varlena; p2lcxt.longVal = memContext; prevcxt = MemoryContextSwitchTo((MemoryContext) p2lcxt.ptrVal); - fetched = heap_tuple_fetch_attr((struct varlena *)p2lvl.ptrVal); + fetched = heap_tuple_fetch_attr((_VL_TYPE) p2lvl.ptrVal); pfree(p2lvl.ptrVal); p2lvl.longVal = 0L; p2lvl.ptrVal = fetched; From ec721fff9b124eeef2fe92337bc9e206b100521a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 12 Aug 2018 19:23:56 -0400 Subject: [PATCH 0144/1087] PassXML example with Java param/return type String. Example to confirm that existing code using String as the Java method parameter and return types for PG XML continues to work as expected. --- .../postgresql/pljava/example/annotation/PassXML.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 a29c355d..a2d88186 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 @@ -378,6 +378,16 @@ public static SQLXML lowLevelXMLEcho_(@SQLType("text") SQLXML sx, int how) return lowLevelXMLEcho(sx, how); } + /** + * Low-level XML echo where the Java parameter and return type are String. + */ + @Function(schema="javatest", implementor="postgresql_xml", type="xml") + public static String lowLevelXMLEcho(@SQLType("xml") String x) + throws SQLException + { + return x; + } + /** * Create some XML, pass it to a {@code SELECT ?} prepared statement, * retrieve it from the result set, and return it via the out-parameter From 1a23379c3db6c4958cfe28f9afad71928bf1c049 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 18 Aug 2018 12:41:42 -0400 Subject: [PATCH 0145/1087] Test for when cursor/portal gets closed. The _pljavaPortalCleanup callback actually doesn't get called until end of transaction. But SPI_finish gets called at routine exit, which is enough to make later use of the portal fail, if SPI_connect isn't called first. --- .../example/annotation/Holdability.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java new file mode 100644 index 00000000..6f7aa1ee --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018- 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.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.postgresql.pljava.ResultSetHandle; +import org.postgresql.pljava.annotation.Function; + +/** + * Demonstrate holdability of ResultSets (test for issue 168). + *

    + * The {@code stashResultSet} method will execute a query and save its + * {@code ResultSet} (wrapped in a {@code ResultSetHandle} in a static + * for later retrieval. The {@code unstashResultSet} method, called later + * in the same transaction, retrieves and returns the result set. A call after + * the transaction has ended will fail. + *

    + * The query selects all rows from {@code pg_description}, a table that should + * always exist, with more rows than the default connection {@code fetchSize}, + * to ensure the stashed {@code ResultSet} has work to do. + */ +public class Holdability implements ResultSetHandle +{ + private static Holdability s_stash; + + private ResultSet m_resultSet; + private Statement m_stmt; + + private Holdability(Statement s, ResultSet rs) + { + m_stmt = s; + m_resultSet = rs; + } + + /** + * Query all rows from {@code pg_description}, but stash the + * {@code ResultSet} for retrieval later in the same transaction by + * {@code unstashResultSet}. + *

    + * This must be called in an open, multiple-statement (non-auto) transaction + * to have any useful effect. + */ + @Function(schema="javatest") + public static void stashResultSet() throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + Statement s = c.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT * FROM pg_catalog.pg_description"); + s_stash = new Holdability(s, rs); + } + + /** + * Return the results stashed earlier in the same transaction by + * {@code stashResultSet}. + */ + @Function(schema="javatest", type="pg_catalog.pg_description") + public static ResultSetHandle unstashResultSet() throws SQLException + { + return s_stash; + } + + /* + * Necessary methods to implement ResultSetHandle follow. + */ + + @Override + public ResultSet getResultSet() throws SQLException + { + return m_resultSet; + } + + @Override + public void close() throws SQLException + { + Connection c = m_stmt.getConnection(); + m_stmt.close(); + c.close(); + } +} From be5e9db556790cefb57d4b76de90a8a8ebe3524c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 18 Aug 2018 14:21:22 -0400 Subject: [PATCH 0146/1087] Ensure SPI is connected to use Portal SPI methods. Using a ResultSet after return of the PL/Java function that created it, during the same open transaction (consistent with the claimed holdability of CLOSE_CURSORS_AT_COMMIT) now works. Update various documentation comments saying it didn't. Also fix a couple wrong return values in SPIDatabaseMetaData concerning holdability, and, in passing, a wrong error message in Invocation_assertConnect. Add a deployment-time test in the Holdability example. --- .../example/annotation/Holdability.java | 21 +++++++++++++++++-- pljava-so/src/main/c/Invocation.c | 16 ++++++++------ pljava-so/src/main/c/type/Portal.c | 2 ++ .../postgresql/pljava/jdbc/ResultSetBase.java | 18 +++++++++------- .../postgresql/pljava/jdbc/SPIConnection.java | 3 +-- .../pljava/jdbc/SPIDatabaseMetaData.java | 4 ++-- .../pljava/jdbc/SingleRowResultSet.java | 20 +++++++++++------- .../pljava/jdbc/SingleRowWriter.java | 9 -------- 8 files changed, 58 insertions(+), 35 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java index 6f7aa1ee..73d4a354 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java @@ -18,7 +18,9 @@ import java.sql.SQLException; import org.postgresql.pljava.ResultSetHandle; + import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; /** * Demonstrate holdability of ResultSets (test for issue 168). @@ -33,6 +35,17 @@ * always exist, with more rows than the default connection {@code fetchSize}, * to ensure the stashed {@code ResultSet} has work to do. */ +@SQLAction(requires={"Holdability.stash", "Holdability.unstash"}, install={ + + "SELECT javatest.stashResultSet()", + + "SELECT " + + " CASE" + + " WHEN 1000 < count(*) THEN javatest.logmessage('INFO', 'Holdability OK')"+ + " ELSE javatest.logmessage('WARNING', 'Holdability suspicious')" + + " END" + + " FROM javatest.unstashResultSet()" +}) public class Holdability implements ResultSetHandle { private static Holdability s_stash; @@ -54,7 +67,7 @@ private Holdability(Statement s, ResultSet rs) * This must be called in an open, multiple-statement (non-auto) transaction * to have any useful effect. */ - @Function(schema="javatest") + @Function(schema="javatest", provides="Holdability.stash") public static void stashResultSet() throws SQLException { Connection c = DriverManager.getConnection("jdbc:default:connection"); @@ -68,7 +81,11 @@ public static void stashResultSet() throws SQLException * Return the results stashed earlier in the same transaction by * {@code stashResultSet}. */ - @Function(schema="javatest", type="pg_catalog.pg_description") + @Function( + schema="javatest", + type="pg_catalog.pg_description", + provides="Holdability.unstash" + ) public static ResultSetHandle unstashResultSet() throws SQLException { return s_stash; diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 1af40e57..501f7b39 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include #include @@ -88,7 +92,7 @@ void Invocation_assertConnect(void) { rslt = SPI_connect(); if ( SPI_OK_CONNECT != rslt ) - elog(ERROR, "SPI_register_trigger_data returned %s", + elog(ERROR, "SPI_connect returned %s", SPI_result_code_string(rslt)); #if PG_VERSION_NUM >= 100000 if ( NULL != currentInvocation->triggerData ) diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index b9ba3030..166a4adf 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -193,6 +193,7 @@ Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jl p2l.longVal = _this; PG_TRY(); { + Invocation_assertConnect(); SPI_cursor_fetch((Portal)p2l.ptrVal, forward == JNI_TRUE, (long)count); result = (jlong)SPI_processed; @@ -340,6 +341,7 @@ Java_org_postgresql_pljava_internal_Portal__1move(JNIEnv* env, jclass clazz, jlo p2l.longVal = _this; PG_TRY(); { + Invocation_assertConnect(); SPI_cursor_move((Portal)p2l.ptrVal, forward == JNI_TRUE, (long)count); result = (jlong)SPI_processed; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java index f6ccc1c0..9793ea03 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2005-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Filip Hrbek + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -167,8 +172,7 @@ public boolean isClosed() } /** - * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. Cursors - * are actually closed when a function returns to SQL. + * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. */ public int getHoldability() throws SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 4b2d0d22..126262c8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -148,8 +148,7 @@ public static Connection getDefault() } /** - * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. Cursors are actually - * closed when a function returns to SQL. + * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. */ @Override public int getHoldability() diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index 76016f20..1748e1bd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -3344,7 +3344,7 @@ public ResultSet getAttributes(String catalog, String schemaPattern, public boolean supportsResultSetHoldability(int holdability) throws SQLException { - return true; + return ResultSet.CLOSE_CURSORS_AT_COMMIT == holdability; } /** @@ -3359,7 +3359,7 @@ public boolean supportsResultSetHoldability(int holdability) */ public int getResultSetHoldability() throws SQLException { - return ResultSet.HOLD_CURSORS_OVER_COMMIT; + return ResultSet.CLOSE_CURSORS_AT_COMMIT; } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java index 042680f0..5c3dd7e9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -281,8 +286,9 @@ public void updateObject(int columnIndex, Object x, int scale) // ************************************************************ /** - * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. Cursors - * are actually closed when a function returns to SQL. + * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. A single-row result + * set serves a special purpose in the call or return of a function, and is + * not guaranteed to be usable beyond that function's return. */ public int getHoldability() { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java index 9ffca389..cf0e64eb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java @@ -152,15 +152,6 @@ public boolean isClosed() return m_tuple == null; } - /** - * Returns {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. Cursors - * are actually closed when a function returns to SQL. - */ - public int getHoldability() - { - return ResultSet.CLOSE_CURSORS_AT_COMMIT; - } - // ************************************************************ // End of implementation of JDBC 4 methods. // ************************************************************ From 0707b899b0d1b97606bbfa113877fa92192f3ed4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 12 Aug 2018 21:00:34 -0400 Subject: [PATCH 0147/1087] Override annotations in AbstractResultSet. Just to make it easier to see what's what. With a small amount of reordering. --- .../pljava/jdbc/AbstractResultSet.java | 203 +++++++++++++++--- 1 file changed, 169 insertions(+), 34 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index 4c438b81..4140fa0d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -36,8 +36,8 @@ import java.util.Map; /** - * The AbstractResultSet serves as a base class for implementations - * of the{@link java.sql.ResultSet} interface. All calls using columnNames are + * The {@code AbstractResultSet} serves as a base class for implementations + * of the {@link java.sql.ResultSet} interface. All calls using columnNames are * translated into the corresponding call with index position computed using * a call to {@link java.sql.ResultSet#findColumn(String) findColumn}. * @@ -45,18 +45,26 @@ */ public abstract class AbstractResultSet implements ResultSet { + // ************************************************************ + // Pre-JDBC 4 + // Getters-by-columnName mapped to getters-by-columnIndex + // ************************************************************ + + @Override public Array getArray(String columnName) throws SQLException { return this.getArray(this.findColumn(columnName)); } + @Override public InputStream getAsciiStream(String columnName) throws SQLException { return this.getAsciiStream(this.findColumn(columnName)); } + @Override public BigDecimal getBigDecimal(String columnName) throws SQLException { @@ -66,166 +74,161 @@ public BigDecimal getBigDecimal(String columnName) /** * @deprecated */ + @Override public BigDecimal getBigDecimal(String columnName, int scale) throws SQLException { return this.getBigDecimal(this.findColumn(columnName), scale); } + @Override public InputStream getBinaryStream(String columnName) throws SQLException { return this.getBinaryStream(this.findColumn(columnName)); } + @Override public Blob getBlob(String columnName) throws SQLException { return this.getBlob(this.findColumn(columnName)); } + @Override public boolean getBoolean(String columnName) throws SQLException { return this.getBoolean(this.findColumn(columnName)); } + @Override public byte getByte(String columnName) throws SQLException { return this.getByte(this.findColumn(columnName)); } + @Override public byte[] getBytes(String columnName) throws SQLException { return this.getBytes(this.findColumn(columnName)); } + @Override public Reader getCharacterStream(String columnName) throws SQLException { return this.getCharacterStream(this.findColumn(columnName)); } + @Override public Clob getClob(String columnName) throws SQLException { return this.getClob(this.findColumn(columnName)); } - public String getCursorName() - throws SQLException - { - return null; - } - + @Override public Date getDate(String columnName) throws SQLException { return this.getDate(this.findColumn(columnName)); } + @Override public Date getDate(String columnName, Calendar cal) throws SQLException { return this.getDate(this.findColumn(columnName), cal); } + @Override public double getDouble(String columnName) throws SQLException { return this.getDouble(this.findColumn(columnName)); } + @Override public float getFloat(String columnName) throws SQLException { return this.getFloat(this.findColumn(columnName)); } + @Override public int getInt(String columnName) throws SQLException { return this.getInt(this.findColumn(columnName)); } + @Override public long getLong(String columnName) throws SQLException { return this.getLong(this.findColumn(columnName)); } + @Override public Object getObject(String columnName) throws SQLException { return this.getObject(this.findColumn(columnName)); } + @Override public Object getObject(String columnName, Map map) throws SQLException { return this.getObject(this.findColumn(columnName), map); } - public T getObject(int columnIndex, Class type) - throws SQLException - { - final Object obj = getObject( columnIndex ); - if ( obj.getClass().equals( type ) ) return (T) obj; - throw new SQLException( "Cannot convert " + obj.getClass().getName() + " to " + type ); - } - - public T getObject(String columnName, Class type) - throws SQLException - { - final Object obj = getObject( columnName ); - if ( obj.getClass().equals( type ) ) return (T) obj; - throw new SQLException( "Cannot convert " + obj.getClass().getName() + " to " + type ); - } - + @Override public Ref getRef(String columnName) throws SQLException { return this.getRef(this.findColumn(columnName)); } + @Override public short getShort(String columnName) throws SQLException { return this.getShort(this.findColumn(columnName)); } - public Statement getStatement() - throws SQLException - { - return null; - } - + @Override public String getString(String columnName) throws SQLException { return this.getString(this.findColumn(columnName)); } + @Override public Time getTime(String columnName) throws SQLException { return this.getTime(this.findColumn(columnName)); } + @Override public Time getTime(String columnName, Calendar cal) throws SQLException { return this.getTime(this.findColumn(columnName), cal); } + @Override public Timestamp getTimestamp(String columnName) throws SQLException { return this.getTimestamp(this.findColumn(columnName)); } + @Override public Timestamp getTimestamp(String columnName, Calendar cal) throws SQLException { @@ -235,156 +238,212 @@ public Timestamp getTimestamp(String columnName, Calendar cal) /** * @deprecated */ + @Override public InputStream getUnicodeStream(String columnName) throws SQLException { return this.getUnicodeStream(this.findColumn(columnName)); } + @Override public URL getURL(String columnName) throws SQLException { return this.getURL(this.findColumn(columnName)); } + // ************************************************************ + // Pre-JDBC 4 + // Updaters-by-columnName mapped to updaters-by-columnIndex + // ************************************************************ + + @Override public void updateArray(String columnName, Array x) throws SQLException { this.updateArray(this.findColumn(columnName), x); } + @Override public void updateAsciiStream(String columnName, InputStream x, int length) throws SQLException { this.updateAsciiStream(this.findColumn(columnName), x, length); } + @Override public void updateBigDecimal(String columnName, BigDecimal x) throws SQLException { this.updateBigDecimal(this.findColumn(columnName), x); } + @Override public void updateBinaryStream(String columnName, InputStream x, int length) throws SQLException { this.updateBinaryStream(this.findColumn(columnName), x, length); } + @Override public void updateBlob(String columnName, Blob x) throws SQLException { this.updateBlob(this.findColumn(columnName), x); } + @Override public void updateBoolean(String columnName, boolean x) throws SQLException { this.updateBoolean(this.findColumn(columnName), x); } + @Override public void updateByte(String columnName, byte x) throws SQLException { this.updateByte(this.findColumn(columnName), x); } + @Override public void updateBytes(String columnName, byte x[]) throws SQLException { this.updateBytes(this.findColumn(columnName), x); } + @Override public void updateCharacterStream(String columnName, Reader x, int length) throws SQLException { this.updateCharacterStream(this.findColumn(columnName), x, length); } + @Override public void updateClob(String columnName, Clob x) throws SQLException { this.updateClob(this.findColumn(columnName), x); } + @Override public void updateDate(String columnName, Date x) throws SQLException { this.updateDate(this.findColumn(columnName), x); } + @Override public void updateDouble(String columnName, double x) throws SQLException { this.updateDouble(this.findColumn(columnName), x); } + @Override public void updateFloat(String columnName, float x) throws SQLException { this.updateFloat(this.findColumn(columnName), x); } + @Override public void updateInt(String columnName, int x) throws SQLException { this.updateInt(this.findColumn(columnName), x); } + @Override public void updateLong(String columnName, long x) throws SQLException { this.updateLong(this.findColumn(columnName), x); } + @Override public void updateNull(String columnName) throws SQLException { this.updateNull(this.findColumn(columnName)); } + @Override public void updateObject(String columnName, Object x) throws SQLException { this.updateObject(this.findColumn(columnName), x); } + @Override public void updateObject(String columnName, Object x, int scale) throws SQLException { this.updateObject(this.findColumn(columnName), x, scale); } + @Override public void updateRef(String columnName, Ref x) throws SQLException { this.updateRef(this.findColumn(columnName), x); } + @Override public void updateShort(String columnName, short x) throws SQLException { this.updateShort(this.findColumn(columnName), x); } + @Override public void updateString(String columnName, String x) throws SQLException { this.updateString(this.findColumn(columnName), x); } + @Override public void updateTime(String columnName, Time x) throws SQLException { this.updateTime(this.findColumn(columnName), x); } + @Override public void updateTimestamp(String columnName, Timestamp x) throws SQLException { this.updateTimestamp(this.findColumn(columnName), x); } + // ************************************************************ + // Pre-JDBC 4 + // Trivial default implementations for some methods inquiring + // ResultSet status. + // ************************************************************ + + /** + * Returns null if not overridden in a subclass. + */ + @Override + public String getCursorName() + throws SQLException + { + return null; + } + + /** + * Returns null if not overridden in a subclass. + */ + @Override + public Statement getStatement() + throws SQLException + { + return null; + } + // ************************************************************ // Implementation of JDBC 4 methods. Methods go here if they // don't throw SQLFeatureNotSupportedException; they can be @@ -392,16 +451,18 @@ public void updateTimestamp(String columnName, Timestamp x) // long as that's an allowed behavior by the JDBC spec. // ************************************************************ + @Override public boolean isWrapperFor(Class iface) throws SQLException { - return iface.isInstance(this); + return iface.isInstance(this); } + @Override public T unwrap(Class iface) throws SQLException { - if ( iface.isInstance(this) ) + if ( iface.isInstance(this) ) return iface.cast(this); throw new SQLFeatureNotSupportedException ( this.getClass().getSimpleName() @@ -413,6 +474,7 @@ public T unwrap(Class iface) // Non-implementation of JDBC 4 methods. // ************************************************************ + @Override public void updateNClob(int columnIndex, NClob nClob) throws SQLException { @@ -421,6 +483,7 @@ public void updateNClob(int columnIndex, NClob nClob) "0A000" ); } + @Override public void updateNClob(String columnLabel, NClob nClob) throws SQLException { @@ -429,6 +492,7 @@ public void updateNClob(String columnLabel, NClob nClob) "0A000" ); } + @Override public void updateNClob(int columnIndex, Reader reader) throws SQLException { @@ -437,6 +501,7 @@ public void updateNClob(int columnIndex, Reader reader) "0A000" ); } + @Override public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { @@ -445,6 +510,7 @@ public void updateNClob(int columnIndex, Reader reader, long length) "0A000" ); } + @Override public void updateNClob(String columnLabel, Reader reader) throws SQLException { @@ -453,6 +519,7 @@ public void updateNClob(String columnLabel, Reader reader) "0A000" ); } + @Override public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { @@ -461,6 +528,7 @@ public void updateNClob(String columnLabel, Reader reader, long length) "0A000" ); } + @Override public void updateClob(int columnIndex, Reader reader) throws SQLException { @@ -468,6 +536,8 @@ public void updateClob(int columnIndex, Reader reader) ".updateClob( int, Reader ) not implemented yet.", "0A000" ); } + + @Override public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { @@ -476,6 +546,7 @@ public void updateClob(int columnIndex, Reader reader, long length) "0A000" ); } + @Override public void updateClob(String columnLabel, Reader reader) throws SQLException { @@ -484,6 +555,7 @@ public void updateClob(String columnLabel, Reader reader) "0A000" ); } + @Override public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { @@ -492,6 +564,7 @@ public void updateClob(String columnLabel, Reader reader, long length) "0A000" ); } + @Override public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { @@ -500,6 +573,7 @@ public void updateBlob(int columnIndex, InputStream inputStream) "0A000" ); } + @Override public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { @@ -508,7 +582,7 @@ public void updateBlob(int columnIndex, InputStream inputStream, long length) "0A000" ); } - + @Override public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { @@ -517,6 +591,7 @@ public void updateBlob(String columnLabel, InputStream inputStream) "0A000" ); } + @Override public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { @@ -525,6 +600,7 @@ public void updateBlob(String columnLabel, InputStream inputStream, long length) "0A000" ); } + @Override public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { @@ -533,6 +609,7 @@ public void updateCharacterStream(int columnIndex, Reader x) "0A000" ); } + @Override public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { @@ -541,6 +618,7 @@ public void updateCharacterStream(int columnIndex, Reader x, long length) "0A000" ); } + @Override public void updateCharacterStream(String ColumnLabel, Reader x) throws SQLException { @@ -549,6 +627,7 @@ public void updateCharacterStream(String ColumnLabel, Reader x) "0A000" ); } + @Override public void updateCharacterStream(String ColumnLabel, Reader x, long length) throws SQLException { @@ -557,7 +636,7 @@ public void updateCharacterStream(String ColumnLabel, Reader x, long length) "0A000" ); } - + @Override public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { @@ -566,6 +645,7 @@ public void updateBinaryStream(String columnLabel, InputStream x) "0A000" ); } + @Override public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { @@ -574,6 +654,7 @@ public void updateBinaryStream(String columnLabel, InputStream x, long length) "0A000" ); } + @Override public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { @@ -582,6 +663,7 @@ public void updateBinaryStream(int columnIndex, InputStream x) "0A000" ); } + @Override public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { @@ -590,6 +672,7 @@ public void updateBinaryStream(int columnIndex, InputStream x, long length) "0A000" ); } + @Override public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { @@ -597,6 +680,7 @@ public void updateAsciiStream(String columnLabel, InputStream x) ".updateAsciiStream( String, InputStream ) not implemented yet.", "0A000" ); } + @Override public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { @@ -604,6 +688,7 @@ public void updateAsciiStream(String columnLabel, InputStream x, long length) ".updateAsciiStream( String, InputStream, long ) not implemented yet.", "0A000" ); } + @Override public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { @@ -612,6 +697,7 @@ public void updateAsciiStream(int columnIndex, InputStream x) "0A000" ); } + @Override public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { @@ -620,6 +706,7 @@ public void updateAsciiStream(int columnIndex, InputStream x, long length) "0A000" ); } + @Override public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { @@ -628,6 +715,7 @@ public void updateNCharacterStream(String columnLabel, Reader reader) "0A000" ); } + @Override public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { @@ -636,6 +724,7 @@ public void updateNCharacterStream(String columnLabel, Reader reader, long lengt "0A000" ); } + @Override public void updateNCharacterStream(int columnIndex, Reader reader) throws SQLException { @@ -644,6 +733,7 @@ public void updateNCharacterStream(int columnIndex, Reader reader) "0A000" ); } + @Override public void updateNCharacterStream(int columnIndex, Reader reader, long length) throws SQLException { @@ -652,6 +742,7 @@ public void updateNCharacterStream(int columnIndex, Reader reader, long length) "0A000" ); } + @Override public Reader getNCharacterStream(String columnLabel) throws SQLException { @@ -660,6 +751,7 @@ public Reader getNCharacterStream(String columnLabel) "0A000" ); } + @Override public Reader getNCharacterStream(int columnIndex) throws SQLException { @@ -667,6 +759,7 @@ public Reader getNCharacterStream(int columnIndex) ".gett( int ) not implemented yet.", "0A000" ); } + @Override public String getNString(int columnIndex) throws SQLException { @@ -674,6 +767,7 @@ public String getNString(int columnIndex) ".getNString( int ) not implemented yet.", "0A000" ); } + @Override public String getNString(String columnLabel) throws SQLException { @@ -682,6 +776,7 @@ public String getNString(String columnLabel) "0A000" ); } + @Override public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { @@ -690,6 +785,7 @@ public void updateSQLXML(int columnIndex, SQLXML xmlObject) "0A000" ); } + @Override public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { @@ -698,6 +794,7 @@ public void updateSQLXML(String columnLabel, SQLXML xmlObject) "0A000" ); } + @Override public SQLXML getSQLXML(int columnIndex) throws SQLException { @@ -705,6 +802,7 @@ public SQLXML getSQLXML(int columnIndex) ".getSQLXML( int ) not implemented yet.", "0A000" ); } + @Override public SQLXML getSQLXML(String columnLabel) throws SQLException { @@ -712,12 +810,15 @@ public SQLXML getSQLXML(String columnLabel) ".getSQLXML( String ) not implemented yet.", "0A000" ); } + @Override public NClob getNClob(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + ".getNClob( String ) not implemented yet.", "0A000" ); } + + @Override public NClob getNClob(int columnIndex) throws SQLException { @@ -725,6 +826,7 @@ public NClob getNClob(int columnIndex) ".getNClob( int ) not implemented yet.", "0A000" ); } + @Override public void updateNString(String columnLabel, String nString) throws SQLException { @@ -733,6 +835,7 @@ public void updateNString(String columnLabel, String nString) "0A000" ); } + @Override public void updateNString(int columnIndex, String nString) throws SQLException { @@ -741,6 +844,7 @@ public void updateNString(int columnIndex, String nString) "0A000" ); } + @Override public void updateRowId(int columnIndex, RowId x) throws SQLException { @@ -749,6 +853,7 @@ public void updateRowId(int columnIndex, RowId x) "0A000" ); } + @Override public void updateRowId(String columnLabel, RowId x) throws SQLException { @@ -757,6 +862,7 @@ public void updateRowId(String columnLabel, RowId x) "0A000" ); } + @Override public RowId getRowId(int columnIndex) throws SQLException { @@ -764,10 +870,39 @@ public RowId getRowId(int columnIndex) "getRowId( int ) not implemented yet.", "0A000" ); } + @Override public RowId getRowId(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + ".getRowId( String ) not implemented yet.", "0A000" ); } + + // ************************************************************ + // Implementation of JDBC 4.1 methods. These are half-baked at + // the moment: the type parameter isn't able to /influence/ + // what type is returned, but only to fail if what gets + // returned by default isn't that. + // Add @Override to these once Java back horizon advances to 7. + // ************************************************************ + + public T getObject(int columnIndex, Class type) + throws SQLException + { + final Object obj = getObject( columnIndex ); + if ( type.isInstance(obj) ) + return type.cast(obj); + throw new SQLException( "Cannot convert " + obj.getClass().getName() + + " to " + type ); + } + + public T getObject(String columnName, Class type) + throws SQLException + { + final Object obj = getObject( columnName ); + if ( type.isInstance(obj) ) + return type.cast(obj); + throw new SQLException( "Cannot convert " + obj.getClass().getName() + + " to " + type ); + } } From a2d7cc28f044c1f63bb36e21f827734608c8544c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Aug 2018 10:49:56 -0400 Subject: [PATCH 0148/1087] Javadoc, Overrides in ObjectResultSet. --- .../pljava/jdbc/ObjectResultSet.java | 307 ++++++++++++++++-- 1 file changed, 279 insertions(+), 28 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java index f92addc7..6de5ef76 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010 PostgreSQL Global Development Group + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -30,27 +35,76 @@ /** + * Implements most getters in terms of {@link #getValue}, {@link #getNumber}, + * or a few other {@code ResultSet} getters that are so implemented, tracks + * {@link #wasNull wasNull}, and provides {@link #getObjectValue(int)} as the + * chief method for subclasses to implement; turns most updaters into + * {@link #updateObject(int,Object)}. * @author Thomas Hallgren */ public abstract class ObjectResultSet extends AbstractResultSet { private boolean m_wasNull = false; + /** + * Returns a private value updated by final methods in this class. + */ + @Override + public boolean wasNull() + { + return m_wasNull; + } /** * This is a noop since warnings are not supported. */ + @Override public void clearWarnings() throws SQLException { } + /** + * Returns null if not overridden in a subclass. + */ + @Override + public SQLWarning getWarnings() + throws SQLException + { + return null; + } + + /** + * Throws "unsupported" exception if not overridden in a subclass. + * @throws SQLException indicating that this feature is not supported. + */ + @Override + public ResultSetMetaData getMetaData() + throws SQLException + { + throw new UnsupportedFeatureException( + "ResultSet meta data is not yet implemented"); + } + + // ************************************************************ + // Pre-JDBC 4 + // Getters-by-columnIndex + // ************************************************************ + + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public Array getArray(int columnIndex) throws SQLException { return (Array)this.getValue(columnIndex, Array.class); } + /** + * Implemented over {@link #getClob(int) getClob}. + */ + @Override public InputStream getAsciiStream(int columnIndex) throws SQLException { @@ -58,6 +112,10 @@ public InputStream getAsciiStream(int columnIndex) return (c == null) ? null : c.getAsciiStream(); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public BigDecimal getBigDecimal(int columnIndex) throws SQLException { @@ -65,14 +123,20 @@ public BigDecimal getBigDecimal(int columnIndex) } /** + * Throws "unsupported" exception. * @deprecated */ + @Override public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { throw new UnsupportedFeatureException("getBigDecimal(int, int)"); } + /** + * Implemented over {@link #getBlob(int) getBlob}. + */ + @Override public InputStream getBinaryStream(int columnIndex) throws SQLException { @@ -80,7 +144,10 @@ public InputStream getBinaryStream(int columnIndex) return (b == null) ? null : b.getBinaryStream(); } - + /** + * Implemented over {@link #getBytes(int) getBytes}. + */ + @Override public Blob getBlob(int columnIndex) throws SQLException { @@ -88,6 +155,10 @@ public Blob getBlob(int columnIndex) return (bytes == null) ? null : new BlobValue(bytes); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public boolean getBoolean(int columnIndex) throws SQLException { @@ -95,6 +166,10 @@ public boolean getBoolean(int columnIndex) return (b == null) ? false : b.booleanValue(); } + /** + * Implemented over {@link #getNumber getNumber}. + */ + @Override public byte getByte(int columnIndex) throws SQLException { @@ -102,12 +177,20 @@ public byte getByte(int columnIndex) return (b == null) ? 0 : b.byteValue(); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public byte[] getBytes(int columnIndex) throws SQLException { return (byte[])this.getValue(columnIndex, byte[].class); } + /** + * Implemented over {@link #getClob(int) getClob}. + */ + @Override public Reader getCharacterStream(int columnIndex) throws SQLException { @@ -115,6 +198,10 @@ public Reader getCharacterStream(int columnIndex) return (c == null) ? null : c.getCharacterStream(); } + /** + * Implemented over {@link #getString(int) getString}. + */ + @Override public Clob getClob(int columnIndex) throws SQLException { @@ -122,18 +209,30 @@ public Clob getClob(int columnIndex) return (str == null) ? null : new ClobValue(str); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public Date getDate(int columnIndex) throws SQLException { return (Date)this.getValue(columnIndex, Date.class); } + /** + * Implemented over {@link #getValue(int,Class,Calendar) getValue}. + */ + @Override public Date getDate(int columnIndex, Calendar cal) throws SQLException { return (Date)this.getValue(columnIndex, Date.class, cal); } + /** + * Implemented over {@link #getNumber getNumber}. + */ + @Override public double getDouble(int columnIndex) throws SQLException { @@ -141,6 +240,10 @@ public double getDouble(int columnIndex) return (d == null) ? 0 : d.doubleValue(); } + /** + * Implemented over {@link #getNumber getNumber}. + */ + @Override public float getFloat(int columnIndex) throws SQLException { @@ -148,6 +251,10 @@ public float getFloat(int columnIndex) return (f == null) ? 0 : f.floatValue(); } + /** + * Implemented over {@link #getNumber getNumber}. + */ + @Override public int getInt(int columnIndex) throws SQLException { @@ -155,6 +262,10 @@ public int getInt(int columnIndex) return (i == null) ? 0 : i.intValue(); } + /** + * Implemented over {@link #getNumber getNumber}. + */ + @Override public long getLong(int columnIndex) throws SQLException { @@ -163,15 +274,10 @@ public long getLong(int columnIndex) } /** - * ResultSetMetaData is not yet supported. - * @throws SQLException indicating that this feature is not supported. + * Implemented over {@link #getObjectValue(int) getObjectValue}. + * Final because it records {@code wasNull} for use by other methods. */ - public ResultSetMetaData getMetaData() - throws SQLException - { - throw new UnsupportedFeatureException("ResultSet meta data is not yet implemented"); - } - + @Override public final Object getObject(int columnIndex) throws SQLException { @@ -180,6 +286,11 @@ public final Object getObject(int columnIndex) return value; } + /** + * Implemented over {@link #getObjectValue(int,Map) getObjectValue}. + * Final because it records {@code wasNull} for use by other methods. + */ + @Override public final Object getObject(int columnIndex, Map map) throws SQLException { @@ -188,12 +299,20 @@ public final Object getObject(int columnIndex, Map map) return value; } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public Ref getRef(int columnIndex) throws SQLException { return (Ref)this.getValue(columnIndex, Ref.class); } + /** + * Implemented over {@link #getNumber getNumber}. + */ + @Override public short getShort(int columnIndex) throws SQLException { @@ -201,30 +320,50 @@ public short getShort(int columnIndex) return (s == null) ? 0 : s.shortValue(); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public String getString(int columnIndex) throws SQLException { return (String)this.getValue(columnIndex, String.class); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public Time getTime(int columnIndex) throws SQLException { return (Time)this.getValue(columnIndex, Time.class); } + /** + * Implemented over {@link #getValue(int,Class,Calendar) getValue}. + */ + @Override public Time getTime(int columnIndex, Calendar cal) throws SQLException { return (Time)this.getValue(columnIndex, Time.class, cal); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public Timestamp getTimestamp(int columnIndex) throws SQLException { return (Timestamp)this.getValue(columnIndex, Timestamp.class); } + /** + * Implemented over {@link #getValue(int,Class,Calendar) getValue}. + */ + @Override public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { @@ -232,6 +371,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) } /** + * Throws "unsupported" exception. * @deprecated */ public InputStream getUnicodeStream(int columnIndex) @@ -240,17 +380,15 @@ public InputStream getUnicodeStream(int columnIndex) throw new UnsupportedFeatureException("ResultSet.getUnicodeStream"); } + /** + * Implemented over {@link #getValue getValue}. + */ + @Override public URL getURL(int columnIndex) throws SQLException { return (URL)this.getValue(columnIndex, URL.class); } - public SQLWarning getWarnings() - throws SQLException - { - return null; - } - /** * Refresh row is not yet implemented. * @throws SQLException indicating that this feature is not supported. @@ -261,144 +399,240 @@ public void refreshRow() throw new UnsupportedFeatureException("Refresh row"); } + // ************************************************************ + // Pre-JDBC 4 + // Updaters-by-columnIndex + // ************************************************************ + + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateArray(int columnIndex, Array x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link ClobValue} and + * {@link #updateObject updateObject}. + */ + @Override public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { try { this.updateObject(columnIndex, - new ClobValue(new InputStreamReader(x, "US-ASCII"), length)); + new ClobValue(new InputStreamReader(x, "US-ASCII"), length)); } catch(UnsupportedEncodingException e) { - throw new SQLException("US-ASCII encoding is not supported by this JVM"); + throw new SQLException( + "US-ASCII encoding is not supported by this JVM"); } } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link BlobValue} and + * {@link #updateBlob updateBlob}. + */ + @Override public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { this.updateBlob(columnIndex, (Blob) new BlobValue(x, length)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateBlob(int columnIndex, Blob x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateBoolean(int columnIndex, boolean x) throws SQLException { this.updateObject(columnIndex, x ? Boolean.TRUE : Boolean.FALSE); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateByte(int columnIndex, byte x) throws SQLException { this.updateObject(columnIndex, new Byte(x)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateBytes(int columnIndex, byte[] x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link ClobValue} and + * {@link #updateClob updateClob}. + */ + @Override public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { this.updateClob(columnIndex, (Clob) new ClobValue(x, length)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateClob(int columnIndex, Clob x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateDate(int columnIndex, Date x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateDouble(int columnIndex, double x) throws SQLException { this.updateObject(columnIndex, new Double(x)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateFloat(int columnIndex, float x) throws SQLException { this.updateObject(columnIndex, new Float(x)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateInt(int columnIndex, int x) throws SQLException { this.updateObject(columnIndex, new Integer(x)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateLong(int columnIndex, long x) throws SQLException { this.updateObject(columnIndex, new Long(x)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateNull(int columnIndex) throws SQLException { this.updateObject(columnIndex, null); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateRef(int columnIndex, Ref x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateShort(int columnIndex, short x) throws SQLException { this.updateObject(columnIndex, new Short(x)); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateString(int columnIndex, String x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateTime(int columnIndex, Time x) throws SQLException { this.updateObject(columnIndex, x); } + /** + * Implemented over {@link #updateObject updateObject}. + */ + @Override public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { this.updateObject(columnIndex, x); } - public boolean wasNull() - { - return m_wasNull; - } + // ************************************************************ + // Implementation methods + // ************************************************************ + /** + * Implemented over {@link #getObjectValue}, tracks {@code wasNull}, + * applies {@link SPIConnection#basicNumericCoersion} to {@code cls}. + */ protected final Number getNumber(int columnIndex, Class cls) throws SQLException { @@ -407,26 +641,43 @@ protected final Number getNumber(int columnIndex, Class cls) return SPIConnection.basicNumericCoersion(cls, value); } + /** + * Implemented over {@link #getObject}, + * applies {@link SPIConnection#basicCoersion} to {@code cls}. + */ protected final Object getValue(int columnIndex, Class cls) throws SQLException { return SPIConnection.basicCoersion(cls, this.getObject(columnIndex)); } + /** + * Implemented over {@link #getObject}, + * applies {@link SPIConnection#basicCalendricalCoersion} to {@code cls}. + */ protected Object getValue(int columnIndex, Class cls, Calendar cal) throws SQLException { - return SPIConnection.basicCalendricalCoersion(cls, this.getObject(columnIndex), cal); + return SPIConnection.basicCalendricalCoersion(cls, + this.getObject(columnIndex), cal); } + /** + * Implemented over {@link #getObjectValue}, complains if {@code typeMap} + * is non-null. + */ protected Object getObjectValue(int columnIndex, Map typeMap) throws SQLException { if(typeMap == null) return this.getObjectValue(columnIndex); - throw new UnsupportedFeatureException("Obtaining values using explicit Map"); + throw new UnsupportedFeatureException( + "Obtaining values using explicit Map"); } + /** + * Primary method for subclass to override to retrieve a value. + */ protected abstract Object getObjectValue(int columnIndex) throws SQLException; } From 1ebd53666e2f868fe29b5efd98c16fd408377bed Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Aug 2018 10:39:59 -0400 Subject: [PATCH 0149/1087] Javadoc, Override, ReadOnlyResultSet --- .../pljava/jdbc/ReadOnlyResultSet.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ReadOnlyResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ReadOnlyResultSet.java index e6a6fb2c..1b315456 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ReadOnlyResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ReadOnlyResultSet.java @@ -1,8 +1,13 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Thomas Hallgren */ package org.postgresql.pljava.jdbc; @@ -11,7 +16,7 @@ /** - * The ReadOnlyResultSet implements all methods that changes the ResultSet + * Implements all methods that change the ResultSet * in any way as methods that yield an {@link UnsupportedFeatureException}. * * @author Thomas Hallgren @@ -21,6 +26,7 @@ public abstract class ReadOnlyResultSet extends ObjectResultSet /** * Returns {@link ResultSet#CONCUR_READ_ONLY}. */ + @Override public int getConcurrency() throws SQLException { @@ -31,6 +37,7 @@ public int getConcurrency() * This feature is not supported on a ReadOnlyResultSet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void cancelRowUpdates() throws SQLException { @@ -41,6 +48,7 @@ public void cancelRowUpdates() * This feature is not supported on a ReadOnlyResultSet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void deleteRow() throws SQLException { @@ -51,6 +59,7 @@ public void deleteRow() * This feature is not supported on a ReadOnlyResultSet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void insertRow() throws SQLException { @@ -61,6 +70,7 @@ public void insertRow() * This is a no-op since the moveToInsertRow() method is * unsupported. */ + @Override public void moveToCurrentRow() throws SQLException { @@ -70,6 +80,7 @@ public void moveToCurrentRow() * This feature is not supported on a ReadOnlyResultSet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void moveToInsertRow() throws SQLException { @@ -80,6 +91,7 @@ public void moveToInsertRow() * This feature is not supported on a ReadOnlyResultSet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void updateRow() throws SQLException { @@ -89,6 +101,7 @@ public void updateRow() /** * Always returns false. */ + @Override public boolean rowDeleted() throws SQLException { @@ -98,6 +111,7 @@ public boolean rowDeleted() /** * Always returns false. */ + @Override public boolean rowInserted() throws SQLException { @@ -107,6 +121,7 @@ public boolean rowInserted() /** * Always returns false. */ + @Override public boolean rowUpdated() throws SQLException { @@ -117,6 +132,7 @@ public boolean rowUpdated() * This feature is not supported on a ReadOnlyResultSet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void updateObject(int columnIndex, Object x) throws SQLException { throw readOnlyException(); @@ -126,6 +142,7 @@ public void updateObject(int columnIndex, Object x) throws SQLException * This feature is not supported on a ReadOnlyResultSet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void updateObject(int columnIndex, Object x, int scale) throws SQLException { From 25811f7b37727a18c578a8909ed246e9767b9a72 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Aug 2018 10:40:30 -0400 Subject: [PATCH 0150/1087] Javadoc, Override, ResultSetBase/subclasses. Fix: an earlier commit incorrectly altered the copyright dates. --- .../postgresql/pljava/jdbc/ResultSetBase.java | 52 +++++++++++++++++-- .../postgresql/pljava/jdbc/SPIResultSet.java | 41 ++++++++++++--- .../pljava/jdbc/SyntheticResultSet.java | 42 ++++++++++++--- 3 files changed, 117 insertions(+), 18 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java index 9793ea03..bd8aa85d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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,41 +17,62 @@ import java.sql.SQLException; /** - * A ResultSet base provides methods that are common both for + * Provides methods that are common both for * a SyntheticResultSet (which is not associated with a * statement) and SPIResultSet. * * @author Filip Hrbek */ -abstract class ResultSetBase extends ReadOnlyResultSet +public abstract class ResultSetBase extends ReadOnlyResultSet { private int m_fetchSize; private int m_row; + /** + * Records a fetch size, and an initial position before the first row. + */ ResultSetBase(int fetchSize) { m_fetchSize = fetchSize; m_row = 0; // First row is 1 so 0 is on undefined position. } + /** + * Always returns {@link #FETCH_FORWARD} if not overridden. + */ + @Override public int getFetchDirection() throws SQLException { return FETCH_FORWARD; } + /** + * Returns the fetch size set by the constructor or with + * {@link #setFetchSize}. + */ + @Override public final int getFetchSize() throws SQLException { return m_fetchSize; } + /** + * Returns the row set by the constructor or with + * {@link #setRow}. + */ + @Override public final int getRow() throws SQLException { return m_row; } + /** + * Always returns {@link #TYPE_FORWARD_ONLY} if not overridden. + */ + @Override public int getType() throws SQLException { @@ -59,9 +80,10 @@ public int getType() } /** - * Cursor positoning is not implemented yet. + * Cursor positioning is not implemented yet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void afterLast() throws SQLException { @@ -69,15 +91,17 @@ public void afterLast() } /** - * Cursor positoning is not implemented yet. + * Cursor positioning is not implemented yet. * @throws SQLException indicating that this feature is not supported. */ + @Override public void beforeFirst() throws SQLException { throw new UnsupportedFeatureException("Cursor positioning"); } + @Override public void close() throws SQLException { @@ -88,21 +112,25 @@ public void close() * Cursor positioning is not implemented yet. * @throws SQLException indicating that this feature is not supported. */ + @Override public boolean first() throws SQLException { throw new UnsupportedFeatureException("Cursor positioning"); } + @Override public boolean isAfterLast() throws SQLException { return m_row < 0; } + @Override public boolean isBeforeFirst() throws SQLException { return m_row == 0; } + @Override public boolean isFirst() throws SQLException { return m_row == 1; @@ -112,6 +140,7 @@ public boolean isFirst() throws SQLException * Cursor positioning is not implemented yet. * @throws SQLException indicating that this feature is not supported. */ + @Override public boolean last() throws SQLException { @@ -122,6 +151,7 @@ public boolean last() * Reverse positioning is not implemented yet. * @throws SQLException indicating that this feature is not supported. */ + @Override public boolean previous() throws SQLException { @@ -132,6 +162,7 @@ public boolean previous() * Cursor positioning is not implemented yet. * @throws SQLException indicating that this feature is not supported. */ + @Override public boolean absolute(int row) throws SQLException { @@ -142,6 +173,7 @@ public boolean absolute(int row) * Cursor positioning is not implemented yet. * @throws SQLException indicating that this feature is not supported. */ + @Override public boolean relative(int rows) throws SQLException { @@ -153,6 +185,7 @@ public boolean relative(int rows) * @throws SQLException indicating that this feature is not supported * for other values on direction. */ + @Override public void setFetchDirection(int direction) throws SQLException { @@ -165,6 +198,7 @@ public void setFetchDirection(int direction) // ************************************************************ + @Override public boolean isClosed() throws SQLException { @@ -184,6 +218,10 @@ public int getHoldability() // End of implementation of JDBC 4 methods. // ************************************************************ + /** + * Sets the fetch size maintained in this class. + */ + @Override public void setFetchSize(int fetchSize) throws SQLException { @@ -192,6 +230,10 @@ public void setFetchSize(int fetchSize) m_fetchSize = fetchSize; } + /** + * Sets the row reported by this class; should probably have + * {@code protected} access. + */ final void setRow(int row) { m_row = row; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java index ab27c437..5c166d96 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java @@ -54,12 +54,7 @@ public class SPIResultSet extends ResultSetBase m_tableRow = -1; } - public int getFetchDirection() - throws SQLException - { - return FETCH_FORWARD; - } - + @Override public void close() throws SQLException { @@ -75,11 +70,13 @@ public void close() } } + @Override public boolean isLast() throws SQLException { return m_currentRow != null && this.peekNext() == null; } + @Override public boolean next() throws SQLException { @@ -90,24 +87,35 @@ public boolean next() return result; } + /** + * This method does return the name of the portal, but beware of attempting + * positioned update/delete, because rows are read from the portal in + * {@link #getFetchSize} batches. + */ + @Override public String getCursorName() throws SQLException { return this.getPortal().getName(); } + @Override public int findColumn(String columnName) throws SQLException { return m_tupleDesc.getColumnIndex(columnName); } + @Override public Statement getStatement() throws SQLException { return m_statement; } + /** + * Return the {@code Portal} associated with this {@code ResultSet}. + */ protected final Portal getPortal() throws SQLException { @@ -116,6 +124,10 @@ protected final Portal getPortal() return m_portal; } + /** + * Get a(nother) table of {@link #getFetchSize} rows from the + * {@link Portal}. + */ protected final TupleTable getTupleTable() throws SQLException { @@ -153,6 +165,9 @@ protected final TupleTable getTupleTable() return m_table; } + /** + * Return the {@link Tuple} most recently returned by {@link #next}. + */ protected final Tuple getCurrentRow() throws SQLException { @@ -161,6 +176,10 @@ protected final Tuple getCurrentRow() return m_currentRow; } + /** + * Get another {@link Tuple} from the {@link TupleTable}, refreshing the + * table as needed. + */ protected final Tuple peekNext() throws SQLException { @@ -173,7 +192,7 @@ protected final Tuple peekNext() if(m_tableRow >= table.getCount() - 1) { - // Current table is exhaused, get the next + // Current table is exhausted, get the next // one. // m_table = null; @@ -185,12 +204,20 @@ protected final Tuple peekNext() return m_nextRow; } + /** + * Implemented over {@link Tuple#getObject Tuple.getObject(TupleDesc,int)}. + */ + @Override // defined in ObjectResultSet protected Object getObjectValue(int columnIndex) throws SQLException { return this.getCurrentRow().getObject(m_tupleDesc, columnIndex); } + /** + * Returns an {@link SPIResultSetMetaData} instance. + */ + @Override public ResultSetMetaData getMetaData() throws SQLException { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java index 60ea3409..a97bcb39 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java @@ -1,8 +1,13 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Filip Hrbek */ package org.postgresql.pljava.jdbc; @@ -13,7 +18,8 @@ /** * A Synthetic ResultSet that provides direct access to data stored - * in a {@link java.util.ArrayList}. This kind of ResultSet has nothing + * in a {@link java.util.ArrayList}; chiefly used to return tabular information + * from {@code ...MetaData} objects. This kind of ResultSet has nothing * common with any statement. * * @author Filip Hrbek @@ -24,6 +30,16 @@ public class SyntheticResultSet extends ResultSetBase private final ArrayList m_tuples; private final HashMap m_fieldIndexes; + /** + * Construct a {@code SyntheticResultSet} whose column types are described + * by an array of {@code ResultSetField} instances, and whose rows are + * supplied as an {@code ArrayList} whose elements are themselves arrays of + * {@code Object}. + * @throws SQLException if a non-null reference at index j in any + * 'row' array is an instance of a class that does not satisfy the + * {@link ResultSetField#canContain canContain} method of the + * {@code ResultSetField} instance at index j. + */ SyntheticResultSet(ResultSetField[] fields, ArrayList tuples) throws SQLException { @@ -55,13 +71,15 @@ public class SyntheticResultSet extends ResultSetBase } } - public void close() + @Override + public void close() throws SQLException { m_tuples.clear(); super.close(); } + @Override public int findColumn(String columnName) throws SQLException { @@ -73,6 +91,11 @@ public int findColumn(String columnName) throw new SQLException("No such field: '" + columnName + "'"); } + /** + * Returns exactly the object that was supplied at {@code columnIndex} + * (less one) in the current row. + */ + @Override // defined in ObjectResultSet protected Object getObjectValue(int columnIndex) throws SQLException { @@ -88,11 +111,13 @@ protected final Object[] getCurrentRow() return (Object[])m_tuples.get(row-1); } + @Override public boolean isLast() throws SQLException { return this.getRow() == m_tuples.size(); } + @Override public boolean next() throws SQLException { int row = this.getRow(); @@ -104,6 +129,11 @@ public boolean next() throws SQLException return false; } + /** + * Returns metadata describing this {@code SyntheticResultSet}, based on the + * {@link ResultSetField ResultSetField}s supplied to the constructor. + */ + @Override public ResultSetMetaData getMetaData() throws SQLException { From 79b86c25c43b90ac27034caef42d78c171580563 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 14 Aug 2018 22:18:08 -0400 Subject: [PATCH 0151/1087] Javadoc, Overrides, SingleRowResultSet/subclasses. --- .../pljava/jdbc/SingleRowReader.java | 28 +++++++++++-- .../pljava/jdbc/SingleRowResultSet.java | 4 +- .../pljava/jdbc/SingleRowWriter.java | 19 +++++++++ .../pljava/jdbc/TriggerResultSet.java | 39 ++++++++++++++----- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java index 2e7ad16d..9bcda844 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -26,6 +31,14 @@ public class SingleRowReader extends SingleRowResultSet private final TupleDesc m_tupleDesc; private final long m_pointer; + /** + * Construct a {@code SingleRowReader} from a {@code HeapTupleHeader} + * and a {@link TupleDesc TupleDesc}. + * @param pointer Just the native pointer to a PG {@code HeapTupleHeader}; + * the Java class of the same name is uninvolved (it's been dead since + * a4f6c9e). + * @param tupleDesc A {@code TupleDesc}; the Java class this time. + */ public SingleRowReader(long pointer, TupleDesc tupleDesc) throws SQLException { @@ -33,10 +46,15 @@ public SingleRowReader(long pointer, TupleDesc tupleDesc) m_tupleDesc = tupleDesc; } + @Override public void close() { } + /** + * Frees the {@code HeapTupleHeader}. + */ + @Override public void finalize() { synchronized(Backend.THREADLOCK) @@ -45,6 +63,7 @@ public void finalize() } } + @Override // defined in ObjectResultSet protected Object getObjectValue(int columnIndex) throws SQLException { @@ -160,6 +179,7 @@ public boolean isClosed() // End of implementation of JDBC 4 methods. // ************************************************************ + @Override // defined in SingleRowResultSet protected final TupleDesc getTupleDesc() { return m_tupleDesc; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java index 5c3dd7e9..f23b3080 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowResultSet.java @@ -108,7 +108,7 @@ public boolean isAfterLast() throws SQLException } /** - * Will always return false since a SingleRowWriter + * Will always return false since a SingleRowResultSet * starts on the one and only row. */ public boolean isBeforeFirst() throws SQLException @@ -234,7 +234,7 @@ public void moveToCurrentRow() } /** - * This feature is not supported on a SingleRowWriter. + * This feature is not supported on a SingleRowResultSet. * @throws SQLException indicating that this feature is not supported. */ public void moveToInsertRow() diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java index cf0e64eb..6d3a27f7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java @@ -27,6 +27,10 @@ /** * A single row, updateable ResultSet, specially made for functions and * procedures that returns complex types or sets. + *

    + * A {@link TupleDesc} must be passed to the constructor. After values have + * been written, the native pointer to a formed {@link Tuple} can be retrieved + * using {@link #getTupleAndClear}. * * @author Thomas Hallgren */ @@ -36,6 +40,10 @@ public class SingleRowWriter extends SingleRowResultSet private final Object[] m_values; private Tuple m_tuple; + /** + * Construct a {@code SingleRowWriter} given a descriptor of the tuple + * structure it should produce. + */ public SingleRowWriter(TupleDesc tupleDesc) throws SQLException { @@ -43,6 +51,11 @@ public SingleRowWriter(TupleDesc tupleDesc) m_values = new Object[tupleDesc.size()]; } + /** + * Returns the value most recently written in the current tuple at the + * specified index, or {@code null} if none has been written. + */ + @Override // defined in ObjectRresultSet protected Object getObjectValue(int columnIndex) throws SQLException { @@ -55,6 +68,7 @@ protected Object getObjectValue(int columnIndex) * Returns true if the row contains any non null * values since all values of the row are null initially. */ + @Override public boolean rowUpdated() throws SQLException { @@ -65,6 +79,7 @@ public boolean rowUpdated() return false; } + @Override public void updateObject(int columnIndex, Object x) throws SQLException { @@ -91,6 +106,7 @@ public void updateObject(int columnIndex, Object x) m_values[columnIndex-1] = x; } + @Override public void cancelRowUpdates() throws SQLException { @@ -100,6 +116,7 @@ public void cancelRowUpdates() /** * Cancels all changes but doesn't really close the set. */ + @Override public void close() throws SQLException { @@ -137,6 +154,7 @@ public long getTupleAndClear() return m_tuple.getNativePointer(); } + @Override // defined in SingleRowResultSet protected final TupleDesc getTupleDesc() { return m_tupleDesc; @@ -146,6 +164,7 @@ protected final TupleDesc getTupleDesc() // Implementation of JDBC 4 methods. // ************************************************************ + @Override public boolean isClosed() throws SQLException { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java index 29a0ee55..52cc73a4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -39,6 +44,7 @@ public TriggerResultSet(TupleDesc tupleDesc, Tuple tuple, boolean readOnly) /** * Cancel all changes made to the Tuple. */ + @Override public void cancelRowUpdates() throws SQLException { @@ -48,6 +54,7 @@ public void cancelRowUpdates() /** * Cancels all changes but doesn't really close the set. */ + @Override public void close() throws SQLException { @@ -58,6 +65,7 @@ public void close() * Returns the concurrency for this ResultSet. * @see java.sql.ResultSet#getConcurrency */ + @Override public int getConcurrency() throws SQLException { return m_readOnly ? CONCUR_READ_ONLY : CONCUR_UPDATABLE; @@ -66,6 +74,7 @@ public int getConcurrency() throws SQLException /** * Returns true if this row has been updated. */ + @Override public boolean rowUpdated() throws SQLException { @@ -75,6 +84,7 @@ public boolean rowUpdated() /** * Store this change for later use */ + @Override public void updateObject(int columnIndex, Object x) throws SQLException { @@ -90,12 +100,14 @@ public void updateObject(int columnIndex, Object x) /** - * Return a 2 element array describing the changes that has been made to - * the contained Tuple. The first element is an int[] containing - * the index of each changed value. The second element is an Object[] - * with containing the corresponding values. + * Return a 3 element array describing the changes that have been made to + * the contained Tuple. The first element the original Tuple, the second + * an {@code int[]} containing + * the index of each changed value, and the third an {@code Object[]} + * containing the corresponding values. * - * @return The 2 element array or null if no change has been made. + * @return The 3 element array or null if no change has + * been made. */ public Object[] getChangeIndexesAndValues() { @@ -119,6 +131,13 @@ public Object[] getChangeIndexesAndValues() return new Object[] { m_tuple, indexes, values }; } + /** + * If the value has not been changed, forwards to + * {@link Tuple#getObject(TupleDesc,int) Tuple.getObject}, with the usual + * behavior for type coercion; if it has been changed, returns the exact + * object that was supplied with the change. + */ + @Override // defined in ObjectResultSet protected Object getObjectValue(int columnIndex) throws SQLException { @@ -135,6 +154,7 @@ protected Object getObjectValue(int columnIndex) return m_tuple.getObject(this.getTupleDesc(), columnIndex); } + @Override // defined in SingleRowResultSet protected final TupleDesc getTupleDesc() { return m_tupleDesc; @@ -146,6 +166,7 @@ protected final TupleDesc getTupleDesc() // ************************************************************ + @Override public boolean isClosed() throws SQLException { From 6ff7c37d14347649656a861c4171d0b468d012c4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 18 Aug 2018 23:28:01 -0400 Subject: [PATCH 0152/1087] Implement JDBC 4.1 ResultSet.getObject. This is the version that takes a Class to determine the class of result to return. To preserve back-compatibility for now, the underlying implementation method is not constrained to return an object of the requested class, and accepts null for that argument, and all the pre-existing methods call it that way, to get the previous behavior. Per a comment in Type.h, it's always been the idea that TypeObtainer methods return singletons (or return from a small number of allocated instances, if there need to be instances with a few different typeIds). That doesn't seem to have been followed always, but it grows in importance with this change; certainly any new TypeClass that is added and expected to be used in a JDBC 4.1/4.2ish way should be careful not to allocate a new Type instance every time it gets mentioned. --- pljava-so/src/main/c/SQLInputFromTuple.c | 2 +- pljava-so/src/main/c/type/Composite.c | 10 ++-- pljava-so/src/main/c/type/HeapTupleHeader.c | 37 +++++++-------- pljava-so/src/main/c/type/Tuple.c | 26 +++++++---- pljava-so/src/main/c/type/TupleDesc.c | 7 ++- pljava-so/src/main/c/type/Type.c | 21 ++++++++- .../include/pljava/type/HeapTupleHeader.h | 20 ++++++-- .../src/main/include/pljava/type/Tuple.h | 20 +++++--- pljava-so/src/main/include/pljava/type/Type.h | 21 ++++++++- .../org/postgresql/pljava/internal/Tuple.java | 28 ++++++++--- .../pljava/jdbc/AbstractResultSet.java | 23 ++-------- .../pljava/jdbc/ObjectResultSet.java | 46 +++++++++++++++++-- .../postgresql/pljava/jdbc/SPIResultSet.java | 9 ++-- .../pljava/jdbc/SingleRowReader.java | 13 ++++-- .../pljava/jdbc/SingleRowWriter.java | 2 +- .../pljava/jdbc/SyntheticResultSet.java | 6 ++- .../pljava/jdbc/TriggerResultSet.java | 13 ++++-- 17 files changed, 208 insertions(+), 96 deletions(-) diff --git a/pljava-so/src/main/c/SQLInputFromTuple.c b/pljava-so/src/main/c/SQLInputFromTuple.c index df33335a..408c8729 100644 --- a/pljava-so/src/main/c/SQLInputFromTuple.c +++ b/pljava-so/src/main/c/SQLInputFromTuple.c @@ -81,5 +81,5 @@ Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1free(JNIEnv* env, jobject _t JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo) { - return HeapTupleHeader_getObject(env, hth, jtd, attrNo); + return HeapTupleHeader_getObject(env, hth, jtd, attrNo, NULL); } diff --git a/pljava-so/src/main/c/type/Composite.c b/pljava-so/src/main/c/type/Composite.c index b8d82bfc..2959c0ef 100644 --- a/pljava-so/src/main/c/type/Composite.c +++ b/pljava-so/src/main/c/type/Composite.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -267,7 +267,7 @@ void Composite_initialize(void) { { "_getObject", - "(JJI)Ljava/lang/Object;", + "(JJILjava/lang/Class;)Ljava/lang/Object;", Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject }, { @@ -329,10 +329,10 @@ Java_org_postgresql_pljava_jdbc_SingleRowReader__1free(JNIEnv* env, jobject _thi /* * Class: org_postgresql_pljava_jdbc_SingleRowReader * Method: _getObject - * Signature: (JJI)Ljava/lang/Object; + * Signature: (JJILjava/lang/Class;)Ljava/lang/Object; */ JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo) +Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo, jclass rqcls) { - return HeapTupleHeader_getObject(env, hth, jtd, attrNo); + return HeapTupleHeader_getObject(env, hth, jtd, attrNo, rqcls); } diff --git a/pljava-so/src/main/c/type/HeapTupleHeader.c b/pljava-so/src/main/c/type/HeapTupleHeader.c index a9fad233..db523e0a 100644 --- a/pljava-so/src/main/c/type/HeapTupleHeader.c +++ b/pljava-so/src/main/c/type/HeapTupleHeader.c @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2012 PostgreSQL Global Development Group + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * PostgreSQL Global Development Group + * Chapman Flack * * @author Thomas Hallgren */ @@ -34,7 +39,8 @@ jobject HeapTupleHeader_getTupleDesc(HeapTupleHeader ht) return result; } -jobject HeapTupleHeader_getObject(JNIEnv* env, jlong hth, jlong jtd, jint attrNo) +jobject HeapTupleHeader_getObject( + JNIEnv* env, jlong hth, jlong jtd, jint attrNo, jclass rqcls) { jobject result = 0; HeapTupleHeader self = (HeapTupleHeader)Invocation_getWrappedPointer(hth); @@ -45,26 +51,15 @@ jobject HeapTupleHeader_getObject(JNIEnv* env, jlong hth, jlong jtd, jint attrNo BEGIN_NATIVE PG_TRY(); { - Oid typeId = SPI_gettypeid((TupleDesc)p2l.ptrVal, (int)attrNo); - if(!OidIsValid(typeId)) - { - Exception_throw(ERRCODE_INVALID_DESCRIPTOR_INDEX, - "Invalid attribute number \"%d\"", (int)attrNo); - } - else + Type type = TupleDesc_getColumnType( + (TupleDesc) p2l.ptrVal, (int) attrNo); + if (type != 0) { Datum binVal; bool wasNull = false; - Type type = Type_fromOid(typeId, Invocation_getTypeMap()); - if(Type_isPrimitive(type)) - /* - * This is a primitive type - */ - type = Type_getObjectType(type); - binVal = GetAttributeByNum(self, (AttrNumber)attrNo, &wasNull); if(!wasNull) - result = Type_coerceDatum(type, binVal).l; + result = Type_coerceDatumAs(type, binVal, rqcls).l; } } PG_CATCH(); diff --git a/pljava-so/src/main/c/type/Tuple.c b/pljava-so/src/main/c/type/Tuple.c index 762f91c8..ee8feba3 100644 --- a/pljava-so/src/main/c/type/Tuple.c +++ b/pljava-so/src/main/c/type/Tuple.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -77,7 +83,7 @@ void Tuple_initialize(void) JNINativeMethod methods[] = { { "_getObject", - "(JJI)Ljava/lang/Object;", + "(JJILjava/lang/Class;)Ljava/lang/Object;", Java_org_postgresql_pljava_internal_Tuple__1getObject }, { @@ -99,7 +105,7 @@ void Tuple_initialize(void) } jobject -Tuple_getObject(TupleDesc tupleDesc, HeapTuple tuple, int index) +Tuple_getObject(TupleDesc tupleDesc, HeapTuple tuple, int index, jclass rqcls) { jobject result = 0; PG_TRY(); @@ -110,7 +116,7 @@ Tuple_getObject(TupleDesc tupleDesc, HeapTuple tuple, int index) bool wasNull = false; Datum binVal = SPI_getbinval(tuple, tupleDesc, (int)index, &wasNull); if(!wasNull) - result = Type_coerceDatum(type, binVal).l; + result = Type_coerceDatumAs(type, binVal, rqcls).l; } } PG_CATCH(); @@ -128,10 +134,10 @@ Tuple_getObject(TupleDesc tupleDesc, HeapTuple tuple, int index) /* * Class: org_postgresql_pljava_internal_Tuple * Method: _getObject - * Signature: (JJI)Ljava/lang/Object; + * Signature: (JJILjava/lang/Class;)Ljava/lang/Object; */ JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_internal_Tuple__1getObject(JNIEnv* env, jclass cls, jlong _this, jlong _tupleDesc, jint index) +Java_org_postgresql_pljava_internal_Tuple__1getObject(JNIEnv* env, jclass cls, jlong _this, jlong _tupleDesc, jint index, jclass rqcls) { jobject result = 0; Ptr2Long p2l; @@ -140,7 +146,7 @@ Java_org_postgresql_pljava_internal_Tuple__1getObject(JNIEnv* env, jclass cls, j BEGIN_NATIVE HeapTuple self = (HeapTuple)p2l.ptrVal; p2l.longVal = _tupleDesc; - result = Tuple_getObject((TupleDesc)p2l.ptrVal, self, (int)index); + result = Tuple_getObject((TupleDesc)p2l.ptrVal, self, (int)index, rqcls); END_NATIVE return result; } diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index 4400eb93..02a9f391 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -57,6 +57,11 @@ jobject TupleDesc_internalCreate(TupleDesc td) return jtd; } +/* + * Returns NULL if an exception has been thrown for an invalid attribute index + * (caller should expeditiously return), otherwise the Type for the column data + * (the one representing the boxing Object type, in the primitive case). + */ Type TupleDesc_getColumnType(TupleDesc tupleDesc, int index) { Type type; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index b6d63e9d..07f46602 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -221,6 +221,25 @@ jvalue Type_coerceDatum(Type self, Datum value) return self->typeClass->coerceDatum(self, value); } +jvalue Type_coerceDatumAs(Type self, Datum value, jclass rqcls) +{ + jstring rqcname; + char *rqcname0; + Type rqtype; + + if ( NULL == rqcls || Type_getJavaClass(self) == rqcls ) + return Type_coerceDatum(self, value); + + rqcname = JNI_callObjectMethod(rqcls, Class_getName); + rqcname0 = String_createNTS(rqcname); + JNI_deleteLocalRef(rqcname); + rqtype = Type_fromJavaType(self->typeId, rqcname0); + pfree(rqcname0); + if ( Type_canReplaceType(rqtype, self) ) + return Type_coerceDatum(rqtype, value); + return Type_coerceDatum(self, value); +} + Datum Type_coerceObject(Type self, jobject object) { return self->typeClass->coerceObject(self, object); diff --git a/pljava-so/src/main/include/pljava/type/HeapTupleHeader.h b/pljava-so/src/main/include/pljava/type/HeapTupleHeader.h index 5618dad0..5f2e3552 100644 --- a/pljava-so/src/main/include/pljava/type/HeapTupleHeader.h +++ b/pljava-so/src/main/include/pljava/type/HeapTupleHeader.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden -* Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -21,11 +27,15 @@ extern "C" { * access to some of the attributes of the HeapTupleHeader structure. * * @author Thomas Hallgren + * + * (As of git commit a4f6c9e, there are uses of this C interface, + * but no uses of the Java class.) *****************************************************************/ extern jobject HeapTupleHeader_getTupleDesc(HeapTupleHeader ht); -extern jobject HeapTupleHeader_getObject(JNIEnv* env, jlong hth, jlong jtd, jint attrNo); +extern jobject HeapTupleHeader_getObject( + JNIEnv* env, jlong hth, jlong jtd, jint attrNo, jclass rqcls); extern void HeapTupleHeader_free(JNIEnv* env, jlong hth); diff --git a/pljava-so/src/main/include/pljava/type/Tuple.h b/pljava-so/src/main/include/pljava/type/Tuple.h index 53639212..8409a8bc 100644 --- a/pljava-so/src/main/include/pljava/type/Tuple.h +++ b/pljava-so/src/main/include/pljava/type/Tuple.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -31,9 +37,11 @@ extern jobject Tuple_internalCreate(HeapTuple tuple, bool mustCopy); extern jobjectArray Tuple_createArray(HeapTuple* tuples, jint size, bool mustCopy); /* - * Return a java object at given index from a HeapTuple + * Return a java object at given index from a HeapTuple (with a best effort to + * produce an object of class rqcls if it is not null). */ -extern jobject Tuple_getObject(TupleDesc tupleDesc, HeapTuple tuple, int index); +extern jobject Tuple_getObject( + TupleDesc tupleDesc, HeapTuple tuple, int index, jclass rqcls); #ifdef __cplusplus } diff --git a/pljava-so/src/main/include/pljava/type/Type.h b/pljava-so/src/main/include/pljava/type/Type.h index c8a0bc6a..b7ba62a7 100644 --- a/pljava-so/src/main/include/pljava/type/Type.h +++ b/pljava-so/src/main/include/pljava/type/Type.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -63,11 +63,20 @@ extern bool Type_isPrimitive(Type self); extern bool Type_canReplaceType(Type self, Type type); /* - * Translate a given Datum into a jvalue accorging to the type represented + * Translate a given Datum into a jvalue according to the type represented * by this instance. */ extern jvalue Type_coerceDatum(Type self, Datum datum); +/* + * Translate a given Datum into a jvalue, where the type represented + * by this instance is derived from the PG type of the datum, and rqcls, if + * not NULL, is the Java class wanted by the caller (JDBC 4.1 feature). + * Reduces to Type_coerceDatum if rqcls is NULL, or there is no TypeClass that + * can replace this Type and produce the requested class. + */ +extern jvalue Type_coerceDatumAs(Type self, Datum datum, jclass rqcls); + /* * Translate a given Object into a Datum accorging to the type represented * by this instance. @@ -237,6 +246,14 @@ extern void Type_closeSRF(Type self, jobject producer); * singleton. The only current exception from this is the * String since it makes use of functions stored in the * Form_pg_type structure. + * + * In adding JDBC 4.1 support, this is decreed: a TypeObtainer + * may return its singleton, if that's what it does, regardless + * of whether the Oid stored there matches the one passed to the + * obtainer. In other words, it may ignore the typeId argument. + * It's often appropriate for the caller to check the returned + * type with Type_canReplaceType to determine if it is usable + * for the intended purpose. */ typedef Type (*TypeObtainer)(Oid typeId); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java index 4acdd8c4..d6b5222c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -24,17 +30,24 @@ public class Tuple extends JavaWrapper /** * Obtains a value from the underlying native HeapTuple * structure. + *

    + * Conversion to a JDBC 4.1 specified class is best effort, if the native + * type system knows how to do so; otherwise, the return value can be + * whatever would have been returned in the legacy case. Caller beware! * @param tupleDesc The Tuple descriptor for this instance. * @param index Index of value in the structure (one based). + * @param type Desired Java class of the result, if the JDBC 4.1 version + * of {@code getObject} has been called; null in all the legacy cases. * @return The value or null. * @throws SQLException If the underlying native structure has gone stale. */ - public Object getObject(TupleDesc tupleDesc, int index) + public Object getObject(TupleDesc tupleDesc, int index, Class type) throws SQLException { synchronized(Backend.THREADLOCK) { - return _getObject(this.getNativePointer(), tupleDesc.getNativePointer(), index); + return _getObject(this.getNativePointer(), + tupleDesc.getNativePointer(), index, type); } } @@ -44,6 +57,7 @@ public Object getObject(TupleDesc tupleDesc, int index) */ protected native void _free(long pointer); - private static native Object _getObject(long pointer, long tupleDescPointer, int index) + private static native Object _getObject( + long pointer, long tupleDescPointer, int index, Class type) throws SQLException; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index 4140fa0d..c17372d2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -879,30 +879,13 @@ public RowId getRowId(String columnLabel) } // ************************************************************ - // Implementation of JDBC 4.1 methods. These are half-baked at - // the moment: the type parameter isn't able to /influence/ - // what type is returned, but only to fail if what gets - // returned by default isn't that. - // Add @Override to these once Java back horizon advances to 7. + // Implementation of JDBC 4.1 methods. + // Add @Override here once Java back horizon advances to 7. // ************************************************************ - public T getObject(int columnIndex, Class type) - throws SQLException - { - final Object obj = getObject( columnIndex ); - if ( type.isInstance(obj) ) - return type.cast(obj); - throw new SQLException( "Cannot convert " + obj.getClass().getName() + - " to " + type ); - } - public T getObject(String columnName, Class type) throws SQLException { - final Object obj = getObject( columnName ); - if ( type.isInstance(obj) ) - return type.cast(obj); - throw new SQLException( "Cannot convert " + obj.getClass().getName() + - " to " + type ); + return this.getObject(this.findColumn(columnName), type); } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java index 6de5ef76..5e24e05c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java @@ -625,6 +625,27 @@ public void updateTimestamp(int columnIndex, Timestamp x) this.updateObject(columnIndex, x); } + // ************************************************************ + // JDBC 4.1 + // Getter-by-columnIndex + // Add @Override here once Java back horizon advances to 7. + // ************************************************************ + + /** + * Implemented over {@link #getObjectValue(int,Class) getObjectValue}. + * Final because it records {@code wasNull} for use by other methods. + */ + public final T getObject(int columnIndex, Class type) + throws SQLException + { + Object value = this.getObjectValue(columnIndex, type); + m_wasNull = (value == null); + if ( type.isInstance(value) ) + return type.cast(value); + throw new SQLException("Cannot convert " + value.getClass().getName() + + " to " + type.getName()); + } + // ************************************************************ // Implementation methods // ************************************************************ @@ -663,8 +684,8 @@ protected Object getValue(int columnIndex, Class cls, Calendar cal) } /** - * Implemented over {@link #getObjectValue}, complains if {@code typeMap} - * is non-null. + * Implemented over {@link #getObjectValue(int)}, complains if + * {@code typeMap} is non-null. */ protected Object getObjectValue(int columnIndex, Map typeMap) throws SQLException @@ -675,9 +696,28 @@ protected Object getObjectValue(int columnIndex, Map typeMap) "Obtaining values using explicit Map"); } + /** + * Implemented over {@link #getObjectValue(int,Class)}, passing null for + * the class. + *

    + * To preserve back-compatible behavior in the 1.5.x branch, this is still + * what ends up getting called in all cases that do not explicitly use the + * JDBC 4.1 new {@link #getObject(int,Class)}. + */ + protected Object getObjectValue(int columnIndex) + throws SQLException + { + return getObjectValue(columnIndex, (Class)null); + } + /** * Primary method for subclass to override to retrieve a value. + *

    + * The signature does not constrain this to return an object of the + * requested class, so it can still be used as before by methods that may do + * additional coercions. When called by {@link #getObject(int,Class)}, that + * caller enforces the class of the result. */ - protected abstract Object getObjectValue(int columnIndex) + protected abstract Object getObjectValue(int columnIndex, Class type) throws SQLException; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java index 5c166d96..71dfe7d4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -205,13 +205,14 @@ protected final Tuple peekNext() } /** - * Implemented over {@link Tuple#getObject Tuple.getObject(TupleDesc,int)}. + * Implemented over + * {@link Tuple#getObject Tuple.getObject(TupleDesc,int,Class)}. */ @Override // defined in ObjectResultSet - protected Object getObjectValue(int columnIndex) + protected Object getObjectValue(int columnIndex, Class type) throws SQLException { - return this.getCurrentRow().getObject(m_tupleDesc, columnIndex); + return this.getCurrentRow().getObject(m_tupleDesc, columnIndex, type); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java index 9bcda844..e45b7e5d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java @@ -64,12 +64,13 @@ public void finalize() } @Override // defined in ObjectResultSet - protected Object getObjectValue(int columnIndex) + protected Object getObjectValue(int columnIndex, Class type) throws SQLException { synchronized(Backend.THREADLOCK) { - return _getObject(m_pointer, m_tupleDesc.getNativePointer(), columnIndex); + return _getObject( + m_pointer, m_tupleDesc.getNativePointer(), columnIndex, type); } } @@ -185,8 +186,14 @@ protected final TupleDesc getTupleDesc() return m_tupleDesc; } + /* + * Looking for the implementation of the following JNI methods? + * Look in type/Composite.c + */ + protected native void _free(long pointer); - private static native Object _getObject(long pointer, long tupleDescPointer, int index) + private static native Object _getObject( + long pointer, long tupleDescPointer, int index, Class type) throws SQLException; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java index 6d3a27f7..239f5099 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java @@ -56,7 +56,7 @@ public SingleRowWriter(TupleDesc tupleDesc) * specified index, or {@code null} if none has been written. */ @Override // defined in ObjectRresultSet - protected Object getObjectValue(int columnIndex) + protected Object getObjectValue(int columnIndex, Class type) throws SQLException { if(columnIndex < 1) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java index a97bcb39..1603c805 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java @@ -8,6 +8,7 @@ * * Contributors: * Filip Hrbek + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -94,9 +95,12 @@ public int findColumn(String columnName) /** * Returns exactly the object that was supplied at {@code columnIndex} * (less one) in the current row. + *

    + * Ignores the {@code type} argument and returns whatever object is there. + * If it is not what the caller needed, let the caller complain. */ @Override // defined in ObjectResultSet - protected Object getObjectValue(int columnIndex) + protected Object getObjectValue(int columnIndex, Class type) throws SQLException { return getCurrentRow()[columnIndex-1]; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java index 52cc73a4..3592b29b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java @@ -133,12 +133,15 @@ public Object[] getChangeIndexesAndValues() /** * If the value has not been changed, forwards to - * {@link Tuple#getObject(TupleDesc,int) Tuple.getObject}, with the usual - * behavior for type coercion; if it has been changed, returns the exact - * object that was supplied with the change. + * {@link Tuple#getObject(TupleDesc,int,Class) Tuple.getObject}, with the + * usual behavior for type coercion; if it has been changed, returns the + * exact object that was supplied with the change. + *

    + * When the caller is the JDBC 4.1 {@link #getObject(int,Class)}, the caller + * will check and complain if the returned object is not of the right class. */ @Override // defined in ObjectResultSet - protected Object getObjectValue(int columnIndex) + protected Object getObjectValue(int columnIndex, Class type) throws SQLException { // Check if this value has been changed. @@ -151,7 +154,7 @@ protected Object getObjectValue(int columnIndex) if(columnIndex == ((Integer)changes.get(idx)).intValue()) return changes.get(idx + 1); } - return m_tuple.getObject(this.getTupleDesc(), columnIndex); + return m_tuple.getObject(this.getTupleDesc(), columnIndex, type); } @Override // defined in SingleRowResultSet From ee754148b90283a4c3c20786964f990f395bcb9f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Aug 2018 03:10:50 -0400 Subject: [PATCH 0153/1087] Allow TypeRoundTripper to test JDBC 4.1 getObject. --- .../example/annotation/TypeRoundTripper.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index 875f2fe4..3279d82b 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -26,6 +26,7 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLType; /** * A class to simplify testing of PL/Java's mappings between PostgreSQL and @@ -128,6 +129,9 @@ private TypeRoundTripper() { } * must have text/varchar type, while ROUNDTRIPPED must match the type of * the input column). * @param in The input row value (required to have exactly one column). + * @param classname Name of class to be explicitly requested (JDBC 4.1 + * feature) from {@code getObject}; pass an empty string (the default) to + * make no such explicit request. * @param out The output row (supplied by PL/Java, representing the column * definition list that follows the call of this function in SQL). * @throws SQLException if {@code in} does not have exactly one column, if @@ -138,14 +142,29 @@ private TypeRoundTripper() { } @Function( schema = "javatest", type = "RECORD", - provides = "TypeRoundTripper.roundTrip" + provides = "TypeRoundTripper.roundTrip", + implementor = "postgresql_ge_80400" // supports function param DEFAULTs ) - public static boolean roundTrip(ResultSet in, ResultSet out) + public static boolean roundTrip( + ResultSet in, @SQLType(defaultValue="") String classname, ResultSet out) throws SQLException { ResultSetMetaData inmd = in.getMetaData(); ResultSetMetaData outmd = out.getMetaData(); + Class clazz = null; + if ( ! "".equals(classname) ) + { + try + { + clazz = Class.forName(classname); + } + catch ( ClassNotFoundException cnfe ) + { + throw new SQLException(cnfe.getMessage(), cnfe); + } + } + if ( 1 != inmd.getColumnCount() ) throw new SQLDataException( "in parameter must be a one-column row type", "22000"); @@ -158,7 +177,7 @@ public static boolean roundTrip(ResultSet in, ResultSet out) String inTypePG = inmd.getColumnTypeName(1); int inTypeJDBC = inmd.getColumnType(1); - Object val = in.getObject(1); + Object val = (null == clazz) ? in.getObject(1) : in.getObject(1, clazz); for ( int i = 1; i <= outcols; ++ i ) { @@ -200,7 +219,8 @@ else if ( "ROUNDTRIPPED".equals(what) ) else throw new SQLDataException( "Output column label \""+ what + "\" should be one of: " + - "TYPEPG, TYPEJDBC, CLASSJDBC, CLASS, TOSTRING, VALUE", + "TYPEPG, TYPEJDBC, CLASSJDBC, CLASS, TOSTRING, " + + "ROUNDTRIPPED", "22000"); } From 2d51cd84cae7ba17385d5b331a30338f83ecd0d0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Aug 2018 10:36:18 -0400 Subject: [PATCH 0154/1087] Implement JDBC 4.2 SQLInput.getObject. --- pljava-so/src/main/c/SQLInputFromTuple.c | 22 ++- .../pljava/jdbc/SQLInputFromTuple.java | 180 ++++++++++++++++-- 2 files changed, 173 insertions(+), 29 deletions(-) diff --git a/pljava-so/src/main/c/SQLInputFromTuple.c b/pljava-so/src/main/c/SQLInputFromTuple.c index 408c8729..7cdf3109 100644 --- a/pljava-so/src/main/c/SQLInputFromTuple.c +++ b/pljava-so/src/main/c/SQLInputFromTuple.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -42,7 +48,7 @@ void SQLInputFromTuple_initialize(void) { { "_getObject", - "(JJI)Ljava/lang/Object;", + "(JJILjava/lang/Class;)Ljava/lang/Object;", Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1getObject }, { @@ -76,10 +82,10 @@ Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1free(JNIEnv* env, jobject _t /* * Class: org_postgresql_pljava_jdbc_SQLInputFromTuple * Method: _getObject - * Signature: (JJI)Ljava/lang/Object; + * Signature: (JJILjava/lang/Class;)Ljava/lang/Object; */ JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo) +Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo, jclass rqcls) { - return HeapTupleHeader_getObject(env, hth, jtd, attrNo, NULL); + return HeapTupleHeader_getObject(env, hth, jtd, attrNo, rqcls); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java index 070f3eb9..756e627b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -32,9 +36,9 @@ import org.postgresql.pljava.internal.TupleDesc; /** - * A single row, updateable ResultSet specially made for triggers. The - * changes made to this ResultSet are remembered and converted to a - * SPI_modify_tuple call prior to function return. + * Implements the {@code SQLInput} interface for a user-defined type (UDT) + * implemented in Java, for the case where a composite type in PostgreSQL is + * used as the UDT's representation, so it can be accessed as a PG tuple. * * @author Thomas Hallgren */ @@ -44,6 +48,11 @@ public class SQLInputFromTuple extends JavaWrapper implements SQLInput private final TupleDesc m_tupleDesc; private boolean m_wasNull; + /** + * Construct an instance, given the (native) pointer to a PG + * {@code HeapTupleHeader}, as well as the TupleDesc (Java object this time) + * describing its structure. + */ public SQLInputFromTuple(long heapTupleHeaderPointer, TupleDesc tupleDesc) throws SQLException { @@ -53,92 +62,151 @@ public SQLInputFromTuple(long heapTupleHeaderPointer, TupleDesc tupleDesc) m_wasNull = false; } + /** + * Implemented over {@link #readValue}. + */ + @Override public Array readArray() throws SQLException { return (Array)this.readValue(Array.class); } + /** + * Implemented over {@link #readClob}. + */ + @Override public InputStream readAsciiStream() throws SQLException { Clob c = this.readClob(); return (c == null) ? null : c.getAsciiStream(); } + /** + * Implemented over {@link #readValue}. + */ + @Override public BigDecimal readBigDecimal() throws SQLException { return (BigDecimal)this.readValue(BigDecimal.class); } + /** + * Implemented over {@link #readBlob}. + */ + @Override public InputStream readBinaryStream() throws SQLException { Blob b = this.readBlob(); return (b == null) ? null : b.getBinaryStream(); } + /** + * Implemented over {@link #readBytes}. + */ + @Override public Blob readBlob() throws SQLException { byte[] bytes = this.readBytes(); return (bytes == null) ? null : new BlobValue(bytes); } + /** + * Implemented over {@link #readValue}. + */ + @Override public boolean readBoolean() throws SQLException { Boolean b = (Boolean)this.readValue(Boolean.class); return (b == null) ? false : b.booleanValue(); } + /** + * Implemented over {@link #readNumber}. + */ + @Override public byte readByte() throws SQLException { Number b = this.readNumber(byte.class); return (b == null) ? 0 : b.byteValue(); } + /** + * Implemented over {@link #readValue}. + */ + @Override public byte[] readBytes() throws SQLException { return (byte[])this.readValue(byte[].class); } + /** + * Implemented over {@link #readClob}. + */ public Reader readCharacterStream() throws SQLException { Clob c = this.readClob(); return (c == null) ? null : c.getCharacterStream(); } + /** + * Implemented over {@link #readString}. + */ public Clob readClob() throws SQLException { String str = this.readString(); return (str == null) ? null : new ClobValue(str); } + /** + * Implemented over {@link #readValue}. + */ + @Override public Date readDate() throws SQLException { return (Date)this.readValue(Date.class); } + /** + * Implemented over {@link #readNumber}. + */ + @Override public double readDouble() throws SQLException { Number d = this.readNumber(double.class); return (d == null) ? 0 : d.doubleValue(); } + /** + * Implemented over {@link #readNumber}. + */ + @Override public float readFloat() throws SQLException { Number f = this.readNumber(float.class); return (f == null) ? 0 : f.floatValue(); } + /** + * Implemented over {@link #readNumber}. + */ + @Override public int readInt() throws SQLException { Number i = this.readNumber(int.class); return (i == null) ? 0 : i.intValue(); } + /** + * Implemented over {@link #readNumber}. + */ + @Override public long readLong() throws SQLException { Number l = this.readNumber(long.class); return (l == null) ? 0 : l.longValue(); } + @Override public Object readObject() throws SQLException { if(m_index < m_tupleDesc.size()) @@ -146,7 +214,9 @@ public Object readObject() throws SQLException Object v; synchronized(Backend.THREADLOCK) { - v = _getObject(this.getNativePointer(), m_tupleDesc.getNativePointer(), ++m_index); + v = _getObject( + this.getNativePointer(), m_tupleDesc.getNativePointer(), + ++m_index, null); } m_wasNull = v == null; return v; @@ -154,56 +224,73 @@ public Object readObject() throws SQLException throw new SQLException("Tuple has no more columns"); } + /** + * Implemented over {@link #readValue}. + */ + @Override public Ref readRef() throws SQLException { return (Ref)this.readValue(Ref.class); } + /** + * Implemented over {@link #readNumber}. + */ + @Override public short readShort() throws SQLException { Number s = this.readNumber(short.class); return (s == null) ? 0 : s.shortValue(); } + /** + * Implemented over {@link #readValue}. + */ + @Override public String readString() throws SQLException { return (String)this.readValue(String.class); } + /** + * Implemented over {@link #readValue}. + */ + @Override public Time readTime() throws SQLException { return (Time)this.readValue(Time.class); } + /** + * Implemented over {@link #readValue}. + */ + @Override public Timestamp readTimestamp() throws SQLException { return (Timestamp)this.readValue(Timestamp.class); } + /** + * Implemented over {@link #readValue}. + */ + @Override public URL readURL() throws SQLException { return (URL)this.readValue(URL.class); } + @Override public boolean wasNull() throws SQLException { return m_wasNull; } - private Number readNumber(Class numberClass) throws SQLException - { - return SPIConnection.basicNumericCoersion(numberClass, this.readObject()); - } - - private Object readValue(Class valueClass) throws SQLException - { - return SPIConnection.basicCoersion(valueClass, this.readObject()); - } // ************************************************************ // Non-implementation of JDBC 4 methods. // ************************************************************ - + /** Not yet implemented. */ + @Override public RowId readRowId() throws SQLException { @@ -213,6 +300,8 @@ public RowId readRowId() "0A000" ); } + /** Not yet implemented. */ + @Override public SQLXML readSQLXML() throws SQLException { @@ -222,6 +311,8 @@ public SQLXML readSQLXML() "0A000" ); } + /** Not yet implemented. */ + @Override public String readNString() throws SQLException { @@ -232,6 +323,8 @@ public String readNString() } + /** Not yet implemented. */ + @Override public NClob readNClob() throws SQLException { @@ -243,11 +336,56 @@ public NClob readNClob() } // ************************************************************ - // End of non-implementation of JDBC 4 methods. + // Implementation of JDBC 4.2 method. + // Add @Override here once Java back horizon advances to 8. + // ************************************************************ + + public T readObject(Class type) throws SQLException + { + if(m_index < m_tupleDesc.size()) + { + Object v; + synchronized(Backend.THREADLOCK) + { + v = _getObject( + this.getNativePointer(), m_tupleDesc.getNativePointer(), + ++m_index, type); + } + m_wasNull = v == null; + if ( type.isInstance(v) ) + return type.cast(v); + throw new SQLException("Cannot convert " + v.getClass().getName() + + " to " + type.getName()); + } + throw new SQLException("Tuple has no more columns"); + } + + // ************************************************************ + // Implementation methods. // ************************************************************ + private Number readNumber(Class numberClass) throws SQLException + { + return SPIConnection.basicNumericCoersion( + numberClass, this.readObject()); + } + + private Object readValue(Class valueClass) throws SQLException + { + return SPIConnection.basicCoersion(valueClass, this.readObject()); + } + protected native void _free(long pointer); - private static native Object _getObject(long pointer, long tupleDescPointer, int index) + /** + * Underlying method that returns the value of the next attribute. + *

    + * The signature does not constrain this to return an object of the + * requested class, so it can still be used as before by methods that may do + * additional coercions. When called by {@link #getObject(Class)}, that + * caller enforces the class of the result. + */ + private static native Object _getObject( + long pointer, long tupleDescPointer, int index, Class type) throws SQLException; } From c85a2d95af8b9437e1349bd4f5bcc5e69afed7a4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Aug 2018 13:22:31 -0400 Subject: [PATCH 0155/1087] Implement date -> java.time.LocalDate The <- direction for later. --- pljava-so/src/main/c/type/Date.c | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/pljava-so/src/main/c/type/Date.c b/pljava-so/src/main/c/type/Date.c index f8d9e973..fca42870 100644 --- a/pljava-so/src/main/c/type/Date.c +++ b/pljava-so/src/main/c/type/Date.c @@ -19,6 +19,69 @@ static jclass s_Date_class; static jmethodID s_Date_init; static jmethodID s_Date_getTime; +static TypeClass s_LocalDateClass; +/* + * The following statics are specific to Java 8 +, and will be initialized + * only on demand (pre-8 application code will have no way to demand them). + */ +static jclass s_LocalDate_class; +static jmethodID s_LocalDate_ofEpochDay; +static jmethodID s_LocalDate_toEpochDay; + +/* + * LocalDate data type. This is introduced with JDBC 4.2 and Java 8. For + * backward-compatibility reasons it does not become the default class returned + * by getObject() for a PostgreSQL date, but application code in Java 8+ can and + * should prefer it, by passing LocalDate.class to getObject. It represents a + * purely local, non-zoned, notion of date, which is exactly what PostgreSQL + * date represents, so the correspondence is direct, with no need to fudge up + * some timezone info just to shoehorn the data into java.sql.Date. + */ + +/* + * This only answers true for (same class or) DATEOID. + * The obtainer (below) only needs to construct and remember one instance. + */ +static bool _LocalDate_canReplaceType(Type self, Type other) +{ + TypeClass cls = Type_getClass(other); + return Type_getClass(self) == cls || Type_getOid(other) == DATEOID; +} + +static jvalue _LocalDate_coerceDatum(Type self, Datum arg) +{ + DateADT pgDate = DatumGetDateADT(arg); + jlong days = (jlong)(pgDate + EPOCH_DIFF); + jvalue result; + result.l = JNI_callStaticObjectMethod( + s_LocalDate_class, s_LocalDate_ofEpochDay, days); + return result; +} + +static Datum _LocalDate_coerceObject(Type self, jobject date) +{ + jlong days = + JNI_callLongMethod(date, s_LocalDate_toEpochDay) - EPOCH_DIFF; + return DateADTGetDatum((DateADT)(days)); +} + +static Type _LocalDate_obtain(Oid typeId) +{ + static Type instance = NULL; + if ( NULL == instance ) + { + s_LocalDate_class = JNI_newGlobalRef(PgObject_getJavaClass( + "java/time/LocalDate")); + s_LocalDate_ofEpochDay = PgObject_getStaticJavaMethod(s_LocalDate_class, + "ofEpochDay", "(J)Ljava/time/LocalDate;"); + s_LocalDate_toEpochDay = PgObject_getJavaMethod(s_LocalDate_class, + "toEpochDay", "()J"); + + instance = TypeClass_allocInstance(s_LocalDateClass, DATEOID); + } + return instance; +} + /* * Date data type. Postgres will pass and expect number of days since * Jan 01 2000. Java uses number of millisecs since midnight Jan 01 1970. @@ -60,4 +123,13 @@ void Date_initialize(void) s_Date_class = JNI_newGlobalRef(PgObject_getJavaClass("java/sql/Date")); s_Date_init = PgObject_getJavaMethod(s_Date_class, "", "(J)V"); s_Date_getTime = PgObject_getJavaMethod(s_Date_class, "getTime", "()J"); + + cls = TypeClass_alloc("type.LocalDate"); + cls->JNISignature = "Ljava/time/LocalDate;"; + cls->javaTypeName = "java.time.LocalDate"; + cls->canReplaceType = _LocalDate_canReplaceType; + cls->coerceDatum = _LocalDate_coerceDatum; + cls->coerceObject = _LocalDate_coerceObject; + s_LocalDateClass = cls; + Type_registerType2(InvalidOid, "java.time.LocalDate", _LocalDate_obtain); } From 3256ac055f1565adbec2dc2929c87e4a1d7569b7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Aug 2018 01:44:00 -0400 Subject: [PATCH 0156/1087] Likewise for java.time.LocalTime --- pljava-so/src/main/c/type/Time.c | 96 +++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/type/Time.c b/pljava-so/src/main/c/type/Time.c index 69e6edba..240a941b 100644 --- a/pljava-so/src/main/c/type/Time.c +++ b/pljava-so/src/main/c/type/Time.c @@ -17,14 +17,95 @@ #include "pljava/type/Timestamp.h" /* - * Time type. Postgres will pass (and expect in return) a local Time. - * The Java java.sql.Time is UTC time and not a perfect fit. Perhaps - * a LocalTime object should be added to the Java domain? + * Types time and timetz. This compilation unit supplies code for both + * PostgreSQL types. The legacy JDBC mapping for both is to java.sql.Time, which + * holds an implicit timezone offset and therefore can't be an equally good fit + * for both. Also, it loses precision: PostgreSQL maintains microseconds, but + * java.sql.Time only holds milliseconds. + * + * Java 8 and JDBC 4.2 introduce java.time.LocalTime and java.time.OffsetTime, + * which directly fit PG's time and timetz, respectively. For compatibility + * reasons, the legacy behavior of getObject (with no Class parameter) is + * unchanged, and still returns the data weirdly shoehorned into java.sql.Time. + * But Java 8 application code can and should use the form of getObject with a + * Class parameter to request java.time.LocalTime or java.time.OffsetTime, as + * appropriate. + * + * The legacy shoehorning adjusts the PostgreSQL-maintained time by its + * associated offset (in the timetz case), or by the current value of the server + * timezone offset (in the time case). Which convention is weirder? */ static jclass s_Time_class; static jmethodID s_Time_init; static jmethodID s_Time_getTime; +static TypeClass s_LocalTimeClass; +static TypeClass s_OffsetTimeClass; +/* + * The following statics are specific to Java 8 +, and will be initialized + * only on demand (pre-8 application code will have no way to demand them). + */ +static jclass s_LocalTime_class; +static jmethodID s_LocalTime_ofNanoOfDay; +static jmethodID s_LocalTime_toNanoOfDay; +static jclass s_OffsetTime_class; +static jmethodID s_OffsetTime_of; +static jmethodID s_OffsetTime_toLocalTime; +static jmethodID s_OffsetTime_getOffset; +static jclass s_ZoneOffset_class; +static jmethodID s_ZoneOffset_ofTotalSeconds; +static jmethodID s_ZoneOffset_getTotalSeconds; + +/* + * This only answers true for (same class or) TIMEOID. + * The obtainer (below) only needs to construct and remember one instance. + */ +static bool _LocalTime_canReplaceType(Type self, Type other) +{ + TypeClass cls = Type_getClass(other); + return Type_getClass(self) == cls || Type_getOid(other) == TIMEOID; +} + +static jvalue _LocalTime_coerceDatum(Type self, Datum arg) +{ + jlong nanos = +#if PG_VERSION_NUM < 100000 + (!integerDateTimes) ? (jlong)floor(1e9 * DatumGetFloat8(arg)) : +#endif + 1000 * DatumGetInt64(arg); + jvalue result; + result.l = JNI_callStaticObjectMethod( + s_LocalTime_class, s_LocalTime_ofNanoOfDay, nanos); + return result; +} + +static Datum _LocalTime_coerceObject(Type self, jobject time) +{ + jlong nanos = JNI_callLongMethod(time, s_LocalTime_toNanoOfDay); + return +#if PG_VERSION_NUM < 100000 + (!integerDateTimes) ? Float8GetDatum(((double)nanos) / 1e9) : +#endif + Int64GetDatum(nanos / 1000); +} + +static Type _LocalTime_obtain(Oid typeId) +{ + static Type instance = NULL; + if ( NULL == instance ) + { + s_LocalTime_class = JNI_newGlobalRef(PgObject_getJavaClass( + "java/time/LocalTime")); + s_LocalTime_ofNanoOfDay = PgObject_getStaticJavaMethod(s_LocalTime_class, + "ofNanoOfDay", "(J)Ljava/time/LocalTime;"); + s_LocalTime_toNanoOfDay = PgObject_getJavaMethod(s_LocalTime_class, + "toNanoOfDay", "()J"); + + instance = TypeClass_allocInstance(s_LocalTimeClass, TIMEOID); + } + return instance; +} + static jlong msecsAtMidnight(void) { AbsoluteTime now = GetCurrentAbsoluteTime() / 86400; @@ -168,4 +249,13 @@ void Time_initialize(void) cls->coerceDatum = _Timetz_coerceDatum; cls->coerceObject = _Timetz_coerceObject; Type_registerType("java.sql.Time", TypeClass_allocInstance(cls, TIMETZOID)); + + cls = TypeClass_alloc("type.LocalTime"); + cls->JNISignature = "Ljava/time/LocalTime;"; + cls->javaTypeName = "java.time.LocalTime"; + cls->coerceDatum = _LocalTime_coerceDatum; + cls->coerceObject = _LocalTime_coerceObject; + cls->canReplaceType = _LocalTime_canReplaceType; + s_LocalTimeClass = cls; + Type_registerType2(InvalidOid, "java.time.LocalTime", _LocalTime_obtain); } From 649739a8b7d3eb0b9a65a8971841c87a4e010aa9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Aug 2018 17:52:48 -0400 Subject: [PATCH 0157/1087] Likewise for java.time.OffsetTime --- pljava-so/src/main/c/type/Time.c | 127 ++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/pljava-so/src/main/c/type/Time.c b/pljava-so/src/main/c/type/Time.c index 240a941b..a308533e 100644 --- a/pljava-so/src/main/c/type/Time.c +++ b/pljava-so/src/main/c/type/Time.c @@ -45,9 +45,11 @@ static TypeClass s_OffsetTimeClass; * The following statics are specific to Java 8 +, and will be initialized * only on demand (pre-8 application code will have no way to demand them). */ +static Type s_LocalTimeInstance; static jclass s_LocalTime_class; static jmethodID s_LocalTime_ofNanoOfDay; static jmethodID s_LocalTime_toNanoOfDay; +static Type s_OffsetTimeInstance; static jclass s_OffsetTime_class; static jmethodID s_OffsetTime_of; static jmethodID s_OffsetTime_toLocalTime; @@ -91,8 +93,7 @@ static Datum _LocalTime_coerceObject(Type self, jobject time) static Type _LocalTime_obtain(Oid typeId) { - static Type instance = NULL; - if ( NULL == instance ) + if ( NULL == s_LocalTimeInstance ) { s_LocalTime_class = JNI_newGlobalRef(PgObject_getJavaClass( "java/time/LocalTime")); @@ -101,9 +102,118 @@ static Type _LocalTime_obtain(Oid typeId) s_LocalTime_toNanoOfDay = PgObject_getJavaMethod(s_LocalTime_class, "toNanoOfDay", "()J"); - instance = TypeClass_allocInstance(s_LocalTimeClass, TIMEOID); + s_LocalTimeInstance = + TypeClass_allocInstance(s_LocalTimeClass, TIMEOID); } - return instance; + return s_LocalTimeInstance; +} + +/* + * This only answers true for (same class or) TIMETZOID. + * The obtainer (below) only needs to construct and remember one instance. + */ +static bool _OffsetTime_canReplaceType(Type self, Type other) +{ + TypeClass cls = Type_getClass(other); + return Type_getClass(self) == cls || Type_getOid(other) == TIMETZOID; +} + +static jvalue _OffsetTime_coerceDatum(Type self, Datum arg) +{ + jvalue localTime; + jobject zoneOffset; + int32 offsetSecs; + jvalue result; + +#if PG_VERSION_NUM < 100000 + if ( !integerDateTimes ) + { + TimeTzADT_dd* tza = (TimeTzADT_dd*)DatumGetPointer(arg); + localTime = + Type_coerceDatum(s_LocalTimeInstance, Float8GetDatum(tza->time)); + offsetSecs = tza->zone; + } + else +#endif + { + TimeTzADT_id* tza = (TimeTzADT_id*)DatumGetPointer(arg); + localTime = + Type_coerceDatum(s_LocalTimeInstance, Int64GetDatum(tza->time)); + offsetSecs = tza->zone; + } + + zoneOffset = JNI_callStaticObjectMethod(s_ZoneOffset_class, + s_ZoneOffset_ofTotalSeconds, - offsetSecs); /* PG/Java signs differ */ + + result.l = JNI_callStaticObjectMethod( + s_OffsetTime_class, s_OffsetTime_of, localTime.l, zoneOffset); + + JNI_deleteLocalRef(localTime.l); + JNI_deleteLocalRef(zoneOffset); + + return result; +} + +static Datum _OffsetTime_coerceObject(Type self, jobject time) +{ + jobject localTime = JNI_callObjectMethod(time, s_OffsetTime_toLocalTime); + jobject zoneOffset = JNI_callObjectMethod(time, s_OffsetTime_getOffset); + jint offsetSecs = + - /* PG/Java signs differ */ + JNI_callIntMethod(zoneOffset, s_ZoneOffset_getTotalSeconds); + Datum result; + +#if PG_VERSION_NUM < 100000 + if ( !integerDateTimes ) + { + TimeTzADT_dd* tza = (TimeTzADT_dd*)palloc(sizeof(TimeTzADT_dd)); + tza->zone = offsetSecs; + tza->time = + DatumGetFloat8(Type_coerceObject(s_LocalTimeInstance, localTime)); + result = PointerGetDatum(tza); + } + else +#endif + { + TimeTzADT_id* tza = (TimeTzADT_id*)palloc(sizeof(TimeTzADT_id)); + tza->zone = offsetSecs; + tza->time = + DatumGetInt64(Type_coerceObject(s_LocalTimeInstance, localTime)); + result = PointerGetDatum(tza); + } + + JNI_deleteLocalRef(localTime); + JNI_deleteLocalRef(zoneOffset); + return result; +} + +static Type _OffsetTime_obtain(Oid typeId) +{ + if ( NULL == s_OffsetTimeInstance ) + { + _LocalTime_obtain(TIMEOID); /* Make sure LocalTime statics are there */ + + s_OffsetTime_class = JNI_newGlobalRef(PgObject_getJavaClass( + "java/time/OffsetTime")); + s_OffsetTime_of = PgObject_getStaticJavaMethod(s_OffsetTime_class, "of", + "(Ljava/time/LocalTime;Ljava/time/ZoneOffset;)" + "Ljava/time/OffsetTime;"); + s_OffsetTime_toLocalTime = PgObject_getJavaMethod(s_OffsetTime_class, + "toLocalTime", "()Ljava/time/LocalTime;"); + s_OffsetTime_getOffset = PgObject_getJavaMethod(s_OffsetTime_class, + "getOffset", "()Ljava/time/ZoneOffset;"); + + s_ZoneOffset_class = JNI_newGlobalRef(PgObject_getJavaClass( + "java/time/ZoneOffset")); + s_ZoneOffset_ofTotalSeconds = PgObject_getStaticJavaMethod( + s_ZoneOffset_class, "ofTotalSeconds", "(I)Ljava/time/ZoneOffset;"); + s_ZoneOffset_getTotalSeconds = PgObject_getJavaMethod( + s_ZoneOffset_class, "getTotalSeconds", "()I"); + + s_OffsetTimeInstance = + TypeClass_allocInstance(s_OffsetTimeClass, TIMETZOID); + } + return s_OffsetTimeInstance; } static jlong msecsAtMidnight(void) @@ -258,4 +368,13 @@ void Time_initialize(void) cls->canReplaceType = _LocalTime_canReplaceType; s_LocalTimeClass = cls; Type_registerType2(InvalidOid, "java.time.LocalTime", _LocalTime_obtain); + + cls = TypeClass_alloc("type.OffsetTime"); + cls->JNISignature = "Ljava/time/OffsetTime;"; + cls->javaTypeName = "java.time.OffsetTime"; + cls->coerceDatum = _OffsetTime_coerceDatum; + cls->coerceObject = _OffsetTime_coerceObject; + cls->canReplaceType = _OffsetTime_canReplaceType; + s_OffsetTimeClass = cls; + Type_registerType2(InvalidOid, "java.time.OffsetTime", _OffsetTime_obtain); } From 4b48ef5a584d9980d77910d1c01201a58eaf9733 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Aug 2018 01:56:05 -0400 Subject: [PATCH 0158/1087] Likewise for java.time.LocalDateTime. --- pljava-so/src/main/c/type/Timestamp.c | 175 +++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 0d0febe7..56f4576b 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -23,12 +23,42 @@ #define EPOCH_DIFF (((uint32)86400) * (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE)) /* - * Timestamp type. Postgres will pass (and expect in return) a local timestamp. - * Java on the other hand has no object that represents local time (localization - * is added when the object is converted to/from readable form). Hence, all - * postgres timestamps must be converted from local time to UTC when passed as - * a parameter to a Java method and all Java Timestamps must be converted from UTC - * to localtime when returned to postgres. + * Types timestamp and timestamptz. This compilation unit supplies code for both + * PostgreSQL types. The legacy JDBC mapping for both is to java.sql.Timestamp, + * which holds an implicit timezone offset and therefore can't be an equally + * good fit for both. + * + * Java 8 and JDBC 4.2 introduce java.time.LocalDateTime and + * java.time.OffsetDateTime, which more directly fit PG's timestamp and + * timestamptz, respectively. For compatibility reasons, the legacy behavior of + * getObject (with no Class parameter) is unchanged, and still returns the data + * weirdly shoehorned into java.sql.Timestamp. But Java 8 application code can + * and should use the form of getObject with a Class parameter to request + * java.time.LocalDateTime or java.time.OffsetDateTime, as appropriate. + * + * Note that it is somewhat misleading for PostgreSQL to call one of these types + * TIMESTAMP WITH TIME ZONE. The stored form does not, in fact, include a time + * zone (and this is in contrast to TIME WITH TIME ZONE, which does). Instead, + * what PostgreSQL means by TIMESTAMP WITH TIMEZONE is that a zone can be given + * (or inferred from the session) when a value is input, and used to adjust the + * value to UTC, and, likewise, the stored UTC value can be output converted to + * a given (or implicit) zone offset. Meanwhile, a TIMESTAMP WITHOUT TIME ZONE + * is just stored as the number of seconds from epoch that would make a clock + * on UTC show the same date and time as the value input. + * + * When producing a java.time.LocalDateTime from a timestamp and vice versa, + * the conversion is just what you would think. When producing an OffsetDateTime + * from a timestamptz, the OffsetDateTime will always have offset zero from UTC. + * That's what the stored PostgreSQL data represents; to produce anything else + * would be lying. When receiving an OffsetDateTime into PostgreSQL, of course + * any zone offset it contains will be used to adjust the value to UTC for + * storage. + * + * The legacy behavior when mapping timestamp and timestamptz to + * java.sql.Timestamp is that a timestamptz is converted in both directions + * without alteration, and a (local!) timestamp is *adjusted as if to UTC from + * the current session's implicit timezone* (and vice versa when receiving + * a value). Weird or not, that's how PL/Java has always done it. */ static jclass s_Timestamp_class; static jmethodID s_Timestamp_init; @@ -39,6 +69,129 @@ static jmethodID s_Timestamp_setNanos; static TypeClass s_TimestampClass; static TypeClass s_TimestamptzClass; +static TypeClass s_LocalDateTimeClass; +static TypeClass s_OffsetDateTimeClass; +/* + * The following statics are specific to Java 8 +, and will be initialized + * only on demand (pre-8 application code will have no way to demand them). + */ +static Type s_LocalDateTimeInstance; +static jclass s_LocalDateTime_class; +static jmethodID s_LocalDateTime_ofEpochSecond; +static jmethodID s_LocalDateTime_atOffset; +static Type s_OffsetDateTimeInstance; +static jclass s_OffsetDateTime_class; +static jmethodID s_OffsetDateTime_of; +static jmethodID s_OffsetDateTime_toEpochSecond; +static jmethodID s_OffsetDateTime_getNano; +static jobject s_ZoneOffset_UTC; + +/* + * This only answers true for (same class or) TIMESTAMPOID. + * The obtainer (below) only needs to construct and remember one instance. + */ +static bool _LocalDateTime_canReplaceType(Type self, Type other) +{ + TypeClass cls = Type_getClass(other); + return Type_getClass(self) == cls || Type_getOid(other) == TIMESTAMPOID; +} + +static jvalue _LocalDateTime_coerceDatum(Type self, Datum arg) +{ + jlong micros; + jint onlyMicros; + jlong secs; + jvalue result; +#if PG_VERSION_NUM < 100000 + if ( !integerDateTimes ) + { + /* 1e6 = 64 * 15625. *64 is a radix point shift, no precision loss */ + double shiftedFracSecs = DatumGetFloat8(arg) * 64; + int64 shiftedSecs = (int64)floor(shiftedFracSecs); + shiftedFracSecs -= (double)shiftedSecs; + micros = (jlong)(15625*shiftedSecs); + micros += ((jlong)floor(31250. * shiftedFracSecs) + 1) / 2; + } + else +#endif + { + micros = DatumGetInt64(arg); + } + /* Expect number of microseconds since 01 Jan 2000. Tease out a non-negative + * sub-second microseconds value (whether this C compiler's signed % + * has trunc or floor behavior). + */ + onlyMicros = (jint)(((micros % 1000000) + 1000000) % 1000000); + secs = EPOCH_DIFF + (micros - onlyMicros) / 1000000; + result.l = JNI_callStaticObjectMethod(s_LocalDateTime_class, + s_LocalDateTime_ofEpochSecond, + secs, 1000 * onlyMicros, s_ZoneOffset_UTC); + return result; +} + +static Datum _LocalDateTime_coerceObject(Type self, jobject timestamp) +{ + jobject offsetDateTime = JNI_callObjectMethod(timestamp, + s_LocalDateTime_atOffset, s_ZoneOffset_UTC); + jlong epochSec = JNI_callLongMethod( + offsetDateTime, s_OffsetDateTime_toEpochSecond) - EPOCH_DIFF; + jint nanos = JNI_callIntMethod( + offsetDateTime, s_OffsetDateTime_getNano); + Datum result; + +#if PG_VERSION_NUM < 100000 + if ( !integerDateTimes ) + { + double secs = (double)epochSec + ((double)nanos)/1e9; + result = Float8GetDatum(secs); + } + else +#endif + { + result = Int64GetDatum(1000000L * epochSec + nanos / 1000); + } + + JNI_deleteLocalRef(offsetDateTime); + return result; +} + +static Type _LocalDateTime_obtain(Oid typeId) +{ + if ( NULL == s_LocalDateTimeInstance ) + { + jclass zoneOffsetCls = PgObject_getJavaClass("java/time/ZoneOffset"); + jfieldID fldUTC = PgObject_getStaticJavaField( + zoneOffsetCls, "UTC", "Ljava/time/ZoneOffset;"); + s_ZoneOffset_UTC = JNI_newGlobalRef(JNI_getStaticObjectField( + zoneOffsetCls, fldUTC)); + JNI_deleteLocalRef(zoneOffsetCls); + + s_LocalDateTime_class = JNI_newGlobalRef(PgObject_getJavaClass( + "java/time/LocalDateTime")); + s_LocalDateTime_ofEpochSecond = PgObject_getStaticJavaMethod( + s_LocalDateTime_class, "ofEpochSecond", + "(JILjava/time/ZoneOffset;)Ljava/time/LocalDateTime;"); + s_LocalDateTime_atOffset = PgObject_getJavaMethod(s_LocalDateTime_class, + "atOffset", "(Ljava/time/ZoneOffset;)Ljava/time/OffsetDateTime;"); + + s_OffsetDateTime_class = JNI_newGlobalRef(PgObject_getJavaClass( + "java/time/OffsetDateTime")); + s_OffsetDateTime_toEpochSecond = PgObject_getJavaMethod( + s_OffsetDateTime_class, "toEpochSecond", "()J"); + s_OffsetDateTime_getNano = PgObject_getJavaMethod( + s_OffsetDateTime_class, "getNano", "()I"); + + /* + * Haven't initialized s_OffsetDateTime_of, but _OffsetDateTime_obtain + * will. + */ + + s_LocalDateTimeInstance = + TypeClass_allocInstance(s_LocalDateTimeClass, TIMESTAMPOID); + } + return s_LocalDateTimeInstance; +} + static bool _Timestamp_canReplaceType(Type self, Type other) { TypeClass cls = Type_getClass(other); @@ -247,4 +400,14 @@ void Timestamp_initialize(void) cls->coerceObject = _Timestamptz_coerceObject; Type_registerType("java.sql.Timestamp", TypeClass_allocInstance(cls, TIMESTAMPTZOID)); s_TimestamptzClass = cls; + + cls = TypeClass_alloc("type.LocalDateTime"); + cls->JNISignature = "Ljava/time/LocalDateTime;"; + cls->javaTypeName = "java.time.LocalDateTime"; + cls->coerceDatum = _LocalDateTime_coerceDatum; + cls->coerceObject = _LocalDateTime_coerceObject; + cls->canReplaceType = _LocalDateTime_canReplaceType; + s_LocalDateTimeClass = cls; + Type_registerType2(InvalidOid, "java.time.LocalDateTime", + _LocalDateTime_obtain); } From 2809a7daf5cbe76cf244732f3d02c947026b63d9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Aug 2018 21:22:47 -0400 Subject: [PATCH 0159/1087] Likewise for java.time.OffsetDateTime. Those are the java.time classes to be supported in JDBC 4.2 for the PG -> Java direction. --- pljava-so/src/main/c/type/Timestamp.c | 100 ++++++++++++++++++++------ 1 file changed, 77 insertions(+), 23 deletions(-) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 56f4576b..c68f3e35 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -86,6 +86,9 @@ static jmethodID s_OffsetDateTime_toEpochSecond; static jmethodID s_OffsetDateTime_getNano; static jobject s_ZoneOffset_UTC; +static Type _LocalDateTime_obtain(Oid); +static Type _OffsetDateTime_obtain(Oid); + /* * This only answers true for (same class or) TIMESTAMPOID. * The obtainer (below) only needs to construct and remember one instance. @@ -133,24 +136,7 @@ static Datum _LocalDateTime_coerceObject(Type self, jobject timestamp) { jobject offsetDateTime = JNI_callObjectMethod(timestamp, s_LocalDateTime_atOffset, s_ZoneOffset_UTC); - jlong epochSec = JNI_callLongMethod( - offsetDateTime, s_OffsetDateTime_toEpochSecond) - EPOCH_DIFF; - jint nanos = JNI_callIntMethod( - offsetDateTime, s_OffsetDateTime_getNano); - Datum result; - -#if PG_VERSION_NUM < 100000 - if ( !integerDateTimes ) - { - double secs = (double)epochSec + ((double)nanos)/1e9; - result = Float8GetDatum(secs); - } - else -#endif - { - result = Int64GetDatum(1000000L * epochSec + nanos / 1000); - } - + Datum result = Type_coerceObject(s_OffsetDateTimeInstance, offsetDateTime); JNI_deleteLocalRef(offsetDateTime); return result; } @@ -181,17 +167,75 @@ static Type _LocalDateTime_obtain(Oid typeId) s_OffsetDateTime_getNano = PgObject_getJavaMethod( s_OffsetDateTime_class, "getNano", "()I"); - /* - * Haven't initialized s_OffsetDateTime_of, but _OffsetDateTime_obtain - * will. - */ - s_LocalDateTimeInstance = TypeClass_allocInstance(s_LocalDateTimeClass, TIMESTAMPOID); + + if ( NULL == s_OffsetDateTimeInstance ) + _OffsetDateTime_obtain(TIMESTAMPTZOID); } return s_LocalDateTimeInstance; } +/* + * This only answers true for (same class or) TIMESTAMPTZOID. + * The obtainer (below) only needs to construct and remember one instance. + */ +static bool _OffsetDateTime_canReplaceType(Type self, Type other) +{ + TypeClass cls = Type_getClass(other); + return Type_getClass(self) == cls || Type_getOid(other) == TIMESTAMPTZOID; +} + +static jvalue _OffsetDateTime_coerceDatum(Type self, Datum arg) +{ + jvalue localDateTime = Type_coerceDatum(s_LocalDateTimeInstance, arg); + jvalue result; + result.l = JNI_callStaticObjectMethod(s_OffsetDateTime_class, + s_OffsetDateTime_of, localDateTime.l, s_ZoneOffset_UTC); + JNI_deleteLocalRef(localDateTime.l); + return result; +} + +static Datum _OffsetDateTime_coerceObject(Type self, jobject timestamp) +{ + jlong epochSec = JNI_callLongMethod( + timestamp, s_OffsetDateTime_toEpochSecond) - EPOCH_DIFF; + jint nanos = JNI_callIntMethod(timestamp, s_OffsetDateTime_getNano); + Datum result; + +#if PG_VERSION_NUM < 100000 + if ( !integerDateTimes ) + { + double secs = (double)epochSec + ((double)nanos)/1e9; + result = Float8GetDatum(secs); + } + else +#endif + { + result = Int64GetDatum(1000000L * epochSec + nanos / 1000); + } + + return result; +} + +static Type _OffsetDateTime_obtain(Oid typeId) +{ + if ( NULL == s_OffsetDateTimeInstance ) + { + s_OffsetDateTimeInstance = + TypeClass_allocInstance(s_OffsetDateTimeClass, TIMESTAMPTZOID); + + if ( NULL == s_LocalDateTimeInstance ) + _LocalDateTime_obtain(TIMESTAMPOID); + + s_OffsetDateTime_of = PgObject_getStaticJavaMethod( + s_OffsetDateTime_class, "of", + "(Ljava/time/LocalDateTime;Ljava/time/ZoneOffset;)" + "Ljava/time/OffsetDateTime;"); + } + return s_OffsetDateTimeInstance; +} + static bool _Timestamp_canReplaceType(Type self, Type other) { TypeClass cls = Type_getClass(other); @@ -410,4 +454,14 @@ void Timestamp_initialize(void) s_LocalDateTimeClass = cls; Type_registerType2(InvalidOid, "java.time.LocalDateTime", _LocalDateTime_obtain); + + cls = TypeClass_alloc("type.OffsetDateTime"); + cls->JNISignature = "Ljava/time/OffsetDateTime;"; + cls->javaTypeName = "java.time.OffsetDateTime"; + cls->coerceDatum = _OffsetDateTime_coerceDatum; + cls->coerceObject = _OffsetDateTime_coerceObject; + cls->canReplaceType = _OffsetDateTime_canReplaceType; + s_OffsetDateTimeClass = cls; + Type_registerType2(InvalidOid, "java.time.OffsetDateTime", + _OffsetDateTime_obtain); } From daf4ac3e8514dc2b43f0843d44adce8fcbe1a5a2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Aug 2018 01:09:17 -0400 Subject: [PATCH 0160/1087] Fix very outdated field name. --- .../java/org/postgresql/pljava/jdbc/SPIConnection.java | 8 ++++---- .../org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 126262c8..ebb0a697 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -742,7 +742,7 @@ public int[] getVersionNumber() throws SQLException /** * Convert a PostgreSQL type name to a {@link Types} integer, using the - * {@code JDBC3_TYPE_NAMES}/{@code JDBC_TYPE_NUMBERS} arrays; used in + * {@code JDBC_TYPE_NAMES}/{@code JDBC_TYPE_NUMBERS} arrays; used in * {@link DatabaseMetaData} and {@link ResultSetMetaData}. */ public int getSQLType(String pgTypeName) @@ -750,8 +750,8 @@ public int getSQLType(String pgTypeName) if (pgTypeName == null) return Types.OTHER; - for (int i = 0;i < JDBC3_TYPE_NAMES.length;i++) - if (pgTypeName.equals(JDBC3_TYPE_NAMES[i])) + for (int i = 0;i < JDBC_TYPE_NAMES.length;i++) + if (pgTypeName.equals(JDBC_TYPE_NAMES[i])) return JDBC_TYPE_NUMBERS[i]; return Types.OTHER; @@ -986,7 +986,7 @@ else if(value instanceof String) * * Tip: keep these grouped together by the Types. value */ - public static final String JDBC3_TYPE_NAMES[] = { + public static final String JDBC_TYPE_NAMES[] = { "int2", "int4", "oid", "int8", diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index 1748e1bd..b44a1866 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -2996,7 +2996,7 @@ public java.sql.ResultSet getUDTs(String catalog, String schemaPattern, + " end as data_type, pg_catalog.obj_description(t.oid, 'pg_type') " + "as remarks, CASE WHEN t.typtype = 'd' then (select CASE"; - for(int i = 0; i < SPIConnection.JDBC3_TYPE_NAMES.length; i++) + for(int i = 0; i < SPIConnection.JDBC_TYPE_NAMES.length; i++) { sql += " when typname = '" + SPIConnection.JDBC_TYPE_NUMBERS[i] + "' then " + SPIConnection.JDBC_TYPE_NUMBERS[i]; From 45ae03b7cebc3ed7ab4499430d7bf3ff11efaca0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Aug 2018 01:09:35 -0400 Subject: [PATCH 0161/1087] Let ResultSetMetaData return JDBC4.2 Types.*ZONE. Works, but may have to be excluded from 1.5.x as it would be a behavior change without an opt-in. --- .../postgresql/pljava/jdbc/SPIConnection.java | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index ebb0a697..0e664400 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1015,28 +1015,54 @@ else if(value instanceof String) * * Tip: keep these grouped together by the Types. value */ - public static final int JDBC_TYPE_NUMBERS[] = - { - Types.SMALLINT, - Types.INTEGER, Types.INTEGER, - Types.BIGINT, - Types.DOUBLE, Types.DOUBLE, - Types.NUMERIC, - Types.REAL, - Types.DOUBLE, - Types.CHAR, Types.CHAR, Types.CHAR, Types.CHAR, Types.CHAR, Types.CHAR, - Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, - Types.BINARY, - Types.BOOLEAN, - Types.BIT, - Types.DATE, - Types.TIME, Types.TIME, - Types.TIMESTAMP, Types.TIMESTAMP, Types.TIMESTAMP, - Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, - Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, - Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, - Types.ARRAY - }; + public static final int JDBC_TYPE_NUMBERS[]; + + static + { + /* + * Try to get the JDBC 4.2 / Java 8 TIME*ZONE types reflectively. + * Once the Java back horizon advances to 8, just do this the easy way. + */ + int ttz = Types.TIME; // Use these values + int tstz = Types.TIMESTAMP; // pre-Java 8 + try + { + ttz = + Types.class.getField("TIME_WITH_TIMEZONE") + .getInt(Types.class); + tstz = + Types.class.getField("TIMESTAMP_WITH_TIMEZONE") + .getInt(Types.class); + } + catch ( NoSuchFieldException nsfe ) { } // ok, not running in Java 8 + catch ( IllegalAccessException iae ) + { + throw new ExceptionInInitializerError(iae); + } + + JDBC_TYPE_NUMBERS = new int[] + { + Types.SMALLINT, + Types.INTEGER, Types.INTEGER, + Types.BIGINT, + Types.DOUBLE, Types.DOUBLE, + Types.NUMERIC, + Types.REAL, + Types.DOUBLE, + Types.CHAR,Types.CHAR,Types.CHAR,Types.CHAR,Types.CHAR,Types.CHAR, + Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, + Types.BINARY, + Types.BOOLEAN, + Types.BIT, + Types.DATE, + Types.TIME, ttz, + Types.TIMESTAMP, Types.TIMESTAMP, tstz, + Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, + Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, + Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, Types.ARRAY, + Types.ARRAY + }; + } // ************************************************************ // Implementation of JDBC 4 methods. Methods go here if they From b43d1f67e3d77369d4a8c34a96f2f2a582335841 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Aug 2018 00:10:44 -0400 Subject: [PATCH 0162/1087] Recognize JDBC 4.2/JSR 310 types in DDR generator. --- .../pljava/sqlgen/DDRProcessor.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 1c4dc394..eaee906e 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -2259,6 +2259,14 @@ class TypeMapper this.addMap(Object.class, "\"any\""); this.addMap(byte[].class, "bytea"); + + // (Once Java back horizon advances to 8, do these the easy way.) + // + this.addMapIfExists("java.time.LocalDate", "date"); + this.addMapIfExists("java.time.LocalTime", "time"); + this.addMapIfExists("java.time.OffsetTime", "timetz"); + this.addMapIfExists("java.time.LocalDateTime", "timestamp"); + this.addMapIfExists("java.time.OffsetDateTime", "timestamptz"); } private boolean mappingsFrozen() @@ -2386,6 +2394,20 @@ void addMap(Class k, String v) addMap( typeMirrorFromClass( k), v); } + /** + * Add a custom mapping from a Java class to an SQL type, if a class + * with the given name exists. + * + * @param k Canonical class name representing the Java type + * @param v String representing the SQL type to be used + */ + void addMapIfExists(String k, String v) + { + TypeElement te = elmu.getTypeElement( k); + if ( null != te ) + addMap( te.asType(), v); + } + /** * Add a custom mapping from a Java class (represented as a TypeMirror) * to an SQL type. From 38a25b4f4f0bec0b152dc6a4c460c39ec8d622a0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Aug 2018 00:38:41 -0400 Subject: [PATCH 0163/1087] Include return types in most DDR method signatures The 'AS' syntax for PL/Java function declarations already accepts explicit return types as well as the parameter types, but the DDR generator has not been emitting them. That means the magic doesn't happen when, say, you have a Java method returning one of the JSR 310 date/time types instead of the expected ones from java.sql. For PL/Java to notice the automatic coercion needed there, it has to see that the Java return type is not the one that naturally corresponds to the SQL type ... so the Java return type needs to be included in the signature. Avoid, however, including the return type in the case of trigger functions, or those that return sets or composite types. Those already get special treatment in the function parsing code, and the explicit return types would get in the way. --- .../main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index eaee906e..46640951 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1613,6 +1613,8 @@ void appendParams( StringBuilder sb, boolean dflts) void appendAS( StringBuilder sb) { + if ( ! ( complexViaInOut || setof || trigger ) ) + sb.append( func.getReturnType()).append( '='); Element e = func.getEnclosingElement(); if ( ! e.getKind().equals( ElementKind.CLASS) ) msg( Kind.ERROR, func, From 64ebb93e3a130e6663f7022d42f835d0b72ce0a1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Aug 2018 23:16:33 -0400 Subject: [PATCH 0164/1087] Object getters by Class must survive null values. --- .../main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java | 2 +- .../main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java index 5e24e05c..f877669b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java @@ -640,7 +640,7 @@ public final T getObject(int columnIndex, Class type) { Object value = this.getObjectValue(columnIndex, type); m_wasNull = (value == null); - if ( type.isInstance(value) ) + if ( m_wasNull || type.isInstance(value) ) return type.cast(value); throw new SQLException("Cannot convert " + value.getClass().getName() + " to " + type.getName()); diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java index 756e627b..0324af67 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java @@ -352,7 +352,7 @@ public T readObject(Class type) throws SQLException ++m_index, type); } m_wasNull = v == null; - if ( type.isInstance(v) ) + if ( m_wasNull || type.isInstance(v) ) return type.cast(v); throw new SQLException("Cannot convert " + v.getClass().getName() + " to " + type.getName()); From b844cae474acafc4226264446d12b31680ade1e2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 22 Aug 2018 00:01:56 -0400 Subject: [PATCH 0165/1087] Introduce TypeBridge. For now, in the 1.5.x series, this is a simple stopgap so that the few places in PL/Java where an object type can be passed to PostgreSQL (SingleRowWriter, TriggerResultSet, SQLOutputToTuple, PreparedStatement) are able to pass an object that isn't of the class expected by default, and have the right native conversion get selected. All of those sites currently work by some variant of putting supplied objects into an Object array or list later passed to the native code, and when an object will not be of the expected class, what is stored in the array should be a TypeBridge.Holder for it. This may be a temporary class that goes away entirely in a future major release of PL/Java that revamps how type mappings are determined. Or, it may evolve and take on greater responsibility in a revamped scheme: type mapping information is, at present, diffused and duplicated a lot of places in PL/Java, and bringing it into one place would not be a bad thing. --- pljava-so/src/main/c/type/Type.c | 64 ++++ pljava-so/src/main/include/pljava/type/Type.h | 12 +- .../postgresql/pljava/jdbc/TypeBridge.java | 273 ++++++++++++++++++ 3 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 07f46602..2795a55a 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -78,6 +78,11 @@ static jclass s_Iterator_class; static jmethodID s_Iterator_hasNext; static jmethodID s_Iterator_next; +static jclass s_TypeBridge_Holder_class; +static jmethodID s_TypeBridge_Holder_className; +static jmethodID s_TypeBridge_Holder_defaultOid; +static jmethodID s_TypeBridge_Holder_payload; + /* Structure used in multi function calls (calls returning * SETOF ) */ @@ -245,6 +250,26 @@ Datum Type_coerceObject(Type self, jobject object) return self->typeClass->coerceObject(self, object); } +Datum Type_coerceObjectBridged(Type self, jobject object) +{ + jstring rqcname; + char *rqcname0; + Type rqtype; + + if ( JNI_FALSE == JNI_isInstanceOf(object, s_TypeBridge_Holder_class) ) + return Type_coerceObject(self, object); + + rqcname = JNI_callObjectMethod(object, s_TypeBridge_Holder_className); + rqcname0 = String_createNTS(rqcname); + JNI_deleteLocalRef(rqcname); + rqtype = Type_fromJavaType(self->typeId, rqcname0); + pfree(rqcname0); + if ( ! Type_canReplaceType(rqtype, self) ) + elog(ERROR, "type bridge failure"); + object = JNI_callObjectMethod(object, s_TypeBridge_Holder_payload); + return Type_coerceObject(rqtype, object); +} + char Type_getAlign(Type self) { return self->align; @@ -716,6 +741,43 @@ TupleDesc _Type_getTupleDesc(Type self, PG_FUNCTION_ARGS) return 0; /* Keep compiler happy */ } +static void addTypeBridge(jclass c, jmethodID m, char const *cName, Oid oid) +{ + jstring jcn = String_createJavaStringFromNTS(cName); + JNI_callStaticObjectMethodLocked(c, m, jcn, oid); + JNI_deleteLocalRef(jcn); +} + +static void initializeTypeBridges() +{ + jclass cls; + jmethodID ofClass; + jmethodID ofInterface; + + cls = PgObject_getJavaClass("org/postgresql/pljava/jdbc/TypeBridge"); + ofClass = PgObject_getStaticJavaMethod(cls, "ofClass", + "(Ljava/lang/String;I)Lorg/postgresql/pljava/jdbc/TypeBridge;"); + ofInterface = PgObject_getStaticJavaMethod(cls, "ofInterface", + "(Ljava/lang/String;I)Lorg/postgresql/pljava/jdbc/TypeBridge;"); + + addTypeBridge(cls, ofClass, "java.time.LocalDate", DATEOID); + addTypeBridge(cls, ofClass, "java.time.LocalDateTime", TIMESTAMPOID); + addTypeBridge(cls, ofClass, "java.time.LocalTime", TIMEOID); + addTypeBridge(cls, ofClass, "java.time.OffsetDateTime", TIMESTAMPTZOID); + addTypeBridge(cls, ofClass, "java.time.OffsetTime", TIMETZOID); + + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/jdbc/TypeBridge$Holder"); + s_TypeBridge_Holder_class = JNI_newGlobalRef(cls); + s_TypeBridge_Holder_className = PgObject_getJavaMethod(cls, "className", + "()Ljava/lang/String;"); + s_TypeBridge_Holder_defaultOid = PgObject_getJavaMethod(cls, "defaultOid", + "()I"); + s_TypeBridge_Holder_payload = PgObject_getJavaMethod(cls, "payload", + "()Ljava/lang/Object;"); +} + /* * Shortcuts to initializers of known types */ @@ -802,6 +864,8 @@ void Type_initialize(void) s_Iterator_class = JNI_newGlobalRef(PgObject_getJavaClass("java/util/Iterator")); s_Iterator_hasNext = PgObject_getJavaMethod(s_Iterator_class, "hasNext", "()Z"); s_Iterator_next = PgObject_getJavaMethod(s_Iterator_class, "next", "()Ljava/lang/Object;"); + + initializeTypeBridges(); } /* diff --git a/pljava-so/src/main/include/pljava/type/Type.h b/pljava-so/src/main/include/pljava/type/Type.h index b7ba62a7..ef8fe5eb 100644 --- a/pljava-so/src/main/include/pljava/type/Type.h +++ b/pljava-so/src/main/include/pljava/type/Type.h @@ -79,10 +79,20 @@ extern jvalue Type_coerceDatumAs(Type self, Datum datum, jclass rqcls); /* * Translate a given Object into a Datum accorging to the type represented - * by this instance. + * by this instance. The caller must be certain that 'object' is an instance + * of a Java type expected by the coercer for this TypeClass. */ extern Datum Type_coerceObject(Type self, jobject object); +/* + * Translate a given Object into a Datum accorging to the type represented + * by this instance. The object may be an instance of TypeBridge.Holder holding + * an object of an alternate Java class than what the coercer for this TypeClass + * expects. Otherwise, it must be an object of the expected class, just as for + * Type_coerceObject. + */ +extern Datum Type_coerceObjectBridged(Type self, jobject object); + /* * Return a Type based on a Postgres Oid. Creates a new type if * necessary. diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java new file mode 100644 index 00000000..d5d6189d --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2018- 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.jdbc; + +import static java.util.Collections.addAll; +import java.util.List; +import java.util.LinkedList; + +/** + * Encapsulate some information about Java object classes and their possible + * mappings to PostgreSQL types. + *

    + * This may be a temporary class that goes away entirely in a future major + * release of PL/Java that revamps how type mappings are determined. Or, it may + * evolve and take on greater responsibility in a revamped scheme: type mapping + * information is, at present, diffused and duplicated a lot of places in + * PL/Java, and bringing it into one place would not be a bad thing. + *

    + * For now, in the 1.5.x series, this is a simple stopgap so that the few places + * in PL/Java where an object type can be passed to PostgreSQL (SingleRowWriter, + * TriggerResultSet, SQLOutputToTuple, PreparedStatement) are able to pass an + * object that isn't of the class expected by default, and have the right native + * conversion get selected. All of those sites currently work by some variant of + * putting supplied objects into an Object array or list later passed to the + * native code, and when an object will not be of the expected class, what is + * stored in the array should be a TypeBridge.Holder for it. + */ +public abstract class TypeBridge +{ + /** + * Canonical name of the Java class or interface that this TypeBridge + * 'captures'. + *

    + * Held as a string so that the class does not need to be loaded for a + * TypeBridge to be made for it. There can be TypeBridges for classes that + * not all supported JRE versions provide. + */ + protected final String m_canonName; + + /** + * Oid of the PostgreSQL type to be associated by default with this Java + * class or interface. + *

    + * Stored as a simple int here, not a PL/Java Oid object, which I am tempted + * to deprecate. + */ + protected final int m_defaultOid; + + /** + * If the Java class associated with the TypeBridge is loaded and + * available, it can be cached here. + *

    + * That will always be the case after a {@link #captures} method has + * returned {@code true}. + */ + protected Class m_cachedClass; + + /** + * List of TypeBridges to check, in order, for one that 'captures' a given + * class. + *

    + * This list is populated as TypeBridges are constructed, and whatever code + * calls the factory methods must take responsibility for the order of the + * list, by not constructing one TypeBridge earlier than another one that it + * would capture. + *

    + * This can't be checked automatically because the classes in question may + * not yet be loaded, or even available. + */ + private static List> m_candidates = + new LinkedList>(); + + /** + * Return an object wrapped, if it is of any type captured by a known + * TypeBridge. + * @param o An object, representing a value to be presented to PostgreSQL. + * @return A Holder wrapping o, or null if no known TypeBridge captures the + * type of o, or o itself is null. + */ + public static TypeBridge.Holder wrap(U o) + { + if ( null == o ) + return null; + Class c = o.getClass(); + for ( TypeBridge tb : m_candidates ) + if ( tb.captures(c) ) + return ((TypeBridge)tb).new Holder(o); + if ( o instanceof TypeBridge.Holder ) + throw new IllegalArgumentException("Not valid as argument: " + + o.toString()); + return null; + } + + private TypeBridge(String cName, int dfltOid) + { + if ( null == cName ) + throw new NullPointerException("TypeBridge cName must be nonnull."); + m_canonName = cName; + m_defaultOid = dfltOid; + m_candidates.add(this); + } + + /* + * For now, anyway, these factory methods are private; only native code + * will be calling them. + */ + + /** + * Construct a TypeBridge given the canonical name of a Java type that need + * not be loaded, but is known to be a class (not an interface). + */ + private static TypeBridge ofClass(String cName, int dOid) + { + return new OfClass(cName, dOid); + } + + /** + * Construct a TypeBridge given the canonical name of a Java type that need + * not be loaded, but is known to be an interface (not a class). + */ + private static TypeBridge ofInterface(String cName, int dOid) + { + return new OfInterface(cName, dOid); + } + + /** + * Construct a TypeBridge directly from a Class object, when available. + */ + private static TypeBridge of(Class c, int dOid) + { + String cn = c.getCanonicalName(); + TypeBridge tb = + c.isInterface() ? ofInterface(cn, dOid) : ofClass(cn, dOid); + tb.m_cachedClass = c; + return tb; + } + + /** + * Determine whether this TypeBridge 'captures' a given Class. + *

    + * If the class this TypeBridge represents has already been loaded and is + * cached here, the test is a simple {@code isAssignableFrom}. Otherwise, + * the test is conducted by climbing the superclasses or superinterfaces, as + * appropriate, of the passed Class, comparing canonical names. If a match + * is found, the winning Class object is cached before returning + * {@code true}. + */ + public final boolean captures(Class c) + { + if ( null != m_cachedClass ) + return m_cachedClass.isAssignableFrom(c); + return virtuallyCaptures(c); + } + + /** + * Method the two subclasses implement to conduct the "Class-less" + * superclass or superinterface check, respectively. + */ + protected abstract boolean virtuallyCaptures(Class c); + + /** + * TypeBridge subclass representing a class (not an interface). + *

    + * Its {@code virtuallyCaptures} method simply climbs the superclass chain. + */ + final static class OfClass extends TypeBridge + { + private OfClass(String cn, int oid) { super(cn, oid); } + + @Override + protected boolean virtuallyCaptures(Class c) + { + for ( ; null != c ; c = c.getSuperclass() ) + { + if ( ! m_canonName.equals(c.getCanonicalName()) ) + continue; + m_cachedClass = (Class)c; + return true; + } + return false; + } + } + + /** + * TypeBridge subclass representing an interface (not a class). + *

    + * Its {@code virtuallyCaptures} method climbs the superinterfaces, + * breadth first. + */ + final static class OfInterface extends TypeBridge + { + private OfInterface(String cn, int oid) { super(cn, oid); } + + @Override + protected boolean virtuallyCaptures(Class c) + { + List> q = new LinkedList>(); + q.add(c); + + while ( 0 < q.size() ) + { + c = q.remove(0); + + if ( ! c.isInterface() ) + { + addAll(q, c.getInterfaces()); + c = c.getSuperclass(); + if ( null != c ) + q.add(c); + continue; + } + + if ( m_canonName.equals(c.getCanonicalName()) ) + { + m_cachedClass = (Class)c; + return true; + } + addAll(q, c.getInterfaces()); + } + return false; + } + } + + /** + * Class that holds an object reference being passed from Java to PG, when + * the object is of one of the known classes that were not accepted by + * PL/Java's JDBC driver before PL/Java 1.5.1. + *

    + * When a native-code Object-to-Datum coercer encounters a Holder instead of + * an object of the normally-expected class for the PostgreSQL type, it can + * retrieve the class, classname, default PG type oid, and the payload + * object itself, from the Holder, and obtain and apply a different coercer + * appropriate to the class. + */ + public final class Holder + { + private final S m_payload; + + private Holder(S o) + { + m_payload = o; + } + + public Class bridgedClass() + { + return m_cachedClass; + } + + public String className() + { + return m_canonName; + } + + public S payload() + { + return m_payload; + } + + public int defaultOid() + { + return m_defaultOid; + } + } +} From 65eb163c4a9a320e796181412209ff907c29cbf6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 22 Aug 2018 12:29:03 -0400 Subject: [PATCH 0166/1087] Put TypeBridge in play. This adds it to {Relation,TupleDesc}.c, where it is used by SingleRowWriter, TriggerResultSet, and SQLOutputToTuple. PreparedStatement will have slightly different requirements, and is left for later. --- pljava-so/src/main/c/type/Relation.c | 2 +- pljava-so/src/main/c/type/TupleDesc.c | 2 +- .../java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java | 3 ++- .../java/org/postgresql/pljava/jdbc/SingleRowWriter.java | 5 +++-- .../java/org/postgresql/pljava/jdbc/TriggerResultSet.java | 4 +++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/type/Relation.c b/pljava-so/src/main/c/type/Relation.c index 6f156db9..101ce2af 100644 --- a/pljava-so/src/main/c/type/Relation.c +++ b/pljava-so/src/main/c/type/Relation.c @@ -235,7 +235,7 @@ Java_org_postgresql_pljava_internal_Relation__1modifyTuple(JNIEnv* env, jclass c type = Type_fromOid(typeId, typeMap); value = JNI_getObjectArrayElement(_values, idx); if(value != 0) - values[idx] = Type_coerceObject(type, value); + values[idx] = Type_coerceObjectBridged(type, value); else { if(nulls == 0) diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index 02a9f391..9c44f335 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -239,7 +239,7 @@ Java_org_postgresql_pljava_internal_TupleDesc__1formTuple(JNIEnv* env, jclass cl if(value != 0) { Type type = Type_fromOid(SPI_gettypeid(self, idx + 1), typeMap); - values[idx] = Type_coerceObject(type, value); + values[idx] = Type_coerceObjectBridged(type, value); nulls[idx] = false; } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java index ddce3499..ab130110 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java @@ -248,6 +248,7 @@ private void writeValue(Object value) throws SQLException { if(m_index >= m_values.length) throw new SQLException("Tuple cannot take more values"); - m_values[m_index++] = value; + TypeBridge.Holder vAlt = TypeBridge.wrap(value); + m_values[m_index++] = null == vAlt ? value : vAlt; } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java index 239f5099..bb819baa 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java @@ -90,7 +90,8 @@ public void updateObject(int columnIndex, Object x) m_values[columnIndex-1] = x; Class c = m_tupleDesc.getColumnClass(columnIndex); - if(!c.isInstance(x) + TypeBridge.Holder xAlt = TypeBridge.wrap(x); + if(null == xAlt && !c.isInstance(x) && !(c == byte[].class && (x instanceof BlobValue))) { if(Number.class.isAssignableFrom(c)) @@ -103,7 +104,7 @@ public void updateObject(int columnIndex, Object x) else x = SPIConnection.basicCoersion(c, x); } - m_values[columnIndex-1] = x; + m_values[columnIndex-1] = null == xAlt ? x : xAlt; } @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java index 3592b29b..89b3e6e3 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java @@ -126,7 +126,9 @@ public Object[] getChangeIndexesAndValues() for(int idx = 0; idx < top; ++idx) { indexes[idx] = ((Integer)changes.get(vIdx++)).intValue(); - values[idx] = changes.get(vIdx++); + Object v = changes.get(vIdx++); + TypeBridge.Holder vAlt = TypeBridge.wrap(v); + values[idx] = null == vAlt ? v : vAlt; } return new Object[] { m_tuple, indexes, values }; } From e1614d468f23d8eb5f7a389024dd689afbf330b6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 22 Aug 2018 19:44:21 -0400 Subject: [PATCH 0167/1087] Add example exercising the java.time types. --- .../pljava/example/annotation/JDBC42_21.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java 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 new file mode 100644 index 00000000..fd65aa75 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2018- 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 org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLActions; + +/** + * Exercise new mappings between date/time types and java.time classes + * (JDBC 4.2 change 21). + *

    + * Defines a method {@link #javaSpecificationGE javaSpecificationGE} that may be + * of use for other examples. + *

    + * Relies on PostgreSQL-version-specific implementor tags set up in the + * {@link ConditionalDDR} example. + */ +@SQLActions({ + @SQLAction( + implementor="postgresql_ge_90300", requires="javaSpecificationGE", + install= + "SELECT CASE WHEN javatest.javaSpecificationGE('1.8')" + + " THEN set_config('pljava.implementors', 'pg_jdbc42_21,' || " + + " current_setting('pljava.implementors'), true) " + + "END" + ), + + @SQLAction( + implementor="pg_jdbc42_21", requires="TypeRoundTripper.roundTrip", + install={ + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.LocalDate passes')" + + " ELSE javatest.logmessage('WARNING', 'java.time.LocalDate fails')" + + " END" + + " FROM" + + " (VALUES" + + " (date '2017-08-21')," + + " (date '1970-03-07')," + + " (date '1919-05-29')" + + " ) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.LocalDate')" + + " AS r(roundtripped date)", + + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.LocalTime passes')" + + " ELSE javatest.logmessage('WARNING', 'java.time.LocalTime fails')" + + " END" + + " FROM" + + " (SELECT current_time::time) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.LocalTime')" + + " AS r(roundtripped time)", + + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.OffsetTime passes')" + + " ELSE javatest.logmessage('WARNING', 'java.time.OffsetTime fails')" + + " END" + + " FROM" + + " (SELECT current_time::timetz) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.OffsetTime')" + + " AS r(roundtripped timetz)", + + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.LocalDateTime passes')" + + " ELSE javatest.logmessage('WARNING','java.time.LocalDateTime fails')"+ + " END" + + " FROM" + + " (VALUES" + + " (timestamp '2017-08-21 18:25:29.900005')," + + " (timestamp '1970-03-07 17:37:49.300009')," + + " (timestamp '1919-05-29 13:08:33.600001')" + + " ) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.LocalDateTime')" + + " AS r(roundtripped timestamp)", + + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.OffsetDateTime passes')"+ + " ELSE javatest.logmessage(" + + " 'WARNING','java.time.OffsetDateTime fails')"+ + " END" + + " FROM" + + " (VALUES" + + " (timestamptz '2017-08-21 18:25:29.900005Z')," + + " (timestamptz '1970-03-07 17:37:49.300009Z')," + + " (timestamptz '1919-05-29 13:08:33.600001Z')" + + " ) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.OffsetDateTime')" + + " AS r(roundtripped timestamptz)" + }) +}) +public class JDBC42_21 +{ + /** + * Return true if running under a Java specification version at least as + * recent as the argument ('1.6', '1.7', '1.8', '9', '10', '11', ...). + */ + @Function(schema="javatest", provides="javaSpecificationGE") + public static boolean javaSpecificationGE(String want) + { + String got = System.getProperty("java.specification.version"); + if ( want.startsWith("1.") ) + want = want.substring(2); + if ( got.startsWith("1.") ) + got = got.substring(2); + return 0 <= Integer.valueOf(got).compareTo(Integer.valueOf(want)); + } +} From 645cf8e650a81d45fc3cd911f48067ebf1a834dc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Aug 2018 18:49:57 -0400 Subject: [PATCH 0168/1087] Put TypeBridge in play for SPIPreparedStatement. In passing, complete the implementation of the 3-argument form of setNull (where the third argument is a PostgreSQL type name), which is the only method on a PreparedStatement by which a parameter type can be set to a specific PostgreSQL type (as opposed to a generic JDBC type with some default mapping to a PostgreSQL type). However, the implementation is still limited, in that any subsequent non-null assignment will reset the PostgreSQL type to the default mapping, if done through one of the pre-JDBC 4.2 setter methods, or setObject with one of the pre-JDBC 4.2 accepted object classes. That is to avoid a material behavior change in a minor release. By contrast, if setObject is called with an object of one of the newly-accepted classes (which had no prior behavior to try to match), any already-assigned PostgreSQL type (as by setNull) will be respected, and the setObject treated as a coercion to it (which can entail an exception if no such coercion is available). That new behavior ought to be the way everything behaves in some future major release, which should use the PG 9.0 improved SPI to determine the parameters' PostgreSQL types from what PG's type inference came up with, and treat all the setter methods as coercions to the determined types. --- pljava-so/src/main/c/ExecutionPlan.c | 2 +- .../pljava/jdbc/SPIPreparedStatement.java | 175 ++++++++++++++++-- 2 files changed, 164 insertions(+), 13 deletions(-) diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index 82ba069c..9ccaece4 100644 --- a/pljava-so/src/main/c/ExecutionPlan.c +++ b/pljava-so/src/main/c/ExecutionPlan.c @@ -89,7 +89,7 @@ static bool coerceObjects(void* ePlan, jobjectArray jvalues, Datum** valuesPtr, jobject value = JNI_getObjectArrayElement(jvalues, idx); if(value != 0) { - values[idx] = Type_coerceObject(type, value); + values[idx] = Type_coerceObjectBridged(type, value); JNI_deleteLocalRef(value); } else diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index f78c40cc..c81ffee9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -64,6 +64,7 @@ public SPIPreparedStatement(SPIConnection conn, String statement, int paramCount Arrays.fill(m_sqlTypes, Types.NULL); } + @Override public void close() throws SQLException { @@ -77,6 +78,7 @@ public void close() Invocation.current().forgetStatement(this); } + @Override public ResultSet executeQuery() throws SQLException { @@ -84,6 +86,7 @@ public ResultSet executeQuery() return this.getResultSet(); } + @Override public int executeUpdate() throws SQLException { @@ -91,77 +94,92 @@ public int executeUpdate() return this.getUpdateCount(); } + @Override public void setNull(int columnIndex, int sqlType) throws SQLException { this.setObject(columnIndex, null, sqlType); } + @Override public void setBoolean(int columnIndex, boolean value) throws SQLException { this.setObject(columnIndex, value ? Boolean.TRUE : Boolean.FALSE, Types.BOOLEAN); } + @Override public void setByte(int columnIndex, byte value) throws SQLException { this.setObject(columnIndex, new Byte(value), Types.TINYINT); } + @Override public void setShort(int columnIndex, short value) throws SQLException { this.setObject(columnIndex, new Short(value), Types.SMALLINT); } + @Override public void setInt(int columnIndex, int value) throws SQLException { this.setObject(columnIndex, new Integer(value), Types.INTEGER); } + @Override public void setLong(int columnIndex, long value) throws SQLException { this.setObject(columnIndex, new Long(value), Types.BIGINT); } + @Override public void setFloat(int columnIndex, float value) throws SQLException { this.setObject(columnIndex, new Float(value), Types.FLOAT); } + @Override public void setDouble(int columnIndex, double value) throws SQLException { this.setObject(columnIndex, new Double(value), Types.DOUBLE); } + @Override public void setBigDecimal(int columnIndex, BigDecimal value) throws SQLException { this.setObject(columnIndex, value, Types.DECIMAL); } + @Override public void setString(int columnIndex, String value) throws SQLException { this.setObject(columnIndex, value, Types.VARCHAR); } + @Override public void setBytes(int columnIndex, byte[] value) throws SQLException { this.setObject(columnIndex, value, Types.VARBINARY); } + @Override public void setDate(int columnIndex, Date value) throws SQLException { this.setObject(columnIndex, value, Types.DATE); } + @Override public void setTime(int columnIndex, Time value) throws SQLException { this.setObject(columnIndex, value, Types.TIME); } + @Override public void setTimestamp(int columnIndex, Timestamp value) throws SQLException { this.setObject(columnIndex, value, Types.TIMESTAMP); } + @Override public void setAsciiStream(int columnIndex, InputStream value, int length) throws SQLException { try @@ -179,16 +197,19 @@ public void setAsciiStream(int columnIndex, InputStream value, int length) throw /** * @deprecated */ + @Override public void setUnicodeStream(int columnIndex, InputStream value, int arg2) throws SQLException { throw new UnsupportedFeatureException("PreparedStatement.setUnicodeStream"); } + @Override public void setBinaryStream(int columnIndex, InputStream value, int length) throws SQLException { this.setObject(columnIndex, new BlobValue(value, length), Types.BLOB); } + @Override public void clearParameters() throws SQLException { @@ -196,21 +217,38 @@ public void clearParameters() Arrays.fill(m_sqlTypes, Types.NULL); } + /** + * Implemented on {@link #setObject(int,Object,int)}, discarding scale. + */ + @Override public void setObject(int columnIndex, Object value, int sqlType, int scale) throws SQLException { this.setObject(columnIndex, value, sqlType); } + @Override public void setObject(int columnIndex, Object value, int sqlType) throws SQLException + { + setObject(columnIndex, value, sqlType, TypeBridge.wrap(value)); + } + + private void setObject( + int columnIndex, Object value, int sqlType, TypeBridge.Holder vAlt) + throws SQLException { if(columnIndex < 1 || columnIndex > m_sqlTypes.length) throw new SQLException("Illegal parameter index"); - Oid id = (sqlType == Types.OTHER) - ? Oid.forJavaObject(value) - : Oid.forSqlType(sqlType); + Oid id = null; + + if ( null != vAlt ) + id = new Oid(vAlt.defaultOid()); + else if ( sqlType != Types.OTHER ) + id = Oid.forSqlType(sqlType); + else + id = Oid.forJavaObject(value); // Default to String. // @@ -218,29 +256,70 @@ public void setObject(int columnIndex, Object value, int sqlType) id = Oid.forSqlType(Types.VARCHAR); Oid op = m_typeIds[--columnIndex]; + + /* + * Coordinate this behavior with the newly-implemented + * setNull(int,int,String), which can have been used to set a specific + * PostgreSQL type oid that is not the default mapping from any JDBC + * type. + * + * If no oid has already been set, unconditionally assign the one just + * chosen above. If the one just chosen matches one already set, do + * nothing. Otherwise, assign the one just chosen and re-prepare, but + * ONLY IF WE HAVE NOT BEEN GIVEN A TYPEBRIDGE.HOLDER. If a Holder is + * supplied, the value is of one of the types newly allowed for 1.5.1; + * it is safe to introduce a different behavior with those, as they had + * no prior behavior to match. + * + * The behavior for the new types is to NOT overwrite whatever PG oid + * may have been already assigned, but to simply pass the Holder and + * hope the native Type implementation knows how to munge the object + * to that PG type. An exception will ensue if it does not. + * + * The ultimate (future major release) way for PreparedStatement + * parameter typing to work will be to rely on the improved SPI from + * PG 9.0 to find out the parameter types PostgreSQL's type inference + * has come up with, and treat assignments here as coercions to those, + * just as for result-set updaters. That will moot most of these goofy + * half-measures here. https://www.postgresql.org/message-id/ + * d5ecbef6-88ee-85d8-7cc2-8c8741174f2d%40anastigmatix.net + */ + if(op == null) m_typeIds[columnIndex] = id; - else if(!op.equals(id)) + else if ( null == vAlt && !op.equals(id) ) { m_typeIds[columnIndex] = id; // We must re-prepare // - if(m_plan != null) + if ( m_plan != null ) + { m_plan.close(); - m_plan = null; + m_plan = null; + } } m_sqlTypes[columnIndex] = sqlType; - m_values[columnIndex] = value; + m_values[columnIndex] = null == vAlt ? value : vAlt; } + @Override public void setObject(int columnIndex, Object value) throws SQLException { if(value == null) - throw new SQLException("Can't assign null unless the SQL type is known"); + throw new SQLException( + "Can't assign null unless the SQL type is known"); + + TypeBridge.Holder vAlt = TypeBridge.wrap(value); - this.setObject(columnIndex, value, SPIConnection.getTypeForClass(value.getClass())); + int sqlType; + if ( null == vAlt ) + sqlType = SPIConnection.getTypeForClass(value.getClass()); + else + sqlType = Types.OTHER; + + this.setObject(columnIndex, value, sqlType, vAlt); } /** @@ -259,6 +338,7 @@ private int[] getSqlTypes() return types; } + @Override public boolean execute() throws SQLException { @@ -286,6 +366,7 @@ public boolean execute(String statement) throw new UnsupportedFeatureException("Can't execute other statements using a prepared statement"); } + @Override public void addBatch() throws SQLException { @@ -297,33 +378,39 @@ public void addBatch() * The prepared statement cannot have other statements added too it. * @throws SQLException indicating that this feature is not supported. */ + @Override public void addBatch(String statement) throws SQLException { throw new UnsupportedFeatureException("Can't add batch statements to a prepared statement"); } + @Override public void setCharacterStream(int columnIndex, Reader value, int length) throws SQLException { this.setObject(columnIndex, new ClobValue(value, length), Types.CLOB); } + @Override public void setRef(int columnIndex, Ref value) throws SQLException { this.setObject(columnIndex, value, Types.REF); } + @Override public void setBlob(int columnIndex, Blob value) throws SQLException { this.setObject(columnIndex, value, Types.BLOB); } + @Override public void setClob(int columnIndex, Clob value) throws SQLException { this.setObject(columnIndex, value, Types.CLOB); } + @Override public void setArray(int columnIndex, Array value) throws SQLException { this.setObject(columnIndex, value, Types.ARRAY); @@ -333,12 +420,14 @@ public void setArray(int columnIndex, Array value) throws SQLException * ResultSetMetaData is not yet supported. * @throws SQLException indicating that this feature is not supported. */ + @Override public ResultSetMetaData getMetaData() throws SQLException { throw new UnsupportedFeatureException("ResultSet meta data is not yet implemented"); } + @Override public void setDate(int columnIndex, Date value, Calendar cal) throws SQLException { @@ -347,6 +436,7 @@ public void setDate(int columnIndex, Date value, Calendar cal) throw new UnsupportedFeatureException("Setting date using explicit Calendar"); } + @Override public void setTime(int columnIndex, Time value, Calendar cal) throws SQLException { @@ -355,6 +445,7 @@ public void setTime(int columnIndex, Time value, Calendar cal) throw new UnsupportedFeatureException("Setting time using explicit Calendar"); } + @Override public void setTimestamp(int columnIndex, Timestamp value, Calendar cal) throws SQLException { @@ -363,12 +454,51 @@ public void setTimestamp(int columnIndex, Timestamp value, Calendar cal) throw new UnsupportedFeatureException("Setting time using explicit Calendar"); } + /** + * This method can (and is the only method that can, until JDBC 4.2 SQLType + * is implemented) assign a specific PostgreSQL type, by name, to a + * PreparedStatement parameter. + *

    + * However, to avoid a substantial behavior change in a 1.5.x minor release, + * its effect is limited for now. Any subsequent assignment of a non-null + * value for the parameter, using any of the setter methods or + * setObject-accepted classes from pre-JDBC 4.2, will reset the associated + * PostgreSQL type to what would have been assigned according to the JDBC + * {@code sqlType} or the type of the object. + *

    + * In contrast, setObject with any of the object types newly recognized + * in PL/Java 1.5.1 will not overwrite the PostgreSQL type assigned by this + * method, but will let it stand, on the assumption that the object's native + * to-Datum coercions will include one that applies to the type. If not, an + * exception will result. + *

    + * The {@code sqlType} supplied here will be remembered, only to be used by + * the somewhat-functionally-impaired {@code ParameterMetaData} + * implementation. It is not checked for compatibility with the supplied + * PostgreSQL {@code typeName} in any way. + */ + @Override public void setNull(int columnIndex, int sqlType, String typeName) throws SQLException { - this.setNull(columnIndex, sqlType); + Oid id = Oid.forTypeName(typeName); + Oid op = m_typeIds[--columnIndex]; + if ( null == op ) + m_typeIds[columnIndex] = id; + else if ( !op.equals(id) ) + { + m_typeIds[columnIndex] = id; + if ( null != m_plan ) + { + m_plan.close(); + m_plan = null; + } + } + m_sqlTypes[columnIndex] = sqlType; + m_values[columnIndex] = null; } + @Override public void setURL(int columnIndex, URL value) throws SQLException { this.setObject(columnIndex, value, Types.DATALINK); @@ -388,6 +518,7 @@ public String toString() * object based on the supplied values. * @return The meta data for parameter values. */ + @Override public ParameterMetaData getParameterMetaData() throws SQLException { @@ -438,6 +569,7 @@ protected long executeBatchEntry(Object batchEntry) // Non-implementation of JDBC 4 methods. // ************************************************************ + @Override public void setNClob(int parameterIndex, Reader reader) throws SQLException @@ -449,6 +581,7 @@ public void setNClob(int parameterIndex, } + @Override public void setNClob(int parameterIndex, NClob value) throws SQLException @@ -460,6 +593,7 @@ public void setNClob(int parameterIndex, } + @Override public void setNClob(int parameterIndex, Reader reader,long length) throws SQLException @@ -472,6 +606,7 @@ public void setNClob(int parameterIndex, } + @Override public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException @@ -483,6 +618,7 @@ public void setBlob(int parameterIndex, "0A000" ); } + @Override public void setBlob(int parameterIndex, InputStream inputStream,long length) throws SQLException @@ -495,6 +631,7 @@ public void setBlob(int parameterIndex, } + @Override public void setClob(int parameterIndex, Reader reader) throws SQLException @@ -505,6 +642,8 @@ public void setClob(int parameterIndex, "0A000" ); } + + @Override public void setClob(int parameterIndex, Reader reader,long length) throws SQLException @@ -517,6 +656,7 @@ public void setClob(int parameterIndex, } + @Override public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException @@ -528,6 +668,8 @@ public void setNCharacterStream(int parameterIndex, "0A000" ); } + + @Override public void setNCharacterStream(int parameterIndex, Reader value,long length) throws SQLException @@ -540,6 +682,7 @@ public void setNCharacterStream(int parameterIndex, } + @Override public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException @@ -552,8 +695,9 @@ public void setCharacterStream(int parameterIndex, } + @Override public void setCharacterStream(int parameterIndex, - Reader reader,long lenght) + Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException @@ -564,6 +708,7 @@ public void setCharacterStream(int parameterIndex, } + @Override public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException @@ -576,8 +721,9 @@ public void setBinaryStream(int parameterIndex, } + @Override public void setBinaryStream(int parameterIndex, - InputStream x,long length) + InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException @@ -588,6 +734,7 @@ public void setBinaryStream(int parameterIndex, } + @Override public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException @@ -600,6 +747,7 @@ public void setAsciiStream(int parameterIndex, } + @Override public void setAsciiStream(int parameterIndex, InputStream x,long length) throws SQLException @@ -612,6 +760,7 @@ public void setAsciiStream(int parameterIndex, } + @Override public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException @@ -622,6 +771,7 @@ public void setSQLXML(int parameterIndex, "0A000" ); } + @Override public void setNString(int parameterIndex, String value) throws SQLException @@ -632,6 +782,7 @@ public void setNString(int parameterIndex, "0A000" ); } + @Override public void setRowId(int parameterIndex, RowId x) throws SQLException From 4494845dddee7d72f967bac3e3401ab0481138f4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 22 Aug 2018 23:34:59 -0400 Subject: [PATCH 0169/1087] Add test of java.time class as PreparedStmt param. --- .../pljava/example/annotation/JDBC42_21.java | 13 ++++++++++- .../example/annotation/TypeRoundTripper.java | 23 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) 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 fd65aa75..c0e374a1 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 @@ -99,7 +99,18 @@ " (timestamptz '1919-05-29 13:08:33.600001Z')" + " ) AS p(orig)," + " javatest.roundtrip(p, 'java.time.OffsetDateTime')" + - " AS r(roundtripped timestamptz)" + " AS r(roundtripped timestamptz)", + + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'OffsetTime as stmt param passes')"+ + " ELSE javatest.logmessage(" + + " 'WARNING','java.time.OffsetTime as stmt param fails')"+ + " END" + + " FROM" + + " (SELECT current_time::timetz) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.OffsetTime', true)" + + " AS r(roundtripped timetz)" }) }) public class JDBC42_21 diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index 3279d82b..9d3c6d64 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -16,6 +16,9 @@ import static java.lang.reflect.Modifier.isPublic; import static java.lang.reflect.Modifier.isStatic; +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Types; @@ -132,6 +135,10 @@ private TypeRoundTripper() { } * @param classname Name of class to be explicitly requested (JDBC 4.1 * feature) from {@code getObject}; pass an empty string (the default) to * make no such explicit request. + * @param prepare Whether the object retrieved from {@code in} should be + * passed as a parameter to an identity {@code PreparedStatement} and the + * result of that be returned. If false (the default), the value from + * {@code in} is simply forwarded directly to {@code out}. * @param out The output row (supplied by PL/Java, representing the column * definition list that follows the call of this function in SQL). * @throws SQLException if {@code in} does not have exactly one column, if @@ -146,7 +153,8 @@ private TypeRoundTripper() { } implementor = "postgresql_ge_80400" // supports function param DEFAULTs ) public static boolean roundTrip( - ResultSet in, @SQLType(defaultValue="") String classname, ResultSet out) + ResultSet in, @SQLType(defaultValue="") String classname, + @SQLType(defaultValue="false") boolean prepare, ResultSet out) throws SQLException { ResultSetMetaData inmd = in.getMetaData(); @@ -179,6 +187,19 @@ public static boolean roundTrip( int inTypeJDBC = inmd.getColumnType(1); Object val = (null == clazz) ? in.getObject(1) : in.getObject(1, clazz); + if ( prepare ) + { + Connection c = getConnection("jdbc:default:connection"); + PreparedStatement ps = c.prepareStatement("SELECT ?"); + ps.setObject(1, val); + ResultSet rs = ps.executeQuery(); + rs.next(); + val = (null == clazz) ? rs.getObject(1) : rs.getObject(1, clazz); + rs.close(); + ps.close(); + c.close(); + } + for ( int i = 1; i <= outcols; ++ i ) { String what = outmd.getColumnLabel(i); From 155f772e33db7e67f06c12e114192164def9e635 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Aug 2018 19:15:16 -0400 Subject: [PATCH 0170/1087] Selectively expose new Types.* for 1.5.1. Because 1.5.1 is to be a minor release, there is an argument for not (yet) changing what ResultSetMetaData and friends will return from getColumnType() for a timetz or timestamptz column, so leave those to return Types.TIME or Types.TIMESTAMP as they always have. Client code that cares can use getColumnTypeName() to distinguish time or timestamp from timetz/timestamptz. At the same time, there is no need to feign ignorance of Types.TIME_WITH_TIMEZONE or TIMESTAMP_WITH_TIMEZONE if a client asks for them, so do add them in the list recognized in Oid.c. --- pljava-so/src/main/c/type/Oid.c | 8 +++++- .../postgresql/pljava/jdbc/SPIConnection.java | 28 +++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index 2c2690b4..d2ba1424 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -118,12 +118,18 @@ Oid Oid_forSqlType(int sqlType) case java_sql_Types_LONGNVARCHAR: case java_sql_Types_NCLOB: case java_sql_Types_SQLXML: + typeId = InvalidOid; /* Not yet mapped */ + break; /* JDBC 4.2 - conditionalize until only Java 8 and later supported */ #ifdef java_sql_Types_REF_CURSOR - case java_sql_Types_REF_CURSOR: case java_sql_Types_TIME_WITH_TIMEZONE: + typeId = TIMETZOID; + break; case java_sql_Types_TIMESTAMP_WITH_TIMEZONE: + typeId = TIMESTAMPTZOID; + break; + case java_sql_Types_REF_CURSOR: #endif default: typeId = InvalidOid; /* Not yet mapped */ diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 0e664400..29a98c40 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1025,20 +1025,20 @@ else if(value instanceof String) */ int ttz = Types.TIME; // Use these values int tstz = Types.TIMESTAMP; // pre-Java 8 - try - { - ttz = - Types.class.getField("TIME_WITH_TIMEZONE") - .getInt(Types.class); - tstz = - Types.class.getField("TIMESTAMP_WITH_TIMEZONE") - .getInt(Types.class); - } - catch ( NoSuchFieldException nsfe ) { } // ok, not running in Java 8 - catch ( IllegalAccessException iae ) - { - throw new ExceptionInInitializerError(iae); - } +// try COMMENTED OUT FOR BACK-COMPATIBILITY REASONS IN PL/JAVA 1.5.x +// { +// ttz = +// Types.class.getField("TIME_WITH_TIMEZONE") +// .getInt(Types.class); +// tstz = +// Types.class.getField("TIMESTAMP_WITH_TIMEZONE") +// .getInt(Types.class); +// } +// catch ( NoSuchFieldException nsfe ) { } // ok, not running in Java 8 +// catch ( IllegalAccessException iae ) +// { +// throw new ExceptionInInitializerError(iae); +// } JDBC_TYPE_NUMBERS = new int[] { From de248df6cfb1e9517bd59bc340c8cf79254baf9c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 25 Aug 2018 12:37:44 -0400 Subject: [PATCH 0171/1087] Fix old timestamp coercion for !integer_datetimes. Testing the new java.time conversions under !integer_datetimes revealed that they work, but the old existing ones weren't quite right ... the same issues as in issue #155, but that fix wasn't tested with !integer_datetimes, so they were overlooked for that case. In passing, simplify the new _LocalDateTime_coerceDatum. Breaking the multiplication by 1e6 into a six-bit exponent adjust and separate 14-bit multiplier was unnecessarily cautious. There are ~ 20 bits we care about to the right of the radix point. All the rest are available after the whole seconds are subtracted out, and even multiplying by 1e9 won't need more than 21 of them (9 go straight to the exponent). To the best of my knowledge, nobody ever made a double precision format with a significand too narrow for 41 bits. --- pljava-so/src/main/c/type/Timestamp.c | 42 +++++++++++++++------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index c68f3e35..eb63f14a 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -101,34 +101,31 @@ static bool _LocalDateTime_canReplaceType(Type self, Type other) static jvalue _LocalDateTime_coerceDatum(Type self, Datum arg) { - jlong micros; jint onlyMicros; jlong secs; jvalue result; #if PG_VERSION_NUM < 100000 if ( !integerDateTimes ) { - /* 1e6 = 64 * 15625. *64 is a radix point shift, no precision loss */ - double shiftedFracSecs = DatumGetFloat8(arg) * 64; - int64 shiftedSecs = (int64)floor(shiftedFracSecs); - shiftedFracSecs -= (double)shiftedSecs; - micros = (jlong)(15625*shiftedSecs); - micros += ((jlong)floor(31250. * shiftedFracSecs) + 1) / 2; + double fracSecs = DatumGetFloat8(arg); + secs = (jlong)floor(fracSecs); + fracSecs -= (double)secs; + onlyMicros = ((jint)floor(2e6 * fracSecs) + 1) / 2; } else #endif { - micros = DatumGetInt64(arg); + int64 micros = DatumGetInt64(arg); + /* Expect number of microseconds since 01 Jan 2000. Tease out a + * non-negative sub-second microseconds value (whether this C compiler's + * signed % has trunc or floor behavior). + */ + onlyMicros = (jint)(((micros % 1000000) + 1000000) % 1000000); + secs = (micros - onlyMicros) / 1000000; } - /* Expect number of microseconds since 01 Jan 2000. Tease out a non-negative - * sub-second microseconds value (whether this C compiler's signed % - * has trunc or floor behavior). - */ - onlyMicros = (jint)(((micros % 1000000) + 1000000) % 1000000); - secs = EPOCH_DIFF + (micros - onlyMicros) / 1000000; result.l = JNI_callStaticObjectMethod(s_LocalDateTime_class, s_LocalDateTime_ofEpochSecond, - secs, 1000 * onlyMicros, s_ZoneOffset_UTC); + EPOCH_DIFF + secs, 1000 * onlyMicros, s_ZoneOffset_UTC); return result; } @@ -272,7 +269,7 @@ static jvalue Timestamp_coerceDatumTZ_id(Type self, Datum arg, bool tzAdjust) #if PG_VERSION_NUM < 100000 static jvalue Timestamp_coerceDatumTZ_dd(Type self, Datum arg, bool tzAdjust) { - jlong mSecs; + jlong secs; jint uSecs; jvalue result; double ts = DatumGetFloat8(arg); @@ -283,9 +280,9 @@ static jvalue Timestamp_coerceDatumTZ_dd(Type self, Datum arg, bool tzAdjust) if(tzAdjust) ts += tz; /* Adjust from local time to UTC */ ts += EPOCH_DIFF; /* Adjust for diff between Postgres and Java (Unix) */ - mSecs = (jlong) floor(ts * 1000.0); /* Convert to millisecs */ - uSecs = (jint) ((ts - floor(ts)) * 1000000.0); /* Preserve microsecs */ - result.l = JNI_newObject(s_Timestamp_class, s_Timestamp_init, mSecs); + secs = (jlong) floor(ts); /* Take just the secs */ + uSecs = (((jint) ((ts - secs) * 2e6)) + 1) / 2; /* Preserve microsecs */ + result.l = JNI_newObject(s_Timestamp_class, s_Timestamp_init, secs * 1000); if(uSecs != 0) JNI_callVoidMethod(result.l, s_Timestamp_setNanos, uSecs * 1000); return result; @@ -328,6 +325,13 @@ static Datum Timestamp_coerceObjectTZ_dd(Type self, jobject jts, bool tzAdjust) double ts; jlong mSecs = JNI_callLongMethod(jts, s_Timestamp_getTime); jint nSecs = JNI_callIntMethod(jts, s_Timestamp_getNanos); + /* + * getNanos() should have supplied non-negative nSecs, whether mSecs is + * positive or negative. So mSecs needs to be floor()ed to a multiple of + * 1000 ms, whether this C compiler does signed integer division with floor + * or trunc. + */ + mSecs -= ((mSecs % 1000) + 1000) % 1000; ts = ((double)mSecs) / 1000.0; /* Convert to seconds */ ts -= EPOCH_DIFF; if(nSecs != 0) From ac64ef673f0f9edb81ba8d4c0dfcf65acd6e7ba6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 25 Aug 2018 14:06:54 -0400 Subject: [PATCH 0172/1087] Have StAXResultAdapter implement XMLStreamWriter. As the TODO comment said: it seems there is a practical difference between the XMLEventWriter and XMLStreamWriter APIs, as the former cannot distinguish between self-closed empty elements and the start-and-end tag form, while the latter can. This should probably be replaced by a class that implements XMLStreamWriter; that's just more tedious and longwinded, with so many more methods to implement. The interesting stuff is more or less done, so no more putting off the tedious and longwinded. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 229 ++++++++++++++---- 1 file changed, 185 insertions(+), 44 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 43f46266..440dcb60 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -109,7 +109,7 @@ import javax.xml.transform.sax.TransformerHandler; import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamWriter; /* ... for SQLXMLImpl.SAXResultAdapter and .SAXUnwrapFilter */ @@ -954,7 +954,7 @@ public T setResult(Class resultClass) { XMLOutputFactory xof = XMLOutputFactory.newFactory(); os = new DeclCheckedOutputStream(os, m_serverCS); - XMLEventWriter xsw = xof.createXMLEventWriter( + XMLStreamWriter xsw = xof.createXMLStreamWriter( os, m_serverCS.name()); xsw = new StAXResultAdapter(xsw, os); return resultClass.cast(new StAXResult(xsw)); @@ -1444,109 +1444,250 @@ public int nextTag() throws XMLStreamException } /** - * Class to wrap a StAX {@code XMLEventWriter} and hook the - * {@code endDocument} event to also close the underlying output stream, + * Class to wrap a StAX {@code XMLStreamWriter} and hook the method + * {@code writeEndDocument} to also close the underlying output stream, * making the {@code SQLXML} object ready to use for storing or returning * the value. */ - static class StAXResultAdapter implements XMLEventWriter + static class StAXResultAdapter implements XMLStreamWriter { - /* - * TODO: it seems there is a practical difference between the - * XMLEventWriter and XMLStreamWriter APIs, as the former cannot - * distinguish between self-closed empty elements and the start-and-end - * tag form, while the latter can. This should probably be replaced - * by a class that implements XMLStreamWriter; that's just more tedious - * and longwinded, with so many more methods to implement. - */ - private XMLEventWriter m_xew; + private XMLStreamWriter m_xsw; private OutputStream m_os; - StAXResultAdapter(XMLEventWriter xew, OutputStream os) + StAXResultAdapter(XMLStreamWriter xsw, OutputStream os) { - m_xew = xew; + m_xsw = xsw; m_os = os; } @Override - public void flush() throws XMLStreamException + public void writeStartElement(String localName) + throws XMLStreamException { - m_xew.flush(); + m_xsw.writeStartElement(localName); } @Override - public void close() throws XMLStreamException + public void writeStartElement(String namespaceURI, String localName) + throws XMLStreamException + { + m_xsw.writeStartElement(namespaceURI, localName); + } + + @Override + public void writeStartElement( + String prefix, String localName, String namespaceURI) + throws XMLStreamException + { + m_xsw.writeStartElement(prefix, namespaceURI, localName); + } + + @Override + public void writeEmptyElement(String namespaceURI, String localName) + throws XMLStreamException + { + m_xsw.writeEmptyElement(namespaceURI, localName); + } + + @Override + public void writeEmptyElement( + String prefix, String localName, String namespaceURI) + throws XMLStreamException + { + m_xsw.writeEmptyElement(prefix, namespaceURI, localName); + } + + @Override + public void writeEmptyElement(String localName) + throws XMLStreamException { - m_xew.close(); + m_xsw.writeEmptyElement(localName); + } + + @Override + public void writeEndElement() throws XMLStreamException + { + m_xsw.writeEndElement(); } /** - * Version of {@code add} that also closes the underlying stream after - * handling an {@code endDocument} event. + * Version of {@code writeEndDocument} that also closes the underlying + * stream. *

    * Note it does not call this class's own close; a * calling transformer may emit a warning if that is done. */ @Override - public void add(XMLEvent event) throws XMLStreamException + public void writeEndDocument() throws XMLStreamException { - m_xew.add(event); - if ( ! event.isEndDocument() ) - return; + m_xsw.writeEndDocument(); + try { m_os.close(); } - catch ( Exception ioe ) + catch ( Exception ioe ) { throw new XMLStreamException( "Failure closing SQLXML StAXResult", ioe); } } - /** - * Include a whole content fragment by supplying another reader. - * That is passed directly to the underlying class, so the - * {@code endDocument} event ending that content will not close - * the stream. - */ @Override - public void add(XMLEventReader reader) throws XMLStreamException + public void close() throws XMLStreamException + { + m_xsw.close(); + } + + @Override + public void flush() throws XMLStreamException + { + m_xsw.flush(); + } + + @Override + public void writeAttribute(String localName, String value) + throws XMLStreamException { - m_xew.add(reader); + m_xsw.writeAttribute(localName, value); + } + + @Override + public void writeAttribute( + String prefix, String namespaceURI, String localName, String value) + throws XMLStreamException + { + m_xsw.writeAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeAttribute( + String namespaceURI, String localName, String value) + throws XMLStreamException + { + m_xsw.writeAttribute(namespaceURI, localName, value); + } + + @Override + public void writeNamespace(String prefix, String namespaceURI) + throws XMLStreamException + { + m_xsw.writeNamespace(prefix, namespaceURI); + } + + @Override + public void writeDefaultNamespace(String namespaceURI) + throws XMLStreamException + { + m_xsw.writeDefaultNamespace(namespaceURI); + } + + @Override + public void writeComment(String data) throws XMLStreamException + { + m_xsw.writeComment(data); + } + + @Override + public void writeProcessingInstruction(String target) + throws XMLStreamException + { + m_xsw.writeProcessingInstruction(target); + } + + @Override + public void writeProcessingInstruction(String target, String data) + throws XMLStreamException + { + m_xsw.writeProcessingInstruction(target, data); + } + + @Override + public void writeCData(String data) throws XMLStreamException + { + m_xsw.writeCData(data); + } + + @Override + public void writeDTD(String dtd) throws XMLStreamException + { + m_xsw.writeDTD(dtd); + } + + @Override + public void writeEntityRef(String name) throws XMLStreamException + { + m_xsw.writeEntityRef(name); + } + + @Override + public void writeStartDocument() throws XMLStreamException + { + m_xsw.writeStartDocument(); + } + + @Override + public void writeStartDocument(String version) throws XMLStreamException + { + m_xsw.writeStartDocument(version); + } + + @Override + public void writeStartDocument(String encoding, String version) + throws XMLStreamException + { + m_xsw.writeStartDocument(encoding, version); + } + + @Override + public void writeCharacters(String text) throws XMLStreamException + { + m_xsw.writeCharacters(text); + } + + @Override + public void writeCharacters(char[] text, int start, int len) + throws XMLStreamException + { + m_xsw.writeCharacters(text, start, len); } @Override public String getPrefix(String uri) throws XMLStreamException { - return m_xew.getPrefix(uri); + return m_xsw.getPrefix(uri); } @Override public void setPrefix(String prefix, String uri) - throws XMLStreamException + throws XMLStreamException { - m_xew.setPrefix(prefix, uri); + m_xsw.setPrefix(prefix, uri); } @Override - public void setDefaultNamespace(String uri) - throws XMLStreamException + public void setDefaultNamespace(String uri) throws XMLStreamException { - m_xew.setDefaultNamespace(uri); + m_xsw.setDefaultNamespace(uri); } @Override public void setNamespaceContext(NamespaceContext context) - throws XMLStreamException + throws XMLStreamException { - m_xew.setNamespaceContext(context); + m_xsw.setNamespaceContext(context); } @Override public NamespaceContext getNamespaceContext() { - return m_xew.getNamespaceContext(); + return m_xsw.getNamespaceContext(); + } + + @Override + public Object getProperty(String name) throws IllegalArgumentException + { + return m_xsw.getProperty(name); } } From 72cbb15cd878264bce4c1829ec33bace357b0800 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 26 Aug 2018 00:08:45 -0400 Subject: [PATCH 0173/1087] Let timestamp values roundtrip even when infinite. The Java classes representing them do not have a notion of 'infinity' or '-infinity' as PostgreSQL does. For now, just produce instances of the desired classes with the naturally converted values, which will appear as particular, crazy late or crazy early, finite timestamps, and just make sure those specific values convert back in the other direction to PostgreSQL 'infinity' and '-infinity'. Java code could use a query to generate one of each object and save them, to recognize or use infinite values later in real data. The above works as advertised with integer_datetimes (which has been the default for many PG versions, and is now not even selectable). With the older, floating-point representation, it doesn't, and given the combination of obsolescence of the float representation and rarity of infinite values in practice, is arguably not worth much effort to improve. Add some tests with infinite values to the JDBC42_21 example, included only when integer_datetimes is on. In passing, update copyright in some files that were touched earlier without doing that. --- .../pljava/example/annotation/JDBC42_21.java | 34 ++++++--- pljava-so/src/main/c/type/Date.c | 14 ++-- pljava-so/src/main/c/type/Time.c | 14 ++-- pljava-so/src/main/c/type/Timestamp.c | 70 +++++++++++++++---- .../src/main/include/pljava/type/Timestamp.h | 25 ++++--- 5 files changed, 117 insertions(+), 40 deletions(-) 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 c0e374a1..e501a10f 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 @@ -78,10 +78,19 @@ " ELSE javatest.logmessage('WARNING','java.time.LocalDateTime fails')"+ " END" + " FROM" + - " (VALUES" + - " (timestamp '2017-08-21 18:25:29.900005')," + - " (timestamp '1970-03-07 17:37:49.300009')," + - " (timestamp '1919-05-29 13:08:33.600001')" + + " (SELECT 'on' = current_setting('integer_datetimes')) AS ck(idt)," + + " LATERAL (" + + " SELECT" + + " value" + + " FROM" + + " (VALUES" + + " (true, timestamp '2017-08-21 18:25:29.900005')," + + " (true, timestamp '1970-03-07 17:37:49.300009')," + + " (true, timestamp '1919-05-29 13:08:33.600001')," + + " (idt, timestamp 'infinity')," + + " (idt, timestamp '-infinity')" + + " ) AS vs(cond, value)" + + " WHERE cond" + " ) AS p(orig)," + " javatest.roundtrip(p, 'java.time.LocalDateTime')" + " AS r(roundtripped timestamp)", @@ -93,10 +102,19 @@ " 'WARNING','java.time.OffsetDateTime fails')"+ " END" + " FROM" + - " (VALUES" + - " (timestamptz '2017-08-21 18:25:29.900005Z')," + - " (timestamptz '1970-03-07 17:37:49.300009Z')," + - " (timestamptz '1919-05-29 13:08:33.600001Z')" + + " (SELECT 'on' = current_setting('integer_datetimes')) AS ck(idt)," + + " LATERAL (" + + " SELECT" + + " value" + + " FROM" + + " (VALUES" + + " (true, timestamptz '2017-08-21 18:25:29.900005Z')," + + " (true, timestamptz '1970-03-07 17:37:49.300009Z')," + + " (true, timestamptz '1919-05-29 13:08:33.600001Z')," + + " (idt, timestamptz 'infinity')," + + " (idt, timestamptz '-infinity')" + + " ) AS vs(cond, value)" + + " WHERE cond" + " ) AS p(orig)," + " javatest.roundtrip(p, 'java.time.OffsetDateTime')" + " AS r(roundtripped timestamptz)", diff --git a/pljava-so/src/main/c/type/Date.c b/pljava-so/src/main/c/type/Date.c index fca42870..b23eeaf7 100644 --- a/pljava-so/src/main/c/type/Date.c +++ b/pljava-so/src/main/c/type/Date.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ diff --git a/pljava-so/src/main/c/type/Time.c b/pljava-so/src/main/c/type/Time.c index a308533e..b1b73568 100644 --- a/pljava-so/src/main/c/type/Time.c +++ b/pljava-so/src/main/c/type/Time.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index eb63f14a..7aa57593 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -89,6 +89,10 @@ static jobject s_ZoneOffset_UTC; static Type _LocalDateTime_obtain(Oid); static Type _OffsetDateTime_obtain(Oid); +#if PG_VERSION_NUM < 100000 +static int32 Timestamp_getTimeZone_dd(double dt); +#endif + /* * This only answers true for (same class or) TIMESTAMPOID. * The obtainer (below) only needs to construct and remember one instance. @@ -118,10 +122,14 @@ static jvalue _LocalDateTime_coerceDatum(Type self, Datum arg) int64 micros = DatumGetInt64(arg); /* Expect number of microseconds since 01 Jan 2000. Tease out a * non-negative sub-second microseconds value (whether this C compiler's - * signed % has trunc or floor behavior). + * signed % has trunc or floor behavior). Factor a 2 out right away to + * avoid wraparound when flooring near the most negative values. */ - onlyMicros = (jint)(((micros % 1000000) + 1000000) % 1000000); - secs = (micros - onlyMicros) / 1000000; + int lowBit = (int)(micros & 1); + micros = (micros ^ lowBit) / 2; + onlyMicros = (jint)(((micros % 500000) + 500000) % 500000); + secs = (micros - onlyMicros) / 500000; + onlyMicros = (onlyMicros << 1) | lowBit; } result.l = JNI_callStaticObjectMethod(s_LocalDateTime_class, s_LocalDateTime_ofEpochSecond, @@ -242,18 +250,24 @@ static bool _Timestamp_canReplaceType(Type self, Type other) static jvalue Timestamp_coerceDatumTZ_id(Type self, Datum arg, bool tzAdjust) { jvalue result; + jint uSecs; + jlong mSecs; int64 ts = DatumGetInt64(arg); /* Expect number of microseconds since 01 Jan 2000. Tease out a non-negative * sub-second microseconds value (whether this C compiler's signed % - * has trunc or floor behavior). + * has trunc or floor behavior). Factor a 2 out right away to + * avoid wraparound when flooring near the most negative values. */ - jint uSecs = (jint)(((ts % 1000000) + 1000000) % 1000000); - jlong mSecs = (ts - uSecs) / 1000; /* Convert to millisecs */ + int lowBit = (int)(ts & 1); + ts = (ts ^ lowBit) / 2; + uSecs = (jint)(((ts % 500000) + 500000) % 500000); + mSecs = (ts - uSecs) / 500; /* Convert to millisecs */ + uSecs = (uSecs << 1) | lowBit; if(tzAdjust) { - int tz = Timestamp_getTimeZone_id(ts); + int tz = Timestamp_getTimeZone_id(ts); /* function expects halved ts */ mSecs += tz * 1000; /* Adjust from local time to UTC */ } @@ -281,7 +295,7 @@ static jvalue Timestamp_coerceDatumTZ_dd(Type self, Datum arg, bool tzAdjust) ts += tz; /* Adjust from local time to UTC */ ts += EPOCH_DIFF; /* Adjust for diff between Postgres and Java (Unix) */ secs = (jlong) floor(ts); /* Take just the secs */ - uSecs = (((jint) ((ts - secs) * 2e6)) + 1) / 2; /* Preserve microsecs */ + uSecs = (((jint) ((ts - (double)secs) * 2e6)) + 1) / 2; /* Preserve usecs */ result.l = JNI_newObject(s_Timestamp_class, s_Timestamp_init, secs * 1000); if(uSecs != 0) JNI_callVoidMethod(result.l, s_Timestamp_setNanos, uSecs * 1000); @@ -301,6 +315,7 @@ static jvalue Timestamp_coerceDatumTZ(Type self, Datum arg, bool tzAdjust) static Datum Timestamp_coerceObjectTZ_id(Type self, jobject jts, bool tzAdjust) { int64 ts; + int lowBit; jlong mSecs = JNI_callLongMethod(jts, s_Timestamp_getTime); jint nSecs = JNI_callIntMethod(jts, s_Timestamp_getNanos); /* @@ -311,11 +326,13 @@ static Datum Timestamp_coerceObjectTZ_id(Type self, jobject jts, bool tzAdjust) */ mSecs -= ((mSecs % 1000) + 1000) % 1000; mSecs -= ((jlong)EPOCH_DIFF) * 1000L; - ts = mSecs * 1000L; /* Convert millisecs to microsecs */ - if(nSecs != 0) - ts += nSecs / 1000; /* Convert nanosecs to microsecs */ - if(tzAdjust) - ts -= ((jlong)Timestamp_getTimeZone_id(ts)) * 1000000L; /* Adjust from UTC to local time */ + ts = mSecs * 500L; /* millisecs to microsecs, save a factor of 2 for now */ + if(tzAdjust) /* Adjust from UTC to local time; function expects halved ts */ + ts -= ((jlong)Timestamp_getTimeZone_id(ts)) * 500000L; + nSecs /= 1000; /* ok, now they are really microsecs */ + lowBit = nSecs & 1; + nSecs >>= 1; /* nSecs >= 0 so >> has a defined C result */ + ts = 2 * (ts + nSecs) | lowBit; return Int64GetDatum(ts); } @@ -381,6 +398,10 @@ static Datum _Timestamptz_coerceObject(Type self, jobject ts) return Timestamp_coerceObjectTZ(self, ts, false); } +/* + * The argument to this function is in seconds from the PostgreSQL epoch, and + * the return is a time zone offset in seconds west of Greenwich. + */ static int32 Timestamp_getTimeZone(pg_time_t time) { #if defined(_MSC_VER) && ( \ @@ -400,17 +421,36 @@ static int32 Timestamp_getTimeZone(pg_time_t time) #else struct pg_tm* tx = pg_localtime(&time, session_timezone); #endif + if ( NULL == tx ) + ereport(ERROR, ( + errcode(ERRCODE_DATA_EXCEPTION), + errmsg("could not resolve timestamp: %m") + )); return -(int32)tx->tm_gmtoff; } +/* + * This is only used here and in Date.c. The caller must know that the argument + * is not a PostgreSQL int64 Timestamp, but, rather, one of those divided by 2. + */ int32 Timestamp_getTimeZone_id(int64 dt) { return Timestamp_getTimeZone( - (dt / INT64CONST(1000000) + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400)); + dt / INT64CONST(500000) + + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400 + ); } #if PG_VERSION_NUM < 100000 -int32 Timestamp_getTimeZone_dd(double dt) +static int32 Timestamp_getTimeZone_dd(double dt) { + if ( TIMESTAMP_NOT_FINITE(dt) ) + { + errno = EOVERFLOW; + ereport(ERROR, ( + errcode(ERRCODE_DATA_EXCEPTION), + errmsg("could not resolve timestamp: %m") + )); + } return Timestamp_getTimeZone( (pg_time_t)rint(dt + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400)); } diff --git a/pljava-so/src/main/include/pljava/type/Timestamp.h b/pljava-so/src/main/include/pljava/type/Timestamp.h index 19a66e12..c3ff9e80 100644 --- a/pljava-so/src/main/include/pljava/type/Timestamp.h +++ b/pljava-so/src/main/include/pljava/type/Timestamp.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -27,13 +33,14 @@ extern "C" { extern int Timestamp_getCurrentTimeZone(void); /* - * Returns the timezone fo the given Timestamp. Comes in two variants. - * The int64 variant will be used when PL/Java is used with a backend - * compiled with integer datetimes. The double variant will be used when - * this is not the case. + * Returns the timezone for the given Timestamp. This is an internal function + * and only declared here because Date.c uses it, and always this int64 variant, + * regardless of whether the backend was compiled with integer datetimes. The + * argument is not a PostgreSQL int64 Timestamp, but rather a PostgreSQL int64 + * Timestamp divided by two. The result is a time zone offset in seconds west + * of Greenwich. */ extern int32 Timestamp_getTimeZone_id(int64 t); -extern int32 Timestamp_getTimeZone_dd(double t); #ifdef __cplusplus } From 021f81bcd765e3c12ff6bd2f59d0b3782cbccd10 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 26 Aug 2018 21:55:25 -0400 Subject: [PATCH 0174/1087] Make installing the unsandboxed example opt-in. The prepareXMLTransform example function relies on Java's TransformerFactory.newTemplates() method, and that seems to fail unless unsandboxed, at least in newer JREs where the "XSLTC" transformer compiler is used. To avoid having the examples jar install an unsandboxed function unasked, require an extra implementor tag to be set in pljava.implementors if the function is wanted. --- .../org/postgresql/pljava/example/annotation/PassXML.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 a2d88186..649b96aa 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 @@ -228,9 +228,14 @@ public static SQLXML castTextXML(@SQLType("text") SQLXML sx) * {@link TransformerFactory#newTemplates newTemplates()} seems to require * {@link Function.Trust#UNSANDBOXED Trust.UNSANDBOXED}, at least for the * XSLTC transform compiler in newer JREs. + *

    + * If you wish this unsandboxed function to be installed, + * set the PostgreSQL variable {@code pljava.implementors} to a list with + * {@code pg_xml_unsandboxed} as an added entry, before installing the + * examples jar. */ @Function(schema="javatest", trust=Function.Trust.UNSANDBOXED, - implementor="postgresql_xml") + implementor="pg_xml_unsandboxed") public static void prepareXMLTransform(String name, SQLXML source, int how) throws SQLException { From 561a34987c3a8d2be8e548536ffc276501a2cb92 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 28 Aug 2018 12:01:37 -0400 Subject: [PATCH 0175/1087] A first go at release notes. Some of the features mentioned here now need actual documentation added to other pages, and then another pass here should add links. --- src/site/markdown/releasenotes.md.vm | 201 ++++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 4 deletions(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 4164ec58..3519a5a1 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -8,12 +8,34 @@ #set($pgfbug = 'http://pgfoundry.org/tracker/?func=detail&atid=334&group_id=1000038&aid=') #set($pgffeat = 'http://pgfoundry.org/tracker/?func=detail&atid=337&group_id=1000038&aid=') #set($ghbug = 'https://github.com/tada/pljava/issues/') +#set($ghpull = 'https://github.com/tada/pljava/pull/') $h2 PL/Java 1.5.1 -This release chiefly adds support for PostgreSQL 9.6 and 10, -and plays more nicely with `pg_upgrade`. With PostgreSQL 9.6 support -comes the ability to declare functions +This release adds support for PostgreSQL 9.6, 10, and 11, +and plays more nicely with `pg_upgrade`. If a PostgreSQL installation +is to be upgraded using `pg_upgrade`, and is running a version of +PL/Java before 1.5.1, the PL/Java version should first be upgraded +in the running PostgreSQL version, and then the PostgreSQL `pg_upgrade` +can be done. + +Significant new functionality includes new datatype mapping support: +SQL `date`, `time`, and `timestamp` values can be mapped to the new +Java classes of the `java.time` package in Java 8 and later (JSR 310 / +JDBC 4.2), which are much more faithful representations of the values +in SQL. Values of `xml` type can be manipulated efficiently using the +JDBC 4.0 `SQLXML` API, supporting several different APIs for XML +processing in Java. + +For Java code that does not use the new date/time classes in the +`java.time` package, some minor conversion inaccuracies (less than +two seconds) in the older mapping to `java.sql.Timestamp` have been +corrected. + +Queries from PL/Java code now produce `ResultSet`s that are usable to the +end of the containing transaction, as they had already been claiming to be. + +With PostgreSQL 9.6 support comes the ability to declare functions `PARALLEL { UNSAFE | RESTRICTED | SAFE }`, and with PG 10 support, transition tables are available to triggers. @@ -32,8 +54,162 @@ Developers wishing to manipulate large objects in PL/Java are able to do so using the SPI JDBC interface and the large-object SQL functions already available in every PostgreSQL version PL/Java currently supports. +$h3 Version compatibility + +PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11beta3, +and older ones back to 8.2, using Java SE 8, 7, or 6. It can _run_ using newer +Java versions including Java 10 (and is expected to work with Java 11 when that +is released). PL/Java functions can be written for, and use features of, the +Java version loaded at run time. See [version compatibility][versions] for more +detail. + +OpenJDK is supported, and can be downloaded in versions using the Hotspot or the +OpenJ9 JVM. Features of modern Java VMs can drastically reduce memory footprint +and startup time, in particular class-data sharing. Several choices of Java +runtime now offer such features: Oracle Java has a simple class data sharing +feature for the JVM itself, freely usable in all supported versions, and an +"application class data sharing" feature in Java 8 and later that can also share +the internal classes of PL/Java, but is a commercial feature requiring a +license from Oracle. As of Java 10, the same application class sharing feature +is present in OpenJDK/Hotspot, where it is freely usable without an additional +license. OpenJDK/OpenJ9 includes a different, and very sophisticated, class +sharing feature, freely usable from Java 8 onward. More on these features +can be found [in the installation docs][vmopts]. + $h3 Changes +$h4 Typing of parameters in prepared statements + +PL/Java currently does not determine the necessary types of `PreparedStatement` +parameters from the results of PostgreSQL's own type analysis of the query +(as a network client would, when using PostgreSQL's "extended query" protocol). +PostgreSQL added the means to do so in SPI only in PostgreSQL 9.0, and a future +PL/Java major release should use it. However, this release does make two small +changes to the current behavior. + +Without the query analysis results from PostgreSQL, PL/Java tries to type the +prepared-statement parameters based on the types of values supplied by the +application Java code. It now has two additional ways to do so: + +* If Java code supplies a Java user-defined type (UDT)--that is, an object + implementing the `SQLData` interface--PL/Java will now call the `SQLData` + method `getSQLTypeName` on that object and use the result to pin down + the PostgreSQL type of the parameter. Existing code should already provide + this method, but could, in the past, have returned a bogus result without + detection, as PL/Java did not use it. + +* Java code can use the three-argument form of `setNull` to specify the exact + PostgreSQL type for a parameter, and then another method can be used to + supply a non-null value for it. If the following non-null value has + a default mapping to a different PostgreSQL type, in most cases it will + overwrite the type supplied with `setNull` and re-plan the query. That was + PL/Java's existing behavior, and was not changed for this minor release. + However, the new types introduced in this release--the `java.time` types and + `SQLXML`--behave in the way that should become universal in a future major + release: the already-supplied PostgreSQL type will be respected, and PL/Java + will try to find a usable coercion to it. + +$h4 Inaccuracies converting TIMESTAMP and TIMESTAMPTZ + +When converting between PostgreSQL values of `timestamp` or `timestamptz` type +and the pre-Java 8 original JDBC type `java.sql.Timestamp`, there were cases +where values earlier than 1 January 2000 would produce exceptions rather than +converting successfully. Those have been fixed. + +Also, converting in the other direction, from `java.sql.Timestamp` to a +PostgreSQL timestamp, an error of up to 1.998 seconds (averaging 0.999) +could be introduced. + +That error has been corrected. If an application has stored Java `Timestamp`s +and corresponding SQL `timestamp`s generated in the past and requires them +to match, it could be affected by this change. + +$h4 New date/time/timestamp API in Java 8 `java.time` package + +The old, and still default, mappings in JDBC from the SQL `date`, `time`, and +`timestamp` types to `java.sql.Date`, `java.sql.Time`, and `java.sql.Timestamp`, +were never well suited to represent the PostgreSQL data types. The `Time` and +`Timestamp` classes were used to map both the with-timezone and without-timezone +variants of the corresponding SQL types and, clearly, could not represent both +equally well. These Java classes all contain timezone dependencies, requiring +the conversion to involve timezone calculations even when converting non-zoned +SQL types, and making the conversion results for non-zoned types implicitly +depend on the current PostgreSQL session timezone setting. + +Applications are strongly encouraged to adopt Java 8 as a minimum language +version and use the new-in-Java-8 types in the `java.time` package, which +eliminate those problems and map the SQL types much more faithfully. +For PL/Java function parameters and returns, the class in the method declaration +can simply be changed. For retrieving date/time/timestamp values from a +`ResultSet` or `SQLInput` object, use the variants of `getObject` / `readObject` +that take a `Class` parameter. The class to use is: + +| PostgreSQL type | `java.time` class | +|--:|:--| +|`date`|`LocalDate`| +|`time without time zone`|`LocalTime`| +|`time with time zone`|`OffsetTime`| +|`timestamp without time zone`|`LocalDateTime`| +|`timestamp with time zone`|`OffsetDateTime`| +[Correspondence of PostgreSQL date/time types and Java 8 `java.time` classes] + +An `OffsetTime` obtained from a PostgreSQL `time with time zone` will have +the time zone offset that was assigned to the PostgreSQL value. +An `OffsetDateTime` obtained from a PostgreSQL `timestamp with time zone` +will always have an offset explicitly zero (UTC), and any `OffsetDateTime` +created in Java with a different offset will be converted to UTC when presented +to PostgreSQL. These behaviors accurately reflect the different ways PostgreSQL +represents these two types. These conversions never depend on the PostgreSQL +session time zone, which is not necessarily consistent between sessions. + +$h4 Newly supported `java.sql.SQLXML` type + +PL/Java has not, until now, supported the JDBC 4.0 `SQLXML` type. PL/Java +functions have been able to work with PostgreSQL XML values by mapping them +as Java `String`, but that conversion could introduce character encoding issues +outside the control of the XML APIs, and also has memory implications if an +application stores, or generates in queries, large XML values. Even if the +processing to be done in the application could be structured to run in constant +bounded memory while streaming through the XML, a conversion to `String` +requires the whole, uncompressed, character-serialized value to be brought into +the Java heap at once, and any heap-size tuning has to account for that +worst-case size. The `java.sql.SQLXML` API solves those problems by allowing +XML manipulation with any of several Java XML APIs with the data remaining in +PostgreSQL native memory, never brought fully into the Java heap unless that is +what the application does. Heap sizing can be based on the just the +application's processing needs. + +The `SQLXML` type can take the place of `String` in PL/Java function parameters +and returns simply by changing their declarations from `String` to `SQLXML`. +When retrieving XML values from `ResultSet` or `SQLInput` objects, the legacy +`getObject / readObject` methods will continue to return `String` for existing +application compatibility, so the specific `getSQLXML / readSQLXML` methods, or +the forms of `getObject / readObject` with a `Class` parameter and passing +`SQLXML.class`, must be used. The [PassXML example][exxml] illustrates use of +the API. + +[exxml]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java + +$h4 New Java property exposes the PostgreSQL server character-set encoding + +A Java system property, `org.postgresql.server.encoding`, is set to the +canonical name of a supported Java `Charset` that corresponds to PostgreSQL's +`server_encoding` setting, if one can be found. If the server encoding's name +is not recognized as any known Java `Charset`, this property will be unset, and +some functionality, such as the `SQLXML` API, may be limited. If a Java +`Charset` does exist (or is made available through a `CharsetProvider`) that +does match the PostgreSQL server encoding, but is not automatically selected +because of a naming mismatch, the `org.postgresql.server.encoding` property can +be set (with a `-D` in `pljava.vmoptions`) to select it by name. + +$h4 ResultSet holdability + +A `ResultSet` obtained from a query done in PL/Java would return the value +`CLOSE_CURSORS_AT_COMMIT` to a caller of its `getHoldability` method, but in +reality would become unusable as soon as the PL/Java function creating it +returned to PostgreSQL. It now remains usable to the end of the transaction, +as claimed. + $h4 PostgreSQL 9.6 and parallel query A function in PL/Java can now be [annotated][apianno] @@ -140,14 +316,31 @@ and a number of considerations for making a PL/Java package. [packaging]: build/package.html +$h3 Enhancement requests addressed + +$h4 Since 1.5.1-BETA1 + +* [java 8 date/time api](${ghbug}137) +* [Annotations don't support CREATE CONSTRAINT TRIGGER](${ghbug}138) +* [Let annotations give defaults to row-type parameters](${ghpull}153) +* [Improve DDR generator on the dont-repeat-yourself dimension for UDT type mapping](${ghpull}159) +* [Support the JDBC 4.0 SQLXML type](${ghpull}171) + $h3 Bugs fixed $h4 Since 1.5.1-BETA1 * [PostgreSQL 10: SPI_modifytuple failed with SPI_ERROR_UNCONNECTED](${ghbug}134) * [SPIConnection prepareStatement doesn't recognize all parameters](${ghbug}136) -* [Annotations don't support CREATE CONSTRAINT TRIGGER](${ghbug}138) * [Ordinary (non-constraint) trigger has no way to suppress operation](${ghbug}142) +* [ResultSetHandle and column definition lists](${ghbug}146) +* [PreparedStatement doesn't get parameter types from PostgreSQL](${ghbug}149) + _(partial improvements)_ +* [internal JDBC: inaccuracies converting TIMESTAMP and TIMESTAMPTZ](${ghbug}155) +* [Missing type mapping for Java return `byte[]`](${ghbug}157) +* [The REMOVE section of DDR is in wrong order for conditionals](${ghbug}163) +* [Loading PL/Java reinitializes timeouts in PostgreSQL >= 9.3](${ghbug}166) +* [JDBC ResultSet.CLOSE_CURSORS_AT_COMMIT reported, but usable life shorter](${ghbug}168) $h4 In 1.5.1-BETA1 From 5e1a27c9172d5f5f8a19427ef546af55bc46a256 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 28 Aug 2018 23:18:00 -0400 Subject: [PATCH 0176/1087] General updates in existing docs. In passing, fix a typo in some example javadoc, and use proper Markdown for em dash. Markdown seems to be TeXlike, using -- for en and --- for em. Reportedly, GitHub Flavored Markdown may mess up ---, but the processor used by Maven/Velocity seems to make it a proper em dash. --- .../pljava/example/annotation/PassXML.java | 2 +- src/site/markdown/build/build.md | 15 ++- src/site/markdown/build/package.md | 6 ++ src/site/markdown/build/versions.md | 25 ++++- src/site/markdown/develop/coercion.md | 2 +- src/site/markdown/install/appcds.md | 39 ++++--- src/site/markdown/install/install.md.vm | 10 +- src/site/markdown/install/oj9vmopt.md | 100 ++++++++++++++++++ src/site/markdown/install/selinux.md | 2 +- src/site/markdown/install/vmoptions.md | 49 +++++++-- src/site/markdown/use/use.md | 15 +++ 11 files changed, 236 insertions(+), 29 deletions(-) create mode 100644 src/site/markdown/install/oj9vmopt.md 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 649b96aa..b96f5cd3 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 @@ -157,7 +157,7 @@ public static SQLXML echoXMLParameter(SQLXML sx, int howin, int howout) *

    * The other version of this method needs a conditional implementor tag * because it cannot be declared in a PostgreSQL instance that was built - * without [@code libxml} support and the PostgreSQL {@code XML} type. + * without {@code libxml} support and the PostgreSQL {@code XML} type. * But this version can, simply by mapping the {@code SQLXML} parameter * and return types to the SQL {@code text} type. The Java code is no * different. diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index 36817563..d8936ec0 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -9,7 +9,9 @@ and produce the files you need, but *not* install them into PostgreSQL. To do that, continue with the [installation instructions][inst]. [mvn]: https://maven.apache.org/ -[java]: http://www.oracle.com/technetwork/java/javase/downloads/index.html +[orjava]: http://www.oracle.com/technetwork/java/javase/downloads/index.html +[OpenJDK]: https://adoptopenjdk.net/ +[hsj9]: https://www.eclipse.org/openj9/oj9_faq.html **In case of build difficulties:** @@ -25,13 +27,20 @@ There is a "troubleshooting the build" section at the end of this page. at the command line, which should tell you the version you have installed. -0. The [Java Development Kit][java] (not just the Java Runtime Environment) +0. The Java Development Kit (not just the Java Runtime Environment) version that you plan to use should be installed, also ideally in your search path so that javac -version - just works. + just works. [Oracle Java][orjava] or [OpenJDK][] can be used, the latter + with [either the Hotspot or the OpenJ9 JVM][hsj9]. It is not necessary to + use the same JDK to build PL/Java that will later be used to run it in the + database, as long as the one used for building is not newer than that used + at run time. In particular, because the build procedure has not been updated + for Java 9 and later, PL/Java requires a Java 8, 7, or 6 JDK to build, but + can then use a later Java version at run time, and support PL/Java + applications using the newer Java features. 0. The PostgreSQL server version that you intend to use should be installed, and on your search path so that the command diff --git a/src/site/markdown/build/package.md b/src/site/markdown/build/package.md index 342d98b2..7f5834a2 100644 --- a/src/site/markdown/build/package.md +++ b/src/site/markdown/build/package.md @@ -23,6 +23,12 @@ the exact file this should refer to. [locatejvm]: ../install/locatejvm.html +Although PL/Java is currently built with a JDK no later than Java 8, it can +then run in the database with a newer JVM, and support application code that +uses the newer Java features. While using Java 8 to build a package, you are +encouraged to set the default `pljava.libjvm_location` to the library of a +later JRE that is expected to be present on your platform. + ## What kind of a package is this? Your package may be for a distribution that has formal guidelines for how diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index 0bf0b591..408ad1b5 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -1,6 +1,6 @@ # Versions of external packages needed to build and use PL/Java -As of November 2015, the following version constraints are known. +As of August 2018, the following version constraints are known. ## Java @@ -11,6 +11,25 @@ As for later versions of Java, backward compatibility in the language is generally good. The most likely problem areas with a new Java version will be additions to the JDBC API that PL/Java has not yet implemented. +In the PL/Java 1.5.x series, the build system has not been reworked for +building with Java 9 or newer. However, PL/Java can be built with Java 8 +and use a newer JVM at run time, simply by setting +[the `pljava.libjvm_location` variable][jvml] to the newer version's library. + +That allows PL/Java to run application code written for the latest Java +versions, and also to take advantage of recent Java implementation advances +such as [class data sharing][cds]. + +PL/Java has been successfully used with [Oracle Java][orj] and with +[OpenJDK][], which is available with +[either the Hotspot or the OpenJ9 JVM][hsj9]. + +[jvml]: ../use/variables.html +[cds]: ../install/vmoptions.html#Class_data_sharing +[orj]: https://www.oracle.com/technetwork/java/javase/downloads/index.html +[OpenJDK]: https://adoptopenjdk.net/ +[hsj9]: https://www.eclipse.org/openj9/oj9_faq.html + ## Maven PL/Java can be built with Maven versions at least as far back as 3.0.4. @@ -40,3 +59,7 @@ the aim is that the current PL/Java should be a possible upgrade there.) More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. + +PL/Java 1.5.1 has been successfully built and run on at least one platform +with PostgreSQL versions from 11 (beta3) to 8.2, the latest maintenance +release for each. diff --git a/src/site/markdown/develop/coercion.md b/src/site/markdown/develop/coercion.md index 2674e260..f7c00e82 100644 --- a/src/site/markdown/develop/coercion.md +++ b/src/site/markdown/develop/coercion.md @@ -195,7 +195,7 @@ other thing PL/Java can do. which can then be associated with a Java class using [SQLJ.ADD_TYPE_MAPPING][atm]. From outside of Java code, it can be manipulated like any PostgreSQL composite type, while to Java code it - will be presented as an instance of the associated Java class--a new + will be presented as an instance of the associated Java class---a new instance at every conversion, however. Java code is provided `SQLInput` and `SQLOutput` implementations that retrieve and set the typed attributes of the composite. Created by a Java class diff --git a/src/site/markdown/install/appcds.md b/src/site/markdown/install/appcds.md index 7047caff..b401ccf2 100644 --- a/src/site/markdown/install/appcds.md +++ b/src/site/markdown/install/appcds.md @@ -1,21 +1,28 @@ -# How to set up application class data sharing - -[Application class data sharing][appcds] is a feature, currently unique -to the Oracle JVM (8u40 and later) that extends the ordinary Java class -data sharing feature to also include selected classes from the application -class path. In PL/Java terms, that means that not only Java's own internal -classes, but PL/Java's also, can be saved in a preprocessed shared archive -and quickly mapped when any backend starts PL/Java. For an overview, see +# How to set up application class data sharing in Hotspot + +For the Hotspot JVM, [Application class data sharing][appcds] is a feature, +first released in the Oracle JVM (8u40 and later) that extends the ordinary +Java class data sharing feature to also include selected classes from the +application class path. In PL/Java terms, that means that not only Java's own +internal classes, but PL/Java's also, can be saved in a preprocessed shared +archive and quickly mapped when any backend starts PL/Java. For an overview, see the [PL/Java VM options page][vmop]. +Starting with Java 10, the feature is also available in +[OpenJDK with Hotspot][OpenJDK]. From Java 8 onward, a different feature +with the same effect is available in [OpenJDK with OpenJ9][OpenJDK]; that +feature is covered [on its own page][cdsJ9]. + [appcds]: http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#app_class_data_sharing [vmop]: vmoptions.html [bcl]: http://www.oracle.com/technetwork/java/javase/terms/license/index.html +[OpenJDK]: https://adoptopenjdk.net/ +[cdsJ9]: oj9vmopt.html#How_to_set_up_class_sharing_in_OpenJ9 ## License considerations -For Java 8, application class data sharing is a "commercial feature" in -Oracle's JVM, and will not work unless `pljava.vmoptions` also contain +In Oracle Java, application class data sharing is a "commercial feature" first +released in Java 8, and will not work unless `pljava.vmoptions` also contain `-XX:+UnlockCommercialFeatures` , with implications described in the "supplemental license terms" of the Oracle [binary code license for Java SE][bcl]. The license seems @@ -25,11 +32,19 @@ negotiating an additional agreement with Oracle if the feature will be used purpose." It is available to consider for any application where the additional performance margin can be given a price. -Looking ahead, in the source for OpenJDK 9 (`share/vm/runtime/globals.hpp`) -are promising signs that equivalent functionality will be available there. +The same feature in OpenJDK with Hotspot is available from Java 10 onward, +and does not require any additional license or `-XX:+UnlockCommercialFeatures` +option. The equivalent feature in OpenJDK with OpenJ9, +[described separately][cdsJ9] is available from Java 8 onward, also with no +additional license or setup needed. ## Setup +The setup instructions on this page are for Hotspot, whether in Oracle Java +or OpenJDK with Hotspot. The two differ only in that, wherever an +`-XX:+UnlockCommercialFeatures` option is shown in the steps below, +**it is needed in Oracle Java but not in OpenJDK/Hotspot**. + Setting up PL/Java to use application class data sharing is a three-step process. Each step is done by setting a different combination of options in `pljava.vmoptions`. These are the three steps in overview: diff --git a/src/site/markdown/install/install.md.vm b/src/site/markdown/install/install.md.vm index aa3ba21b..79359b02 100644 --- a/src/site/markdown/install/install.md.vm +++ b/src/site/markdown/install/install.md.vm @@ -8,6 +8,14 @@ #set($h2 = '##') #set($h3 = '###') +$h2 Selecting a current Java version to use with PL/Java + +Having used a Java 8 or earlier JDK to build PL/Java, you will probably want to +use PL/Java with a newer Java version at run time, so your PL/Java application +code can use the latest Java features. When you reach the step +[setting PL/Java configuration variables](#PLJava_configuration_variables), +the `pljava.libjvm_location` variable will allow you to do that. + $h2 For the impatient After completing the [build][bld]: @@ -138,7 +146,7 @@ things right on the first try, you might set them after, too.) For example: pathname of the file that is called `pljava/sharedir/pljava/pljava-${project.version}.jar` in the installer jar. Note: this variable isn't meant to point to the code you develop and - use in PL/Java--that's what the [`sqlj.install_jar function`][sqjij] + use in PL/Java---that's what the [`sqlj.install_jar function`][sqjij] is for. [sqjij]: https://github.com/tada/pljava/wiki/SQL-functions diff --git a/src/site/markdown/install/oj9vmopt.md b/src/site/markdown/install/oj9vmopt.md new file mode 100644 index 00000000..a2301182 --- /dev/null +++ b/src/site/markdown/install/oj9vmopt.md @@ -0,0 +1,100 @@ +# PL/Java VM option recommendations for the OpenJ9 JVM + +The OpenJ9 JVM accepts a number of standard options that are the same as +those accepted by Hotspot, but also many nonstandardized ones that are not. +A complete list of options it accepts can be found [here][oj9opts]. + +There is one option likely to benefit _every_ PL/Java configuration: + +* [`-Xquickstart`][xqs] + +[xqs]: https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.vm.80.doc/docs/xquickstart.html + +Beyond that, and the usual opportunities to adjust memory allocations and +garbage-collector settings, anyone setting up PL/Java with OpenJ9 should +seriously consider setting up class sharing, which is much simpler in +OpenJ9 than in Hotspot, and is the subject of the rest of this page. + +## How to set up class sharing in OpenJ9 + +OpenJ9 is an [alternative to the Hotspot JVM][hsj9] that is available in +[OpenJDK][] (which can be downloaded with the choice of either JVM). + +OpenJ9 includes a _dynamically managed_ class data sharing feature +comparable to Hotspot's [application class data sharing][iads], but +much less fuss to set up. + +To see how much less, the Hotspot setup is a manual, three-step affair +to be done in advance of production use. You choose some code to run that you +hope will exercise all the classes you would like in the shared +archive and dump the loaded-class list, then generate the shared archive +from that list, and finally save the right option in `pljava.vmoptions` to have +the shared archive used at run time. + +By contrast, you set up OpenJ9 to share classes with the following step: + +1. Add an `-Xshareclasses` option to `pljava.vmoptions` to tell OpenJ9 to + share classes. + +OpenJ9 will then, if the first time, create a shared archive and dynamically +manage it, adding ahead-of-time-compiled versions of classes as they are +used in your application. The details are [described here][ej9cds]. + +[oj9opts]: https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.vm.80.doc/docs/x_jvm_commands.html +[ej9cds]: https://www.ibm.com/developerworks/library/j-class-sharing-openj9/ +[iads]: appcds.html +[vmop]: vmoptions.html +[OpenJDK]: https://adoptopenjdk.net/ +[hsj9]: https://www.eclipse.org/openj9/oj9_faq.html + +### Setup + +Arrange `pljava.vmoptions` to contain an option `-Xshareclasses`. + +The option can take various suboptions. Two interesting ones are: + + -Xshareclasses:name=/path/to/some/file + -Xshareclasses:cacheDir=/path/to/some/dir + +to control where PL/Java's shared class versions get cached. The first variant +specifies the exact file that will be memory mapped, while the second specifies +what directory will contain the (automatically named) file. + +Using either suboption (or both; suboptions are separated by commas), you can +arrange for PL/Java's shared classes to be cached separately from other uses +of Java on the same system. You could even, by saving different +`pljava.vmoptions` settings per database or per user, arrange separate class +caches for distinct applications using PL/Java. + +If you wish to emulate the Hotspot class sharing feature where a shared class +archive is created ahead of time and then frozen, you can let the application +run for a while with the `-Xshareclasses` option not containing `readonly`, +until the shared cache has been well warmed, and then add `readonly` to the +`-Xshareclasses` option saved in `pljava.vmoptions`. + +All of the suboptions accepted by `-Xshareclasses` are listed [here][xsc]. + +[xsc]: https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.vm.80.doc/docs/xshareclasses.html + +### Java libraries + +If your own PL/Java code depends on other Java libraries distributed as +jars, the usual recommendation would be to install those as well into the +database with `sqlj.install_jar`, and use `sqlj.set_classpath` to make them +available. That keeps everything handled uniformly within the database. + +On the other hand, if you are building a shared archive, and some of the +dependency libraries are substantial, you could consider instead storing +those jars on the file system and naming them in `pljava.classpath`. Those +library classes can then also be included in the shared archive. + +Not everything from the original jar file can go into the shared archive. +After the archive has been built, the original jars still must be on the +file system and named in `pljava.classpath`. + +When using class sharing, consider adding `-Xverify:all` to +the other VM options. Java sometimes applies more relaxed verification to +classes it loads from the system classpath. With class sharing in use, classes +may be loaded and verified early, then saved in the shared archive for quick +loading later. In those circumstances, the cost of requesting verification for +all classes may not be prohibitive. diff --git a/src/site/markdown/install/selinux.md b/src/site/markdown/install/selinux.md index f5bedc4a..81cd840f 100644 --- a/src/site/markdown/install/selinux.md +++ b/src/site/markdown/install/selinux.md @@ -1,7 +1,7 @@ # Using PL/Java with SELinux These notes were made running PostgreSQL and PL/Java on a Red Hat system, -but should be applicable--with possible changes to details--on other systems +but should be applicable---with possible changes to details---on other systems running SELinux. Anything that gets run by `postgres` itself runs under a special SELinux context diff --git a/src/site/markdown/install/vmoptions.md b/src/site/markdown/install/vmoptions.md index 129b15f9..6dc995dd 100644 --- a/src/site/markdown/install/vmoptions.md +++ b/src/site/markdown/install/vmoptions.md @@ -4,6 +4,9 @@ The PostgreSQL configuration variable `pljava.vmoptions` can be used to supply custom options to the Java VM when it is started. Several of these options are likely to be worth setting. +If using [the OpenJ9 JVM][hsj9], be sure to look also at the +[VM options specific to OpenJ9][vmoptJ9]. + ## Byte order for PL/Java-implemented user-defined types PL/Java is free of byte-order issues except when using its features for building @@ -19,9 +22,17 @@ the topic and an advance notice of an expected future migration step. Class data sharing is a commonly-supported Java virtual machine feature that reduces both VM startup time and memory footprint by having many of -Java's runtime classes preprocessed into a `classes.jsa` file that can +Java's runtime classes preprocessed into a file that can be quickly memory-mapped into the process at Java startup, and shared -if there are multiple processes running Java VMs. It is enabled by including +if there are multiple processes running Java VMs. + +How to set it up differs depending on the Java VM in use, Hotspot +(in [Oracle Java][orjava] or [OpenJDK with Hotspot][OpenJDK]), or OpenJ9 +(an [alternate JVM][hsj9] also available with [OpenJDK][]). The instructions on +this page are specific to Hotspot. For the corresponding feature in OpenJ9, +which is worth a good look, see the [class sharing in OpenJ9][cdsJ9] page. + +For Hotspot, the class data sharing is enabled by including -Xshare:on @@ -65,9 +76,10 @@ installed. It can be created with the simple command `java -Xshare:dump` ### Preloading PL/Java's classes as well as the Java runtime's -The basic class data sharing feature includes only Java's own runtime -classes in the shared archive. When using Java 8 from Oracle, 8u40 or -later, an expanded feature called [application class data sharing][appcds] +In Hotspot, the basic class data sharing feature includes only Java's own +runtime classes in the shared archive. When using Java 8 from Oracle, 8u40 or +later, or OpenJDK with Hotspot starting with Java 10, an expanded feature +called [application class data sharing][appcds] is available, with which you can build a shared archive that preloads PL/Java's classes as well as Java's own. In rough terms this doubles the improvement in startup time seen with basic class data sharing alone, @@ -75,10 +87,14 @@ for a total improvement (compared to no sharing) of 30 to 35 percent. It also allows the memory per backend to be even further scaled back, as discussed under "plausible settings" below. +([In OpenJ9][cdsJ9], the basic class sharing feature since Java 8 already +includes this ability, with no additional setup steps needed.) + #### Licensing considerations Basic class data sharing is widely available with no restriction, but -*application class data sharing* is a "commercial feature" exclusive to +*application class data sharing* [in Oracle Java][orjava] is a +"commercial feature" that first appeared in Oracle Java 8. It will not work unless `pljava.vmoptions` also contain `-XX:+UnlockCommercialFeatures` , with implications described in the "supplemental license terms" of the Oracle @@ -89,14 +105,29 @@ negotiating an additional agreement with Oracle if the feature will be used purpose." It is available to consider for any application where the additional performance margin can be given a price. -Looking ahead to Java 9, there are some promising signs that OpenJDK may have -an equivalent feature. +[In OpenJDK (with Hotspot)][OpenJDK], starting in Java 10, the same feature +is available and set up in the same way, but is freely usable; it does not +require any additional license, and does not require any +`-XX:+UnlockCommercialFeatures` to be added to the options. + +[In OpenJDK (with OpenJ9)][OpenJDK], the class-sharing feature present from +Java 8 onward will naturally share PL/Java's classes as well as the Java +runtime's, with no additional setup steps. + +Here are the instructions for +[setting up application class data sharing in Hotspot][iads]. -Here are the [instructions for setting up application class data sharing][iads]. +Here are instructions for [setting up class sharing (including application +classes!) in OpenJ9][cdsJ9]. +[orjava]: http://www.oracle.com/technetwork/java/javase/downloads/index.html +[OpenJDK]: https://adoptopenjdk.net/ +[hsj9]: https://www.eclipse.org/openj9/oj9_faq.html [appcds]: http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#app_class_data_sharing [bcl]: http://www.oracle.com/technetwork/java/javase/terms/license/index.html [iads]: appcds.html +[vmoptJ9]: oj9vmopt.html +[cdsJ9]: oj9vmopt.html#How_to_set_up_class_sharing_in_OpenJ9 ## `-XX:+DisableAttachMechanism` diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index b11a1424..68bbcd99 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -35,6 +35,21 @@ internal PL/Java classes that may change from release to release. ## Special topics +### Choices when mapping data types + +#### Date and time types + +PostgreSQL `date`, `time`, and `timestamp` types can still be matched to the +original JDBC `java.sql.Date`, `java.sql.Time`, and `java.sql.Timestamp`, +but application code is encouraged to move to Java 8 or later and use the +[new classes in the `java.time` package in Java 8](datetime.html) instead. + +#### XML type + +PL/Java can map PostgreSQL `xml` data to `java.lang.String`, but there are +significant advantages to using the +[JDBC 4.0 `java.sql.SQLXML` type](sqlxml.html) for processing XML. + ### Parallel query PostgreSQL 9.3 introduced [background worker processes][bgworker] From ad17cef3ba0a1b75f604416ad016dbcdda68b72f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 1 Sep 2018 22:13:54 -0400 Subject: [PATCH 0177/1087] Document JSR 310 date/time API, and SQLXML. --- src/site/markdown/releasenotes.md.vm | 27 +-- src/site/markdown/use/datetime.md | 118 ++++++++++ src/site/markdown/use/sqlxml.md | 307 +++++++++++++++++++++++++++ 3 files changed, 435 insertions(+), 17 deletions(-) create mode 100644 src/site/markdown/use/datetime.md create mode 100644 src/site/markdown/use/sqlxml.md diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 3519a5a1..5f9619e9 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -91,8 +91,8 @@ Without the query analysis results from PostgreSQL, PL/Java tries to type the prepared-statement parameters based on the types of values supplied by the application Java code. It now has two additional ways to do so: -* If Java code supplies a Java user-defined type (UDT)--that is, an object - implementing the `SQLData` interface--PL/Java will now call the `SQLData` +* If Java code supplies a Java user-defined type (UDT)---that is, an object + implementing the `SQLData` interface---PL/Java will now call the `SQLData` method `getSQLTypeName` on that object and use the result to pin down the PostgreSQL type of the parameter. Existing code should already provide this method, but could, in the past, have returned a bogus result without @@ -104,10 +104,10 @@ application Java code. It now has two additional ways to do so: a default mapping to a different PostgreSQL type, in most cases it will overwrite the type supplied with `setNull` and re-plan the query. That was PL/Java's existing behavior, and was not changed for this minor release. - However, the new types introduced in this release--the `java.time` types and - `SQLXML`--behave in the way that should become universal in a future major - release: the already-supplied PostgreSQL type will be respected, and PL/Java - will try to find a usable coercion to it. + However, the new types introduced in this release---the `java.time` types + and `SQLXML`---behave in the way that should become universal in a future + major release: the already-supplied PostgreSQL type will be respected, and + PL/Java will try to find a usable coercion to it. $h4 Inaccuracies converting TIMESTAMP and TIMESTAMPTZ @@ -153,14 +153,7 @@ that take a `Class` parameter. The class to use is: |`timestamp with time zone`|`OffsetDateTime`| [Correspondence of PostgreSQL date/time types and Java 8 `java.time` classes] -An `OffsetTime` obtained from a PostgreSQL `time with time zone` will have -the time zone offset that was assigned to the PostgreSQL value. -An `OffsetDateTime` obtained from a PostgreSQL `timestamp with time zone` -will always have an offset explicitly zero (UTC), and any `OffsetDateTime` -created in Java with a different offset will be converted to UTC when presented -to PostgreSQL. These behaviors accurately reflect the different ways PostgreSQL -represents these two types. These conversions never depend on the PostgreSQL -session time zone, which is not necessarily consistent between sessions. +Details on these mappings are [added to the documentation](use/datetime.html). $h4 Newly supported `java.sql.SQLXML` type @@ -185,10 +178,10 @@ When retrieving XML values from `ResultSet` or `SQLInput` objects, the legacy `getObject / readObject` methods will continue to return `String` for existing application compatibility, so the specific `getSQLXML / readSQLXML` methods, or the forms of `getObject / readObject` with a `Class` parameter and passing -`SQLXML.class`, must be used. The [PassXML example][exxml] illustrates use of -the API. +`SQLXML.class`, must be used. A [documentation page](use/sqlxml.html) has been +added, and the [PassXML example][exxml] illustrates use of the API. -[exxml]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java +[exxml]: pljava-examples/apidocs/index.html?org/postgresql/pljava/example/annotation/PassXML.html $h4 New Java property exposes the PostgreSQL server character-set encoding diff --git a/src/site/markdown/use/datetime.md b/src/site/markdown/use/datetime.md new file mode 100644 index 00000000..6a28bd2c --- /dev/null +++ b/src/site/markdown/use/datetime.md @@ -0,0 +1,118 @@ +# Mapping between PostgreSQL and Java date/time types + +## Legacy JDBC mappings + +The first mappings to be specified in JDBC used the JDBC-specific classes +`java.sql.Date`, `java.sql.Time`, and `java.sql.Timestamp`, all of which +are based on `java.util.Date` (but only as an implementation detail; they +should always be treated as their own types and not as instances of +`java.util.Date`). + +PL/Java function parameters and returns can be declared in Java to have those +types, objects of those types can be passed to `PreparedStatement.setObject`, +`ResultSet.updateObject`, and `SQLOutput.writeObject` methods, as well as to +the methods that are specific to those types. The JDBC `getObject` and +`readObject` methods that do not take a `Class` parameter will return +objects of those types when retrieving PostgreSQL date or time values. + +### Shortcomings + +Those classes have never been a good representation for PostgreSQL date/time +values, because they are based on `java.util.Date`, which implies knowledge of +a time zone, even when they are used to represent PostgreSQL values with no time +zone at all. For all of these conversions but one, PL/Java must do time zone +computations, with the one exception being, unintuitively, `timestamp with time +zone`. The conversions of non-zoned values involve a hidden dependency on the +PostgreSQL session's current setting of `TimeZone`, which can vary from session +to session at the connecting client's preference. + +## Java 8 / JDBC 4.2 date/time mappings + +Java 8 introduced the much improved set of date/time classes in the `java.time` +package specified by [JSR 310][jsr310]. JDBC 4.2 (the version in Java 8) +allows those as alternate Java class mappings of the SQL types `date`, +`time` (with and without timezone), and `timestamp` (with/without timezone). +These new types are a much better fit to the corresponding PostgreSQL types than +the original JDBC `java.sql` `Date`/`Time`/`Timestamp` classes. + +To avoid a breaking change, JDBC 4.2 does not modify what any of the +pre-existing JDBC API does by default. The `getDate`, `getTime`, and +`getTimestamp` methods on a `ResultSet` still return the same `java.sql` types, +and so does `getObject` in the form that does not specify a class. Instead, the +update takes advantage of the general purpose `ResultSet.getObject` methods that +take a `Class` parameter (added in JDBC 4.1), and likewise the +`SQLInput.readObject` method with a `Class` parameter (overlooked in 4.1 but +added in 4.2), so a caller can request a `java.time` class by passing the right +`Class`: + +| PostgreSQL type | Pass to `getObject`/`readObject` | +|--:|:--| +|`date`|`java.time.LocalDate.class`| +|`time without time zone`|`java.time.LocalTime.class`| +|`time with time zone`|`java.time.OffsetTime.class`| +|`timestamp without time zone`|`java.time.LocalDateTime.class`| +|`timestamp with time zone`|`java.time.OffsetDateTime.class`| +[Correspondence of PostgreSQL date/time types and Java 8 `java.time` classes] + +The `java.time` types can also be used as parameter and return types of PL/Java +functions without special effort (the generated function declarations will make +the right conversions happen), and passed to the setter methods of prepared +statements, writable result sets (for triggers or composite-returning +functions), and `SQLOutput` for UDTs. + +Conversions to and from these types never involve the PostgreSQL session time +zone, which can vary from session to session. Any code developed for PL/Java +and Java 8 or newer is strongly encouraged to use these types for date/time +manipulations, for their much better fit to the PostgreSQL types. + +### Mapping of time and timestamp with time zone + +When a `time with time zone` is mapped to a `java.time.OffsetTime`, the Java +value will have a zone offset equal to the one assigned to the value in +PostgreSQL, and so in the reverse direction. + +When a `timestamp with time zone` is mapped to a `java.time.OffsetDateTime`, +the Java value will always have a zone offset of zero (UTC). When an +`OffsetDateTime` created in Java is mapped to a PostgreSQL +`timestamp with time zone`, if its offset is not zero, the value adjusted to UTC +is used. + +These different behaviors accurately reflect how PostgreSQL treats +the two types differently. + +### Infinite dates and timestamps + +PostgreSQL allows `date` and `timestamp` (with or without time zone) values of +`infinity` and `-infinity`. + +There is no such notion in the corresponding Java classes (the original JDBC +ones or the JDBC 4.2 / JSR 310 ones), but PL/Java will map those PostgreSQL +values repeatably to certain values of the Java classes, and will map Java +objects with those exact values back to PostgreSQL `infinity` or `-infinity` +on the return trip. Java code that needs to recognize those values could do +an initial query returning `infinity` and `-infinity` and save the resulting +Java values to compare others against. It must compare with `equals()`; it +cannot assume that the mapping will produce the very same Java objects +repeatedly, but only objects with equal values. + +When timestamps are mapped to the `java.time` classes, the mapping will have +the useful property that `-infinity` really is earlier than other +PostgreSQL-representable values, and `infinity` really is later. That does not +hold under the old `java.sql.Timestamp` mapping, where both values will be +distant from the present but not further specified. + +The convenient relation does not hold for dates at all, under the `java.sql` or +`java.time` mappings; `infinity` and `-infinity` just have to be treated as two +special values. They come out as two consecutive days in the late Miocene, +as it happens, in the third week of June. + +#### Infinite timestamps without `integer_datetimes` + +In PostgreSQL builds with `integer_datetimes` as `off` (a configuration that is +non-default since PostgreSQL 8.4, and impossible since PG 10), an error results +if a timestamp being converted to Java has either infinite value. As uses of +infinite timestamps are probably rare and the configuration is long out of use, +there is no plan to lift this limitation unless an issue is opened to address a +practical need. + +[jsr310]: https://www.threeten.org/ diff --git a/src/site/markdown/use/sqlxml.md b/src/site/markdown/use/sqlxml.md new file mode 100644 index 00000000..4ee698e0 --- /dev/null +++ b/src/site/markdown/use/sqlxml.md @@ -0,0 +1,307 @@ +# Working with XML + +## In PL/Java before 1.5.1 + +PL/Java functions before 1.5.1 have been able to access a value of XML type as +a `String` object. That has been workable, but an extra burden if porting code +that used the JDBC 4.0 `java.sql.SQLXML` API, and with notable shortcomings. + +### Shortcomings + +#### Character set encoding + +PostgreSQL stores XML values serialized according to `server_encoding`, and +depending on that setting, conversion to a Java `String` can involve +transcoding. + +XML has rules to handle characters that may be representable in one encoding +but not another, but the `String` conversion is unaware of them, and may fail +to produce a transcoding that represents the same XML value. + +#### Memory footprint + +While a database design using XML may be such that each XML datum is +individually very small, it is also easy to store---or generate in +queries---large XML values. When mapped to a Java `String`, such an XML value +must have its full, uncompressed, character-serialized size allocated +on the Java heap and be copied there from native memory, before the Java +code even begins to make use of it. Even in cases where the Java processing to +be done could be organized to stream through parse events in constant-bounded +memory, the `String` representation forces the entire XML value to occupy Java +memory at once. Any tuning of PL/Java's heap size allowance could have to +consider a worst-case estimate of that size, or risk failures at run time. + +## The JDBC 4.0 `java.sql.SQLXML` API + +PL/Java 1.5.1 adds support for this API. A Java parameter or return type in a +a PL/Java function can be declared to be `SQLXML`, and such objects can be +retrieved from `ResultSet` and `SQLInput` objects, and used as +`PreparedStatement` parameters or in `SQLOutput` and updatable `ResultSet` +objects. + +### Reading a PostgreSQL XML value as a _readable_ `SQLXML` object + +An `SQLXML` instance can have the "conceptual states" _readable_ and _not +readable_, _writable_ and _not writable_. In PL/Java, an instance passed in as a +parameter to a function, or retrieved from a `ResultSet`, is _readable_ and _not +writable_, and can be used as input to Java processing using any of the +following methods: + +`getBinaryStream()` +: Obtain an `InputStream` with the raw, byte-stream-serialized XML, which will + have to be passed to an XML parser. The parser will have to determine the + encoding used from the declaration at the start of the stream, or assume + UTF-8 if there is none, as the standard provides. + +`getCharacterStream()` +: Like `getBinaryStream` but as a stream of Java characters, with the underlying + encoding already decoded. May be convenient for use with parsing code that + isn't able to recognize and honor the encoding declaration, but any standard + XML parser would work as well from `getBinaryStream`, which should be + preferred when possible. A parser working from the binary stream is able to + handle transcoding, if needed, in an XML-aware way. With this method, any + needed transcoding is done without XML-awareness to produce the character + stream. + +`getString()` +: Obtain the entire serialized XML value decoded as a Java `String`. Has the + same memory footprint and encoding implications discussed for the legacy + conversion to `String`, but may be convenient for some purposes or for + values known to be small. + +`getSource(javax.xml.transform.stream.StreamSource.class)` +: Equivalent to one of the first two methods, but with the stream wrapped in + a `Source` object, directly usable with Java XML transformation APIs. + +`getSource(javax.xml.transform.sax.SAXSource.class)` +: Obtain a `Source` object that presents the XML in parsed form via the SAX API, + where the caller can register callback methods for XML constructs of + interest, and then have Java stream through the XML value, calling those + methods. + +`getSource(javax.xml.transform.sax.StAXSource.class)` +: Obtain a `Source` object that presents the XML in parsed form via the StAX + API, where the value can be streamed through by calling StAX pull methods + to get one XML construct at a time. Java code written to this API can more + clearly reflect the expected structure of the XML document, compared to + code written in the callback style for SAX. + +`getSource(javax.xml.transform.sax.DOMSource.class)` +: Obtain a `Source` object presenting the XML fully parsed as a navigable, + in-memory DOM tree. + +`getSource(null)` +: Obtain a `Source` object of a type chosen by the implementation. Useful when + the `Source` object will be passed to a standard Java transformation API, + which can handle any of the above forms, letting the `SQLXML` implementation + choose one that it implements efficiently. + +Exactly one of these methods can be called exactly once on a _readable_ `SQLXML` +object, which is thereafter _not readable_. (The _not readable_ state prevents +a second call to any of the getter methods; it does not, of course, prevent +reading the XML content through the one stream, `String`, or `Source` obtained +from the getter method that was just called.) + +Except in the `String` or DOM form, which bring the entire XML value into Java +memory at once, the XML content is streamed directly from native PostgreSQL +memory as Java code reads it, never accumulating in the Java heap unless that +is what the application code does with it. Java heap sizing, therefore, can +be based on just what the application Java code will do with the data. + +The most convenient API to use in an application will often be SAX or StAX, +in which the code can operate at the level of already-parsed, natural XML +constructs. Code designed to work with a navigable DOM tree can easily obtain +that form (but it should be understood that DOM will pull the entire content +into Java memory at once, in a memory-hungry form that can easily be twenty +times the size of the serialized value). + +#### Obtaining a _readable_ `SQLXML` object + +To obtain a _readable_ instance, declare `java.sql.SQLXML` as the type of a +function parameter where PostgreSQL will pass an XML argument, or use the +`getSQLXML` or `getObject(..., SQLXML.class)` methods on a `ResultSet`, or the +`readSQLXML` or `readObject(SQLXML.class)` methods on `SQLInput`. A fully +JDBC-4.0 compliant driver would also return `SQLXML` instances from the +non-specific `getObject` and `readObject` methods, but in PL/Java, those have +historically returned `String`. Because 1.5.1 is not a major release, their +behavior has not changed, and the more-specific methods must be used to obtain +`SQLXML` instances. + +### Creating/returning a PostgreSQL XML value with a _writable_ `SQLXML` object + +PL/Java will supply an empty `SQLXML` instance that is _writable_ and _not +readable_ via the `Connection` method `createSQLXML()`. It can be used as an +output destination for any of several Java XML APIs, through a selection of +`set...` methods exactly mirroring the available `get...` methods described +above. + +_The API is unusual: except for `setString`, which takes a `String` parameter +and returns `void` as a typical "setter" method would, the other setter methods +are used for the object they return---an `OutputStream`, `Writer`, or +`Result`---which the calling code should then use to add content to the XML +value._ + +Exactly one setter method can be called exactly once on a _writable_ `SQLXML` +object, which is thereafter _not writable_. (The _not writable_ state prevents +a second call to any setter method; XML content must still be written via the +stream or `Result` obtained from the one setter that was just called, except +in the case of `setString`, which populates the value at once.) Content being +written to the `SQLXML` object is accumulated in PostgreSQL native memory, +not the Java heap. + +A `SQLXML` object, once it has been fully written and closed, can be +returned from a Java function, passed as a `PreparedStatement` parameter to a +nested query, or stored into writable `ResultSet`s used for composite function +or trigger results. It can be used exactly once in any of those ways, which +transfer its ownership back to PostgreSQL, leaving it inaccessible from Java. + +#### When a _writable_ `SQLXML` object is considered closed + +A _writable_ `SQLXML` object cannot be presented to PostgreSQL before it is +closed to confirm that writing is complete. (One written by `setString` is +considered written, closed, and ready to use immediately.) + +When it is written using a stream obtained from `setBinaryStream`, +`setCharacterStream`, or +`setResult(javax.xml.transform.stream.StreamResult.class)`, it +is considered closed when the stream's `close` method is called. +This will typically _not_ be done by a Java `Transformer` with the stream +as its result, and so should be explicitly called after such a transformation +completes. + +When written using a `SAXResult`, it is considered closed when the +`ContentHandler`'s `endDocument` method is called, and when written using a +`StAXResult`, it is considered closed when the `XMLStreamWriter`'s +`writeEndDocument` method is called. When one of these flavors of `Result` is +used with a Java `Transformer`, these methods will have been called in the +normal course of the transformation, so nothing special needs to be done after +the transformation completes. + +What it means to `close` a `DOMResult` is murkier. The application code must +call the `DOMResult`'s `setNode` method, passing what will be the root node of +the result document. This can be done before or after (or while) child nodes and +content are added to that node. However, to avoid undefined behavior, +application code must make no further modification to that DOM tree after the +`SQLXML` object has been presented to PostgreSQL (whether via a +`PreparedStatement` `set` method, `ResultSet` `update` method, +`SQLOutput` `write` method, or returned as the function result). + +#### Using a `Result` object as a `Transformer` result + +Classes that extend `javax.xml.transform.Transformer` will generally accept +any flavor of `Result` object and select the right API to write the +transformation result to it. There is often no need to care which `Result` +flavor to provide, so it is common to call `setResult(null)` to let the +`SQLXML` implementation itself choose a flavor based on implementation-specific +efficiency considerations. + +In the case of a `DOMResult`, if the `Result` object is simply passed to a +`Transformer` without calling `setNode` first, the `Transformer` itself will +put an empty `Document` node there, which is then populated with the +transformation results. + +A `Document` node, however, enforces conformance to the strict rules of +`XML(DOCUMENT)` form (described below). If the content to be written will +conform only to the looser rules of `XML(CONTENT)` form, application code should +call `setNode` supplying an empty `DocumentFragment` node, before passing the +`Result` object to a `Transformer`. + +The flavor of `Result` returned by `setResult(null)` will never +(in PL/Java) be `DOMResult`. + +### Using an unread _readable_ `SQLXML` object as a written one + +The general rule that only a _writable_ instance (that has been written and +closed) can be used as a function result, or passed into a nested query, admits +one exception, allowing a _readable_ instance that Java code has obtained but +not read. That makes it simple for Java code to obtain an `SQLXML` instance +passed in as a parameter, or from a query, and use it directly as a result or a +nested-query parameter. Any one instance can be used this way no more than once. + +### `XML(DOCUMENT)` and `XML(CONTENT)` + +An XML value in SQL can have the type `XML(DOCUMENT)` or `XML(CONTENT)` (as +those are defined in the ISO SQL standard, 2006 and later), which PostgreSQL +does not currently treat as distinguishable types. The `DOCUMENT` form must have +exactly one root element, may have a document-type declaration (DTD), and has +strict limits on where other +constructs (other than comments and processing instructions) can occur. A value +in `CONTENT` form may have no root element, or more than one element at top +level, and other constructs such as character data outside of a root element +where `DOCUMENT` form would not allow them. + +#### How both forms are accommodated when reading + +Java code using a _readable_ `SQLXML` instance as input should be prepared to +encounter either form (unless it has out-of-band knowledge of which form will be +supplied). If it requests a `DOMSource`, `getNode()` will return a `Document` +node, if the value met all the requirements for `DOCUMENT`, or a +`DocumentFragment` node, if it was parsable as `CONTENT`. Java code requesting a +`SAXSource` or `StAXSource` should be prepared to handle a sequence of +constructs that might not be encountered when parsing a strictly conforming +`DOCUMENT`. Java code that requests an `InputStream`, `Reader`, `String`, or +`StreamSource` will be on its own to parse the data in whichever form appears. + +#### How both forms are accommodated when writing + +Java code using a _writable_ SQLXML instance to produce a result may write +either `DOCUMENT` or `CONTENT` form. If using `DOMResult`, it must supply a +`DocumentFragment` node to produce a `CONTENT` result, as a `Document` node will +enforce the `DOCUMENT` requirements. + +### An `SQLXML` object has transaction lifetime + +The JDBC spec provides that an `SQLXML` instance is "valid for the duration of +the transaction in which it was created." One PL/Java function can hold an +`SQLXML` instance (in a static or session variable or data structure), and other +PL/Java functions called later in the same transaction can continue reading from +or writing to it. If the transaction has committed or rolled back, those +operations will generate an exception. + +Once a _writable_ `SQLXML` object, or an unread, _readable_ one, has been +presented to PostgreSQL as the result of a PL/Java function or through a +`PreparedStatement`/`ResultSet`/`SQLOutput` setter method, it is no longer +accessible in Java. + +During a transaction, resources held by a `SQLXML` object are reclaimed as soon +as a _readable_ one has been fully read, or a _writable_ one has been presented +to PostgreSQL and PostgreSQL is done with it. If application code holds a +readable `SQLXML` object that it determines it will not read, or a writable one +it will not present to PostgreSQL, it can call the `free` method to allow the +resources to be reclaimed sooner than the transaction's end. + +### Lazy detoasting + +PostgreSQL can represent large XML values in "TOASTed" form, which may be in +memory but compressed (XML typically compresses to a small fraction of its +serialized size), or may be a small pointer to a location in storage. A +_readable_ `SQLXML` instance over a TOASTed value will not be detoasted until +Java code actually begins to read it, so the memory footprint of an instance +being held but not yet read is kept low. + +### Validation of content + +Some of the methods by which a _writable_ instance can be written are not +XML-specific APIs, but allow arbitrary content to be written (as a `String`, +`Writer`, or `OutputStream`). When written by those methods, type safety is +upheld by verifying that the written content can be successfully reparsed, +accepting either `DOCUMENT` or `CONTENT` form. + +It remains possible to declare the Java type `String` for function parameters +and returns of XML type, and to retrieve and supply `String` for `ResultSet` +columns and `PreparedStatement` parameters of XML type. This legacy mapping +from `String` to XML uses PostgreSQL's `xml_in` function to verify the form of a +`String` from Java. That function may reject some valid values if the server +configuration variable `xmloption` is not first set to `DOCUMENT` or `CONTENT` +to match the type of the value. + +### Usable with or without native XML support in PostgreSQL + +In symmetry to using Java `String` for SQL XML types, PL/Java allows the Java +`SQLXML` type to be used with PostgreSQL data of type `text`. This allows full +use of the Java XML APIs even in PostgreSQL instances built without XML support. +All of the `SQLXML` behaviors described above also apply in this usage. + +If a _readable_ `SQLXML` instance obtained from a `text` value is directly used +to set or return a value of PostgreSQL's XML type, the XML-ness of the content +is verified. From 64a28051e1bae6b79efea52310b3d410a55443d4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 2 Sep 2018 13:22:13 -0400 Subject: [PATCH 0178/1087] Schema-qualify internally-generated names ... in various places except DatabaseMetaData. That thing'll be worth a commit all its own. In cases where a type name can't be qualified (because it's just an alias known to the parser), that's ok, because those known aliases have precedence over newly-created types, unless quoted. --- .../pljava/sqlgen/DDRProcessor.java | 52 ++++++------- .../pljava/internal/InstallHelper.java | 41 +++++----- .../postgresql/pljava/jdbc/SPIConnection.java | 2 +- .../pljava/management/Commands.java | 74 ++++++++++++------- .../pljava/sqlj/EntryStreamHandler.java | 3 +- .../org/postgresql/pljava/sqlj/Loader.java | 16 ++-- 6 files changed, 111 insertions(+), 77 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index ad8107a4..a1c17bca 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1632,7 +1632,7 @@ public String[] deployStrings() appendNameAndParams( sb, true); sb.append( "\n\tRETURNS "); if ( trigger ) - sb.append( "trigger"); + sb.append( "pg_catalog.trigger"); else { if ( setof ) @@ -1642,7 +1642,7 @@ public String[] deployStrings() else if ( null != setofComponent ) sb.append( tmpr.getSQLType( setofComponent, func)); else if ( setof ) - sb.append( "RECORD"); + sb.append( "pg_catalog.RECORD"); else sb.append( tmpr.getSQLType( func.getReturnType(), func)); } @@ -1707,10 +1707,10 @@ public String[] undeployStrings() static enum BaseUDTFunctionID { - INPUT( "in", "cstring, oid, integer", null), - OUTPUT( "out", null, "cstring"), - RECEIVE( "recv", "internal, oid, integer", null), - SEND( "send", null, "bytea"); + INPUT( "in", "pg_catalog.cstring, pg_catalog.oid, integer", null), + OUTPUT( "out", null, "pg_catalog.cstring"), + RECEIVE( "recv", "pg_catalog.internal, pg_catalog.oid, integer", null), + SEND( "send", null, "pg_catalog.bytea"); BaseUDTFunctionID( String suffix, String param, String ret) { this.suffix = suffix; @@ -2228,7 +2228,7 @@ class TypeMapper { protoMappings = new ArrayList>(); - // Primitives + // Primitives (these need not, indeed cannot, be schema-qualified) // this.addMap(boolean.class, "boolean"); this.addMap(Boolean.class, "boolean"); @@ -2249,27 +2249,29 @@ class TypeMapper // Known common mappings // - this.addMap(Number.class, "numeric"); - this.addMap(String.class, "varchar"); - this.addMap(java.util.Date.class, "timestamp"); - this.addMap(Timestamp.class, "timestamp"); - this.addMap(Time.class, "time"); - this.addMap(java.sql.Date.class, "date"); - this.addMap(java.sql.SQLXML.class, "xml"); - this.addMap(BigInteger.class, "numeric"); - this.addMap(BigDecimal.class, "numeric"); - this.addMap(ResultSet.class, "record"); - this.addMap(Object.class, "\"any\""); - - this.addMap(byte[].class, "bytea"); + this.addMap(Number.class, "pg_catalog.numeric"); + this.addMap(String.class, "pg_catalog.varchar"); + this.addMap(java.util.Date.class, "pg_catalog.timestamp"); + this.addMap(Timestamp.class, "pg_catalog.timestamp"); + this.addMap(Time.class, "pg_catalog.time"); + this.addMap(java.sql.Date.class, "pg_catalog.date"); + this.addMap(java.sql.SQLXML.class, "pg_catalog.xml"); + this.addMap(BigInteger.class, "pg_catalog.numeric"); + this.addMap(BigDecimal.class, "pg_catalog.numeric"); + this.addMap(ResultSet.class, "pg_catalog.record"); + this.addMap(Object.class, "pg_catalog.\"any\""); + + this.addMap(byte[].class, "pg_catalog.bytea"); // (Once Java back horizon advances to 8, do these the easy way.) // - this.addMapIfExists("java.time.LocalDate", "date"); - this.addMapIfExists("java.time.LocalTime", "time"); - this.addMapIfExists("java.time.OffsetTime", "timetz"); - this.addMapIfExists("java.time.LocalDateTime", "timestamp"); - this.addMapIfExists("java.time.OffsetDateTime", "timestamptz"); + this.addMapIfExists("java.time.LocalDate", "pg_catalog.date"); + this.addMapIfExists("java.time.LocalTime", "pg_catalog.time"); + this.addMapIfExists("java.time.OffsetTime", "pg_catalog.timetz"); + this.addMapIfExists("java.time.LocalDateTime", + "pg_catalog.timestamp"); + this.addMapIfExists("java.time.OffsetDateTime", + "pg_catalog.timestamptz"); } private boolean mappingsFrozen() diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 6139f13c..c174b8c8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -222,7 +222,7 @@ private static void handlers( Connection c, Statement s, String module_path) { s.execute( "CREATE OR REPLACE FUNCTION sqlj.java_call_handler()" + - " RETURNS language_handler" + + " RETURNS pg_catalog.language_handler" + " AS " + eQuote(module_path) + " LANGUAGE C"); s.execute("REVOKE ALL PRIVILEGES" + @@ -243,7 +243,7 @@ private static void handlers( Connection c, Statement s, String module_path) s.execute( "CREATE OR REPLACE FUNCTION sqlj.javau_call_handler()" + - " RETURNS language_handler" + + " RETURNS pg_catalog.language_handler" + " AS " + eQuote(module_path) + " LANGUAGE C"); s.execute("REVOKE ALL PRIVILEGES" + @@ -337,7 +337,8 @@ private static void deployment( Connection c, Statement s, SchemaVariant sv) * messed up schema that never appeared in the git history, if it happened * to match on the tested parts. The variant EMPTY is returned if nothing is * in the schema (based on a direct query of pg_depend, which ought to be - * reliable) except an entry for the extension if applicable. A null return + * reliable) except an entry for the extension if applicable, or for the + * table temporarily created there during CREATE EXTENSION. A null return * indicates that whatever is there didn't match the tests for any known * variant. */ @@ -401,20 +402,26 @@ private static SchemaVariant recognizeSchema( PreparedStatement ps = c.prepareStatement( "SELECT count(*) " + "FROM pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + - "WHERE refclassid = 'pg_catalog.pg_namespace'::regclass " + - "AND refobjid = n.oid AND nspname = 'sqlj' " + - "AND deptype = 'n' " + - "AND NOT EXISTS ( " + - " SELECT 1 FROM " + - " pg_catalog.pg_class sqc JOIN pg_catalog.pg_namespace sqn " + - " ON relnamespace = sqn.oid " + - " WHERE " + - " nspname = 'pg_catalog' AND relname = 'pg_extension' " + - " AND classid = sqc.oid " + + "WHERE" + + " refclassid OPERATOR(pg_catalog.=)" + + " 'pg_catalog.pg_namespace'::regclass " + + " AND refobjid OPERATOR(pg_catalog.=) n.oid" + + " AND nspname OPERATOR(pg_catalog.=) 'sqlj' " + + " AND deptype OPERATOR(pg_catalog.=) 'n' " + + " AND NOT EXISTS ( " + + " SELECT 1 FROM " + + " pg_catalog.pg_class sqc JOIN pg_catalog.pg_namespace sqn " + + " ON relnamespace OPERATOR(pg_catalog.=) sqn.oid " + + " WHERE " + + " nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + + " AND relname OPERATOR(pg_catalog.=) 'pg_extension' " + + " AND classid OPERATOR(pg_catalog.=) sqc.oid " + " OR " + - " nspname = 'sqlj' AND relname = ?" + - " AND classid = 'pg_catalog.pg_class'::regclass " + - " AND objid = sqc.oid)"); + " nspname OPERATOR(pg_catalog.=) 'sqlj'" + + " AND relname OPERATOR(pg_catalog.=) ?" + + " AND classid OPERATOR(pg_catalog.=)" + + " 'pg_catalog.pg_class'::regclass " + + " AND objid OPERATOR(pg_catalog.=) sqc.oid)"); ps.setString(1, loadpath_tbl); rs = ps.executeQuery(); if ( rs.next() && 0 == rs.getInt(1) ) @@ -451,7 +458,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) s.execute( "CREATE TABLE sqlj.jar_descriptor " + "(jarId, ordinal, entryId) AS SELECT " + - "CAST(jarId AS INT), CAST(0 AS INT2), " + + "CAST(jarId AS INT), CAST(0 AS pg_catalog.INT2), " + "deploymentDesc FROM sqlj.jar_repository " + "WHERE deploymentDesc IS NOT NULL"); s.execute( diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 595c961c..55cac482 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -702,7 +702,7 @@ public int[] getVersionNumber() throws SQLException return VERSION_NUMBER; ResultSet rs = createStatement().executeQuery( - "SELECT version()"); + "SELECT pg_catalog.version()"); try { diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 9132dee1..e3ea1765 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -247,14 +247,18 @@ /* * Attention: any evolution of the schema here needs to be reflected in * o.p.p.internal.InstallHelper.SchemaVariant and .recognizeSchema(). + * + * Schema-qualification of a type with a typmod, e.g. pg_catalog.varchar(100), + * is possible from PostgreSQL 8.3 onward, but not in 8.2. As a compromise, use + * the two-word CHARACTER VARYING syntax, to evade capture by a user type. */ @SQLAction(install={ " CREATE TABLE sqlj.jar_repository(" + " jarId SERIAL PRIMARY KEY," + -" jarName VARCHAR(100) UNIQUE NOT NULL," + -" jarOrigin VARCHAR(500) NOT NULL," + -" jarOwner NAME NOT NULL," + -" jarManifest TEXT" + +" jarName CHARACTER VARYING(100) UNIQUE NOT NULL," + +" jarOrigin CHARACTER VARYING(500) NOT NULL," + +" jarOwner pg_catalog.NAME NOT NULL," + +" jarManifest pg_catalog.TEXT" + " )", " COMMENT ON TABLE sqlj.jar_repository IS" + " 'Information on jars loaded by PL/Java, one row per jar.'", @@ -262,10 +266,10 @@ " CREATE TABLE sqlj.jar_entry(" + " entryId SERIAL PRIMARY KEY," + -" entryName VARCHAR(200) NOT NULL," + +" entryName CHARACTER VARYING(200) NOT NULL," + " jarId INT NOT NULL" + " REFERENCES sqlj.jar_repository ON DELETE CASCADE," + -" entryImage BYTEA NOT NULL," + +" entryImage pg_catalog.BYTEA NOT NULL," + " UNIQUE(jarId, entryName)" + " )", " COMMENT ON TABLE sqlj.jar_entry IS" + @@ -274,7 +278,7 @@ " CREATE TABLE sqlj.jar_descriptor(" + " jarId INT REFERENCES sqlj.jar_repository ON DELETE CASCADE," + -" ordinal INT2," + +" ordinal pg_catalog.INT2," + " PRIMARY KEY (jarId, ordinal)," + " entryId INT NOT NULL REFERENCES sqlj.jar_entry ON DELETE CASCADE" + " )", @@ -285,8 +289,8 @@ " GRANT SELECT ON sqlj.jar_descriptor TO public", " CREATE TABLE sqlj.classpath_entry(" + -" schemaName VARCHAR(30) NOT NULL," + -" ordinal INT2 NOT NULL," + +" schemaName CHARACTER VARYING(30) NOT NULL," + +" ordinal pg_catalog.INT2 NOT NULL," + " jarId INT NOT NULL" + " REFERENCES sqlj.jar_repository ON DELETE CASCADE," + " PRIMARY KEY(schemaName, ordinal)" + @@ -299,8 +303,8 @@ " CREATE TABLE sqlj.typemap_entry(" + " mapId SERIAL PRIMARY KEY," + -" javaName VARCHAR(200) NOT NULL," + -" sqlName NAME NOT NULL" + +" javaName CHARACTER VARYING(200) NOT NULL," + +" sqlName pg_catalog.NAME NOT NULL" + " )", " COMMENT ON TABLE sqlj.typemap_entry IS" + " 'A row for each SQL type <-> Java type custom mapping.'", @@ -350,7 +354,8 @@ public static void addClassImages(int jarId, InputStream urlStream, int sz) PreparedStatement us = SQLUtils .getDefaultConnection() .prepareStatement( - "UPDATE sqlj.jar_repository SET jarManifest = ? WHERE jarId = ?"); + "UPDATE sqlj.jar_repository SET jarManifest = ? " + + "WHERE jarId OPERATOR(pg_catalog.=) ?"); try { us.setString(1, manifest); @@ -398,8 +403,9 @@ public static void addClassImages(int jarId, InputStream urlStream, int sz) if ( descIdFetchStmt == null ) descIdFetchStmt = SQLUtils.getDefaultConnection() .prepareStatement( - "SELECT entryId FROM sqlj.jar_entry" - + " WHERE jarId = ? AND entryName = ?"); + "SELECT entryId FROM sqlj.jar_entry " + + "WHERE jarId OPERATOR(pg_catalog.=) ?" + + " AND entryName OPERATOR(pg_catalog.=) ?"); descIdFetchStmt.setInt(1, jarId); descIdFetchStmt.setString(2, entryName); rs = descIdFetchStmt.executeQuery(); @@ -561,7 +567,8 @@ public static void dropTypeMapping(String sqlTypeName) throws SQLException { sqlTypeName = getFullSqlNameOwned(sqlTypeName); stmt = SQLUtils.getDefaultConnection().prepareStatement( - "DELETE FROM sqlj.typemap_entry WHERE sqlName = ?"); + "DELETE FROM sqlj.typemap_entry " + + "WHERE sqlName OPERATOR(pg_catalog.=) ?"); stmt.setString(1, sqlTypeName); stmt.executeUpdate(); } @@ -597,9 +604,13 @@ public static String getClassPath(String schemaName) throws SQLException stmt = SQLUtils .getDefaultConnection() .prepareStatement( - "SELECT r.jarName" - + " FROM sqlj.jar_repository r INNER JOIN sqlj.classpath_entry c ON r.jarId = c.jarId" - + " WHERE c.schemaName = ? ORDER BY c.ordinal"); + "SELECT r.jarName" + + " FROM" + + " sqlj.jar_repository r" + + " INNER JOIN sqlj.classpath_entry c" + + " ON r.jarId OPERATOR(pg_catalog.=) c.jarId" + + " WHERE c.schemaName OPERATOR(pg_catalog.=) ?" + + " ORDER BY c.ordinal"); stmt.setString(1, schemaName); rs = stmt.executeQuery(); @@ -705,7 +716,9 @@ public static void removeJar(String jarName, boolean undeploy) PreparedStatement stmt = SQLUtils .getDefaultConnection() - .prepareStatement("DELETE FROM sqlj.jar_repository WHERE jarId = ?"); + .prepareStatement( + "DELETE FROM sqlj.jar_repository " + + "WHERE jarId OPERATOR(pg_catalog.=) ?"); try { stmt.setInt(1, jarId); @@ -807,7 +820,8 @@ public static void setClassPath(String schemaName, String path) // entries = new ArrayList(); stmt = SQLUtils.getDefaultConnection().prepareStatement( - "SELECT jarId FROM sqlj.jar_repository WHERE jarName = ?"); + "SELECT jarId FROM sqlj.jar_repository " + + "WHERE jarName OPERATOR(pg_catalog.=) ?"); try { for(;;) @@ -841,7 +855,8 @@ public static void setClassPath(String schemaName, String path) // Delete the old classpath // stmt = SQLUtils.getDefaultConnection().prepareStatement( - "DELETE FROM sqlj.classpath_entry WHERE schemaName = ?"); + "DELETE FROM sqlj.classpath_entry " + + "WHERE schemaName OPERATOR(pg_catalog.=) ?"); try { stmt.setString(1, schemaName); @@ -979,8 +994,8 @@ private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) .prepareStatement( "SELECT e.entryImage" + " FROM sqlj.jar_descriptor d INNER JOIN sqlj.jar_entry e" - + " ON d.entryId = e.entryId" - + " WHERE d.jarId = ?" + + " ON d.entryId OPERATOR(pg_catalog.=) e.entryId" + + " WHERE d.jarId OPERATOR(pg_catalog.=) ?" + " ORDER BY d.ordinal"); try { @@ -1035,7 +1050,8 @@ private static String getFullSqlNameOwned(String sqlTypeName) "SELECT n.nspname, t.typname," + " pg_catalog.pg_has_role(?, t.typowner, 'USAGE')" + " FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n" - + " WHERE t.oid = ? AND n.oid = t.typnamespace"); + + " WHERE t.oid OPERATOR(pg_catalog.=) ?" + + " AND n.oid OPERATOR(pg_catalog.=) t.typnamespace"); try { @@ -1099,7 +1115,8 @@ private static int getJarId(String jarName, AclId[] ownerRet) PreparedStatement stmt = SQLUtils .getDefaultConnection() .prepareStatement( - "SELECT jarId, jarOwner FROM sqlj.jar_repository WHERE jarName = ?"); + "SELECT jarId, jarOwner FROM sqlj.jar_repository " + + "WHERE jarName OPERATOR(pg_catalog.=) ?"); try { return getJarId(stmt, jarName, ownerRet); @@ -1123,7 +1140,8 @@ private static Oid getSchemaId(String schemaName) throws SQLException ResultSet rs = null; PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( - "SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = ?"); + "SELECT oid FROM pg_catalog.pg_namespace " + + "WHERE nspname OPERATOR(pg_catalog.=) ?"); try { stmt.setString(1, schemaName); @@ -1212,7 +1230,7 @@ private static void replaceJar(String urlString, String jarName, .prepareStatement( "UPDATE sqlj.jar_repository " + "SET jarOrigin = ?, jarOwner = ?, jarManifest = NULL " - + "WHERE jarId = ?"); + + "WHERE jarId OPERATOR(pg_catalog.=) ?"); try { stmt.setString(1, urlString); @@ -1228,7 +1246,7 @@ private static void replaceJar(String urlString, String jarName, } stmt = SQLUtils.getDefaultConnection().prepareStatement( - "DELETE FROM sqlj.jar_entry WHERE jarId = ?"); + "DELETE FROM sqlj.jar_entry WHERE jarId OPERATOR(pg_catalog.=) ?"); try { stmt.setInt(1, jarId); diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/EntryStreamHandler.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/EntryStreamHandler.java index 42b92ad9..1ac02cd9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/EntryStreamHandler.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/EntryStreamHandler.java @@ -73,7 +73,8 @@ public void connect() { stmt = SQLUtils.getDefaultConnection().prepareStatement( - "SELECT entryName, entryImage FROM sqlj.jar_entry WHERE entryId = ?"); + "SELECT entryName, entryImage FROM sqlj.jar_entry " + + "WHERE entryId OPERATOR(pg_catalog.=) ?"); stmt.setInt(1, m_entryId); rs = stmt.executeQuery(); if(rs.next()) diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index cf83b7b7..4347bce2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -90,7 +90,7 @@ public static ClassLoader getCurrentLoader() ResultSet rs = null; try { - rs = stmt.executeQuery("SELECT current_schema()"); + rs = stmt.executeQuery("SELECT pg_catalog.current_schema()"); if(!rs.next()) throw new SQLException("Unable to determine current schema"); schema = rs.getString(1); @@ -132,11 +132,16 @@ public static ClassLoader getSchemaLoader(String schemaName) // outer = conn.prepareStatement( "SELECT r.jarId" + - " FROM sqlj.jar_repository r INNER JOIN sqlj.classpath_entry c ON r.jarId = c.jarId" + - " WHERE c.schemaName = ? ORDER BY c.ordinal DESC"); + " FROM" + + " sqlj.jar_repository r" + + " INNER JOIN sqlj.classpath_entry c" + + " ON r.jarId OPERATOR(pg_catalog.=) c.jarId" + + " WHERE c.schemaName OPERATOR(pg_catalog.=) ?" + + " ORDER BY c.ordinal DESC"); inner = conn.prepareStatement( - "SELECT entryId, entryName FROM sqlj.jar_entry WHERE jarId = ?"); + "SELECT entryId, entryName FROM sqlj.jar_entry " + + "WHERE jarId OPERATOR(pg_catalog.=) ?"); outer.setString(1, schemaName); ResultSet rs = outer.executeQuery(); @@ -304,7 +309,8 @@ protected Class findClass(final String name) // for the duration of the loader. // stmt = SQLUtils.getDefaultConnection().prepareStatement( - "SELECT entryImage FROM sqlj.jar_entry WHERE entryId = ?"); + "SELECT entryImage FROM sqlj.jar_entry " + + "WHERE entryId OPERATOR(pg_catalog.=) ?"); stmt.setInt(1, entryId[0]); rs = stmt.executeQuery(); From a7ce56fa82afbde6f2ecbf3b063863919e58f2d6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 2 Sep 2018 22:03:49 -0400 Subject: [PATCH 0179/1087] Schema-qualifications in DatabaseMetaData In passing, remove one PostgreSQL version test for a version before 8, no longer supported. Also in passing, unbreak a couple of queries used for getTables() when SYSTEM TABLE or SYSTEM INDEX is the type of interest, and useSchemas is NOSCHEMAS. The feeling of accomplishment (the case got broken in 6a7eda5, or at least one did) lasts only long enough to discover both cases have been wholly unused for longer than that: useSchemas has been hardcoded to SCHEMAS since inception. Bit more digging to see what the other case was intended for. --- .../pljava/jdbc/SPIDatabaseMetaData.java | 408 ++++++++++++------ 1 file changed, 269 insertions(+), 139 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index b44a1866..d27e0d6e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -67,10 +67,12 @@ protected int getMaxNameLength() throws SQLException { if(NAMEDATALEN == 0) { - String sql = "SELECT t.typlen FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n" + - " WHERE t.typnamespace=n.oid" + - " AND t.typname='name'" + - " AND n.nspname='pg_catalog'"; + String sql = + "SELECT t.typlen" + + " FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n" + + " WHERE t.typnamespace OPERATOR(pg_catalog.=) n.oid" + + " AND t.typname OPERATOR(pg_catalog.=) 'name'" + + " AND n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'"; ResultSet rs = m_connection.createStatement().executeQuery(sql); if(!rs.next()){ throw new SQLException( @@ -102,7 +104,7 @@ public boolean allTablesAreSelectable() throws SQLException } /* - * What is the URL for this database? @return the url or null if it cannott + * What is the URL for this database? @return the url or null if it cannot * be generated @exception SQLException if a database access error occurs */ public String getURL() throws SQLException @@ -1339,7 +1341,7 @@ private static String resolveSchemaConditionWithOperator( //This means that only "visible" schemas are searched. //It was approved to change to *all* schemas. //return expr + " " + operator + " ANY (current_schemas(true))"; - return "1=1"; + return "1 OPERATOR(pg_catalog.=) 1"; } //schema is specified => search in this schema else if(!"".equals(schema)) @@ -1363,7 +1365,8 @@ else if(!"".equals(schema)) */ private static String resolveSchemaCondition(String expr, String schema) { - return resolveSchemaConditionWithOperator(expr, schema, "="); + return resolveSchemaConditionWithOperator( + expr, schema, "OPERATOR(pg_catalog.=)"); } /** @@ -1407,10 +1410,15 @@ public java.sql.ResultSet getProcedures(String catalog, + java.sql.DatabaseMetaData.procedureReturnsResult + " AS PROCEDURE_TYPE " + " FROM pg_catalog.pg_namespace n, pg_catalog.pg_proc p " - + " LEFT JOIN pg_catalog.pg_description d ON (p.oid=d.objoid) " - + " LEFT JOIN pg_catalog.pg_class c ON (d.classoid=c.oid AND c.relname='pg_proc') " - + " LEFT JOIN pg_catalog.pg_namespace pn ON (c.relnamespace=pn.oid AND pn.nspname='pg_catalog') " - + " WHERE p.pronamespace=n.oid " + + " LEFT JOIN pg_catalog.pg_description d" + + " ON (p.oid OPERATOR(pg_catalog.=) d.objoid) " + + " LEFT JOIN pg_catalog.pg_class c ON (" + + " d.classoid OPERATOR(pg_catalog.=) c.oid" + + " AND c.relname OPERATOR(pg_catalog.=) 'pg_proc') " + + " LEFT JOIN pg_catalog.pg_namespace pn ON (" + + " c.relnamespace OPERATOR(pg_catalog.=) pn.oid" + + " AND pn.nspname OPERATOR(pg_catalog.=) 'pg_catalog') " + + " WHERE p.pronamespace OPERATOR(pg_catalog.=) n.oid " + " AND " + resolveSchemaPatternCondition( "n.nspname", schemaPattern); if(procedureNamePattern != null) @@ -1482,11 +1490,17 @@ public java.sql.ResultSet getProcedureColumns(String catalog, f[12] = new ResultSetField("REMARKS", TypeOid.VARCHAR, getMaxNameLength()); - String sql = "SELECT n.nspname,p.proname,p.prorettype,p.proargtypes, t.typtype::varchar,t.typrelid " - + " FROM pg_catalog.pg_proc p,pg_catalog.pg_namespace n, pg_catalog.pg_type t " - + " WHERE p.pronamespace=n.oid AND p.prorettype=t.oid " - + " AND " + resolveSchemaPatternCondition( - "n.nspname", schemaPattern); + String sql = + "SELECT" + + " n.nspname, p.proname, p.prorettype, p.proargtypes," + + " t.typtype::pg_catalog.varchar, t.typrelid " + + " FROM" + + " pg_catalog.pg_proc p, pg_catalog.pg_namespace n," + + " pg_catalog.pg_type t" + + " WHERE p.pronamespace OPERATOR(pg_catalog.=) n.oid" + + " AND p.prorettype OPERATOR(pg_catalog.=) t.oid " + + " AND " + resolveSchemaPatternCondition( + "n.nspname", schemaPattern); if(procedureNamePattern != null) { sql += " AND p.proname LIKE '" @@ -1607,46 +1621,63 @@ public java.sql.ResultSet getProcedureColumns(String catalog, * should be "%" @param types a list of table types to include; null returns * all types @return each row is a table description @exception SQLException * if a database-access error occurs. + * + * September 2018: instead of rewriting all these CASE foo WHEN WHEN WHEN + * structures to avoid the implicit = operator, just cast the WHEN operands + * to the known type ("char") of the proband, as there is sure to be an + * =("char","char") in pg_catalog. */ public java.sql.ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String types[]) throws SQLException { String useSchemas = "SCHEMAS"; String select = "SELECT NULL AS TABLE_CAT, n.nspname AS TABLE_SCHEM, c.relname AS TABLE_NAME, " - + " CASE n.nspname LIKE 'pg!_%' ESCAPE '!' OR n.nspname = 'information_schema' " + + " CASE" + + " n.nspname LIKE 'pg!_%' ESCAPE '!'" + + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema' " + " WHEN true THEN CASE " - + " WHEN n.nspname = 'pg_catalog' OR n.nspname = 'information_schema' THEN CASE c.relkind " - + " WHEN 'r' THEN 'SYSTEM TABLE' " - + " WHEN 'v' THEN 'SYSTEM VIEW' " - + " WHEN 'i' THEN 'SYSTEM INDEX' " - + " ELSE NULL " - + " END " - + " WHEN n.nspname = 'pg_toast' THEN CASE c.relkind " - + " WHEN 'r' THEN 'SYSTEM TOAST TABLE' " - + " WHEN 'i' THEN 'SYSTEM TOAST INDEX' " - + " ELSE NULL " + + " WHEN" + + " n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema'" + + " THEN CASE c.relkind " + + " WHEN 'r'::pg_catalog.\"char\" THEN 'SYSTEM TABLE' " + + " WHEN 'v'::pg_catalog.\"char\" THEN 'SYSTEM VIEW' " + + " WHEN 'i'::pg_catalog.\"char\" THEN 'SYSTEM INDEX' " + + " ELSE NULL " + + " END " + + " WHEN n.nspname OPERATOR(pg_catalog.=) 'pg_toast'" + + " THEN CASE c.relkind " + + " WHEN 'r'::pg_catalog.\"char\" THEN 'SYSTEM TOAST TABLE' " + + " WHEN 'i'::pg_catalog.\"char\" THEN 'SYSTEM TOAST INDEX' " + + " ELSE NULL " + + " END " + + " ELSE CASE c.relkind " + + " WHEN 'r'::pg_catalog.\"char\" THEN 'TEMPORARY TABLE' " + + " WHEN 'i'::pg_catalog.\"char\" THEN 'TEMPORARY INDEX' " + + " ELSE NULL " + + " END " + " END " - + " ELSE CASE c.relkind " - + " WHEN 'r' THEN 'TEMPORARY TABLE' " - + " WHEN 'i' THEN 'TEMPORARY INDEX' " + + " WHEN false THEN CASE c.relkind " + + " WHEN 'r'::pg_catalog.\"char\" THEN 'TABLE' " + + " WHEN 'i'::pg_catalog.\"char\" THEN 'INDEX' " + + " WHEN 'S'::pg_catalog.\"char\" THEN 'SEQUENCE' " + + " WHEN 'v'::pg_catalog.\"char\" THEN 'VIEW' " + " ELSE NULL " + " END " - + " END " - + " WHEN false THEN CASE c.relkind " - + " WHEN 'r' THEN 'TABLE' " - + " WHEN 'i' THEN 'INDEX' " - + " WHEN 'S' THEN 'SEQUENCE' " - + " WHEN 'v' THEN 'VIEW' " - + " ELSE NULL " - + " END " + " ELSE NULL " + " END " + " AS TABLE_TYPE, d.description AS REMARKS " + " FROM pg_catalog.pg_namespace n, pg_catalog.pg_class c " - + " LEFT JOIN pg_catalog.pg_description d ON (c.oid = d.objoid AND d.objsubid = 0) " - + " LEFT JOIN pg_catalog.pg_class dc ON (d.classoid=dc.oid AND dc.relname='pg_class') " - + " LEFT JOIN pg_catalog.pg_namespace dn ON (dn.oid=dc.relnamespace AND dn.nspname='pg_catalog') " - + " WHERE c.relnamespace = n.oid " + + " LEFT JOIN pg_catalog.pg_description d ON (" + + " c.oid OPERATOR(pg_catalog.=) d.objoid" + + " AND d.objsubid OPERATOR(pg_catalog.=) 0) " + + " LEFT JOIN pg_catalog.pg_class dc ON (" + + " d.classoid OPERATOR(pg_catalog.=) dc.oid" + + " AND dc.relname OPERATOR(pg_catalog.=) 'pg_class') " + + " LEFT JOIN pg_catalog.pg_namespace dn ON (" + + " dn.oid OPERATOR(pg_catalog.=) dc.relnamespace" + + " AND dn.nspname OPERATOR(pg_catalog.=) 'pg_catalog') " + + " WHERE c.relnamespace OPERATOR(pg_catalog.=) n.oid " + " AND " + resolveSchemaPatternCondition( "n.nspname", schemaPattern); String orderby = " ORDER BY TABLE_TYPE,TABLE_SCHEM,TABLE_NAME "; @@ -1684,64 +1715,97 @@ public java.sql.ResultSet getTables(String catalog, String schemaPattern, HashMap ht = new HashMap(); s_tableTypeClauses.put("TABLE", ht); ht.put("SCHEMAS", - "c.relkind = 'r' AND n.nspname NOT LIKE 'pg!_%' ESCAPE '!' AND n.nspname <> 'information_schema'"); + "c.relkind OPERATOR(pg_catalog.=) 'r' " + + "AND n.nspname NOT LIKE 'pg!_%' ESCAPE '!' " + + "AND n.nspname OPERATOR(pg_catalog.<>) 'information_schema'"); ht.put("NOSCHEMAS", - "c.relkind = 'r' AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'r' " + + "AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("VIEW", ht); ht.put("SCHEMAS", - "c.relkind = 'v' AND n.nspname <> 'pg_catalog' AND n.nspname <> 'information_schema'"); + "c.relkind OPERATOR(pg_catalog.=) 'v' " + + "AND n.nspname OPERATOR(pg_catalog.<>) 'pg_catalog' " + + "AND n.nspname OPERATOR(pg_catalog.<>) 'information_schema'"); ht.put("NOSCHEMAS", - "c.relkind = 'v' AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'v' " + + "AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("INDEX", ht); ht.put("SCHEMAS", - "c.relkind = 'i' AND n.nspname NOT LIKE 'pg!_%' ESCAPE '!' AND n.nspname <> 'information_schema'"); + "c.relkind OPERATOR(pg_catalog.=) 'i' " + + "AND n.nspname NOT LIKE 'pg!_%' ESCAPE '!' " + + "AND n.nspname OPERATOR(pg_catalog.<>) 'information_schema'"); ht.put("NOSCHEMAS", - "c.relkind = 'i' AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'i' " + + "AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("SEQUENCE", ht); - ht.put("SCHEMAS", "c.relkind = 'S'"); - ht.put("NOSCHEMAS", "c.relkind = 'S'"); + ht.put("SCHEMAS", "c.relkind OPERATOR(pg_catalog.=) 'S'"); + ht.put("NOSCHEMAS", "c.relkind OPERATOR(pg_catalog.=) 'S'"); ht = new HashMap(); s_tableTypeClauses.put("SYSTEM TABLE", ht); ht.put("SCHEMAS", - "c.relkind = 'r' AND (n.nspname = 'pg_catalog' OR n.nspname = 'information_schema')"); + "c.relkind OPERATOR(pg_catalog.=) 'r' AND (" + + " n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema')"); ht.put("NOSCHEMAS", - "c.relkind = 'r' AND c.relname LIKE 'pg!_%' ESCAPE '!' AND c.relname NOT LIKE 'pgLIKE 'pg!_toast!_%' ESCAPE '!'toast!_%' ESCAPE '!' AND c.relname NOT LIKE 'pg!_temp!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'r' " + + "AND c.relname LIKE 'pg!_%' ESCAPE '!' " + + "AND c.relname NOT LIKE 'pg!_toast!_%' ESCAPE '!' " + + "AND c.relname NOT LIKE 'pg!_temp!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("SYSTEM TOAST TABLE", ht); - ht.put("SCHEMAS", "c.relkind = 'r' AND n.nspname = 'pg_toast'"); + ht.put("SCHEMAS", + "c.relkind OPERATOR(pg_catalog.=) 'r' " + + "AND n.nspname OPERATOR(pg_catalog.=) 'pg_toast'"); ht.put("NOSCHEMAS", - "c.relkind = 'r' AND c.relname LIKE 'pg!_toast!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'r' " + + "AND c.relname LIKE 'pg!_toast!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("SYSTEM TOAST INDEX", ht); - ht.put("SCHEMAS", "c.relkind = 'i' AND n.nspname = 'pg_toast'"); + ht.put("SCHEMAS", + "c.relkind OPERATOR(pg_catalog.=) 'i' " + + "AND n.nspname OPERATOR(pg_catalog.=) 'pg_toast'"); ht.put("NOSCHEMAS", - "c.relkind = 'i' AND c.relname LIKE 'pg!_toast!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'i' " + + "AND c.relname LIKE 'pg!_toast!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("SYSTEM VIEW", ht); ht.put("SCHEMAS", - "c.relkind = 'v' AND (n.nspname = 'pg_catalog' OR n.nspname = 'information_schema') "); - ht.put("NOSCHEMAS", "c.relkind = 'v' AND c.relname LIKE 'pg!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'v' AND (" + + " n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema') "); + ht.put("NOSCHEMAS", + "c.relkind OPERATOR(pg_catalog.=) 'v' " + + "AND c.relname LIKE 'pg!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("SYSTEM INDEX", ht); ht.put("SCHEMAS", - "c.relkind = 'i' AND (n.nspname = 'pg_catalog' OR n.nspname = 'information_schema') "); + "c.relkind OPERATOR(pg_catalog.=) 'i' AND (" + + " n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema') "); ht.put("NOSCHEMAS", - "c.relkind = 'v' AND c.relname LIKE 'pg!_%' ESCAPE '!' AND c.relname NOT LIKE 'pg!_toast!_%' ESCAPE '!' AND c.relname NOT LIKE 'pg!_temp!_%' ESCAPE '!'"); + "c.relkind OPERATOR(pg_catalog.=) 'i' " + + "AND c.relname LIKE 'pg!_%' ESCAPE '!' " + + "AND c.relname NOT LIKE 'pg!_toast!_%' ESCAPE '!' " + + "AND c.relname NOT LIKE 'pg!_temp!_%' ESCAPE '!'"); ht = new HashMap(); s_tableTypeClauses.put("TEMPORARY TABLE", ht); ht.put("SCHEMAS", - "c.relkind = 'r' AND n.nspname LIKE 'pg!_temp!_%' ESCAPE '!' "); + "c.relkind OPERATOR(pg_catalog.=) 'r' " + + "AND n.nspname LIKE 'pg!_temp!_%' ESCAPE '!' "); ht.put("NOSCHEMAS", - "c.relkind = 'r' AND c.relname LIKE 'pg!_temp!_%' ESCAPE '!' "); + "c.relkind OPERATOR(pg_catalog.=) 'r' " + + "AND c.relname LIKE 'pg!_temp!_%' ESCAPE '!' "); ht = new HashMap(); s_tableTypeClauses.put("TEMPORARY INDEX", ht); ht.put("SCHEMAS", - "c.relkind = 'i' AND n.nspname LIKE 'pg!_temp!_%' ESCAPE '!' "); + "c.relkind OPERATOR(pg_catalog.=) 'i' " + + "AND n.nspname LIKE 'pg!_temp!_%' ESCAPE '!' "); ht.put("NOSCHEMAS", - "c.relkind = 'i' AND c.relname LIKE 'pg!_temp!_%' ESCAPE '!' "); + "c.relkind OPERATOR(pg_catalog.=) 'i' " + + "AND c.relname LIKE 'pg!_temp!_%' ESCAPE '!' "); } // These are the default tables, used when NULL is passed to getTables @@ -1757,7 +1821,11 @@ public java.sql.ResultSet getTables(String catalog, String schemaPattern, */ public java.sql.ResultSet getSchemas() throws SQLException { - String sql = "SELECT nspname AS TABLE_SCHEM FROM pg_catalog.pg_namespace WHERE nspname <> 'pg_toast' AND nspname NOT LIKE 'pg!_temp!_%' ESCAPE '!' ORDER BY TABLE_SCHEM"; + String sql = + "SELECT nspname AS TABLE_SCHEM FROM pg_catalog.pg_namespace " + + "WHERE nspname OPERATOR(pg_catalog.<>) 'pg_toast' " + + "AND nspname NOT LIKE 'pg!_temp!_%' ESCAPE '!' " + + "ORDER BY TABLE_SCHEM"; return createMetaDataStatement().executeQuery(sql); } @@ -1866,17 +1934,29 @@ public java.sql.ResultSet getColumns(String catalog, String schemaPattern, f[17] = new ResultSetField("IS_NULLABLE", TypeOid.VARCHAR, getMaxNameLength()); - String sql = "SELECT n.nspname,c.relname,a.attname," - + " a.atttypid as atttypid,a.attnotnull,a.atttypmod," - + " a.attlen::int4 as attlen,a.attnum,def.adsrc,dsc.description " + String sql = "SELECT n.nspname, c.relname, a.attname," + + " a.atttypid as atttypid, a.attnotnull, a.atttypmod," + + " a.attlen::pg_catalog.int4 as attlen, a.attnum, def.adsrc," + + " dsc.description" + " FROM pg_catalog.pg_namespace n " - + " JOIN pg_catalog.pg_class c ON (c.relnamespace = n.oid) " - + " JOIN pg_catalog.pg_attribute a ON (a.attrelid=c.oid) " - + " LEFT JOIN pg_catalog.pg_attrdef def ON (a.attrelid=def.adrelid AND a.attnum = def.adnum) " - + " LEFT JOIN pg_catalog.pg_description dsc ON (c.oid=dsc.objoid AND a.attnum = dsc.objsubid) " - + " LEFT JOIN pg_catalog.pg_class dc ON (dc.oid=dsc.classoid AND dc.relname='pg_class') " - + " LEFT JOIN pg_catalog.pg_namespace dn ON (dc.relnamespace=dn.oid AND dn.nspname='pg_catalog') " - + " WHERE a.attnum > 0 AND NOT a.attisdropped " + + " JOIN pg_catalog.pg_class c" + + " ON (c.relnamespace OPERATOR(pg_catalog.=) n.oid) " + + " JOIN pg_catalog.pg_attribute a " + + " ON (a.attrelid OPERATOR(pg_catalog.=) c.oid) " + + " LEFT JOIN pg_catalog.pg_attrdef def ON (" + + " a.attrelid OPERATOR(pg_catalog.=) def.adrelid" + + " AND a.attnum OPERATOR(pg_catalog.=) def.adnum) " + + " LEFT JOIN pg_catalog.pg_description dsc ON (" + + " c.oid OPERATOR(pg_catalog.=) dsc.objoid" + + " AND a.attnum OPERATOR(pg_catalog.=) dsc.objsubid) " + + " LEFT JOIN pg_catalog.pg_class dc ON (" + + " dc.oid OPERATOR(pg_catalog.=) dsc.classoid" + + " AND dc.relname OPERATOR(pg_catalog.=) 'pg_class') " + + " LEFT JOIN pg_catalog.pg_namespace dn ON (" + + " dc.relnamespace OPERATOR(pg_catalog.=) dn.oid" + + " AND dn.nspname OPERATOR(pg_catalog.=) 'pg_catalog') " + + " WHERE a.attnum OPERATOR(pg_catalog.>) 0" + + " AND NOT a.attisdropped " + " AND " + resolveSchemaPatternCondition( "n.nspname", schemaPattern); @@ -2025,14 +2105,17 @@ public java.sql.ResultSet getColumnPrivileges(String catalog, String sql = "SELECT n.nspname,c.relname,u.usename,c.relacl,a.attname " + " FROM pg_catalog.pg_namespace n, pg_catalog.pg_class c, pg_catalog.pg_user u, pg_catalog.pg_attribute a " - + " WHERE c.relnamespace = n.oid " - + " AND u.usesysid = c.relowner " + " AND c.oid = a.attrelid " - + " AND c.relkind = 'r' " - + " AND a.attnum > 0 AND NOT a.attisdropped " + + " WHERE c.relnamespace OPERATOR(pg_catalog.=) n.oid " + + " AND u.usesysid OPERATOR(pg_catalog.=) c.relowner " + + " AND c.oid OPERATOR(pg_catalog.=) a.attrelid " + + " AND c.relkind OPERATOR(pg_catalog.=) 'r' " + + " AND a.attnum OPERATOR(pg_catalog.>) 0" + + " AND NOT a.attisdropped " + " AND " + resolveSchemaCondition( "n.nspname", schema); - sql += " AND c.relname = '" + escapeQuotes(table) + "' "; + sql += " AND c.relname OPERATOR(pg_catalog.=) '" + + escapeQuotes(table) + "' "; if(columnNamePattern != null && !"".equals(columnNamePattern)) { sql += " AND a.attname LIKE '" + escapeQuotes(columnNamePattern) @@ -2127,7 +2210,8 @@ public java.sql.ResultSet getTablePrivileges(String catalog, String sql = "SELECT n.nspname,c.relname,u.usename,c.relacl " + " FROM pg_catalog.pg_namespace n, pg_catalog.pg_class c, pg_catalog.pg_user u " + " WHERE c.relnamespace = n.oid " - + " AND u.usesysid = c.relowner " + " AND c.relkind = 'r' " + + " AND u.usesysid OPERATOR(pg_catalog.=) c.relowner " + + " AND c.relkind OPERATOR(pg_catalog.=) 'r' " + " AND " + resolveSchemaPatternCondition( "n.nspname", schemaPattern); @@ -2330,13 +2414,17 @@ public java.sql.ResultSet getBestRowIdentifier(String catalog, String where = ""; String from = " FROM pg_catalog.pg_namespace n, pg_catalog.pg_class ct, pg_catalog.pg_class ci, pg_catalog.pg_attribute a, pg_catalog.pg_index i "; - where = " AND ct.relnamespace = n.oid " + where = " AND ct.relnamespace OPERATOR(pg_catalog.=) n.oid " + " AND " + resolveSchemaCondition( "n.nspname", schema); String sql = "SELECT a.attname, a.atttypid as atttypid " + from - + " WHERE ct.oid=i.indrelid AND ci.oid=i.indexrelid " - + " AND a.attrelid=ci.oid AND i.indisprimary " - + " AND ct.relname = '" + escapeQuotes(table) + "' " + where + + " WHERE ct.oid OPERATOR(pg_catalog.=) i.indrelid " + + " AND ci.oid OPERATOR(pg_catalog.=) i.indexrelid " + + " AND a.attrelid OPERATOR(pg_catalog.=) ci.oid" + + " AND i.indisprimary " + + " AND ct.relname OPERATOR(pg_catalog.=) '" + + escapeQuotes(table) + "' " + + where + " ORDER BY a.attnum "; ResultSet rs = m_connection.createStatement().executeQuery(sql); @@ -2439,17 +2527,21 @@ public java.sql.ResultSet getPrimaryKeys(String catalog, String schema, String where = ""; String select = "SELECT NULL AS TABLE_CAT, n.nspname AS TABLE_SCHEM, "; from = " FROM pg_catalog.pg_namespace n, pg_catalog.pg_class ct, pg_catalog.pg_class ci, pg_catalog.pg_attribute a, pg_catalog.pg_index i "; - where = " AND ct.relnamespace = n.oid AND " + + where = " AND ct.relnamespace OPERATOR(pg_catalog.=) n.oid AND " + resolveSchemaCondition("n.nspname", schema); String sql = select + " ct.relname AS TABLE_NAME, " - + " a.attname AS COLUMN_NAME, " + " a.attnum::int2 AS KEY_SEQ, " + + " a.attname AS COLUMN_NAME," + + " a.attnum::pg_catalog.int2 AS KEY_SEQ, " + " ci.relname AS PK_NAME " + from - + " WHERE ct.oid=i.indrelid AND ci.oid=i.indexrelid " - + " AND a.attrelid=ci.oid AND i.indisprimary "; + + " WHERE ct.oid OPERATOR(pg_catalog.=) i.indrelid" + + " AND ci.oid OPERATOR(pg_catalog.=) i.indexrelid " + + " AND a.attrelid OPERATOR(pg_catalog.=) ci.oid" + + " AND i.indisprimary "; if(table != null && !"".equals(table)) { - sql += " AND ct.relname = '" + escapeQuotes(table) + "' "; + sql += " AND ct.relname OPERATOR(pg_catalog.=) '" + + escapeQuotes(table) + "' "; } sql += where + " ORDER BY table_name, pk_name, key_seq"; @@ -2505,33 +2597,38 @@ protected java.sql.ResultSet getImportedExportedKeys(String primaryCatalog, * multiple unique indexes covering the same keys can be created which * make it difficult to determine the PK_NAME field. */ - String sql = "SELECT NULL::text AS PKTABLE_CAT, pkn.nspname AS PKTABLE_SCHEM, pkc.relname AS PKTABLE_NAME, pka.attname AS PKCOLUMN_NAME, " - + "NULL::text AS FKTABLE_CAT, fkn.nspname AS FKTABLE_SCHEM, fkc.relname AS FKTABLE_NAME, fka.attname AS FKCOLUMN_NAME, " - + "pos.n::int2 AS KEY_SEQ, " + String sql = "SELECT " + + "NULL::pg_catalog.text AS PKTABLE_CAT, " + + "pkn.nspname AS PKTABLE_SCHEM, pkc.relname AS PKTABLE_NAME, " + + "pka.attname AS PKCOLUMN_NAME, " + + "NULL::pg_catalog.text AS FKTABLE_CAT, " + + "fkn.nspname AS FKTABLE_SCHEM, fkc.relname AS FKTABLE_NAME, " + + "fka.attname AS FKCOLUMN_NAME, " + + "pos.n::pg_catalog.int2 AS KEY_SEQ, " + "CASE con.confupdtype " - + " WHEN 'c' THEN " + + " WHEN 'c'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeyCascade - + " WHEN 'n' THEN " + + " WHEN 'n'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeySetNull - + " WHEN 'd' THEN " + + " WHEN 'd'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeySetDefault - + " WHEN 'r' THEN " + + " WHEN 'r'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeyRestrict - + " WHEN 'a' THEN " + + " WHEN 'a'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeyNoAction - + " ELSE NULL END::int2 AS UPDATE_RULE, " + + " ELSE NULL END::pg_catalog.int2 AS UPDATE_RULE, " + "CASE con.confdeltype " - + " WHEN 'c' THEN " + + " WHEN 'c'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeyCascade - + " WHEN 'n' THEN " + + " WHEN 'n'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeySetNull - + " WHEN 'd' THEN " + + " WHEN 'd'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeySetDefault - + " WHEN 'r' THEN " + + " WHEN 'r'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeyRestrict - + " WHEN 'a' THEN " + + " WHEN 'a'::pg_catalog.\"char\" THEN " + DatabaseMetaData.importedKeyNoAction - + " ELSE NULL END::int2 AS DELETE_RULE, " + + " ELSE NULL END::pg_catalog.int2 AS DELETE_RULE, " + "con.conname AS FK_NAME, pkic.relname AS PK_NAME, " + "CASE " + " WHEN con.condeferrable AND con.condeferred THEN " @@ -2540,27 +2637,42 @@ protected java.sql.ResultSet getImportedExportedKeys(String primaryCatalog, + DatabaseMetaData.importedKeyInitiallyImmediate + " ELSE " + DatabaseMetaData.importedKeyNotDeferrable - + " END::int2 AS DEFERRABILITY " + + " END::pg_catalog.int2 AS DEFERRABILITY " + " FROM " + " pg_catalog.pg_namespace pkn, pg_catalog.pg_class pkc, pg_catalog.pg_attribute pka, " + " pg_catalog.pg_namespace fkn, pg_catalog.pg_class fkc, pg_catalog.pg_attribute fka, " + " pg_catalog.pg_constraint con, " + " pg_catalog.generate_series(1, " + getMaxIndexKeys() + ") pos(n), " + " pg_catalog.pg_depend dep, pg_catalog.pg_class pkic " - + " WHERE pkn.oid = pkc.relnamespace AND pkc.oid = pka.attrelid AND pka.attnum = con.confkey[pos.n] AND con.confrelid = pkc.oid " - + " AND fkn.oid = fkc.relnamespace AND fkc.oid = fka.attrelid AND fka.attnum = con.conkey[pos.n] AND con.conrelid = fkc.oid " - + " AND con.contype = 'f' AND con.oid = dep.objid AND pkic.oid = dep.refobjid AND pkic.relkind = 'i' AND dep.classid = 'pg_constraint'::regclass::oid AND dep.refclassid = 'pg_class'::regclass::oid " + - " AND " + resolveSchemaCondition("pkn.nspname", primarySchema) + - " AND " + resolveSchemaCondition("fkn.nspname", foreignSchema); + + " WHERE pkn.oid OPERATOR(pg_catalog.=) pkc.relnamespace" + + " AND pkc.oid OPERATOR(pg_catalog.=) pka.attrelid" + + " AND pka.attnum OPERATOR(pg_catalog.=) con.confkey[pos.n]" + + " AND con.confrelid OPERATOR(pg_catalog.=) pkc.oid " + + " AND fkn.oid OPERATOR(pg_catalog.=) fkc.relnamespace" + + " AND fkc.oid OPERATOR(pg_catalog.=) fka.attrelid" + + " AND fka.attnum OPERATOR(pg_catalog.=) con.conkey[pos.n]" + + " AND con.conrelid OPERATOR(pg_catalog.=) fkc.oid " + + " AND con.contype OPERATOR(pg_catalog.=) 'f'" + + " AND con.oid OPERATOR(pg_catalog.=) dep.objid" + + " AND pkic.oid OPERATOR(pg_catalog.=) dep.refobjid" + + " AND pkic.relkind OPERATOR(pg_catalog.=) 'i'" + + " AND dep.classid OPERATOR(pg_catalog.=)" + + " 'pg_constraint'::pg_catalog.regclass::pg_catalog.oid" + + " AND dep.refclassid OPERATOR(pg_catalog.=)" + + " 'pg_class'::pg_catalog.regclass::pg_catalog.oid" + + " AND " + resolveSchemaCondition("pkn.nspname", primarySchema) + + " AND " + resolveSchemaCondition("fkn.nspname", foreignSchema); if(primaryTable != null && !"".equals(primaryTable)) { - sql += " AND pkc.relname = '" + escapeQuotes(primaryTable) + sql += " AND pkc.relname OPERATOR(pg_catalog.=) '" + + escapeQuotes(primaryTable) + "' "; } if(foreignTable != null && !"".equals(foreignTable)) { - sql += " AND fkc.relname = '" + escapeQuotes(foreignTable) + sql += " AND fkc.relname OPERATOR(pg_catalog.=) '" + + escapeQuotes(foreignTable) + "' "; } @@ -2755,7 +2867,9 @@ public java.sql.ResultSet getTypeInfo() throws SQLException f[16] = new ResultSetField("SQL_DATETIME_SUB", TypeOid.INT4, 4); f[17] = new ResultSetField("NUM_PREC_RADIX", TypeOid.INT4, 4); - String sql = "SELECT typname FROM pg_catalog.pg_type where typrelid = 0"; + String sql = + "SELECT typname FROM pg_catalog.pg_type " + + "WHERE typrelid OPERATOR(pg_catalog.=) 0"; ResultSet rs = m_connection.createStatement().executeQuery(sql); // cache some results, this will keep memory useage down, and speed @@ -2853,7 +2967,7 @@ public java.sql.ResultSet getIndexInfo(String catalog, String schema, String select = "SELECT NULL AS TABLE_CAT, n.nspname AS TABLE_SCHEM, "; String from = " FROM pg_catalog.pg_namespace n, pg_catalog.pg_class ct, pg_catalog.pg_class ci, pg_catalog.pg_index i, pg_catalog.pg_attribute a, pg_catalog.pg_am am "; String where = - " AND n.oid = ct.relnamespace " + + " AND n.oid OPERATOR(pg_catalog.=) ct.relnamespace " + " AND " + resolveSchemaCondition("n.nspname", schema); String sql = select @@ -2861,22 +2975,27 @@ public java.sql.ResultSet getIndexInfo(String catalog, String schema, + " CASE i.indisclustered " + " WHEN true THEN " + java.sql.DatabaseMetaData.tableIndexClustered - + " ELSE CASE am.amname " - + " WHEN 'hash' THEN " + + " ELSE CASE" + + " WHEN am.amname OPERATOR(pg_catalog.=) 'hash' THEN " + java.sql.DatabaseMetaData.tableIndexHashed + " ELSE " + java.sql.DatabaseMetaData.tableIndexOther + " END " - + " END::int2 AS TYPE, " - + " a.attnum::int2 AS ORDINAL_POSITION, " + + " END::pg_catalog.int2 AS TYPE, " + + " a.attnum::pg_catalog.int2 AS ORDINAL_POSITION, " + " a.attname AS COLUMN_NAME, " + " NULL AS ASC_OR_DESC, " + " ci.reltuples AS CARDINALITY, " + " ci.relpages AS PAGES, " + " NULL AS FILTER_CONDITION " + from - + " WHERE ct.oid=i.indrelid AND ci.oid=i.indexrelid AND a.attrelid=ci.oid AND ci.relam=am.oid " - + where + " AND ct.relname = '" + escapeQuotes(tableName) + "' "; + + " WHERE ct.oid OPERATOR(pg_catalog.=) i.indrelid" + + " AND ci.oid OPERATOR(pg_catalog.=) i.indexrelid" + + " AND a.attrelid OPERATOR(pg_catalog.=) ci.oid" + + " AND ci.relam OPERATOR(pg_catalog.=) am.oid " + + where + + " AND ct.relname OPERATOR(pg_catalog.=) '" + + escapeQuotes(tableName) + "' "; if(unique) { @@ -2987,26 +3106,35 @@ public boolean supportsBatchUpdates() throws SQLException public java.sql.ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { - String sql = "select " - + "null as type_cat, n.nspname as type_schem, t.typname as type_name, null as class_name, " - + "CASE WHEN t.typtype='c' then " - + java.sql.Types.STRUCT - + " else " - + java.sql.Types.DISTINCT - + " end as data_type, pg_catalog.obj_description(t.oid, 'pg_type') " - + "as remarks, CASE WHEN t.typtype = 'd' then (select CASE"; + String sql = + "SELECT" + + " null AS type_cat, n.nspname AS type_schem," + + " t.typname AS type_name, null AS class_name," + + " CASE WHEN t.typtype OPERATOR(pg_catalog.=) 'c' THEN " + + java.sql.Types.STRUCT + + " ELSE " + + java.sql.Types.DISTINCT + + " END AS data_type," + + " pg_catalog.obj_description(t.oid, 'pg_type') AS remarks," + + " CASE WHEN t.typtype OPERATOR(pg_catalog.=) 'd' THEN (" + + " select CASE"; for(int i = 0; i < SPIConnection.JDBC_TYPE_NAMES.length; i++) { - sql += " when typname = '" + SPIConnection.JDBC_TYPE_NUMBERS[i] - + "' then " + SPIConnection.JDBC_TYPE_NUMBERS[i]; + sql += " WHEN typname OPERATOR(pg_catalog.=) '" + + SPIConnection.JDBC_TYPE_NUMBERS[i] + + "' THEN " + SPIConnection.JDBC_TYPE_NUMBERS[i]; } - sql += " else " + sql += " ELSE " + java.sql.Types.OTHER - + " end from pg_type where oid=t.typbasetype) " - + "else null end as base_type " - + "from pg_catalog.pg_type t, pg_catalog.pg_namespace n where t.typnamespace = n.oid and n.nspname != 'pg_catalog' and n.nspname != 'pg_toast'"; + + " END" + + " FROM pg_type WHERE oid OPERATOR(pg_catalog.=) t.typbasetype) " + + "ELSE null END AS base_type " + + "FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n " + + "WHERE t.typnamespace OPERATOR(pg_catalog.=) n.oid " + + "AND n.nspname OPERATOR(pg_catalog.<>) 'pg_catalog' " + + "AND n.nspname OPERATOR(pg_catalog.<>) 'pg_toast'"; String toAdd = ""; if(types != null) @@ -3017,10 +3145,10 @@ public java.sql.ResultSet getUDTs(String catalog, String schemaPattern, switch(types[i]) { case java.sql.Types.STRUCT: - toAdd += " or t.typtype = 'c'"; + toAdd += " or t.typtype OPERATOR(pg_catalog.=) 'c'"; break; case java.sql.Types.DISTINCT: - toAdd += " or t.typtype = 'd'"; + toAdd += " or t.typtype OPERATOR(pg_catalog.=) 'd'"; break; } } @@ -3028,7 +3156,9 @@ public java.sql.ResultSet getUDTs(String catalog, String schemaPattern, } else { - toAdd += " and t.typtype IN ('c','d') "; + toAdd += + " AND t.typtype IN " + + "('c'::pg_catalog.\"char\", 'd'::pg_catalog.\"char\") "; } // spec says that if typeNamePattern is a fully qualified name // then the schema and catalog are ignored @@ -3112,7 +3242,7 @@ private Statement createMetaDataStatement() throws SQLException */ public boolean supportsSavepoints() throws SQLException { - return this.getDatabaseMajorVersion() >= 8; + return true; // PG < 8 no longer supported } /** From 2f592b1faa0c6a23981b7065a4853b02d9fb7785 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 2 Sep 2018 22:16:41 -0400 Subject: [PATCH 0180/1087] Axe NOSCHEMAS clauses and a bubble sort. All of those NOSCHEMAS clauses for DatabaseMetaData.getTable were intended to support PostgreSQL versions before schemas. They have never not been dead code in PL/Java's git history, and schemas came to PostgreSQL in 7.3, well before the 8.2 currently the back-compatibility horizon. Out with 'em. Also out with a little custom bubble sort for putting an array of String into natural order, much as Arrays.sort would. --- .../pljava/jdbc/SPIDatabaseMetaData.java | 114 +++--------------- 1 file changed, 20 insertions(+), 94 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index d27e0d6e..f0b9a722 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -26,6 +26,7 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.util.ArrayList; +import static java.util.Arrays.sort; import java.util.HashMap; import org.postgresql.pljava.internal.AclId; @@ -1695,12 +1696,9 @@ public java.sql.ResultSet getTables(String catalog, String schemaPattern, sql += " AND (false "; for(int i = 0; i < types.length; i++) { - HashMap clauses = (HashMap)s_tableTypeClauses.get(types[i]); - if(clauses != null) - { - String clause = (String)clauses.get(useSchemas); + String clause = s_tableTypeClauses.get(types[i]); + if(clause != null) sql += " OR ( " + clause + " ) "; - } } sql += ") "; sql += orderby; @@ -1708,104 +1706,48 @@ public java.sql.ResultSet getTables(String catalog, String schemaPattern, return createMetaDataStatement().executeQuery(sql); } - private static final HashMap s_tableTypeClauses; + private static final HashMap s_tableTypeClauses; static { - s_tableTypeClauses = new HashMap(); - HashMap ht = new HashMap(); - s_tableTypeClauses.put("TABLE", ht); - ht.put("SCHEMAS", + s_tableTypeClauses = new HashMap(); + s_tableTypeClauses.put("TABLE", "c.relkind OPERATOR(pg_catalog.=) 'r' " + "AND n.nspname NOT LIKE 'pg!_%' ESCAPE '!' " + "AND n.nspname OPERATOR(pg_catalog.<>) 'information_schema'"); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'r' " + - "AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("VIEW", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("VIEW", "c.relkind OPERATOR(pg_catalog.=) 'v' " + "AND n.nspname OPERATOR(pg_catalog.<>) 'pg_catalog' " + "AND n.nspname OPERATOR(pg_catalog.<>) 'information_schema'"); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'v' " + - "AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("INDEX", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("INDEX", "c.relkind OPERATOR(pg_catalog.=) 'i' " + "AND n.nspname NOT LIKE 'pg!_%' ESCAPE '!' " + "AND n.nspname OPERATOR(pg_catalog.<>) 'information_schema'"); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'i' " + - "AND c.relname NOT LIKE 'pg!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("SEQUENCE", ht); - ht.put("SCHEMAS", "c.relkind OPERATOR(pg_catalog.=) 'S'"); - ht.put("NOSCHEMAS", "c.relkind OPERATOR(pg_catalog.=) 'S'"); - ht = new HashMap(); - s_tableTypeClauses.put("SYSTEM TABLE", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("SEQUENCE", + "c.relkind OPERATOR(pg_catalog.=) 'S'"); + s_tableTypeClauses.put("SYSTEM TABLE", "c.relkind OPERATOR(pg_catalog.=) 'r' AND (" + " n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema')"); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'r' " + - "AND c.relname LIKE 'pg!_%' ESCAPE '!' " + - "AND c.relname NOT LIKE 'pg!_toast!_%' ESCAPE '!' " + - "AND c.relname NOT LIKE 'pg!_temp!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("SYSTEM TOAST TABLE", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("SYSTEM TOAST TABLE", "c.relkind OPERATOR(pg_catalog.=) 'r' " + "AND n.nspname OPERATOR(pg_catalog.=) 'pg_toast'"); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'r' " + - "AND c.relname LIKE 'pg!_toast!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("SYSTEM TOAST INDEX", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("SYSTEM TOAST INDEX", "c.relkind OPERATOR(pg_catalog.=) 'i' " + "AND n.nspname OPERATOR(pg_catalog.=) 'pg_toast'"); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'i' " + - "AND c.relname LIKE 'pg!_toast!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("SYSTEM VIEW", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("SYSTEM VIEW", "c.relkind OPERATOR(pg_catalog.=) 'v' AND (" + " n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema') "); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'v' " + - "AND c.relname LIKE 'pg!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("SYSTEM INDEX", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("SYSTEM INDEX", "c.relkind OPERATOR(pg_catalog.=) 'i' AND (" + " n.nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + " OR n.nspname OPERATOR(pg_catalog.=) 'information_schema') "); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'i' " + - "AND c.relname LIKE 'pg!_%' ESCAPE '!' " + - "AND c.relname NOT LIKE 'pg!_toast!_%' ESCAPE '!' " + - "AND c.relname NOT LIKE 'pg!_temp!_%' ESCAPE '!'"); - ht = new HashMap(); - s_tableTypeClauses.put("TEMPORARY TABLE", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("TEMPORARY TABLE", "c.relkind OPERATOR(pg_catalog.=) 'r' " + "AND n.nspname LIKE 'pg!_temp!_%' ESCAPE '!' "); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'r' " + - "AND c.relname LIKE 'pg!_temp!_%' ESCAPE '!' "); - ht = new HashMap(); - s_tableTypeClauses.put("TEMPORARY INDEX", ht); - ht.put("SCHEMAS", + s_tableTypeClauses.put("TEMPORARY INDEX", "c.relkind OPERATOR(pg_catalog.=) 'i' " + "AND n.nspname LIKE 'pg!_temp!_%' ESCAPE '!' "); - ht.put("NOSCHEMAS", - "c.relkind OPERATOR(pg_catalog.=) 'i' " + - "AND c.relname LIKE 'pg!_temp!_%' ESCAPE '!' "); } // These are the default tables, used when NULL is passed to getTables @@ -1851,7 +1793,7 @@ public java.sql.ResultSet getCatalogs() throws SQLException public java.sql.ResultSet getTableTypes() throws SQLException { String types[] = (String[])s_tableTypeClauses.keySet().toArray(new String[s_tableTypeClauses.size()]); - sortStringArray(types); + sort(types); ResultSetField f[] = new ResultSetField[1]; ArrayList v = new ArrayList(); @@ -2141,7 +2083,7 @@ public java.sql.ResultSet getColumnPrivileges(String catalog, acls = (String[])rs.getObject("relacl"); permissions = parseACL(acls, owner); permNames = (String[])permissions.keySet().toArray(new String[permissions.size()]); - sortStringArray(permNames); + sort(permNames); for(int i = 0; i < permNames.length; i++) { ArrayList grantees = (ArrayList)permissions.get(permNames[i]); @@ -2238,7 +2180,7 @@ public java.sql.ResultSet getTablePrivileges(String catalog, acls = (String[])rs.getObject("relacl"); permissions = parseACL(acls, owner); permNames = (String[])permissions.keySet().toArray(new String[permissions.size()]); - sortStringArray(permNames); + sort(permNames); for(int i = 0; i < permNames.length; i++) { ArrayList grantees = (ArrayList)permissions.get(permNames[i]); @@ -2263,22 +2205,6 @@ public java.sql.ResultSet getTablePrivileges(String catalog, return createSyntheticResultSet(f, v); } - private static void sortStringArray(String s[]) - { - for(int i = 0; i < s.length - 1; i++) - { - for(int j = i + 1; j < s.length; j++) - { - if(s[i].compareTo(s[j]) > 0) - { - String tmp = s[i]; - s[i] = s[j]; - s[j] = tmp; - } - } - } - } - /** * Add the user described by the given acl to the ArrayLists of users with the * privileges described by the acl. From 97627352a743f0d21d9367cce54a3d287b4dad52 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 2 Sep 2018 22:58:48 -0400 Subject: [PATCH 0181/1087] Cover schema-qualification in release notes. --- src/site/markdown/releasenotes.md.vm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 5f9619e9..c9aeab69 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -41,6 +41,18 @@ transition tables are available to triggers. $h3 Security +$h4 Schema-qualification + +PL/Java now more consistently schema-qualifies objects in queries and DDL +it generates internally, as a measure of defense-in-depth in case the database +it is installed in has not been [protected][prot1058] from [CVE-2018-1058][]. + +_No schema-qualification work has been done on the example code._ If the +examples jar will be installed, it should be in a database that +[the recommended steps have been taken to secure][prot1058]. + +$h4 Some large-object code removed + 1.5.1 removes the code at issue in [CVE-2016-0768][], which pertained to PostgreSQL large objects, but had never been documented or exposed as API. @@ -54,6 +66,9 @@ Developers wishing to manipulate large objects in PL/Java are able to do so using the SPI JDBC interface and the large-object SQL functions already available in every PostgreSQL version PL/Java currently supports. +[CVE-2018-1058]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1058 +[prot1058]: https://wiki.postgresql.org/wiki/A_Guide_to_CVE-2018-1058:_Protect_Your_Search_Path#Next_Steps:_How_Can_I_Protect_My_Databases.3F + $h3 Version compatibility PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11beta3, From ce8c0d866a4cf174d79b5bc81ef3e91b9ee0d1d6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 3 Sep 2018 21:18:54 -0400 Subject: [PATCH 0182/1087] One last type you mightn't think to schema-qualify --- .../main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index a1c17bca..0ee638a3 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -2505,7 +2505,7 @@ String getSQLType(TypeMirror tm, Element e, e, rslt, array, row, defaults, withDefault); if ( tm.getKind().equals( TypeKind.VOID) ) - return "void"; // can't be a parameter type so no defaults apply + return "pg_catalog.void"; // return type only; no defaults apply if ( tm.getKind().equals( TypeKind.ERROR) ) { From 4758f8905441af76a2909c0bc5662df7db644217 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 3 Sep 2018 13:58:02 -0400 Subject: [PATCH 0183/1087] Share PL/Java and application classes in OpenJ9. The OpenJ9 JVM has a facility for dynamically saving AOT-compiled classes in a shared cache, which is even available for classes of PL/Java application code, provided PL/Java's class loader does a bit of coordination with J9's class sharing API. The token generated for the J9 SharedClassTokenHelper is, for now, fixed, which will be bad news if a jar is updated. It should be computed in a way that ensures it is different after an update. --- .../org/postgresql/pljava/sqlj/Loader.java | 186 +++++++++++++++++- 1 file changed, 184 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 4347bce2..37377624 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -7,6 +7,9 @@ package org.postgresql.pljava.sqlj; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; import java.net.MalformedURLException; import java.net.URL; import java.sql.Connection; @@ -291,9 +294,10 @@ private static URL entryURL(int entryId) { super(parent); m_entries = entries; + m_j9Helper = ifJ9getHelper(); // null if not under OpenJ9 with sharing } - protected Class findClass(final String name) + protected Class findClass(final String name) throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); @@ -319,7 +323,18 @@ protected Class findClass(final String name) byte[] img = rs.getBytes(1); rs.close(); rs = null; - return this.defineClass(name, img, 0, img.length); + + String ifJ9token = null; + Object o = ifJ9findSharedClass(name); + if ( o instanceof byte[] ) + img = (byte[]) o; + else if ( o instanceof String ) + ifJ9token = (String)o; + + Class cls = this.defineClass(name, img, 0, img.length); + + ifJ9storeSharedClass(ifJ9token, cls); // noop for null token + return cls; } } catch(SQLException e) @@ -353,4 +368,171 @@ protected Enumeration findResources(String name) entryIds = new int[0]; return new EntryEnumeration(entryIds); } + + /* + * Detect and integrate with the OpenJ9 JVM class sharing facility. + * https://www.ibm.com/developerworks/library/j-class-sharing-openj9/#usingthehelperapi + * https://github.com/eclipse/openj9/blob/master/jcl/src/openj9.sharedclasses/share/classes/com/ibm/oti/shared/ + */ + + private static final Object s_j9HelperFactory; + private static final Method s_j9GetTokenHelper; + private static final Method s_j9FindSharedClass; + private static final Method s_j9StoreSharedClass; + private final Object m_j9Helper; + + /** + * Return an OpenJ9 {@code SharedClassTokenHelper} if running on an OpenJ9 + * JVM with sharing enabled; otherwise return null. + */ + private Object ifJ9getHelper() + { + if ( null == s_j9HelperFactory ) + return null; + try + { + return s_j9GetTokenHelper.invoke(s_j9HelperFactory, this); + } + catch ( IllegalAccessException e ) + { + throw new SecurityException(e); + } + catch ( InvocationTargetException ite ) + { + Throwable t = ite.getCause(); + if ( t instanceof Error ) + throw (Error)t; + if ( t instanceof RuntimeException ) + throw (RuntimeException)t; + throw new UndeclaredThrowableException(t, t.getMessage()); + } + } + + /** + * Find a class definition in the OpenJ9 shared cache (if running under + * OpenJ9, and sharing is enabled, and the class is there). + * @return null if not running under J9 with sharing; a {@code byte[]} if + * the class is found in the shared cache, or a {@code String} token that + * should be passed to {@code ifJ9storeSharedClass} later. + */ + private Object ifJ9findSharedClass(String className) + { + if ( null == m_j9Helper ) + return null; + + String token = "foo"; + + try + { + byte[] cookie = (byte[]) + s_j9FindSharedClass.invoke(m_j9Helper, token, className); + if ( null == cookie ) + return token; + return cookie; + } + catch ( IllegalAccessException e ) + { + throw new SecurityException(e); + } + catch ( InvocationTargetException ite ) + { + Throwable t = ite.getCause(); + if ( t instanceof Error ) + throw (Error)t; + if ( t instanceof RuntimeException ) + throw (RuntimeException)t; + throw new UndeclaredThrowableException(t, t.getMessage()); + } + } + + /** + * Store a newly-defined class in the OpenJ9 shared class cache if running + * under OpenJ9 with sharing enabled (implied if {@code token} is non-null, + * per the convention that its value came from {@code ifJ9findSharedClass}). + * @param token A token generated by {@code ifJ9findSharedClass}, non-null + * only if J9 sharing is active and the class is not already cached. This + * method is a noop if {@code token} is null. + * @param cls The newly-defined class. + */ + private void ifJ9storeSharedClass(String token, Class cls) + { + if ( null == token ) + return; + assert(null != m_j9Helper); + + try + { + s_j9StoreSharedClass.invoke(m_j9Helper, token, cls); + } + catch ( IllegalAccessException e ) + { + throw new SecurityException(e); + } + catch ( InvocationTargetException ite ) + { + Throwable t = ite.getCause(); + if ( t instanceof Error ) + throw (Error)t; + if ( t instanceof RuntimeException ) + throw (RuntimeException)t; + throw new UndeclaredThrowableException(t, t.getMessage()); + } + } + + /* + * Detect if this is an OpenJ9 JVM with sharing enabled, setting the related + * static fields for later reflective access to its sharing helpers if so. + */ + static + { + Object factory = null; + Method getHelper = null; + Method findShared = null; + Method storeShared = null; + + try + { + /* If this throws ClassNotFoundException, the JVM isn't OpenJ9. */ + Class shared = ClassLoader.getSystemClassLoader().loadClass( + "com.ibm.oti.shared.Shared"); + + Method getFactory = + shared.getMethod("getSharedClassHelperFactory", null); + + /* If getFactory returns null, sharing is not enabled. */ + factory = getFactory.invoke(null); + if ( null != factory ) + { + Class factoryClass = getFactory.getReturnType(); + getHelper = + factoryClass.getMethod("getTokenHelper", ClassLoader.class); + Class helperClass = getHelper.getReturnType(); + findShared = + helperClass.getMethod( + "findSharedClass", String.class, String.class); + storeShared = + helperClass.getMethod( + "storeSharedClass", String.class, Class.class); + } + } + catch ( ClassNotFoundException cnfe ) + { + /* Not running on an OpenJ9 JVM. Leave all the statics null. */ + } + catch ( RuntimeException rte ) + { + throw rte; + } + catch ( Exception e ) + { + throw new ExceptionInInitializerError(e); + } + finally + { + s_j9HelperFactory = factory; + s_j9GetTokenHelper = getHelper; + s_j9FindSharedClass = findShared; + s_j9StoreSharedClass = storeShared; + } + } } From bb3ecf19a78f8053160997bfda95b0f5c878fb10 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 3 Sep 2018 16:47:03 -0400 Subject: [PATCH 0184/1087] Derive a cache token from the entryId. This is dead simple, and workable because (in the current, 1.5.0 sqlj schema), the entryId is a SERIAL column, and all entries are deleted/reinserted by replace_jar. Of course this means that every cached class from the jar is invalidated even if few have changed, but optimizing that can be future work. --- .../pljava/management/Commands.java | 9 +++++ .../org/postgresql/pljava/sqlj/Loader.java | 33 ++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index e3ea1765..47f542d2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -251,6 +251,15 @@ * Schema-qualification of a type with a typmod, e.g. pg_catalog.varchar(100), * is possible from PostgreSQL 8.3 onward, but not in 8.2. As a compromise, use * the two-word CHARACTER VARYING syntax, to evade capture by a user type. + * + * In this (1.5.0) incarnation of the schema, jar_repository and jar_entry are + * both indexed by SERIAL columns. The replace_jar operation is an UPDATE to + * jar_repository (so the jar's id is preserved), but deletes and reinserts to + * jar_entry (so ALL classes get new ids). This makes the entryId sufficient as + * a class-cache token to ensure old cached versions are recognized as invalid + * (although at the cost of doing so for *every single class* in a jar even if + * many are unchanged). It is used that way in the cache-token construction in + * o.p.p.sqlj.Loader, which could need to be revisited if this behavior changes. */ @SQLAction(install={ " CREATE TABLE sqlj.jar_repository(" + diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 37377624..ac6e8ef0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -320,16 +320,26 @@ protected Class findClass(final String name) rs = stmt.executeQuery(); if(rs.next()) { - byte[] img = rs.getBytes(1); - rs.close(); - rs = null; + /* + * PostgreSQL found the entry tuple. It's possible that the + * OpenJ9 class-sharing hook below will find an alternate + * byte[] to use, so defer hauling the real one into Java + * memory from the ResultSet until we know we need it. + */ + byte[] img = null; String ifJ9token = null; - Object o = ifJ9findSharedClass(name); + Object o = ifJ9findSharedClass(name, entryId[0]); if ( o instanceof byte[] ) img = (byte[]) o; - else if ( o instanceof String ) - ifJ9token = (String)o; + else + { + img = rs.getBytes(1); /* Ok, we need it. */ + if ( o instanceof String ) + ifJ9token = (String) o; + } + rs.close(); + rs = null; Class cls = this.defineClass(name, img, 0, img.length); @@ -411,16 +421,23 @@ private Object ifJ9getHelper() /** * Find a class definition in the OpenJ9 shared cache (if running under * OpenJ9, and sharing is enabled, and the class is there). + * @param className name of the class to seek. + * @param tokenSource something passed by the caller from which we can + * generate a token that is sure to be different if the class has been + * updated. For now, just the int entryId, which is sufficient because that + * is a SERIAL column and entries are deleted/reinserted by replace_jar. + * There is just the one caller, so the type and usage of this parameter can + * be changed to whatever is appropriate should the schema evolve. * @return null if not running under J9 with sharing; a {@code byte[]} if * the class is found in the shared cache, or a {@code String} token that * should be passed to {@code ifJ9storeSharedClass} later. */ - private Object ifJ9findSharedClass(String className) + private Object ifJ9findSharedClass(String className, int tokenSource) { if ( null == m_j9Helper ) return null; - String token = "foo"; + String token = Integer.toString(tokenSource); try { From 1afe5adb415caa88389bc556a33eb4c0a778d48c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 3 Sep 2018 18:29:55 -0400 Subject: [PATCH 0185/1087] Document application-class sharing in OpenJ9. --- src/site/markdown/install/oj9vmopt.md | 71 ++++++++++++++++++++------- src/site/markdown/releasenotes.md.vm | 10 ++++ 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/site/markdown/install/oj9vmopt.md b/src/site/markdown/install/oj9vmopt.md index a2301182..fa24d2c7 100644 --- a/src/site/markdown/install/oj9vmopt.md +++ b/src/site/markdown/install/oj9vmopt.md @@ -20,9 +20,23 @@ OpenJ9 than in Hotspot, and is the subject of the rest of this page. OpenJ9 is an [alternative to the Hotspot JVM][hsj9] that is available in [OpenJDK][] (which can be downloaded with the choice of either JVM). -OpenJ9 includes a _dynamically managed_ class data sharing feature -comparable to Hotspot's [application class data sharing][iads], but -much less fuss to set up. +OpenJ9 includes a _dynamically managed_ class data sharing feature: it is +able to cache ahead-of-time compiled versions of classes in a file to be +sharably memory-mapped by all backends running PL/Java. The shared cache +significantly reduces both the aggregate memory footprint of multiple +backend JVMs and the per-JVM startup time. It is [described here][ej9cds]. + +The OpenJ9 class-sharing feature is similar to Hotspot's +[application class data sharing][iads], but with a major advantage in the +context of PL/Java: it is able to share not only classes of the Java runtime +itself and those on `pljava.classpath` (PL/Java's own internals), but also +classes from application jars loaded with `sqlj.install_jar`. The Hotspot +counterpart can share only the first two of those categories. + +OpenJ9 sharing is also free of the commercial-license encumbrance on the +Hotspot feature in Oracle Java 8 and later (OpenJDK with Hotspot also includes +the feature, without the encumbrance, but only from Java 10 on). +OpenJ9 sharing is also much less fuss to set up. To see how much less, the Hotspot setup is a manual, three-step affair to be done in advance of production use. You choose some code to run that you @@ -38,7 +52,7 @@ By contrast, you set up OpenJ9 to share classes with the following step: OpenJ9 will then, if the first time, create a shared archive and dynamically manage it, adding ahead-of-time-compiled versions of classes as they are -used in your application. The details are [described here][ej9cds]. +used in your application. [oj9opts]: https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.vm.80.doc/docs/x_jvm_commands.html [ej9cds]: https://www.ibm.com/developerworks/library/j-class-sharing-openj9/ @@ -46,6 +60,7 @@ used in your application. The details are [described here][ej9cds]. [vmop]: vmoptions.html [OpenJDK]: https://adoptopenjdk.net/ [hsj9]: https://www.eclipse.org/openj9/oj9_faq.html +[shclutil]: https://www.ibm.com/developerworks/library/j-class-sharing-openj9/#sharedclassesutilities ### Setup @@ -66,15 +81,29 @@ of Java on the same system. You could even, by saving different `pljava.vmoptions` settings per database or per user, arrange separate class caches for distinct applications using PL/Java. +All of the suboptions accepted by `-Xshareclasses` are listed [here][xsc]. + +[xsc]: https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.vm.80.doc/docs/xshareclasses.html + +#### Hotspot-like loaded-once-and-frozen class share, or dynamic one + If you wish to emulate the Hotspot class sharing feature where a shared class archive is created ahead of time and then frozen, you can let the application run for a while with the `-Xshareclasses` option not containing `readonly`, until the shared cache has been well warmed, and then add `readonly` to the -`-Xshareclasses` option saved in `pljava.vmoptions`. +`-Xshareclasses` option as saved in `pljava.vmoptions`. -All of the suboptions accepted by `-Xshareclasses` are listed [here][xsc]. +It will then be necessary (as it is with Hotspot) to expressly repeat the +process when new versions of the JRE or PL/Java are installed, or (unlike +Hotspot, which does not share them) application jars are updated. This is +not because OpenJ9 would continue loading the wrong versions from cache, +but because it would necessarily bypass the cache to load the current ones. -[xsc]: https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.vm.80.doc/docs/xshareclasses.html +If the `readonly` option is not used, the OpenJ9 shared cache will dynamically +cache new versions of classes as they are loaded. It does not, however, purge +older versions automatically. There are [shared classes utilities][shclutil] +available to monitor utilization of the cache space, and to reset caches if +needed. ### Java libraries @@ -82,19 +111,27 @@ If your own PL/Java code depends on other Java libraries distributed as jars, the usual recommendation would be to install those as well into the database with `sqlj.install_jar`, and use `sqlj.set_classpath` to make them available. That keeps everything handled uniformly within the database. +With OpenJ9 sharing, there is no downside to this approach, as classes +installed in the database are shared, just as those on the system classpath. -On the other hand, if you are building a shared archive, and some of the -dependency libraries are substantial, you could consider instead storing -those jars on the file system and naming them in `pljava.classpath`. Those -library classes can then also be included in the shared archive. - -Not everything from the original jar file can go into the shared archive. -After the archive has been built, the original jars still must be on the -file system and named in `pljava.classpath`. +### Thorough class verification When using class sharing, consider adding `-Xverify:all` to -the other VM options. Java sometimes applies more relaxed verification to +the other VM options, at least while warming a cache that you will then treat +as `readonly`. Java sometimes applies more relaxed verification to classes it loads from the system classpath. With class sharing in use, classes may be loaded and verified early, then saved in the shared archive for quick loading later. In those circumstances, the cost of requesting verification for -all classes may not be prohibitive. +all classes may not be prohibitive, while increasing robustness against damaged +class files. + +### Cache invalidation if database or PL/Java reinitialized + +The way that PL/Java's class loading currently integrates with OpenJ9 class +sharing relies on a PostgreSQL `SERIAL` column to distinguish updated versions +of classes loaded with `sqlj.install_jar`/`replace_jar`. + +If the database is recreated, PL/Java is deinstalled and reinstalled, or +anything else happens to restart the `SERIAL` sequence, it may be wise to +destroy any existing OpenJ9 class share, to avoid incorrectly matching +older cached versions of classes. diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index c9aeab69..aaaa3916 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -19,6 +19,16 @@ PL/Java before 1.5.1, the PL/Java version should first be upgraded in the running PostgreSQL version, and then the PostgreSQL `pg_upgrade` can be done. +The documentation is expanded on the topic of shared-memory precompiled +class cache features, which can substantially improve JVM startup time +and memory footprint, and are now available across Oracle Java, OpenJDK +with Hotspot, and OpenJDK with OpenJ9. When running on OpenJ9, PL/Java +cooperates with the JVM to include even the application's classes +(those loaded with `install_jar`) in the shared cache, something not +yet possible with Hotspot. While the advanced sharing feature in Oracle +Java is still subject to a commercial licensing encumbrance, the equivalent +(or superior, with OpenJ9) features in OpenJDK are not encumbered. + Significant new functionality includes new datatype mapping support: SQL `date`, `time`, and `timestamp` values can be mapped to the new Java classes of the `java.time` package in Java 8 and later (JSR 310 / From 900906d52be2dd75286d9e1d4f39b5b7403deeae Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 4 Sep 2018 08:36:13 -0400 Subject: [PATCH 0186/1087] Give a nod to the caching of JIT hints. --- src/site/markdown/install/oj9vmopt.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/site/markdown/install/oj9vmopt.md b/src/site/markdown/install/oj9vmopt.md index fa24d2c7..941fa1c7 100644 --- a/src/site/markdown/install/oj9vmopt.md +++ b/src/site/markdown/install/oj9vmopt.md @@ -105,6 +105,13 @@ older versions automatically. There are [shared classes utilities][shclutil] available to monitor utilization of the cache space, and to reset caches if needed. +With a dynamic shared cache, OpenJ9 may also continue to refine the shared +data even for unchanged classes that have already been cached. It does not +replace the originally cached representations, but over time can add JIT hints +based on profile data collected in longer-running processes, which can help +new, shorter-lived processes more quickly reach the same level of optimization +as key methods are just-in-time recompiled. + ### Java libraries If your own PL/Java code depends on other Java libraries distributed as From 5433cd5582adf4e808a57f64392675bbfd61ffa3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 4 Sep 2018 23:59:48 -0400 Subject: [PATCH 0187/1087] Avoid unnecessary jar_entry lookups. The choice to use entryId for generating the J9 class caching token enables a further optimization, as the entryId is known in advance so the token depends on nothing from the jar_entry row. The check in the class cache can be made before querying jar_entry at all. --- .../org/postgresql/pljava/sqlj/Loader.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index ac6e8ef0..10eede17 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -304,11 +304,29 @@ protected Class findClass(final String name) int[] entryId = (int[])m_entries.get(path); if(entryId != null) { + /* + * Check early whether running on OpenJ9 JVM and the shared cache + * has the class. It is possible this early because the entryId is + * being used to generate the token, and it is known before even + * doing the jar_entry query. It would be possible to use something + * like the row's xmin instead, in which case this test would have + * to be moved after retrieving the row. + * + * ifJ9findSharedClass can only return a byte[], a String, or null. + */ + Object o = ifJ9findSharedClass(name, entryId[0]); + if ( o instanceof byte[] ) + { + byte[] img = (byte[]) o; + return defineClass(name, img, 0, img.length); + } + String ifJ9token = (String) o; // used below when storing class + PreparedStatement stmt = null; ResultSet rs = null; try { - // This code rely heavily on the fact that the connection + // This code relies heavily on the fact that the connection // is a singleton and that the prepared statement will live // for the duration of the loader. // @@ -320,24 +338,7 @@ protected Class findClass(final String name) rs = stmt.executeQuery(); if(rs.next()) { - /* - * PostgreSQL found the entry tuple. It's possible that the - * OpenJ9 class-sharing hook below will find an alternate - * byte[] to use, so defer hauling the real one into Java - * memory from the ResultSet until we know we need it. - */ - byte[] img = null; - - String ifJ9token = null; - Object o = ifJ9findSharedClass(name, entryId[0]); - if ( o instanceof byte[] ) - img = (byte[]) o; - else - { - img = rs.getBytes(1); /* Ok, we need it. */ - if ( o instanceof String ) - ifJ9token = (String) o; - } + byte[] img = rs.getBytes(1); rs.close(); rs = null; From d4c94b5ec7007d515e9e47f19d30a5c7f5d6faba Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 5 Sep 2018 19:38:07 -0400 Subject: [PATCH 0188/1087] Document side-effect of 5433cd5. Checking the shared cache before querying for the jar_entry row saves some time, but also changes a behavior after a jar has been replaced. replace_jar clears the class loaders and function cache in the session where it is executed, but not in others; the historic (and still current) behavior is that they continue executing the old classes they have loaded, but will hit a ClassNotFoundException the moment another class needs to be loaded from the old jar. It is now possible they would keep right on executing the old code successfully, if the needed class is found in the shared cache. While that's a behavior change, it doesn't seem important to revert. There's nothing very documented or innately desirable about the old behavior anyway, and use of J9 sharing is opt-in. So just document it. In passing, also moderate the recommendation of -Xquickstart; it goes without saying that a sufficiently compute-heavy workload could see a net slowdown because of the less optimized code, but what's striking is how little effort that takes. So replace the recommendation with a more carefully hedged one. --- src/site/markdown/install/oj9vmopt.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/site/markdown/install/oj9vmopt.md b/src/site/markdown/install/oj9vmopt.md index 941fa1c7..440dfe04 100644 --- a/src/site/markdown/install/oj9vmopt.md +++ b/src/site/markdown/install/oj9vmopt.md @@ -4,10 +4,16 @@ The OpenJ9 JVM accepts a number of standard options that are the same as those accepted by Hotspot, but also many nonstandardized ones that are not. A complete list of options it accepts can be found [here][oj9opts]. -There is one option likely to benefit _every_ PL/Java configuration: +There is one option that should be considered for any PL/Java configuration: * [`-Xquickstart`][xqs] +It can reduce the JVM startup time by doing less JIT compilation and at lower +optimization levels. On the other hand, if the work to be done in PL/Java is +substantial enough, the increased run time of the less-optimized code can make +the overall performance effect net negative. It should be measured under +expected conditions. + [xqs]: https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.vm.80.doc/docs/xquickstart.html Beyond that, and the usual opportunities to adjust memory allocations and @@ -112,6 +118,20 @@ based on profile data collected in longer-running processes, which can help new, shorter-lived processes more quickly reach the same level of optimization as key methods are just-in-time recompiled. +### Effect of `sqlj.replace_jar` + +When PL/Java replaces a jar, the class loaders and cached function mappings +are reset in the backend that replaced the jar, so subsequent PL/Java function +calls in that backend will use the new classes. + +In other sessions active at the time the jar is replaced, without OpenJ9 class +sharing, execution will continue with the already-loaded classes, unless/until +another class needs to be loaded from the old jar, which will fail with a +`ClassNotFoundException`. + +With OpenJ9 class sharing, other sessions may continue executing even as they +load classes, as long as the old class versions are found in the shared cache. + ### Java libraries If your own PL/Java code depends on other Java libraries distributed as @@ -124,8 +144,8 @@ installed in the database are shared, just as those on the system classpath. ### Thorough class verification When using class sharing, consider adding `-Xverify:all` to -the other VM options, at least while warming a cache that you will then treat -as `readonly`. Java sometimes applies more relaxed verification to +the other VM options, perhaps once while warming a cache that you will then +treat as `readonly`. Java sometimes applies more relaxed verification to classes it loads from the system classpath. With class sharing in use, classes may be loaded and verified early, then saved in the shared archive for quick loading later. In those circumstances, the cost of requesting verification for From f76ae5ff9782c17b58bf3e3029a079f64fad0f05 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 5 Sep 2018 20:58:03 -0400 Subject: [PATCH 0189/1087] Add links to a performance-tuning wiki page. It should prove worthwhile to have a place on the wiki to accumulate more experience with effective option settings (especially for the less-well-studied OpenJ9) than happens to make it into these released docs. --- src/site/markdown/install/oj9vmopt.md | 8 ++++++++ src/site/markdown/install/vmoptions.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/site/markdown/install/oj9vmopt.md b/src/site/markdown/install/oj9vmopt.md index 440dfe04..8f626683 100644 --- a/src/site/markdown/install/oj9vmopt.md +++ b/src/site/markdown/install/oj9vmopt.md @@ -162,3 +162,11 @@ If the database is recreated, PL/Java is deinstalled and reinstalled, or anything else happens to restart the `SERIAL` sequence, it may be wise to destroy any existing OpenJ9 class share, to avoid incorrectly matching older cached versions of classes. + +### Performance tuning tips on the wiki + +Between releases of this documentation, breaking news, tips, and metrics +on PL/Java performance tuning may be shared +[on the performance-tuning wiki page][ptwp]. + +[ptwp]: https://github.com/tada/pljava/wiki/Performance-tuning diff --git a/src/site/markdown/install/vmoptions.md b/src/site/markdown/install/vmoptions.md index 6dc995dd..c6b7dcea 100644 --- a/src/site/markdown/install/vmoptions.md +++ b/src/site/markdown/install/vmoptions.md @@ -220,3 +220,11 @@ collection. In a test using PL/Java to do trivial work (nothing but `SELECT sqlj.get_classpath('public')`), the sweet spot comes around `-Xms5m` (which seems to end up allocating 6, but completes with no GC in my testing). + +### Performance tuning tips on the wiki + +Between releases of this documentation, breaking news, tips, and metrics +on PL/Java performance tuning may be shared +[on the performance-tuning wiki page][ptwp]. + +[ptwp]: https://github.com/tada/pljava/wiki/Performance-tuning From a5d4e5dd11c1e482f106477a38d932acf266d73b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 3 Sep 2018 20:04:11 -0400 Subject: [PATCH 0190/1087] Simplify by using JSR 310 date/time classes. They output and parse the same character format as the XML Schema types, and the local time/timestamp classes don't have the counterintuitive dependency on the session time zone. Also, the decision to introduce SQLXML as a non-compatibility-breaking change means it also has to be requested explicitly from the driver, just like the JSR 310 types. --- .../pljava/example/annotation/S9.java | 281 ++++++------------ 1 file changed, 96 insertions(+), 185 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java index f77b33b3..ff836ef6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java @@ -11,21 +11,16 @@ */ package org.postgresql.pljava.example.annotation; -import java.io.StringReader; - import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Connection; -import java.sql.Date; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import static java.sql.ResultSetMetaData.columnNoNulls; import java.sql.SQLXML; import java.sql.Statement; -import java.sql.Time; -import java.sql.Timestamp; import java.sql.Types; import java.sql.SQLException; @@ -33,20 +28,20 @@ import java.sql.SQLNonTransientException; import java.sql.SQLSyntaxErrorException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + import static java.util.Arrays.asList; import java.util.Collection; import java.util.Collections; -import java.util.GregorianCalendar; -import static java.util.GregorianCalendar.DST_OFFSET; -import static java.util.GregorianCalendar.ZONE_OFFSET; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Properties; -import java.util.TimeZone; -import static java.util.TimeZone.getTimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -57,8 +52,6 @@ import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI; import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE; -import javax.xml.transform.stream.StreamSource; - import static net.sf.saxon.om.NameChecker.isValidNCName; import net.sf.saxon.om.SequenceIterator; @@ -90,13 +83,9 @@ import net.sf.saxon.tree.iter.LookaheadIterator; -import net.sf.saxon.type.BuiltInAtomicType; -import net.sf.saxon.type.ValidationFailure; - import net.sf.saxon.value.Base64BinaryValue; -import static net.sf.saxon.value.CalendarValue.NO_TIMEZONE; -import net.sf.saxon.value.DateTimeValue; import net.sf.saxon.value.HexBinaryValue; +import net.sf.saxon.value.TimeValue; import org.postgresql.pljava.ResultSetProvider; @@ -227,14 +216,19 @@ enum Nulls { ABSENT, NIL }; * XML Schema thinks an ISO 8601 duration must have no sign * anywhere but at the very beginning before the P. PostgreSQL * thinks that's the one place a sign must never be, and instead - * it should appear in front of every numeric field. (It will - * accept input where the signs vary, but not produce such output.) + * it should appear in front of every numeric field. (PostgreSQL + * accepts input where the signs vary, and there are cases where it + * cannot be normalized away: P1M-1D is a thing, and can't be + * simplified until anchored at a date to know how long the month + * is! The XML Schema type simply can't represent that, so mapping + * of such a value must simply fail, as we'll ensure below.) * So, here's a regex with a capturing group for a leading -, and * one for any field-leading -, and one for the absence of a field- * leading -. Any PostgreSQL or XS duration ought to match overall, * but the capturing group matches should be either (f,f,t) or * (f,t,f) for a PostgreSQL duration, or either (f,f,t) or (t,f,t) - * for an XS duration. + * for an XS duration. (f,t,t) would be a PostgreSQL interval with + * mixed signs, and inconvertible. */ s_intervalSigns = Pattern.compile( "(-)?+(?:[PYMWDTH](?:(?:(-)|())\\d++)?+)++(?:(?:[.,]\\d*+)?+S)?+"); @@ -1014,7 +1008,7 @@ private static void xmlCastAsNonXML( XdmAtomicValue bv; ItemType xt = p.typeXS().getItemType(); if ( xt.matches(av) ) // perhaps we can skip one bout of casting - bv = av; + bv = av; // XXX xt==DATE_TIME, av isa DATE_TIME_STAMP gets through else throw new UnsupportedOperationException( "Casting AV to BV is not yet implemented (column " + col + ")"); @@ -1043,14 +1037,17 @@ else if ( ItemType.FLOAT.subsumes(xt) ) else if ( ItemType.DOUBLE.subsumes(xt) ) rs.updateObject(col, bv.getValue()); - else if ( ItemType.DATE.subsumes(xt) ) // try it, see what goes wrong - rs.updateString(col, bv.getStringValue()); + else if ( ItemType.DATE.subsumes(xt) ) + rs.updateObject(col, LocalDate.parse(bv.getStringValue())); else if ( ItemType.DATE_TIME_STAMP.subsumes(xt) ) - rs.updateObject(col, xdmTimestampToJDBC(bv, true)); + rs.updateObject(col, OffsetDateTime.parse(bv.getStringValue())); else if ( ItemType.DATE_TIME.subsumes(xt) ) - rs.updateObject(col, xdmTimestampToJDBC(bv, false)); + rs.updateObject(col, LocalDateTime.parse(bv.getStringValue())); else if ( ItemType.TIME.subsumes(xt) ) // no handy tz/notz distinction - rs.updateString(col, bv.getStringValue()); + rs.updateObject(col, + ((TimeValue)bv.getUnderlyingValue()).hasTimezone() + ? OffsetTime.parse(bv.getStringValue()) + : LocalTime.parse(bv.getStringValue())); else if ( ItemType.YEAR_MONTH_DURATION.subsumes(xt) ) rs.updateString(col, toggleIntervalRepr(bv.getStringValue())); @@ -1098,16 +1095,16 @@ private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( return new XdmAtomicValue((Boolean)dv); if ( ItemType.DATE.equals(xst) ) - return new XdmAtomicValue(((Date)dv).toString(), xst); + return new XdmAtomicValue(dv.toString(), xst); - if ( ItemType.TIME.equals(xst) ) // XXX with/without tz matters here... - return new XdmAtomicValue(((Time)dv).toString(), xst); + if ( ItemType.TIME.equals(xst) ) + return new XdmAtomicValue(dv.toString(), xst); if ( ItemType.DATE_TIME.equals(xst) ) - return jdbcTimestampToXdm(dv, false); + return new XdmAtomicValue(dv.toString(), xst); - if ( ItemType.DATE_TIME_STAMP.equals(xst) ) // XXX here too... - return jdbcTimestampToXdm(dv, true); + if ( ItemType.DATE_TIME_STAMP.equals(xst) ) + return new XdmAtomicValue(dv.toString(), xst); if ( ItemType.DURATION.equals(xst) ) return new XdmAtomicValue(toggleIntervalRepr((String)dv), xst); @@ -1117,153 +1114,6 @@ private static XdmAtomicValue mapJDBCofSQLvalueToXdmAtomicValue( "0N000"); } - /* - * The current (pre-JSR 310) behavior of PL/Java type mapping is to - * unconditionally return a java.sql.Timestamp instance from getObject, - * for both PostgreSQL types timestamp and timestamptz. - * - * In the timestamptz case, the instance directly reflects what PostgreSQL - * has stored, which is the originally-entered value adjusted to zone Z, - * and the most-faithful way to map that to xdm is as a DATE_TIME_STAMP - * with exactly those properties (there is no information available from - * which its original time zone of entry could be reconstructed). - * - * In the non-tz case, PL/Java has done something you might not expect, - * and adjusted the stored value to Z time by assuming it is a local time - * stored in PostgreSQL's current (as of this moment) session_timezone. - * (It does that because there's no such thing as a "local" - * java.sql.Timestamp. When PL/Java's JDBC implementation catches up to - * JSR 310, that'll provide an alternative that does less violence to the - * principle of least astonishment). - * - * Until then, the game here can only be to get PostgreSQL's - * session_timezone and adjust the JDBC value back! - */ - static XdmAtomicValue jdbcTimestampToXdm(Object v, boolean withTimeZone) - throws SQLException, XPathException - { - Timestamp ts = (Timestamp)v; - long millisFromEpoch = ts.getTime(); - int nanos = ts.getNanos(); - - /* - * The fractional second, down to milliseconds, is redundantly - * represented in millisFromEpoch and the high part of nanos. The nanos - * value is always positive, so needs to be (scaled and) subtracted from - * millisFromEpoch, leaving the latter (which is signed) a multiple of - * 1000, so the / 1000 will not have to round. - */ - long secsFromEpoch = (millisFromEpoch - nanos / 1000000) / 1000; - - DateTimeValue dtv; - - if ( withTimeZone ) - { - dtv = DateTimeValue.fromJavaInstant(secsFromEpoch, nanos); - /* fromJavaInstant implicitly assigns a time zone of Z */ - ValidationFailure vf = - dtv.convertToSubType(BuiltInAtomicType.DATE_TIME_STAMP); - if ( null != vf ) - throw vf.makeException(); - } - else - { - TimeZone tz = getSessionTimeZone(); - int offsetInMillis = tz.getOffset(millisFromEpoch); - secsFromEpoch += offsetInMillis / 1000; - // the following line brought to you by OCD - nanos += 1000000 * (offsetInMillis % 1000); - dtv = DateTimeValue.fromJavaInstant(secsFromEpoch, nanos); - dtv = (DateTimeValue)dtv.removeTimezone(); - } - - return makeAtomicValue(dtv); - } - - /* - * Undo what jdbcTimestampToXdm does, for the same variously astonishing - * reasons. - * - * There is an earlier step of the XMLCAST specification that (once it is - * implemented) will lead to surprising results here if the original xdm - * value being cast was with-time-zone but we are casting it to without. - * In that case, it will have been converted to UTC already before its - * timezone was dropped. The without-time-zone case here has to apply - * another "to-UTC" conversion, in this case from the PG session time zone, - * only because the JDBC driver will reverse that conversion. Such a value - * will end up in UTC, as the SQL/XML spec requires for a value that is - * cast from with time zone to without, and so is not really so surprising - * after all. If the value being cast is originally without time zone, that - * earlier step will not have altered it, and what is done here will be - * reversed by the JDBC driver, leaving the value as it appeared in xdm. - * - * In any case, because of that earlier step (once it's been implemented), - * it can be assumed that the value presented here has hasTimezone() if and - * only if withTimeZone is true. - * - * One wrinkle: because DATE_TIME_STAMP is a subtype of DATE_TIME, the - * current test to short-circuit that casting step can actually send us a - * DATE_TIME_STAMP here when expecting a DATE_TIME (withTimeZone false). - * We'll produce (if asserts are disabled ;) the right result if the thing's - * zone is Z, but not in general, so that special case will have to be - * handled when the earlier cast step gets implemented. - */ - static Object xdmTimestampToJDBC(XdmAtomicValue v, boolean withTimeZone) - throws SQLException, XPathException - { - DateTimeValue dtv = (DateTimeValue)v.getUnderlyingValue(); - assert dtv.hasTimezone() == withTimeZone; - - GregorianCalendar gc; - - if ( withTimeZone ) - { - dtv = dtv.adjustToUTC(NO_TIMEZONE); - gc = dtv.getCalendar(); - } - else - { - gc = dtv.getCalendar(); - gc.setTimeZone(getSessionTimeZone()); - gc.clear(DST_OFFSET); - gc.clear(ZONE_OFFSET); - } - - long millis = gc.getTimeInMillis(); - int micros = dtv.getMicrosecond(); - /* - * The fractional-second part, down to milliseconds, is redundantly - * represented in millis and the high digits of micros. That doesn't - * require special attention, because ts.setNanos() appears to be - * an overwrite. - */ - Timestamp ts = new Timestamp(millis); - ts.setNanos(1000 * micros); - return ts; - } - - /* - * Current PL/Java behavior is *not* to set Java's implicit time zone to - * match the PostgreSQL session timezone ... but to use it implicitly in the - * course of some value conversions. If we need to know it, we need to ask - * for it. For now, do this in the simplest way, and hope that at least the - * TimeZone class is doing some useful caching.... - */ - static TimeZone getSessionTimeZone() throws SQLException - { - Statement s = s_dbc.createStatement(); - try - { - ResultSet rs = s.executeQuery("SHOW TIME ZONE"); - rs.next(); - return getTimeZone(rs.getString(1)); - } - finally - { - s.close(); - } - } - /* * Toggle the lexical representation of an interval/duration between the * form PostgreSQL likes and the form XML Schema likes. Only negative values @@ -1355,9 +1205,17 @@ int typeJDBC() throws SQLException * not yet return those values. As a workaround until it does, * recheck here using the PG type name string, if TIME or TIMESTAMP * is the JDBC type that the driver returned. + * + * Also for backward compatibility, the driver still returns + * Types.OTHER for XML, rather than Types.SQLXML. Check and fix that + * here too. */ switch ( tj ) { + case Types.OTHER: + if ( "xml".equals(typePG()) ) + tj = Types.SQLXML; + break; case Types.TIME: if ( "timetz".equals(typePG()) ) tj = Types.TIME_WITH_TIMEZONE; @@ -1375,6 +1233,36 @@ Object valueJDBC() throws SQLException { if ( m_valueJDBCValid ) return m_valueJDBC; + /* + * When JDBC 4.2 added support for the JSR 310 date/time types, for + * back-compatibility purposes, it did not change what types a plain + * getObject(...) would return for them, which could break existing + * code. Instead, it's necessary to use the form of getObject that + * takes a Class, and ask for the new classes explicitly. + * + * Similarly, PL/Java up through 1.5.0 has always returned a String + * from getObject for a PostgreSQL xml type. Here, the JDBC standard + * provides that a SQLXML object should be returned, and that should + * happen in a future major PL/Java release, but for now, the plain + * getObject will still return String, so it is also necessary to + * ask for the SQLXML type explicitly. + */ + switch ( typeJDBC() ) + { + case Types.DATE: + return setValueJDBC(implValueJDBC(LocalDate.class)); + case Types.TIME: + return setValueJDBC(implValueJDBC(LocalTime.class)); + case Types.TIME_WITH_TIMEZONE: + return setValueJDBC(implValueJDBC(OffsetTime.class)); + case Types.TIMESTAMP: + return setValueJDBC(implValueJDBC(LocalDateTime.class)); + case Types.TIMESTAMP_WITH_TIMEZONE: + return setValueJDBC(implValueJDBC(OffsetDateTime.class)); + case Types.SQLXML: + return setValueJDBC(implValueJDBC(SQLXML.class)); + default: + } return setValueJDBC(implValueJDBC()); } @@ -1486,6 +1374,16 @@ protected Object implValueJDBC() throws SQLException "valueJDBC() on synthetic binding"); } + /* + * This implementation just forwards to the type-less version, then + * fails if that did not return the wanted type. Override if a smarter + * behavior is possible. + */ + protected T implValueJDBC(Class type) throws SQLException + { + return type.cast(implValueJDBC()); + } + protected SequenceType implTypeXS(boolean forContextItem) throws SQLException { @@ -1639,6 +1537,11 @@ protected Object implValueJDBC() throws SQLException { return m_resultSet.getObject(m_idx); } + + protected T implValueJDBC(Class type) throws SQLException + { + return m_resultSet.getObject(m_idx, type); + } } class Parameter extends Binding.Parameter @@ -1676,6 +1579,11 @@ protected Object implValueJDBC() throws SQLException { return m_resultSet.getObject(m_idx); } + + protected T implValueJDBC(Class type) throws SQLException + { + return m_resultSet.getObject(m_idx, type); + } } } @@ -1706,11 +1614,14 @@ static class ContextItem extends Binding.ContextItem ContextItem(ItemType it) { m_typeXS = it; - } - - protected int implTypeJDBC() throws SQLException - { - return Types.OTHER; // anything canCastAsXmlSequence won't toss + /* + * There needs to be a dummy JDBC type to return when queried + * for purposes of assertCanCastAsXmlSequence. It can literally + * be any type outside of the few that method rejects. Because + * the XS type is already known, nothing else will need to ask + * for this, or care. + */ + m_typeJDBC = Types.OTHER; } } } From bec61306b24a95826b1cf0f2e476f579f9e412fa Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 4 Sep 2018 01:20:26 -0400 Subject: [PATCH 0191/1087] Include Saxon example, but not built by default. Add a Maven profile -Psaxon-examples that will pull in Saxon-HE as a dependency and build the Saxon example, which (unlike the rest of the PL/Java sources) depends on Java 8. In passing, update the version of Saxon-HE in the dependency. For now, to avoid javadoc conniptions when the Saxon dependency isn't present, exclude the saxon example from javadoc unconditionally. Maybe there is a way to tie that also to the profile activation, later. --- pljava-examples/pom.xml | 53 +++++++++++++++++-- .../example/{annotation => saxon}/S9.java | 2 +- 2 files changed, 49 insertions(+), 6 deletions(-) rename pljava-examples/src/main/java/org/postgresql/pljava/example/{annotation => saxon}/S9.java (99%) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 5070544d..ec738a3b 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -9,20 +9,52 @@ pljava-examples PL/Java examples Examples of Java stored procedures using PL/Java + + + + saxon-examples + + + net.sf.saxon + Saxon-HE + [9.8.0-14,) + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org/postgresql/pljava/example/saxon/*.java + + + + + + + + org.postgresql pljava-api ${project.version} - - net.sf.saxon - Saxon-HE - 9.8.0-12 - + + org.apache.maven.plugins + maven-compiler-plugin + + + org/postgresql/pljava/example/*.java + org/postgresql/pljava/example/annotation/*.java + + + org.apache.maven.plugins maven-jar-plugin @@ -35,4 +67,15 @@ + + + + org.apache.maven.plugins + maven-javadoc-plugin + + org.postgresql.pljava.example.saxon + + + + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java similarity index 99% rename from pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java rename to pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java index ff836ef6..a6bd1d83 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java @@ -9,7 +9,7 @@ * Contributors: * Chapman Flack */ -package org.postgresql.pljava.example.annotation; +package org.postgresql.pljava.example.saxon; import java.math.BigDecimal; import java.math.BigInteger; From 6d3b222629a88395159b972bdd9c6efaa586a6cd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 7 Sep 2018 00:03:34 -0400 Subject: [PATCH 0192/1087] Document the optionally-built Saxon example code. Copy in a bunch of the javadoc, as the javadoc (for now) is unconditionally excluded from the build. Remove the column-definition-list alias it seems I unnecessarily included in the original javadoc. --- .../postgresql/pljava/example/saxon/S9.java | 2 +- pljava-examples/src/site/markdown/index.md | 17 ++ src/site/markdown/examples/examples.md.vm | 26 +++ src/site/markdown/examples/saxon.md | 165 ++++++++++++++++++ src/site/markdown/releasenotes.md.vm | 6 + 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/site/markdown/examples/saxon.md diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java index a6bd1d83..b08e2804 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java @@ -168,7 +168,7 @@ * 'concat(SIZE[@unit ne "sq_km"], " ", SIZE[@unit ne "sq_km"]/@unit)', * 'let $e := zero-or-one(PREMIER_NAME)/string() * return if ( empty($e) )then $DPREMIER else $e' - * ]) AS "xmltable" ( + * ]) AS ( * id int, ordinality int, "COUNTRY_NAME" text, country_id text, * size_sq_km float, size_other text, premier_name text * ); diff --git a/pljava-examples/src/site/markdown/index.md b/pljava-examples/src/site/markdown/index.md index 36bd108b..1591ea94 100644 --- a/pljava-examples/src/site/markdown/index.md +++ b/pljava-examples/src/site/markdown/index.md @@ -9,6 +9,23 @@ If you arrived here from a search for PL/Java examples, you probably want (Note: the source browser link shows the current development sources, which may differ from a particular release.) +### Optionally-built example code for XML processing with Saxon + +The [optional example code][exsaxon] for providing actual XML Query-based +alternatives to PostgreSQL's XPath 1.0-based query and `XMLTABLE` functions +does not get built by default, because it pulls in the sizeable [Saxon-HE][] +library from Saxonica, and because (unlike the rest of PL/Java) it requires +Java 8. + +To include these optional functions when building the examples, be sure to use +a Java 8 build environment, and add `-Psaxon-examples` to the `mvn` command +line. The functions are [documented here][exsaxon]. + + [esug]: ../examples/examples.html [tbtes]: https://github.com/tada/pljava/tree/master/pljava-examples/src/main/java/org/postgresql/pljava/example [rtj]: apidocs/index.html +[appcds]: ../install/appcds.html +[j9cds]: ../install/oj9vmopt.html#How_to_set_up_class_sharing_in_OpenJ9 +[Saxon-HE]: http://www.saxonica.com/html/products/products.html +[exsaxon]: ../examples/saxon.html diff --git a/src/site/markdown/examples/examples.md.vm b/src/site/markdown/examples/examples.md.vm index e0fd6a4a..ad1cef14 100644 --- a/src/site/markdown/examples/examples.md.vm +++ b/src/site/markdown/examples/examples.md.vm @@ -95,3 +95,29 @@ Then use some example function: ------------- 4 ``` + +$h2 Optionally-built example code + +Some example code is included in source form but not built by default, +because it assumes a later Java version than PL/Java should require to build, +or pulls in extra libraries, or both. Optional examples can be built by adding +corresponding `-P` profile options to the `mvn` command line when building. + +$h3 Using the Saxon XML-processing library for true `XMLQUERY` and `XMLTABLE` + +In the source directory `org/postgresql/pljava/example/saxon` is example code +for XML processing functions similar to `XMLQUERY` and `XMLTABLE` but using +the XQuery language as the SQL/XML standard actually specifies (in contrast +to the similar functions built into PostgreSQL, which support only XPath, +and XPath 1.0, at that). + +This code is not built by default because it pulls in the sizeable [Saxon-HE][] +library from Saxonica, and because it requires Java 8. + +To include these optional functions when building the examples, be sure to use +a Java 8 build environment, and add `-Psaxon-examples` to the `mvn` command +line. + +The Saxon example [documentation is here](../examples/saxon.html). + +[Saxon-HE]: http://www.saxonica.com/html/products/products.html diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md new file mode 100644 index 00000000..942a7788 --- /dev/null +++ b/src/site/markdown/examples/saxon.md @@ -0,0 +1,165 @@ +## Optionally-built example code for XML processing with Saxon + +In the source directory `org/postgresql/pljava/example/saxon` is example code +for XML processing functions similar to `XMLQUERY` and `XMLTABLE` but using +the XQuery language as the SQL/XML standard actually specifies (in contrast +to the similar functions built into PostgreSQL, which support only XPath, +and XPath 1.0, at that). + +This code is not built by default, because it pulls in the sizeable [Saxon-HE][] +library from Saxonica, and because (unlike the rest of PL/Java) it requires +Java 8. + +To include these optional functions when building the examples, be sure to use +a Java 8 build environment, and add `-Psaxon-examples` to the `mvn` command +line. + +### Using the Saxon examples + +The simplest installation method is to use `sqlj.install_jar` twice, once to +install the PL/Java examples jar in the usual way (perhaps with the name `ex` +and with `deploy => true`), and once to install (perhaps with the name `saxon`) +the Saxon-HE jar that Maven will have downloaded during the build. That jar +will be found in your Maven repository (likely `~/.m2/repository/` unless you +have directed it elsewhere) below the path `net/sf/saxon`. + +Then use `sqlj.set_classpath` to set a path including both jars (`'ex:saxon'` if +you used the names suggested above). + +This is work-in-progress code, currently incomplete, and for purposes of +example. + +### Calling XML functions without SQL syntactic sugar + +The XML querying and `XMLTABLE` functions built into PostgreSQL get special +treatment from the SQL parser to give them syntax that is more SQLish. + +The functions provided here have to work as ordinary SQL user-defined +functions, so calls to them can look a bit more verbose when written out +in SQL, but in a way that can be recognized as a straightforward rewriting +of the SQLish standard syntax. + +For example, suppose there is a table `catalog_as_xml` with a single row +whose `x` column is a (respectably sized) XML document recording the stuff +in `pg_catalog`. It could be created like this: + + CREATE TABLE catalog_as_xml(x) AS + SELECT schema_to_xml('pg_catalog', false, true, ''); + +#### An `XMLQUERY`-like function + +In the syntax of the SQL/XML standard, here is a query that would return an XML +element representing the declaration of the function with the name +`numeric_avg` (if PostgreSQL really had the standard `XMLQUERY` function built +in): + + SELECT XMLQUERY('/pg_catalog/pg_proc[proname eq $NAME]' + PASSING BY VALUE x, 'numeric_avg' AS NAME + RETURNING CONTENT EMPTY ON EMPTY) + FROM catalog_as_xml; + +It binds the 'context item' of the query to `x`, and the `NAME` +parameter to the given value, then evaluates the query and returns XML +"CONTENT" (a tree structure with a document node at the root, but not +necessarily meeting all the requirements of an XML "DOCUMENT"). It can be +rewritten as this call to the `xq_ret_content` method provided here: + + SELECT javatest.xq_ret_content('/pg_catalog/pg_proc[proname eq $NAME]', + PASSING => p, nullOnEmpty => false) + FROM catalog_as_xml, + LATERAL (SELECT x AS ".", 'numeric_avg' AS NAME) AS p; + +In the rewritten form, the type of value returned is determined by which +function is called, and the parameters to pass to the query are moved out to +a separate `SELECT` that supplies their values, types, and names (with +the context item now given the name ".") and is passed by its alias into the +query function. + +In the standard, parameters and results (of XML types) can be passed +`BY VALUE` or `BY REF`, where the latter means that the same +nodes will retain their XQuery node identities over calls (note that this is +a meaning unrelated to what "by value" and "by reference" usually mean in +PostgreSQL's documentation). PostgreSQL's implementation of the XML type +provides no way for `BY REF` semantics to be implemented, so everything +happening here happens `BY VALUE` implicitly, and does not need to be +specified. + +#### An `XMLTABLE`-like function + +The function `xmltable` here implements (much of) the +standard function of the same name. Because it is the same name, it has to +be either schema-qualified or double-quoted in a call to avoid confusion +with the reserved word. A rewritten form of the +[first example in the PostgreSQL manual][xmltex1] could be: + + SELECT xmltable.* + FROM + xmldata, + + LATERAL (SELECT data AS ".", 'not specified'::text AS DPREMIER) AS p, + + "xmltable"('//ROWS/ROW', PASSING => p, COLUMNS => ARRAY[ + 'xs:int(@id)', null, 'string(COUNTRY_NAME)', + 'string(COUNTRY_ID)', 'xs:double(SIZE[@unit eq "sq_km"])', + 'concat(SIZE[@unit ne "sq_km"], " ", SIZE[@unit ne "sq_km"]/@unit)', + 'let $e := zero-or-one(PREMIER_NAME)/string() + return if ( empty($e) )then $DPREMIER else $e' + ]) AS ( + id int, ordinality int, "COUNTRY_NAME" text, country_id text, + size_sq_km float, size_other text, premier_name text + ); + +[xmltex1]: https://www.postgresql.org/docs/10/static/functions-xml.html#FUNCTIONS-XML-PROCESSING-XMLTABLE + +Again, the context item and a parameter (here the desired default value for +`PREMIER`, passed in as the parameter `DPREMIER`) are supplied by a separate +query producing the row `p` that is given as `"xmltable"`'s `PASSING` argument. +The result column names and types are now specified in the `AS` list following +the function call, and the column XML Query expressions are supplied as the +`COLUMNS` array. The array must have length equal to the result column `AS` +list (there is no defaulting an omitted column expression to an element test +using the column's name, as there is in the standard function). The array is +allowed to have one null element, marking that column `FOR ORDINALITY`. + +The explicit casts and `zero-or-one` will not be needed once the +full automatic casting rules (for now only partially implemented) are +in place. The default, as shown, is handled by passing the desired default +value as a parameter and rewriting the column expression to apply it in place +of an empty sequence. This lacks, for now, some functionality of the standard +`XMLTABLE`, where the default expression can refer to other columns of the +same output row. + +### Minimizing startup time + +Saxon is a large library, and benefits greatly from precompilation into a +memory-mappable persistent cache, using the +[application class data sharing][appcds] feature in Oracle Java or in +OpenJDK with Hotspot, or the [class sharing][j9cds] feature in OpenJDK with +OpenJ9. + +The OpenJ9 feature is simpler to set up. Because it can cache classes straight +from PL/Java installed jars, the setup can be done exactly as described above, +and the OpenJ9 class sharing, if enabled, will just work. OpenJ9 class-sharing +setup [instructions are here][j9cds]. + +The Hotspot `AppCDS` feature is more work to set up, and can only cache classes +on the JVM system classpath, so the Saxon jar would have to be installed on +the filesystem and named in `pljava.classpath` instead of simply installing it +in PL/Java. It also needs to be stripped of its `jarsigner` metadata, which the +Hotspot `AppCDS` can't handle. Hotspot `AppCDS` setup +[instructions are here][appcds]. + +A comparison shown on the PL/Java [performance-tuning wiki page][ptwp] appears +to give Hotspot a significant advantage for a Saxon-heavy workload, so the more +complex Hotspot setup may remain worthwhile as long as that comparison holds. + +The `AppCDS` feature in Oracle Java is still (when last checked) a commercial +feature, not to be used in production without a specific license from Oracle. +OpenJDK, as of Java 10, ships Hotspot with the same feature included, without +the encumbrance. + + +[appcds]: ../install/appcds.html +[j9cds]: ../install/oj9vmopt.html#How_to_set_up_class_sharing_in_OpenJ9 +[Saxon-HE]: http://www.saxonica.com/html/products/products.html +[ptwp]: https://github.com/tada/pljava/wiki/Performance-tuning diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index aaaa3916..4abb12ba 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -206,7 +206,13 @@ the forms of `getObject / readObject` with a `Class` parameter and passing `SQLXML.class`, must be used. A [documentation page](use/sqlxml.html) has been added, and the [PassXML example][exxml] illustrates use of the API. +A [not-built-by-default new example][exsaxon] (because it depends on Java 8 and +the Saxon-HE XML-processing library) provides a partial implementation of true +`XMLQUERY` and `XMLTABLE` functions for PostgreSQL, using the standard-specified +XML Query language rather than the XPath 1.0 of the native PostgreSQL functions. + [exxml]: pljava-examples/apidocs/index.html?org/postgresql/pljava/example/annotation/PassXML.html +[exsaxon]: examples/saxon.html $h4 New Java property exposes the PostgreSQL server character-set encoding From 79b19ece4efa523bd90e66d73655206ee0bf2011 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 7 Sep 2018 21:30:02 -0400 Subject: [PATCH 0193/1087] Send reader to tuning page for AppCDS setup details. The details of getting Hotspot to share Saxon classes are too fiddly to devote the space to in general docs, but are covered in all their fiddly glory in a methodology section on the performance-tuning wiki page, so direct the reader there to learn all about them. A bit of other minor copyediting. --- src/site/markdown/examples/saxon.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md index 942a7788..81d4022c 100644 --- a/src/site/markdown/examples/saxon.md +++ b/src/site/markdown/examples/saxon.md @@ -32,7 +32,8 @@ example. ### Calling XML functions without SQL syntactic sugar The XML querying and `XMLTABLE` functions built into PostgreSQL get special -treatment from the SQL parser to give them syntax that is more SQLish. +treatment from the SQL parser to give them syntax that is more SQLish than +an ordinary function call. The functions provided here have to work as ordinary SQL user-defined functions, so calls to them can look a bit more verbose when written out @@ -103,7 +104,7 @@ with the reserved word. A rewritten form of the 'string(COUNTRY_ID)', 'xs:double(SIZE[@unit eq "sq_km"])', 'concat(SIZE[@unit ne "sq_km"], " ", SIZE[@unit ne "sq_km"]/@unit)', 'let $e := zero-or-one(PREMIER_NAME)/string() - return if ( empty($e) )then $DPREMIER else $e' + return if ( empty($e) ) then $DPREMIER else $e' ]) AS ( id int, ordinality int, "COUNTRY_NAME" text, country_id text, size_sq_km float, size_other text, premier_name text @@ -147,9 +148,11 @@ on the JVM system classpath, so the Saxon jar would have to be installed on the filesystem and named in `pljava.classpath` instead of simply installing it in PL/Java. It also needs to be stripped of its `jarsigner` metadata, which the Hotspot `AppCDS` can't handle. Hotspot `AppCDS` setup -[instructions are here][appcds]. +[general instructions are here][appcds], and specific details for setting up +this example for `AppCDS` can be found on the +[performance-tuning wiki page][ptwp] in the section devoted to it. -A comparison shown on the PL/Java [performance-tuning wiki page][ptwp] appears +A comparison shown on that performance-tuning page appears to give Hotspot a significant advantage for a Saxon-heavy workload, so the more complex Hotspot setup may remain worthwhile as long as that comparison holds. From 7b757d8d05292445de3a911da9da30603e7431d6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 7 Sep 2018 22:44:32 -0400 Subject: [PATCH 0194/1087] Add advice about uppercasing parameter names. The toUppercase() in SPIResultSetMetaData.getColumnLabel() seems to be required by nothing in JDBC that I can find. I see nothing like it in pgjdbc. But it has been in PL/Java since 2005. It can't be just yanked out abruptly, but it's got to be deprecated and removed in some future release. Therefore, advise not to rely on the behavior for parameter names passed into XQuery here, but to really, for the time being, spell them in all upper case, both in the XQuery text and in the SQL. In PostgreSQL, of course, that also means they have to be quoted in the SQL. --- .../postgresql/pljava/example/saxon/S9.java | 31 ++++++++++++++----- src/site/markdown/examples/saxon.md | 28 ++++++++++++++--- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java index b08e2804..83ca56ed 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java @@ -116,8 +116,8 @@ * an XML element representing the declaration of a function with a specified * name: *

    - * SELECT XMLQUERY('/pg_catalog/pg_proc[proname eq $NAME]'
    - *                 PASSING BY VALUE x, 'numeric_avg' AS NAME
    + * SELECT XMLQUERY('/pg_catalog/pg_proc[proname eq $FUNCNAME]'
    + *                 PASSING BY VALUE x, 'numeric_avg' AS FUNCNAME
      *                 RETURNING CONTENT EMPTY ON EMPTY)
      * FROM catalog_as_xml;
      *
    @@ -128,10 +128,10 @@ * necessarily meeting all the requirements of an XML "DOCUMENT"). It can be * rewritten as this call to the {@link #xq_ret_content xq_ret_content} method: *
    - * SELECT javatest.xq_ret_content('/pg_catalog/pg_proc[proname eq $NAME]',
    + * SELECT javatest.xq_ret_content('/pg_catalog/pg_proc[proname eq $FUNCNAME]',
      *                                PASSING => p, nullOnEmpty => false)
      * FROM catalog_as_xml,
    - * LATERAL (SELECT x AS ".", 'numeric_avg' AS NAME) AS p;
    + * LATERAL (SELECT x AS ".", 'numeric_avg' AS "FUNCNAME") AS p;
      *
    *

    * In the rewritten form, the type of value returned is determined by which @@ -140,6 +140,16 @@ * the context item now given the name ".") and is passed by its alias into the * query function. *

    + * Because of an unconditional uppercasing that PL/Java's JDBC driver currently + * applies to column names, any parameter names, such as `FUNCNAME` above, must + * be spelled in uppercase where used in the XQuery text, or they will not be + * recognized. Because the unconditional uppercasing is highly likely to be + * dropped in a future PL/Java release, it is wisest until then to use only + * parameter names that really are uppercase, both in the XQuery text where they + * are used and in the SQL expression that supplies them. In PostgreSQL, + * identifiers that are not quoted are _lower_cased, so they must be both + * uppercase and quoted, in the SQL syntax, to be truly uppercase. + *

    * In the standard, parameters and results (of XML types) can be passed * {@code BY VALUE} or {@code BY REF}, where the latter means that the same * nodes will retain their XQuery node identities over calls (note that this is @@ -160,7 +170,7 @@ * FROM * xmldata, * - * LATERAL (SELECT data AS ".", 'not specified'::text AS DPREMIER) AS p, + * LATERAL (SELECT data AS ".", 'not specified'::text AS "DPREMIER") AS p, * * "xmltable"('//ROWS/ROW', PASSING => p, COLUMNS => ARRAY[ * 'xs:int(@id)', null, 'string(COUNTRY_NAME)', @@ -177,6 +187,10 @@ * The explicit casts and {@code zero-or-one} will not be needed once the * full automatic casting rules (for now only partially implemented) are * in place. + *

    + * The `DPREMIER` parameter passed from SQL to the XQuery expression is spelled + * in uppercase (and also, in the SQL expression supplying it, quoted), for the + * reasons explained above for the {@code xq_ret_content} function. * @author Chapman Flack */ public class S9 implements ResultSetProvider @@ -263,9 +277,12 @@ enum Nulls { ABSENT, NIL }; * context item to be specified with no {@code AS} at all. Beware, though, * that PostgreSQL likes to invent column names from any function or type * name that may appear in the value expression, so this shorthand will not - * always work, while {@code AS "."} will.) JDBC uppercases all column + * always work, while {@code AS "."} will.) PL/Java's internal JDBC uppercases all column * names, so any uses of the corresponding variables in the query must have - * the names in upper case. + * the names in upper case. It is safest to also uppercase their appearances + * in the SQL (for which, in PostgreSQL, they must be quoted), so that the + * JDBC uppercasing is not being relied on. It is likely to be dropped in a + * future PL/Java release. * @param namespaces An even-length String array where, of each pair of * consecutive entries, the first is a namespace prefix and the second is * to URI to which to bind it. The zero-length prefix sets the default diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md index 81d4022c..cae02d2e 100644 --- a/src/site/markdown/examples/saxon.md +++ b/src/site/markdown/examples/saxon.md @@ -54,8 +54,8 @@ element representing the declaration of the function with the name `numeric_avg` (if PostgreSQL really had the standard `XMLQUERY` function built in): - SELECT XMLQUERY('/pg_catalog/pg_proc[proname eq $NAME]' - PASSING BY VALUE x, 'numeric_avg' AS NAME + SELECT XMLQUERY('/pg_catalog/pg_proc[proname eq $FUNCNAME]' + PASSING BY VALUE x, 'numeric_avg' AS FUNCNAME RETURNING CONTENT EMPTY ON EMPTY) FROM catalog_as_xml; @@ -65,10 +65,10 @@ parameter to the given value, then evaluates the query and returns XML necessarily meeting all the requirements of an XML "DOCUMENT"). It can be rewritten as this call to the `xq_ret_content` method provided here: - SELECT javatest.xq_ret_content('/pg_catalog/pg_proc[proname eq $NAME]', + SELECT javatest.xq_ret_content('/pg_catalog/pg_proc[proname eq $FUNCNAME]', PASSING => p, nullOnEmpty => false) FROM catalog_as_xml, - LATERAL (SELECT x AS ".", 'numeric_avg' AS NAME) AS p; + LATERAL (SELECT x AS ".", 'numeric_avg' AS "FUNCNAME") AS p; In the rewritten form, the type of value returned is determined by which function is called, and the parameters to pass to the query are moved out to @@ -76,6 +76,20 @@ a separate `SELECT` that supplies their values, types, and names (with the context item now given the name ".") and is passed by its alias into the query function. +An alert reader may notice that the example above includes a named parameter, +`FUNCNAME`, and it is spelled in uppercase in the XQuery expression that uses +it, and is spelled in uppercase _and quoted_ in the sub-`SELECT` that supplies +it. The reason is an unconditional `toUppercase()` in PL/Java's internal JDBC +driver, which is not anything the JDBC standard requires, but has been there +in PL/Java since 2005. For now, therefore, no matter how a parameter name is +spelled in the sub-`SELECT`, it must appear in uppercase in the XQuery +expression using it, or it will not be recognized. A future PL/Java release +is highly likely to stop forcibly uppercasing the names. At that time, any code +relying on the uppercasing will break. Therefore, it is wisest, until then, to +call this function with all parameter names spelled in uppercase both in the +SQL and in the XQuery text, and on the SQL side that requires quoting the name +to avoid the conventional lowercasing done by PostgreSQL. + In the standard, parameters and results (of XML types) can be passed `BY VALUE` or `BY REF`, where the latter means that the same nodes will retain their XQuery node identities over calls (note that this is @@ -97,7 +111,7 @@ with the reserved word. A rewritten form of the FROM xmldata, - LATERAL (SELECT data AS ".", 'not specified'::text AS DPREMIER) AS p, + LATERAL (SELECT data AS ".", 'not specified'::text AS "DPREMIER") AS p, "xmltable"('//ROWS/ROW', PASSING => p, COLUMNS => ARRAY[ 'xs:int(@id)', null, 'string(COUNTRY_NAME)', @@ -122,6 +136,10 @@ list (there is no defaulting an omitted column expression to an element test using the column's name, as there is in the standard function). The array is allowed to have one null element, marking that column `FOR ORDINALITY`. +The parameter being passed into the XQuery expressions here, `DPREMIER`, is +spelled in uppercase (and, on the SQL side, quoted), for the reasons explained +above for the `XMLQUERY`-like function. + The explicit casts and `zero-or-one` will not be needed once the full automatic casting rules (for now only partially implemented) are in place. The default, as shown, is handled by passing the desired default From a7452ef5ae1736d5140d95158df5a49fe98537b5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 7 Sep 2018 23:42:21 -0400 Subject: [PATCH 0195/1087] Tired of ugly double-outline .source divs. The default Maven site template for some reason creates two immediately nested div elements with class "source", which are both subject to the goofy rule putting a box around. Result: two boxes and doubled padding and margins. Web styling isn't my chief joy in life and I don't feel like fixing the template, but I am very tired of the ugly double boxes, so at least this is something. --- src/site/resources/css/site.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/site/resources/css/site.css diff --git a/src/site/resources/css/site.css b/src/site/resources/css/site.css new file mode 100644 index 00000000..194643be --- /dev/null +++ b/src/site/resources/css/site.css @@ -0,0 +1,14 @@ +/* You can override this file with your own styles */ + +div.source { + border-left: none; + border-right: none; + margin: 0; + padding: 2px 0; +} + +div.source > div.source { + border: none; + padding: 0; + margin: 0; +} From 294fce997f265a682ae2a745c891bab4ce5d0d90 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 8 Sep 2018 19:47:11 -0400 Subject: [PATCH 0196/1087] Avoid trying to use type names in Type.c messages. The simplest (or simplest-looking) way was to call format_type_be, but that function throws an error if the passed Oid is InvalidOid or an unknown type, so instead of the intended useful message in the log, a less helpful one gets thrown (and possibly turns a debug message or warning into an error). There is no equally-easy-to-call backend function to format a regtype with a benign fallback if it isn't known. The fmgr-callable format_type() works, but then you're going cstring::text::cstring just to make a message that might be at debug level and go nowhere. Just put the numeric oids in the message. --- pljava-so/src/main/c/type/Type.c | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index b050073a..eef8b07c 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -139,9 +139,7 @@ static Type _getCoerce(Type self, Type other, Oid fromOid, Oid toOid, Type Type_getCoerceIn(Type self, Type other) { - elog(DEBUG2, "Type_getCoerceIn(%s,%s)", - format_type_be(self->typeId), - format_type_be(other->typeId)); + elog(DEBUG2, "Type_getCoerceIn(%d,%d)", self->typeId, other->typeId); return _getCoerce(self, other, other->typeId, self->typeId, &(self->inCoercions), Coerce_createIn); } @@ -149,9 +147,7 @@ Type Type_getCoerceIn(Type self, Type other) Type Type_getCoerceOut(Type self, Type other) { - elog(DEBUG2, "Type_getCoerceOut(%s,%s)", - format_type_be(self->typeId), - format_type_be(other->typeId)); + elog(DEBUG2, "Type_getCoerceOut(%d,%d)", self->typeId, other->typeId); return _getCoerce(self, other, self->typeId, other->typeId, &(self->outCoercions), Coerce_createOut); } @@ -174,26 +170,23 @@ static Type _getCoerce(Type self, Type other, Oid fromOid, Oid toOid, switch ( cpt ) { case COERCION_PATH_NONE: - elog(ERROR, "no conversion function from %s to %s", - format_type_be(fromOid), - format_type_be(toOid)); + elog(ERROR, "no conversion function from (regtype) %d to %d", + fromOid, toOid); case COERCION_PATH_RELABELTYPE: /* * Binary compatible type. No need for a special coercer. * Unless ... it's a domain .... */ if ( ! IsBinaryCoercible(fromOid, toOid) && DomainHasConstraints(toOid)) - elog(WARNING, "disregarding domain constraints of %s", - format_type_be(toOid)); + elog(WARNING, "disregarding domain constraints of (regtype) %d", + toOid); return self; case COERCION_PATH_COERCEVIAIO: - elog(ERROR, "COERCEVIAIO not implemented from %s to %s", - format_type_be(fromOid), - format_type_be(toOid)); + elog(ERROR, "COERCEVIAIO not implemented from (regtype) %d to %d", + fromOid, toOid); case COERCION_PATH_ARRAYCOERCE: - elog(ERROR, "ARRAYCOERCE not implemented from %s to %s", - format_type_be(fromOid), - format_type_be(toOid)); + elog(ERROR, "ARRAYCOERCE not implemented from (regtype) %d to %d", + fromOid, toOid); case COERCION_PATH_FUNC: break; } From 3a77b0fbc0dbaac7f05d761fa68d0d12b3afa77e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 9 Sep 2018 08:31:03 -0400 Subject: [PATCH 0197/1087] Future-proof the TypeRoundTripper. The result column names already match case-insensitively, as the javadoc says, but only because of the unconditional toUpperCase() being done in SPIResultSetMetaData. One day that should get the axe, and these comparisons should still be case insensitive, as they are documented to be. --- .../pljava/example/annotation/TypeRoundTripper.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index 9d3c6d64..08028ce0 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -204,32 +204,32 @@ public static boolean roundTrip( { String what = outmd.getColumnLabel(i); - if ( "TYPEPG".equals(what) ) + if ( "TYPEPG".equalsIgnoreCase(what) ) { assertTypeJDBC(outmd, i, VARCHAR); out.updateObject(i, inTypePG); } - else if ( "TYPEJDBC".equals(what) ) + else if ( "TYPEJDBC".equalsIgnoreCase(what) ) { assertTypeJDBC(outmd, i, VARCHAR); out.updateObject(i, typeNameJDBC(inTypeJDBC)); } - else if ( "CLASSJDBC".equals(what) ) + else if ( "CLASSJDBC".equalsIgnoreCase(what) ) { assertTypeJDBC(outmd, i, VARCHAR); out.updateObject(i, inmd.getColumnClassName(1)); } - else if ( "CLASS".equals(what) ) + else if ( "CLASS".equalsIgnoreCase(what) ) { assertTypeJDBC(outmd, i, VARCHAR); out.updateObject(i, val.getClass().getName()); } - else if ( "TOSTRING".equals(what) ) + else if ( "TOSTRING".equalsIgnoreCase(what) ) { assertTypeJDBC(outmd, i, VARCHAR); out.updateObject(i, val.toString()); } - else if ( "ROUNDTRIPPED".equals(what) ) + else if ( "ROUNDTRIPPED".equalsIgnoreCase(what) ) { if ( ! inTypePG.equals(outmd.getColumnTypeName(i)) ) throw new SQLDataException( From 8bc481eaf48f6359d4957e10fb154cc925eced86 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 9 Sep 2018 08:24:04 -0400 Subject: [PATCH 0198/1087] Preserve Java 6 buildability. The AbstractResultSet needs to declare abstract the getObject- with-class method that won't really appear in the interface until Java 7. The PassXML example should use getSQLXML(), which existed in Java 6, rather than getObject(..., SQLXML.class), which did not. The TypeRoundTripper example has inherent reason to use the getObject(..., Class) method, so just move it into an example/jdk7 directory that is only built when a JDK [1.7,) profile is active, much like what was done with the Saxon example. But in this case, do not exclude it from the javadoc build; I would rather say "Java 6 is too old to build the docs" than leave more stuff out of the docs so Java 6 can build them. Why two different older versions of maven-resources-plugin were specified in two different places may just have been an artifact of history. 3.0.1 is its latest version with a class file version not greater than 50. The JDBC42_21 example is a strange case; it tests some JDBC4.2 functionality, so it definitely won't work before Java 8, but is written so it can still be compiled in Java 6. However, the SQL generator will object to its 'requires' dependency on TypeRoundTripper, which got moved to the jdk7 package, so this must go there too. --- pljava-api/pom.xml | 1 - pljava-examples/pom.xml | 19 +++++++++++++++++++ .../pljava/example/annotation/PassXML.java | 4 ++-- .../{annotation => jdk7}/JDBC42_21.java | 7 ++++++- .../TypeRoundTripper.java | 2 +- pljava-packaging/pom.xml | 1 - .../pljava/jdbc/AbstractResultSet.java | 3 +++ pom.xml | 9 +++++++++ 8 files changed, 40 insertions(+), 6 deletions(-) rename pljava-examples/src/main/java/org/postgresql/pljava/example/{annotation => jdk7}/JDBC42_21.java (94%) rename pljava-examples/src/main/java/org/postgresql/pljava/example/{annotation => jdk7}/TypeRoundTripper.java (99%) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 2d639b84..a5216666 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -37,7 +37,6 @@ maven-resources-plugin - 2.5 copy-service-config diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index ec738a3b..e64805fa 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -34,6 +34,25 @@ + + + + [1.7,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org/postgresql/pljava/example/jdk7/*.java + + + + + + 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 b96f5cd3..c573a064 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 @@ -411,7 +411,7 @@ public static boolean xmlInStmtAndRS(ResultSet out) throws SQLException if ( Types.SQLXML != rs.getMetaData().getColumnType(1) ) logMessage("WARNING", "ResultSetMetaData.getColumnType() misreports SQLXML"); - x = rs.getObject(1, SQLXML.class); + x = rs.getSQLXML(1); ps.close(); out.updateObject(1, x); return true; @@ -638,7 +638,7 @@ public static SQLXML xmlFromComposite() throws SQLException ps.setObject(1, obj); ResultSet r = ps.executeQuery(); r.next(); - obj = r.getObject(1, PassXML.class); + obj = (PassXML)r.getObject(1); ps.close(); return obj.m_value; } 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/jdk7/JDBC42_21.java similarity index 94% rename from pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java rename to pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/JDBC42_21.java index e501a10f..fc183f6c 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/jdk7/JDBC42_21.java @@ -9,7 +9,12 @@ * Contributors: * Chapman Flack */ -package org.postgresql.pljava.example.annotation; +/* + * At the moment, this is written so it will compile in jdk6, even though it + * won't work before jdk8. It's in the exclude-prior-to-jdk7 package though, + * because it has an order dependency on TypeRoundTripper, which is there. + */ +package org.postgresql.pljava.example.jdk7; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java similarity index 99% rename from pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java rename to pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java index 08028ce0..f07d51d2 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java @@ -9,7 +9,7 @@ * Contributors: * Chapman Flack */ -package org.postgresql.pljava.example.annotation; +package org.postgresql.pljava.example.jdk7; import java.lang.reflect.Field; import java.lang.reflect.Modifier; diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index f4fdec2e..14504338 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -83,7 +83,6 @@ org.apache.maven.plugins maven-resources-plugin - 2.7 pljava extension files diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index ef90f38f..4240e846 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -882,4 +882,7 @@ public T getObject(String columnName, Class type) { return this.getObject(this.findColumn(columnName), type); } + + public abstract T getObject(int columnIndex, Class type) + throws SQLException; // placeholder; remove when Java back horizon >= 7 } diff --git a/pom.xml b/pom.xml index 9c74a07d..56ccbc35 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,15 @@ + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.1 + + + org.apache.maven.plugins From 0d174347b463bd7f12505c2336362583009b83bf Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 10 Sep 2018 02:13:17 -0400 Subject: [PATCH 0199/1087] Add to developer docs on type mappings. Adding a few new type mappings turns out to be a productive exercise for learning stuff that the docs could stand to cover more deeply. --- src/site/markdown/develop/coercion.md | 251 ++++++++++++++++++++++++-- 1 file changed, 236 insertions(+), 15 deletions(-) diff --git a/src/site/markdown/develop/coercion.md b/src/site/markdown/develop/coercion.md index f7c00e82..295d4f46 100644 --- a/src/site/markdown/develop/coercion.md +++ b/src/site/markdown/develop/coercion.md @@ -78,10 +78,135 @@ from `PgObject`. Often there is a close relationship between a C 'class' and a Java class of the same name, with instances of one holding references to the other. +#### Types + The `type` subdirectory in `pljava-so` contains -the C sources for a class `Type`, which inherits directly from `PgObject`, -and many subclasses of `Type` representing different known SQL types -and how they correspond to Java types. +the C sources for a class `Type`, which extends a `TypeClass`, which inherits +from `PgObject`. A `TypeClass` is associated with a single Java (primitive +or reference) type, and might have only a single `Type` that extends it, +associated with a single PostgreSQL type. In that simple case, the singleton +`Type` instance can be directly "registered" in the caches that are keyed +by PostgreSQL type oid or by Java type, respectively, by the function +`Type_registerType`. + +#### Type obtainers + +It is also possible that a single `TypeClass` can be extended by more than +one `Type`, one for each of multiple PostgreSQL types. In that case, an +alternate function `Type_registerType2` will cache, not a single already-created +`Type` instance, but a `TypeObtainer` function, which can be used to obtain +a `Type` extending its associated `TypeClass` and bound to a specific PostgreSQL +type. + +An obtainer function should not allocate a brand new `Type` on every call, but +return an existing `Type` if there already is one for the requested PostgreSQL +type. If a `TypeClass` and its associated Java type can only sensibly map a +small few PostgreSQL types, it could even be overkill for the obtainer to use a +hash map or the like to remember the instances it has returned; it could simply +have a few static variables to cache the few instances it will need, and return +the right one after comparing its oid argument to a few constants. + +The `TypeClass` for `SQLXML` works that way, with an obtainer that will only +return a `Type` instance for PostgreSQL `xml`, or for PostgreSQL `text` (in case +the Java caller wants to process a text value known to contain XML, or is being +used in a PostgreSQL server that was built without the `xml` type). + +An alternative to using an obtainer in that case would be for the initialization +method of the `TypeClass` to simply create more than one `Type` right away, and +register them all directly with `Type_registerType`, needing no obtainer +function. An example is the `TypeClass` representing `java.sql.Timestamp`, which +creates two `Type` instances and registers them immediately, one each for the +PostgreSQL `timestamp` and `timestamptz` types, as both are mapped to this +Java class by default. + +#### Exceptional behavior of `String` + +At the other extreme, the `TypeClass` for `String` provides an obtainer that +will supply a `Type` for any PostgreSQL type it is asked to, and will rely on +the PostgreSQL text input and output methods for that type to handle the +conversion. This is how it is possible in PL/Java to request or supply a +`String` whatever the underlying PostgreSQL type. + +The obtainer for `String`, at present, does not do any bookkeeping to return +one `Type` per PostgreSQL type oid it is called for. It simply allocates a new +one on every call. That makes it an exception to the [comment in `Type.h`][thsc] +specifying singleton behavior, but the exception is as ancient as the comment. + +[thsc]: https://github.com/tada/pljava/blob/d2285d74/src/C/include/pljava/type/Type.h#L105 + +#### Obtainer vs. direct registration + +In the more common case where a `TypeClass` will only sensibly have a few `Type` +children, the choice to simply create and register those directly or to use a +`TypeObtainer` can be influenced by a few considerations. + +The `TypeClass` for `java.sql.Timestamp` directly registers its two children +because it is the default mapping according to JDBC for both PostgreSQL types +`timestamp` and `timestamptz`. The two `Type`s are directly registered, keyed +by those two type oids, and directly retrieved from the cache when a PostgreSQL +value of either type has to be mapped. + +In contrast, JDBC 4.2 introduced non-default mappings for both SQL types: +a `timestamp` can map to a `java.time.LocalDateTime`, and a `timestamptz` can +map to a `java.time.OffsetDateTime`, but only when the Java code explicitly +requests. So, the `TypeClass` for `LocalDateTime` does not directly register +a `Type` corresponding to SQL `timestamp`. It registers a type obtainer, which +can only return a singleton `Type` for that exact SQL type, and does so when +asked. + +For the same reason, the `TypeClass` for `SQLXML` relies on an obtainer. +Although an alternate mapping for the `text` type, it would normally be +the default mapping for type `xml` according to JDBC 4, and would simply +register that `Type` directly. However, PL/Java has long mapped the `xml` +type to `String` by default, so for now (until a later, major release), +it treats `SQLXML` as an alternative mapping Java code may explicitly use. + +#### Lazy initialization + +In the case of the new JDBC 4.2 date/time optional mappings, there is another +reason for each new `TypeClass` to provide a `TypeObtainer`, even though each +`TypeObtainer` will only support exactly one PostgreSQL type. The corresponding +Java classes do not exist before Java 8, and PL/Java supports earlier releases, +so it cannot unconditionally load those classes at initialization time. Each +corresponding `TypeClass` defers that part of its initialization to the first +call of its obtainer, which only happens if the Java code has referred to the +class and therefore it's known to exist. + +A side benefit of this approach is laziness in its own right: less class loading +done at initialization before even knowing whether the classes will be needed. +In future work, it may be possible to further reduce PL/Java's +time-to-first-result by applying the technique more widely to +types that use direct registration now. + +#### `Type_canReplaceType` + +When there is a registered default mapping from a PostgreSQL type to +a `Type` _a_, and the Java type associated with that `TypeClass` is not the one +used in the Java code, the Java type expected by the code will be +looked up and resolved to a `TypeClass`, and from there by its +type obtainer to a second `Type` _b_. The `Type_canReplaceType` method of _b_ +will be called, passing _a_. If it returns `true`, the `Type` _b_ and its +methods will be used instead of _a_ to handle the coercions from +PostgreSQL `Datum` to Java type and vice versa. Otherwise, PL/Java will +seek a chain of PostgreSQL type coercions to bridge the gap. + +The design is slightly awkward at present, because `Type_canReplaceType` +is applied to two `Type`s (or has one as receiver and one as argument, in the +"C objects" view), so it has to be applied to the result, _b_, of the type +obtainer, essentially to find out whether calling that obtainer was worth +doing. A simpler design might result by changing its argument to a `TypeClass`. + +In the current design, redundant checks are largely avoided by not expecting +the type obtainer to do error reporting. If it supports more than one PostgreSQL +type, it should use the PostgreSQL type oid that is passed to determine which +`Type` instance to return. If the PostgreSQL oid is not one of those, it should +simply return whichever `Type` instance represents its primary or most +natural mapping. It does not need to report that the PostgreSQL oid is +unsupported; it can leave that to its can-replace method. A corollary is that +a type obtainer supporting exactly one PostgreSQL type may return its +singleton `Type` instance unconditionally, ignoring its argument. + +#### Coercions Each C `Type` has a method `coerceDatum` that takes a PostgreSQL `Datum` and produces the corresponding Java value, and a method `coerceObject` that @@ -143,14 +268,73 @@ SQL declaration is a domain, constraints on the domain are not checked, allowing the function to return values of the base type that should not be possible in the domain. This is a bug. -### Parameters supplied to a JDBC `PreparedStatement` from Java - -These are passed through the `coerceObject` method of a C `Type` selected -according to the SQL type that the query plan has for the parameter. The -type map for the innermost PL/Java invocation on the call stack is consulted -if necessary, so these rules are equivalent to the first two in the -"parameters and return values" case. However, see "additional JDBC coercions" -below. +### A general rule, with one present exception + +As the steps above reveal, for both directions of conversion, it is the +_PostgreSQL_ type that starts the algorithm off. The known mappings are +used to find a prospective Java type from it, and then if the actual Java +type appearing in the code is not the expected one, plans are adjusted +accordingly. + +This pattern is seen elsewhere in the ISO SQL standard, in Part 14 on +XML-related specifications, which include how to convert values of SQL types +to XML Schema data types and the reverse. Again, for both conversion directions, +the algorithms begin with the SQL type, then adjust if the prospective mapped +type is not the one expected. + +#### Parameters supplied to a JDBC `PreparedStatement` from Java + +The sole exception in PL/Java is the JDBC `PreparedStatement`, and only for +the _parameters supplied to_ the statement. _Results from it_ are handled +consistently with the general rule. + +Ordinarily, when preparing a query that contains parameters, PostgreSQL's +parsing and analysis will reach conclusions about what SQL types the parameters +will need to have so that the query makes sense. JDBC presents those conclusions +to the Java code through the `getParameterMetaData` method once the query has +been prepared, so that the Java code can supply values of appropriate types, +or necessary coercions can be done. The (client side) pgJDBC driver is able +to implement `getParameterMetaData` because the PostgreSQL frontend-backend +protocol allows for sending a query to prepare and having the server send back +a `ParameterDescription` message with the needed type information. + +For curious historical reasons, PostgreSQL has been able to supply remote +clients with that `ParameterDescription` information since PG 7.4 came out +in 2003, but a module _loaded right inside the backend_ like PL/Java could +not request the same information using SPI until PG 9.0 in 2010, and +[still not easily][sne]. By then, PL/Java had long been 'faking' +`ParameterMetaData` in a way that reverses the usual type mapping pattern. + +#### How `ParameterMetaData` gets faked + +PL/Java, when creating a `PreparedStatement`, does not submit the query +immediately to PostgreSQL for analysis. Instead, it initializes all of +the parameter types to unknown, and allows the Java code to go ahead and +call the `set...()` methods to supply values. Using the supplied _Java_ types +as starting points, it fills in the parameter types by following the usual +mappings backward. If the Java code does, in fact, call `getParameterMetaData`, +PL/Java returns the types determined that way for any parameters that have +already been set, and (arbitrarily) `VARCHAR` for any that have not. Only +when the Java code executes the statement the first time does PL/Java submit +the query to PostgreSQL to prepare, passing along the type mappings assumed +so far, and hoping PostgreSQL can make sense of it. + +While getting the general rule wrong and differing from client-side pgJDBC, +this is not completely unworkable, and has been PL/Java's behavior +[since 2004][fpm]. Any resulting surprises can generally be resolved by +some rewriting of the query or use of other PL/Java JDBC methods that more +directly indicate the intended PostgreSQL types. +[Some small changes in PL/Java 1.5.1][tpps] may help in some cases. 1.5.1 also +introduces `TypeBridge`s, described later on this page. + +A future major release of PL/Java should use the additions to PostgreSQL SPI +and bring the treatment of `PreparedStatement` parameters into conformance +with the general rule. (That release, therefore, will have to support +PostgreSQL versions no earlier than 9.0.) + +[sne]: https://www.postgresql.org/message-id/874liv1auh.fsf%40news-spur.riddles.org.uk +[fpm]: https://github.com/tada/pljava/blob/86793a2f/src/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java#L425 +[tpps]: ../releasenotes.html#Typing_of_parameters_in_prepared_statements JDBC defines some `setObject` and `setNull` methods on `PreparedStatement` that must be passed a `java.sql.Types` constant. The JDBC constant will be @@ -314,6 +498,11 @@ PL/Java base types. Methods for doing that are covered on the ## Additional JDBC coercions +The JDBC standard facilities for managing a type map are not implemented +or used, and `getTypeMap` will always return `null`. All of PL/Java's uses +of the type map managed with `SQLJ.ADD_TYPE_MAPPING` take place below the +level of the JDBC mechanisms. + When reading or writing values through any of the JDBC interfaces (except `SQLInput`/`SQLOutput` in raw mode), there is another layer of type coercion that can be applied, after (when reading) or before (when @@ -322,10 +511,42 @@ implemented entirely in Java, and can be found in `SPIConnection.java` under the names `basicCoersion`, `basicNumericCoersion`, and `basicCalendricalCoersion`. -The JDBC standard facilities for managing a type map are not implemented -or used, and `getTypeMap` will always return `null`. All of PL/Java's uses -of the type map managed with `SQLJ.ADD_TYPE_MAPPING` take place below the -level of the JDBC mechanisms. +These three, however, are inconsistently applied. They are used on values +written by Java to the single-row writable `ResultSet`s that PL/Java provides +for composite function results, but not those written to the similar +`ResultSet`s provided to triggers, or prepared statement parameters, or +`SQLOutput` in typed tuple mode. They also cannot be assumed to cover all +cases since JDBC 4.1 and 4.2 introduced new type mappings that can be used +in place of the default ones (such as `java.time.OffsetTime` for `timetz`). + +Therefore, a future PL/Java release will probably phase out those three methods +in favor of a more general method. + +## The `TypeBridge` class + +A start on the replacement of those three methods has already been made in the +work to support the `java.time` types and `SQLXML` in PL/Java 1.5.1. The +support of these alternative mappings requires that the Java types be +recognized as alternate mappings known to the native code, and passed intact +to the native layer with no attempt to coerce them to the expected types first. +To do that, a Java value that is of one of the known supported alternate types +is wrapped in a `TypeBridge.Holder` to link the value with explicit information +on the needed type conversion. As the first step in phasing out the +inconsistently-applied `SPIConnection` basic coercions, they are never applied +at all to a `TypeBridge.Holder`. At present, `TypeBridge`s are used only +for the newly-added type mappings, to avoid a behavior change for pre-existing +ones. + +The `TypeBridge` class is not intended as a mechanism for user-extensible +type mappings (the existing facilities for user-defined types should be used). +There will be a small, stable number of `TypeBridge`s corresponding to known +type mappings added in the JDBC spec, or otherwise chosen for native support +in PL/Java. For any `TypeBridge` wrapping a Java value there must be a +native-code `TypeClass` registered for the Java class the bridge is meant +to carry. There is one function in `Type.c` to initialize and register all of +the known handful of `TypeBridge`s. When new ones are added, the list must be +kept in an order such that if bridge _a_ is registered before bridge _b_, then +_a_ will not capture the Java type registered to _b_. ## The user-defined-type function slot switcheroo From 8ab1149d308618aa408a1d462935183cc7a8aef6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 10 Sep 2018 03:55:35 -0400 Subject: [PATCH 0200/1087] Fix javadoc nits in examples. Two examples moved to jdk7 needed imports added to resolve doc links to a class in their former package. Speaking of packages, also add package-info.java where it was missing. --- .../java/org/postgresql/pljava/example/jdk7/JDBC42_21.java | 2 ++ .../postgresql/pljava/example/jdk7/TypeRoundTripper.java | 2 ++ .../org/postgresql/pljava/example/jdk7/package-info.java | 5 +++++ .../java/org/postgresql/pljava/example/package-info.java | 7 +++++++ .../org/postgresql/pljava/example/saxon/package-info.java | 7 +++++++ 5 files changed, 23 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/package-info.java create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/package-info.java create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/package-info.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/JDBC42_21.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/JDBC42_21.java index fc183f6c..29c6bfe1 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/JDBC42_21.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/JDBC42_21.java @@ -20,6 +20,8 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLActions; +import org.postgresql.pljava.example.annotation.ConditionalDDR; // for javadoc + /** * Exercise new mappings between date/time types and java.time classes * (JDBC 4.2 change 21). diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java index f07d51d2..662ff775 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java @@ -31,6 +31,8 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; +import org.postgresql.pljava.example.annotation.ConditionalDDR; // for javadoc + /** * A class to simplify testing of PL/Java's mappings between PostgreSQL and * Java/JDBC types. diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/package-info.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/package-info.java new file mode 100644 index 00000000..e3c80cbb --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/package-info.java @@ -0,0 +1,5 @@ +/** + * Examples only built on Java 7 or later. + * @author Chapman Flack + */ +package org.postgresql.pljava.example.jdk7; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/package-info.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/package-info.java new file mode 100644 index 00000000..8a348b8c --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/package-info.java @@ -0,0 +1,7 @@ +/** + * The examples that have been around the longest, and are deployed using + * hand-written SQL deployment code (see {@code src/main/resources/deployment}), + * not having been reworked to use annotations yet. + * @author Thomas Hallgren + */ +package org.postgresql.pljava.example; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/package-info.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/package-info.java new file mode 100644 index 00000000..59cfbd7c --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/package-info.java @@ -0,0 +1,7 @@ +/** + * Examples using the Saxon-HE library for XML processing, and requiring Java 8. + * Only built if {@code -Psaxon-examples} is given on the {@code mvn} command + * line (which will also cause Saxon-HE to be downloaded). + * @author Chapman Flack + */ +package org.postgresql.pljava.example.saxon; From 445ae939cc4769a732e02ef8008d38acb6389247 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 10 Sep 2018 04:01:53 -0400 Subject: [PATCH 0201/1087] Poke migration-management versions for 1.5.1-BETA2. -packaging/build.xml already makes an update .sql from 1.5.1-BETA1, the last released version. No change needed before this release. --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index c174b8c8..ac6dfbee 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -495,6 +495,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_5_1_BETA2 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA1 = REL_1_5_0; static final SchemaVariant REL_1_5_0_BETA3 = REL_1_5_0; static final SchemaVariant REL_1_5_0_BETA2 = REL_1_5_0_BETA3; From 9857152d0fe5fbfd2a8b3c6d42ffe5816e5f00b2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 10 Sep 2018 04:27:56 -0400 Subject: [PATCH 0202/1087] Add control file in preparation for next release. Now that 1.5.1-BETA2 is released, the next release should include an extension SQL file allowing upgrade from 1.5.1-BETA2. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index dac3d42e..b54b04f6 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Sat, 15 Sep 2018 20:46:38 -0400 Subject: [PATCH 0203/1087] Missed in an earlier doc edit. The example parameter named NAME was changed to FUNCNAME, but one use was missed. --- src/site/markdown/examples/saxon.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md index cae02d2e..fb4d225a 100644 --- a/src/site/markdown/examples/saxon.md +++ b/src/site/markdown/examples/saxon.md @@ -59,7 +59,7 @@ in): RETURNING CONTENT EMPTY ON EMPTY) FROM catalog_as_xml; -It binds the 'context item' of the query to `x`, and the `NAME` +It binds the 'context item' of the query to `x`, and the `FUNCNAME` parameter to the given value, then evaluates the query and returns XML "CONTENT" (a tree structure with a document node at the root, but not necessarily meeting all the requirements of an XML "DOCUMENT"). It can be From 47b9cdc6d86a92553187e99505e541ea3ecf4bc2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 15 Sep 2018 23:42:16 -0400 Subject: [PATCH 0204/1087] Bring hello, world example up to date. It wasn't updated to reflect the schema qualification added in 64a2805 or the Java return type in AS added in 38a25b4. --- src/site/markdown/use/hello.md.vm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/site/markdown/use/hello.md.vm b/src/site/markdown/use/hello.md.vm index 4170a7d9..e26fbb20 100644 --- a/src/site/markdown/use/hello.md.vm +++ b/src/site/markdown/use/hello.md.vm @@ -314,16 +314,16 @@ SQLActions[]={ "BEGIN INSTALL BEGIN PostgreSQL CREATE OR REPLACE FUNCTION hello( - toWhom varchar) - RETURNS varchar + toWhom pg_catalog.varchar) + RETURNS pg_catalog.varchar LANGUAGE java VOLATILE - AS 'com.example.proj.Hello.hello(java.lang.String)' + AS 'java.lang.String=com.example.proj.Hello.hello(java.lang.String)' END PostgreSQL; END INSTALL", "BEGIN REMOVE BEGIN PostgreSQL DROP FUNCTION hello( - toWhom varchar) + toWhom pg_catalog.varchar) END PostgreSQL; END REMOVE" } From 499bd212cf431b3449e7eab852944cd3fd754a44 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 17 Sep 2018 22:02:39 -0400 Subject: [PATCH 0205/1087] Sop for certain Rhino-based JavaScript engines. The last assignment in this JavaScript snippet sets computedPath to a concatenation (with +) of two strings, and Rhino handles that in a clever way (borrowed from V8, it seems), producing an instance of its ConsString class. https://mozilla.github.io/rhino/javadoc/org/mozilla/javascript/ConsString.html ... which works very nicely if the JSR-223 adapter layer between Rhino and javax.script API knows about the ConsString object and converts transparently to a Java String. It does, in the Oracle Java 6 and 7 builds. The adapter layer is a separate project, not part of Rhino proper. What apparently happened with the OpenJDK 6 and 7 is that they bundled perfectly good versions of Rhino with somewhat incomplete versions of the adapter, and that adapter does not know how to make a Java String from a ConsString. Using the ConsString to construct a new, ordinary JavaScript string seems to solve the problem; the adapter knows what to do with one of those. --- pljava-packaging/build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index b54b04f6..0c2ff971 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -354,7 +354,7 @@ if (found) { var fsep = properties.getProperty('file.separator'); var plen = fsep.length() - 1; /* original separator had length 1 */ plen += prefix.length; - computedPath = replacement + computedPath.slice(plen); + computedPath = String(replacement + computedPath.slice(plen)); } ]]> From 92b15e4a3642d98c61f71bc23919507f84727b7f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 21 Sep 2018 01:33:28 -0400 Subject: [PATCH 0206/1087] Use script task to quote libjvmdefault as C string This is needed because the current practice of passing the default JVM path as a command-line macro definition to the C compiler and relying on C preprocessor stringification to make a string literal of it only works when the path includes no word-like components that collide with macros in existence during the build. (Issue #176) It seemed tidier to use scriptdef, but that plain doesn't work from a maven-antrun-plugin configuration; the plugin writes out a script file containing every part of the scriptdef except the script. The bug report hasn't been assigned yet... https://issues.apache.org/jira/browse/MANTRUN-207 too bad, as the only problem looks to be right here: http://svn.apache.org/viewvc/maven/plugins/tags/maven-antrun-plugin-1.8/src/main/java/org/apache/maven/plugin/antrun/AntrunXmlPlexusConfigurationWriter.java?revision=1646987&view=markup#l69 So, for now, this is done with a script tag, no scriptdef. --- pljava-so/pom.xml | 83 +++++++++++++++++++++++++++++++--- pljava-so/src/main/c/Backend.c | 2 +- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 5b935acd..e6da2972 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -252,7 +252,7 @@ - PLJAVA_LIBJVMDEFAULT=${pljava.libjvmdefault} + PLJAVA_LIBJVMDEFAULT=${pljava.qlibjvmdefault} @@ -277,16 +277,22 @@ - + org.apache.maven.plugins maven-antrun-plugin 1.7 - pg_config to pgsql.properties initialize @@ -295,6 +301,71 @@ + + + diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 7279ba24..f319d9d9 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1366,7 +1366,7 @@ static void registerGUCOptions(void) NULL, /* extended description */ &libjvmlocation, #ifdef PLJAVA_LIBJVMDEFAULT - CppAsString2(PLJAVA_LIBJVMDEFAULT), + PLJAVA_LIBJVMDEFAULT, #else "libjvm", #endif From d4735cb934f91acc5deb1be3add7781a0d3c0df0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 21 Sep 2018 02:01:52 -0400 Subject: [PATCH 0207/1087] Must also work when option is not given... --- pljava-so/pom.xml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index e6da2972..e27217e8 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -358,9 +358,13 @@ function quoteStringForC(s) } var jvmdflt = java.lang.System.getProperty('pljava.libjvmdefault'); -var jvmdfltQuoted = quoteStringForC(jvmdflt); -var mvnProject = project.getReference("maven.project"); // pljava-so -mvnProject.getProperties().setProperty("pljava.qlibjvmdefault", jvmdfltQuoted); +if ( null !== jvmdflt ) +{ + var jvmdfltQuoted = quoteStringForC(jvmdflt); + var mvnProject = project.getReference("maven.project"); // pljava-so + mvnProject.getProperties() + .setProperty("pljava.qlibjvmdefault", jvmdfltQuoted); +} ]]> From 75f1b7cf5b69bf593ed16384f4bb98c052c2bd66 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 21 Sep 2018 19:22:08 -0400 Subject: [PATCH 0208/1087] Elaborate on comment while I'm thinking of it. For present purposes, it isn't necessary to complete this C stringifier to handle escaping of codepoints above 0x9F, but at least mention in the comment what work would be needed. --- pljava-so/pom.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index e27217e8..1b50e08c 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -310,8 +310,18 @@ standard. Group 3 is for other miscellaneous characters that will be given hex escapes (and group 4 captures an empty string if the lookahead is also a valid hex digit, which quoteStringForC will have to be careful to separate - from the hex escape. Only one of groups (1,2,3) will be present at any one + from the hex escape). Only one of groups (1,2,3) will be present at any one match position. + Group 3, for now, only captures control characters (Unicode category Cc); + they all have codepoints less than 0xA0, so the \x escape is the right way + to emit them. It would be easy to extend the regex to capture other, extended + characters (codepoints 0xA0 or higher) that one prefers to render as escapes, + but if that is done, the group-3 case in quoteStringForC() then needs to be + completed to use \u form (for codepoints 0xA0 or above but less than 0x10000) + or \U form for those 0x10000 or above. The \u and \U forms both consume a + fixed number (4 or 8) of hex digits, so when generating those, there is no + need to pay attention to group 4 or do anything special if a hex digit + follows. */ var mustBeQuotedForC = java.util.regex.Pattern.compile( From b72bd199f6e0a3fbfef15fbb67a3f3baecdb22ec Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Sep 2018 17:21:34 -0400 Subject: [PATCH 0209/1087] Add a ddr.reproducible option to SQL generator. When true, it constrains the order of the generated DDR to be consistent over successive compilations of the same sources, useful in distribution builds where reproducibility is valued. When false, it selects the previous behavior, where descriptors can have different order from one compilation to the next of the same sources; during testing, this can help to detect whether there are necessary provides/requires relationships that haven't been declared. Addresses issue #181. The 'reproducible' option also fixes the order of Java-to-SQL type mappings known to the generator, though this is an internal detail; the ordered mappings are not emitted explicitly into the DDR, and if a change in their order would affect what is written there, that would probably indicate a more serious issue. --- .../pljava/annotation/package-info.java | 28 ++++- .../pljava/sqlgen/DDRProcessor.java | 115 ++++++++++++++++-- 2 files changed, 128 insertions(+), 15 deletions(-) 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 32c6984e..de7309e5 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 @@ -66,17 +66,35 @@ * 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 + *
    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 * 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 requires and - * provides strings, which can be used to control the order of + * annotations in this package accept arbitrary {@code requires} and + * {@code provides} strings, which can be used to control the order of * statements in the generated descriptor. The strings given for - * requires and provides have no meaning to the + * {@code requires} and {@code provides} have no meaning to the * compiler, except that it will make sure not to write anything that - * requires some string X into the generated script - * before whatever provides it. + * {@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 + * 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 + * 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 + * declared. When the {@code ddr.reproducible} option is {@code true}, the order + * of statements in the deployment descriptor will be one of the possible + * 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. * */ package org.postgresql.pljava.annotation; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 0ee638a3..144fa241 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -46,6 +46,7 @@ import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; +import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; @@ -109,6 +110,7 @@ @SupportedAnnotationTypes({"org.postgresql.pljava.annotation.*"}) @SupportedOptions ({ + "ddr.reproducible", // default true "ddr.name.trusted", // default "java" "ddr.name.untrusted", // default "javaU" "ddr.implementor", // implementor when not annotated, default "PostgreSQL" @@ -157,6 +159,7 @@ class DDRProcessorImpl // Similarly, the TypeMapper should be easily available to code below. // final TypeMapper tmpr; + final SnippetTiebreaker snippetTiebreaker; // Options obtained from the invocation // @@ -164,6 +167,7 @@ class DDRProcessorImpl final String nameUntrusted; final String output; final String defaultImplementor; + final boolean reproducible; // Certain known types that need to be recognized in the processed code // @@ -226,6 +230,14 @@ class DDRProcessorImpl output = optv; else output = "pljava.ddr"; + + optv = opts.get( "ddr.reproducible"); + if ( null != optv ) + reproducible = Boolean.parseBoolean( optv); + else + reproducible = true; + + snippetTiebreaker = reproducible ? new SnippetTiebreaker() : null; TY_ITERATOR = typu.getDeclaredType( elmu.getTypeElement( java.util.Iterator.class.getName())); @@ -528,10 +540,18 @@ else if ( ! defaultImplementor.equals( imp) ) Queue> fwdBlocked = new LinkedList>(); Queue> revBlocked = new LinkedList>(); - Queue> fwdReady = new LinkedList>(); - Queue> revReady = new LinkedList>(); - - Queue> q = new LinkedList>(); + Queue> fwdReady; + Queue> revReady; + if ( reproducible ) + { + fwdReady = new PriorityQueue( 11, snippetTiebreaker); + revReady = new PriorityQueue( 11, snippetTiebreaker); + } + else + { + fwdReady = new LinkedList>(); + revReady = new LinkedList>(); + } for ( VertexPair vp : snippetVPairs ) { @@ -574,6 +594,7 @@ Snippet[] order( Set consumer) { Snippet[] snips = new Snippet [ ready.size() + blocked.size() ]; + Vertex cycleBreaker = null; queuerunning: for ( int i = 0 ; ; ) @@ -605,10 +626,27 @@ Snippet[] order( continue; if ( provider.containsKey( v.payload.implementor()) ) continue; - -- v.indegree; - it.remove(); - ready.add( v); - continue queuerunning; + if ( reproducible ) + { + if (null == cycleBreaker || + 0 < snippetTiebreaker.compare(cycleBreaker, v)) + cycleBreaker = v; + } + else + { + -- v.indegree; + it.remove(); + ready.add( v); + continue queuerunning; + } + } + if ( null != cycleBreaker ) + { + blocked.remove( cycleBreaker); + -- cycleBreaker.indegree; + ready.add( cycleBreaker); + cycleBreaker = null; + continue; } /* * Got here? It's a real cycle ... nothing to be done. @@ -2338,8 +2376,17 @@ private void workAroundJava7Breakage() } } - Queue>> q = - new LinkedList>>(); + Queue>> q; + if ( reproducible ) + { + q = new PriorityQueue>>( + 11, new TypeTiebreaker()); + } + else + { + q = new LinkedList>>(); + } + for ( Vertex> v : vs ) if ( 0 == v.indegree ) q.add( v); @@ -2814,3 +2861,51 @@ class ImpProvider implements Snippet @Override public String[] requires() { return s.requires(); } @Override public boolean characterize() { return s.characterize(); } } + +class SnippetTiebreaker implements Comparator> +{ + @Override + public int compare( Vertex o1, Vertex o2) + { + Snippet s1 = o1.payload; + Snippet s2 = o2.payload; + int diff = s1.implementor().compareTo( s2.implementor()); + if ( 0 != diff ) + return diff; + String[] ds1 = s1.deployStrings(); + String[] ds2 = s2.deployStrings(); + diff = ds1.length - ds2.length; + if ( 0 != diff ) + return diff; + for ( int i = 0 ; i < ds1.length ; ++ i ) + { + diff = ds1[i].compareTo( ds2[i]); + if ( 0 != diff ) + return diff; + } + assert s1 == s2 : "Two distinct Snippets compare equal by tiebreaker"; + return 0; + } +} + +class TypeTiebreaker +implements Comparator>> +{ + @Override + public int compare( + Vertex> o1, + Vertex> o2) + { + Map.Entry m1 = o1.payload; + Map.Entry m2 = o2.payload; + int diff = m1.getValue().compareTo( m2.getValue()); + if ( 0 != diff ) + return diff; + diff = m1.getKey().toString().compareTo( m2.getKey().toString()); + if ( 0 != diff ) + return diff; + assert + m1 == m2 : "Two distinct type mappings compare equal by tiebreaker"; + return 0; + } +} From 4af7781b4bec3a07889cc0ec55d8962464a40584 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Sep 2018 20:20:57 -0400 Subject: [PATCH 0210/1087] Let ExecutionPlan methods override read_only. They are used only in SPIStatement and SPIPreparedStatement, which will be given extended API to specify SPI_READONLY_DEFAULT (the current behavior) or SPI_READONLY_FORCED or SPI_READONLY_CLEARED. In passing, deprecate an exec() method in SPI.java that seems unused not only in the current codebase, but in the entire git history of the project (and does not allow control of read_only). --- pljava-so/src/main/c/ExecutionPlan.c | 45 ++++++++++++++----- .../pljava/internal/ExecutionPlan.java | 42 ++++++++++++----- .../org/postgresql/pljava/internal/SPI.java | 13 ++++-- 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index 9ccaece4..5df0847f 100644 --- a/pljava-so/src/main/c/ExecutionPlan.c +++ b/pljava-so/src/main/c/ExecutionPlan.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -26,6 +32,11 @@ /* Class 07 - Dynamic SQL Error */ #define ERRCODE_PARAMETER_COUNT_MISMATCH MAKE_SQLSTATE('0','7', '0','0','1') +/* These three values must match those in ExecutionPlan.java */ +#define SPI_READONLY_DEFAULT 0 +#define SPI_READONLY_FORCED 1 +#define SPI_READONLY_CLEARED 2 + /* Make this datatype available to the postgres system. */ extern void ExecutionPlan_initialize(void); @@ -35,7 +46,7 @@ void ExecutionPlan_initialize(void) { { "_cursorOpen", - "(JJLjava/lang/String;[Ljava/lang/Object;)Lorg/postgresql/pljava/internal/Portal;", + "(JJLjava/lang/String;[Ljava/lang/Object;S)Lorg/postgresql/pljava/internal/Portal;", Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen }, { @@ -45,7 +56,7 @@ void ExecutionPlan_initialize(void) }, { "_execute", - "(JJ[Ljava/lang/Object;I)I", + "(JJ[Ljava/lang/Object;SI)I", Java_org_postgresql_pljava_internal_ExecutionPlan__1execute }, { @@ -117,10 +128,10 @@ static bool coerceObjects(void* ePlan, jobjectArray jvalues, Datum** valuesPtr, /* * Class: org_postgresql_pljava_internal_ExecutionPlan * Method: _cursorOpen - * Signature: (JJLjava/lang/String;[Ljava/lang/Object;)Lorg/postgresql/pljava/internal/Portal; + * Signature: (JJLjava/lang/String;[Ljava/lang/Object;S)Lorg/postgresql/pljava/internal/Portal; */ JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jstring cursorName, jobjectArray jvalues) +Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jstring cursorName, jobjectArray jvalues, jshort readonly_spec) { jobject jportal = 0; if(_this != 0) @@ -138,12 +149,17 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jcla { Portal portal; char* name = 0; + bool read_only; if(cursorName != 0) name = String_createNTS(cursorName); Invocation_assertConnect(); + if ( SPI_READONLY_DEFAULT == readonly_spec ) + read_only = Function_isCurrentReadOnly(); + else + read_only = (SPI_READONLY_FORCED == readonly_spec); portal = SPI_cursor_open( - name, p2l.ptrVal, values, nulls, Function_isCurrentReadOnly()); + name, p2l.ptrVal, values, nulls, read_only); if(name != 0) pfree(name); if(values != 0) @@ -198,10 +214,10 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1isCursorPlan(JNIEnv* env, jc /* * Class: org_postgresql_pljava_internal_ExecutionPlan * Method: _execute - * Signature: (JJ[Ljava/lang/Object;I)V + * Signature: (JJ[Ljava/lang/Object;SI)V */ JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1execute(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jobjectArray jvalues, jint count) +Java_org_postgresql_pljava_internal_ExecutionPlan__1execute(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jobjectArray jvalues, jshort readonly_spec, jint count) { jint result = 0; if(_this != 0) @@ -217,9 +233,14 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1execute(JNIEnv* env, jclass p2l.longVal = _this; if(coerceObjects(p2l.ptrVal, jvalues, &values, &nulls)) { + bool read_only; Invocation_assertConnect(); + if ( SPI_READONLY_DEFAULT == readonly_spec ) + read_only = Function_isCurrentReadOnly(); + else + read_only = (SPI_READONLY_FORCED == readonly_spec); result = (jint)SPI_execute_plan( - p2l.ptrVal, values, nulls, Function_isCurrentReadOnly(), (int)count); + p2l.ptrVal, values, nulls, read_only, (int)count); if(result < 0) Exception_throwSPI("execute_plan", result); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java index 9cd5593c..7a2293a9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -23,6 +29,11 @@ public class ExecutionPlan static final float CACHE_LOAD_FACTOR = 0.75f; + /* These three values must match those in ExecutionPlan.c */ + public static final short SPI_READONLY_DEFAULT = 0; + public static final short SPI_READONLY_FORCED = 1; + public static final short SPI_READONLY_CLEARED = 2; + private long m_pointer; /** @@ -140,16 +151,21 @@ public void close() * @param cursorName Name of the cursor or null for a system * generated name. * @param parameters Values for the parameters. + * @param read_only One of the values {@code SPI_READONLY_DEFAULT}, + * {@code SPI_READONLY_FORCED}, or {@code SPI_READONLY_CLEARED} (in the + * default case, the native code will defer to + * {@code Function_isCurrentReadOnly}. * @return The Portal that represents the opened cursor. * @throws SQLException If the underlying native structure has gone stale. */ - public Portal cursorOpen(String cursorName, Object[] parameters) + public Portal cursorOpen( + String cursorName, Object[] parameters, short read_only) throws SQLException { synchronized(Backend.THREADLOCK) { return _cursorOpen(m_pointer, System.identityHashCode(Thread - .currentThread()), cursorName, parameters); + .currentThread()), cursorName, parameters, read_only); } } @@ -174,18 +190,23 @@ public boolean isCursorPlan() throws SQLException * Execute the plan using the internal SPI_execp function. * * @param parameters Values for the parameters. + * @param read_only One of the values {@code SPI_READONLY_DEFAULT}, + * {@code SPI_READONLY_FORCED}, or {@code SPI_READONLY_CLEARED} (in the + * default case, the native code will defer to + * {@code Function_isCurrentReadOnly}. * @param rowCount The maximum number of tuples to create. A value of * rowCount of zero is interpreted as no limit, * i.e., run to completion. * @return One of the status codes declared in class {@link SPI}. * @throws SQLException If the underlying native structure has gone stale. */ - public int execute(Object[] parameters, int rowCount) throws SQLException + public int execute(Object[] parameters, short read_only, int rowCount) + throws SQLException { synchronized(Backend.THREADLOCK) { return _execute(m_pointer, System.identityHashCode(Thread - .currentThread()), parameters, rowCount); + .currentThread()), parameters, read_only, rowCount); } } @@ -219,13 +240,14 @@ public static ExecutionPlan prepare(String statement, Oid[] argTypes) } private static native Portal _cursorOpen(long pointer, long threadId, - String cursorName, Object[] parameters) throws SQLException; + String cursorName, Object[] parameters, short read_only) + throws SQLException; private static native boolean _isCursorPlan(long pointer) throws SQLException; private static native int _execute(long pointer, long threadId, - Object[] parameters, int rowCount) throws SQLException; + Object[] parameters, short read_only, int rowCount) throws SQLException; private static native long _prepare(long threadId, String statement, Oid[] argTypes) throws SQLException; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java index 567838c4..a392fd5f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2018 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 @@ -50,8 +50,10 @@ public class SPI * of rowCount of zero is interpreted as no limit, i.e., * run to completion. * @return One of the declared status codes. + * @deprecated This seems never to have been used in git history of project. */ - public static int exec(String command, int rowCount) + @Deprecated + private static int exec(String command, int rowCount) { synchronized(Backend.THREADLOCK) { @@ -105,7 +107,10 @@ public static TupleTable getTupTable(TupleDesc known) } /** - * Returns a textual representatio of a result code + * Returns a textual representation of a result code. + */ + /* + * XXX PG 11 introduces a real SPI_result_code_string function. */ public static String getResultText(int resultCode) { @@ -181,7 +186,9 @@ public static String getResultText(int resultCode) return s; } + @Deprecated private native static int _exec(long threadId, String command, int rowCount); + private native static long _getProcessed(); private native static int _getResult(); private native static void _freeTupTable(); From 9ff8655fd5608327cdc2df7685e935a26b042b06 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Sep 2018 20:40:38 -0400 Subject: [PATCH 0211/1087] Give Statement an SPIReadOnlyControl interface. --- .../pljava/jdbc/SPIReadOnlyControl.java | 44 +++++++++++++++++++ .../postgresql/pljava/jdbc/SPIStatement.java | 30 +++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/jdbc/SPIReadOnlyControl.java diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIReadOnlyControl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIReadOnlyControl.java new file mode 100644 index 00000000..30bde8af --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIReadOnlyControl.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018 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.jdbc; + +/** + * An extended interface on {@code Statement} (accessible with {@code unwrap()}) + * allowing control of the {@code read_only} flag that PostgreSQL SPI will see + * when the statement is next executed. + *

    + * Currently an internal interface, not in {@code pljava-api}, as the known need + * so far is just for the internal class loader. + */ +public interface SPIReadOnlyControl +{ + /** + * Specify that the statement, when next executed, will have the + * behavior recommended in the PostgreSQL SPI documentation: + * {@code read_only} will be set to {@code true} if the currently-executing + * PL/Java function is declared {@code IMMUTABLE}, {@code false} otherwise. + */ + void defaultReadOnly(); + + /** + * Specify that the statement, when next executed, will have have + * {@code read_only} set to {@code true} unconditionally. + */ + void forceReadOnly(); + + /** + * Specify that the statement, when next executed, will have have + * {@code read_only} set to {@code false} unconditionally. + */ + void clearReadOnly(); +} + diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java index c8249a5f..76cc3d6e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java @@ -30,7 +30,7 @@ * * @author Thomas Hallgren */ -public class SPIStatement implements Statement +public class SPIStatement implements Statement, SPIReadOnlyControl { private final SPIConnection m_connection; @@ -43,6 +43,7 @@ public class SPIStatement implements Statement private long m_updateCount = 0; private ArrayList m_batch = null; private boolean m_closed = false; + private short m_readonly_spec = ExecutionPlan.SPI_READONLY_DEFAULT; public SPIStatement(SPIConnection conn) { @@ -130,14 +131,15 @@ protected boolean executePlan(ExecutionPlan plan, Object[] paramValues) boolean isResultSet = plan.isCursorPlan(); if(isResultSet) { - Portal portal = plan.cursorOpen(m_cursorName, paramValues); + Portal portal = plan.cursorOpen( + m_cursorName, paramValues, m_readonly_spec); m_resultSet = new SPIResultSet(this, portal, m_maxRows); } else { try { - plan.execute(paramValues, m_maxRows); + plan.execute(paramValues, m_readonly_spec, m_maxRows); m_updateCount = SPI.getProcessed(); } finally @@ -498,5 +500,27 @@ public void closeOnCompletion() throws SQLException + ".closeOneCompletion() not implemented yet.", "0A000" ); } + + // ************************************************************ + // Implementation of the SPIReadOnlyControl extended interface + // ************************************************************ + + @Override + public void defaultReadOnly() + { + m_readonly_spec = ExecutionPlan.SPI_READONLY_DEFAULT; + } + + @Override + public void forceReadOnly() + { + m_readonly_spec = ExecutionPlan.SPI_READONLY_FORCED; + } + + @Override + public void clearReadOnly() + { + m_readonly_spec = ExecutionPlan.SPI_READONLY_CLEARED; + } } From 163505c0393280fc7ed2fb03b4d6869a41362238 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Sep 2018 01:13:15 -0400 Subject: [PATCH 0212/1087] Have Loader force SPI read_only false as needed. This is meant to eliminate the hacks needed for classloading initiated from deployment descriptors in the course of install_jar or replace_jar, as detailed in issue #178. It is not the only possible approach, but was favored in https://www.postgresql.org/message-id/14247.1537418670%40sss.pgh.pa.us An approach that could be less invasive and tidier was proposed in https://www.postgresql.org/message-id/5BA6BA96.5030309%40anastigmatix.net but would presuppose later PostgreSQL API such as PushCopiedSnapshot, which does not appear until 9.1. On this branch, PL/Java still supports earlier versions, so this approach will have to do. --- .../org/postgresql/pljava/sqlj/Loader.java | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 10eede17..8c4502e8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2018 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.sqlj; @@ -30,6 +36,33 @@ import org.postgresql.pljava.internal.Oid; import org.postgresql.pljava.jdbc.SQLUtils; +/* + * Import an interface (internal-only, at least for now) that allows overriding + * the default derivation of the read_only parameter to SPI query execution + * functions. The default behavior follows the recommendation in the SPI docs + * to use read_only => true if the currently-executing PL function is declared + * IMMUTABLE, and read_only => false otherwise. + * + * Several queries in this class will use this interface to force read_only to + * be false, even though the queries clearly do nothing but reading. The reason + * may not be obvious: + * + * One effect of the read_only parameter in SPI is the selection of the snapshot + * used to evaluate the query. When read_only is true, a snapshot from the + * beginning of the command is used, which cannot see even the modifications + * made in this transaction since that point. + * + * Where that becomes a problem is during evaluation of a deployment descriptor + * as part of install_jar or replace_jar. The command began by loading some new + * or changed classes, and is now executing deployment commands, which may very + * well need to load those classes. But some of the loading requests may happen + * to come through functions that are declared IMMUTABLE (type IO functions, for + * example), which, under the default behavior, would mean SPI gets passed + * read_only => true and selects a snapshot from before the new classes were + * there, and loading fails. That is why read_only is always forced false here. + */ +import org.postgresql.pljava.jdbc.SPIReadOnlyControl; + /** * @author Thomas Hallgren */ @@ -146,6 +179,8 @@ public static ClassLoader getSchemaLoader(String schemaName) "SELECT entryId, entryName FROM sqlj.jar_entry " + "WHERE jarId OPERATOR(pg_catalog.=) ?"); + outer.unwrap(SPIReadOnlyControl.class).clearReadOnly(); + inner.unwrap(SPIReadOnlyControl.class).clearReadOnly(); outer.setString(1, schemaName); ResultSet rs = outer.executeQuery(); try @@ -231,6 +266,7 @@ public Object get(Object key) }; ClassLoader loader = Loader.getSchemaLoader(schema); Statement stmt = SQLUtils.getDefaultConnection().createStatement(); + stmt.unwrap(SPIReadOnlyControl.class).clearReadOnly(); ResultSet rs = null; try { @@ -335,6 +371,7 @@ protected Class findClass(final String name) "WHERE entryId OPERATOR(pg_catalog.=) ?"); stmt.setInt(1, entryId[0]); + stmt.unwrap(SPIReadOnlyControl.class).clearReadOnly(); rs = stmt.executeQuery(); if(rs.next()) { From c0f71d021a3f326468d07f7b30085d7f291ffe92 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Sep 2018 01:29:40 -0400 Subject: [PATCH 0213/1087] Remove workarounds that were formerly needed. ... in the process, silencing the PostgreSQL warnings that the VarlenaUDTTest I/O functions should not be VOLATILE. --- .../example/annotation/ComplexScalar.java | 25 ++----------------- .../example/annotation/UDTScalarIOTest.java | 24 ++---------------- .../example/annotation/VarlenaUDTTest.java | 6 +++++ 3 files changed, 10 insertions(+), 45 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 215deed4..d35cacae 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -32,10 +32,8 @@ /** * Complex (re and im parts are doubles) implemented in Java as a scalar UDT. */ -@SQLAction(requires={ - "scalar complex type", "complex assertHasValues", "complexscalar boot fn" - }, install={ - "SELECT javatest.complexscalar()", +@SQLAction(requires = { "scalar complex type", "complex assertHasValues" }, + install = { "SELECT javatest.assertHasValues(" + " CAST('(1,2)' AS javatest.complex), 1, 2)" } @@ -149,23 +147,4 @@ public void writeSQL(SQLOutput stream) throws SQLException { stream.writeDouble(m_x); stream.writeDouble(m_y); } - - /** - * A no-op function that forces the ComplexScalar class to be loaded. - * This is only necessary because the deployment-descriptor install - * actions contain a query making use of this type, and PostgreSQL does - * not expect type in/out/send/recv functions to need an updated - * snapshot, so it will try to find this class in the snapshot from - * before the jar was installed, and fail. By providing this function, - * which defaults to volatile so it gets an updated snapshot, and - * calling it first, the class will be found and loaded; once it is - * loaded, the user-defined type operations are able to find it. - *

    - * Again, this is only an issue when trying to make use of the newly - * loaded UDT from right within the deployment descriptor for the jar. - */ - @Function(schema="javatest", provides="complexscalar boot fn") - public static void ComplexScalar() - { - } } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java index 7d970b41..0ead377d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UDTScalarIOTest.java @@ -54,9 +54,8 @@ * supported JDBC data types, which it writes on output, and reads/verifies on * input. */ -@SQLAction(requires={"udtscalariotest type", "udtscalariotest boot fn"}, - install={ - "SELECT javatest.udtscalariotest()", // force class to resolve +@SQLAction(requires= { "udtscalariotest type" }, + install = { "SELECT CAST('' AS javatest.udtscalariotest)" // test send/recv }) @BaseUDT(schema="javatest", provides="udtscalariotest type") @@ -217,23 +216,4 @@ public void readSQL(SQLInput stream, String typeName) throws SQLException if ( ! s_url.equals(stream.readURL()) ) throw new SQLException("url mismatch"); } - - /** - * A no-op function that forces the UDTScalarIOTest class to be loaded. - * This is only necessary because the deployment-descriptor install - * actions contain a query making use of this type, and PostgreSQL does - * not expect type in/out/send/recv functions to need an updated - * snapshot, so it will try to find this class in the snapshot from - * before the jar was installed, and fail. By providing this function, - * which defaults to volatile so it gets an updated snapshot, and - * calling it first, the class will be found and loaded; once it is - * loaded, the user-defined type operations are able to find it. - *

    - * Again, this is only an issue when trying to make use of the newly - * loaded UDT from right within the deployment descriptor for the jar. - */ - @Function(schema="javatest", provides="udtscalariotest boot fn") - public static void udtscalariotest() - { - } } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java index db1cb76a..ae1be899 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java @@ -18,6 +18,8 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.BaseUDT; +import org.postgresql.pljava.annotation.Function; +import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; /** * A User Defined Type with varlena storage, testing github issue 52. @@ -46,6 +48,7 @@ public class VarlenaUDTTest implements SQLData { public VarlenaUDTTest() { } + @Function(effects=IMMUTABLE) public static VarlenaUDTTest parse( String s, String typname) { int i = Integer.parseInt( s); VarlenaUDTTest u = new VarlenaUDTTest(); @@ -54,6 +57,7 @@ public static VarlenaUDTTest parse( String s, String typname) { return u; } + @Function(effects=IMMUTABLE) public String toString() { return String.valueOf( apop); } @@ -62,11 +66,13 @@ public String getSQLTypeName() { return typname; } + @Function(effects=IMMUTABLE) public void writeSQL( SQLOutput stream) throws SQLException { for ( int i = 0 ; i < apop ; ++ i ) stream.writeByte( (byte)'a'); } + @Function(effects=IMMUTABLE) public void readSQL( SQLInput stream, String typname) throws SQLException { this.typname = typname; int i = 0; From 3b135090ffea4eab21894386d94cb29539ddb822 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 28 Sep 2018 21:31:38 -0400 Subject: [PATCH 0214/1087] No really, avoid #if among macro arguments. Thanks to Ken Olson for the reminder that the Windows MSVC compiler won't accept conditional-compilation syntax within an invocation of a preprocessor macro. 5e01f8f was already about that, but a few uses have crept back in through my inattention. Honestly, MSVC is doing a service here, as a quick check of the standard shows that preprocessing directives within macro invocations really do have undefined behavior. Addresses issue #182. --- pljava-so/src/main/c/Backend.c | 36 ++++++++++++++++------------ pljava-so/src/main/c/InstallHelper.c | 7 ++++-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index f319d9d9..3f1495b1 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1356,6 +1356,22 @@ static jint initializeJavaVM(JVMOptList *optList) GUCBOOTVAL(bootValue) (context), GUCFLAGS(flags) GUCCHECK(check_hook) \ assign_hook, show_hook) +#ifndef PLJAVA_LIBJVMDEFAULT +#define PLJAVA_LIBJVMDEFAULT "libjvm" +#endif + +#if PG_VERSION_NUM >= 90200 +#define PLJAVA_ENABLE_DEFAULT true +#else +#define PLJAVA_ENABLE_DEFAULT false +#endif + +#if PG_VERSION_NUM < 110000 +#define PLJAVA_IMPLEMENTOR_FLAGS GUC_LIST_INPUT | GUC_LIST_QUOTE +#else +#define PLJAVA_IMPLEMENTOR_FLAGS GUC_LIST_INPUT +#endif + static void registerGUCOptions(void) { static char pathbuf[MAXPGPATH]; @@ -1365,11 +1381,7 @@ static void registerGUCOptions(void) "Path to the libjvm (.so, .dll, etc.) file in Java's jre/lib area", NULL, /* extended description */ &libjvmlocation, - #ifdef PLJAVA_LIBJVMDEFAULT - PLJAVA_LIBJVMDEFAULT, - #else - "libjvm", - #endif + PLJAVA_LIBJVMDEFAULT, PGC_SUSET, 0, /* flags */ check_libjvm_location, @@ -1442,11 +1454,7 @@ static void registerGUCOptions(void) "settings changed before LOADing PL/Java may be rejected, so they must " "be made after LOAD, but before the virtual machine is started.", &pljavaEnabled, - #if PG_VERSION_NUM >= 90200 - true, /* boot value */ - #else - false, /* boot value */ - #endif + PLJAVA_ENABLE_DEFAULT, /* boot value */ PGC_USERSET, 0, /* flags */ check_enabled, /* check hook */ @@ -1460,11 +1468,7 @@ static void registerGUCOptions(void) &implementors, "postgresql", /* boot value */ PGC_USERSET, - GUC_LIST_INPUT - #if PG_VERSION_NUM < 110000 - | GUC_LIST_QUOTE - #endif - , + PLJAVA_IMPLEMENTOR_FLAGS, NULL, /* check hook */ NULL, NULL); /* assign hook, show hook */ @@ -1478,6 +1482,8 @@ static void registerGUCOptions(void) #undef BOOL_GUC #undef INT_GUC #undef STRING_GUC +#undef PLJAVA_ENABLE_DEFAULT +#undef PLJAVA_IMPLEMENTOR_FLAGS static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS); diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 06b79553..94384a28 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -517,11 +517,14 @@ char *InstallHelper_hello() nativeVer = String_createJavaStringFromNTS(SO_VERSION_STRING); serverBuiltVer = String_createJavaStringFromNTS(PG_VERSION_STR); - InitFunctionCallInfoData(fcinfo, NULL, 0, #if PG_VERSION_NUM >= 90100 + InitFunctionCallInfoData(fcinfo, NULL, 0, InvalidOid, /* collation */ -#endif NULL, NULL); +#else + InitFunctionCallInfoData(fcinfo, NULL, 0, + NULL, NULL); +#endif runningVer = DatumGetTextP(pgsql_version(&fcinfo)); serverRunningVer = String_createJavaString(runningVer); pfree(runningVer); From 542576eb88b20214d2eaa75dbd7a8194c8b94d7f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 28 Sep 2018 21:33:18 -0400 Subject: [PATCH 0215/1087] Exclude Saxon 9.9 for now. Saxon 9.9 is out, but some API has changed, so it is not drop-in. Addresses issue #185. --- pljava-examples/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index e64805fa..bc966369 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -17,7 +17,7 @@ net.sf.saxon Saxon-HE - [9.8.0-14,) + [9.8.0-14,9.9) From e019799690bdfab3be22784cf5c5bdcf5eda4031 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 1 Oct 2018 23:05:20 -0400 Subject: [PATCH 0216/1087] Explicitly pass parked and actual as jlong. The size of Size can be architecture-specific and differ from that of jlong. --- pljava-so/src/main/c/VarlenaWrapper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 3f2d28a7..e347c904 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -249,7 +249,7 @@ jobject pljava_VarlenaWrapper_Input( vr = JNI_newObjectLocked(s_VarlenaWrapper_Input_class, s_VarlenaWrapper_Input_init, pljava_DualState_key(), p2lro.longVal, p2lcxt.longVal, p2lpin.longVal, p2ldatum.longVal, - parked, actual, dbb); + (jlong)parked, (jlong)actual, dbb); if ( NULL != dbb ) JNI_deleteLocalRef(dbb); From 08245a49ffa6216cd8933029482ce2a8bdd50d2f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 3 Oct 2018 21:03:28 -0400 Subject: [PATCH 0217/1087] Catch up release notes and versions.md. Also add the date to the latest prior release (for consistency with earlier entries). --- src/site/markdown/build/versions.md | 2 +- src/site/markdown/releasenotes.md.vm | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index 408ad1b5..df83f5dd 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -61,5 +61,5 @@ More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. PL/Java 1.5.1 has been successfully built and run on at least one platform -with PostgreSQL versions from 11 (beta3) to 8.2, the latest maintenance +with PostgreSQL versions from 11 (beta4) to 8.2, the latest maintenance release for each. diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 4abb12ba..65b97de4 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -81,7 +81,7 @@ available in every PostgreSQL version PL/Java currently supports. $h3 Version compatibility -PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11beta3, +PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11beta4, and older ones back to 8.2, using Java SE 8, 7, or 6. It can _run_ using newer Java versions including Java 10 (and is expected to work with Java 11 when that is released). PL/Java functions can be written for, and use features of, the @@ -342,7 +342,11 @@ and a number of considerations for making a PL/Java package. $h3 Enhancement requests addressed -$h4 Since 1.5.1-BETA1 +$h4 Since 1.5.1-BETA2 + +* [Add a ddr.reproducible option to SQL generator](${ghbug}186) + +$h4 In 1.5.1-BETA2 * [java 8 date/time api](${ghbug}137) * [Annotations don't support CREATE CONSTRAINT TRIGGER](${ghbug}138) @@ -352,7 +356,15 @@ $h4 Since 1.5.1-BETA1 $h3 Bugs fixed -$h4 Since 1.5.1-BETA1 +$h4 Since 1.5.1-BETA2 + +* [self-install jar ClassCastException (...ConsString to String), some java 6/7 runtimes](${ghbug}179) +* [i386 libjvm_location gets mangled as .../jre/lib/1/server/libjvm.so](${ghbug}176) +* [java.lang.ClassNotFoundException installing examples jar](${ghbug}178) +* [Preprocessor errors building on Windows with MSVC](${ghbug}182) +* [Saxon example does not build since Saxon 9.9 released](${ghbug}185) + +$h4 In 1.5.1-BETA2 * [PostgreSQL 10: SPI_modifytuple failed with SPI_ERROR_UNCONNECTED](${ghbug}134) * [SPIConnection prepareStatement doesn't recognize all parameters](${ghbug}136) @@ -398,7 +410,7 @@ $h2 Earlier releases #set($h4 = '#####') #set($h5 = '######') -$h2 PL/Java 1.5.0 +$h2 PL/Java 1.5.0 (29 March 2016) This, the first PL/Java numbered release since 1.4.3 in 2011, combines compatibility with the latest PostgreSQL and Java versions with modernized From be8b43c938a51c4f22bf9db28e7e17654b249824 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 5 Oct 2018 19:36:17 -0400 Subject: [PATCH 0218/1087] Detect failure to rename over existing file. Handle some platforms (*cough* Windows *cough*) where the write-as-temp-file-then-rename technique is complicated by the inability to rename over an existing file. --- org/gjt/cuspy/JarX.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/org/gjt/cuspy/JarX.java b/org/gjt/cuspy/JarX.java index 3c022260..42a006b6 100644 --- a/org/gjt/cuspy/JarX.java +++ b/org/gjt/cuspy/JarX.java @@ -640,7 +640,10 @@ public void extract( JarEntry je, InputStream is) } } - tmpf.renameTo( f); + if ( ! tmpf.renameTo( f) ) { + if ( ! f.delete() || ! tmpf.renameTo( f) ) + System.err.println( "RENAME FAILED!"); + } } /**Copy bytes from an input to an output stream until end. From 4247447f86f239dc4db8bc8ce9845895377bbfa0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 6 Oct 2018 13:32:46 -0400 Subject: [PATCH 0219/1087] Add a test for issue #192. It's sufficient to use the TypeRoundTripper to ask for the JDBC class name for a column of type point (after the Point example has installed itself as a mirror of that type). The call to ResultSetMetaData's getColumnClassName() fails, because it uses Oid.getJavaClass, which tries to use the class loader that loaded Oid itself, not a schema loader that could see the installed jar providing Point. In passing, remove some unnecessary alias names from column definition lists in the documentation and tests. At some point, I must have been convinced they were needed, but this code is intended as an example, so shouldn't be needlessly cluttered. --- .../pljava/example/jdk7/TypeRoundTripper.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java index 662ff775..d90a8903 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/jdk7/TypeRoundTripper.java @@ -86,14 +86,14 @@ * orig = roundtripped AS good, * *FROM * (VALUES (timestamptz '2017-08-21 18:25:29.900005Z')) AS p(orig), - * roundtrip(p) AS r(roundtripped timestamptz); + * roundtrip(p) AS (roundtripped timestamptz); * *

    * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example. */ @SQLAction(implementor = "postgresql_ge_90300", // funcs see earlier FROM items - requires = "TypeRoundTripper.roundTrip", + requires = {"TypeRoundTripper.roundTrip", "point mirror type"}, install = { " SELECT" + " CASE WHEN every(orig = roundtripped)" + @@ -106,7 +106,7 @@ " (timestamp '1970-03-07 17:37:49.300009')," + " (timestamp '1919-05-29 13:08:33.600001')" + " ) AS p(orig)," + - " roundtrip(p) AS r(roundtripped timestamp)", + " roundtrip(p) AS (roundtripped timestamp)", " SELECT" + " CASE WHEN every(orig = roundtripped)" + @@ -119,7 +119,16 @@ " (timestamptz '1970-03-07 17:37:49.300009Z')," + " (timestamptz '1919-05-29 13:08:33.600001Z')" + " ) AS p(orig)," + - " roundtrip(p) AS r(roundtripped timestamptz)", + " roundtrip(p) AS (roundtripped timestamptz)", + + " SELECT" + + " CASE WHEN classjdbc = 'org.postgresql.pljava.example.annotation.Point'" + + " THEN javatest.logmessage('INFO', 'issue192 test passes')" + + " ELSE javatest.logmessage('WARNING', 'issue192 test fails')" + + " END" + + " FROM" + + " (VALUES (point '0,0')) AS p," + + " roundtrip(p) AS (classjdbc text)", } ) public class TypeRoundTripper From bc81bf5d778edc1a01064d9f9b65b3c8c5592321 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 6 Oct 2018 13:20:19 -0400 Subject: [PATCH 0220/1087] Use schema class loader in Oid.getJavaClass Add a Function_getCurrentLoader() to retrieve a saved reference to the initiating loader used to find the function's containing class. That should be the "schema loader" for the schema in which the function is declared. Tempting to cheat and just use getClassLoader on the function's class, but that would return the defining loader, in cases where it and the initiating loader are different. The choice to use the schema loader for the innermost executing PL/Java function is for consistency with the documented convention (in the developer notes) of using the innermost executing function's schema to determine the type map to use. Taking a step back, the whole Oid class seems like something between a misnomer and a solution-in-search-of-a-problem. It is something of a stash for information on the correspondences between PG and Java types, but hardly a complete one, and a lot of that information is in other places; meanwhile, plenty of things in PG, besides types, have Oids. In some future version, I envision this Oid class gone, with its type mapping information merged into something more like a much-evolved TypeBridge that would centralize a lot of type-correspondence knowledge that is held now in many places. There might be an entirely different class to represent oids, if that is useful, with subclasses corresponding to the different reg* oid types in PG. But for now, the existing Oid class gets nursed along. --- pljava-so/src/main/c/Function.c | 40 ++++++++++++++++--- pljava-so/src/main/c/type/Oid.c | 21 ++++++++++ pljava-so/src/main/include/pljava/Function.h | 7 ++++ .../org/postgresql/pljava/internal/Oid.java | 15 ++++++- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 475dfc78..1ee5bc86 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -61,6 +61,12 @@ struct Function_ */ jclass clazz; + /** + * Weak global reference to the class loader for the schema in which this + * function is declared. + */ + jweak schemaLoader; + union { struct @@ -499,7 +505,8 @@ static void setupUDT(Function self, ParseResult info, Form_pg_proc procStruct) ReleaseSysCache(typeTup); } -static jclass Function_loadClass(jstring schemaName, char const *className); +static jclass Function_loadClass( + jstring schemaName, char const *className, jweak *loaderref); Type Function_checkTypeUDT(Oid typeId, Form_pg_type typeStruct) { @@ -524,7 +531,7 @@ Type Function_checkTypeUDT(Oid typeId, Form_pg_type typeStruct) procStruct = (Form_pg_proc)GETSTRUCT(procTup); schemaName = getSchemaName(procStruct->pronamespace); - clazz = Function_loadClass(schemaName, info.className); + clazz = Function_loadClass(schemaName, info.className, NULL); JNI_deleteLocalRef(schemaName); t = (Type)UDT_registerUDT(clazz, typeId, typeStruct, 0, true); @@ -595,7 +602,8 @@ static void Function_init(Function self, ParseResult info, Form_pg_proc procStru currentInvocation->function = self; - self->clazz = Function_loadClass(schemaName, info->className); + self->clazz = Function_loadClass( + schemaName, info->className, &(self->schemaLoader)); JNI_deleteLocalRef(schemaName); @@ -674,9 +682,11 @@ static void Function_init(Function self, ParseResult info, Form_pg_proc procStru } /* - * Return a global ref to the loaded class. + * Return a global ref to the loaded class. Store a weak global ref to the + * initiating loader at *loaderref if non-null. */ -static jclass Function_loadClass(jstring schemaName, char const *className) +static jclass Function_loadClass( + jstring schemaName, char const *className, jweak *loaderref) { jobject tmp; jobject loader; @@ -691,6 +701,10 @@ static jclass Function_loadClass(jstring schemaName, char const *className) classJstr = String_createJavaStringFromNTS(className); tmp = JNI_callObjectMethod(loader, s_ClassLoader_loadClass, classJstr); + + if ( NULL != loaderref ) + *loaderref = JNI_newWeakGlobalRef(loader); + JNI_deleteLocalRef(loader); JNI_deleteLocalRef(classJstr); @@ -898,3 +912,19 @@ bool Function_isCurrentReadOnly(void) return currentInvocation->function->readOnly; } +jobject Function_currentLoader(void) +{ + Function f; + jweak weakRef; + + if ( NULL == currentInvocation ) + return NULL; + f = currentInvocation->function; + if ( NULL == f ) + return NULL; + weakRef = f->schemaLoader; + if ( NULL == weakRef ) + return NULL; + return JNI_newLocalRef(weakRef); +} + diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index 19f823e0..6e74d9ae 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -18,6 +18,7 @@ #include "pljava/type/Oid.h" #include "pljava/type/String.h" #include "pljava/Exception.h" +#include "pljava/Function.h" #include "pljava/Invocation.h" static jclass s_Oid_class; @@ -193,6 +194,11 @@ void Oid_initialize(void) "(I)Ljava/lang/String;", Java_org_postgresql_pljava_internal_Oid__1getJavaClassName }, + { + "_getCurrentLoader", + "()Ljava/lang/ClassLoader;", + Java_org_postgresql_pljava_internal_Oid__1getCurrentLoader + }, { 0, 0, 0 }}; jobject tmp; @@ -304,3 +310,18 @@ Java_org_postgresql_pljava_internal_Oid__1getJavaClassName(JNIEnv* env, jclass c END_NATIVE return result; } + +/* + * Class: org_postgresql_pljava_internal_Oid + * Method: _getCurrentLoader + * Signature: ()Ljava/lang/ClassLoader; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_Oid__1getCurrentLoader(JNIEnv *env, jclass cls) +{ + jobject result; + BEGIN_NATIVE + result = Function_currentLoader(); + END_NATIVE + return result; +} diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 5f3b27dc..c9383e77 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -74,6 +74,13 @@ extern jobject Function_getTypeMap(Function self); */ extern bool Function_isCurrentReadOnly(void); +/* + * Return a local reference to the initiating (schema) class loader used to load + * the currently-executing function, or NULL if there is no currently-executing + * function or the schema loaders have been cleared and that loader is gone. + */ +extern jobject Function_currentLoader(void); + /* * A nameless Function singleton with the property ! isCurrentReadOnly() */ diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java index 7d3a9778..3e079e83 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java @@ -149,13 +149,18 @@ public Class getJavaClass() if(c == null) { String className; + ClassLoader loader; synchronized(Backend.THREADLOCK) { className = _getJavaClassName(m_native); + loader = _getCurrentLoader(); } try { - c = Class.forName(getCanonicalClassName(className, 0)); + String canonName = getCanonicalClassName(className, 0); + if ( null == loader ) + loader = getClass().getClassLoader(); + c = Class.forName(canonName, true, loader); } catch(ClassNotFoundException e) { @@ -247,4 +252,12 @@ private native static int _forSqlType(int sqlType) private native static String _getJavaClassName(int nativeOid) throws SQLException; + + /** + * Return the (initiating, "schema") ClassLoader of the innermost + * currently-executing PL/Java function, or null if there is none or the + * schema loaders have since been cleared and the loader is gone. + */ + private native static ClassLoader _getCurrentLoader() + throws SQLException; } From 3a9dbbbf1d0b371fb92f9eca2dbf3a4a5a01a01b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 6 Oct 2018 00:25:44 -0400 Subject: [PATCH 0221/1087] Avoid timestamp in SetOfRecordTest. The SetOfRecordTest is only there to demonstrate that sets of RECORD can be returned, and that casts are done from a ResultSetHandle as needed. Using a timestamp as one of the test values can make it also (in some circumstances) a demonstration of java.sql.Timestamp's unintuitive dependence on local time zone settings ... which is why the JDBC 4.2 mapping to java.time.LocalDateTime is preferable, which the documentation and release notes already explain. The purpose of SetOfRecordTest can be achieved just as well using types that don't have that sort of environment dependency. --- .../pljava/example/annotation/SetOfRecordTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java index 976c8cab..6fc74dbe 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java @@ -36,16 +36,16 @@ " SELECT " + " CASE WHEN r IS DISTINCT FROM ROW('Foo'::varchar, 1::integer, 1.5::float, " + " 23.67::decimal(8,2), '2005-06-01'::date, '20:56'::time, " + -" '2006-02-04 23:55:10'::timestamp) " + +" '192.168'::cidr) " + " THEN javatest.logmessage('WARNING', 'SetOfRecordTest not ok') " + " ELSE javatest.logmessage('INFO', 'SetOfRecordTest ok') " + " END " + " FROM " + " javatest.executeselecttorecords( " + " 'select ''Foo'', 1, 1.5, 23.67, ''2005-06-01'', ''20:56''::time, " + -" ''2006-02-04 23:55:10''') " + +" ''192.168.0''') " + " AS r(t_varchar varchar, t_integer integer, t_float float, " + -" t_decimal decimal(8,2), t_date date, t_time time, t_timestamp timestamp)" +" t_decimal decimal(8,2), t_date date, t_time time, t_cidr cidr)" ) public class SetOfRecordTest implements ResultSetHandle { From c676735abedd609cab3d2b9d175e32e60b4bb00d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 6 Oct 2018 19:47:31 -0400 Subject: [PATCH 0222/1087] Release notes and documentation polishing Java 11 is out now, so replace the "expected to work in" with "works in". Call attention to Oracle's licensing change in Java 11. Clarify the considerations around building from unreleased snapshot sources, and how to determine the branch with most recent activity. Add a note on the SQL syntax needed to call the Saxon-based example functions as one travels back through PostgreSQL versions. Mention issue #190 and suggest a workaround for builders of packages for Windows. --- src/site/markdown/build/build.md | 6 ++++++ src/site/markdown/build/package.md | 12 ++++++++++-- src/site/markdown/build/versions.md | 9 ++++++--- src/site/markdown/examples/saxon.md | 9 +++++++++ src/site/markdown/install/appcds.md | 22 +++++++++++++++++++--- src/site/markdown/install/vmoptions.md | 12 ++++++++++++ src/site/markdown/releasenotes.md.vm | 8 ++++---- 7 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index d8936ec0..fa6a2b38 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -117,6 +117,12 @@ sources. From a clone, you can also build specific released versions, by first using `git checkout` with the tag that identifies the release. +Building from unreleased, development sources will be of most interest when +hacking on PL/Java itself. The GitHub "Branches" page can be used to see which +branch has had the most recent development activity (this will not always be +the branch named `master`; periods of development can be focused on the branch +corresponding to current releases). + [git]: https://git-scm.com/ ## The build diff --git a/src/site/markdown/build/package.md b/src/site/markdown/build/package.md index 7f5834a2..aa65c48d 100644 --- a/src/site/markdown/build/package.md +++ b/src/site/markdown/build/package.md @@ -21,14 +21,22 @@ where the `...` is the path to the JVM library shared object where it would be by default on your target platform. See [here][locatejvm] to find the exact file this should refer to. -[locatejvm]: ../install/locatejvm.html - Although PL/Java is currently built with a JDK no later than Java 8, it can then run in the database with a newer JVM, and support application code that uses the newer Java features. While using Java 8 to build a package, you are encouraged to set the default `pljava.libjvm_location` to the library of a later JRE that is expected to be present on your platform. +**Note:** when building on Windows, the `-Dpljava.libjvmdefault` option is +likely to produce a failed build or the wrong stored value for the library +path. A fix for this option on Windows is unlikely (see [issue 190][bug190]); +if preparing a package for Windows, it will be simplest to use a patch that +changes the definition of `PLJAVA_LIBJVMDEFAULT` in +`pljava-so/src/main/c/Backend.c`. + +[locatejvm]: ../install/locatejvm.html +[bug190]: https://github.com/tada/pljava/issues/190 + ## What kind of a package is this? Your package may be for a distribution that has formal guidelines for how diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index df83f5dd..0a9a8f05 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -1,6 +1,6 @@ # Versions of external packages needed to build and use PL/Java -As of August 2018, the following version constraints are known. +As of October 2018, the following version constraints are known. ## Java @@ -8,8 +8,11 @@ No version of Java before 1.6 ("Java 6") is supported. The PL/Java code makes use of Java features first appearing in Java 6. As for later versions of Java, backward compatibility in the language is -generally good. The most likely problem areas with a new Java version will -be additions to the JDBC API that PL/Java has not yet implemented. +generally good. Before Java 8, most likely problem areas with a new Java +version tended to be additions to the JDBC API that PL/Java had not yet +implemented. Since Java 8, even JDBC additions have not caused problems for +existing PL/Java code, as they have taken advantage of the default-methods +feature introduced in that release. In the PL/Java 1.5.x series, the build system has not been reworked for building with Java 9 or newer. However, PL/Java can be built with Java 8 diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md index fb4d225a..bb33463c 100644 --- a/src/site/markdown/examples/saxon.md +++ b/src/site/markdown/examples/saxon.md @@ -148,6 +148,15 @@ of an empty sequence. This lacks, for now, some functionality of the standard `XMLTABLE`, where the default expression can refer to other columns of the same output row. +#### Syntax in older PostgreSQL versions + +The desugared syntax shown above can be used in PostgreSQL versions as old +as 9.5. In 9.4 and 9.3, the same syntax, but with `=>` replaced by `:=` for +the named parameters, can be used. The functions remain usable in still +earlier PostgreSQL versions, but with increasingly convoluted SQL syntax +needed to call them (before 9.3, for example, there was no `LATERAL` in a +`SELECT`, a function could not refer to earlier `FROM` items, and so on). + ### Minimizing startup time Saxon is a large library, and benefits greatly from precompilation into a diff --git a/src/site/markdown/install/appcds.md b/src/site/markdown/install/appcds.md index b401ccf2..3ac4f7b8 100644 --- a/src/site/markdown/install/appcds.md +++ b/src/site/markdown/install/appcds.md @@ -18,6 +18,7 @@ feature is covered [on its own page][cdsJ9]. [bcl]: http://www.oracle.com/technetwork/java/javase/terms/license/index.html [OpenJDK]: https://adoptopenjdk.net/ [cdsJ9]: oj9vmopt.html#How_to_set_up_class_sharing_in_OpenJ9 +[o]: https://blogs.oracle.com/java-platform-group/oracle-jdk-releases-for-java-11-and-later ## License considerations @@ -34,8 +35,22 @@ additional performance margin can be given a price. The same feature in OpenJDK with Hotspot is available from Java 10 onward, and does not require any additional license or `-XX:+UnlockCommercialFeatures` -option. The equivalent feature in OpenJDK with OpenJ9, -[described separately][cdsJ9] is available from Java 8 onward, also with no +option. + +Starting in Java 11, Oracle offers +[Oracle-branded downloads of both "Oracle JDK" and "Oracle's OpenJDK builds"][o] +that are "functionally identical aside from some cosmetic and packaging +differences". "Oracle's OpenJDK builds" may be used for production or +commercial purposes with no additional licensing, while any such use of +"Oracle JDK" requires a commercial license. The application class data sharing +feature is available in both, and no longer requires the +`-XX:+UnlockCommercialFeatures` option in either case (not in +"Oracle's OpenJDK builds" because their use is unrestricted, and not in +"Oracle JDK" because the "commercial feature" is now, effectively, the entire +JDK). + +The equivalent feature in OpenJDK with OpenJ9, +[described separately][cdsJ9], is available from Java 8 onward, also with no additional license or setup needed. ## Setup @@ -43,7 +58,8 @@ additional license or setup needed. The setup instructions on this page are for Hotspot, whether in Oracle Java or OpenJDK with Hotspot. The two differ only in that, wherever an `-XX:+UnlockCommercialFeatures` option is shown in the steps below, -**it is needed in Oracle Java but not in OpenJDK/Hotspot**. +**it is needed in Oracle Java 8, 9, or 10, but not in OpenJDK/Hotspot, or +Oracle JDK 11 or later**. Setting up PL/Java to use application class data sharing is a three-step process. Each step is done by setting a different combination of options diff --git a/src/site/markdown/install/vmoptions.md b/src/site/markdown/install/vmoptions.md index c6b7dcea..7956f31d 100644 --- a/src/site/markdown/install/vmoptions.md +++ b/src/site/markdown/install/vmoptions.md @@ -110,6 +110,17 @@ is available and set up in the same way, but is freely usable; it does not require any additional license, and does not require any `-XX:+UnlockCommercialFeatures` to be added to the options. +Starting in Java 11, Oracle offers +[Oracle-branded downloads of both "Oracle JDK" and "Oracle's OpenJDK builds"][o] +that are "functionally identical aside from some cosmetic and packaging +differences". "Oracle's OpenJDK builds" may be used for production or +commercial purposes with no additional licensing, while any such use of +"Oracle JDK" requires a commercial license. The application class data sharing +feature is available in both, and no longer requires the +`-XX:+UnlockCommercialFeatures` in either case (not in "Oracle's OpenJDK builds" +because their use is unrestricted, and not in "Oracle JDK" because the +"commercial feature" is now, effectively, the entire JDK). + [In OpenJDK (with OpenJ9)][OpenJDK], the class-sharing feature present from Java 8 onward will naturally share PL/Java's classes as well as the Java runtime's, with no additional setup steps. @@ -128,6 +139,7 @@ classes!) in OpenJ9][cdsJ9]. [iads]: appcds.html [vmoptJ9]: oj9vmopt.html [cdsJ9]: oj9vmopt.html#How_to_set_up_class_sharing_in_OpenJ9 +[o]: https://blogs.oracle.com/java-platform-group/oracle-jdk-releases-for-java-11-and-later ## `-XX:+DisableAttachMechanism` diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 65b97de4..77ca6e9d 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -83,10 +83,9 @@ $h3 Version compatibility PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11beta4, and older ones back to 8.2, using Java SE 8, 7, or 6. It can _run_ using newer -Java versions including Java 10 (and is expected to work with Java 11 when that -is released). PL/Java functions can be written for, and use features of, the -Java version loaded at run time. See [version compatibility][versions] for more -detail. +Java versions including Java 11. PL/Java functions can be written for, and use +features of, the Java version loaded at run time. See +[version compatibility][versions] for more detail. OpenJDK is supported, and can be downloaded in versions using the Hotspot or the OpenJ9 JVM. Features of modern Java VMs can drastically reduce memory footprint @@ -363,6 +362,7 @@ $h4 Since 1.5.1-BETA2 * [java.lang.ClassNotFoundException installing examples jar](${ghbug}178) * [Preprocessor errors building on Windows with MSVC](${ghbug}182) * [Saxon example does not build since Saxon 9.9 released](${ghbug}185) +* [Segfault in VarlenaWrapper.Input on 32-bit](${ghbug}177) $h4 In 1.5.1-BETA2 From bf6a5ee4c003d176bbaca8037e675842e651bc6f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Oct 2018 13:33:33 -0400 Subject: [PATCH 0223/1087] Release notes and documentation update --- src/site/markdown/examples/saxon.md | 7 +++++-- src/site/markdown/releasenotes.md.vm | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md index bb33463c..042a67e2 100644 --- a/src/site/markdown/examples/saxon.md +++ b/src/site/markdown/examples/saxon.md @@ -154,8 +154,11 @@ The desugared syntax shown above can be used in PostgreSQL versions as old as 9.5. In 9.4 and 9.3, the same syntax, but with `=>` replaced by `:=` for the named parameters, can be used. The functions remain usable in still earlier PostgreSQL versions, but with increasingly convoluted SQL syntax -needed to call them (before 9.3, for example, there was no `LATERAL` in a -`SELECT`, a function could not refer to earlier `FROM` items, and so on). +needed to call them; before 9.3, for example, there was no `LATERAL` in a +`SELECT`, and a function could not refer to earlier `FROM` items. Before 9.0, +named-parameter notation can't be used in function calls. Before 8.4, the +functions would have to be declared without their `DEFAULT` clauses and the +`IntervalStyle` settings, and would not work with PostgreSQL interval values. ### Minimizing startup time diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 77ca6e9d..b1204f41 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -363,6 +363,9 @@ $h4 Since 1.5.1-BETA2 * [Preprocessor errors building on Windows with MSVC](${ghbug}182) * [Saxon example does not build since Saxon 9.9 released](${ghbug}185) * [Segfault in VarlenaWrapper.Input on 32-bit](${ghbug}177) +* [Windows: self-install jar silently fails to replace existing files](${ghbug}189) +* [ERROR: java.sql.SQLException: _some Java class name_](${ghbug}192) +* [SetOfRecordTest with timestamp column influenced by environment ](${ghbug}195) $h4 In 1.5.1-BETA2 From 0fb319cd1ace6500017d98873c1d54588b28b6c5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Oct 2018 13:39:40 -0400 Subject: [PATCH 0224/1087] Preserve Java 6 buildability Maven devs released a plugin version that's Java 7+ only. --- pom.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 56ccbc35..3eb1cda6 100644 --- a/pom.xml +++ b/pom.xml @@ -69,10 +69,15 @@ + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + org.apache.maven.plugins maven-resources-plugin - 3.0.1 + 3.0.1 From cd183f2dd92bdc2cbec0d8c8cbadd68def924091 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Oct 2018 16:44:20 -0400 Subject: [PATCH 0225/1087] Poke migration-management versions for 1.5.1-BETA3. -packaging/build.xml already makes an update .sql from 1.5.1-BETA2, the last released version. No change needed before this release. --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index ac6dfbee..a80168fe 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -495,6 +495,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_5_1_BETA3 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA2 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA1 = REL_1_5_0; static final SchemaVariant REL_1_5_0_BETA3 = REL_1_5_0; From 98a29ad9e8d1ebdea208eb067953c7f1b1040c53 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Oct 2018 16:55:59 -0400 Subject: [PATCH 0226/1087] Add control file in preparation for next release. Now that 1.5.1-BETA3 is released, the next release should include an extension SQL file allowing upgrade from 1.5.1-BETA3. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 0c2ff971..8e6f4077 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Mon, 15 Oct 2018 21:32:58 -0400 Subject: [PATCH 0227/1087] Catch up build-version claims to PG 11. --- src/site/markdown/build/versions.md | 2 +- src/site/markdown/releasenotes.md.vm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index 0a9a8f05..8dac5fa1 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -64,5 +64,5 @@ More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. PL/Java 1.5.1 has been successfully built and run on at least one platform -with PostgreSQL versions from 11 (beta4) to 8.2, the latest maintenance +with PostgreSQL versions from 11 to 8.2, the latest maintenance release for each. diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index b1204f41..15eb8d93 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -81,7 +81,7 @@ available in every PostgreSQL version PL/Java currently supports. $h3 Version compatibility -PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11beta4, +PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11, and older ones back to 8.2, using Java SE 8, 7, or 6. It can _run_ using newer Java versions including Java 11. PL/Java functions can be written for, and use features of, the Java version loaded at run time. See From 0a1163588ca0d25c6fb0647c11b81563ea96e84c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 16 Oct 2018 23:55:06 -0400 Subject: [PATCH 0228/1087] Release notes and documentation update. Add credits to release notes, elaborate on Saxon example limitations, add note about Maven repositories no longer accepting secure connections with the old protocols in Java 6 and 7. A formerly noted Ubuntu issue is now resolved, with the solution of #179. --- src/site/markdown/build/build.md | 3 +++ src/site/markdown/build/ubuntu.md | 15 --------------- src/site/markdown/build/versions.md | 9 +++++++++ src/site/markdown/examples/saxon.md | 17 ++++++++++++++++- src/site/markdown/releasenotes.md.vm | 18 ++++++++++++++++-- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index fa6a2b38..9b3ba419 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -73,6 +73,7 @@ you expect. Please review any of the following that apply to your situation: * [Version compatibility](versions.html) +* [Maven failures when downloading dependencies][protofail] * Building on [FreeBSD](freebsd.html) * Building on [Mac OS X](macosx.html) * Building on [Solaris](solaris.html) @@ -88,6 +89,8 @@ Please review any of the following that apply to your situation: [making a package for a software distribution](package.html) * Building [with debugging or optimization options](debugopt.html) +[protofail]: versions.html#Maven_failures_when_downloading_dependencies + ## Obtaining PL/Java sources ### Sources for a specific release diff --git a/src/site/markdown/build/ubuntu.md b/src/site/markdown/build/ubuntu.md index 3d59c24f..26b20a06 100644 --- a/src/site/markdown/build/ubuntu.md +++ b/src/site/markdown/build/ubuntu.md @@ -11,19 +11,4 @@ packages, you may also need to separately install: * `libecpg-dev` * `libkrb5-dev` -## Self-extracting jar may fail with some Ubuntu-packaged Java versions - -The final product of the build is a jar file meant to be self-extracting -(it contains a short JavaScript snippet that runs `pg_config` to learn where -the extracted files should be put), but there seem to be issues with the -JavaScript engine in some Ubuntu-packaged Java 6 and Java 7 versions. -Java 8 works fine. There is more information in an -[Ubuntu bug report][ubr] and a [StackOverflow thread][sot]. - -In the worst case, if Java 8 is not an option and one of the affected Java 6 -or 7 builds must be used, simply extract the jar file normally and move the -few files it contains into their proper locations. - [gnxbi]: build.html -[ubr]: https://bugs.launchpad.net/ubuntu/+source/openjdk-7/+bug/1553654 -[sot]: http://stackoverflow.com/questions/35713768/ diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index 8dac5fa1..475f6d84 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -27,6 +27,15 @@ PL/Java has been successfully used with [Oracle Java][orj] and with [OpenJDK][], which is available with [either the Hotspot or the OpenJ9 JVM][hsj9]. +### Maven failures when downloading dependencies + +As of late 2017, important Maven remote repository servers no longer accept +connections using the encryption protocols available in Java 7 or 6. Although +PL/Java can still, in principle, be built using those Java versions (if all +dependencies are already in the build host's local repository), Maven may fail +to download necessary dependencies unless run with Java 8, which supports the +newer protocol versions needed to reach the servers. + [jvml]: ../use/variables.html [cds]: ../install/vmoptions.html#Class_data_sharing [orj]: https://www.oracle.com/technetwork/java/javase/downloads/index.html diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md index 042a67e2..49d16f23 100644 --- a/src/site/markdown/examples/saxon.md +++ b/src/site/markdown/examples/saxon.md @@ -27,7 +27,22 @@ Then use `sqlj.set_classpath` to set a path including both jars (`'ex:saxon'` if you used the names suggested above). This is work-in-progress code, currently incomplete, and for purposes of -example. +example. Two known current limitations: + +* `XMLTABLE` output columns can have non-XML ("atomic") types only. As a + common use of `XMLTABLE` is to process XML and get atomic types out, it + should be quite useful even with this limitation. An XML type can be + returned, if needed, by using a `text` output column, wrapping the XQuery + column expression in `serialize()`, and then applying SQL `XMLPARSE` to + the resulting column, at some cost in efficiency. + +* `XMLTABLE` column expressions must have the exact XQuery types corresponding + to the output columns' SQL types; the automatic casts provided in the spec + are not yet implemented. This is no blocker in practice, as any XQuery + column expression can be written with an explicit cast to the needed type, + which is exactly what the spec's automated behavior would be. + +Both of these limitations are intended to be temporary. ### Calling XML functions without SQL syntactic sugar diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 15eb8d93..45c5d393 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -341,7 +341,7 @@ and a number of considerations for making a PL/Java package. $h3 Enhancement requests addressed -$h4 Since 1.5.1-BETA2 +$h4 In 1.5.1-BETA3 * [Add a ddr.reproducible option to SQL generator](${ghbug}186) @@ -355,7 +355,7 @@ $h4 In 1.5.1-BETA2 $h3 Bugs fixed -$h4 Since 1.5.1-BETA2 +$h4 In 1.5.1-BETA3 * [self-install jar ClassCastException (...ConsString to String), some java 6/7 runtimes](${ghbug}179) * [i386 libjvm_location gets mangled as .../jre/lib/1/server/libjvm.so](${ghbug}176) @@ -402,6 +402,20 @@ $h3 Updated PostgreSQL APIs tracked * `AllocSetContextCreate` * `DefineCustom...Variable` (no `GUC_LIST_QUOTE` in extensions) +$h3 Credits + +There is a PL/Java 1.5.1 thanks in part to +Christoph Berg, +Thom Brown, +Luca Ferrari, +Chapman Flack, +Petr Michalek, +Steve Millington, +Kenneth Olson, +Fabian Zeindl, +original creator Thomas Hallgren, +and the many contributors to earlier versions. + $h2 Earlier releases ## A nice thing about using Velocity is that each release can be entered at From b69aaf52a1208d4d021f579578a863f41103955a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 17 Oct 2018 00:13:15 -0400 Subject: [PATCH 0229/1087] Add AOL properties for ppc64le.Linux.gpp. Christoph Berg submitted these properties upstream as a nar-maven-plugin pull request, but as long as that is not merged yet, may as well have the file here in case anyone needs it. --- pljava-so/aol.ppc64le-linux-gpp.properties | 48 ++++++++++++++++++++ src/site/markdown/build/build.md | 1 + src/site/markdown/build/ppc64le-linux-gpp.md | 9 ++++ 3 files changed, 58 insertions(+) create mode 100644 pljava-so/aol.ppc64le-linux-gpp.properties create mode 100644 src/site/markdown/build/ppc64le-linux-gpp.md diff --git a/pljava-so/aol.ppc64le-linux-gpp.properties b/pljava-so/aol.ppc64le-linux-gpp.properties new file mode 100644 index 00000000..7d5edf51 --- /dev/null +++ b/pljava-so/aol.ppc64le-linux-gpp.properties @@ -0,0 +1,48 @@ +## +# AOL properties for compilation on Linux ppc64le +# using the GNU tools. From Christoph Berg, not in upstream NAR plugin yet: +# https://github.com/maven-nar/nar-maven-plugin/pull/328/files +# +# To use these definitions, add -Dnar.aolProperties=path/to/this/file +# on the mvn command line. +## + +# +# PPC64LE Linux +# +ppc64le.Linux.linker=g++ + +ppc64le.Linux.gpp.cpp.compiler=g++ +ppc64le.Linux.gpp.cpp.defines=Linux GNU_GCC +ppc64le.Linux.gpp.cpp.options=-Wall -Wno-long-long -Wpointer-arith -Wconversion -fPIC -m64 +ppc64le.Linux.gpp.cpp.includes=**/*.cc **/*.cpp **/*.cxx +ppc64le.Linux.gpp.cpp.excludes= + +ppc64le.Linux.gpp.c.compiler=gcc +ppc64le.Linux.gpp.c.defines=Linux GNU_GCC +ppc64le.Linux.gpp.c.options=-Wall -Wno-long-long -Wpointer-arith -Wconversion -fPIC -m64 +ppc64le.Linux.gpp.c.includes=**/*.c +ppc64le.Linux.gpp.c.excludes= + +ppc64le.Linux.gpp.fortran.compiler=gfortran +ppc64le.Linux.gpp.fortran.defines=Linux GNU_GCC +ppc64le.Linux.gpp.fortran.options=-Wall +ppc64le.Linux.gpp.fortran.includes=**/*.f **/*.for **/*.f90 +ppc64le.Linux.gpp.fortran.excludes= + +ppc64le.Linux.gpp.java.include=include;include/linux +ppc64le.Linux.gpp.java.runtimeDirectory=jre/lib/ppc64le/server + +ppc64le.Linux.gpp.lib.prefix=lib +ppc64le.Linux.gpp.shared.prefix=lib +ppc64le.Linux.gpp.static.extension=a +ppc64le.Linux.gpp.shared.extension=so* +ppc64le.Linux.gpp.plugin.extension=so +ppc64le.Linux.gpp.jni.extension=so +ppc64le.Linux.gpp.executable.extension= + +# FIXME to be removed when NAR-6 +ppc64le.Linux.gcc.static.extension=a +ppc64le.Linux.gcc.shared.extension=so* +ppc64le.Linux.gcc.plugin.extension=so +ppc64le.Linux.gcc.jni.extension=so diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index 9b3ba419..26333ed3 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -78,6 +78,7 @@ Please review any of the following that apply to your situation: * Building on [Mac OS X](macosx.html) * Building on [Solaris](solaris.html) * Building on [Ubuntu](ubuntu.html) +* Building on [Linux `ppc64le`](ppc64le-linux-gpp.html) * Building on Microsoft Windows: [with Visual Studio](buildmsvc.html) | [with MinGW-w64](mingw64.html) * Building on an EnterpriseDB PostgreSQL distribution that bundles system diff --git a/src/site/markdown/build/ppc64le-linux-gpp.md b/src/site/markdown/build/ppc64le-linux-gpp.md new file mode 100644 index 00000000..372087c6 --- /dev/null +++ b/src/site/markdown/build/ppc64le-linux-gpp.md @@ -0,0 +1,9 @@ +# Building on Linux ppc64le with GNU tools + +Until the `nar-maven-plugin` upstream has architecture-os-linker entries +for `ppc64le.Linux.gpp`, the `pljava-so` project directory +contains an extra settings file `aol.ppc64le-linux-gpp.properties`. To use it, +add `-Dnar.aolProperties=pljava-so/aol.ppc64le-linux-gpp.properties` +to the `mvn` command line: + + mvn -Dnar.aolProperties=pljava-so/aol.ppc64le-linux-gpp.properties clean install From 69c8a5ee9918b8499e4acf16fdcb45e27aacfbc1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 17 Oct 2018 00:28:39 -0400 Subject: [PATCH 0230/1087] Poke migration-management versions for 1.5.1. -packaging/build.xml already makes an update .sql from 1.5.1-BETA3, the last released version. No change needed there before this release. --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index a80168fe..eb411ec1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -495,6 +495,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_5_1 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA3 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA2 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA1 = REL_1_5_0; From 9d61e9d38578b1ab96b9b31a398a6d10aa2b198f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 17 Oct 2018 01:08:38 -0400 Subject: [PATCH 0231/1087] Add control file in preparation for next release. Now that 1.5.1 is released, the next release should include an extension SQL file allowing upgrade from 1.5.1. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 8e6f4077..ea07992e 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Wed, 17 Oct 2018 20:53:27 -0400 Subject: [PATCH 0232/1087] (Belatedly) advance version to 1.5.2-SNAPSHOT. This should have been part of the preceding commit, as it just followed a full release of 1.5.1. I do not seem to be holding to the plan announced in 2353235, to keep the version string unchanged all through a RELx_y_STABLE branch. (The plan only lasted until f087234). It would indeed simplify merges from the branch back to master. But it hardly makes sense for a version string to stay x.y.z-SNAPSHOT after x.y.z has been released. In the next RELx_y_STABLE branch, perhaps the convention should be that the SNAPSHOT version string is simply x.y-SNAPSHOT. That could then be left alone through the life of the branch. --- pljava-ant/pom.xml | 2 +- pljava-api/pom.xml | 2 +- pljava-deploy/pom.xml | 2 +- pljava-examples/pom.xml | 2 +- pljava-packaging/pom.xml | 2 +- pljava-so/pom.xml | 2 +- pljava/pom.xml | 2 +- pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index 1d11aadc..b9cecacd 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pljava-ant PL/Java Ant tasks diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index a5216666..ffadbf67 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pljava-api PL/Java API diff --git a/pljava-deploy/pom.xml b/pljava-deploy/pom.xml index 9bdcc801..2ebae03b 100644 --- a/pljava-deploy/pom.xml +++ b/pljava-deploy/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pljava-deploy PL/Java Deploy diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index bc966369..8372cea2 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pljava-examples PL/Java examples diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 14504338..642e8941 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pljava-packaging PL/Java packaging diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 1b50e08c..830c150f 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pljava-so PL/Java backend native code diff --git a/pljava/pom.xml b/pljava/pom.xml index 0d78c08e..8ab7a34b 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pljava PL/Java backend Java code diff --git a/pom.xml b/pom.xml index 3eb1cda6..a3c6f618 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.postgresql pljava.app - 1.5.1-SNAPSHOT + 1.5.2-SNAPSHOT pom PostgreSQL PL/Java https://tada.github.io/pljava/ From 500fc9f38f11d290f2864cd87ddf214513dd1022 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 Oct 2018 02:20:15 -0400 Subject: [PATCH 0233/1087] Java7ify: diamond operator in merged classes. --- .../pljava/sqlgen/DDRProcessor.java | 22 +++++++++---------- .../pljava/example/annotation/PassXML.java | 2 +- .../postgresql/pljava/example/saxon/S9.java | 5 ++--- .../postgresql/pljava/internal/DualState.java | 4 ++-- .../internal/MarkableSequenceInputStream.java | 4 ++-- .../pljava/jdbc/SPIDatabaseMetaData.java | 2 +- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 2 +- .../postgresql/pljava/jdbc/TypeBridge.java | 9 ++++---- 8 files changed, 23 insertions(+), 27 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 0513ee4f..9546120e 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -462,8 +462,8 @@ void defensiveEarlyCharacterize() void generateDescriptor() { boolean errorRaised = false; - Set fwdConsumers = new HashSet(); - Set revConsumers = new HashSet(); + Set fwdConsumers = new HashSet<>(); + Set revConsumers = new HashSet<>(); for ( VertexPair v : snippetVPairs ) { @@ -535,20 +535,20 @@ else if ( ! defaultImplementor.equals( imp) ) if ( errorRaised ) return; - Queue> fwdBlocked = new LinkedList>(); - Queue> revBlocked = new LinkedList>(); + Queue> fwdBlocked = new LinkedList<>(); + Queue> revBlocked = new LinkedList<>(); Queue> fwdReady; Queue> revReady; if ( reproducible ) { - fwdReady = new PriorityQueue( 11, snippetTiebreaker); - revReady = new PriorityQueue( 11, snippetTiebreaker); + fwdReady = new PriorityQueue<>( 11, snippetTiebreaker); + revReady = new PriorityQueue<>( 11, snippetTiebreaker); } else { - fwdReady = new LinkedList>(); - revReady = new LinkedList>(); + fwdReady = new LinkedList<>(); + revReady = new LinkedList<>(); } for ( VertexPair vp : snippetVPairs ) @@ -2352,7 +2352,7 @@ private void workAroundJava7Breakage() protoMappings.size()); for ( Map.Entry me : protoMappings ) - vs.add( new Vertex>( me)); + vs.add( new Vertex<>( me)); for ( int i = vs.size(); i --> 1; ) { @@ -2472,9 +2472,7 @@ void addMap(TypeMirror tm, String v) "called after workAroundJava7Breakage", tm.toString(), v); return; } - protoMappings.add( - new AbstractMap.SimpleImmutableEntry( tm, v) - ); + protoMappings.add( new AbstractMap.SimpleImmutableEntry<>( tm, v)); } /** 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 c573a064..fc2ee2b5 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 @@ -125,7 +125,7 @@ public class PassXML implements SQLData static TransformerFactory s_tf = TransformerFactory.newInstance(); - static Map s_tpls = new HashMap(); + static Map s_tpls = new HashMap<>(); /** * Echo an XML parameter back, exercising seven different ways diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java index 83ca56ed..e8c9c18d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java @@ -1160,7 +1160,7 @@ static Iterable> namespaceBindings(String[] nbs) if ( 1 == nbs.length % 2 ) throw new SQLSyntaxErrorException( "Namespace binding array must have even length", "42000"); - Map m = new HashMap(); + Map m = new HashMap<>(); for ( int i = 0; i < nbs.length; i += 2 ) { @@ -1441,8 +1441,7 @@ static class BindingsFromResultSet extends Binding.Assemblage int nParams = m_rsmd.getColumnCount(); ContextItem contextItem = null; - Map n2b = - new HashMap(); + Map n2b = new HashMap<>(); for ( int i = 1; i <= nParams; ++i ) { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 5f71825f..cc522a4e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -89,13 +89,13 @@ public abstract class DualState extends WeakReference * in selected places where it makes sense to do so. */ private static ReferenceQueue s_releasedInstances = - new ReferenceQueue(); + new ReferenceQueue<>(); /** * All instances are added to this collection upon creation. */ private static Deque s_liveInstances = - new LinkedBlockingDeque(); + new LinkedBlockingDeque<>(); /** * Pointer value of the {@code ResourceOwner} this instance belongs to, diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/MarkableSequenceInputStream.java b/pljava/src/main/java/org/postgresql/pljava/internal/MarkableSequenceInputStream.java index 4319b198..d4dd3c8c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/MarkableSequenceInputStream.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/MarkableSequenceInputStream.java @@ -66,7 +66,7 @@ public MarkableSequenceInputStream(InputStream... streams) { if ( null == streams ) throw new NullPointerException("MarkableSequenceInputStream(null)"); - LinkedList isl = new LinkedList(); + LinkedList isl = new LinkedList<>(); for ( InputStream s : streams ) { if ( null == s ) @@ -90,7 +90,7 @@ public MarkableSequenceInputStream(InputStream... streams) public MarkableSequenceInputStream(BlockingQueue queue) { m_streams = - new FetchingListIterator( + new FetchingListIterator<>( new LinkedList(), queue, NO_MORE); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index f0b9a722..5c18297d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -1709,7 +1709,7 @@ public java.sql.ResultSet getTables(String catalog, String schemaPattern, private static final HashMap s_tableTypeClauses; static { - s_tableTypeClauses = new HashMap(); + s_tableTypeClauses = new HashMap<>(); s_tableTypeClauses.put("TABLE", "c.relkind OPERATOR(pg_catalog.=) 'r' " + "AND n.nspname NOT LIKE 'pg!_%' ESCAPE '!' " + diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 440dcb60..451dc6e6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -133,7 +133,7 @@ public abstract class SQLXMLImpl implements SQLXML protected SQLXMLImpl(V backing) { - m_backing = new AtomicReference(backing); + m_backing = new AtomicReference<>(backing); } @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java index d5d6189d..7738bc05 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeBridge.java @@ -76,8 +76,7 @@ public abstract class TypeBridge * This can't be checked automatically because the classes in question may * not yet be loaded, or even available. */ - private static List> m_candidates = - new LinkedList>(); + private static List> m_candidates = new LinkedList<>(); /** * Return an object wrapped, if it is of any type captured by a known @@ -120,7 +119,7 @@ private TypeBridge(String cName, int dfltOid) */ private static TypeBridge ofClass(String cName, int dOid) { - return new OfClass(cName, dOid); + return new OfClass<>(cName, dOid); } /** @@ -129,7 +128,7 @@ private static TypeBridge ofClass(String cName, int dOid) */ private static TypeBridge ofInterface(String cName, int dOid) { - return new OfInterface(cName, dOid); + return new OfInterface<>(cName, dOid); } /** @@ -203,7 +202,7 @@ final static class OfInterface extends TypeBridge @Override protected boolean virtuallyCaptures(Class c) { - List> q = new LinkedList>(); + List> q = new LinkedList<>(); q.add(c); while ( 0 < q.size() ) From ec18090d29744b8388f09283baf5d47435ea313f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 Oct 2018 23:01:01 -0400 Subject: [PATCH 0234/1087] Reorder methods in AbstractResultSet for clarity. Saved this wholesale reordering to apply now (and merge up to master too) when, for once, there aren't outstanding branches to get jumbled up by doing it. --- .../pljava/jdbc/AbstractResultSet.java | 208 +++++++++--------- 1 file changed, 106 insertions(+), 102 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index 4240e846..5233774e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -471,405 +471,409 @@ public T unwrap(Class iface) } @Override - public void updateSQLXML(int columnIndex, SQLXML xmlObject) + public SQLXML getSQLXML(int columnIndex) throws SQLException { - updateObject(columnIndex, xmlObject); + return getObject(columnIndex, SQLXML.class); } @Override - public void updateSQLXML(String columnLabel, SQLXML xmlObject) + public SQLXML getSQLXML(String columnLabel) throws SQLException { - updateObject(columnLabel, xmlObject); + return getObject(columnLabel, SQLXML.class); } @Override - public SQLXML getSQLXML(int columnIndex) + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { - return getObject(columnIndex, SQLXML.class); + updateObject(columnIndex, xmlObject); } @Override - public SQLXML getSQLXML(String columnLabel) + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { - return getObject(columnLabel, SQLXML.class); + updateObject(columnLabel, xmlObject); } // ************************************************************ - // Non-implementation of JDBC 4 methods. + // Non-implementation of JDBC 4 get methods. // ************************************************************ @Override - public void updateNClob(int columnIndex, NClob nClob) + public Reader getNCharacterStream(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNClob( int, NClob ) not implemented yet.", + throw new SQLFeatureNotSupportedException( this.getClass() + + ".getNCharacterStream( String ) not implemented yet.", "0A000" ); } @Override - public void updateNClob(String columnLabel, NClob nClob) + public Reader getNCharacterStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNClob( String, NClob ) not implemented yet.", - "0A000" ); + ".gett( int ) not implemented yet.", "0A000" ); } @Override - public void updateNClob(int columnIndex, Reader reader) + public NClob getNClob(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNClob( int, Reader ) not implemented yet.", - "0A000" ); + ".getNClob( String ) not implemented yet.", "0A000" ); } @Override - public void updateNClob(int columnIndex, Reader reader, long length) + public NClob getNClob(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNClob( int, Reader, long ) not implemented yet.", - "0A000" ); + ".getNClob( int ) not implemented yet.", "0A000" ); } @Override - public void updateNClob(String columnLabel, Reader reader) + public String getNString(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNClob( String, Reader ) not implemented yet.", + ".getNString( String ) not implemented yet.", "0A000" ); } @Override - public void updateNClob(String columnLabel, Reader reader, long length) + public String getNString(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNClob( String, Reader, long ) not implemented yet.", - "0A000" ); + ".getNString( int ) not implemented yet.", "0A000" ); } @Override - public void updateClob(int columnIndex, Reader reader) + public RowId getRowId(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateClob( int, Reader ) not implemented yet.", - "0A000" ); + ".getRowId( String ) not implemented yet.", "0A000" ); } @Override - public void updateClob(int columnIndex, Reader reader, long length) + public RowId getRowId(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateClob( int, Reader, long ) not implemented yet.", - "0A000" ); + "getRowId( int ) not implemented yet.", "0A000" ); } + // ************************************************************ + // Non-implementation of JDBC 4 update methods. + // ************************************************************ + @Override - public void updateClob(String columnLabel, Reader reader) + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateClob( String, Reader ) not implemented yet.", - "0A000" ); + ".updateAsciiStream( String, InputStream ) not implemented yet.", "0A000" ); } @Override - public void updateClob(String columnLabel, Reader reader, long length) + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateClob( String, Reader, long ) not implemented yet.", - "0A000" ); + ".updateAsciiStream( String, InputStream, long ) not implemented yet.", "0A000" ); } @Override - public void updateBlob(int columnIndex, InputStream inputStream) + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBlob( int, InputStream ) not implemented yet.", + ".updateAsciiStream( int, InputStream ) not implemented yet.", "0A000" ); } @Override - public void updateBlob(int columnIndex, InputStream inputStream, long length) + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBlob( int, InputStream, long ) not implemented yet.", + ".updateAsciiStream( int, InputStream, long ) not implemented yet.", "0A000" ); } @Override - public void updateBlob(String columnLabel, InputStream inputStream) + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBlob( String, InputStream ) not implemented yet.", + ".updateBinaryStream( String, InputStream ) not implemented yet.", "0A000" ); } @Override - public void updateBlob(String columnLabel, InputStream inputStream, long length) + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBlob( String, InputStream, long ) not implemented yet.", + ".updateBinaryStream( String, InputStream, long ) not implemented yet.", "0A000" ); } @Override - public void updateCharacterStream(int columnIndex, Reader x) + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateCharacterStream( int, Reader ) not implemented yet.", + ".updateBinaryStream( int, InputStream ) not implemented yet.", "0A000" ); } @Override - public void updateCharacterStream(int columnIndex, Reader x, long length) + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateCharacterStream( int, Reader, long ) not implemented yet.", + ".updateBinaryStream( int, InputStream, long ) not implemented yet.", "0A000" ); } @Override - public void updateCharacterStream(String ColumnLabel, Reader x) + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateCharacterStream( String, Reader ) not implemented yet.", + ".updateBlob( String, InputStream ) not implemented yet.", "0A000" ); } @Override - public void updateCharacterStream(String ColumnLabel, Reader x, long length) + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateCharacterStream( String, Reader, long ) not implemented yet.", + ".updateBlob( String, InputStream, long ) not implemented yet.", "0A000" ); } @Override - public void updateBinaryStream(String columnLabel, InputStream x) + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBinaryStream( String, InputStream ) not implemented yet.", + ".updateBlob( int, InputStream ) not implemented yet.", "0A000" ); } @Override - public void updateBinaryStream(String columnLabel, InputStream x, long length) + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBinaryStream( String, InputStream, long ) not implemented yet.", + ".updateBlob( int, InputStream, long ) not implemented yet.", "0A000" ); } @Override - public void updateBinaryStream(int columnIndex, InputStream x) + public void updateCharacterStream(String ColumnLabel, Reader x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBinaryStream( int, InputStream ) not implemented yet.", + ".updateCharacterStream( String, Reader ) not implemented yet.", "0A000" ); } @Override - public void updateBinaryStream(int columnIndex, InputStream x, long length) + public void updateCharacterStream(String ColumnLabel, Reader x, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateBinaryStream( int, InputStream, long ) not implemented yet.", + ".updateCharacterStream( String, Reader, long ) not implemented yet.", "0A000" ); } @Override - public void updateAsciiStream(String columnLabel, InputStream x) + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateAsciiStream( String, InputStream ) not implemented yet.", "0A000" ); + ".updateCharacterStream( int, Reader ) not implemented yet.", + "0A000" ); } @Override - public void updateAsciiStream(String columnLabel, InputStream x, long length) + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateAsciiStream( String, InputStream, long ) not implemented yet.", "0A000" ); + ".updateCharacterStream( int, Reader, long ) not implemented yet.", + "0A000" ); } @Override - public void updateAsciiStream(int columnIndex, InputStream x) + public void updateClob(String columnLabel, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateAsciiStream( int, InputStream ) not implemented yet.", + ".updateClob( String, Reader ) not implemented yet.", "0A000" ); } @Override - public void updateAsciiStream(int columnIndex, InputStream x, long length) + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateAsciiStream( int, InputStream, long ) not implemented yet.", + ".updateClob( String, Reader, long ) not implemented yet.", "0A000" ); } @Override - public void updateNCharacterStream(String columnLabel, Reader reader) + public void updateClob(int columnIndex, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNCharacterStream( String, Reader ) not implemented yet.", + ".updateClob( int, Reader ) not implemented yet.", "0A000" ); } @Override - public void updateNCharacterStream(String columnLabel, Reader reader, long length) + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNCharacterStream( String, Reader, long ) not implemented yet.", + ".updateClob( int, Reader, long ) not implemented yet.", "0A000" ); } @Override - public void updateNCharacterStream(int columnIndex, Reader reader) + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNCharacterStream( int, Reader ) not implemented yet.", + ".updateNCharacterStream( String, Reader ) not implemented yet.", "0A000" ); } @Override - public void updateNCharacterStream(int columnIndex, Reader reader, long length) + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNCharaterStream( int, Reader, long] ) not implemented yet.", + ".updateNCharacterStream( String, Reader, long ) not implemented yet.", "0A000" ); } @Override - public Reader getNCharacterStream(String columnLabel) + public void updateNCharacterStream(int columnIndex, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".getNCharacterStream( String ) not implemented yet.", + ".updateNCharacterStream( int, Reader ) not implemented yet.", "0A000" ); } @Override - public Reader getNCharacterStream(int columnIndex) + public void updateNCharacterStream(int columnIndex, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".gett( int ) not implemented yet.", "0A000" ); + ".updateNCharaterStream( int, Reader, long] ) not implemented yet.", + "0A000" ); } @Override - public String getNString(int columnIndex) + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".getNString( int ) not implemented yet.", "0A000" ); + ".updateNClob( String, NClob ) not implemented yet.", + "0A000" ); } @Override - public String getNString(String columnLabel) + public void updateNClob(String columnLabel, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".getNString( String ) not implemented yet.", + ".updateNClob( String, Reader ) not implemented yet.", "0A000" ); } @Override - public NClob getNClob(String columnLabel) + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".getNClob( String ) not implemented yet.", "0A000" ); + ".updateNClob( String, Reader, long ) not implemented yet.", + "0A000" ); } @Override - public NClob getNClob(int columnIndex) + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".getNClob( int ) not implemented yet.", "0A000" ); + ".updateNClob( int, NClob ) not implemented yet.", + "0A000" ); } @Override - public void updateNString(String columnLabel, String nString) + public void updateNClob(int columnIndex, Reader reader) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNString( String, String ) not implemented yet.", + ".updateNClob( int, Reader ) not implemented yet.", "0A000" ); } @Override - public void updateNString(int columnIndex, String nString) + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateNString( String, Object[] ) not implemented yet.", + ".updateNClob( int, Reader, long ) not implemented yet.", "0A000" ); } @Override - public void updateRowId(int columnIndex, RowId x) + public void updateNString(String columnLabel, String nString) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateRowId( int, RowId ) not implemented yet.", + ".updateNString( String, String ) not implemented yet.", "0A000" ); } @Override - public void updateRowId(String columnLabel, RowId x) + public void updateNString(int columnIndex, String nString) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".updateRowId( String, RowId ) not implemented yet.", + ".updateNString( String, Object[] ) not implemented yet.", "0A000" ); } @Override - public RowId getRowId(int columnIndex) + public void updateRowId(String columnLabel, RowId x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - "getRowId( int ) not implemented yet.", "0A000" ); + ".updateRowId( String, RowId ) not implemented yet.", + "0A000" ); } @Override - public RowId getRowId(String columnLabel) + public void updateRowId(int columnIndex, RowId x) throws SQLException { throw new SQLFeatureNotSupportedException( this.getClass() + - ".getRowId( String ) not implemented yet.", "0A000" ); + ".updateRowId( int, RowId ) not implemented yet.", + "0A000" ); } // ************************************************************ From b05ad097c069235167fc6d30c544896a89abcead Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 Oct 2018 23:15:17 -0400 Subject: [PATCH 0235/1087] Java7ify: full citizenship for JDBC 4.1 methods. --- .../org/postgresql/pljava/jdbc/AbstractResultSet.java | 5 +---- .../org/postgresql/pljava/jdbc/ObjectResultSet.java | 2 +- .../java/org/postgresql/pljava/jdbc/SPIConnection.java | 10 +++++----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java index 5233774e..b7141df2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/AbstractResultSet.java @@ -878,15 +878,12 @@ public void updateRowId(int columnIndex, RowId x) // ************************************************************ // Implementation of JDBC 4.1 methods. - // Add @Override here once Java back horizon advances to 7. // ************************************************************ + @Override public T getObject(String columnName, Class type) throws SQLException { return this.getObject(this.findColumn(columnName), type); } - - public abstract T getObject(int columnIndex, Class type) - throws SQLException; // placeholder; remove when Java back horizon >= 7 } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java index f877669b..0cdbd531 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java @@ -628,13 +628,13 @@ public void updateTimestamp(int columnIndex, Timestamp x) // ************************************************************ // JDBC 4.1 // Getter-by-columnIndex - // Add @Override here once Java back horizon advances to 7. // ************************************************************ /** * Implemented over {@link #getObjectValue(int,Class) getObjectValue}. * Final because it records {@code wasNull} for use by other methods. */ + @Override public final T getObject(int columnIndex, Class type) throws SQLException { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 5866b67e..85ae3b8a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1218,21 +1218,21 @@ public Clob createClob() // Non-implementation of JDBC 4.1 methods. // ************************************************************ - // add @Override once Java 7 is back-support limit + @Override public void abort(Executor executor) throws SQLException { throw new SQLFeatureNotSupportedException( "SPIConnection.abort(Executor) not implemented yet.", "0A000" ); } - // add @Override once Java 7 is back-support limit + @Override public int getNetworkTimeout() throws SQLException { throw new SQLFeatureNotSupportedException( "SPIConnection.getNetworkTimeout() not implemented yet.", "0A000" ); } - // add @Override once Java 7 is back-support limit + @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { @@ -1240,14 +1240,14 @@ public void setNetworkTimeout(Executor executor, int milliseconds) "SPIConnection.setNetworkTimeout(Executor,int) not implemented yet.", "0A000" ); } - // add @Override once Java 7 is back-support limit + @Override public String getSchema() throws SQLException { throw new SQLFeatureNotSupportedException( "SPIConnection.getSchema() not implemented yet.", "0A000" ); } - // add @Override once Java 7 is back-support limit + @Override public void setSchema(String schema) throws SQLException { throw new SQLFeatureNotSupportedException( From b0dd2211a3f63062e559cf3cd156e9a3e3a04024 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 30 Oct 2018 23:36:13 -0400 Subject: [PATCH 0236/1087] Repair java.sql.Date breakage from 72cbb15. That commit was part of the work of adding the java.time types for 1.5.1 (issue #137). A potential for overflow in timezone handling for timestamps was corrected by changing the API of Timestamp_getTimeZone_id(). There was known to be one other use of that function, in Date.c. A comment was even added in Timestamp.h advising that there was a known use in Date.c. But somehow updating that known use in Date.c slipped through the cracks. The end result was that the new, preferable java.time API got added (preferable in part because it eliminates the unexpected timezone dependencies when manipulating local dates and times), but in the process a spooky timezone-dependent off-by-a-day error got introduced to the old date <-> java.sql.Date conversions. --- pljava-so/src/main/c/type/Date.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pljava-so/src/main/c/type/Date.c b/pljava-so/src/main/c/type/Date.c index b23eeaf7..9d1c68f6 100644 --- a/pljava-so/src/main/c/type/Date.c +++ b/pljava-so/src/main/c/type/Date.c @@ -95,8 +95,8 @@ static Type _LocalDate_obtain(Oid typeId) static jvalue _Date_coerceDatum(Type self, Datum arg) { DateADT pgDate = DatumGetDateADT(arg); - int64 ts = (int64)pgDate * INT64CONST(86400000000); - int tz = Timestamp_getTimeZone_id(ts); + int64 ts = (int64)pgDate * INT64CONST(43200000000); + int tz = Timestamp_getTimeZone_id(ts); /* ts in 2 usec units */ jlong date = (jlong)(pgDate + EPOCH_DIFF); @@ -109,8 +109,12 @@ static jvalue _Date_coerceDatum(Type self, Datum arg) static Datum _Date_coerceObject(Type self, jobject date) { - jlong milliSecs = JNI_callLongMethod(date, s_Date_getTime) - INT64CONST(86400000) * EPOCH_DIFF; - jlong secs = milliSecs / 1000 - Timestamp_getTimeZone_id(milliSecs * 1000); + jlong milliSecs = + JNI_callLongMethod(date, s_Date_getTime) + - INT64CONST(86400000) * EPOCH_DIFF; + jlong secs = + milliSecs / 1000 + - Timestamp_getTimeZone_id(milliSecs * 500); /* those 2 usec units */ return DateADTGetDatum((DateADT)(secs / 86400)); } From 64e62ef0cb6e68548ee5755258e3507c4857d4a6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 31 Oct 2018 12:41:03 -0400 Subject: [PATCH 0237/1087] Regression test and documentation for issue #199. --- .../pljava/example/annotation/PreJSR310.java | 94 +++++++++++++++++++ src/site/markdown/releasenotes.md.vm | 29 +++++- src/site/markdown/use/datetime.md | 8 ++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java new file mode 100644 index 00000000..9f3913a0 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018- 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.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.Statement; +import java.sql.ResultSet; +import java.sql.Savepoint; +import java.sql.SQLException; + +import static java.util.logging.Logger.getAnonymousLogger; +import java.util.TimeZone; + +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; + +/** + * Some tests of pre-JSR 310 date/time/timestamp conversions. + *

    + * For now, just {@code java.sql.Date}, thanks to issue #199. + *

    + * This example relies on {@code implementor} tags reflecting the PostgreSQL + * version, set up in the {@link ConditionalDDR} example. + */ +@SQLAction(implementor="postgresql_ge_90300", // needs LATERAL + requires="issue199", install={ + "SELECT javatest.issue199()" +}) +public class PreJSR310 +{ + private static final String TZPRAGUE = "Europe/Prague"; + + /** + * Test for a regression in PG date to/from java.sql.Date conversion + * identified in issue #199. + *

    + * Checks that two months of consecutive dates in October/November 2018 + * are converted correctly in the Europe/Prague timezone. The actual issue + * was by no means limited to that timezone, but this test reproducibly + * detects it. + */ + @Function(schema="javatest", provides="issue199") + public static void issue199() throws SQLException + { + TimeZone oldZone = TimeZone.getDefault(); + TimeZone tzPrague = TimeZone.getTimeZone(TZPRAGUE); + Connection c = DriverManager.getConnection("jdbc:default:connection"); + Statement s = c.createStatement(); + Savepoint svpt = c.setSavepoint(); + boolean ok = true; + try + { + TimeZone.setDefault(tzPrague); + s.execute("SET LOCAL TIME ZONE '" + TZPRAGUE + "'"); + + ResultSet rs = s.executeQuery( + "SELECT" + + " d, to_char(d, 'YYYY-MM-DD')" + + " FROM" + + " generate_series(0, 60) AS s(i)," + + " LATERAL (SELECT date '2018-10-01' + i) AS t(d)"); + while ( rs.next() ) + { + Date dd = rs.getDate(1); + String ds = rs.getString(2); + if ( ! ds.equals(dd.toString()) ) + ok = false; + } + } + finally + { + TimeZone.setDefault(oldZone); + c.rollback(svpt); // restore prior PG timezone + s.close(); + c.close(); + } + + if ( ok ) + getAnonymousLogger().info("issue 199 test ok"); + else + getAnonymousLogger().warning("issue 199 test not ok"); + } +} diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 45c5d393..806d4c2b 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,7 +10,34 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') -$h2 PL/Java 1.5.1 +$h2 PL/Java 1.5.2 + +A pure bug-fix release, correcting a regression in 1.5.1 that was not caught +in pre-release testing, and could leave +[conversions between PostgreSQL `date` and `java.sql.Date`](${ghbug}199) off +by one day in certain timezones and times of the year. + +1.5.1 added support for the newer `java.time` classes from JSR 310 / JDBC 4.2, +which are [recommended as superior alternatives](use/datetime.html) to the +older conversions involving `java.sql.Date` and related classes. The new +versions are superior in part because they do not have hidden timezone +dependencies. + +However, the change to the historical `java.sql.Date` conversion behavior was +inadvertent, and is fixed in this release. + +$h3 Open issues with date/time/timestamp conversions + +During preparation of this release, other issues of longer standing were also +uncovered in the legacy conversions between PG `date`, `time`, and +`timestamp` classes and the `java.sql` types. They are detailed in +[issue #200](${ghbug}200). Because they are not regressions but long-established +behavior, they are left untouched in this release, and will be fixed in +a future release. + +The Java 8 `java.time` conversions are free of these issues as well. + +$h2 PL/Java 1.5.1 (17 October 2018) This release adds support for PostgreSQL 9.6, 10, and 11, and plays more nicely with `pg_upgrade`. If a PostgreSQL installation diff --git a/src/site/markdown/use/datetime.md b/src/site/markdown/use/datetime.md index 6a28bd2c..b56ada32 100644 --- a/src/site/markdown/use/datetime.md +++ b/src/site/markdown/use/datetime.md @@ -26,6 +26,14 @@ zone`. The conversions of non-zoned values involve a hidden dependency on the PostgreSQL session's current setting of `TimeZone`, which can vary from session to session at the connecting client's preference. +There are known issues of long standing in PL/Java's conversions to and from +these types, detailed in [issue #200][issue200]. While these particular issues +are expected to be fixed in a future PL/Java release, the Java 8 / JDBC 4.2 +mappings described next are the strongly-recommended alternative to the legacy +mappings, avoiding these issues entirely. + +[issue200]: https://github.com/tada/pljava/issues/200 + ## Java 8 / JDBC 4.2 date/time mappings Java 8 introduced the much improved set of date/time classes in the `java.time` From d106e3996a48bcdd99cd54322c6e67435ca70a60 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Nov 2018 00:28:24 -0500 Subject: [PATCH 0238/1087] Poke migration-management versions for 1.5.2. -packaging/build.xml already makes an update .sql from 1.5.1, the last released version. No change needed there before this release. --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index eb411ec1..a0b10d01 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -495,6 +495,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_5_2 = REL_1_5_0; static final SchemaVariant REL_1_5_1 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA3 = REL_1_5_0; static final SchemaVariant REL_1_5_1_BETA2 = REL_1_5_0; From 78ef01b6e0b4b4d6811f955e2673bd20c9298dfb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Nov 2018 01:03:54 -0500 Subject: [PATCH 0239/1087] Add control file in preparation for next release. Now that 1.5.2 is released, the next release should include an extension SQL file allowing upgrade from 1.5.2. Advance version to 1.5.3-SNAPSHOT. --- pljava-ant/pom.xml | 2 +- pljava-api/pom.xml | 2 +- pljava-deploy/pom.xml | 2 +- pljava-examples/pom.xml | 2 +- pljava-packaging/build.xml | 4 ++++ pljava-packaging/pom.xml | 2 +- pljava-so/pom.xml | 2 +- pljava/pom.xml | 2 +- pom.xml | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index b9cecacd..c672f4c5 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pljava-ant PL/Java Ant tasks diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index ffadbf67..3850d515 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pljava-api PL/Java API diff --git a/pljava-deploy/pom.xml b/pljava-deploy/pom.xml index 2ebae03b..0103bd17 100644 --- a/pljava-deploy/pom.xml +++ b/pljava-deploy/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pljava-deploy PL/Java Deploy diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 8372cea2..606ea509 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pljava-examples PL/Java examples diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index ea07992e..db07e296 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pljava-packaging PL/Java packaging diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 830c150f..d22da81d 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pljava-so PL/Java backend native code diff --git a/pljava/pom.xml b/pljava/pom.xml index 8ab7a34b..1dc458c7 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pljava PL/Java backend Java code diff --git a/pom.xml b/pom.xml index a3c6f618..789c1a76 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.postgresql pljava.app - 1.5.2-SNAPSHOT + 1.5.3-SNAPSHOT pom PostgreSQL PL/Java https://tada.github.io/pljava/ From f424406496f7b5388ca9d4420e71cc1a729bb88e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Jan 2019 14:45:17 -0500 Subject: [PATCH 0240/1087] Handle the retirement of dynloader.h. Upstream 842cb9f moves to simply expecting dlfcn.h to be present on most platforms, and covers the rest in port.h (already included in c.h, which we already include). --- pljava-so/src/main/c/Backend.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 3f1495b1..91c603b9 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -26,7 +26,19 @@ #include #include #include -#include + +#if PG_VERSION_NUM >= 120000 + #ifdef HAVE_DLOPEN + #include + #endif + #define pg_dlopen(f) dlopen((f), RTLD_NOW | RTLD_GLOBAL) + #define pg_dlsym(h,s) dlsym((h), (s)) + #define pg_dlclose(h) dlclose((h)) + #define pg_dlerror() dlerror() +#else + #include +#endif + #include #include #include From 42dbcea30ea10f1dbb7d0554b889c342e555e37a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Jan 2019 14:43:57 -0500 Subject: [PATCH 0241/1087] Handle the retirement of magical oids. Upstream 578b229 turns the oid columns of system catalogs into ordinary columns, to be retrieved in the ordinary way by name or column number. Non-system-catalogs don't get to have system-managed oids at all any more. (You could give a table an oid-typed column named oid and make a key of it, but no function is provided to automatically assign a value to such a column.) --- .../postgresql/pljava/example/Triggers.java | 7 +++--- .../main/resources/deployment/examples.ddr | 2 +- pljava-so/src/main/c/InstallHelper.c | 14 +++++++---- pljava-so/src/main/c/type/AclId.c | 23 ++++++++++++++----- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/Triggers.java index a0279118..2051cc1b 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/Triggers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -8,6 +8,7 @@ * * Contributors: * Tada AB + * Chapman Flack */ package org.postgresql.pljava.example; @@ -35,8 +36,8 @@ public class Triggers { public static void afterUsernameInsert(TriggerData td) throws SQLException { Logger log = Logger.getAnonymousLogger(); - log.info("After username insert, oid of tuple = " - + td.getNew().getInt("oid")); + log.info("After username insert, username = " + + td.getNew().getInt("username")); } public static void afterUsernameUpdate(TriggerData td) throws SQLException { diff --git a/pljava-examples/src/main/resources/deployment/examples.ddr b/pljava-examples/src/main/resources/deployment/examples.ddr index ff7a464f..af5376d0 100644 --- a/pljava-examples/src/main/resources/deployment/examples.ddr +++ b/pljava-examples/src/main/resources/deployment/examples.ddr @@ -20,7 +20,7 @@ SQLActions[ ] = { ( name text, username text not null - ) WITH OIDS; + ); CREATE FUNCTION javatest.insert_username() RETURNS trigger diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 94384a28..cf4fbfc3 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2019 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 @@ -34,9 +34,15 @@ #endif #include -#if PG_VERSION_NUM < 90000 +#if PG_VERSION_NUM >= 120000 +#include +#define GetNamespaceOid(k1) \ + GetSysCacheOid1(NAMESPACENAME, Anum_pg_namespace_oid, k1) +#elif PG_VERSION_NUM >= 90000 +#define GetNamespaceOid(k1) GetSysCacheOid1(NAMESPACENAME, k1) +#else #define SearchSysCache1(cid, k1) SearchSysCache(cid, k1, 0, 0, 0) -#define GetSysCacheOid1(cid, k1) GetSysCacheOid(cid, k1, 0, 0, 0) +#define GetNamespaceOid(k1) GetSysCacheOid(NAMESPACENAME, k1, 0, 0, 0) #endif #include "pljava/InstallHelper.h" @@ -317,7 +323,7 @@ static void getExtensionLoadPath() * working model" and that code is a lot more fiddly than you would guess. */ if ( InvalidOid == get_relname_relid(LOADPATH_TBL_NAME, - GetSysCacheOid1(NAMESPACENAME, CStringGetDatum("sqlj"))) ) + GetNamespaceOid(CStringGetDatum("sqlj"))) ) return; SPI_connect(); diff --git a/pljava-so/src/main/c/type/AclId.c b/pljava-so/src/main/c/type/AclId.c index d5414540..504b8cf6 100644 --- a/pljava-so/src/main/c/type/AclId.c +++ b/pljava-so/src/main/c/type/AclId.c @@ -1,14 +1,19 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include #include #include +#include #include "pljava/type/AclId.h" #include "pljava/type/Oid.h" @@ -146,7 +151,13 @@ Java_org_postgresql_pljava_internal_AclId__1fromName(JNIEnv* env, jclass clazz, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("role \"%s\" does not exist", roleName))); - result = AclId_create(HeapTupleGetOid(roleTup)); + result = AclId_create( +#if PG_VERSION_NUM >= 120000 + ((Form_pg_authid) GETSTRUCT(roleTup))->oid +#else + HeapTupleGetOid(roleTup) +#endif + ); ReleaseSysCache(roleTup); } PG_CATCH(); From a29e0024e4d50ac073039b0430785593f45f87f7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Jan 2019 15:31:54 -0500 Subject: [PATCH 0242/1087] Handle the retirement of nabstime. Upstream cda6a8d makes nabstime.h go away. GetCurrentAbsoluteTime() was being used here, but didn't do anything time(NULL) doesn't do. --- pljava-so/src/main/c/type/Time.c | 7 ++++--- pljava-so/src/main/c/type/Timestamp.c | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/type/Time.c b/pljava-so/src/main/c/type/Time.c index b1b73568..8222cbaf 100644 --- a/pljava-so/src/main/c/type/Time.c +++ b/pljava-so/src/main/c/type/Time.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -12,8 +12,9 @@ * * @author Thomas Hallgren */ +#include + #include -#include #include #include @@ -224,7 +225,7 @@ static Type _OffsetTime_obtain(Oid typeId) static jlong msecsAtMidnight(void) { - AbsoluteTime now = GetCurrentAbsoluteTime() / 86400; + pg_time_t now = (pg_time_t)time(NULL) / 86400; return INT64CONST(1000) * (jlong)(now * 86400); } diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 7aa57593..b3ef3e4c 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -12,8 +12,9 @@ * PostgreSQL Global Development Group * Chapman Flack */ +#include + #include -#include #include #include "pljava/Backend.h" @@ -458,7 +459,7 @@ static int32 Timestamp_getTimeZone_dd(double dt) int32 Timestamp_getCurrentTimeZone(void) { - return Timestamp_getTimeZone((pg_time_t)GetCurrentAbsoluteTime()); + return Timestamp_getTimeZone((pg_time_t)time(NULL)); } extern void Timestamp_initialize(void); From 900eca681ed5c6c928ff52dca5b27ed46700b2e4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Jan 2019 15:51:05 -0500 Subject: [PATCH 0243/1087] Handle extensibility of TupleTableSlots. Upstream 4da597e seems rather big, but perhaps only affects the spelling of one function used here (ExecCopySlotTuple -> ExecCopySlotHeapTuple). --- pljava-so/src/main/c/type/TupleTable.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/type/TupleTable.c b/pljava-so/src/main/c/type/TupleTable.c index 1f710658..6d583ee2 100644 --- a/pljava-so/src/main/c/type/TupleTable.c +++ b/pljava-so/src/main/c/type/TupleTable.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -19,6 +19,10 @@ #include "pljava/type/Tuple.h" #include "pljava/type/TupleDesc.h" +#if PG_VERSION_NUM < 120000 +#define ExecCopySlotHeapTuple(tts) ExecCopySlotTuple((tts)) +#endif + static jclass s_TupleTable_class; static jmethodID s_TupleTable_init; @@ -35,7 +39,7 @@ jobject TupleTable_createFromSlot(TupleTableSlot* tts) curr = MemoryContextSwitchTo(JavaMemoryContext); tupdesc = TupleDesc_internalCreate(tts->tts_tupleDescriptor); - tuple = ExecCopySlotTuple(tts); + tuple = ExecCopySlotHeapTuple(tts); tuples = Tuple_createArray(&tuple, 1, false); MemoryContextSwitchTo(curr); From 0a6bd68ed7b01866ce21f2f36d065d0edb81f084 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Jan 2019 17:45:10 -0500 Subject: [PATCH 0244/1087] Handle retirement of pg_attrdef.adsrc. Upstream fe50382 makes the adsrc column go away. It had been deprecated for so long, no version check is even necessary to use the 'new' alternative. --- .../java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index f0b9a722..4c05db6f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2005-2019 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 @@ -1878,7 +1878,8 @@ public java.sql.ResultSet getColumns(String catalog, String schemaPattern, String sql = "SELECT n.nspname, c.relname, a.attname," + " a.atttypid as atttypid, a.attnotnull, a.atttypmod," - + " a.attlen::pg_catalog.int4 as attlen, a.attnum, def.adsrc," + + " a.attlen::pg_catalog.int4 as attlen, a.attnum," + + " pg_catalog.pg_get_expr(def.adbin, c.oid) AS adsrc," + " dsc.description" + " FROM pg_catalog.pg_namespace n " + " JOIN pg_catalog.pg_class c" From 6f8f7151ec801105499eca64e7b7e9646015d4fc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 3 Feb 2019 23:06:28 -0500 Subject: [PATCH 0245/1087] Reunite a comment with the code it refers to. This comment got separated from the code it describes back in a4f6c9e, and has been mystifying readers ever since. --- pljava-so/src/main/c/Function.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 1ee5bc86..f9671d8c 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -800,6 +800,9 @@ Datum Function_invoke(Function self, PG_FUNCTION_ARGS) if(self->isUDT) return self->func.udt.udtFunction(self->func.udt.udt, fcinfo); + /* a class loader or other mechanism might have connected already. This + * connection must be dropped since its parent context is wrong. + */ if(self->func.nonudt.isMultiCall && SRF_IS_FIRSTCALL()) Invocation_assertDisconnect(); @@ -816,9 +819,6 @@ Datum Function_invoke(Function self, PG_FUNCTION_ARGS) int32 idx; Type* types = self->func.nonudt.paramTypes; - /* a class loader or other mechanism might have connected already. This - * connection must be dropped since its parent context is wrong. - */ if(Type_isDynamic(invokerType)) invokerType = Type_getRealType(invokerType, get_fn_expr_rettype(fcinfo->flinfo), self->func.nonudt.typeMap); From ca2ec4406b50f3a39ea8aafc8486fbf08b6ffc92 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 17 Feb 2019 22:22:45 -0500 Subject: [PATCH 0246/1087] Fix wrong #include a29e002 should have included , not . --- pljava-so/src/main/c/type/Time.c | 2 +- pljava-so/src/main/c/type/Timestamp.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/type/Time.c b/pljava-so/src/main/c/type/Time.c index 8222cbaf..a274d59e 100644 --- a/pljava-so/src/main/c/type/Time.c +++ b/pljava-so/src/main/c/type/Time.c @@ -12,7 +12,7 @@ * * @author Thomas Hallgren */ -#include +#include #include #include diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index b3ef3e4c..1963194f 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -12,7 +12,7 @@ * PostgreSQL Global Development Group * Chapman Flack */ -#include +#include #include #include From ba7a7b75cec0bb44714bc800adcefacc594415b8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 17 Feb 2019 23:05:00 -0500 Subject: [PATCH 0247/1087] Handle variable-length function-call info. Upstream a9c35cf made some changes to the FunctionCallInfoData struct that don't affect PL/Java's code, and also changed its name, which does. --- pljava-so/src/main/c/InstallHelper.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index cf4fbfc3..68203700 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -506,7 +506,12 @@ char *InstallHelper_hello() jstring nativeVer; jstring serverBuiltVer; jstring serverRunningVer; - FunctionCallInfoData fcinfo; +#if PG_VERSION_NUM >= 120000 + FunctionCallInfoBaseData +#else + FunctionCallInfoData +#endif + fcinfo; text *runningVer; jstring user; jstring dbname; From 9d033d53465c866f389d2d5cc7789f16de79b6bd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 17 Feb 2019 23:23:50 -0500 Subject: [PATCH 0248/1087] Corrections to user and packaging instructions. They did not present the right mvn commands to build particular subprojects in isolation. --- src/site/markdown/build/package.md | 16 ++++++++++------ src/site/markdown/use/use.md | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/site/markdown/build/package.md b/src/site/markdown/build/package.md index aa65c48d..72c54a8d 100644 --- a/src/site/markdown/build/package.md +++ b/src/site/markdown/build/package.md @@ -94,8 +94,8 @@ if any. (PL/Java's [hello world example](../use/hello.html) illustrates using Maven to build code for use in PL/Java, which assumes the local Maven repo contains `pljava-api`.) -To build `pljava-api` in isolation. simply change into the `pljava-api` -subdirectory and run `mvn clean install`. It builds quickly and independently +To build `pljava-api` in isolation. simply run +`mvn --projects pljava-api clean install`. It builds quickly and independently of the rest of the project, with fewer build dependencies than the project as a whole. @@ -109,8 +109,8 @@ is the useful one to have in an installation target host's repository.) ### PL/Java API javadocs The PL/Java build does not automatically build javadocs. Those that go with -`pljava-api` can be easily generated by changing into the `pljava-api` -subdirectory and running `mvn javadoc:javadoc` to build them, then collecting +`pljava-api` can be easily generated by running +`mvn --projects pljava-api javadoc:javadoc` to build them, then collecting the `apidocs` subtree from `target/site`. They can be included in the same package as `pljava-api` or in a separate javadoc package, as your guidelines may require. @@ -121,13 +121,17 @@ A full PL/Java build also builds `pljava-examples`, which typically will also be installed into PostgreSQL's _SHAREDIR_`/pljava` directory. If the packaging guidelines encourage placing examples into a separate package, this jar file can be excluded from the main package and delivered in a separate one. -The examples can be built in isolation by changing into the `pljava-examples` -subdirectory and running `mvn clean package`, as long as the `pljava-api` has +The examples can be built in isolation by running +`mvn --projects pljava-examples clean package`, as long as the `pljava-api` has been built first and installed into the build host's local Maven repository. Note that many of the examples do double duty as tests, as described in _confirming the build_ below. +When building for (and with) Java 8 or later and PostgreSQL 8.4 or later, +the XML examples based on the Saxon library should also be built, +by adding `-Psaxon-examples` to the `mvn` command line. + ## Scripting the build Options on the `mvn` command line may be useful in the scripted build for diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index 68bbcd99..716597b2 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -19,8 +19,8 @@ to be in your local Maven repository. If you have built PL/Java from source using `mvn clean install`, it will be there already. If not, an easy way to install the API into your local repository -is to download the PL/Java source, _change into the `pljava-api` -directory_, and run `mvn clean install` there. It will quickly build +is to download the PL/Java source, and run +`mvn --projects pljava-api clean install` there. It will quickly build and install the API jar, without requiring the various build-time dependencies needed when all of PL/Java is being built. From d2700dc1a51d6ea1765e9af99d32ebd085431564 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 17 Feb 2019 18:25:25 -0500 Subject: [PATCH 0249/1087] SQLXML nits when server encoding != UTF-8 StAX will object to reading a stream that declares a non-IANA-registered encoding--even if Java has a corresponding Charset--unless an obscure feature allow-java-encodings has been set. Also, the StAXResultAdapter needs to flush the XMLStreamWriter before closing the underlying stream; writing in UTF-8 encoding did not expose that, but writing in another encoding can. When writing the PostgreSQL XML data type, throw conformance to the wind and omit any declaration of the encoding in use, even when not UTF-8. Some of the PostgreSQL core functions (IS DOCUMENT for sure) can misbehave if the named encoding isn't something libxml approves of, even when it is the correct name of the server encoding in use. Improve the PassXML example to show that, when writing a SQLXML object and not through the SAX, StAX, or DOM API, it is the application's responsibility to arrange for the stream to be in the server encoding. --- .../pljava/example/annotation/PassXML.java | 8 ++ .../postgresql/pljava/jdbc/SQLXMLImpl.java | 77 +++++++++++++++---- 2 files changed, 69 insertions(+), 16 deletions(-) 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 c573a064..f253a74b 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 @@ -40,6 +40,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import static javax.xml.transform.OutputKeys.ENCODING; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Templates; @@ -288,6 +289,13 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) try { Transformer t = s_tf.newTransformer(); + /* + * For the non-SAX/StAX/DOM flavors of output, you're responsible + * for setting the Transformer to use the server encoding. + */ + if ( howout < 5 ) + t.setOutputProperty(ENCODING, + System.getProperty("org.postgresql.server.encoding")); t.transform(src, rlt); } catch ( TransformerException te ) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 440dcb60..495aefdf 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -375,11 +375,11 @@ static InputStream correctedDeclStream( /* * At this point, for better or worse, the loop is done. There may - * or may not be more of m_vwi left to read; the probe may or may + * or may not be more of m_backing left to read; the probe may or may * not have found a decl. If it didn't, prefix() will treat whatever * had been read as readahead and hand it all back, so it suffices * here to create a SequenceInputStream of the prefix and whatever - * is or isn't left of m_vwi. + * is or isn't left of m_backing. * A bonus is that the SequenceInputStream closes each underlying * stream as it reaches EOF. After the last stream is used up, the * SequenceInputStream remains open-at-EOF until explicitly closed, @@ -642,6 +642,13 @@ public T getSource(Class sourceClass) { XMLInputFactory xif = XMLInputFactory.newFactory(); xif.setProperty(xif.IS_NAMESPACE_AWARE, true); + if ( ! m_serverCS.isRegistered() ) + { + String hack = + "http://apache.org/xml/features/allow-java-encodings"; + if ( xif.isPropertySupported(hack) ) + xif.setProperty(hack, Boolean.TRUE); + } XMLStreamReader xsr = xif.createXMLStreamReader( correctedDeclStream(is, false)); @@ -1043,6 +1050,12 @@ protected void verify(InputStream is) throws Exception } } + /** + * Filter an {@code OutputStream} by ensuring it doesn't begin with a + * declaration of a character encoding other than the server encoding, and + * passing any declaration along in an edited form more palatable to + * PostgreSQL. + */ static class DeclCheckedOutputStream extends FilterOutputStream { private Charset m_serverCS; @@ -1126,12 +1139,35 @@ public void close() throws IOException } } + /** + * Called after enough bytes have been passed to the {@code DeclProbe} + * for it to know whether a decl is present and correct, to throw an + * exception if an encoding is declared that is not the server encoding, + * and then pass the (possibly edited) decl and any readahead along + * to the output. + *

    + * It is assumed that the stream is being generated by code that does + * encoding declarations properly, so should have one if any code + * other than UTF-8 is in use. (For now, in a mood of leniency, + * {@code false} is passed to {@code checkEncoding}'s {@code strict} + * parameter, so an exception will be generated only if the stream + * explicitly declares an encoding that isn't the server encoding. This + * could one day be made configurable, perhaps as a {@code Connection} + * property. + *

    + * It's assumed that the destination of the stream is PostgreSQL's + * native XML datatype, where some of the native functions can fall over + * if an encoding declaration is present (even if it correctly matches + * the server encoding), so any decl generated into the output will be + * edited to remove any reference to encoding; this can fall short of + * strict conformance, but works better with the PG core implementation. + */ private void check() throws IOException, SQLException { if ( null == m_probe ) return; m_probe.checkEncoding(m_serverCS, false); - byte[] prefix = m_probe.prefix(m_serverCS); + byte[] prefix = m_probe.prefix(null /* not m_serverCS */); m_probe = null; // Do not check more than once. out.write(prefix); } @@ -1521,6 +1557,7 @@ public void writeEndElement() throws XMLStreamException public void writeEndDocument() throws XMLStreamException { m_xsw.writeEndDocument(); + m_xsw.flush(); try { @@ -2059,6 +2096,10 @@ private boolean isSpace(byte b) * a nonmatching parse or following a matching one. * @param serverCharset The encoding to be named in the declaration if * one is generated (which is forced if the encoding isn't UTF-8). + * Pass null to force the omission of any encoding declaration; this is + * needed when writing to the native PG XML datatype, as some PG native + * functions such as IS DOCUMENT can misbehave if the declaration is + * present (even if it correctly matches the server encoding). * @return A byte array representing the declaration if any, followed * by any readahead. */ @@ -2077,7 +2118,8 @@ byte[] prefix(Charset serverCharset) throws IOException */ boolean canOmitVersion = true; // no declaration => 1.0 byte[] version = new byte[] { '1', '.', '0' }; - boolean canOmitEncoding = "UTF-8".equals(serverCharset.name()); + boolean canOmitEncoding = + null == serverCharset || "UTF-8".equals(serverCharset.name()); boolean canOmitStandalone = true; byte[] parseResult = m_save.toByteArray(); @@ -2123,18 +2165,21 @@ byte[] prefix(Charset serverCharset) throws IOException baos.write('"'); baos.write(version); baos.write('"'); - baos.write(' '); - baos.write(s_tpl, 17, 8); // encoding - baos.write('='); - baos.write('"'); - /* - * This is no different from all the rest of this class in - * relying on the current fact that all supported PG server - * encodings match ASCII as far as the characters for decls go. - * It's just a bit more explicit here. - */ - baos.write(serverCharset.name().getBytes(serverCharset)); - baos.write('"'); + if ( null != serverCharset ) + { + baos.write(' '); + baos.write(s_tpl, 17, 8); // encoding + baos.write('='); + baos.write('"'); + /* + * This is no different from all the rest of this class in + * relying on the current fact that all supported PG server + * encodings match ASCII as far as the characters for decls + * go. It's just a bit more explicit here. + */ + baos.write(serverCharset.name().getBytes(serverCharset)); + baos.write('"'); + } if ( ! canOmitStandalone ) { baos.write(' '); From b9e18b8edf1669b595275f345f521d17526756a5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 17 Feb 2019 01:29:37 -0500 Subject: [PATCH 0250/1087] The allow-java-encodings feature doesn't work. The property is recognized, and is set in the property manager. But com.sun.org.apache.xerces.internal.impl.XMLEntityManager has overloaded reset methods, one taking a PropertyManager, and one taking a ComponentManager. Only the second one honors the feature. XMLStreamReaderImpl.reset() calls the first one, natch. But all is not lost, as createXMLStreamReader with an encoding parameter seems to accept it without caring about its IANA-or-Java-ness. It seems a bit redundant to both arrange to have the proper encoding declared in the stream and to pass it to createXMLStreamReader, but it gets the job done. --- .../main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 495aefdf..a598c6ca 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -642,16 +642,9 @@ public T getSource(Class sourceClass) { XMLInputFactory xif = XMLInputFactory.newFactory(); xif.setProperty(xif.IS_NAMESPACE_AWARE, true); - if ( ! m_serverCS.isRegistered() ) - { - String hack = - "http://apache.org/xml/features/allow-java-encodings"; - if ( xif.isPropertySupported(hack) ) - xif.setProperty(hack, Boolean.TRUE); - } XMLStreamReader xsr = xif.createXMLStreamReader( - correctedDeclStream(is, false)); + correctedDeclStream(is, false), m_serverCS.name()); if ( m_wrapped ) xsr = new StAXUnwrapFilter(xsr); return sourceClass.cast(new StAXSource(xsr)); From 7e22c0b34c2f988aaa61512abdb9edac25806a41 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 10:44:46 -0500 Subject: [PATCH 0251/1087] Prepare to migrate to DualState from other idioms. Older code where a Java object is associated with some native state has been using some combination of Object.finalize() (deprecated for removal in newer Java environments), JavaWrapper, Invocation_createLocalWrapper, with a lot of details scattered in a lot of places, and not a lot of explanation. The DualState mechanism introduced with VarlenaWrapper should be able to subsume all of those cases, eliminate the use of Object.finalize, and reduce (maybe eliminate?) the use of JNI from threads other than the main PG thread. Prepare the way by declaring that the "resource owner" associated with a DualState does not have to be only a PG ResourceOwner. It can also be the address of some other sort of allocated struct that has a scope-like behavior. In PG 9.5 and later, a MemoryContext with a MemoryContextCallback could serve, and PL/Java's currentInvocation can be used to scope a state to the currently-executing PL/Java function. Unless the memory allocator's broken, such alternative allocated addresses won't be mistakable for actual ResourceOwners, so no special magic should be needed for DualState objects scoped in any of those ways to coexist. In passing, change the queue implementation for s_liveInstances in DualState. LinkedBlockingDeque was more heavyweight than needed; it was chosen in haste after starting with ConcurrentLinkedDeque and then finding it wasn't in Java 6, but ConcurrentLinkedQueue is, and has all the capabilities needed. Its iterator().remove() method really runs in constant amortized time, as it just leaves the unlinking to be done on a subsequent traversal, which happens regularly in our case. If it can be proven that no manipulation of s_liveInstances will happen except on one thread interacting with PG, it could be overkill to be using a concurrent queue at all. Get rid of HeapTupleHeader.java, which has been dead since a4f6c9e. --- pljava-so/src/main/c/DualState.c | 56 ++++++++--- pljava-so/src/main/include/pljava/DualState.h | 4 +- .../postgresql/pljava/internal/DualState.java | 99 ++++++++++++++++++- .../pljava/internal/HeapTupleHeader.java | 56 ----------- 4 files changed, 138 insertions(+), 77 deletions(-) delete mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/HeapTupleHeader.java diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 704a782e..b3163d43 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2019 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 @@ -33,17 +33,56 @@ static jobject s_DualState_key; static void resourceReleaseCB(ResourceReleasePhase phase, bool isCommit, bool isTopLevel, void *arg); +/* + * Return a capability that is only expected to be accessible to native code. + */ jobject pljava_DualState_key(void) { return s_DualState_key; } +/* + * Rather than using finalizers (deprecated in recent Java anyway), which can + * increase the number of threads needing to interact with PG, DualState objects + * will be enqueued on a ReferenceQueue when their referents become unreachable, + * and this function should be called from strategically-chosen points in native + * code so the thread already interacting with PG will clean the enqueued items. + */ void pljava_DualState_cleanEnqueuedInstances(void) { JNI_callStaticVoidMethodLocked(s_DualState_class, s_DualState_cleanEnqueuedInstances); } +/* + * Called when the lifespan/scope of a particular PG resource owner is about to + * expire, to make the associated DualState objects inaccessible from Java. As + * described in DualState.java, the argument will often be a PG ResourceOwner + * (when this function is called by resourceReleaseCB), but pointers to other + * structures can also be used (such a pointer clearly can't be confused with a + * ResourceOwner existing at the same time). In PG 9.5+, it could be a + * MemoryContext, with a MemoryContextCallback established to call this + * function. For items whose scope is limited to a single PL/Java function + * invocation, this can be a pointer to the Invocation. + */ +void pljava_DualState_nativeRelease(void *ro) +{ + Ptr2Long p2l; + + /* + * This static assertion does not need to be in every file + * that uses Ptr2Long, but it should be somewhere once, so here it is. + */ + StaticAssertStmt(sizeof p2l.ptrVal <= sizeof p2l.longVal, + "Pointer will not fit in long on this platform"); + + p2l.longVal = 0L; + p2l.ptrVal = ro; + JNI_callStaticVoidMethodLocked(s_DualState_class, + s_DualState_resourceOwnerRelease, + p2l.longVal); +} + void pljava_DualState_initialize(void) { jclass clazz; @@ -103,15 +142,6 @@ void pljava_DualState_initialize(void) static void resourceReleaseCB(ResourceReleasePhase phase, bool isCommit, bool isTopLevel, void *arg) { - Ptr2Long p2l; - - /* - * This static assertion does not need to be in every file - * that uses Ptr2Long, but it should be somewhere once, so here it is. - */ - StaticAssertStmt(sizeof p2l.ptrVal <= sizeof p2l.longVal, - "Pointer will not fit in long on this platform"); - /* * The way ResourceOwnerRelease is implemented, callbacks to loadable * modules (like us!) happen /after/ all of the built-in releasey actions @@ -124,11 +154,7 @@ static void resourceReleaseCB(ResourceReleasePhase phase, if ( RESOURCE_RELEASE_LOCKS != phase ) return; - p2l.longVal = 0L; - p2l.ptrVal = CurrentResourceOwner; - JNI_callStaticVoidMethodLocked(s_DualState_class, - s_DualState_resourceOwnerRelease, - p2l.longVal); + pljava_DualState_nativeRelease(CurrentResourceOwner); } diff --git a/pljava-so/src/main/include/pljava/DualState.h b/pljava-so/src/main/include/pljava/DualState.h index 6148f1a5..16f4ecf6 100644 --- a/pljava-so/src/main/include/pljava/DualState.h +++ b/pljava-so/src/main/include/pljava/DualState.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2019 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -27,6 +27,8 @@ extern void pljava_DualState_cleanEnqueuedInstances(void); extern void pljava_DualState_initialize(void); +extern void pljava_DualState_nativeRelease(void *); + #ifdef __cplusplus } #endif diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 5f71825f..c1bfc98c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2019 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 @@ -16,9 +16,9 @@ import java.sql.SQLException; -import java.util.Deque; +import java.util.Queue; import java.util.Iterator; -import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ConcurrentLinkedQueue; /** * Base class for object state with corresponding Java and native components. @@ -54,6 +54,18 @@ * called at their {@link #nativeStateReleased nativeStateReleased} methods * when the corresponding {@code ResourceOwner} is released in PostgreSQL. *

    + * However, this class does not require the {@code resourceOwner} parameter to + * be, in all cases, a pointer to a PostgreSQL {@code ResourceOwner}. It is + * treated simply as an opaque {@code long} value, to be compared to a value + * passed at release time (as if in a {@code ResourceOwner} callback). Other + * values (such as pointers to other allocated structures, which of course + * cannot match any PG {@code ResourceOwner} existing at the same time) can also + * be used. In PostgreSQL 9.5 and later, a {@code MemoryContext} could be used, + * with its address passed to a {@code MemoryContextCallback} for release. For + * state that is scoped to a single invocation of a PL/Java function, the + * address of the {@code Invocation} can be used. Such references can be + * considered "generalized" resource owners. + *

    * Instances will be enqueued on a {@link ReferenceQueue} when found by the Java * garbage collector to be unreachable. The * {@link #cleanEnqueuedInstances cleanEnqueuedInstances} static method will @@ -94,8 +106,8 @@ public abstract class DualState extends WeakReference /** * All instances are added to this collection upon creation. */ - private static Deque s_liveInstances = - new LinkedBlockingDeque(); + private static Queue s_liveInstances = + new ConcurrentLinkedQueue(); /** * Pointer value of the {@code ResourceOwner} this instance belongs to, @@ -362,6 +374,83 @@ private Key() } } + /** + * A {@code DualState} subclass serving only to guard access to a single + * nonzero {@code long} value (typically a native pointer). + *

    + * Nothing in particular is done to the native resource at the time of + * {@code javaStateReleased} or {@code javaStateUnreachable}; if it is + * subject to reclamation, this class assumes it will be shortly, in the + * normal operation of the native code. This can be appropriate for native + * state that was set up by a native caller for a short lifetime, such as a + * single function invocation. + */ + public static abstract class SingleGuardedLong extends DualState + { + private volatile long m_value; + + protected SingleGuardedLong( + Key cookie, T referent, long resourceOwner, long guardedLong) + { + super(cookie, referent, resourceOwner); + m_value = guardedLong; + } + + @Override + public String toString(Object o) + { + return String.format( + "%s GuardedLong(%x)", super.toString(o), m_value); + } + + /** + * For this class, the native state is valid whenever the wrapped + * value is not null. + */ + @Override + protected boolean nativeStateIsValid() + { + return 0 != m_value; + } + + /** + * When the native state is released, the wrapped value is zeroed + * to indicate the state is no longer valid; no other action is taken, + * on the assumption that the resource owner's release will be + * followed by wholesale reclamation of the guarded state anyway. + */ + @Override + protected void nativeStateReleased() + { + m_value = 0; + } + + /** + * When the Java state is released, the wrapped pointer is zeroed to + * indicate the state is no longer valid; no other action is taken. + *

    + * This overrides the inherited default, which would have removed this + * instance from the live instances collection. Users of this class + * should not call this method directly, but simply call + * {@link #enqueue enqueue}, and let the reclamation happen when the + * queue is processed. + */ + @Override + protected void javaStateReleased() + { + m_value = 0; + } + + /** + * Allows a subclass to obtain the wrapped value. + */ + protected long getValue() throws SQLException + { + assertNativeStateIsValid(); + return m_value; + } + } + /** * A {@code DualState} subclass whose only native resource releasing action * needed is {@code pfree} of a single pointer. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/HeapTupleHeader.java b/pljava/src/main/java/org/postgresql/pljava/internal/HeapTupleHeader.java deleted file mode 100644 index 887c401d..00000000 --- a/pljava/src/main/java/org/postgresql/pljava/internal/HeapTupleHeader.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html - */ -package org.postgresql.pljava.internal; - -import java.sql.SQLException; - -/** - * The HeapTupleHeader correspons to the internal PostgreSQL - * HeapTupleHeader struct. - * - * @author Thomas Hallgren - */ -public class HeapTupleHeader extends JavaWrapper -{ - private final TupleDesc m_tupleDesc; - - HeapTupleHeader(long pointer, TupleDesc tupleDesc) - { - super(pointer); - m_tupleDesc = tupleDesc; - } - - /** - * Obtains a value from the underlying native HeapTupleHeader - * structure. - * @param index Index of value in the structure (one based). - * @return The value or null. - * @throws SQLException If the underlying native structure has gone stale. - */ - public final Object getObject(int index) - throws SQLException - { - synchronized(Backend.THREADLOCK) - { - return _getObject(this.getNativePointer(), m_tupleDesc.getNativePointer(), index); - } - } - - /** - * Obtains the TupleDesc that describes the tuple and returns it. - * @return The TupleDesc that describes this tuple. - */ - public final TupleDesc getTupleDesc() - { - return m_tupleDesc; - } - - protected native void _free(long pointer); - - private static native Object _getObject(long pointer, long tupleDescPointer, int index) - throws SQLException; -} From fdc0cd7d1de44ed26090d5541a3d8204f64e2889 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 16:25:08 -0500 Subject: [PATCH 0252/1087] HeapTupleHeader -> SingleRowReader The architecture has been confusing for a while around HeapTupleHeader (with a long-abandoned Java class and still-used .h/.c files simply providing functions used for SingleRowReader and SQLInputFromTuple, but with some of the associated native functions found in Composite.c). Collect all that stuff into SingleRowReader.[hc], to back the SingleRowReader Java class in the typical way. SQLInputFromTuple can simply be a subclass of that, implementing SQLInput. --- pljava-so/src/main/c/Backend.c | 4 +- pljava-so/src/main/c/DualState.c | 4 + pljava-so/src/main/c/Invocation.c | 12 +- pljava-so/src/main/c/SQLInputFromTuple.c | 83 ++++------- pljava-so/src/main/c/type/Composite.c | 59 +------- pljava-so/src/main/c/type/HeapTupleHeader.c | 81 ----------- pljava-so/src/main/c/type/SingleRowReader.c | 135 ++++++++++++++++++ pljava-so/src/main/c/type/UDT.c | 19 +-- .../main/include/pljava/SQLInputFromTuple.h | 18 ++- .../include/pljava/type/HeapTupleHeader.h | 45 ------ .../include/pljava/type/SingleRowReader.h | 42 ++++++ .../pljava/jdbc/SQLInputFromTuple.java | 86 +++-------- .../pljava/jdbc/SingleRowReader.java | 65 +++++---- 13 files changed, 304 insertions(+), 349 deletions(-) delete mode 100644 pljava-so/src/main/c/type/HeapTupleHeader.c create mode 100644 pljava-so/src/main/c/type/SingleRowReader.c delete mode 100644 pljava-so/src/main/include/pljava/type/HeapTupleHeader.h create mode 100644 pljava-so/src/main/include/pljava/type/SingleRowReader.h diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 3f1495b1..4108c09c 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -125,7 +125,6 @@ extern void XactListener_initialize(void); extern void SubXactListener_initialize(void); extern void SQLInputFromChunk_initialize(void); extern void SQLOutputToChunk_initialize(void); -extern void SQLInputFromTuple_initialize(void); extern void SQLOutputToTuple_initialize(void); @@ -848,7 +847,6 @@ static void initPLJavaClasses(void) SubXactListener_initialize(); SQLInputFromChunk_initialize(); SQLOutputToChunk_initialize(); - SQLInputFromTuple_initialize(); SQLOutputToTuple_initialize(); InstallHelper_initialize(); diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index b3163d43..c79da4bc 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -21,6 +21,8 @@ /* * Includes for objects dependent on DualState, so they can be initialized here */ +#include "pljava/type/SingleRowReader.h" +#include "pljava/SQLInputFromTuple.h" #include "pljava/VarlenaWrapper.h" static jclass s_DualState_class; @@ -136,6 +138,8 @@ void pljava_DualState_initialize(void) /* * Call initialize() methods of known classes built upon DualState. */ + pljava_SingleRowReader_initialize(); + pljava_SQLInputFromTuple_initialize(); pljava_VarlenaWrapper_initialize(); } diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index b3290d23..af18a68d 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -172,6 +172,11 @@ void Invocation_popInvocation(bool wasException) CallLocal* cl; Invocation* ctx = currentInvocation->previous; + /* + * If a Java Invocation instance was created and associated with this + * invocation, delete the reference (after calling its onExit method, for + * non-exceptional returns). + */ if(currentInvocation->invocation != 0) { if(!wasException) @@ -179,6 +184,11 @@ void Invocation_popInvocation(bool wasException) JNI_deleteGlobalRef(currentInvocation->invocation); } + /* + * Do nativeRelease for any DualState instances scoped to this invocation. + */ + pljava_DualState_nativeRelease(currentInvocation); + /* * Check for any DualState objects that became unreachable and can be freed. */ diff --git a/pljava-so/src/main/c/SQLInputFromTuple.c b/pljava-so/src/main/c/SQLInputFromTuple.c index 7cdf3109..8aae6567 100644 --- a/pljava-so/src/main/c/SQLInputFromTuple.c +++ b/pljava-so/src/main/c/SQLInputFromTuple.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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,79 +13,44 @@ * @author Thomas Hallgren */ #include -#include "pljava/type/HeapTupleHeader.h" +#include "pljava/type/SingleRowReader.h" #include "pljava/type/TupleDesc.h" +#include "pljava/DualState.h" #include "pljava/Invocation.h" #include "pljava/SQLInputFromTuple.h" -#include "org_postgresql_pljava_jdbc_SQLInputFromTuple.h" - static jclass s_SQLInputFromTuple_class; static jmethodID s_SQLInputFromTuple_init; -jobject SQLInputFromTuple_create(HeapTupleHeader hth, TupleDesc td) +jobject pljava_SQLInputFromTuple_create(HeapTupleHeader hth) { - jobject tupleDesc; + Ptr2Long p2lht; + Ptr2Long p2lro; jobject result; - jlong pointer; - - if(hth == 0) - return 0; + jobject jtd = pljava_SingleRowReader_getTupleDesc(hth); - tupleDesc = TupleDesc_create(td); - pointer = Invocation_createLocalWrapper(hth); - result = JNI_newObject(s_SQLInputFromTuple_class, s_SQLInputFromTuple_init, pointer, tupleDesc); - JNI_deleteLocalRef(tupleDesc); - return result; -} + p2lht.longVal = 0L; + p2lro.longVal = 0L; -/* Make this datatype available to the postgres system. - */ -extern void SQLInputFromTuple_initialize(void); -void SQLInputFromTuple_initialize(void) -{ - JNINativeMethod methods[] = - { - { - "_getObject", - "(JJILjava/lang/Class;)Ljava/lang/Object;", - Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1getObject - }, - { - "_free", - "(J)V", - Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1free - }, - { 0, 0, 0 } - }; + p2lht.ptrVal = hth; + p2lro.ptrVal = currentInvocation; - s_SQLInputFromTuple_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/jdbc/SQLInputFromTuple")); - PgObject_registerNatives2(s_SQLInputFromTuple_class, methods); - s_SQLInputFromTuple_init = PgObject_getJavaMethod(s_SQLInputFromTuple_class, "", "(JLorg/postgresql/pljava/internal/TupleDesc;)V"); -} + result = + JNI_newObject(s_SQLInputFromTuple_class, s_SQLInputFromTuple_init, + pljava_DualState_key(), p2lro.longVal, p2lht.longVal, jtd); -/**************************************** - * JNI methods - ****************************************/ - -/* - * Class: org_postgresql_pljava_jdbc_SQLInputFromTuple - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1free(JNIEnv* env, jobject _this, jlong hth) -{ - HeapTupleHeader_free(env, hth); + JNI_deleteLocalRef(jtd); + return result; } -/* - * Class: org_postgresql_pljava_jdbc_SQLInputFromTuple - * Method: _getObject - * Signature: (JJILjava/lang/Class;)Ljava/lang/Object; +/* Make this datatype available to the postgres system. */ -JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_jdbc_SQLInputFromTuple__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo, jclass rqcls) +void pljava_SQLInputFromTuple_initialize(void) { - return HeapTupleHeader_getObject(env, hth, jtd, attrNo, rqcls); + jclass cls = + PgObject_getJavaClass("org/postgresql/pljava/jdbc/SQLInputFromTuple"); + s_SQLInputFromTuple_init = PgObject_getJavaMethod(cls, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJLorg/postgresql/pljava/internal/TupleDesc;)V"); + s_SQLInputFromTuple_class = JNI_newGlobalRef(cls); + JNI_deleteLocalRef(cls); } diff --git a/pljava-so/src/main/c/type/Composite.c b/pljava-so/src/main/c/type/Composite.c index 2959c0ef..f17db9b3 100644 --- a/pljava-so/src/main/c/type/Composite.c +++ b/pljava-so/src/main/c/type/Composite.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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,7 +17,7 @@ #include "pljava/type/Type_priv.h" #include "pljava/type/Composite.h" #include "pljava/type/TupleDesc.h" -#include "pljava/type/HeapTupleHeader.h" +#include "pljava/type/SingleRowReader.h" #include "pljava/Invocation.h" #include "org_postgresql_pljava_jdbc_SingleRowReader.h" @@ -46,9 +46,6 @@ static jclass s_ResultSetHandle_class; static jclass s_ResultSetPicker_class; static jmethodID s_ResultSetPicker_init; -static jclass s_SingleRowReader_class; -static jmethodID s_SingleRowReader_init; - static jclass s_SingleRowWriter_class; static jmethodID s_SingleRowWriter_init; static jmethodID s_SingleRowWriter_getTupleAndClear; @@ -172,19 +169,14 @@ static void _Composite_closeSRF(Type self, jobject rowProducer) */ static jvalue _Composite_coerceDatum(Type self, Datum arg) { - jobject tupleDesc; jvalue result; - jlong pointer; HeapTupleHeader hth = DatumGetHeapTupleHeader(arg); result.l = 0; if(hth == 0) return result; - tupleDesc = HeapTupleHeader_getTupleDesc(hth); - pointer = Invocation_createLocalWrapper(hth); - result.l = JNI_newObject(s_SingleRowReader_class, s_SingleRowReader_init, pointer, tupleDesc); - JNI_deleteLocalRef(tupleDesc); + result.l = pljava_SingleRowReader_create(hth); return result; } @@ -263,25 +255,6 @@ Type Composite_obtain(Oid typeId) extern void Composite_initialize(void); void Composite_initialize(void) { - JNINativeMethod methods[] = - { - { - "_getObject", - "(JJILjava/lang/Class;)Ljava/lang/Object;", - Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject - }, - { - "_free", - "(J)V", - Java_org_postgresql_pljava_jdbc_SingleRowReader__1free - }, - { 0, 0, 0 } - }; - - s_SingleRowReader_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/jdbc/SingleRowReader")); - PgObject_registerNatives2(s_SingleRowReader_class, methods); - s_SingleRowReader_init = PgObject_getJavaMethod(s_SingleRowReader_class, "", "(JLorg/postgresql/pljava/internal/TupleDesc;)V"); - s_SingleRowWriter_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/jdbc/SingleRowWriter")); s_SingleRowWriter_init = PgObject_getJavaMethod(s_SingleRowWriter_class, "", "(Lorg/postgresql/pljava/internal/TupleDesc;)V"); s_SingleRowWriter_getTupleAndClear = PgObject_getJavaMethod(s_SingleRowWriter_class, "getTupleAndClear", "()J"); @@ -310,29 +283,3 @@ void Composite_initialize(void) Type_registerType2(InvalidOid, "java.sql.ResultSet", Composite_obtain); } - -/**************************************** - * JNI methods - ****************************************/ - -/* - * Class: org_postgresql_pljava_jdbc_SingleRowReader - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_jdbc_SingleRowReader__1free(JNIEnv* env, jobject _this, jlong hth) -{ - HeapTupleHeader_free(env, hth); -} - -/* - * Class: org_postgresql_pljava_jdbc_SingleRowReader - * Method: _getObject - * Signature: (JJILjava/lang/Class;)Ljava/lang/Object; - */ -JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo, jclass rqcls) -{ - return HeapTupleHeader_getObject(env, hth, jtd, attrNo, rqcls); -} diff --git a/pljava-so/src/main/c/type/HeapTupleHeader.c b/pljava-so/src/main/c/type/HeapTupleHeader.c deleted file mode 100644 index db523e0a..00000000 --- a/pljava-so/src/main/c/type/HeapTupleHeader.c +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2004-2018 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: - * Tada AB - * PostgreSQL Global Development Group - * Chapman Flack - * - * @author Thomas Hallgren - */ -#include "pljava/type/Type_priv.h" -#include "pljava/type/HeapTupleHeader.h" - -#include -#include -#include - -#include "pljava/Exception.h" -#include "pljava/Invocation.h" -#include "pljava/type/TupleDesc.h" - -jobject HeapTupleHeader_getTupleDesc(HeapTupleHeader ht) -{ - jobject result; - TupleDesc tupleDesc = - lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(ht), - HeapTupleHeaderGetTypMod(ht)); - result = TupleDesc_create(tupleDesc); - /* - * TupleDesc_create() creates a copy of the tuple descriptor, so - * can release this now - */ - ReleaseTupleDesc(tupleDesc); - return result; -} - -jobject HeapTupleHeader_getObject( - JNIEnv* env, jlong hth, jlong jtd, jint attrNo, jclass rqcls) -{ - jobject result = 0; - HeapTupleHeader self = (HeapTupleHeader)Invocation_getWrappedPointer(hth); - if(self != 0 && jtd != 0) - { - Ptr2Long p2l; - p2l.longVal = jtd; - BEGIN_NATIVE - PG_TRY(); - { - Type type = TupleDesc_getColumnType( - (TupleDesc) p2l.ptrVal, (int) attrNo); - if (type != 0) - { - Datum binVal; - bool wasNull = false; - binVal = GetAttributeByNum(self, (AttrNumber)attrNo, &wasNull); - if(!wasNull) - result = Type_coerceDatumAs(type, binVal, rqcls).l; - } - } - PG_CATCH(); - { - Exception_throw_ERROR("GetAttributeByNum"); - } - PG_END_TRY(); - END_NATIVE - } - return result; - -} - -void HeapTupleHeader_free(JNIEnv* env, jlong hth) -{ - BEGIN_NATIVE_NO_ERRCHECK - Invocation_freeLocalWrapper(hth); - END_NATIVE -} diff --git a/pljava-so/src/main/c/type/SingleRowReader.c b/pljava-so/src/main/c/type/SingleRowReader.c new file mode 100644 index 00000000..efdcfb88 --- /dev/null +++ b/pljava-so/src/main/c/type/SingleRowReader.c @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2004-2019 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: + * Tada AB + * PostgreSQL Global Development Group + * Chapman Flack + * + * @author Thomas Hallgren + */ +#include "pljava/type/Type_priv.h" +#include "pljava/type/SingleRowReader.h" + +#include +#include +#include + +#include "pljava/DualState.h" +#include "pljava/Exception.h" +#include "pljava/Invocation.h" +#include "pljava/type/TupleDesc.h" + +#include "org_postgresql_pljava_jdbc_SingleRowReader.h" + +static jclass s_SingleRowReader_class; +static jmethodID s_SingleRowReader_init; + +jobject pljava_SingleRowReader_getTupleDesc(HeapTupleHeader ht) +{ + jobject result; + TupleDesc tupleDesc = + lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(ht), + HeapTupleHeaderGetTypMod(ht)); + result = TupleDesc_create(tupleDesc); + /* + * TupleDesc_create() creates a copy of the tuple descriptor, so + * can release this now + */ + ReleaseTupleDesc(tupleDesc); + return result; +} + +jobject pljava_SingleRowReader_create(HeapTupleHeader ht) +{ + Ptr2Long p2lht; + Ptr2Long p2lro; + jobject result; + jobject jtd = pljava_SingleRowReader_getTupleDesc(ht); + + p2lht.longVal = 0L; + p2lro.longVal = 0L; + + p2lht.ptrVal = ht; + p2lro.ptrVal = currentInvocation; + + result = + JNI_newObject(s_SingleRowReader_class, s_SingleRowReader_init, + pljava_DualState_key(), p2lro.longVal, p2lht.longVal, jtd); + + JNI_deleteLocalRef(jtd); + return result; +} + +/* Make this datatype available to the postgres system. + */ +void pljava_SingleRowReader_initialize(void) +{ + JNINativeMethod methods[] = + { + { + "_getObject", + "(JJILjava/lang/Class;)Ljava/lang/Object;", + Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject + }, + { 0, 0, 0 } + }; + jclass cls = + PgObject_getJavaClass("org/postgresql/pljava/jdbc/SingleRowReader"); + PgObject_registerNatives2(cls, methods); + s_SingleRowReader_init = PgObject_getJavaMethod(cls, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJLorg/postgresql/pljava/internal/TupleDesc;)V"); + s_SingleRowReader_class = JNI_newGlobalRef(cls); + JNI_deleteLocalRef(cls); +} + + +/**************************************** + * JNI methods + ****************************************/ + +/* + * Class: org_postgresql_pljava_jdbc_SingleRowReader + * Method: _getObject + * Signature: (JJILjava/lang/Class;)Ljava/lang/Object; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject(JNIEnv* env, jclass clazz, jlong hth, jlong jtd, jint attrNo, jclass rqcls) +{ + jobject result = 0; + if(hth != 0 && jtd != 0) + { + Ptr2Long p2lhth; + Ptr2Long p2ltd; + p2lhth.longVal = hth; + p2ltd.longVal = jtd; + BEGIN_NATIVE + PG_TRY(); + { + Type type = TupleDesc_getColumnType( + (TupleDesc) p2ltd.ptrVal, (int) attrNo); + if (type != 0) + { + Datum binVal; + bool wasNull = false; + binVal = GetAttributeByNum( + (HeapTupleHeader)p2lhth.ptrVal, + (AttrNumber)attrNo, &wasNull); + if(!wasNull) + result = Type_coerceDatumAs(type, binVal, rqcls).l; + } + } + PG_CATCH(); + { + Exception_throw_ERROR("GetAttributeByNum"); + } + PG_END_TRY(); + END_NATIVE + } + return result; +} diff --git a/pljava-so/src/main/c/type/UDT.c b/pljava-so/src/main/c/type/UDT.c index 2dd750df..63ba76ae 100644 --- a/pljava-so/src/main/c/type/UDT.c +++ b/pljava-so/src/main/c/type/UDT.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -86,11 +92,8 @@ static jobject coerceScalarDatum(UDT self, Datum arg) static jobject coerceTupleDatum(UDT udt, Datum arg) { jobject result = JNI_newObject(Type_getJavaClass((Type)udt), udt->init); - Oid typeId = ((Type)udt)->typeId; - TupleDesc tupleDesc = lookup_rowtype_tupdesc_noerror(typeId, -1, true); jobject inputStream = - SQLInputFromTuple_create(DatumGetHeapTupleHeader(arg), tupleDesc); - ReleaseTupleDesc(tupleDesc); + pljava_SQLInputFromTuple_create(DatumGetHeapTupleHeader(arg)); JNI_callVoidMethod(result, udt->readSQL, inputStream, udt->sqlTypeName); JNI_deleteLocalRef(inputStream); return result; diff --git a/pljava-so/src/main/include/pljava/SQLInputFromTuple.h b/pljava-so/src/main/include/pljava/SQLInputFromTuple.h index 1483f92c..85f815a3 100644 --- a/pljava-so/src/main/include/pljava/SQLInputFromTuple.h +++ b/pljava-so/src/main/include/pljava/SQLInputFromTuple.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -24,7 +30,9 @@ extern "C" { * ***********************************************************************/ -extern jobject SQLInputFromTuple_create(HeapTupleHeader hth, TupleDesc td); +extern void pljava_SQLInputFromTuple_initialize(void); + +extern jobject pljava_SQLInputFromTuple_create(HeapTupleHeader hth); #ifdef __cplusplus } /* end of extern "C" declaration */ diff --git a/pljava-so/src/main/include/pljava/type/HeapTupleHeader.h b/pljava-so/src/main/include/pljava/type/HeapTupleHeader.h deleted file mode 100644 index 5f2e3552..00000000 --- a/pljava-so/src/main/include/pljava/type/HeapTupleHeader.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2004-2018 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: - * Tada AB - * Chapman Flack - * - * @author Thomas Hallgren - */ -#ifndef __pljava_type_HeapTupleHeader_h -#define __pljava_type_HeapTupleHeader_h - -#include "pljava/type/Type.h" -#ifdef __cplusplus -extern "C" { -#endif - -#include - -/***************************************************************** - * The HeapTupleHeader java class extends the NativeStruct and provides JNI - * access to some of the attributes of the HeapTupleHeader structure. - * - * @author Thomas Hallgren - * - * (As of git commit a4f6c9e, there are uses of this C interface, - * but no uses of the Java class.) - *****************************************************************/ - -extern jobject HeapTupleHeader_getTupleDesc(HeapTupleHeader ht); - -extern jobject HeapTupleHeader_getObject( - JNIEnv* env, jlong hth, jlong jtd, jint attrNo, jclass rqcls); - -extern void HeapTupleHeader_free(JNIEnv* env, jlong hth); - -#ifdef __cplusplus -} -#endif -#endif diff --git a/pljava-so/src/main/include/pljava/type/SingleRowReader.h b/pljava-so/src/main/include/pljava/type/SingleRowReader.h new file mode 100644 index 00000000..3894da80 --- /dev/null +++ b/pljava-so/src/main/include/pljava/type/SingleRowReader.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack + * + * @author Thomas Hallgren + */ +#ifndef __pljava_type_SingleRowReader_h +#define __pljava_type_SingleRowReader_h + +#include "pljava/type/Type.h" +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/***************************************************************** + * The SingleRowReader java class presents a ResultSet view of a + * single tuple, represented by a HeapTupleHeader and a TupleDesc + * describing its structure. + * + * @author Thomas Hallgren (as HeapTupleHeader.h) + *****************************************************************/ + +extern void pljava_SingleRowReader_initialize(void); + +extern jobject pljava_SingleRowReader_getTupleDesc(HeapTupleHeader); + +extern jobject pljava_SingleRowReader_create(HeapTupleHeader); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java index ef0724aa..35f5310a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -25,14 +25,14 @@ import java.sql.RowId; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLNonTransientException; import java.sql.SQLInput; import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; import org.postgresql.pljava.internal.Backend; -import org.postgresql.pljava.internal.JavaWrapper; +import org.postgresql.pljava.internal.DualState; import org.postgresql.pljava.internal.TupleDesc; /** @@ -42,24 +42,30 @@ * * @author Thomas Hallgren */ -public class SQLInputFromTuple extends JavaWrapper implements SQLInput +public class SQLInputFromTuple extends SingleRowReader implements SQLInput { private int m_index; - private final TupleDesc m_tupleDesc; - private boolean m_wasNull; + private final int m_columns; /** * Construct an instance, given the (native) pointer to a PG * {@code HeapTupleHeader}, as well as the TupleDesc (Java object this time) * describing its structure. */ - public SQLInputFromTuple(long heapTupleHeaderPointer, TupleDesc tupleDesc) + public SQLInputFromTuple(DualState.Key cookie, long resourceOwner, + long heapTupleHeaderPointer, TupleDesc tupleDesc) throws SQLException { - super(heapTupleHeaderPointer); - m_tupleDesc = tupleDesc; - m_index = 0; - m_wasNull = false; + super(cookie, resourceOwner, heapTupleHeaderPointer, tupleDesc); + m_index = 0; + m_columns = tupleDesc.size(); + } + + protected int nextIndex() throws SQLException + { + if ( m_index >= m_columns ) + throw new SQLNonTransientException("Tuple has no more columns"); + return ++m_index; } /** @@ -209,19 +215,7 @@ public long readLong() throws SQLException @Override public Object readObject() throws SQLException { - if(m_index < m_tupleDesc.size()) - { - Object v; - synchronized(Backend.THREADLOCK) - { - v = _getObject( - this.getNativePointer(), m_tupleDesc.getNativePointer(), - ++m_index, null); - } - m_wasNull = v == null; - return v; - } - throw new SQLException("Tuple has no more columns"); + return getObject(nextIndex()); } /** @@ -279,12 +273,6 @@ public URL readURL() throws SQLException return (URL)this.readValue(URL.class); } - @Override - public boolean wasNull() throws SQLException - { - return m_wasNull; - } - // ************************************************************ // Implementation of JDBC 4 methods. Methods go here if they // don't throw SQLFeatureNotSupportedException; they can be @@ -345,50 +333,20 @@ public NClob readNClob() public T readObject(Class type) throws SQLException { - if(m_index < m_tupleDesc.size()) - { - Object v; - synchronized(Backend.THREADLOCK) - { - v = _getObject( - this.getNativePointer(), m_tupleDesc.getNativePointer(), - ++m_index, type); - } - m_wasNull = v == null; - if ( m_wasNull || type.isInstance(v) ) - return type.cast(v); - throw new SQLException("Cannot convert " + v.getClass().getName() + - " to " + type.getName()); - } - throw new SQLException("Tuple has no more columns"); + return getObject(nextIndex(), type); } // ************************************************************ - // Implementation methods. + // Implementation methods, over methods of ObjectResultSet. // ************************************************************ private Number readNumber(Class numberClass) throws SQLException { - return SPIConnection.basicNumericCoersion( - numberClass, this.readObject()); + return getNumber(nextIndex(), numberClass); } private Object readValue(Class valueClass) throws SQLException { - return SPIConnection.basicCoersion(valueClass, this.readObject()); + return getValue(nextIndex(), valueClass); } - - protected native void _free(long pointer); - - /** - * Underlying method that returns the value of the next attribute. - *

    - * The signature does not constrain this to return an object of the - * requested class, so it can still be used as before by methods that may do - * additional coercions. When called by {@link #getObject(Class)}, that - * caller enforces the class of the result. - */ - private static native Object _getObject( - long pointer, long tupleDescPointer, int index, Class type) - throws SQLException; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java index e45b7e5d..563a1430 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * * All rights reserved. This program and the accompanying materials @@ -17,6 +17,7 @@ import java.sql.SQLException; import org.postgresql.pljava.internal.Backend; +import org.postgresql.pljava.internal.DualState; import org.postgresql.pljava.internal.TupleDesc; /** @@ -29,20 +30,48 @@ public class SingleRowReader extends SingleRowResultSet { private final TupleDesc m_tupleDesc; - private final long m_pointer; + private final State m_state; + + private static class State + extends DualState.SingleGuardedLong + { + private State( + DualState.Key cookie, SingleRowReader srr, long ro, long hth) + { + super(cookie, srr, ro, hth); + } + + /** + * Return the HeapTupleHeader pointer. + *

    + * As long as this value is used in instance methods on SingleRowReader + * (or subclasses) and only while they hold Backend.THREADLOCK, it isn't + * necessary to also hold the monitor on this State object. The state + * can't go java-unreachable while an instance method's on the stack, + * and as long as we're on the thread that's in PG, the Invocation that + * state is scoped to can't be popped before we return. + */ + private long getHeapTupleHeaderPtr() throws SQLException + { + return getValue(); + } + } /** * Construct a {@code SingleRowReader} from a {@code HeapTupleHeader} * and a {@link TupleDesc TupleDesc}. - * @param pointer Just the native pointer to a PG {@code HeapTupleHeader}; - * the Java class of the same name is uninvolved (it's been dead since - * a4f6c9e). + * @param cookie Capability obtained from native code to construct a + * {@code SingleRowReader} instance. + * @param resourceOwner Value identifying a scope in PostgreSQL during which + * the native state encapsulated here will be valid. + * @param hth Native pointer to a PG {@code HeapTupleHeader} * @param tupleDesc A {@code TupleDesc}; the Java class this time. */ - public SingleRowReader(long pointer, TupleDesc tupleDesc) + public SingleRowReader(DualState.Key cookie, long resourceOwner, long hth, + TupleDesc tupleDesc) throws SQLException { - m_pointer = pointer; + m_state = new State(cookie, this, resourceOwner, hth); m_tupleDesc = tupleDesc; } @@ -51,18 +80,6 @@ public void close() { } - /** - * Frees the {@code HeapTupleHeader}. - */ - @Override - public void finalize() - { - synchronized(Backend.THREADLOCK) - { - _free(m_pointer); - } - } - @Override // defined in ObjectResultSet protected Object getObjectValue(int columnIndex, Class type) throws SQLException @@ -70,7 +87,8 @@ protected Object getObjectValue(int columnIndex, Class type) synchronized(Backend.THREADLOCK) { return _getObject( - m_pointer, m_tupleDesc.getNativePointer(), columnIndex, type); + m_state.getHeapTupleHeaderPtr(), m_tupleDesc.getNativePointer(), + columnIndex, type); } } @@ -186,13 +204,6 @@ protected final TupleDesc getTupleDesc() return m_tupleDesc; } - /* - * Looking for the implementation of the following JNI methods? - * Look in type/Composite.c - */ - - protected native void _free(long pointer); - private static native Object _getObject( long pointer, long tupleDescPointer, int index, Class type) throws SQLException; From 53f86100e56e7fe1fb782f17ff6984528c30db4e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 18:37:09 -0500 Subject: [PATCH 0253/1087] DualState-ify TupleDesc. Still has a public getNativePointer() because both SingleRowReader and Tuple refer to it. --- pljava-so/src/main/c/Backend.c | 2 +- pljava-so/src/main/c/DualState.c | 36 +++++++ pljava-so/src/main/c/SQLOutputToTuple.c | 16 +++- pljava-so/src/main/c/type/Composite.c | 4 +- pljava-so/src/main/c/type/Portal.c | 4 +- pljava-so/src/main/c/type/Relation.c | 16 +++- pljava-so/src/main/c/type/SingleRowReader.c | 6 +- pljava-so/src/main/c/type/Tuple.c | 4 +- pljava-so/src/main/c/type/TupleDesc.c | 50 ++++------ pljava-so/src/main/c/type/TupleTable.c | 6 +- pljava-so/src/main/c/type/Type.c | 2 - .../src/main/include/pljava/type/TupleDesc.h | 24 +++-- .../postgresql/pljava/internal/DualState.java | 95 +++++++++++++++++++ .../postgresql/pljava/internal/TupleDesc.java | 63 +++++++++--- 14 files changed, 251 insertions(+), 77 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 4108c09c..334fb271 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -838,8 +838,8 @@ static void initPLJavaClasses(void) Invocation_initialize(); Exception_initialize2(); SPI_initialize(); - pljava_DualState_initialize(); Type_initialize(); + pljava_DualState_initialize(); Function_initialize(); Session_initialize(); PgSavepoint_initialize(); diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index c79da4bc..f366ca67 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -13,6 +13,7 @@ #include "org_postgresql_pljava_internal_DualState_SinglePfree.h" #include "org_postgresql_pljava_internal_DualState_SingleMemContextDelete.h" +#include "org_postgresql_pljava_internal_DualState_SingleFreeTupleDesc.h" #include "pljava/DualState.h" #include "pljava/PgObject.h" @@ -22,6 +23,7 @@ * Includes for objects dependent on DualState, so they can be initialized here */ #include "pljava/type/SingleRowReader.h" +#include "pljava/type/TupleDesc.h" #include "pljava/SQLInputFromTuple.h" #include "pljava/VarlenaWrapper.h" @@ -110,6 +112,16 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod singleFreeTupleDescMethods[] = + { + { + "_freeTupleDesc", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SingleFreeTupleDesc__1freeTupleDesc + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( @@ -133,6 +145,11 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleMemContextDeleteMethods); JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SingleFreeTupleDesc"); + PgObject_registerNatives2(clazz, singleFreeTupleDescMethods); + JNI_deleteLocalRef(clazz); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); /* @@ -140,6 +157,7 @@ void pljava_DualState_initialize(void) */ pljava_SingleRowReader_initialize(); pljava_SQLInputFromTuple_initialize(); + pljava_TupleDesc_initialize(); pljava_VarlenaWrapper_initialize(); } @@ -198,3 +216,21 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleMemContextDelete__1memC MemoryContextDelete(p2l.ptrVal); END_NATIVE } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_SingleFreeTupleDesc + * Method: _freeTupleDesc + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SingleFreeTupleDesc__1freeTupleDesc( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + FreeTupleDesc(p2l.ptrVal); + END_NATIVE +} diff --git a/pljava-so/src/main/c/SQLOutputToTuple.c b/pljava-so/src/main/c/SQLOutputToTuple.c index 4cd8dd31..1cdee9b8 100644 --- a/pljava-so/src/main/c/SQLOutputToTuple.c +++ b/pljava-so/src/main/c/SQLOutputToTuple.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -17,7 +23,7 @@ static jmethodID s_SQLOutputToTuple_getTuple; jobject SQLOutputToTuple_create(TupleDesc td) { - jobject tupleDesc = TupleDesc_create(td); + jobject tupleDesc = pljava_TupleDesc_create(td); jobject result = JNI_newObject(s_SQLOutputToTuple_class, s_SQLOutputToTuple_init, tupleDesc); JNI_deleteLocalRef(tupleDesc); return result; diff --git a/pljava-so/src/main/c/type/Composite.c b/pljava-so/src/main/c/type/Composite.c index f17db9b3..d70c0321 100644 --- a/pljava-so/src/main/c/type/Composite.c +++ b/pljava-so/src/main/c/type/Composite.c @@ -81,7 +81,7 @@ static Datum _Composite_invoke(Type self, jclass cls, jmethodID method, jvalue* bool hasRow; Datum result = 0; TupleDesc tupleDesc = Type_getTupleDesc(self, fcinfo); - jobject jtd = TupleDesc_create(tupleDesc); + jobject jtd = pljava_TupleDesc_create(tupleDesc); jobject singleRowWriter = _createWriter(jtd); int numArgs = fcinfo->nargs; @@ -128,7 +128,7 @@ static jobject _Composite_getSRFCollector(Type self, PG_FUNCTION_ARGS) if(tupleDesc == 0) ereport(ERROR, (errmsg("Unable to find tuple descriptor"))); - tmp1 = TupleDesc_create(tupleDesc); + tmp1 = pljava_TupleDesc_create(tupleDesc); tmp2 = _createWriter(tmp1); JNI_deleteLocalRef(tmp1); return tmp2; diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index 255ef6bd..e1b4a4b3 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -254,7 +254,7 @@ Java_org_postgresql_pljava_internal_Portal__1getTupleDesc(JNIEnv* env, jclass cl BEGIN_NATIVE Ptr2Long p2l; p2l.longVal = _this; - result = TupleDesc_create(((Portal)p2l.ptrVal)->tupDesc); + result = pljava_TupleDesc_create(((Portal)p2l.ptrVal)->tupDesc); END_NATIVE } return result; diff --git a/pljava-so/src/main/c/type/Relation.c b/pljava-so/src/main/c/type/Relation.c index 101ce2af..0e723e96 100644 --- a/pljava-so/src/main/c/type/Relation.c +++ b/pljava-so/src/main/c/type/Relation.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -158,7 +164,7 @@ Java_org_postgresql_pljava_internal_Relation__1getTupleDesc(JNIEnv* env, jclass if(self != 0) { BEGIN_NATIVE - result = TupleDesc_create(self->rd_att); + result = pljava_TupleDesc_create(self->rd_att); END_NATIVE } return result; diff --git a/pljava-so/src/main/c/type/SingleRowReader.c b/pljava-so/src/main/c/type/SingleRowReader.c index efdcfb88..40e0b796 100644 --- a/pljava-so/src/main/c/type/SingleRowReader.c +++ b/pljava-so/src/main/c/type/SingleRowReader.c @@ -36,9 +36,9 @@ jobject pljava_SingleRowReader_getTupleDesc(HeapTupleHeader ht) TupleDesc tupleDesc = lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(ht), HeapTupleHeaderGetTypMod(ht)); - result = TupleDesc_create(tupleDesc); + result = pljava_TupleDesc_create(tupleDesc); /* - * TupleDesc_create() creates a copy of the tuple descriptor, so + * pljava_TupleDesc_create() creates a copy of the tuple descriptor, so * can release this now */ ReleaseTupleDesc(tupleDesc); @@ -111,7 +111,7 @@ Java_org_postgresql_pljava_jdbc_SingleRowReader__1getObject(JNIEnv* env, jclass BEGIN_NATIVE PG_TRY(); { - Type type = TupleDesc_getColumnType( + Type type = pljava_TupleDesc_getColumnType( (TupleDesc) p2ltd.ptrVal, (int) attrNo); if (type != 0) { diff --git a/pljava-so/src/main/c/type/Tuple.c b/pljava-so/src/main/c/type/Tuple.c index ee8feba3..b0c89583 100644 --- a/pljava-so/src/main/c/type/Tuple.c +++ b/pljava-so/src/main/c/type/Tuple.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -110,7 +110,7 @@ Tuple_getObject(TupleDesc tupleDesc, HeapTuple tuple, int index, jclass rqcls) jobject result = 0; PG_TRY(); { - Type type = TupleDesc_getColumnType(tupleDesc, index); + Type type = pljava_TupleDesc_getColumnType(tupleDesc, index); if(type != 0) { bool wasNull = false; diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index 9c44f335..b80375c4 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -16,6 +16,7 @@ #include "org_postgresql_pljava_internal_TupleDesc.h" #include "pljava/Backend.h" +#include "pljava/DualState.h" #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/type/Type_priv.h" @@ -33,19 +34,19 @@ static jmethodID s_TupleDesc_init; * TupleDesc, which will be freed later when Java code calls the native method * _free(). Therefore the caller is done with its TupleDesc when this returns. */ -jobject TupleDesc_create(TupleDesc td) +jobject pljava_TupleDesc_create(TupleDesc td) { jobject jtd = 0; if(td != 0) { MemoryContext curr = MemoryContextSwitchTo(JavaMemoryContext); - jtd = TupleDesc_internalCreate(td); + jtd = pljava_TupleDesc_internalCreate(td); MemoryContextSwitchTo(curr); } return jtd; } -jobject TupleDesc_internalCreate(TupleDesc td) +jobject pljava_TupleDesc_internalCreate(TupleDesc td) { jobject jtd; Ptr2Long tdH; @@ -53,7 +54,15 @@ jobject TupleDesc_internalCreate(TupleDesc td) td = CreateTupleDescCopyConstr(td); tdH.longVal = 0L; /* ensure that the rest is zeroed out */ tdH.ptrVal = td; - jtd = JNI_newObject(s_TupleDesc_class, s_TupleDesc_init, tdH.longVal, (jint)td->natts); + /* + * Passing NULL as the ResourceOwner means this will never be matched by a + * nativeRelease call; that's appropriate (for now) as the TupleDesc copy is + * being made into JavaMemoryContext, which never gets reset, so only + * unreachability from the Java side will free it. + * XXX what about invalidating if DDL alters the column layout? + */ + jtd = JNI_newObject(s_TupleDesc_class, s_TupleDesc_init, + pljava_DualState_key(), NULL, tdH.longVal, (jint)td->natts); return jtd; } @@ -62,7 +71,7 @@ jobject TupleDesc_internalCreate(TupleDesc td) * (caller should expeditiously return), otherwise the Type for the column data * (the one representing the boxing Object type, in the primitive case). */ -Type TupleDesc_getColumnType(TupleDesc tupleDesc, int index) +Type pljava_TupleDesc_getColumnType(TupleDesc tupleDesc, int index) { Type type; Oid typeId = SPI_gettypeid(tupleDesc, index); @@ -80,14 +89,14 @@ Type TupleDesc_getColumnType(TupleDesc tupleDesc, int index) static jvalue _TupleDesc_coerceDatum(Type self, Datum arg) { jvalue result; - result.l = TupleDesc_create((TupleDesc)DatumGetPointer(arg)); + result.l = pljava_TupleDesc_create((TupleDesc)DatumGetPointer(arg)); return result; } /* Make this datatype available to the postgres system. */ -extern void TupleDesc_initialize(void); -void TupleDesc_initialize(void) +extern void pljava_TupleDesc_initialize(void); +void pljava_TupleDesc_initialize(void) { TypeClass cls; JNINativeMethod methods[] = { @@ -111,16 +120,12 @@ void TupleDesc_initialize(void) "(JI)Lorg/postgresql/pljava/internal/Oid;", Java_org_postgresql_pljava_internal_TupleDesc__1getOid }, - { - "_free", - "(J)V", - Java_org_postgresql_pljava_internal_TupleDesc__1free - }, { 0, 0, 0 }}; s_TupleDesc_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/TupleDesc")); PgObject_registerNatives2(s_TupleDesc_class, methods); - s_TupleDesc_init = PgObject_getJavaMethod(s_TupleDesc_class, "", "(JI)V"); + s_TupleDesc_init = PgObject_getJavaMethod(s_TupleDesc_class, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJI)V"); cls = JavaWrapperClass_alloc("type.TupleDesc"); cls->JNISignature = "Lorg/postgresql/pljava/internal/TupleDesc;"; @@ -260,21 +265,6 @@ Java_org_postgresql_pljava_internal_TupleDesc__1formTuple(JNIEnv* env, jclass cl return result; } -/* - * Class: org_postgresql_pljava_internal_TupleDesc - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_TupleDesc__1free(JNIEnv* env, jobject _this, jlong pointer) -{ - BEGIN_NATIVE_NO_ERRCHECK - Ptr2Long p2l; - p2l.longVal = pointer; - FreeTupleDesc((TupleDesc)p2l.ptrVal); - END_NATIVE -} - /* * Class: org_postgresql_pljava_internal_TupleDesc * Method: _getOid diff --git a/pljava-so/src/main/c/type/TupleTable.c b/pljava-so/src/main/c/type/TupleTable.c index 1f710658..5808721c 100644 --- a/pljava-so/src/main/c/type/TupleTable.c +++ b/pljava-so/src/main/c/type/TupleTable.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -34,7 +34,7 @@ jobject TupleTable_createFromSlot(TupleTableSlot* tts) curr = MemoryContextSwitchTo(JavaMemoryContext); - tupdesc = TupleDesc_internalCreate(tts->tts_tupleDescriptor); + tupdesc = pljava_TupleDesc_internalCreate(tts->tts_tupleDescriptor); tuple = ExecCopySlotTuple(tts); tuples = Tuple_createArray(&tuple, 1, false); @@ -62,7 +62,7 @@ jobject TupleTable_create(SPITupleTable* tts, jobject knownTD) curr = MemoryContextSwitchTo(JavaMemoryContext); if(knownTD == 0) - knownTD = TupleDesc_internalCreate(tts->tupdesc); + knownTD = pljava_TupleDesc_internalCreate(tts->tupdesc); tuples = Tuple_createArray(tts->vals, (jint)tupcount, true); MemoryContextSwitchTo(curr); diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index eef8b07c..11f5a823 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -811,7 +811,6 @@ extern void Portal_initialize(void); extern void Relation_initialize(void); extern void TriggerData_initialize(void); extern void Tuple_initialize(void); -extern void TupleDesc_initialize(void); extern void TupleTable_initialize(void); extern void Composite_initialize(void); @@ -855,7 +854,6 @@ void Type_initialize(void) Portal_initialize(); TriggerData_initialize(); Relation_initialize(); - TupleDesc_initialize(); Tuple_initialize(); TupleTable_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/TupleDesc.h b/pljava-so/src/main/include/pljava/type/TupleDesc.h index 7a1a6bd1..5b45489e 100644 --- a/pljava-so/src/main/include/pljava/type/TupleDesc.h +++ b/pljava-so/src/main/include/pljava/type/TupleDesc.h @@ -1,15 +1,20 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ #ifndef __pljava_TupleDesc_h #define __pljava_TupleDesc_h -#include "pljava/type/JavaWrapper.h" #ifdef __cplusplus extern "C" { #endif @@ -17,7 +22,7 @@ extern "C" { #include /******************************************************************** - * The TupleDesc java class extends the NativeStruct and provides JNI + * The TupleDesc java class provides JNI * access to some of the attributes of the TupleDesc structure. * * @author Thomas Hallgren @@ -28,13 +33,14 @@ extern "C" { * is NULL a Java exception has been initiated and the caller * should return to Java ASAP. */ -extern Type TupleDesc_getColumnType(TupleDesc tupleDesc, int index); +extern Type pljava_TupleDesc_getColumnType(TupleDesc tupleDesc, int index); /* * Create the org.postgresql.pljava.TupleDesc instance */ -extern jobject TupleDesc_create(TupleDesc tDesc); -extern jobject TupleDesc_internalCreate(TupleDesc tDesc); +extern jobject pljava_TupleDesc_create(TupleDesc tDesc); +extern jobject pljava_TupleDesc_internalCreate(TupleDesc tDesc); +extern void pljava_TupleDesc_initialize(void); #ifdef __cplusplus } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index c1bfc98c..84ae65a8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -643,4 +643,99 @@ protected long getMemoryContext() throws SQLException private native void _memContextDelete(long pointer); } + + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code FreeTupleDesc} of a single pointer. + */ + public static abstract class SingleFreeTupleDesc extends DualState + { + private volatile long m_pointer; + + protected SingleFreeTupleDesc( + Key cookie, T referent, long resourceOwner, long ftdTarget) + { + super(cookie, referent, resourceOwner); + m_pointer = ftdTarget; + } + + @Override + public String toString(Object o) + { + return String.format("%s FreeTupleDesc(%x)", super.toString(o), + m_pointer); + } + + /** + * For this class, the native state is valid whenever the wrapped + * pointer is not null. + */ + @Override + protected boolean nativeStateIsValid() + { + return 0 != m_pointer; + } + + /** + * When the native state is released, the wrapped pointer is nulled + * to indicate the state is no longer valid; no + * {@code FreeTupleDesc} call is + * made, on the assumption that the resource owner's release will be + * followed by wholesale release of the containing memory context + * anyway. + */ + @Override + protected void nativeStateReleased() + { + m_pointer = 0; + } + + /** + * When the Java state is released, the wrapped pointer is nulled to + * indicate the state is no longer valid, and a + * {@code FreeTupleDesc} + * call is made so the native memory is released without having to wait + * for release of its containing context. + *

    + * This overrides the inherited default, which would have removed this + * instance from the live instances collection. Users of this class + * should not call this method directly, but simply call + * {@link #enqueue enqueue}, and let the reclamation happen when the + * queue is processed. + */ + @Override + protected void javaStateReleased() + { + synchronized(Backend.THREADLOCK) + { + long p = m_pointer; + m_pointer = 0; + if ( 0 != p ) + _freeTupleDesc(p); + } + } + + /** + * This override simply calls + * {@link #javaStateReleased javaStateReleased}, so there is no + * difference in the effect of the Java object being explicitly + * released, or found unreachable by the garbage collector. + */ + @Override + protected void javaStateUnreachable() + { + javaStateReleased(); + } + + /** + * Allows a subclass to obtain the wrapped pointer value. + */ + protected long getPointer() throws SQLException + { + assertNativeStateIsValid(); + return m_pointer; + } + + private native void _freeTupleDesc(long pointer); + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java b/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java index 47c962b7..1fbe84a3 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -14,17 +20,54 @@ * * @author Thomas Hallgren */ -public class TupleDesc extends JavaWrapper +public class TupleDesc { + private final State m_state; private final int m_size; private Class[] m_columnClasses; - TupleDesc(long pointer, int size) throws SQLException + TupleDesc(DualState.Key cookie, long resourceOwner, long pointer, int size) + throws SQLException { - super(pointer); + m_state = new State(cookie, this, resourceOwner, pointer); m_size = size; } + private static class State + extends DualState.SingleFreeTupleDesc + { + private State( + DualState.Key cookie, TupleDesc td, long ro, long hth) + { + super(cookie, td, ro, hth); + } + + /** + * Return the TupleDesc pointer. + *

    + * As long as this value is used in instance methods on TupleDesc + * (or subclasses, or on something that holds a reference to this + * TupleDesc) and only while they hold Backend.THREADLOCK, it isn't + * necessary to also hold the monitor on this State object. The state + * can't go java-unreachable while an instance method's on the stack, + * and as long as we're on the thread that's in PG, the Invocation that + * state is scoped to can't be popped before we return. + */ + private long getTupleDescPtr() throws SQLException + { + return getPointer(); + } + } + + /** + * Return pointer to native TupleDesc structure as a long; use only while + * a reference to this class is live and the THREADLOCK is held. + */ + public final long getNativePointer() throws SQLException + { + return m_state.getTupleDescPtr(); + } + /** * Returns the name of the column at index. * @param index The one based index of the column. @@ -113,12 +156,6 @@ public Oid getOid(int index) } } - /** - * Calls the backend function FreeTupleDesc(TupleDesc desc) - * @param pointer The native pointer to the source TupleDesc - */ - protected native void _free(long pointer); - private static native String _getColumnName(long _this, int index) throws SQLException; private static native int _getColumnIndex(long _this, String colName) throws SQLException; private static native Tuple _formTuple(long _this, Object[] values) throws SQLException; From d69dbeb7c0090e5f9dd7e2681aa841a7ebc1d49c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 18:43:13 -0500 Subject: [PATCH 0254/1087] JavaWrapper concealed additional magic. Any TypeClass created with JavaWrapperClass_alloc automagically has a _coerceObject method that returns its wrapped pointer. Switching TupleDesc to use plain TypeClass_alloc, which leaves that method unimplemented; will ereport "Pure virtual method called" if a TupleDesc object is ever a PL/Java function return, or passed to a writable ResultSet or PreparedStatement. That doesn't seem to happen. --- pljava-so/src/main/c/type/TupleDesc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index b80375c4..303a2914 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -127,7 +127,7 @@ void pljava_TupleDesc_initialize(void) s_TupleDesc_init = PgObject_getJavaMethod(s_TupleDesc_class, "", "(Lorg/postgresql/pljava/internal/DualState$Key;JJI)V"); - cls = JavaWrapperClass_alloc("type.TupleDesc"); + cls = TypeClass_alloc("type.TupleDesc"); cls->JNISignature = "Lorg/postgresql/pljava/internal/TupleDesc;"; cls->javaTypeName = "org.postgresql.pljava.internal.TupleDesc"; cls->coerceDatum = _TupleDesc_coerceDatum; From 7f8ebd6eb28203f0ee4e7cb30f00d0cdd57345bf Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 19:55:50 -0500 Subject: [PATCH 0255/1087] DualState-ify Tuple. Still has a public getNativePointer() because Relation, TriggerData, SQLOutputToTuple, and SingleRowWriter refer to it. In passing, it appears that TupleTable is implemented by constructing a Java array populated with individual Java Tuple instances, one for every tuple in the table. There's got to be a lazier way.... --- pljava-so/src/main/c/DualState.c | 40 ++++++++ pljava-so/src/main/c/type/Relation.c | 2 +- pljava-so/src/main/c/type/TriggerData.c | 4 +- pljava-so/src/main/c/type/Tuple.c | 57 +++++------ pljava-so/src/main/c/type/TupleDesc.c | 2 +- pljava-so/src/main/c/type/TupleTable.c | 4 +- pljava-so/src/main/c/type/Type.c | 2 - .../src/main/include/pljava/type/Tuple.h | 13 +-- .../postgresql/pljava/internal/DualState.java | 95 +++++++++++++++++++ .../org/postgresql/pljava/internal/Tuple.java | 51 ++++++++-- 10 files changed, 213 insertions(+), 57 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index f366ca67..c1376038 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -14,6 +14,7 @@ #include "org_postgresql_pljava_internal_DualState_SinglePfree.h" #include "org_postgresql_pljava_internal_DualState_SingleMemContextDelete.h" #include "org_postgresql_pljava_internal_DualState_SingleFreeTupleDesc.h" +#include "org_postgresql_pljava_internal_DualState_SingleHeapFreeTuple.h" #include "pljava/DualState.h" #include "pljava/PgObject.h" @@ -23,10 +24,15 @@ * Includes for objects dependent on DualState, so they can be initialized here */ #include "pljava/type/SingleRowReader.h" +#include "pljava/type/Tuple.h" #include "pljava/type/TupleDesc.h" #include "pljava/SQLInputFromTuple.h" #include "pljava/VarlenaWrapper.h" +#if PG_VERSION_NUM < 80400 +#include /* heap_freetuple was there then */ +#endif + static jclass s_DualState_class; static jmethodID s_DualState_resourceOwnerRelease; @@ -122,6 +128,16 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod singleHeapFreeTupleMethods[] = + { + { + "_heapFreeTuple", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SingleHeapFreeTuple__1heapFreeTuple + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( @@ -150,6 +166,11 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleFreeTupleDescMethods); JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SingleHeapFreeTuple"); + PgObject_registerNatives2(clazz, singleHeapFreeTupleMethods); + JNI_deleteLocalRef(clazz); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); /* @@ -158,6 +179,7 @@ void pljava_DualState_initialize(void) pljava_SingleRowReader_initialize(); pljava_SQLInputFromTuple_initialize(); pljava_TupleDesc_initialize(); + pljava_Tuple_initialize(); pljava_VarlenaWrapper_initialize(); } @@ -234,3 +256,21 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleFreeTupleDesc__1freeTup FreeTupleDesc(p2l.ptrVal); END_NATIVE } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_SingleHeapFreeTuple + * Method: _heapFreeTuple + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SingleHeapFreeTuple__1heapFreeTuple( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + heap_freetuple(p2l.ptrVal); + END_NATIVE +} diff --git a/pljava-so/src/main/c/type/Relation.c b/pljava-so/src/main/c/type/Relation.c index 0e723e96..5330c54e 100644 --- a/pljava-so/src/main/c/type/Relation.c +++ b/pljava-so/src/main/c/type/Relation.c @@ -275,7 +275,7 @@ Java_org_postgresql_pljava_internal_Relation__1modifyTuple(JNIEnv* env, jclass c } PG_END_TRY(); if(tuple != 0) - result = Tuple_create(tuple); + result = pljava_Tuple_create(tuple); END_NATIVE } return result; diff --git a/pljava-so/src/main/c/type/TriggerData.c b/pljava-so/src/main/c/type/TriggerData.c index 1d65bc3f..8964a05f 100644 --- a/pljava-so/src/main/c/type/TriggerData.c +++ b/pljava-so/src/main/c/type/TriggerData.c @@ -182,7 +182,7 @@ Java_org_postgresql_pljava_internal_TriggerData__1getTriggerTuple(JNIEnv* env, j if(self != 0) { BEGIN_NATIVE - result = Tuple_create(self->tg_trigtuple); + result = pljava_Tuple_create(self->tg_trigtuple); END_NATIVE } return result; @@ -201,7 +201,7 @@ Java_org_postgresql_pljava_internal_TriggerData__1getNewTuple(JNIEnv* env, jclas if(self != 0) { BEGIN_NATIVE - result = Tuple_create(self->tg_newtuple); + result = pljava_Tuple_create(self->tg_newtuple); END_NATIVE } return result; diff --git a/pljava-so/src/main/c/type/Tuple.c b/pljava-so/src/main/c/type/Tuple.c index b0c89583..1f92141f 100644 --- a/pljava-so/src/main/c/type/Tuple.c +++ b/pljava-so/src/main/c/type/Tuple.c @@ -18,6 +18,7 @@ #include "org_postgresql_pljava_internal_Tuple.h" #include "pljava/Backend.h" +#include "pljava/DualState.h" #include "pljava/Exception.h" #include "pljava/type/Type_priv.h" #include "pljava/type/Tuple.h" @@ -29,31 +30,31 @@ static jmethodID s_Tuple_init; /* * org.postgresql.pljava.type.Tuple type. */ -jobject Tuple_create(HeapTuple ht) +jobject pljava_Tuple_create(HeapTuple ht) { jobject jht = 0; if(ht != 0) { MemoryContext curr = MemoryContextSwitchTo(JavaMemoryContext); - jht = Tuple_internalCreate(ht, true); + jht = pljava_Tuple_internalCreate(ht, true); MemoryContextSwitchTo(curr); } return jht; } -jobjectArray Tuple_createArray(HeapTuple* vals, jint size, bool mustCopy) +jobjectArray pljava_Tuple_createArray(HeapTuple* vals, jint size, bool mustCopy) { jobjectArray tuples = JNI_newObjectArray(size, s_Tuple_class, 0); while(--size >= 0) { - jobject heapTuple = Tuple_internalCreate(vals[size], mustCopy); + jobject heapTuple = pljava_Tuple_internalCreate(vals[size], mustCopy); JNI_setObjectArrayElement(tuples, size, heapTuple); JNI_deleteLocalRef(heapTuple); } return tuples; } -jobject Tuple_internalCreate(HeapTuple ht, bool mustCopy) +jobject pljava_Tuple_internalCreate(HeapTuple ht, bool mustCopy) { jobject jht; Ptr2Long htH; @@ -63,21 +64,29 @@ jobject Tuple_internalCreate(HeapTuple ht, bool mustCopy) htH.longVal = 0L; /* ensure that the rest is zeroed out */ htH.ptrVal = ht; - jht = JNI_newObject(s_Tuple_class, s_Tuple_init, htH.longVal); + /* + * Passing NULL as the ResourceOwner means this will never be matched by a + * nativeRelease call; that's appropriate (for now) as the Tuple copy is + * being made into JavaMemoryContext, which never gets reset, so only + * unreachability from the Java side will free it. + * XXX? this seems like a lot of tuple copying. + */ + jht = JNI_newObject(s_Tuple_class, s_Tuple_init, + pljava_DualState_key(), NULL, htH.longVal); return jht; } static jvalue _Tuple_coerceDatum(Type self, Datum arg) { jvalue result; - result.l = Tuple_create((HeapTuple)DatumGetPointer(arg)); + result.l = pljava_Tuple_create((HeapTuple)DatumGetPointer(arg)); return result; } /* Make this datatype available to the postgres system. */ -extern void Tuple_initialize(void); -void Tuple_initialize(void) +extern void pljava_Tuple_initialize(void); +void pljava_Tuple_initialize(void) { TypeClass cls; JNINativeMethod methods[] = { @@ -86,18 +95,14 @@ void Tuple_initialize(void) "(JJILjava/lang/Class;)Ljava/lang/Object;", Java_org_postgresql_pljava_internal_Tuple__1getObject }, - { - "_free", - "(J)V", - Java_org_postgresql_pljava_internal_Tuple__1free - }, { 0, 0, 0 }}; s_Tuple_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/Tuple")); PgObject_registerNatives2(s_Tuple_class, methods); - s_Tuple_init = PgObject_getJavaMethod(s_Tuple_class, "", "(J)V"); + s_Tuple_init = PgObject_getJavaMethod(s_Tuple_class, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); - cls = JavaWrapperClass_alloc("type.Tuple"); + cls = TypeClass_alloc("type.Tuple"); cls->JNISignature = "Lorg/postgresql/pljava/internal/Tuple;"; cls->javaTypeName = "org.postgresql.pljava.internal.Tuple"; cls->coerceDatum = _Tuple_coerceDatum; @@ -105,7 +110,8 @@ void Tuple_initialize(void) } jobject -Tuple_getObject(TupleDesc tupleDesc, HeapTuple tuple, int index, jclass rqcls) +pljava_Tuple_getObject( + TupleDesc tupleDesc, HeapTuple tuple, int index, jclass rqcls) { jobject result = 0; PG_TRY(); @@ -146,22 +152,7 @@ Java_org_postgresql_pljava_internal_Tuple__1getObject(JNIEnv* env, jclass cls, j BEGIN_NATIVE HeapTuple self = (HeapTuple)p2l.ptrVal; p2l.longVal = _tupleDesc; - result = Tuple_getObject((TupleDesc)p2l.ptrVal, self, (int)index, rqcls); + result = pljava_Tuple_getObject((TupleDesc)p2l.ptrVal, self, (int)index, rqcls); END_NATIVE return result; } - -/* - * Class: org_postgresql_pljava_internal_Tuple - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_Tuple__1free(JNIEnv* env, jobject _this, jlong pointer) -{ - BEGIN_NATIVE_NO_ERRCHECK - Ptr2Long p2l; - p2l.longVal = pointer; - heap_freetuple(p2l.ptrVal); - END_NATIVE -} diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index 303a2914..1f46c5e5 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -251,7 +251,7 @@ Java_org_postgresql_pljava_internal_TupleDesc__1formTuple(JNIEnv* env, jclass cl curr = MemoryContextSwitchTo(JavaMemoryContext); tuple = heap_form_tuple(self, values, nulls); - result = Tuple_internalCreate(tuple, false); + result = pljava_Tuple_internalCreate(tuple, false); MemoryContextSwitchTo(curr); pfree(values); pfree(nulls); diff --git a/pljava-so/src/main/c/type/TupleTable.c b/pljava-so/src/main/c/type/TupleTable.c index 5808721c..bf2035a7 100644 --- a/pljava-so/src/main/c/type/TupleTable.c +++ b/pljava-so/src/main/c/type/TupleTable.c @@ -36,7 +36,7 @@ jobject TupleTable_createFromSlot(TupleTableSlot* tts) tupdesc = pljava_TupleDesc_internalCreate(tts->tts_tupleDescriptor); tuple = ExecCopySlotTuple(tts); - tuples = Tuple_createArray(&tuple, 1, false); + tuples = pljava_Tuple_createArray(&tuple, 1, false); MemoryContextSwitchTo(curr); @@ -64,7 +64,7 @@ jobject TupleTable_create(SPITupleTable* tts, jobject knownTD) if(knownTD == 0) knownTD = pljava_TupleDesc_internalCreate(tts->tupdesc); - tuples = Tuple_createArray(tts->vals, (jint)tupcount, true); + tuples = pljava_Tuple_createArray(tts->vals, (jint)tupcount, true); MemoryContextSwitchTo(curr); return JNI_newObject(s_TupleTable_class, s_TupleTable_init, knownTD, tuples); diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 11f5a823..1b8c4e61 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -810,7 +810,6 @@ extern void ExecutionPlan_initialize(void); extern void Portal_initialize(void); extern void Relation_initialize(void); extern void TriggerData_initialize(void); -extern void Tuple_initialize(void); extern void TupleTable_initialize(void); extern void Composite_initialize(void); @@ -854,7 +853,6 @@ void Type_initialize(void) Portal_initialize(); TriggerData_initialize(); Relation_initialize(); - Tuple_initialize(); TupleTable_initialize(); Composite_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/Tuple.h b/pljava-so/src/main/include/pljava/type/Tuple.h index 8409a8bc..97a3a108 100644 --- a/pljava-so/src/main/include/pljava/type/Tuple.h +++ b/pljava-so/src/main/include/pljava/type/Tuple.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -15,7 +15,6 @@ #ifndef __pljava_Tuple_h #define __pljava_Tuple_h -#include "pljava/type/JavaWrapper.h" #ifdef __cplusplus extern "C" { #endif @@ -32,16 +31,18 @@ extern "C" { /* * Create the org.postgresql.pljava.Tuple instance */ -extern jobject Tuple_create(HeapTuple tuple); -extern jobject Tuple_internalCreate(HeapTuple tuple, bool mustCopy); -extern jobjectArray Tuple_createArray(HeapTuple* tuples, jint size, bool mustCopy); +extern jobject pljava_Tuple_create(HeapTuple tuple); +extern jobject pljava_Tuple_internalCreate(HeapTuple tuple, bool mustCopy); +extern jobjectArray pljava_Tuple_createArray( + HeapTuple* tuples, jint size, bool mustCopy); /* * Return a java object at given index from a HeapTuple (with a best effort to * produce an object of class rqcls if it is not null). */ -extern jobject Tuple_getObject( +extern jobject pljava_Tuple_getObject( TupleDesc tupleDesc, HeapTuple tuple, int index, jclass rqcls); +extern void pljava_Tuple_initialize(void); #ifdef __cplusplus } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 84ae65a8..e5767201 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -738,4 +738,99 @@ protected long getPointer() throws SQLException private native void _freeTupleDesc(long pointer); } + + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code heap_freetuple} of a single pointer. + */ + public static abstract class SingleHeapFreeTuple extends DualState + { + private volatile long m_pointer; + + protected SingleHeapFreeTuple( + Key cookie, T referent, long resourceOwner, long hftTarget) + { + super(cookie, referent, resourceOwner); + m_pointer = hftTarget; + } + + @Override + public String toString(Object o) + { + return String.format("%s heap_freetuple(%x)", super.toString(o), + m_pointer); + } + + /** + * For this class, the native state is valid whenever the wrapped + * pointer is not null. + */ + @Override + protected boolean nativeStateIsValid() + { + return 0 != m_pointer; + } + + /** + * When the native state is released, the wrapped pointer is nulled + * to indicate the state is no longer valid; no + * {@code heap_freetuple} call is + * made, on the assumption that the resource owner's release will be + * followed by wholesale release of the containing memory context + * anyway. + */ + @Override + protected void nativeStateReleased() + { + m_pointer = 0; + } + + /** + * When the Java state is released, the wrapped pointer is nulled to + * indicate the state is no longer valid, and a + * {@code heap_freetuple} + * call is made so the native memory is released without having to wait + * for release of its containing context. + *

    + * This overrides the inherited default, which would have removed this + * instance from the live instances collection. Users of this class + * should not call this method directly, but simply call + * {@link #enqueue enqueue}, and let the reclamation happen when the + * queue is processed. + */ + @Override + protected void javaStateReleased() + { + synchronized(Backend.THREADLOCK) + { + long p = m_pointer; + m_pointer = 0; + if ( 0 != p ) + _heapFreeTuple(p); + } + } + + /** + * This override simply calls + * {@link #javaStateReleased javaStateReleased}, so there is no + * difference in the effect of the Java object being explicitly + * released, or found unreachable by the garbage collector. + */ + @Override + protected void javaStateUnreachable() + { + javaStateReleased(); + } + + /** + * Allows a subclass to obtain the wrapped pointer value. + */ + protected long getPointer() throws SQLException + { + assertNativeStateIsValid(); + return m_pointer; + } + + private native void _heapFreeTuple(long pointer); + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java index d6b5222c..ff97905f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -20,11 +20,48 @@ * * @author Thomas Hallgren */ -public class Tuple extends JavaWrapper +public class Tuple { - Tuple(long pointer) + private final State m_state; + + Tuple(DualState.Key cookie, long resourceOwner, long pointer) { - super(pointer); + m_state = new State(cookie, this, resourceOwner, pointer); + } + + private static class State + extends DualState.SingleHeapFreeTuple + { + private State( + DualState.Key cookie, Tuple t, long ro, long ht) + { + super(cookie, t, ro, ht); + } + + /** + * Return the HeapTuple pointer. + *

    + * As long as this value is used in instance methods on TupleDesc + * (or subclasses, or on something that holds a reference to this + * TupleDesc) and only while they hold Backend.THREADLOCK, it isn't + * necessary to also hold the monitor on this State object. The state + * can't go java-unreachable while an instance method's on the stack, + * and as long as we're on the thread that's in PG, the Invocation that + * state is scoped to can't be popped before we return. + */ + private long getHeapTuplePtr() throws SQLException + { + return getPointer(); + } + } + + /** + * Return pointer to native HeapTuple structure as a long; use only while + * a reference to this class is live and the THREADLOCK is held. + */ + public final long getNativePointer() throws SQLException + { + return m_state.getHeapTuplePtr(); } /** @@ -51,12 +88,6 @@ public Object getObject(TupleDesc tupleDesc, int index, Class type) } } - /** - * Calls the backend function heap_freetuple(HeapTuple tuple) - * @param pointer The native pointer to the source HeapTuple - */ - protected native void _free(long pointer); - private static native Object _getObject( long pointer, long tupleDescPointer, int index, Class type) throws SQLException; From d60d2bc1aa30e4b1522ca00be61fa003655b40a0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 20:50:30 -0500 Subject: [PATCH 0256/1087] DualState-ify Relation. Relation was handled with Invocation_createLocalWrapper, that is, as something with a lifetime scoped to the current PL/Java function invocation. That's been duplicated, using DualState with currentInvocation passed as the resource owner. But it could be worth investigating how and where Relation objects are actually getting constructed, to see if that's the right scope for all cases. --- pljava-so/src/main/c/DualState.c | 2 + pljava-so/src/main/c/type/Relation.c | 71 +++++++++++-------- pljava-so/src/main/c/type/TriggerData.c | 2 +- pljava-so/src/main/c/type/Type.c | 2 - .../src/main/include/pljava/type/Relation.h | 19 +++-- .../postgresql/pljava/internal/Relation.java | 42 ++++++++--- 6 files changed, 89 insertions(+), 49 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index c1376038..6ef55091 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -23,6 +23,7 @@ /* * Includes for objects dependent on DualState, so they can be initialized here */ +#include "pljava/type/Relation.h" #include "pljava/type/SingleRowReader.h" #include "pljava/type/Tuple.h" #include "pljava/type/TupleDesc.h" @@ -176,6 +177,7 @@ void pljava_DualState_initialize(void) /* * Call initialize() methods of known classes built upon DualState. */ + pljava_Relation_initialize(); pljava_SingleRowReader_initialize(); pljava_SQLInputFromTuple_initialize(); pljava_TupleDesc_initialize(); diff --git a/pljava-so/src/main/c/type/Relation.c b/pljava-so/src/main/c/type/Relation.c index 5330c54e..0951acde 100644 --- a/pljava-so/src/main/c/type/Relation.c +++ b/pljava-so/src/main/c/type/Relation.c @@ -16,6 +16,7 @@ #include #include "org_postgresql_pljava_internal_Relation.h" +#include "pljava/DualState.h" #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/SPI.h" @@ -31,24 +32,28 @@ static jmethodID s_Relation_init; /* * org.postgresql.pljava.Relation type. */ -jobject Relation_create(Relation td) +jobject pljava_Relation_create(Relation r) { - return (td == 0) ? 0 : JNI_newObject( + Ptr2Long p2lr; + + if ( NULL == r ) + return NULL; + + p2lr.longVal = 0L; + p2lr.ptrVal = r; + + return JNI_newObject( s_Relation_class, s_Relation_init, - Invocation_createLocalWrapper(td)); + pljava_DualState_key(), + currentInvocation, + p2lr.longVal); } -extern void Relation_initialize(void); -void Relation_initialize(void) +void pljava_Relation_initialize(void) { JNINativeMethod methods[] = { - { - "_free", - "(J)V", - Java_org_postgresql_pljava_internal_Relation__1free - }, { "_getName", "(J)Ljava/lang/String;", @@ -74,25 +79,13 @@ void Relation_initialize(void) s_Relation_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/Relation")); PgObject_registerNatives2(s_Relation_class, methods); - s_Relation_init = PgObject_getJavaMethod(s_Relation_class, "", "(J)V"); + s_Relation_init = PgObject_getJavaMethod(s_Relation_class, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); } /**************************************** * JNI methods ****************************************/ -/* - * Class: org_postgresql_pljava_internal_Relation - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_Relation__1free(JNIEnv* env, jobject _this, jlong pointer) -{ - BEGIN_NATIVE_NO_ERRCHECK - Invocation_freeLocalWrapper(pointer); - END_NATIVE -} - /* * Class: org_postgresql_pljava_internal_Relation * Method: _getName @@ -102,7 +95,11 @@ JNIEXPORT jstring JNICALL Java_org_postgresql_pljava_internal_Relation__1getName(JNIEnv* env, jclass clazz, jlong _this) { jstring result = 0; - Relation self = Invocation_getWrappedPointer(_this); + Relation self; + Ptr2Long p2l; + p2l.longVal = _this; + self = (Relation)p2l.ptrVal; + if(self != 0) { BEGIN_NATIVE @@ -131,7 +128,11 @@ JNIEXPORT jstring JNICALL Java_org_postgresql_pljava_internal_Relation__1getSchema(JNIEnv* env, jclass clazz, jlong _this) { jstring result = 0; - Relation self = Invocation_getWrappedPointer(_this); + Relation self; + Ptr2Long p2l; + p2l.longVal = _this; + self = (Relation)p2l.ptrVal; + if(self != 0) { BEGIN_NATIVE @@ -160,7 +161,11 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_Relation__1getTupleDesc(JNIEnv* env, jclass clazz, jlong _this) { jobject result = 0; - Relation self = Invocation_getWrappedPointer(_this); + Relation self; + Ptr2Long p2l; + p2l.longVal = _this; + self = (Relation)p2l.ptrVal; + if(self != 0) { BEGIN_NATIVE @@ -188,15 +193,19 @@ Java_org_postgresql_pljava_internal_Relation__1getTupleDesc(JNIEnv* env, jclass JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_Relation__1modifyTuple(JNIEnv* env, jclass clazz, jlong _this, jlong _tuple, jintArray _indexes, jobjectArray _values) { - Relation self = Invocation_getWrappedPointer(_this); jobject result = 0; + Relation self; + Ptr2Long p2lr; + p2lr.longVal = _this; + self = (Relation)p2lr.ptrVal; + if(self != 0 && _tuple != 0) { - Ptr2Long p2l; - p2l.longVal = _tuple; + Ptr2Long p2lt; + p2lt.longVal = _tuple; BEGIN_NATIVE - HeapTuple tuple = (HeapTuple)p2l.ptrVal; + HeapTuple tuple = (HeapTuple)p2lt.ptrVal; PG_TRY(); { jint idx; diff --git a/pljava-so/src/main/c/type/TriggerData.c b/pljava-so/src/main/c/type/TriggerData.c index 8964a05f..6b340e2c 100644 --- a/pljava-so/src/main/c/type/TriggerData.c +++ b/pljava-so/src/main/c/type/TriggerData.c @@ -163,7 +163,7 @@ Java_org_postgresql_pljava_internal_TriggerData__1getRelation(JNIEnv* env, jclas if(self != 0) { BEGIN_NATIVE - result = Relation_create(self->tg_relation); + result = pljava_Relation_create(self->tg_relation); END_NATIVE } return result; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 1b8c4e61..cb556af4 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -808,7 +808,6 @@ extern void byte_array_initialize(void); extern void JavaWrapper_initialize(void); extern void ExecutionPlan_initialize(void); extern void Portal_initialize(void); -extern void Relation_initialize(void); extern void TriggerData_initialize(void); extern void TupleTable_initialize(void); @@ -852,7 +851,6 @@ void Type_initialize(void) ExecutionPlan_initialize(); Portal_initialize(); TriggerData_initialize(); - Relation_initialize(); TupleTable_initialize(); Composite_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/Relation.h b/pljava-so/src/main/include/pljava/type/Relation.h index e3d81d76..5fac1a4a 100644 --- a/pljava-so/src/main/include/pljava/type/Relation.h +++ b/pljava-so/src/main/include/pljava/type/Relation.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -17,7 +23,7 @@ extern "C" { #include /******************************************************************* - * The Relation java class extends the NativeStruct and provides JNI + * The Relation java class provides JNI * access to some of the attributes of the Relation structure. * * @author Thomas Hallgren @@ -26,7 +32,8 @@ extern "C" { /* * Create an instance of org.postgresql.pljava.Relation */ -extern jobject Relation_create(Relation rel); +extern jobject pljava_Relation_create(Relation rel); +extern void pljava_Relation_initialize(void); #ifdef __cplusplus } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java index 397eb6e7..04094eff 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java @@ -14,13 +14,39 @@ * * @author Thomas Hallgren */ -public class Relation extends JavaWrapper +public class Relation { private TupleDesc m_tupleDesc; + private final State m_state; - Relation(long pointer) + Relation(DualState.Key cookie, long resourceOwner, long pointer) { - super(pointer); + m_state = new State(cookie, this, resourceOwner, pointer); + } + + private static class State + extends DualState.SingleGuardedLong + { + private State( + DualState.Key cookie, Relation r, long ro, long hth) + { + super(cookie, r, ro, hth); + } + + /** + * Return the Relation pointer. + *

    + * As long as this value is used in instance methods on Relation + * (or subclasses) and only while they hold Backend.THREADLOCK, it isn't + * necessary to also hold the monitor on this State object. The state + * can't go java-unreachable while an instance method's on the stack, + * and as long as we're on the thread that's in PG, the Invocation that + * state is scoped to can't be popped before we return. + */ + private long getRelationPtr() throws SQLException + { + return getValue(); + } } /** @@ -32,7 +58,7 @@ public String getName() { synchronized(Backend.THREADLOCK) { - return _getName(this.getNativePointer()); + return _getName(m_state.getRelationPtr()); } } @@ -45,7 +71,7 @@ public String getSchema() { synchronized(Backend.THREADLOCK) { - return _getSchema(this.getNativePointer()); + return _getSchema(m_state.getRelationPtr()); } } @@ -60,7 +86,7 @@ public TupleDesc getTupleDesc() { synchronized(Backend.THREADLOCK) { - m_tupleDesc = _getTupleDesc(this.getNativePointer()); + m_tupleDesc = _getTupleDesc(m_state.getRelationPtr()); } } return m_tupleDesc; @@ -90,12 +116,10 @@ public Tuple modifyTuple(Tuple original, int[] fieldNumbers, Object[] values) { synchronized(Backend.THREADLOCK) { - return _modifyTuple(this.getNativePointer(), original.getNativePointer(), fieldNumbers, values); + return _modifyTuple(m_state.getRelationPtr(), original.getNativePointer(), fieldNumbers, values); } } - protected native void _free(long pointer); - private static native String _getName(long pointer) throws SQLException; From 7dfb064ec9111a25044d340faa547e7e93c94b1a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 21:41:30 -0500 Subject: [PATCH 0257/1087] DualState-ify TriggerData. --- pljava-so/src/main/c/DualState.c | 2 + pljava-so/src/main/c/Function.c | 5 +- pljava-so/src/main/c/type/TriggerData.c | 118 +++++++++++------- pljava-so/src/main/c/type/Type.c | 2 - .../main/include/pljava/type/TriggerData.h | 22 ++-- .../pljava/internal/TriggerData.java | 52 ++++++-- 6 files changed, 136 insertions(+), 65 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 6ef55091..1464fd97 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -25,6 +25,7 @@ */ #include "pljava/type/Relation.h" #include "pljava/type/SingleRowReader.h" +#include "pljava/type/TriggerData.h" #include "pljava/type/Tuple.h" #include "pljava/type/TupleDesc.h" #include "pljava/SQLInputFromTuple.h" @@ -180,6 +181,7 @@ void pljava_DualState_initialize(void) pljava_Relation_initialize(); pljava_SingleRowReader_initialize(); pljava_SQLInputFromTuple_initialize(); + pljava_TriggerData_initialize(); pljava_TupleDesc_initialize(); pljava_Tuple_initialize(); pljava_VarlenaWrapper_initialize(); diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index f9671d8c..e8063d16 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -853,7 +853,7 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) Datum ret; TriggerData *td = (TriggerData*)fcinfo->context; - arg.l = TriggerData_create(td); + arg.l = pljava_TriggerData_create(td); if(arg.l == 0) return 0; @@ -888,7 +888,8 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) #endif MemoryContext currCtx = Invocation_switchToUpperContext(); ret = PointerGetDatum( - TriggerData_getTriggerReturnTuple(arg.l, &fcinfo->isnull)); + pljava_TriggerData_getTriggerReturnTuple( + arg.l, &fcinfo->isnull)); /* Triggers are not allowed to set the fcinfo->isnull, even when * they return null. diff --git a/pljava-so/src/main/c/type/TriggerData.c b/pljava-so/src/main/c/type/TriggerData.c index 6b340e2c..0d005764 100644 --- a/pljava-so/src/main/c/type/TriggerData.c +++ b/pljava-so/src/main/c/type/TriggerData.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -10,9 +16,9 @@ #include #include "org_postgresql_pljava_internal_TriggerData.h" #include "pljava/Invocation.h" +#include "pljava/DualState.h" #include "pljava/Exception.h" #include "pljava/type/Type_priv.h" -#include "pljava/type/JavaWrapper.h" #include "pljava/type/String.h" #include "pljava/type/TriggerData.h" #include "pljava/type/Tuple.h" @@ -23,21 +29,31 @@ static jclass s_TriggerData_class; static jmethodID s_TriggerData_init; static jmethodID s_TriggerData_getTriggerReturnTuple; -jobject TriggerData_create(TriggerData* triggerData) +jobject pljava_TriggerData_create(TriggerData* triggerData) { - return (triggerData == 0) ? 0 : JNI_newObject( + Ptr2Long p2ltd; + + if ( NULL == triggerData ) + return NULL; + + p2ltd.longVal = 0L; + p2ltd.ptrVal = triggerData; + + return JNI_newObject( s_TriggerData_class, s_TriggerData_init, - Invocation_createLocalWrapper(triggerData)); + pljava_DualState_key(), + currentInvocation, + p2ltd.longVal); } -HeapTuple TriggerData_getTriggerReturnTuple(jobject jtd, bool* wasNull) +HeapTuple pljava_TriggerData_getTriggerReturnTuple(jobject jtd, bool* wasNull) { Ptr2Long p2l; HeapTuple ret = 0; p2l.longVal = JNI_callLongMethod(jtd, s_TriggerData_getTriggerReturnTuple); if(p2l.longVal != 0) - ret = heap_copytuple((HeapTuple)p2l.ptrVal); + ret = heap_copytuple((HeapTuple)p2l.ptrVal); /* unconditional copy?? */ else *wasNull = true; return ret; @@ -45,17 +61,12 @@ HeapTuple TriggerData_getTriggerReturnTuple(jobject jtd, bool* wasNull) /* Make this datatype available to the postgres system. */ -extern void TriggerData_initialize(void); -void TriggerData_initialize(void) +void pljava_TriggerData_initialize(void) { TypeClass cls; + jclass jcls; JNINativeMethod methods[] = { - { - "_free", - "(J)V", - Java_org_postgresql_pljava_internal_TriggerData__1free - }, { "_getRelation", "(J)Lorg/postgresql/pljava/internal/Relation;", @@ -119,11 +130,15 @@ void TriggerData_initialize(void) { 0, 0, 0 } }; - s_TriggerData_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/TriggerData")); - PgObject_registerNatives2(s_TriggerData_class, methods); + jcls = PgObject_getJavaClass("org/postgresql/pljava/internal/TriggerData"); + PgObject_registerNatives2(jcls, methods); - s_TriggerData_init = PgObject_getJavaMethod(s_TriggerData_class, "", "(J)V"); - s_TriggerData_getTriggerReturnTuple = PgObject_getJavaMethod(s_TriggerData_class, "getTriggerReturnTuple", "()J"); + s_TriggerData_init = PgObject_getJavaMethod(jcls, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); + s_TriggerData_getTriggerReturnTuple = PgObject_getJavaMethod( + jcls, "getTriggerReturnTuple", "()J"); + s_TriggerData_class = JNI_newGlobalRef(jcls); + JNI_deleteLocalRef(jcls); /* Use interface name for signatures. */ @@ -137,19 +152,6 @@ void TriggerData_initialize(void) * JNI methods ****************************************/ -/* - * Class: org_postgresql_pljava_internal_TriggerData - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_TriggerData__1free(JNIEnv* env, jobject _this, jlong pointer) -{ - BEGIN_NATIVE_NO_ERRCHECK - Invocation_freeLocalWrapper(pointer); - END_NATIVE -} - /* * Class: org_postgresql_pljava_TriggerData * Method: _getRelation @@ -159,7 +161,9 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getRelation(JNIEnv* env, jclass clazz, jlong _this) { jobject result = 0; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -178,7 +182,9 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getTriggerTuple(JNIEnv* env, jclass clazz, jlong _this) { jobject result = 0; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -197,7 +203,9 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getNewTuple(JNIEnv* env, jclass clazz, jlong _this) { jobject result = 0; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -216,7 +224,9 @@ JNIEXPORT jobjectArray JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getArguments(JNIEnv* env, jclass clazz, jlong _this) { jobjectArray result = 0; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) { char** cpp; @@ -247,7 +257,9 @@ JNIEXPORT jstring JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getName(JNIEnv* env, jclass clazz, jlong _this) { jstring result = 0; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -266,7 +278,9 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredAfter(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_AFTER(self->tg_event); return result; @@ -281,7 +295,9 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredBefore(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BEFORE(self->tg_event); return result; @@ -296,7 +312,9 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredForEachRow(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_FOR_ROW(self->tg_event); return result; @@ -311,7 +329,9 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredForStatement(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_FOR_STATEMENT(self->tg_event); return result; @@ -326,7 +346,9 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredByDelete(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BY_DELETE(self->tg_event); return result; @@ -341,7 +363,9 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredByInsert(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BY_INSERT(self->tg_event); return result; @@ -356,7 +380,9 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredByUpdate(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; - TriggerData* self = Invocation_getWrappedPointer(_this); + Ptr2Long p2l; + p2l.longVal = _this; + TriggerData* self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BY_UPDATE(self->tg_event); return result; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index cb556af4..0594df61 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -808,7 +808,6 @@ extern void byte_array_initialize(void); extern void JavaWrapper_initialize(void); extern void ExecutionPlan_initialize(void); extern void Portal_initialize(void); -extern void TriggerData_initialize(void); extern void TupleTable_initialize(void); extern void Composite_initialize(void); @@ -850,7 +849,6 @@ void Type_initialize(void) JavaWrapper_initialize(); ExecutionPlan_initialize(); Portal_initialize(); - TriggerData_initialize(); TupleTable_initialize(); Composite_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/TriggerData.h b/pljava-so/src/main/include/pljava/type/TriggerData.h index b1f8676b..df26d1c4 100644 --- a/pljava-so/src/main/include/pljava/type/TriggerData.h +++ b/pljava-so/src/main/include/pljava/type/TriggerData.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -18,7 +24,7 @@ extern "C" { #include /********************************************************************** - * The TriggerData java class extends the NativeStruct and provides JNI + * The TriggerData java class provides JNI * access to some of the attributes of the TriggerData structure. * * @author Thomas Hallgren @@ -27,7 +33,7 @@ extern "C" { /* * Create the org.postgresql.pljava.TriggerData object. */ -extern jobject TriggerData_create(TriggerData* triggerData); +extern jobject pljava_TriggerData_create(TriggerData* triggerData); /* * Obtains the returned Tuple after trigger has been processed. @@ -35,7 +41,9 @@ extern jobject TriggerData_create(TriggerData* triggerData); * is connected (and that a longer-lived memory context than SPI's is selected, * if the caller wants the result to survive SPI_finish). */ -extern HeapTuple TriggerData_getTriggerReturnTuple(jobject jtd, bool* wasNull); +extern HeapTuple pljava_TriggerData_getTriggerReturnTuple( + jobject jtd, bool* wasNull); +extern void pljava_TriggerData_initialize(void); #ifdef __cplusplus } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java index 5619df7f..29361932 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -17,7 +23,7 @@ * * @author Thomas Hallgren */ -public class TriggerData extends JavaWrapper implements org.postgresql.pljava.TriggerData +public class TriggerData implements org.postgresql.pljava.TriggerData { private Relation m_relation; private TriggerResultSet m_old = null; @@ -25,10 +31,41 @@ public class TriggerData extends JavaWrapper implements org.postgresql.pljava.Tr private Tuple m_newTuple; private Tuple m_triggerTuple; private boolean m_suppress = false; + private final State m_state; - TriggerData(long pointer) + TriggerData(DualState.Key cookie, long resourceOwner, long pointer) { - super(pointer); + m_state = new State(cookie, this, resourceOwner, pointer); + } + + private static class State + extends DualState.SingleGuardedLong + { + private State( + DualState.Key cookie, TriggerData td, long ro, long hth) + { + super(cookie, td, ro, hth); + } + + /** + * Return the TriggerData pointer. + *

    + * As long as this value is used in instance methods on TriggerData + * (or subclasses) and only while they hold Backend.THREADLOCK, it isn't + * necessary to also hold the monitor on this State object. The state + * can't go java-unreachable while an instance method's on the stack, + * and as long as we're on the thread that's in PG, the Invocation that + * state is scoped to can't be popped before we return. + */ + private long getTriggerDataPtr() throws SQLException + { + return getValue(); + } + } + + private long getNativePointer() throws SQLException + { + return m_state.getTriggerDataPtr(); } @Override @@ -362,7 +399,6 @@ public boolean isFiredByUpdate() } } - protected native void _free(long pointer); private static native Relation _getRelation(long pointer) throws SQLException; private static native Tuple _getTriggerTuple(long pointer) throws SQLException; private static native Tuple _getNewTuple(long pointer) throws SQLException; From 0e48fa23810923425be9281f33dbbe5ef35478a9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 22:28:47 -0500 Subject: [PATCH 0258/1087] DualState-ify ErrorData. --- pljava-so/src/main/c/DualState.c | 36 +++++++ pljava-so/src/main/c/Exception.c | 16 +++- pljava-so/src/main/c/JNICalls.c | 18 ++-- pljava-so/src/main/c/type/ErrorData.c | 34 +++++-- pljava-so/src/main/c/type/Type.c | 2 - .../src/main/include/pljava/type/ErrorData.h | 21 ++-- .../postgresql/pljava/internal/DualState.java | 95 +++++++++++++++++++ .../postgresql/pljava/internal/ErrorData.java | 67 +++++++++++-- 8 files changed, 251 insertions(+), 38 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 1464fd97..6fd56f8c 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -15,6 +15,7 @@ #include "org_postgresql_pljava_internal_DualState_SingleMemContextDelete.h" #include "org_postgresql_pljava_internal_DualState_SingleFreeTupleDesc.h" #include "org_postgresql_pljava_internal_DualState_SingleHeapFreeTuple.h" +#include "org_postgresql_pljava_internal_DualState_SingleFreeErrorData.h" #include "pljava/DualState.h" #include "pljava/PgObject.h" @@ -23,6 +24,7 @@ /* * Includes for objects dependent on DualState, so they can be initialized here */ +#include "pljava/type/ErrorData.h" #include "pljava/type/Relation.h" #include "pljava/type/SingleRowReader.h" #include "pljava/type/TriggerData.h" @@ -140,6 +142,16 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod singleFreeErrorDataMethods[] = + { + { + "_freeErrorData", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SingleFreeErrorData__1freeErrorData + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( @@ -173,11 +185,17 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleHeapFreeTupleMethods); JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SingleFreeErrorData"); + PgObject_registerNatives2(clazz, singleFreeErrorDataMethods); + JNI_deleteLocalRef(clazz); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); /* * Call initialize() methods of known classes built upon DualState. */ + pljava_ErrorData_initialize(); pljava_Relation_initialize(); pljava_SingleRowReader_initialize(); pljava_SQLInputFromTuple_initialize(); @@ -278,3 +296,21 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleHeapFreeTuple__1heapFre heap_freetuple(p2l.ptrVal); END_NATIVE } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_SingleFreeErrorData + * Method: _freeErrorData + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SingleFreeErrorData__1freeErrorData( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + FreeErrorData(p2l.ptrVal); + END_NATIVE +} diff --git a/pljava-so/src/main/c/Exception.c b/pljava-so/src/main/c/Exception.c index ea35eba7..23b3c0ad 100644 --- a/pljava-so/src/main/c/Exception.c +++ b/pljava-so/src/main/c/Exception.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -152,7 +158,7 @@ void Exception_throw_ERROR(const char* funcName) jobject ex; PG_TRY(); { - jobject ed = ErrorData_getCurrentError(); + jobject ed = pljava_ErrorData_getCurrentError(); FlushErrorState(); diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 2f536d69..3f4016b3 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -115,7 +121,7 @@ static void endCall(JNIEnv* env) */ jobject jed = (*env)->CallObjectMethod(env, exh, ServerException_getErrorData); if(jed != 0) - ReThrowError(ErrorData_getErrorData(jed)); + ReThrowError(pljava_ErrorData_getErrorData(jed)); } /* There's no return from this call. */ @@ -139,7 +145,7 @@ static void endCallMonitorHeld(JNIEnv* env) */ jobject jed = (*env)->CallObjectMethod(env, exh, ServerException_getErrorData); if(jed != 0) - ReThrowError(ErrorData_getErrorData(jed)); + ReThrowError(pljava_ErrorData_getErrorData(jed)); } /* There's no return from this call. */ diff --git a/pljava-so/src/main/c/type/ErrorData.c b/pljava-so/src/main/c/type/ErrorData.c index 4bc5186d..70ac26ce 100644 --- a/pljava-so/src/main/c/type/ErrorData.c +++ b/pljava-so/src/main/c/type/ErrorData.c @@ -1,12 +1,19 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ #include "org_postgresql_pljava_internal_ErrorData.h" +#include "pljava/DualState.h" #include "pljava/Exception.h" #include "pljava/type/Type_priv.h" #include "pljava/type/ErrorData.h" @@ -16,7 +23,7 @@ static jclass s_ErrorData_class; static jmethodID s_ErrorData_init; static jmethodID s_ErrorData_getNativePointer; -jobject ErrorData_getCurrentError(void) +jobject pljava_ErrorData_getCurrentError(void) { Ptr2Long p2l; jobject jed; @@ -27,11 +34,18 @@ jobject ErrorData_getCurrentError(void) p2l.longVal = 0L; /* ensure that the rest is zeroed out */ p2l.ptrVal = errorData; - jed = JNI_newObject(s_ErrorData_class, s_ErrorData_init, p2l.longVal); + /* + * Passing NULL as the ResourceOwner means this will never be matched by a + * nativeRelease call; that's appropriate (for now) as the ErrorData copy is + * being made into JavaMemoryContext, which never gets reset, so only + * unreachability from the Java side will free it. + */ + jed = JNI_newObject(s_ErrorData_class, s_ErrorData_init, + pljava_DualState_key(), NULL, p2l.longVal); return jed; } -ErrorData* ErrorData_getErrorData(jobject jed) +ErrorData* pljava_ErrorData_getErrorData(jobject jed) { Ptr2Long p2l; p2l.longVal = JNI_callLongMethod(jed, s_ErrorData_getNativePointer); @@ -40,8 +54,7 @@ ErrorData* ErrorData_getErrorData(jobject jed) /* Make this datatype available to the postgres system. */ -extern void ErrorData_initialize(void); -void ErrorData_initialize(void) +void pljava_ErrorData_initialize(void) { JNINativeMethod methods[] = { { @@ -134,7 +147,8 @@ void ErrorData_initialize(void) s_ErrorData_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/ErrorData")); PgObject_registerNatives2(s_ErrorData_class, methods); - s_ErrorData_init = PgObject_getJavaMethod(s_ErrorData_class, "", "(J)V"); + s_ErrorData_init = PgObject_getJavaMethod(s_ErrorData_class, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); s_ErrorData_getNativePointer = PgObject_getJavaMethod(s_ErrorData_class, "getNativePointer", "()J"); } diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 0594df61..8e3ee952 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -800,7 +800,6 @@ extern void Timestamp_initialize(void); extern void Oid_initialize(void); extern void AclId_initialize(void); -extern void ErrorData_initialize(void); extern void String_initialize(void); extern void byte_array_initialize(void); @@ -842,7 +841,6 @@ void Type_initialize(void) Oid_initialize(); AclId_initialize(); - ErrorData_initialize(); byte_array_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/ErrorData.h b/pljava-so/src/main/include/pljava/type/ErrorData.h index adc838bf..7f0a089b 100644 --- a/pljava-so/src/main/include/pljava/type/ErrorData.h +++ b/pljava-so/src/main/include/pljava/type/ErrorData.h @@ -1,15 +1,20 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ #ifndef __pljava_ErrorData_h #define __pljava_ErrorData_h -#include "pljava/type/JavaWrapper.h" #ifdef __cplusplus extern "C" { #endif @@ -24,13 +29,13 @@ extern "C" { * Create the org.postgresql.pljava.internal.ErrorData that represents * the current error obtaind from CopyErrorData(). */ -extern jobject ErrorData_getCurrentError(void); +extern jobject pljava_ErrorData_getCurrentError(void); /* * Extract the native ErrorData from a Java ErrorData. */ -extern ErrorData* ErrorData_getErrorData(jobject jerrorData); - +extern ErrorData* pljava_ErrorData_getErrorData(jobject jerrorData); +extern void pljava_ErrorData_initialize(void); #ifdef __cplusplus } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index e5767201..eff0b506 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -833,4 +833,99 @@ protected long getPointer() throws SQLException private native void _heapFreeTuple(long pointer); } + + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code FreeErrorData} of a single pointer. + */ + public static abstract class SingleFreeErrorData extends DualState + { + private volatile long m_pointer; + + protected SingleFreeErrorData( + Key cookie, T referent, long resourceOwner, long fedTarget) + { + super(cookie, referent, resourceOwner); + m_pointer = fedTarget; + } + + @Override + public String toString(Object o) + { + return String.format("%s FreeErrorData(%x)", super.toString(o), + m_pointer); + } + + /** + * For this class, the native state is valid whenever the wrapped + * pointer is not null. + */ + @Override + protected boolean nativeStateIsValid() + { + return 0 != m_pointer; + } + + /** + * When the native state is released, the wrapped pointer is nulled + * to indicate the state is no longer valid; no + * {@code FreeErrorData} call is + * made, on the assumption that the resource owner's release will be + * followed by wholesale release of the containing memory context + * anyway. + */ + @Override + protected void nativeStateReleased() + { + m_pointer = 0; + } + + /** + * When the Java state is released, the wrapped pointer is nulled to + * indicate the state is no longer valid, and a + * {@code FreeErrorData} + * call is made so the native memory is released without having to wait + * for release of its containing context. + *

    + * This overrides the inherited default, which would have removed this + * instance from the live instances collection. Users of this class + * should not call this method directly, but simply call + * {@link #enqueue enqueue}, and let the reclamation happen when the + * queue is processed. + */ + @Override + protected void javaStateReleased() + { + synchronized(Backend.THREADLOCK) + { + long p = m_pointer; + m_pointer = 0; + if ( 0 != p ) + _freeErrorData(p); + } + } + + /** + * This override simply calls + * {@link #javaStateReleased javaStateReleased}, so there is no + * difference in the effect of the Java object being explicitly + * released, or found unreachable by the garbage collector. + */ + @Override + protected void javaStateUnreachable() + { + javaStateReleased(); + } + + /** + * Allows a subclass to obtain the wrapped pointer value. + */ + protected long getPointer() throws SQLException + { + assertNativeStateIsValid(); + return m_pointer; + } + + private native void _freeErrorData(long pointer); + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java index cbdea9d0..b174f430 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java @@ -1,22 +1,75 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; +import java.lang.reflect.UndeclaredThrowableException; +import java.sql.SQLException; + /** * The ErrorData correspons to the ErrorData obtained * using an internal PostgreSQL CopyErrorData call. * * @author Thomas Hallgren */ -public class ErrorData extends JavaWrapper +public class ErrorData { - ErrorData(long pointer) + private final State m_state; + + ErrorData(DualState.Key cookie, long resourceOwner, long pointer) { - super(pointer); + m_state = new State(cookie, this, resourceOwner, pointer); + } + + private static class State + extends DualState.SingleFreeErrorData + { + private State( + DualState.Key cookie, ErrorData ed, long ro, long ht) + { + super(cookie, ed, ro, ht); + } + + /** + * Return the ErrorData pointer. + *

    + * As long as this value is used in instance methods on ErrorData + * (or subclasses, or on something that holds a reference to this + * ErrorData) and only while they hold Backend.THREADLOCK, it isn't + * necessary to also hold the monitor on this State object. The state + * can't go java-unreachable while an instance method's on the stack, + * and as long as we're on the thread that's in PG, the Invocation that + * state is scoped to can't be popped before we return. + */ + private long getErrorDataPtr() throws SQLException + { + return getPointer(); + } + } + + /** + * Return pointer to native ErrorData structure as a long; use only while + * a reference to this class is live and the THREADLOCK is held. + */ + private final long getNativePointer() + { + try + { + return m_state.getErrorDataPtr(); + } + catch ( SQLException e ) + { + throw new UndeclaredThrowableException(e, e.getMessage()); + } } /** From d5da82fe3964228331798da23f62cc4990be8205 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 24 Feb 2019 23:48:18 -0500 Subject: [PATCH 0259/1087] Axe JavaWrapper, Invocation CallLocals, finalizers DualState is now the way anything in Java that's paired with native state, and formerly extended JavaWrapper and/or used Java Object.finalize() or Invocation_createLocalWrapper, is now managed. Those other mechanisms are removed. JavaMemoryContext is now defined in Backend.c. Nothing uses Object.finalize() now. --- pljava-so/src/main/c/Backend.c | 5 + pljava-so/src/main/c/Invocation.c | 111 +----------------- pljava-so/src/main/c/SQLOutputToTuple.c | 2 +- pljava-so/src/main/c/type/JavaWrapper.c | 80 ------------- pljava-so/src/main/c/type/Type.c | 2 - .../src/main/include/pljava/Invocation.h | 27 ++--- .../main/include/pljava/type/JavaWrapper.h | 34 ------ .../pljava/internal/JavaWrapper.java | 49 -------- 8 files changed, 17 insertions(+), 293 deletions(-) delete mode 100644 pljava-so/src/main/c/type/JavaWrapper.c delete mode 100644 pljava-so/src/main/include/pljava/type/JavaWrapper.h delete mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/JavaWrapper.java diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 334fb271..504f3ad2 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -91,6 +91,7 @@ extern PLJAVADLLEXPORT void _PG_init(void); #define LOCAL_REFERENCE_COUNT 128 +MemoryContext JavaMemoryContext; jlong mainThreadId; static JavaVM* s_javaVM = 0; @@ -824,6 +825,10 @@ static void initPLJavaClasses(void) { 0, 0, 0 } }; + JavaMemoryContext = AllocSetContextCreate(TopMemoryContext, + "PL/Java", + ALLOCSET_DEFAULT_SIZES); + Exception_initialize(); elog(DEBUG2, "checking for a PL/Java Backend class on the given classpath"); diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index af18a68d..9aaced65 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -23,29 +23,6 @@ #define LOCAL_FRAME_SIZE 128 -struct CallLocal_ -{ - /** - * Pointer to the call local structure. - */ - void* pointer; - - /** - * The invocation where this CallLocal was allocated - */ - Invocation* invocation; - - /** - * Next CallLocal in a double linked list - */ - CallLocal* next; - - /** - * Previous CallLocal in a double linked list - */ - CallLocal* prev; -}; - static jmethodID s_Invocation_onExit; static unsigned int s_callLevel = 0; @@ -133,7 +110,6 @@ void Invocation_pushBootContext(Invocation* ctx) ctx->errorOccured = false; ctx->inExprContextCB = false; ctx->previous = 0; - ctx->callLocals = 0; #if PG_VERSION_NUM >= 100000 ctx->triggerData = 0; #endif @@ -158,7 +134,6 @@ void Invocation_pushInvocation(Invocation* ctx, bool trusted) ctx->errorOccured = false; ctx->inExprContextCB = false; ctx->previous = currentInvocation; - ctx->callLocals = 0; #if PG_VERSION_NUM >= 100000 ctx->triggerData = 0; #endif @@ -169,7 +144,6 @@ void Invocation_pushInvocation(Invocation* ctx, bool trusted) void Invocation_popInvocation(bool wasException) { - CallLocal* cl; Invocation* ctx = currentInvocation->previous; /* @@ -211,94 +185,11 @@ void Invocation_popInvocation(bool wasException) PG_END_TRY(); MemoryContextSwitchTo(ctx->upperContext); } - - /** - * Reset all local wrappers that has been allocated during this call. Yank them - * from the double linked list but do *not* remove them. - */ - cl = currentInvocation->callLocals; - if(cl != 0) - { - CallLocal* first = cl; - do - { - cl->pointer = 0; - cl->invocation = 0; - cl = cl->next; - } while(cl != first); - } + currentInvocation = ctx; --s_callLevel; } -void Invocation_freeLocalWrapper(jlong wrapper) -{ - Ptr2Long p2l; - Invocation* ctx; - CallLocal* cl; - CallLocal* prev; - - p2l.longVal = wrapper; - cl = (CallLocal*)p2l.ptrVal; - prev = cl->prev; - if(prev != cl) - { - /* Disconnect - */ - CallLocal* next = cl->next; - prev->next = next; - next->prev = prev; - } - - /* If this CallLocal is freed before its owning invocation was - * popped then there's a risk that this is the first CallLocal - * in the list. - */ - ctx = cl->invocation; - if(ctx != 0 && ctx->callLocals == cl) - { - if(prev == cl) - prev = 0; - ctx->callLocals = prev; - } - pfree(cl); -} - -void* Invocation_getWrappedPointer(jlong wrapper) -{ - Ptr2Long p2l; - p2l.longVal = wrapper; - return ((CallLocal*)p2l.ptrVal)->pointer; -} - -jlong Invocation_createLocalWrapper(void* pointer) -{ - /* Create a local wrapper for the pointer - */ - Ptr2Long p2l; - CallLocal* cl = (CallLocal*)MemoryContextAlloc(JavaMemoryContext, sizeof(CallLocal)); - CallLocal* prev = currentInvocation->callLocals; - if(prev == 0) - { - currentInvocation->callLocals = cl; - cl->prev = cl; - cl->next = cl; - } - else - { - CallLocal* next = prev->next; - cl->prev = prev; - cl->next = next; - prev->next = cl; - next->prev = cl; - } - cl->pointer = pointer; - cl->invocation = currentInvocation; - p2l.longVal = 0L; /* ensure that the rest is zeroed out */ - p2l.ptrVal = cl; - return p2l.longVal; -} - MemoryContext Invocation_switchToUpperContext(void) { diff --git a/pljava-so/src/main/c/SQLOutputToTuple.c b/pljava-so/src/main/c/SQLOutputToTuple.c index 1cdee9b8..e57a37b7 100644 --- a/pljava-so/src/main/c/SQLOutputToTuple.c +++ b/pljava-so/src/main/c/SQLOutputToTuple.c @@ -13,8 +13,8 @@ * @author Thomas Hallgren */ #include +#include "pljava/type/Type.h" #include "pljava/type/TupleDesc.h" -#include "pljava/type/JavaWrapper.h" #include "pljava/SQLOutputToTuple.h" static jclass s_SQLOutputToTuple_class; diff --git a/pljava-so/src/main/c/type/JavaWrapper.c b/pljava-so/src/main/c/type/JavaWrapper.c deleted file mode 100644 index 2300d9a4..00000000 --- a/pljava-so/src/main/c/type/JavaWrapper.c +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html - * - * @author Thomas Hallgren - */ -#include "org_postgresql_pljava_internal_JavaWrapper.h" -#include "pljava/type/Type_priv.h" -#include "pljava/type/JavaWrapper.h" -#include "pljava/Backend.h" -#include "pljava/Exception.h" - -static jclass s_JavaWrapper_class; -static jfieldID s_JavaWrapper_m_pointer; - -MemoryContext JavaMemoryContext; - -static jlong _getPointer(jobject managed) -{ - if(managed == 0) - { - Exception_throw(ERRCODE_INTERNAL_ERROR, "Null JavaWrapper object"); - return 0; - } - - return JNI_getLongField(managed, s_JavaWrapper_m_pointer); -} - -static Datum _JavaWrapper_coerceObject(Type self, jobject nStruct) -{ - Ptr2Long p2l; - p2l.longVal = _getPointer(nStruct); - return PointerGetDatum(p2l.ptrVal); -} - -TypeClass JavaWrapperClass_alloc(const char* name) -{ - TypeClass self = TypeClass_alloc(name); - self->coerceObject = _JavaWrapper_coerceObject; - return self; -} - -extern void JavaWrapper_initialize(void); -void JavaWrapper_initialize(void) -{ - JNINativeMethod methods[] = - { - { - "_free", - "(J)V", - Java_org_postgresql_pljava_internal_JavaWrapper__1free - }, - { 0, 0, 0 } - }; - - s_JavaWrapper_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/JavaWrapper")); - PgObject_registerNatives2(s_JavaWrapper_class, methods); - s_JavaWrapper_m_pointer = PgObject_getJavaField(s_JavaWrapper_class, "m_pointer", "J"); - - JavaMemoryContext = AllocSetContextCreate(TopMemoryContext, - "PL/Java", - ALLOCSET_DEFAULT_SIZES); -} - -/* - * Class: org_postgresql_pljava_internal_JavaWrapper - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_JavaWrapper__1free(JNIEnv* env, jobject _this, jlong pointer) -{ - BEGIN_NATIVE_NO_ERRCHECK - Ptr2Long p2l; - p2l.longVal = pointer; - pfree(p2l.ptrVal); - END_NATIVE -} diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 8e3ee952..a060c43c 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -804,7 +804,6 @@ extern void AclId_initialize(void); extern void String_initialize(void); extern void byte_array_initialize(void); -extern void JavaWrapper_initialize(void); extern void ExecutionPlan_initialize(void); extern void Portal_initialize(void); extern void TupleTable_initialize(void); @@ -844,7 +843,6 @@ void Type_initialize(void) byte_array_initialize(); - JavaWrapper_initialize(); ExecutionPlan_initialize(); Portal_initialize(); TupleTable_initialize(); diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index b1ed2ba0..b0e9eb4b 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -19,9 +25,6 @@ extern "C" { #endif -struct CallLocal_; -typedef struct CallLocal_ CallLocal; - struct Invocation_ { /** @@ -66,12 +69,6 @@ struct Invocation_ */ bool errorOccured; - /** - * List of call local structures that has been wrapped - * during this invocation. - */ - CallLocal* callLocals; - #if PG_VERSION_NUM >= 100000 /** * TriggerData pointer, if the function is being called as a trigger, @@ -103,10 +100,6 @@ extern void Invocation_pushInvocation(Invocation* ctx, bool trusted); extern void Invocation_popInvocation(bool wasException); -extern jlong Invocation_createLocalWrapper(void* pointer); -extern void* Invocation_getWrappedPointer(jlong wrapper); -extern void Invocation_freeLocalWrapper(jlong wrapper); - extern jobject Invocation_getTypeMap(void); /* diff --git a/pljava-so/src/main/include/pljava/type/JavaWrapper.h b/pljava-so/src/main/include/pljava/type/JavaWrapper.h deleted file mode 100644 index 7491dafc..00000000 --- a/pljava-so/src/main/include/pljava/type/JavaWrapper.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html - * - * @author Thomas Hallgren - */ -#ifndef __pljava_JavaWrapper_h -#define __pljava_JavaWrapper_h - -#include "pljava/type/Type.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/************************************************************************** - * The JavaWrapper is a Java class that maintains a pointer to a - * piece of memory allocated in the special JavaMemoryContext. - * - * @author Thomas Hallgren - *************************************************************************/ - -/* - * Allocates a new TypeClass and assigns a default coerceObject method used by - * all JavaWrapper derivates. - */ -extern TypeClass JavaWrapperClass_alloc(const char* name); - -#ifdef __cplusplus -} -#endif -#endif diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/JavaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/JavaWrapper.java deleted file mode 100644 index 5b21ecc8..00000000 --- a/pljava/src/main/java/org/postgresql/pljava/internal/JavaWrapper.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root directory of this distribution or at - * http://eng.tada.se/osprojects/COPYRIGHT.html - */ -package org.postgresql.pljava.internal; - -public abstract class JavaWrapper -{ - private final long m_pointer; - - /** - * Creates an instance of this class that will be attached to a native - * structure represented by pointer. This constructor must only be called - * from native code. - * - * @param pointer The wapped pointer. - */ - protected JavaWrapper(long pointer) - { - m_pointer = pointer; - } - - public void finalize() - { - synchronized(Backend.THREADLOCK) - { - _free(m_pointer); - } - } - - /** - * Returns the native pointer - */ - public final long getNativePointer() - { - return m_pointer; - } - - /** - * Calls the C function pfree() with the given pointer as an argument. - * Subclasses may override this method if special handling is needed when - * freeing up the object. - * - * @param pointer The pointer to free. - */ - protected native void _free(long pointer); -} From 77680ba69299340e1238f377e911af8fd83eb6c3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 25 Feb 2019 22:23:52 -0500 Subject: [PATCH 0260/1087] Expose some DualState statistics as an MBean. --- .../postgresql/pljava/internal/DualState.java | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index eff0b506..a4345c65 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -20,6 +20,10 @@ import java.util.Iterator; import java.util.concurrent.ConcurrentLinkedQueue; +import static java.lang.management.ManagementFactory.getPlatformMBeanServer; +import javax.management.ObjectName; +import javax.management.JMException; + /** * Base class for object state with corresponding Java and native components. *

    @@ -109,6 +113,22 @@ public abstract class DualState extends WeakReference private static Queue s_liveInstances = new ConcurrentLinkedQueue(); + /** + * Bean to expose DualState allocation/release statistics to JMX management + * tools. + */ + private static final Statistics s_stats = new Statistics(); + + static { + try + { + ObjectName n = new ObjectName( + "org.postgresql.pljava:type=DualState,name=Statistics"); + getPlatformMBeanServer().registerMBean(s_stats, n); + } + catch ( JMException e ) { } + } + /** * Pointer value of the {@code ResourceOwner} this instance belongs to, * if any. @@ -148,7 +168,10 @@ protected DualState(Key cookie, T referent, long resourceOwner) checkCookie(cookie); m_resourceOwner = resourceOwner; + s_liveInstances.add(this); + + s_stats.construct(); } /** @@ -313,20 +336,29 @@ public String toString(Object o) */ private static void resourceOwnerRelease(long resourceOwner) { + long total = 0L, delist = 0L, release = 0L; + for ( Iterator i = s_liveInstances.iterator(); i.hasNext(); ) { + ++ total; DualState s = i.next(); if ( s.m_resourceOwner == resourceOwner ) { + ++ delist; i.remove(); synchronized ( s ) { if ( s.nativeStateIsValid() ) + { + ++ release; s.nativeStateReleased(); + } } } } + + s_stats.resourceOwnerPoll(delist, release, total); } /** @@ -340,19 +372,28 @@ private static void resourceOwnerRelease(long resourceOwner) */ private static void cleanEnqueuedInstances() { + long total = 0L, delist = 0L, release = 0L; + DualState s; while ( null != (s = (DualState)s_releasedInstances.poll()) ) { - s_liveInstances.remove(s); + ++ total; + if ( s_liveInstances.remove(s) ) + ++ delist; try { if ( null == s.get() ) s.javaStateUnreachable(); else + { + ++ release; s.javaStateReleased(); + } } catch ( Throwable t ) { } /* JDK 9 Cleaner ignores exceptions, so */ } + + s_stats.referenceQueueDrain(delist, total - release, release, total); } /** @@ -928,4 +969,71 @@ protected long getPointer() throws SQLException private native void _freeErrorData(long pointer); } + + /** + * Bean exposing some {@code DualState} allocation and lifecycle statistics + * for viewing in a JMX management client. + */ + public static interface StatisticsMBean + { + long getEnlisted(); + long getDelisted(); + long getJavaUnreachable(); + long getJavaReleased(); + long getNativeReleased(); + long getResourceOwnerPolls(); + long getResourceOwnerHits(); + long getResourceOwnerMisses(); + long getReferenceQueueDrains(); + long getReferenceQueueDrained(); + } + + static class Statistics implements StatisticsMBean + { + public long getEnlisted() { return enlisted; } + public long getDelisted() { return delisted; } + public long getJavaUnreachable() { return javaUnreachable; } + public long getJavaReleased() { return javaReleased; } + public long getNativeReleased() { return nativeReleased; } + public long getResourceOwnerPolls() { return resourceOwnerPolls; } + public long getResourceOwnerHits() { return resourceOwnerHits; } + public long getResourceOwnerMisses() { return resourceOwnerMisses; } + public long getReferenceQueueDrains() { return referenceQueueDrains; } + public long getReferenceQueueDrained() { return referenceQueueDrained; } + + private long enlisted = 0L; + private long delisted = 0L; + private long javaUnreachable = 0L; + private long javaReleased = 0L; + private long nativeReleased = 0L; + private long resourceOwnerPolls = 0L; + private long resourceOwnerHits = 0L; + private long resourceOwnerMisses = 0L; + private long referenceQueueDrains = 0L; + private long referenceQueueDrained = 0L; + + final void construct() + { + ++ enlisted; + } + + final void resourceOwnerPoll(long delist, long release, long total) + { + ++ resourceOwnerPolls; + resourceOwnerHits += delist; + resourceOwnerMisses += total - delist; + nativeReleased += release; + delisted += delist; + } + + final void referenceQueueDrain( + long delist, long unreachable, long release, long total) + { + ++ referenceQueueDrains; + referenceQueueDrained += total; + javaUnreachable += unreachable; + javaReleased += release; + delisted += delist; + } + } } From cce44685ff5591f0869ff12aacac4177b8bf09ea Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 1 Mar 2019 21:19:43 -0500 Subject: [PATCH 0261/1087] An optimization for non-transient native scopes. DualState works when NULL is passed as a resource owner, in which case the native state had better be in some long-lived area like JavaMemoryContext, because no call to nativeRelease with a real pointer will ever match it, so javaStateUnreachable or javaStateReleased will be the only ways it can be freed. (It follows that any DualState flavor that will be used that way had better have a freeing action as part of its javaStateUnreachable / javaStateReleased.) It seems preferable to keep things in more tightly scoped contexts and give them real owners, but this pattern was used in enough of the former-JavaWrapper cases just migrated that it's worth supporting (at least until further analysis can migrate some of those to use appropriate scopes). So, a worthwhile optimization is to track objects that lack resource owners in a simpler data structure, that does not need to support any retrieval by resource owner. They still have to be kept live somehow (otherwise Java forgets about them and will not enqueue them when their referents are unreachable), but a simple IdentityHashMap suffices, and they can be removed in O(1) when found unreachable or released by Java. For objects with resource owners, the tracking structure is now a hash map from owner to doubly-linked list of the associated instances. The list is directly implemented with prev and next references in the instances themselves, rather than using a Collections class, so that when an instance is found unreachable or released from Java, it also can be removed from the list in O(1) with no searching. Exit of the native resource owner scope is, of course, handled by looking up the owner in the map and processing every instance remaining on the list. The structures used now are plain vanilla java.util implementations with no claimed thread safety properties (and the same is true of the directly-implemented doubly-linked list code). The Javadoc now explains at length the usage requirements that must be observed for the design to be safe. --- .../postgresql/pljava/internal/DualState.java | 365 ++++++++++++++---- 1 file changed, 293 insertions(+), 72 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index a4345c65..74956140 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -16,9 +16,9 @@ import java.sql.SQLException; -import java.util.Queue; -import java.util.Iterator; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.IdentityHashMap; +import java.util.HashMap; +import java.util.Map; import static java.lang.management.ManagementFactory.getPlatformMBeanServer; import javax.management.ObjectName; @@ -48,15 +48,11 @@ * native state. * *

    - * The introduction of this class represents yet another pattern - * within PL/Java for objects that combine Java and native state. It is meant - * to be general (and documented) enough to support future gradual migration of - * other existing patterns to it. - *

    * A parameter to the {@code DualState} constructor is a {@code ResourceOwner}, * a PostgreSQL implementation concept introduced in PG 8.0. Instances will be * called at their {@link #nativeStateReleased nativeStateReleased} methods - * when the corresponding {@code ResourceOwner} is released in PostgreSQL. + * when the corresponding {@code ResourceOwner} is released in PostgreSQL, if + * they are still reachable in Java. *

    * However, this class does not require the {@code resourceOwner} parameter to * be, in all cases, a pointer to a PostgreSQL {@code ResourceOwner}. It is @@ -88,6 +84,77 @@ * {@link #javaStateReleased javaStateReleased} method, which will be called by * {@code cleanEnqueuedInstances} if the weak reference is nonnull, indicating * the instance was enqueued explicitly rather than by the garbage collector. + *

    + * Alternatively, a close action from Java can be handled by calling the + * {@code javaStateReleased} method directly, rather than depending on the + * reference queue. In that case, the {@code javaStateReleased} method must call + * its {@code super} implementation in this class, to take care of removing the + * instance from the data structures tracking it here. In contrast, a + * {@code javaStateReleased} method that is intended to be called via the + * reference queue should not call {@code super}; the reference-queue + * processing loop will already have removed the instance from the data + * structures before calling the method. + *

    + * The convention of calling {@code javaStateReleased} vie the reference queue + * is likely to be most often what's wanted, as the method may need to make + * native calls to release native state, and processing of the reference queue + * will always take place on a thread in the proper state for doing that. + *

    + * There are different abstract subclasses of {@code DualState} that wrap + * different sorts of PostgreSQL native state, and encapsulate what needs to be + * done when such state is released from the Java or native side. Each subclass + * needs to provide an appropriately-typed {@code protected} method for + * obtaining a reference to the wrapped native state, first verifying that it is + * still valid. More such subclasses can be added as needed. + *

    + * A client class of {@code DualState} will typically contain a static nested + * class that further extends one of these abstract subclasses, and the client + * instance will hold a strong reference to an instance of that + * {@code DualState} subclass constructed at the same time. The client class + * must synchronize on the nested state class instance whenever calling + * the methods that check validity or return native state references, and must + * hold that monitor for the duration of any activity that depends on that + * validity or reference result. A client class may not retain such a + * reference after exiting the synchronized section, but must synchronize and + * obtain it again when next needed. + *

    + * The data structures used internally in this class to track + * created instances through their life cycles are not synchronized or + * thread-safe. The design rests on the following requirements: + *

      + *
    • The structures are only traversed or modified during: + *
        + *
      • Instance construction + *
      • Reference queue processing (instances found unreachable by Java's + * garbage collector, or {@code enqueue}d directly as an explicit means of + * release) + *
      • Exit of a resource owner's scope + *
      • Direct call of {@code javaStateReleased} (i.e., not via the reference + * queue). + *
      + *
    • PL/Java uses synchronization to control which thread is entitled to + * interact with PostgreSQL native code. Historically, native access has not + * been restricted to only one thread, but to only one thread at a time. A + * different thread can obtain the lock to call into PostgreSQL only when the + * thread currently holding it has called or returned into Java. + *
    • Construction of any {@code DualState} instance is to take place only on + * a thread that holds the lock for native access. The requirement to pass any + * constructor a {@code DualState.Key} instance, obtainable by native code, is + * intended to reinforce that convention. It is not abuse-proof: Java code could + * retain a {@code Key} reference and reuse it on a thread without the lock, but + * that would be a deliberate coding error. + *
    • Reference queue processing takes place only at chosen points where a + * thread enters or exits native code, and therefore holds the lock. + *
    • Resource-owner callbacks originate in native code, on a thread that holds + * the lock. + *
    + *

    + * The above rules protect the data structures from concurrency risks in all + * cases except direct calls to {@code javaStateReleased}. In any subclass where + * {@code javaStateReleased} is intended to be called directly, the overriding + * method must call the {@code super} implementation in this class to remove the + * instance from the data structures, and must hold the lock for native access + * when it does so (whether or not it has any actual need to call native code). */ public abstract class DualState extends WeakReference { @@ -104,14 +171,39 @@ public abstract class DualState extends WeakReference * The queue is only processed by a private method called from native code * in selected places where it makes sense to do so. */ - private static ReferenceQueue s_releasedInstances = + private static final ReferenceQueue s_releasedInstances = new ReferenceQueue(); /** - * All instances are added to this collection upon creation. + * All instances in a non-transient native scope are added here upon + * creation, to keep them visible to the garbage collector. + *

    + * Because they are not in a transient native scope, only the + * {@code javaStateUnreachable} or {@code javaStateReleased} lifecycle + * events can occur, and in either case the object is in hand with no + * searching, and can be removed from this structure in O(1). */ - private static Queue s_liveInstances = - new ConcurrentLinkedQueue(); + private static final IdentityHashMap + s_unscopedInstances = new IdentityHashMap(); + + /** + * All native-scoped instances are added to this structure upon creation. + *

    + * The hash map takes a resource owner to the doubly-linked list of + * instances it owns. The list is implemented directly with the two list + * fields here (rather than by a Collections class), so that an instance can + * be unlinked with no searching in the case of {@code javaStateUnreachable} + * or {@code javaStateReleased}, where the instance to be unlinked is + * already at hand. The list head is of a dummy {@code DualState} subclass. + */ + private static final Map s_scopedInstances = + new HashMap(); + + /** Backward link in per-resource-owner list. */ + DualState m_prev; + + /** Forward link in per-resource-owner list. */ + DualState m_next; /** * Bean to expose DualState allocation/release statistics to JMX management @@ -159,19 +251,48 @@ protected static void checkCookie(Key cookie) * @param referent The Java object whose state this instance represents. * @param resourceOwner Pointer value of the native {@code ResourceOwner} * whose release callback will indicate that this object's native state is - * no longer valid. + * no longer valid. If zero (a NULL pointer in C), it indicates that the + * state is held in long-lived native memory (such as JavaMemoryContext), + * and can only be released via {@code javaStateUnreachable} or + * {@code javaStateReleased}. */ protected DualState(Key cookie, T referent, long resourceOwner) { super(referent, s_releasedInstances); + long scoped = 0L; + checkCookie(cookie); m_resourceOwner = resourceOwner; - s_liveInstances.add(this); + if ( 0 != resourceOwner ) + { + scoped = 1L; + DualState.ListHead head = s_scopedInstances.get(resourceOwner); + if ( null == head ) + { + head = new DualState.ListHead(resourceOwner); + s_scopedInstances.put(resourceOwner, head); + } + m_prev = head; + m_next = head.m_next; + m_prev.m_next = m_next.m_prev = this; + } + else + s_unscopedInstances.put(this, this); - s_stats.construct(); + s_stats.construct(scoped); + } + + /** + * Private constructor only for dummy instances to use as the list heads + * for per-resource-owner lists. + */ + private DualState(T referent, long resourceOwner) + { + super(referent); + m_resourceOwner = resourceOwner; } /** @@ -215,20 +336,45 @@ protected void javaStateUnreachable() * method on the referent object). This can be handled two ways: *

      *
    • A {@code close} or similar method calls this directly. This instance - * must be removed from the {@code liveInstances} collection. This default - * implementation does so. + * must be removed from the appropriate collection. This default + * implementation does so. Overriding methods can call it as {@code super} + * for that part of the job. *
    • A {@code close} or similar method simply calls * {@link #enqueue enqueue} instead of this method. This method will be * called when the queue is processed, the next time native code calls * {@link #cleanEnqueuedInstances cleanEnqueuedInstances}. For that case, * this method should be overridden to do whatever other cleanup is in - * order, but not remove the instance from {@code liveInstances}, + * order, but not call {@code super} and not remove the + * instance from the collection here, * which will have happened just before this method is called. *
    + *

    + * Convention wisdom would normally favor the first form, handling releases + * directly and not enqueueing things where it can be avoided. For the + * purposes of {@code DualState}, though, the second pattern can be + * advantageous, letting releases be handled via the reference queue, + * because the queue is always processed in a thread able to call into + * PostgreSQL, which instances with native state to be freed will typically + * need to do. + *

    + * Note: if a state subclass has releases managed by calling {@code enqueue} + * but has not overridden this method, the statistics bean will end up + * double-counting releases for that class. */ protected void javaStateReleased() { - s_liveInstances.remove(this); + long scoped = 0L, unscoped = 0L; + + if ( 0 != m_resourceOwner ) + { + if ( remove() ) + scoped = 1L; + } + else + if ( null != s_unscopedInstances.remove(this) ) + unscoped = 1L; + + s_stats.javaRelease(scoped, unscoped); } /** @@ -336,29 +482,41 @@ public String toString(Object o) */ private static void resourceOwnerRelease(long resourceOwner) { - long total = 0L, delist = 0L, release = 0L; + long total = 0L, release = 0L; + + DualState head = s_scopedInstances.remove(resourceOwner); + if ( null == head ) + return; - for ( Iterator i = s_liveInstances.iterator(); - i.hasNext(); ) + DualState t = head.m_next; + head.m_prev = head.m_next = null; + for ( DualState s = t ; s != head ; s = t ) { + t = s.m_next; + s.m_prev = s.m_next = null; ++ total; - DualState s = i.next(); - if ( s.m_resourceOwner == resourceOwner ) + if ( null == s.get() ) // Unreachable from Java, no action needed + continue; + /* + * This synchronized() is part of DualState's contract with clients. + * They are responsible for synchronizing on the state instance + * whenever they obtain the wrapped native state (which is verified + * to still be valid at that time) and to hold that monitor for the + * duration of whatever operation needs access to that state. By + * acquiring the same monitor here, native state is blocked from + * vanishing while it is actively in use. + */ + synchronized ( s ) { - ++ delist; - i.remove(); - synchronized ( s ) + if ( s.nativeStateIsValid() ) { - if ( s.nativeStateIsValid() ) - { - ++ release; - s.nativeStateReleased(); - } + ++ release; + s.nativeStateReleased(); } } } - s_stats.resourceOwnerPoll(delist, release, total); + s_stats.resourceOwnerPoll(release, total); } /** @@ -372,14 +530,24 @@ private static void resourceOwnerRelease(long resourceOwner) */ private static void cleanEnqueuedInstances() { - long total = 0L, delist = 0L, release = 0L; + long total = 0L, delistScoped = 0L, delistUnscoped = 0L, release = 0L; DualState s; while ( null != (s = (DualState)s_releasedInstances.poll()) ) { ++ total; - if ( s_liveInstances.remove(s) ) - ++ delist; + + if ( 0 != s.m_resourceOwner ) + { + if ( s.remove() ) + ++ delistScoped; + } + else + { + if ( null != s_unscopedInstances.remove(s) ) + ++ delistUnscoped; + } + try { if ( null == s.get() ) @@ -393,7 +561,24 @@ private static void cleanEnqueuedInstances() catch ( Throwable t ) { } /* JDK 9 Cleaner ignores exceptions, so */ } - s_stats.referenceQueueDrain(delist, total - release, release, total); + s_stats.referenceQueueDrain( + delistScoped, delistUnscoped, total - release, release, total); + } + + /** + * Remove this instance from the per-resource-owner linked list holding it. + * @return true if this instance was on a list, and has been removed + */ + private boolean remove() + { + if ( null == m_prev || null == m_next ) + return false; + if ( this == m_prev.m_next ) + m_prev.m_next = m_next; + if ( this == m_next.m_prev ) + m_next.m_prev = m_prev; + m_prev = m_next = null; + return true; } /** @@ -415,6 +600,30 @@ private Key() } } + /** + * Dummy DualState concrete class whose instances only serve as list + * headers in per-resource-owner lists of instances. + */ + private static class ListHead extends DualState // because why not? + { + /** + * Construct a {@code ListHead} instance. As a subclass of + * {@code DualState}, it can't help having a resource owner field, so + * may as well use it to store the resource owner that the list is for, + * in case it's of interest in debugging. + * @param ownr The resource owner + */ + private ListHead(long ownr) + { + super("", ownr); // An instance needs some object to be its referent + clear(); // ... but doesn't need it for long! + m_prev = m_next = this; + } + + protected boolean nativeStateIsValid() { return false; } + protected void nativeStateReleased() { } + } + /** * A {@code DualState} subclass serving only to guard access to a single * nonzero {@code long} value (typically a native pointer). @@ -976,64 +1185,76 @@ protected long getPointer() throws SQLException */ public static interface StatisticsMBean { - long getEnlisted(); - long getDelisted(); + long getConstructed(); + long getEnlistedScoped(); + long getEnlistedUnscoped(); + long getDelistedScoped(); + long getDelistedUnscoped(); long getJavaUnreachable(); long getJavaReleased(); long getNativeReleased(); - long getResourceOwnerPolls(); - long getResourceOwnerHits(); - long getResourceOwnerMisses(); - long getReferenceQueueDrains(); - long getReferenceQueueDrained(); + long getResourceOwnerPasses(); + long getReferenceQueuePasses(); + long getReferenceQueueItems(); } static class Statistics implements StatisticsMBean { - public long getEnlisted() { return enlisted; } - public long getDelisted() { return delisted; } + public long getConstructed() { return constructed; } + public long getEnlistedScoped() { return enlistedScoped; } + public long getEnlistedUnscoped() { return enlistedUnscoped; } + public long getDelistedScoped() { return delistedScoped; } + public long getDelistedUnscoped() { return delistedUnscoped; } public long getJavaUnreachable() { return javaUnreachable; } public long getJavaReleased() { return javaReleased; } public long getNativeReleased() { return nativeReleased; } - public long getResourceOwnerPolls() { return resourceOwnerPolls; } - public long getResourceOwnerHits() { return resourceOwnerHits; } - public long getResourceOwnerMisses() { return resourceOwnerMisses; } - public long getReferenceQueueDrains() { return referenceQueueDrains; } - public long getReferenceQueueDrained() { return referenceQueueDrained; } - - private long enlisted = 0L; - private long delisted = 0L; + public long getResourceOwnerPasses() { return resourceOwnerPasses; } + public long getReferenceQueuePasses() { return referenceQueuePasses; } + public long getReferenceQueueItems() { return referenceQueueItems; } + + private long constructed = 0L; + private long enlistedScoped = 0L; + private long enlistedUnscoped = 0L; + private long delistedScoped = 0L; + private long delistedUnscoped = 0L; private long javaUnreachable = 0L; private long javaReleased = 0L; private long nativeReleased = 0L; - private long resourceOwnerPolls = 0L; - private long resourceOwnerHits = 0L; - private long resourceOwnerMisses = 0L; - private long referenceQueueDrains = 0L; - private long referenceQueueDrained = 0L; + private long resourceOwnerPasses = 0L; + private long referenceQueuePasses = 0L; + private long referenceQueueItems = 0L; + + final void construct(long scoped) + { + ++ constructed; + enlistedScoped += scoped; + enlistedUnscoped += 1L - scoped; + } - final void construct() + final void resourceOwnerPoll(long delist, long total) { - ++ enlisted; + ++ resourceOwnerPasses; + nativeReleased += delist; + delistedScoped += total; } - final void resourceOwnerPoll(long delist, long release, long total) + final void javaRelease(long scoped, long unscoped) { - ++ resourceOwnerPolls; - resourceOwnerHits += delist; - resourceOwnerMisses += total - delist; - nativeReleased += release; - delisted += delist; + ++ javaReleased; + delistedScoped += scoped; + delistedUnscoped += unscoped; } final void referenceQueueDrain( - long delist, long unreachable, long release, long total) + long delistScoped, long delistUnscoped, + long unreachable, long release, long total) { - ++ referenceQueueDrains; - referenceQueueDrained += total; + ++ referenceQueuePasses; + referenceQueueItems += total; javaUnreachable += unreachable; javaReleased += release; - delisted += delist; + delistedScoped += delistScoped; + delistedUnscoped += delistUnscoped; } } } From 79803e0eb2ec39dcb0f27c13b98b5b9679a2446d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 1 Mar 2019 21:43:14 -0500 Subject: [PATCH 0262/1087] Add pljava.java_thread_pg_entry GUC. With the elimination of PL/Java internal classes that use Object.finalize(), PL/Java now has no inherent need to enter PG from any Java thread other than the main one that originated in PG. That may open space to simplify the thread-coordination conventions that have been a source of much complexity in PL/Java's code (including some fairly unconvincing bits, like the stack-base manipulations, not to mention the whole premise of mixing bytecode monitorenter/exit with JNI MonitorEnter/Exit, which the JNI spec has always called unsupported. It has been mostly, usually working ok in Hotspot, but is less reliable in OpenJ9). However, it is possible that user code that is run in PL/Java could create new threads that make calls on the database. How common that is in the wild may be hard to know. So a GUC is introduced here to assist in vetting existing code. Its default value is 'allow', giving PL/Java's historical behavior: any Java thread can enter PG while the main thread has entered Java. If set to 'error', any thread other than the main one will incur an exception if it tries to enter PG. If 'block', the main thread will skip all of its usual monitor releases/acquires around calls into Java, meaning it will hold the monitor at all times, and any other thread that tries to enter PG will indefinitely block. (Should that happen, a JMX tool like JConsole can be used to identify the blocked thread.) The 'error' setting can be used to find out if the code being run with PL/Java has any other-threads-entering-PG issues. If the workload is known to have no such issues, the 'block' setting is recommended for simplicity and sanity. --- pljava-so/src/main/c/Backend.c | 92 +++++++++++++++++++- pljava-so/src/main/c/JNICalls.c | 28 +++++- pljava-so/src/main/include/pljava/JNICalls.h | 19 ++++ src/site/markdown/use/variables.md | 24 +++++ 4 files changed, 158 insertions(+), 5 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 504f3ad2..36b63a3e 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -97,6 +97,10 @@ jlong mainThreadId; static JavaVM* s_javaVM = 0; static jclass s_Backend_class; static jmethodID s_setTrusted; + +/* + * GUC states + */ static char* libjvmlocation; static char* vmoptions; static char* classpath; @@ -105,6 +109,13 @@ static int statementCacheSize; static bool pljavaDebug; static bool pljavaReleaseLingeringSavepoints; static bool pljavaEnabled; + +#if PG_VERSION_NUM >= 80400 +static int java_thread_pg_entry; +#else +static char* java_thread_pg_entry; +#endif + static bool s_currentTrust; static int s_javaLogLevel; @@ -317,6 +328,29 @@ static void initsequencer(enum initstage is, bool tolerant); #define ASSIGNSTRINGHOOK(name) ASSIGNHOOK(name, const char *) #endif +#if PG_VERSION_NUM >= 80400 +#define ASSIGNENUMHOOK(name) ASSIGNHOOK(name,int) +#define ENUMBOOTVAL(entry) ((entry).val) +#define ENUMHOOKRET true +#else +#define ASSIGNENUMHOOK(name) ASSIGNSTRINGHOOK(name) +#define ENUMBOOTVAL(entry) ((char *)((entry).name)) +#define ENUMHOOKRET newval +struct config_enum_entry +{ + const char *name; + int val; + bool hidden; +}; +#endif + +static const struct config_enum_entry java_thread_pg_entry_options[] = { + {"allow", 0, false}, + {"error", 1, false}, + {"block", 3, false}, + {NULL, 0, false} +}; + ASSIGNSTRINGHOOK(libjvm_location) { ASSIGNRETURNIFCHECK(newval); @@ -369,6 +403,29 @@ ASSIGNHOOK(enabled, bool) ASSIGNRETURN(true); } +ASSIGNENUMHOOK(java_thread_pg_entry) +{ +#if PG_VERSION_NUM >= 80400 + int val = newval; +#else + int val = -1; + struct config_enum_entry const *e; + for ( e = java_thread_pg_entry_options; NULL != e->name; ++ e ) + { + if ( 0 == strcmp(e->name, newval) ) + { + val = e->val; + } + } + if ( -1 == val ) + ASSIGNRETURN(NULL); +#endif + ASSIGNRETURNIFCHECK(ENUMHOOKRET); + pljava_JNI_setThreadPolicy( !!(val&1) /*error*/, !(val&2) /*monitorops*/); + ASSIGNRETURN(ENUMHOOKRET); +#undef RETVAL +} + /* * There are a few ways to arrive in the initsequencer. * 1. From _PG_init (called exactly once when the library is loaded for ANY @@ -1330,6 +1387,10 @@ static jint initializeJavaVM(JVMOptList *optList) StaticAssertStmt(NULL != (valueAddr), "NULL valueAddr for GUC"); \ *(a) = (v); #define GUCFLAGS(f) +#define DefineCustomEnumVariable(name, short_desc, long_desc, valueAddr, \ + options, context, assign_hook, show_hook) \ + DefineCustomStringVariable((name), (short_desc), (long_desc), (valueAddr), \ + (context), (assign_hook), (show_hook)) #endif #if PG_VERSION_NUM >= 90100 @@ -1343,21 +1404,28 @@ static jint initializeJavaVM(JVMOptList *optList) GUCBOOTASSIGN((valueAddr), (bootValue)) \ DefineCustomBoolVariable((name), (short_desc), (long_desc), (valueAddr), \ GUCBOOTVAL(bootValue) (context), GUCFLAGS(flags) GUCCHECK(check_hook) \ - assign_hook, show_hook) + (assign_hook), (show_hook)) #define INT_GUC(name, short_desc, long_desc, valueAddr, bootValue, minValue, \ maxValue, context, flags, check_hook, assign_hook, show_hook) \ GUCBOOTASSIGN((valueAddr), (bootValue)) \ DefineCustomIntVariable((name), (short_desc), (long_desc), (valueAddr), \ GUCBOOTVAL(bootValue) (minValue), (maxValue), (context), \ - GUCFLAGS(flags) GUCCHECK(check_hook) assign_hook, show_hook) + GUCFLAGS(flags) GUCCHECK(check_hook) (assign_hook), (show_hook)) #define STRING_GUC(name, short_desc, long_desc, valueAddr, bootValue, context, \ flags, check_hook, assign_hook, show_hook) \ GUCBOOTASSIGN((char const **)(valueAddr), (bootValue)) \ DefineCustomStringVariable((name), (short_desc), (long_desc), (valueAddr), \ GUCBOOTVAL(bootValue) (context), GUCFLAGS(flags) GUCCHECK(check_hook) \ - assign_hook, show_hook) + (assign_hook), (show_hook)) + +#define ENUM_GUC(name, short_desc, long_desc, valueAddr, bootValue, options, \ + context, flags, check_hook, assign_hook, show_hook) \ + GUCBOOTASSIGN((valueAddr), (bootValue)) \ + DefineCustomEnumVariable((name), (short_desc), (long_desc), (valueAddr), \ + GUCBOOTVAL(bootValue) (options), (context), GUCFLAGS(flags) \ + GUCCHECK(check_hook) (assign_hook), (show_hook)) #ifndef PLJAVA_LIBJVMDEFAULT #define PLJAVA_LIBJVMDEFAULT "libjvm" @@ -1475,6 +1543,23 @@ static void registerGUCOptions(void) NULL, /* check hook */ NULL, NULL); /* assign hook, show hook */ + ENUM_GUC( + "pljava.java_thread_pg_entry", + "Policy for entry to PG code by Java threads other than the main one", + "If 'allow', any Java thread can enter PG while the main thread has " + "entered Java. If 'error', any thread other than the main one will " + "incur an exception if it tries to enter PG. If 'block', the main " + "thread will never release its lock, so any other thread that tries " + "to enter PG will indefinitely block.", + &java_thread_pg_entry, + ENUMBOOTVAL(java_thread_pg_entry_options[0]), /* allow */ + java_thread_pg_entry_options, + PGC_USERSET, + 0, /* flags */ + NULL, /* check hook */ + assign_java_thread_pg_entry, + NULL); /* display hook */ + EmitWarningsOnPlaceholders("pljava"); } @@ -1485,6 +1570,7 @@ static void registerGUCOptions(void) #undef BOOL_GUC #undef INT_GUC #undef STRING_GUC +#undef ENUM_GUC #undef PLJAVA_ENABLE_DEFAULT #undef PLJAVA_IMPLEMENTOR_FLAGS diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 3f4016b3..1c1cc896 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -25,14 +25,26 @@ JNIEnv* jniEnv; jint (JNICALL *pljava_createvm)(JavaVM **, void **, void *); +static JNIEnv* primordialJNIEnv; + static jobject s_threadLock; +static bool s_refuseOtherThreads = false; +static bool s_doMonitorOps = true; + +void pljava_JNI_setThreadPolicy(bool refuseOtherThreads, bool doMonitorOps) +{ + s_refuseOtherThreads = refuseOtherThreads; + s_doMonitorOps = doMonitorOps; +} + + #define BEGIN_JAVA { JNIEnv* env = jniEnv; jniEnv = 0; #define END_JAVA jniEnv = env; } #define BEGIN_CALL \ BEGIN_JAVA \ - if((*env)->MonitorExit(env, s_threadLock) < 0) \ + if(s_doMonitorOps && ((*env)->MonitorExit(env, s_threadLock) < 0)) \ elog(ERROR, "Java exit monitor failure"); #define END_CALL endCall(env); } @@ -108,7 +120,7 @@ static void endCall(JNIEnv* env) if(exh != 0) (*env)->ExceptionClear(env); - if((*env)->MonitorEnter(env, s_threadLock) < 0) + if(s_doMonitorOps && ((*env)->MonitorEnter(env, s_threadLock) < 0)) elog(ERROR, "Java enter monitor failure"); jniEnv = env; @@ -155,6 +167,15 @@ static void endCallMonitorHeld(JNIEnv* env) bool beginNativeNoErrCheck(JNIEnv* env) { + if ( s_refuseOtherThreads && env != primordialJNIEnv ) + { + env = JNI_setEnv(env); + Exception_throw(ERRCODE_INTERNAL_ERROR, + "Attempt by non-initial thread to enter PostgreSQL from Java"); + JNI_setEnv(env); + return false; + } + if((env = JNI_setEnv(env)) != 0) { /* The backend is *not* awaiting the return of a call to the JVM @@ -604,7 +625,10 @@ jint JNI_createVM(JavaVM** javaVM, JavaVMInitArgs* vmArgs) JNIEnv* env = 0; jint jstat = pljava_createvm(javaVM, (void **)&env, vmArgs); if(jstat == JNI_OK) + { jniEnv = env; + primordialJNIEnv = env; + } return jstat; } diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index cab3d6ff..7b37a9e0 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -60,6 +60,25 @@ extern jmethodID UnsupportedOperationException_init; extern jclass NoSuchMethodError_class; +/* + * Method called from Backend.c to set the thread policy. The first parameter + * indicates whether to throw an exception if a thread other than the main one + * tries to use BEGIN_NATIVE. The second indicates whether JNI calls should try + * to release the "threadlock" monitor when calling into Java and reacquire it + * on return. If false, the monitor will be held forever, blocking any other + * Java thread that tries to use the synchronized native methods. So, the + * combinations are: + * false, true: PL/Java's historical behavior: monitor is released/reacquired, + * other threads allowed into PG when the main thread is in Java. + * true, true: Useful for checking whether application code has any other + * threads that try to enter PG; they will incur exceptions. + * true, false: Useful in production if all PG access is known to be done on + * the main thread only; other threads that try will simply block + * (JConsole can show them) rather that incurring exceptions; many + * monitor operations eliminated. + */ +extern void pljava_JNI_setThreadPolicy(bool,bool); + /* * A few very specialized JNI method-invocation wrappers, that do NOT do * one thing all the rest of the method wrappers do. These do NOT release the diff --git a/src/site/markdown/use/variables.md b/src/site/markdown/use/variables.md index 08d782b1..2eb02760 100644 --- a/src/site/markdown/use/variables.md +++ b/src/site/markdown/use/variables.md @@ -52,6 +52,30 @@ These PostgreSQL configuration variables can influence PL/Java's operation: directly in a `SET` command, while in 11 and after, such a value needs to be a (single-quoted) string explicitly containing the double quotes._ +`pljava.java_thread_pg_entry` +: A choice of `allow`, `error`, or `block` controlling PL/Java's thread + management. Java makes heavy use of threading, while PostgreSQL may not be + accessed by multiple threads concurrently. PL/Java's historical behavior is + `allow`, which serializes access by Java threads into PostgreSQL, allowing + a different Java thread in only when the current one calls or returns into + Java. PL/Java formerly made some use of Java object finalizers, which + required this approach, as finalizers run in their own thread. + + PL/Java itself no longer requires the ability for any thread to access + PostgreSQL other than the original main thread. User code developed for + PL/Java, however, may still rely on that ability. To test whether it does, + the `error` setting can be used here, and any attempt by a Java thread other + than the main one to enter PostgreSQL will incur an exception (and stack + trace, written to the server's standard error channel). When confident that + there is no code that will need to enter PostgreSQL except on the main + thread, the `block` setting can be used. That will eliminate PL/Java's + frequent lock acquisitions and releases when the main thread crosses between + PostgreSQL and Java, and will simply indefinitely block any other Java + thread that attempts to enter PostgreSQL. This is an efficient setting, but + can lead to blocked threads or a deadlocked backend if used with code that + does attempt to access PG from more than one thread. (A JMX client, like + JConsole, can identify the blocked threads, should that occur.) + `pljava.libjvm_location` : Used by PL/Java to load the Java runtime. The full path to a `libjvm` shared object (filename typically ending with `.so`, `.dll`, or `.dylib`). From 66c24104af4ac1a8938678f8067a2d56b7200adb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 8 Mar 2019 12:11:18 -0500 Subject: [PATCH 0263/1087] Minor cleanup. Remove an #undef directive left over from a discarded implementation approach, and give a method parameter a clearer name than it was left with after former cut-n-paste. --- pljava-so/src/main/c/Backend.c | 1 - .../main/java/org/postgresql/pljava/internal/DualState.java | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 36b63a3e..28b6ee74 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -423,7 +423,6 @@ ASSIGNENUMHOOK(java_thread_pg_entry) ASSIGNRETURNIFCHECK(ENUMHOOKRET); pljava_JNI_setThreadPolicy( !!(val&1) /*error*/, !(val&2) /*monitorops*/); ASSIGNRETURN(ENUMHOOKRET); -#undef RETVAL } /* diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 74956140..86bb6036 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -1231,10 +1231,10 @@ final void construct(long scoped) enlistedUnscoped += 1L - scoped; } - final void resourceOwnerPoll(long delist, long total) + final void resourceOwnerPoll(long released, long total) { ++ resourceOwnerPasses; - nativeReleased += delist; + nativeReleased += released; delistedScoped += total; } From cfb9e7fd2de271a4210c45ab11b3a8d6123271a8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 8 Mar 2019 12:07:10 -0500 Subject: [PATCH 0264/1087] Simplify "thread ID" tracking. The mechanism for fudging the stack base to mollify PostgreSQL's stack depth check when the Java thread executing in PG changes (only possible when pljava.java_thread_pg_entry is 'allow') has long involved computing the System.identityHashCode() of the current Thread object, and passing that to the native code to be compared to that of the last known thread running in PG code. That was unnecessarily roundabout, given that every native method call passes a JNIEnv pointer to the native code, which the JNI spec "design overview" clarifies to be valid only per thread, while guaranteeing to pass the same pointer in calls from the same thread, so it has the properties needed of an ID. --- pljava-so/src/main/c/Backend.c | 5 ++-- pljava-so/src/main/c/ExecutionPlan.c | 26 +++++++++---------- pljava-so/src/main/c/JNICalls.c | 3 +++ pljava-so/src/main/c/SPI.c | 10 +++---- pljava-so/src/main/c/type/Portal.c | 16 ++++++------ pljava-so/src/main/include/pljava/pljava.h | 6 ++--- .../pljava/internal/ExecutionPlan.java | 17 +++++------- .../postgresql/pljava/internal/Portal.java | 15 ++++------- .../org/postgresql/pljava/internal/SPI.java | 6 ++--- .../postgresql/pljava/internal/Session.java | 21 ++++++++------- 10 files changed, 60 insertions(+), 65 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 28b6ee74..76c4879d 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -92,7 +92,6 @@ extern PLJAVADLLEXPORT void _PG_init(void); #define LOCAL_REFERENCE_COUNT 128 MemoryContext JavaMemoryContext; -jlong mainThreadId; static JavaVM* s_javaVM = 0; static jclass s_Backend_class; @@ -1320,8 +1319,8 @@ static void addUserJVMOptions(JVMOptList* optList) static void initJavaSession(void) { jclass sessionClass = PgObject_getJavaClass("org/postgresql/pljava/internal/Session"); - jmethodID init = PgObject_getStaticJavaMethod(sessionClass, "init", "()J"); - mainThreadId = JNI_callStaticLongMethod(sessionClass, init); + jmethodID init = PgObject_getStaticJavaMethod(sessionClass, "init", "()V"); + JNI_callStaticVoidMethod(sessionClass, init); JNI_deleteLocalRef(sessionClass); if(JNI_exceptionCheck()) diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index 5df0847f..1846f806 100644 --- a/pljava-so/src/main/c/ExecutionPlan.c +++ b/pljava-so/src/main/c/ExecutionPlan.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -46,7 +46,7 @@ void ExecutionPlan_initialize(void) { { "_cursorOpen", - "(JJLjava/lang/String;[Ljava/lang/Object;S)Lorg/postgresql/pljava/internal/Portal;", + "(JLjava/lang/String;[Ljava/lang/Object;S)Lorg/postgresql/pljava/internal/Portal;", Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen }, { @@ -56,12 +56,12 @@ void ExecutionPlan_initialize(void) }, { "_execute", - "(JJ[Ljava/lang/Object;SI)I", + "(J[Ljava/lang/Object;SI)I", Java_org_postgresql_pljava_internal_ExecutionPlan__1execute }, { "_prepare", - "(JLjava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)J", + "(Ljava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)J", Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare }, { @@ -128,17 +128,17 @@ static bool coerceObjects(void* ePlan, jobjectArray jvalues, Datum** valuesPtr, /* * Class: org_postgresql_pljava_internal_ExecutionPlan * Method: _cursorOpen - * Signature: (JJLjava/lang/String;[Ljava/lang/Object;S)Lorg/postgresql/pljava/internal/Portal; + * Signature: (JLjava/lang/String;[Ljava/lang/Object;S)Lorg/postgresql/pljava/internal/Portal; */ JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jstring cursorName, jobjectArray jvalues, jshort readonly_spec) +Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jclass clazz, jlong _this, jstring cursorName, jobjectArray jvalues, jshort readonly_spec) { jobject jportal = 0; if(_this != 0) { BEGIN_NATIVE STACK_BASE_VARS - STACK_BASE_PUSH(threadId) + STACK_BASE_PUSH(env) PG_TRY(); { Ptr2Long p2l; @@ -214,17 +214,17 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1isCursorPlan(JNIEnv* env, jc /* * Class: org_postgresql_pljava_internal_ExecutionPlan * Method: _execute - * Signature: (JJ[Ljava/lang/Object;SI)V + * Signature: (J[Ljava/lang/Object;SI)V */ JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1execute(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jobjectArray jvalues, jshort readonly_spec, jint count) +Java_org_postgresql_pljava_internal_ExecutionPlan__1execute(JNIEnv* env, jclass clazz, jlong _this, jobjectArray jvalues, jshort readonly_spec, jint count) { jint result = 0; if(_this != 0) { BEGIN_NATIVE STACK_BASE_VARS - STACK_BASE_PUSH(threadId) + STACK_BASE_PUSH(env) PG_TRY(); { Ptr2Long p2l; @@ -264,15 +264,15 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1execute(JNIEnv* env, jclass /* * Class: org_postgresql_pljava_internal_ExecutionPlan * Method: _prepare - * Signature: (JLjava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)J; + * Signature: (Ljava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)J; */ JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass clazz, jlong threadId, jstring jcmd, jobjectArray paramTypes) +Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass clazz, jstring jcmd, jobjectArray paramTypes) { jlong result = 0; BEGIN_NATIVE STACK_BASE_VARS - STACK_BASE_PUSH(threadId) + STACK_BASE_PUSH(env) PG_TRY(); { char* cmd; diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 1c1cc896..fbd71d12 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -25,6 +25,8 @@ JNIEnv* jniEnv; jint (JNICALL *pljava_createvm)(JavaVM **, void **, void *); +void* mainThreadId; /* declared in pljava.h */ + static JNIEnv* primordialJNIEnv; static jobject s_threadLock; @@ -628,6 +630,7 @@ jint JNI_createVM(JavaVM** javaVM, JavaVMInitArgs* vmArgs) { jniEnv = env; primordialJNIEnv = env; + mainThreadId = env; } return jstat; } diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index 6018e055..5632dee2 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -31,7 +31,7 @@ void SPI_initialize(void) JNINativeMethod methods[] = { { "_exec", - "(JLjava/lang/String;I)I", + "(Ljava/lang/String;I)I", Java_org_postgresql_pljava_internal_SPI__1exec }, { @@ -65,10 +65,10 @@ void SPI_initialize(void) /* * Class: org_postgresql_pljava_internal_SPI * Method: _exec - * Signature: (JLjava/lang/String;I)I + * Signature: (Ljava/lang/String;I)I */ JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_SPI__1exec(JNIEnv* env, jclass cls, jlong threadId, jstring cmd, jint count) +Java_org_postgresql_pljava_internal_SPI__1exec(JNIEnv* env, jclass cls, jstring cmd, jint count) { jint result = 0; @@ -77,7 +77,7 @@ Java_org_postgresql_pljava_internal_SPI__1exec(JNIEnv* env, jclass cls, jlong th if(command != 0) { STACK_BASE_VARS - STACK_BASE_PUSH(threadId) + STACK_BASE_PUSH(env) PG_TRY(); { Invocation_assertConnect(); diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index e1b4a4b3..da702f0b 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -120,7 +120,7 @@ void Portal_initialize(void) }, { "_fetch", - "(JJZJ)J", + "(JZJ)J", Java_org_postgresql_pljava_internal_Portal__1fetch }, { @@ -140,7 +140,7 @@ void Portal_initialize(void) }, { "_move", - "(JJZJ)J", + "(JZJ)J", Java_org_postgresql_pljava_internal_Portal__1move }, { 0, 0, 0 } @@ -178,10 +178,10 @@ Java_org_postgresql_pljava_internal_Portal__1getPortalPos(JNIEnv* env, jclass cl /* * Class: org_postgresql_pljava_internal_Portal * Method: _fetch - * Signature: (JJZJ)J + * Signature: (JZJ)J */ JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jboolean forward, jlong count) +Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jlong _this, jboolean forward, jlong count) { jlong result = 0; if(_this != 0) @@ -189,7 +189,7 @@ Java_org_postgresql_pljava_internal_Portal__1fetch(JNIEnv* env, jclass clazz, jl BEGIN_NATIVE Ptr2Long p2l; STACK_BASE_VARS - STACK_BASE_PUSH(threadId) + STACK_BASE_PUSH(env) /* * One call to cleanEnqueued... is made in Invocation_popInvocation, @@ -336,10 +336,10 @@ Java_org_postgresql_pljava_internal_Portal__1isAtEnd(JNIEnv* env, jclass clazz, /* * Class: org_postgresql_pljava_internal_Portal * Method: _move - * Signature: (JJZJ)J + * Signature: (JZJ)J */ JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_Portal__1move(JNIEnv* env, jclass clazz, jlong _this, jlong threadId, jboolean forward, jlong count) +Java_org_postgresql_pljava_internal_Portal__1move(JNIEnv* env, jclass clazz, jlong _this, jboolean forward, jlong count) { jlong result = 0; if(_this != 0) @@ -347,7 +347,7 @@ Java_org_postgresql_pljava_internal_Portal__1move(JNIEnv* env, jclass clazz, jlo BEGIN_NATIVE Ptr2Long p2l; STACK_BASE_VARS - STACK_BASE_PUSH(threadId) + STACK_BASE_PUSH(env) p2l.longVal = _this; PG_TRY(); diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index e132360a..44c691e3 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -91,7 +91,7 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); * that initially called Java finally returns, the intended longjmp (the one * to the original value of Warn_restart) must be made. */ -extern jlong mainThreadId; +extern void* mainThreadId; extern bool pljavaEntryFence(JNIEnv* env); extern JNIEnv* currentJNIEnv; extern MemoryContext JavaMemoryContext; @@ -145,7 +145,7 @@ char* stack_base_ptr; #endif #define STACK_BASE_VARS \ - jlong saveMainThreadId = 0; \ + void* saveMainThreadId = 0; \ _STACK_BASE_TYPE saveStackBasePtr; #define STACK_BASE_PUSH(threadId) \ diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java index 7a2293a9..10340740 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -164,8 +164,7 @@ public Portal cursorOpen( { synchronized(Backend.THREADLOCK) { - return _cursorOpen(m_pointer, System.identityHashCode(Thread - .currentThread()), cursorName, parameters, read_only); + return _cursorOpen(m_pointer, cursorName, parameters, read_only); } } @@ -205,8 +204,7 @@ public int execute(Object[] parameters, short read_only, int rowCount) { synchronized(Backend.THREADLOCK) { - return _execute(m_pointer, System.identityHashCode(Thread - .currentThread()), parameters, read_only, rowCount); + return _execute(m_pointer, parameters, read_only, rowCount); } } @@ -232,24 +230,23 @@ public static ExecutionPlan prepare(String statement, Oid[] argTypes) { synchronized(Backend.THREADLOCK) { - plan = new ExecutionPlan(key, _prepare( - System.identityHashCode(Thread.currentThread()), statement, argTypes)); + plan = new ExecutionPlan(key, _prepare(statement, argTypes)); } } return plan; } - private static native Portal _cursorOpen(long pointer, long threadId, + private static native Portal _cursorOpen(long pointer, String cursorName, Object[] parameters, short read_only) throws SQLException; private static native boolean _isCursorPlan(long pointer) throws SQLException; - private static native int _execute(long pointer, long threadId, + private static native int _execute(long pointer, Object[] parameters, short read_only, int rowCount) throws SQLException; - private static native long _prepare(long threadId, String statement, Oid[] argTypes) + private static native long _prepare(String statement, Oid[] argTypes) throws SQLException; private static native void _invalidate(long pointer); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java index 79b8e734..0617aa38 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -99,10 +99,7 @@ public long fetch(boolean forward, long count) { synchronized(Backend.THREADLOCK) { - long fetched = - _fetch(m_pointer, - System.identityHashCode(Thread.currentThread()), - forward, count); + long fetched = _fetch(m_pointer, forward, count); if ( fetched < 0 ) throw new ArithmeticException( "fetched too many rows to report in a Java signed long"); @@ -158,9 +155,7 @@ public long move(boolean forward, long count) { synchronized(Backend.THREADLOCK) { - long moved = _move(m_pointer, - System.identityHashCode(Thread.currentThread()), - forward, count); + long moved = _move(m_pointer, forward, count); if ( moved < 0 ) throw new ArithmeticException( "moved too many rows to report in a Java signed long"); @@ -177,7 +172,7 @@ private static native long _getPortalPos(long pointer) private static native TupleDesc _getTupleDesc(long pointer) throws SQLException; - private static native long _fetch(long pointer, long threadId, boolean forward, long count) + private static native long _fetch(long pointer, boolean forward, long count) throws SQLException; private static native void _close(long pointer); @@ -188,6 +183,6 @@ private static native boolean _isAtEnd(long pointer) private static native boolean _isAtStart(long pointer) throws SQLException; - private static native long _move(long pointer, long threadId, boolean forward, long count) + private static native long _move(long pointer, boolean forward, long count) throws SQLException; } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java index a392fd5f..61346c18 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -57,7 +57,7 @@ private static int exec(String command, int rowCount) { synchronized(Backend.THREADLOCK) { - return _exec(System.identityHashCode(Thread.currentThread()), command, rowCount); + return _exec(command, rowCount); } } @@ -187,7 +187,7 @@ public static String getResultText(int resultCode) } @Deprecated - private native static int _exec(long threadId, String command, int rowCount); + private native static int _exec(String command, int rowCount); private native static long _getProcessed(); private native static int _getResult(); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java index 60103ff2..f96a1b7a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -207,15 +213,10 @@ public String getOuterUserSchema() /** * Called from native code when the JVM is instantiated. */ - static long init() + static void init() throws SQLException { ELogHandler.init(); - - // Should be replace with a Thread.getId() once we abandon - // Java 1.4 - // - return System.identityHashCode(Thread.currentThread()); } private static native boolean _setUser(AclId userId, boolean isLocalChange); From 3310c1443da01f2fa08c79c09f628b871540ff09 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Apr 2019 14:34:12 -0400 Subject: [PATCH 0265/1087] I've had it with these typos. Also, there was an orphaned comment in pljava.h describing the field that was no longer there, but in Invocation.h. The comment for it in Invocation.h is adequate, so the one in pljava.h is removed. --- pljava-so/src/main/c/Exception.c | 2 +- pljava-so/src/main/c/Invocation.c | 6 +++--- pljava-so/src/main/c/JNICalls.c | 2 +- pljava-so/src/main/c/type/Portal.c | 2 +- pljava-so/src/main/include/pljava/Invocation.h | 2 +- pljava-so/src/main/include/pljava/pljava.h | 9 --------- .../java/org/postgresql/pljava/internal/ErrorData.java | 6 +++--- 7 files changed, 10 insertions(+), 19 deletions(-) diff --git a/pljava-so/src/main/c/Exception.c b/pljava-so/src/main/c/Exception.c index 23b3c0ad..3113a774 100644 --- a/pljava-so/src/main/c/Exception.c +++ b/pljava-so/src/main/c/Exception.c @@ -163,7 +163,7 @@ void Exception_throw_ERROR(const char* funcName) FlushErrorState(); ex = JNI_newObject(ServerException_class, ServerException_init, ed); - currentInvocation->errorOccured = true; + currentInvocation->errorOccurred = true; elog(DEBUG2, "Exception in function %s", funcName); diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 9aaced65..2f181941 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -107,7 +107,7 @@ void Invocation_pushBootContext(Invocation* ctx) ctx->trusted = false; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; - ctx->errorOccured = false; + ctx->errorOccurred = false; ctx->inExprContextCB = false; ctx->previous = 0; #if PG_VERSION_NUM >= 100000 @@ -131,7 +131,7 @@ void Invocation_pushInvocation(Invocation* ctx, bool trusted) ctx->trusted = trusted; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; - ctx->errorOccured = false; + ctx->errorOccurred = false; ctx->inExprContextCB = false; ctx->previous = currentInvocation; #if PG_VERSION_NUM >= 100000 @@ -226,7 +226,7 @@ Java_org_postgresql_pljava_jdbc_Invocation__1getCurrent(JNIEnv* env, jclass cls) JNIEXPORT void JNICALL Java_org_postgresql_pljava_jdbc_Invocation__1clearErrorCondition(JNIEnv* env, jclass cls) { - currentInvocation->errorOccured = false; + currentInvocation->errorOccurred = false; } /* diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index fbd71d12..3c7fbb9d 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -202,7 +202,7 @@ bool beginNative(JNIEnv* env) return false; } - if(currentInvocation->errorOccured) + if(currentInvocation->errorOccurred) { /* An elog with level higher than ERROR was issued. The transaction * state is unknown. There's no way the JVM is allowed to enter the diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index da702f0b..60e9e099 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -291,7 +291,7 @@ Java_org_postgresql_pljava_internal_Portal__1close(JNIEnv* env, jclass clazz, jl if(portal->cleanup == _pljavaPortalCleanup) portal->cleanup = s_originalCleanupProc; - if(!(currentInvocation->errorOccured || currentInvocation->inExprContextCB)) + if(!(currentInvocation->errorOccurred || currentInvocation->inExprContextCB)) SPI_cursor_close(portal); END_NATIVE } diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index b0e9eb4b..c174d693 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -67,7 +67,7 @@ struct Invocation_ * be prevented until this flag is reset (by a rollback * of a savepoint or function exit). */ - bool errorOccured; + bool errorOccurred; #if PG_VERSION_NUM >= 100000 /** diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index 44c691e3..e9b82373 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -82,15 +82,6 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); #define PG_INT32_MAX (0x7FFFFFFF) #endif - -/* The errorOccured will be set when a call from Java into one of the - * backend functions results in a elog that causes a longjmp (Levels >= ERROR) - * that was trapped using the PLJAVA_TRY/PLJAVA_CATCH macros. - * When this happens, all further calls from Java must be blocked since the - * state of the current transaction is unknown. Further more, once the function - * that initially called Java finally returns, the intended longjmp (the one - * to the original value of Warn_restart) must be made. - */ extern void* mainThreadId; extern bool pljavaEntryFence(JNIEnv* env); extern JNIEnv* currentJNIEnv; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java index b174f430..92fccca7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java @@ -117,7 +117,7 @@ public boolean isShowFuncname() } /** - * Returns The file where the error occured + * Returns The file where the error occurred */ public String getFilename() { @@ -128,7 +128,7 @@ public String getFilename() } /** - * Returns The line where the error occured + * Returns The line where the error occurred */ public int getLineno() { @@ -139,7 +139,7 @@ public int getLineno() } /** - * Returns the name of the function where the error occured + * Returns the name of the function where the error occurred */ public String getFuncname() { From a15892109ea7ebe54e19ce49faf404a97888d9aa Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Apr 2019 14:20:00 -0400 Subject: [PATCH 0266/1087] New approach to concurrency for DualState. To date, Java synchronized blocks have been made to serve the purpose of concurrency management for DualState objects, but the fit has been awkward at best. If the concurrency needs for DualState are viewed on a clean slate: - a DualState mates up some state in PostgreSQL (with lifetime/validity determined by PostgreSQL mechanisms like resource owners and memory contexts) with some Java state (with lifetime/validity determined by Java mechanisms). - Java can have many threads, but PostgreSQL only supports one (or one at a time, depending on the pljava.java_thread_pg_entry setting). - JNI calls that will enter PostgreSQL routines must be serialized onto one thread (or one at a time), but direct examination of a chunk of native memory, or even JNI calls to thread-safe, non-PostgreSQL routines, ought to be permitted from other threads, as long as the native state being examined is expected not to change during the access. - That requires mechanisms for not just data visibility across threads, but for exclusion. A thread must be able to 'pin' some native state briefly while peeking at it, and any code that handles a PostgreSQL callback that is about to change or release native state must use a locking operation that will block for any existing pin. That leads to a structure like a vanilla reader/writer lock, where the read lock (hereafter called a 'pin') can be taken by any number of threads, while the write lock (hereafter simply 'lock') can only be held by one thread, is blocked by any existing pins, and blocks any later requested pins. For this application, a further simplification is that it makes sense to limit all operations that could need the lock to run on one particular thread (the one for which Backend.threadMayEnterPG() would return true), so there is no need to support multiple conflicting requests for locks. Every DualState instance needs to remember when its native state has been invalidated, or its Java state has been explicitly freed or found unreachable. These conditions can be checked atomically as part of any pin request, so pin() will throw the appropriate exceptions if the state should not be accessed, saving client code some extra checks and TOCTTOU races. If the pin succeeds, access to the state is ok while it is held. The Java synchronization mechanisms offered in java.util.concurrent do not include anything with exactly these behaviors, but do include the lower-level primitives on which it can be built, as is done here. --- .../postgresql/pljava/internal/Backend.java | 19 + .../postgresql/pljava/internal/DualState.java | 1858 +++++++++++------ 2 files changed, 1275 insertions(+), 602 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index 2faec5f7..c6fc1541 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -47,6 +47,25 @@ public static synchronized Session getSession() return s_session; } + /** + * Return true if the current thread may JNI-call into Postgres. + *

    + * In PL/Java's threading model, only one thread (or only one thread at a + * time, depending on the setting of {@code pljava.java_thread_pg_entry}) + * may make calls into the native PostgreSQL code. + *

    + * Note: The setting {@code pljava.java_thread_pg_entry=error} is an + * exception; under that setting this method will return true for any + * thread that acquires the {@code THREADLOCK} monitor, but any such thread + * that isn't the actual original PG thread will have an exception thrown + * if it calls into PG. + * @return true if the current thread is the one prepared to enter PG. + */ + public static boolean threadMayEnterPG() + { + return Thread.holdsLock(THREADLOCK); + } + /** * Returns the configuration option as read from the Global * Unified Config package (GUC). diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 86bb6036..2c9ca659 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -16,9 +16,17 @@ import java.sql.SQLException; -import java.util.IdentityHashMap; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.Map; +import java.util.Queue; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import static java.util.concurrent.locks.LockSupport.park; +import static java.util.concurrent.locks.LockSupport.unpark; import static java.lang.management.ManagementFactory.getPlatformMBeanServer; import javax.management.ObjectName; @@ -44,15 +52,24 @@ * associated state should be released. *

  • Its associated native state is released or invalidated (such as by exit * of a corresponding context). If the object is still reachable from Java, it - * must be marked to throw an exception for any future attempted access to its + * must throw an exception for any future attempted access to its * native state.
  • * *

    + * A subclass overrides the {@link #javaStateReleased javaStateReleased}, + * {@link #javaStateUnreachable javaStateUnreachable}, or + * {@link #nativeStateReleased nativeStateReleased} methods, respectively, + * to add behavior for those life cycle events. + *

    + * A subclass calls {@link #releaseFromJava releaseFromJava} to signal an event + * of the first kind. Events of the second kind are, naturally, detected by the + * Java garbage collector. To detect events of the third kind, a resource owner + * must be associated with the instance. + *

    * A parameter to the {@code DualState} constructor is a {@code ResourceOwner}, - * a PostgreSQL implementation concept introduced in PG 8.0. Instances will be - * called at their {@link #nativeStateReleased nativeStateReleased} methods - * when the corresponding {@code ResourceOwner} is released in PostgreSQL, if - * they are still reachable in Java. + * a PostgreSQL implementation concept introduced in PG 8.0. A + * {@code nativeStateReleased} event occurs when the corresponding + * {@code ResourceOwner} is released in PostgreSQL. *

    * However, this class does not require the {@code resourceOwner} parameter to * be, in all cases, a pointer to a PostgreSQL {@code ResourceOwner}. It is @@ -66,95 +83,164 @@ * address of the {@code Invocation} can be used. Such references can be * considered "generalized" resource owners. *

    - * Instances will be enqueued on a {@link ReferenceQueue} when found by the Java - * garbage collector to be unreachable. The - * {@link #cleanEnqueuedInstances cleanEnqueuedInstances} static method will - * call those instances at their - * {@link #javaStateUnreachable javaStateUnreachable} methods if the weak - * reference has already been cleared by the garbage collector. The method - * should clean up any lingering native state. + * Java code may execute in multiple threads, but PostgreSQL is not + * multi-threaded; at any given time, there is no more than one thread that may + * safely make JNI calls into PostgreSQL routines (for that thread, + * {@code Backend.threadMayEnterPG()} returns true). Depending on the setting of + * the {@code pljava.java_thread_pg_entry} PostgreSQL configuration variable, + * that may be the same one thread for the duration of a session, or it may be + * possible for one thread to relinquish that status and another thread to take + * it: for the {@code pljava.java_thread_pg_entry} setting {@code allow}, the + * status is represented by holding the object monitor on + * {@code Backend.THREADLOCK}, and {@code Backend.threadMayEnterPG()} returns + * true for whatever thread holds it. Under that setting, there can be moments + * when {@code Backend.threadMayEnterPG()} is not true for any thread, if one + * has released the monitor and no other thread has yet acquired it. For brevity + * in what follows, "the PG thread" will be used to mean whatever thread, at a + * given moment, would observe {@code Backend.threadMayEnterPG()} to return + * true. + *

    + * Some methods of {@code DualState} and subclasses may be called from any Java + * thread, while some must be called from the PG thread. The life-cycle + * callbacks, {@code javaStateReleased}, {@code javaStateUnreachable}, and + * {@code nativeStateReleased}, are called by the implementation, and always + * on the PG thread. + *

    + * The Java Memory Model imposes strict conditions for updates to memory state + * made in one thread to be visible to other threads. Methods that are known to + * be called only on the PG thread can sidestep those complexities, at least + * to the extent that they manipulate only data structures not accessed in other + * threads. This is true even under the {@code pljava.java_thread_pg_entry} + * setting {@code allow}, where "the PG thread" may not always be the same + * thread. Because a Java synchronization event is involved whenever + * "the PG thread" changes, unbroken visibility is assured, just as it would be + * in one unchanging thread, so one can say "the PG thread" for convenience and + * without loss of generality. *

    - * As the native cleanup is likely to involve calls into PostgreSQL, to reduce - * thread contention, {@code cleanEnqueuedInstances} should be called in one or - * more likely places from a thread already known to be entering/exiting Java - * from/to PostgreSQL. + * For the {@code nativeStateReleased} lifecycle event, rules for memory + * visibility are not enough; a mechanism for mutual exclusion is needed. The + * callback is made on the PG thread from PostgreSQL code that is in the process + * of invalidating the native state, and will do so once the callback returns. + * If any other Java thread is actively referring to that native state, there is + * no choice but to block the PG thread making the callback until such other + * threads are no longer relying on the native state. *

    - * If convenient, explicit close actions from Java can be handled similarly, - * by having the close method call {@link #enqueue enqueue}, and providing a - * {@link #javaStateReleased javaStateReleased} method, which will be called by - * {@code cleanEnqueuedInstances} if the weak reference is nonnull, indicating - * the instance was enqueued explicitly rather than by the garbage collector. + * To that end, the {@link #pin pin} and {@link #unpin unpin} methods are + * provided, and must be used to surround any block of code that accesses the + * native state: + *

    + *pin();
    + *try
    + *{
    + *    ... code that dereferences or relies on
    + *    a valid native state ...
    + *}
    + *finally
    + *{
    + *    unpin();
    + *}
    + *
    *

    - * Alternatively, a close action from Java can be handled by calling the - * {@code javaStateReleased} method directly, rather than depending on the - * reference queue. In that case, the {@code javaStateReleased} method must call - * its {@code super} implementation in this class, to take care of removing the - * instance from the data structures tracking it here. In contrast, a - * {@code javaStateReleased} method that is intended to be called via the - * reference queue should not call {@code super}; the reference-queue - * processing loop will already have removed the instance from the data - * structures before calling the method. + * Pins are lightweight, nonexclusive (any number of threads may simultaneously + * pin the same {@code DualState} instance), and reentrant (a single thread may + * obtain and release nested pins on the same instance). The code protected by a + * pin is ideally a short sequence representing a simple operation (reading a + * value, or refilling a small buffer with data) on the native state. The chief + * purpose of holding a pin is to hold off the possible invalidation of the + * native state until the pin is released. *

    - * The convention of calling {@code javaStateReleased} vie the reference queue - * is likely to be most often what's wanted, as the method may need to make - * native calls to release native state, and processing of the reference queue - * will always take place on a thread in the proper state for doing that. + * If either the native state or the Java state has been released already (by + * the resource owner callback or an explicit call to {@code releaseFromJava}, + * respectively), {@code pin()} will detect that and throw the appropriate + * exception. Otherwise, the state is safe to make use of until {@code unpin}. + * A subclass can customize the messages or {@code SQLSTATE} codes for the + * exceptions {@code pin()} may throw, by overriding one or more of + * {@link #identifierForMessage identifierForMessage}, + * {@link #invalidMessage invalidMessage}, + * {@link #releasedMessage releasedMessage}, + * {@link #invalidSqlState invalidSqlState}, or + * {@link #releasedSqlState releasedSqlState}. + *

    + * Code that holds a pin may safely act on components of the native state from + * any thread, so long as the actions do not include native calls to PostgreSQL + * routines (directly or transitively). Access to the native memory through a + * direct byte buffer would be a permitted example, or even calls to JNI methods + * to retrieve fields from C {@code struct}s or chase pointers through a data + * structure, as long as only thread-safe routines from the C runtime are called + * and no routines of PostgreSQL itself, and as long as the memory or structure + * being accessed is known to be safe from modification by PostgreSQL while the + * pin is held. If the future, PL/Java may one day have an annotation that can + * be used to mark native methods that satisfy these limits; at present, there + * has been no effort to segregate them into those that do and those that don't. + * Native methods that may (under any circumstances!) invoke PG routines must + * be invoked on the PG thread. + *

    + * The exclusive counterparts to {@code pin} and {@code unpin} are + * {@link #lock lock} and {@link #unlock(int,boolean) unlock}, which are not + * expected to be used as widely. The chief use of {@code lock}/{@code unlock} + * is around the call to {@code nativeStateReleased} when handling a resource + * owner callback from PostgreSQL. They can be used in subclasses to surround + * modifications to the state, as needed. A {@code lock} will block until all + * earlier-acquired pins are released; subsequent pins block until the lock is + * released. Only the PG thread may use {@code lock}/{@code unlock}. An + * {@code upgrade} argument to {@code lock} allows the lock to be acquired + * when the PG thread already holds a pin; it should be specified + * only when inspection of the code identifies a nearby enclosing pin and + * confirms that the planned locked actions will not break the pinning code's + * assumptions. Pins can be freely acquired by the PG thread while it holds a + * lock; the coding convention's strict nesting assures they will all be + * released before the lock is. + *

    + * In an explicit call to {@code releaseFromJava}, which may be made from any + * thread, the instance is immediately, atomically, flagged as released. No + * subsequent pin will succeed. Pins already held are unaffected, so there must + * be no changes made to the state, at the time {@code releaseFromJava} is + * called, that could confuse any code that already holds a pin and is relying + * on the state. Such changes must be made in the {@code javaStateReleased} + * callback, which will execute only after release of the last pin, if any, and + * always on the PG thread. If the last pin is released by a thread other than + * the PG thread, the callback does not execute immediately, but via a queue + * that is polled from the PG thread at convenient points. + *

    + * Instances whose referents are found unreachable by Java's garbage collector + * are placed on the same queue, so their {@code javaStateUnreachable} callbacks + * will be executed on the PG thread when the queue is polled. The callbacks + * should clean up any lingering native state. + *

    + * As the callbacks are executed on the PG thread, any native calls they may + * need to make into PostgreSQL are allowed without extra ceremony. *

    * There are different abstract subclasses of {@code DualState} that wrap * different sorts of PostgreSQL native state, and encapsulate what needs to be - * done when such state is released from the Java or native side. Each subclass - * needs to provide an appropriately-typed {@code protected} method for - * obtaining a reference to the wrapped native state, first verifying that it is - * still valid. More such subclasses can be added as needed. + * done when such state is released from the Java or native side. More such + * subclasses can be added as needed. *

    * A client class of {@code DualState} will typically contain a static nested * class that further extends one of these abstract subclasses, and the client * instance will hold a strong reference to an instance of that - * {@code DualState} subclass constructed at the same time. The client class - * must synchronize on the nested state class instance whenever calling - * the methods that check validity or return native state references, and must - * hold that monitor for the duration of any activity that depends on that - * validity or reference result. A client class may not retain such a - * reference after exiting the synchronized section, but must synchronize and - * obtain it again when next needed. + * {@code DualState} subclass constructed at the same time. *

    - * The data structures used internally in this class to track - * created instances through their life cycles are not synchronized or + * This class uses some private data structures, to track + * created instances through their life cycles, that are not synchronized or * thread-safe. The design rests on the following requirements: *

      *
    • The structures are only traversed or modified during: *
        *
      • Instance construction *
      • Reference queue processing (instances found unreachable by Java's - * garbage collector, or {@code enqueue}d directly as an explicit means of - * release) + * garbage collector, or enqueued following {@code releaseFromJava}) *
      • Exit of a resource owner's scope - *
      • Direct call of {@code javaStateReleased} (i.e., not via the reference - * queue). *
      - *
    • PL/Java uses synchronization to control which thread is entitled to - * interact with PostgreSQL native code. Historically, native access has not - * been restricted to only one thread, but to only one thread at a time. A - * different thread can obtain the lock to call into PostgreSQL only when the - * thread currently holding it has called or returned into Java. + *
    • There is only one PG thread, or only one at a time. *
    • Construction of any {@code DualState} instance is to take place only on - * a thread that holds the lock for native access. The requirement to pass any + * the PG thread. The requirement to pass any * constructor a {@code DualState.Key} instance, obtainable by native code, is - * intended to reinforce that convention. It is not abuse-proof: Java code could - * retain a {@code Key} reference and reuse it on a thread without the lock, but - * that would be a deliberate coding error. + * intended to reinforce that convention. It is not abuse-proof, or intended as + * a security mechanism, but only a guard against programming mistakes. *
    • Reference queue processing takes place only at chosen points where a - * thread enters or exits native code, and therefore holds the lock. - *
    • Resource-owner callbacks originate in native code, on a thread that holds - * the lock. + * thread enters or exits native code, on the PG thread. + *
    • Resource-owner callbacks originate in native code, on the PG thread. *
    - *

    - * The above rules protect the data structures from concurrency risks in all - * cases except direct calls to {@code javaStateReleased}. In any subclass where - * {@code javaStateReleased} is intended to be called directly, the overriding - * method must call the {@code super} implementation in this class to remove the - * instance from the data structures, and must hold the lock for native access - * when it does so (whether or not it has any actual need to call native code). */ public abstract class DualState extends WeakReference { @@ -163,12 +249,11 @@ public abstract class DualState extends WeakReference *

    * They will turn up on this queue (with referent already set null) if * the garbage collector has determined them to be unreachable. They can - * also arrive here (with referent not yet set null) if some Java - * method (such as a {@code close} or {@code free} has called - * {@code enqueue}; whether the referent is null allows the cases to be - * distinguished. + * also arrive here (also with referent nulled) following + * {@code releaseFromJava} if the release of the last pin occurs on a thread + * other than the PG thread. *

    - * The queue is only processed by a private method called from native code + * The queue is only processed by a private method called on the PG thread * in selected places where it makes sense to do so. */ private static final ReferenceQueue s_releasedInstances = @@ -200,10 +285,162 @@ public abstract class DualState extends WeakReference new HashMap(); /** Backward link in per-resource-owner list. */ - DualState m_prev; + private DualState m_prev; /** Forward link in per-resource-owner list. */ - DualState m_next; + private DualState m_next; + + /** + * The sole thread (at a given moment) allowed to interact with Postgres and + * to acquire mutate locks on {@code DualState} instances. + *

    + * Depending on the setting of {@code pljava.java_thread_pg_entry}, this may + * refer to the same thread at all times, or be different threads, one at a + * time. + *

    + * Not volatile; atomic operations that follow any update to it will ensure + * its visibility. + */ + private static Thread s_mutatorThread; + + /** + * Tracker (using thread-local storage) of possibly re-entrant pins held + * on objects by the current thread. + *

    + * Organized as a stack, enforcing a strict nesting protocol for pins. + */ + private static final PinCount.Holder s_pinCount = new PinCount.Holder(); + + /** + * One (state object, pin count) entry on a stack of a thread's held pins. + */ + static final class PinCount + { + /** + * DualState object on which the pins counted by this entry are held. + */ + final DualState m_referent; + /** + * Count of pins held on {@code m_referent} at this stack level. + *

    + * The stack may hold earlier entries tracking additional pins on the + * same object, if the thread took a pin on some other object in + * between. + */ + short m_count; + + /** + * Construct a new {@code PinCount} for a given referent, with count + * zero. + */ + PinCount(DualState referent) + { + if ( null == referent ) + throw new NullPointerException("null referent of a PinCount"); + m_referent = referent; + } + + /** + * Thread-local stack of {@code PinCount} entries. + */ + static final class Holder extends ThreadLocal> + { + @Override + protected Deque initialValue() + { + return new ArrayDeque(); + } + + /** + * Increment a thread-local count of pins for a DualState object. + * @return true if there was already at least one pin counted for + * the object (that is, no real pin will need to be taken; this is + * a reentrant pin). + */ + boolean pin(DualState s) + { + boolean result = false; // assume a real pin must be taken + Deque counts = get(); + PinCount pc = counts.peek(); + if ( null == pc || ! pc.m_referent.equals(s) ) + { + result = hasPin(s, counts); + counts.push(pc = new PinCount(s)); + } + if ( 0 < pc.m_count ++ ) + return true; + return result; + } + + /** + * Decrement a thread-local count of pins for a DualState object. + * @return true if there remains at least one pin counted for + * the object (that is, no real pin will need to be released; + * this is a reentrant unpin). + */ + boolean unpin(DualState s) + { + Deque counts = get(); + PinCount pc = counts.peek(); + if ( null == pc || ! pc.m_referent.equals(s) ) + throw new IllegalThreadStateException( + "mispairing of DualState pin/unpin"); + if ( 0 == -- pc.m_count ) + { + counts.pop(); + return hasPin(s, counts); + } + return true; + } + + /** + * True if the current thread holds one or more pins on {@code s}. + */ + boolean hasPin(DualState s) + { + return hasPin(s, get()); + } + + /** + * True if a stack of {@code PinCount}s contains any with a non-zero + * count for object {@code s}. + */ + private boolean hasPin(DualState s, Deque counts) + { + for ( PinCount pc : counts ) + if ( pc.m_referent.equals(s) && 0 < pc.m_count ) + return true; + return false; + } + } + } + + /** Thread local record of when the PG thread is invoking callbacks. */ + private static final CleanupTracker s_inCleanup = new CleanupTracker(); + + /** Thread local boolean with pairing enter/exit operations. */ + static final class CleanupTracker extends ThreadLocal + { + boolean enter() + { + assert Backend.threadMayEnterPG() : m("inCleanup.enter thread"); + assert ! inCleanup() : m("inCleanup.enter re-entered"); + set(Boolean.TRUE); + return true; + } + + boolean exit() + { + assert inCleanup() : m("inCleanup.exit mispaired"); + set(Boolean.FALSE); + return true; + } + + boolean inCleanup() + { + return Boolean.TRUE == get(); + } + } /** * Bean to expose DualState allocation/release statistics to JMX management @@ -232,11 +469,48 @@ public abstract class DualState extends WeakReference */ protected static void checkCookie(Key cookie) { + assert Backend.threadMayEnterPG(); if ( ! Key.class.isInstance(cookie) ) throw new UnsupportedOperationException( "Operation on DualState instance without cookie"); } + /** Flag held in lock state showing the native state has been released. */ + private static final int NATIVE_RELEASED = 0x80000000; + /** Flag held in lock state showing the Java state has been released. */ + private static final int JAVA_RELEASED = 0x40000000; + /** Flag held in lock state showing a lock has been acquired. */ + private static final int MUTATOR_HOLDS = 0x20000000; + /** Flag held in lock state showing a lock is pending. */ + private static final int MUTATOR_WANTS = 0x10000000; + /** Reserved, clear bit above count of pinners awaiting lock release. */ + private static final int WAITERS_GUARD = 0x08000000; + /** Mask for count of pending pinners awaiting lock release. */ + private static final int WAITERS_MASK = 0x07ffc000; + /** The bit shift to get WAITERS count from PINNERS count. */ + private static final int WAITERS_SHIFT = 14; + /** Reserved, clear bit above count of current valid pins. */ + private static final int PINNERS_GUARD = 0x00002000; + /** Mask for count of current pinners holding valid pins. */ + private static final int PINNERS_MASK = 0x00001fff; + + /** Lock state, also records whether native or Java release has occurred. */ + private final AtomicInteger m_state; + + /** Threads waiting for pins pending release of lock. */ + private final Queue m_waiters; + + /** True if argument is zero. */ + static boolean z(int i) { return 0 == i; } + + /** + * Return the argument; convenient breakpoint targe for failed assertions. + */ + static T m(T detail) + { + return detail; + } + /** * Construct a {@code DualState} instance with a reference to the Java * object whose state it represents. @@ -260,12 +534,28 @@ protected DualState(Key cookie, T referent, long resourceOwner) { super(referent, s_releasedInstances); - long scoped = 0L; - checkCookie(cookie); + long scoped = 0L; + m_resourceOwner = resourceOwner; + m_state = new AtomicInteger(); + m_waiters = new ConcurrentLinkedQueue(); + + assert Backend.threadMayEnterPG() : m("DualState construction"); + /* + * The following stanza publishes 'this' into one of the static data + * structures, for resource-owner-scoped or non-native-scoped instances, + * respectively. That may look like escape of 'this' from an unfinished + * constructor, but the structures are private, and only manipulated + * during construction and release, always on the thread cleared to + * enter PG. Depending on the pljava.java_thread_pg_entry setting, that + * might or might not always be the same thread: but if it isn't, a + * synchronizing action must occur when a different thread takes over. + * That will happen after this constructor returns, so the reference is + * safely published. + */ if ( 0 != resourceOwner ) { scoped = 1L; @@ -276,7 +566,7 @@ protected DualState(Key cookie, T referent, long resourceOwner) s_scopedInstances.put(resourceOwner, head); } m_prev = head; - m_next = head.m_next; + m_next = ((DualState)head).m_next; m_prev.m_next = m_next.m_prev = this; } else @@ -291,30 +581,35 @@ protected DualState(Key cookie, T referent, long resourceOwner) */ private DualState(T referent, long resourceOwner) { - super(referent); + super(referent); // as a WeakReference subclass, must have a referent + super.clear(); // but nobody ever said for how long. m_resourceOwner = resourceOwner; + m_prev = m_next = this; + m_state = null; + m_waiters = null; } - /** - * Return {@code true} if the native state is still valid. An abstract - * method so it can be tailored to whatever native state is maintained - * by an implementing class. - */ - protected abstract boolean nativeStateIsValid(); - /** * Method that will be called when the associated {@code ResourceOwner} * is released, indicating that the native portion of the state * is no longer valid. The implementing class should clean up - * whatever is appropriate to that event, and must ensure that - * {@code nativeStateIsValid} will thereafter return {@code false}. + * whatever is appropriate to that event. *

    - * This object's monitor will always be held when this method is called - * during resource owner release. The class whose state this is must - * synchronize and hold this object's monitor for the duration of any - * operation that could refer to the native state. + * This object's exclusive {@code lock()} will always be held when this + * method is called during resource owner release. The class whose state + * this is must use {@link #pin() pin()}, followed by + * {@link #unpin() unpin()} in a {@code finally} block, around every + * (ideally short) block of code that could refer to the native state. + *

    + * This default implementation does nothing. + * @param javaStateLive true is passed if the instance's "Java state" is + * still considered live, that is, {@code releaseFromJava} has not been + * called, and the garbage collector has not determined the referent to be + * unreachable. */ - protected abstract void nativeStateReleased(); + protected void nativeStateReleased(boolean javaStateLive) + { + } /** * Method that will be called when the Java garbage collector has determined @@ -322,102 +617,741 @@ private DualState(T referent, long resourceOwner) * implementation does nothing; a subclass should override it to do any * cleanup, or release of native resources, that may be required. *

    + * If the {@code nativeStateLive} parameter is false, this method must avoid + * any action (such as freeing) it would otherwise take on the associated + * native state; if it does not, double-free crashes can result. + *

    * It is not necessary for this method to remove the instance from the - * {@code liveInstances} collection; that will have been done just before + * live-instances} data structures; that will have been done just before * this method is called. + * @param nativeStateLive true is passed if the instance's "native state" is + * still considered live, that is, no resource-owner callback has been + * invoked to stamp it invalid (nor has it been "adopted"). */ - protected void javaStateUnreachable() + protected void javaStateUnreachable(boolean nativeStateLive) { } /** - * Method that can be called to indicate that Java code has explicitly - * released the instance (for example, through calling a {@code close} - * method on the referent object). This can be handled two ways: - *

      - *
    • A {@code close} or similar method calls this directly. This instance - * must be removed from the appropriate collection. This default - * implementation does so. Overriding methods can call it as {@code super} - * for that part of the job. - *
    • A {@code close} or similar method simply calls - * {@link #enqueue enqueue} instead of this method. This method will be - * called when the queue is processed, the next time native code calls - * {@link #cleanEnqueuedInstances cleanEnqueuedInstances}. For that case, - * this method should be overridden to do whatever other cleanup is in - * order, but not call {@code super} and not remove the - * instance from the collection here, - * which will have happened just before this method is called. - *
    + * Called after client code has called {@code releaseFromJava}, always on + * a thread for which {@code Backend.threadMayEnterPG()} is true, and after + * any pins held on the state have been released. *

    - * Convention wisdom would normally favor the first form, handling releases - * directly and not enqueueing things where it can be avoided. For the - * purposes of {@code DualState}, though, the second pattern can be - * advantageous, letting releases be handled via the reference queue, - * because the queue is always processed in a thread able to call into - * PostgreSQL, which instances with native state to be freed will typically - * need to do. + * This should not be called directly. When Java code has called + * {@code releaseFromJava}, the state will be changed to 'released' + * immediately, though without actually disturbing any state that might be + * referenced by threads with existing pins. This method will be called + * at some later time, always on a thread able to enter PG, and with no + * other threads having the native state pinned, so this is the place for + * any actual release of native state that may be needed. *

    - * Note: if a state subclass has releases managed by calling {@code enqueue} - * but has not overridden this method, the statistics bean will end up - * double-counting releases for that class. + * If the {@code nativeStateLive} parameter is false, this method must avoid + * any action (such as freeing) it would otherwise take on the associated + * native state; if it does not, double-free crashes can result. + *

    + * This default implementation calls {@code javaStateUnreachable}, which, in + * typical cases, will have the same cleanup to do. + * @param nativeStateLive true is passed if the instance's "native state" is + * still considered live, that is, no resource-owner callback has been + * invoked to stamp it invalid (nor has it been "adopted"). */ - protected void javaStateReleased() + protected void javaStateReleased(boolean nativeStateLive) { - long scoped = 0L, unscoped = 0L; + javaStateUnreachable(nativeStateLive); + } - if ( 0 != m_resourceOwner ) + /** + * What Java code will call to explicitly release this instance + * (in the implementation of {@code close}, for example). + *

    + * The state is immediately marked 'released' to prevent future use, while + * a call to {@code javaStateReleased} will be deferred until after any pins + * currently held on the state have been released. + */ + protected final void releaseFromJava() + { + int s = 0; // start by assuming state is simple with no pins + int t; + for ( ;; ) { - if ( remove() ) - scoped = 1L; + t = s | JAVA_RELEASED; + if ( m_state.compareAndSet(s, t) ) + break; + s = m_state.get(); + if ( !z(s & JAVA_RELEASED) ) + return; // whoever beat us will take care of the next steps. } - else - if ( null != s_unscopedInstances.remove(this) ) - unscoped = 1L; + /* + * The state is now marked JAVA_RELEASED. If there is currently a + * non-zero count of pins, just return; the last pinner out will take + * the next step. Otherwise, it's up to us. + */ + if ( !z(s & (WAITERS_MASK | PINNERS_MASK)) ) + return; + + scheduleJavaReleased(s); + } + + /** + * Throws {@code UnsupportedOperationException}; {@code releaseFromJava} + * must be used rather than calling this method directly. + */ + @Override + public final boolean enqueue() + { + throw new UnsupportedOperationException( + "directly calling enqueue() on a DualState object is not " + + "supported; use releaseFromJava()."); + } + + /** + * Throws {@code UnsupportedOperationException}; {@code releaseFromJava} + * must be used rather than calling this method directly. + */ + @Override + public final void clear() + { + throw new UnsupportedOperationException( + "directly calling clear() on a DualState object is not " + + "supported; use releaseFromJava()."); + } + + /** + * Obtain a pin on this state, if it is still valid, blocking if necessary + * until release of a lock. + *

    + * Pins are re-entrant; a thread may obtain more than one on the same + * object, in strictly nested fashion. Only the outer acquisition (and + * corresponding release) will have any memory synchronization effect; + * likewise, only the outer acquisition will detect release of the object + * and throw the associated exception. + * @throws SQLException if the native state or the Java state has been + * released. + */ + public final void pin() throws SQLException + { + if ( s_pinCount.pin(this) ) + return; // reentrant pin, no need for sync effort + + int s = m_state.incrementAndGet(); // be optimistic + if ( z(s & ~ PINNERS_MASK) ) // nothing in s but a pin count? -> + return; // ... uncontended win! + if ( !z(s & NATIVE_RELEASED) ) + { + backoutPinBeforeEnqueue(s); + throw new SQLException(invalidMessage(), invalidSqlState()); + } + if ( !z(s & JAVA_RELEASED) ) + { + backoutPinBeforeEnqueue(s); + throw new SQLException(releasedMessage(), releasedSqlState()); + } + if ( !z(s & PINNERS_GUARD) ) + { + m_state.decrementAndGet(); // recovery is questionable in this case + s_pinCount.unpin(this); + throw new Error("DualState pin tracking capacity exceeded"); + } + /* + * The state is either MUTATOR_HOLDS or MUTATOR_WANTS. In either case, + * we're too late to get a pin right now, and need to join the waiters + * queue and move our bit from the PINNERS_MASK region to the + * WAITERS_MASK region (by adding the value of the least waiters bit + * minus one, which is equal to PINNERS_GUARD|PINNERS_MASK). + * + * Proceeding in that order allows the mutator thread (if it is in + * MUTATOR_HOLDS and already unparked), when it releases, to ensure it + * sees us in the queue, by spinning as long as it sees any bits in the + * 'wrong' area. + * + * If moving our bit leaves zero under PINNERS_MASK and it's the + * MUTATOR_WANTS case, we promote and unpark the mutator before parking. + */ + m_waiters.add(Thread.currentThread()); + int t; + /* + * Top-of-loop invariant, s has either MUTATOR_HOLDS or MUTATOR_WANTS, + * and we're counted under PINNERS_MASK, but under WAITERS_MASK is where + * we belong. + * + * Construct t from s, but moving us under WAITERS_MASK; if that leaves + * zero under PINNERS_MASK and s has MUTATOR_WANTS, promote it to + * MUTATOR_HOLDS. Try to CAS the new state into place. + * + * The top-of-loop invariant must still hold if the CAS fails + * and s is refetched: a state without MUTATOR_HOLDS or MUTATOR_WANTS + * cannot be reached as long as we are looping, because our presence in + * the PINNERS count prevents a WANTS advancing to HOLDS, and also + * blocks the final CAS in the release of a HOLDS. + */ + for ( ;; s = m_state.get() ) + { + t = s + (PINNERS_GUARD|PINNERS_MASK); + /* + * Not necessary to check here for NATIVE_RELEASED - it only gets + * set at the release of a lock, which is prevented while we spin. + * JAVA_RELEASED could have appeared, though. + */ + if ( !z(s & JAVA_RELEASED) ) + { + backoutPinAfterEnqueue(s); + throw new SQLException(releasedMessage(), releasedSqlState()); + } + if ( !z(s & PINNERS_GUARD) ) + { + backoutPinAfterEnqueue(s); + throw new Error("DualState wait tracking capacity exceeded"); + } + if ( !z(t & MUTATOR_WANTS) && z(t & PINNERS_MASK) ) + t += MUTATOR_WANTS; // promote to MUTATOR_HOLDS, next bit left + if ( m_state.compareAndSet(s, t) ) + break; + } + if ( !z(t & MUTATOR_HOLDS) && !z(s & MUTATOR_WANTS)) // promoted by us + unpark(s_mutatorThread); + + /* + * Invariant: t is the state before we park, and must have either + * MUTATOR_WANTS or MUTATOR_HOLDS (loop will exit if a state is fetched + * that has neither). + */ + for ( ;; t = s ) + { + park(this); + s = m_state.get(); + if ( !z(s & (NATIVE_RELEASED | JAVA_RELEASED)) ) + backoutPinAfterPark(s, t); // does not return + if ( !z(s & MUTATOR_HOLDS) ) // can only be a spurious unpark + continue; + if ( z(s & MUTATOR_WANTS) ) // no HOLDS, no WANTS, so + break; // we have our pin and are free to go + /* + * The newly-updated state has MUTATOR_WANTS. Check t (the pre-park + * state) to tease apart the cases for what that could mean. + */ + if ( !z(t & MUTATOR_HOLDS) ) // t, the pre-park state. + { + /* + * If MUTATOR_HOLDS was set when we parked, what the current + * state tells us is unambiguous: if it is now MUTATOR_WANTS, + * the earlier lock was released, we have our pin and are free + * to go, and the current MUTATOR_WANTS must wait for us. + */ + break; + } + /* + * This case is trickier. It was WANTS when we parked. The WANTS + * we now see could be the same WANTS we parked on, making this a + * spurious unpark, or it could be a new one that raced us to this + * point after the earlier one advanced to HOLDS, released, and + * unparked us. If that happened, we have our pin and are free to go + * (the new WANTS waits for us), and we can distinguish the cases + * because we are still in the queue in the former case but not the + * latter. (There is no race with the mutator thread draining the + * queue, because it does that with WANTS and HOLDS both clear, and + * remember, it is the only thread that gets to request a lock.) + * and we have our pin. In that case, we are no longer in the + * waiters queue, giving us a way to tell the cases apart. + */ + if ( m_waiters.contains(Thread.currentThread()) ) + continue; + break; + } + } + + /** + * Release a pin. + *

    + * If the current thread has pinned the same object more than once, only the + * last {@code unpin} will have any memory synchronization effect. + */ + public final void unpin() + { + if ( s_pinCount.unpin(this) ) + return; // it was a reentrant pin, no need for sync effort - s_stats.javaRelease(scoped, unscoped); + int s = 1; // start by assuming state is simple with only one pinner, us + int t; + for ( ;; s = m_state.get() ) + { + assert 1 <= (s & PINNERS_MASK) : m("DualState unpin < 1 pinner"); + t = s - 1; + if ( !z(t & MUTATOR_WANTS) && z(t & PINNERS_MASK) ) + t += MUTATOR_WANTS; // promote to MUTATOR_HOLDS, next bit left + if ( m_state.compareAndSet(s, t) ) + break; + } + + /* + * If there is a javaReleased event to schedule and a mutator to unpark, + * do them in that order, so the mutator will not see the event's + * clearing/enqueueing in progress. + */ + if ( !z(t & JAVA_RELEASED) && z(t & (WAITERS_MASK | PINNERS_MASK)) ) + scheduleJavaReleased(t); + if ( !z(t & MUTATOR_HOLDS) && !z(s & MUTATOR_WANTS)) // promoted by us + unpark(s_mutatorThread); + } + + /** + * Whether the current thread has pinned this object, for use in assertions. + * @return true if the current thread holds a(t least one) pin on + * the receiver, or is the PG thread and holds the lock. + */ + public final boolean pinnedByCurrentThread() + { + return s_pinCount.hasPin(this) || s_inCleanup.inCleanup(); + } + + /** + * Back out an in-progress pin before our thread has been placed on the + * queue. + */ + private void backoutPinBeforeEnqueue(int s) + { + s_pinCount.unpin(this); + int t; + for ( ;; s = m_state.get() ) + { + assert 1 <= (s & PINNERS_MASK) : m("backoutPinBeforeEnqueue"); + t = s - 1; + if ( !z(t & MUTATOR_WANTS) && z(t & PINNERS_MASK) ) + t += MUTATOR_WANTS; // promote to MUTATOR_HOLDS, next bit left + if ( m_state.compareAndSet(s, t) ) + break; + } + /* + * See unpin() for why these are in this order. + */ + if ( !z(t & JAVA_RELEASED) && z(t & (WAITERS_MASK | PINNERS_MASK)) ) + scheduleJavaReleased(t); + if ( !z(t & MUTATOR_HOLDS) && !z(s & MUTATOR_WANTS)) // promoted by us + unpark(s_mutatorThread); } /** - * Throw an {@code SQLException} with a specified message and SQLSTATE code - * if {@code nativeStateIsValid} returns {@code false}. + * Back out an in-progress pin after our thread has been placed on the + * queue, but before success of the CAS that counts us under WAITERS_MASK + * rather than PINNERS_MASK. */ - public void assertNativeStateIsValid(String message, String sqlState) - throws SQLException + private void backoutPinAfterEnqueue(int s) { - if ( ! nativeStateIsValid() ) - throw new SQLException(message, sqlState); + m_waiters.remove(Thread.currentThread()); + backoutPinBeforeEnqueue(s); } /** - * Throw an {@code SQLException} with a specified message and SQLSTATE code - * of 55000 "object not in prerequisite state" if {@code nativeStateIsValid} - * returns {@code false}. + * Back out a pin acquisition attempt from within the park loop (which + * includes a slim chance that the pin is, in fact, acquired by the time + * this method can complete, in which case the pin is immediately released). + *

    + * This is only called when a condition is detected during park for which an + * exception is to be thrown. Therefore, after backing out, this method + * throws the corresponding exception; it never returns normally. + * @param s the most recently fetched state + * @param t prior state from before parking + * @throws SQLException appropriate for the reason this method was called */ - public void assertNativeStateIsValid(String message) - throws SQLException + private void backoutPinAfterPark(int s, int t) throws SQLException { - assertNativeStateIsValid(message, "55000"); + boolean wasHolds = !z(t & MUTATOR_HOLDS); // t, the pre-park state + /* + * Quickly ADD a (fictitious) PINNER, which will reliably jam any more + * transitions by the mutator thread (WANTS to HOLDS, or HOLDS to + * released) while we determine what to do next. + */ + for ( ;; s = m_state.get() ) + { + t = s + 1; + if ( m_state.compareAndSet(s, t) ) + break; + } + + /* + * From the current state, determine whether our pin has in fact been + * acquired (so to back it out we must in fact unpin), or is still + * pending, using the same logic explained at the end of pin() above. + */ + boolean mustUnpin = + z(t & (MUTATOR_HOLDS | MUTATOR_WANTS)) + || z(t & MUTATOR_HOLDS) + && (wasHolds || ! m_waiters.contains(Thread.currentThread())); + + /* + * If the pin has been acquired and must be unpinned, we simply subtract + * the fictitious extra pinner added in this method, then unpin. + * + * Otherwise, we are still enqueued, and counted in the WAITERS region + * (along with our fictitious added PINNER). Subtract out the WAITER, + * which leaves (with the added PINNER) exactly the situation that + * backoutPinAfterEnqueue() knows how to clean up. + */ + + int delta; + if ( mustUnpin ) + { + delta = 1; + assert 1 < (t & PINNERS_MASK) : m("backoutPinAfterPark(acquired)"); + } + else + { + delta = 1 << WAITERS_SHIFT; + assert delta <= (t & WAITERS_MASK) : m("backoutPinAfterPark"); + } + s = t; + for ( ;; s = m_state.get() ) + { + t = s - delta; + if ( m_state.compareAndSet(s, t) ) + break; + } + + if ( mustUnpin ) + unpin(); + else + backoutPinBeforeEnqueue(t); + /* + * One of the following conditions was the reason this method was + * called, so throw the appropriate exception. + */ + if ( !z(s & NATIVE_RELEASED) ) + throw new SQLException(invalidMessage(), invalidSqlState()); + if ( !z(s & JAVA_RELEASED) ) + throw new SQLException(releasedMessage(), releasedSqlState()); } /** - * Throw an {@code SQLException} with a default message and SQLSTATE code - * of 55000 "object not in prerequisite state" if {@code nativeStateIsValid} - * returns {@code false}. + * Arrange the real work of cleaning up for an instance released by Java, + * as soon as there are no pins held on it. + *

    + * This is called immediately by {@code releaseFromJava} if there are no + * pins at the time; otherwise, it is called by {@code unpin} when the last + * pin is released. + *

    + * It calls {@code javaStateReleased} directly if the current thread may + * enter the native PostgreSQL code; otherwise, it adds the instance to + * the reference queue, to be handled when the queue is polled by such + * a thread. */ - public void assertNativeStateIsValid() - throws SQLException + private void scheduleJavaReleased(int s) { - if ( ! nativeStateIsValid() ) + T r = get(); + /* + * If get() returned a non-null reference, clearly the garbage collector + * has not cleared this yet (and is now prevented from doing so, by the + * strong reference r). + * + * Clearing it here explicitly relieves the garbage collector of further + * need to track it for later enqueueing. + */ + super.clear(); + + if ( Backend.threadMayEnterPG() ) { - Object referent = get(); - String message; - if ( null != referent ) - message = referent.getClass().getName(); + if ( null != r ) + { + assert s_inCleanup.enter(); // no-op when assertions disabled + try + { + delist(); + javaStateReleased(z(s & NATIVE_RELEASED)); + s_stats.javaRelease(); + } + finally + { + assert s_inCleanup.exit(); + } + } else - message = getClass().getName(); - message += " used beyond its PostgreSQL lifetime"; - throw new SQLException(message, "55000"); + { + /* + * The referent was already cleared, meaning we may already be + * enqueued. We do not want to call javaStateReleased more than + * once (as could happen if it is called from here and also when + * the queue is drained). Instead, just do a queue drain here, + * having what amounts to a decent hint that at least one item + * may be on it. + */ + cleanEnqueuedInstances(); + } + } + else + super.enqueue(); + } + + /** + * Take an exclusive lock in preparation to mutate the state. + *

    + * Only a thread for which {@code Backend.threadMayEnterPG()} returns true + * may acquire this lock. + * @param upgrade whether to acquire the lock without blocking even in the + * presence of a pin held by this thread; should be true only in cases where + * inspection shows a nearby enclosing pin whose assumptions clearly will + * not be violated by the actions to be taken under the lock. + * @return A semi-redacted version of the lock state, enough to discern + * whether it contains {@code NATIVE_RELEASED} or {@code JAVA_RELEASED} + * in case the caller cares, and for the paired {@code unlock} call to know + * whether this was a reentrant call, or should really be released. + */ + protected final int lock(boolean upgrade) + { + if ( ! Backend.threadMayEnterPG() ) + throw new IllegalThreadStateException( + "This thread may not mutate a DualState object"); + s_mutatorThread = Thread.currentThread(); + + assert !upgrade || pinnedByCurrentThread() : m("upgrade without pin"); + + int s = upgrade ? 1 : 0; // to start, assume simple state, no other pins + int t; + int contended = 0; + for ( ;; ) + { + t = s; + if ( upgrade ) + t += (PINNERS_GUARD|PINNERS_MASK); // hide my pin as a waiter + t |= ( !z(t & PINNERS_MASK) ? MUTATOR_WANTS : MUTATOR_HOLDS ); + if ( m_state.compareAndSet(s, t) ) + break; + s = m_state.get(); + if ( !z(s & MUTATOR_HOLDS) ) // surprise! this is a reentrant call. + return s & (NATIVE_RELEASED | JAVA_RELEASED | MUTATOR_HOLDS); + if ( z(s & PINNERS_MASK) ) + upgrade = false; // apparently we have no pin to upgrade + } + while ( z(t & MUTATOR_HOLDS) ) + { + contended = 1; + park(this); + t = m_state.get(); + } + s_stats.lockContended(contended); + return t & (NATIVE_RELEASED | JAVA_RELEASED) | (upgrade? 1 : 0); + } + + /** + * Calls {@link #unlock(int,boolean) unlock(s, false)}. + * @param s must be the value returned by the {@code lock} call. + */ + protected final void unlock(int s) + { + unlock(s, false); + } + + /** + * Release a lock, optionally setting the {@code NATIVE_RELEASED} flag + * atomically in the process. + * @param s must be the value returned by the {@code lock} call. + * @param isNativeRelease whether to set the {@code NATIVE_RELEASED} flag. + */ + protected final void unlock(int s, boolean isNativeRelease) + { + int t; + if ( !z(s & MUTATOR_HOLDS) ) + { + /* + * The paired lock() determined it was already held (this was a + * reentrant acquisition), so that the obvious thing to do here + * is nothing. However, if the caller wants to set NATIVE_RELEASED + * (and it wasn't already), that has to happen, even if nothing + * else does. + */ + if ( isNativeRelease && z(s & NATIVE_RELEASED) ) + { + for ( ;; s = m_state.get() ) + { + t = s | NATIVE_RELEASED; + if ( m_state.compareAndSet(s, t) ) + break; + } + } + return; + } + + boolean upgrade = !z(s & 1); // saved there in the last line of lock() + + /* + * We are here, so this is a real unlock action. In the same motion, we + * will CAS in the NATIVE_RELEASED bit if the caller wants it. + */ + int release = isNativeRelease ? NATIVE_RELEASED : 0; + + s = MUTATOR_HOLDS; // start by assuming state is simple, just our lock + if ( upgrade ) + s |= 1 << WAITERS_SHIFT; // ok, assume our stashed pin is there too + for ( ;; ) + { + t = s & ~(MUTATOR_HOLDS|WAITERS_MASK|PINNERS_MASK); + t |= release | ( (s & WAITERS_MASK) >>> WAITERS_SHIFT ); + /* + * Zero the PINNERS region in s, so the CAS will fail if anything's + * there. During MUTATOR_HOLDS, the only bits under PINNERS_MASK + * represent new would-be pinners while they add themselves to the + * queue, so we just spin for them here until they've moved their + * bits under WAITERS_MASK where they belong. Then trust the queue. + */ + s &= ~ PINNERS_MASK; + if ( m_state.compareAndSet(s, t) ) + break; + s = m_state.get(); + assert !z(s & MUTATOR_HOLDS) : m("DualState mispaired unlock"); + /* + * Most of our CAS spins in this class require only that no other + * thread write s between our fetch and CAS, so 'starving' other + * threads can't last long, and in fact guarantees rapid success. + * This spin, however, could go on for as long as it takes for some + * other thread to enqueue itself and move its bit out of the way. + * That's still a very short spin, unless we are short of CPUs and + * actually competing with the thread we're waiting for. Hence + * a yield seems prudent, if pinner count is the reason we spin. + */ + if ( !z(s & PINNERS_MASK) ) + Thread.yield(); + } + /* + * It's good to be the only thread allowed to mutate. Nobody else will + * touch this queue until the next time we want a mutate lock, so simply + * drain it and unpark every waiting thread. + */ + t &= PINNERS_MASK; // should equal the number of threads on the queue + if ( upgrade ) // my pin bit was stashed as a waiter, but nothing queued + -- t; + s_stats.pinContended(t); + Thread thr; + while ( null != (thr = m_waiters.poll()) ) + { + -- t; + unpark(thr); } + assert 0 == t : m("Miscount of DualState wait queue"); + } + + /** + * Specialized version of {@link #lock lock} for use by code implementing an + * {@code adopt} operation (in which complete control of an object is handed + * back to PostgreSQL and it is dissociated from Java). + *

    + * Can only be called on the PG thread, which must already hold a pin. + * No other thread can hold a pin, and neither the {@code NATIVE_RELEASED} + * nor {@code JAVA_RELEASED} flag may be set. This method is non-blocking + * and will simply throw an exception if these preconditions are not + * satisfied. + * @param cookie Capability held by native code to invoke special + * {@code DualState} methods. + */ + protected final void adoptionLock(Key cookie) throws SQLException + { + checkCookie(cookie); + s_mutatorThread = Thread.currentThread(); + assert pinnedByCurrentThread() : m("adoptionLock without pin"); + int s = 1; // must be: quiescent (our pin only), unreleased + int t = NATIVE_RELEASED | JAVA_RELEASED | MUTATOR_HOLDS + | 1 << WAITERS_SHIFT; + if ( ! m_state.compareAndSet(s, t) ) + throw new SQLException( + "Attempt by PostgreSQL to adopt a released or non-quiescent " + + "Java object"); + } + + /** + * Specialized version of {@link #unlock(int, boolean) unlock} for use by + * code implementing an {@code adopt} operation (in which complete control + * of an object is handed back to PostgreSQL and it is dissociated from + * Java). + *

    + * Must only be called on the PG thread, which must have acquired + * {@code adoptionLock}. Invokes the {@code nativeStateReleased} callback, + * then releases the lock, leaving both {@code NATIVE_RELEASED} + * and {@code JAVA_RELEASED} flags set. When the calling code releases the + * prior pin it was expected to hold, the {@code javaStateReleased} callback + * will execute. A value of false will be passed to both callbacks. + * @param cookie Capability held by native code to invoke special + * {@code DualState} methods. + */ + protected final void adoptionUnlock(Key cookie) throws SQLException + { + checkCookie(cookie); + int s = NATIVE_RELEASED | JAVA_RELEASED | MUTATOR_HOLDS + | 1 << WAITERS_SHIFT; + int t = NATIVE_RELEASED | JAVA_RELEASED | 1; + + /* + * nativeStateReleased is, as usual, executed while holding the lock. + */ + nativeStateReleased(false); + + if ( ! m_state.compareAndSet(s, t) ) + throw new SQLException("Release failed while adopting Java object"); + + /* + * The release of our pre-existing pin will take care of delisting and + * executing javaStateReleased. + */ + } + + /** + * Return a string identifying this object in a way useful within an + * exception message for use of this state after native release or Java + * release. + *

    + * This implementation returns the class name of the referent, or of this + * object if the referent has already been cleared. + */ + protected String identifierForMessage() + { + String id; + Object referent = get(); + if ( null != referent ) + id = referent.getClass().getName(); + else + id = getClass().getName(); + return id; + } + + /** + * Return a string for an exception message reporting the use of this object + * after the native state has been released. + *

    + * This implementation returns {@code identifierForMessage()} with + * " used beyond its PostgreSQL lifetime" appended. + */ + protected String invalidMessage() + { + return identifierForMessage() + " used beyond its PostgreSQL lifetime"; + } + + /** + * Return a string for an exception message reporting the use of this object + * after the Java state has been released. + *

    + * This implementation returns {@code identifierForMessage()} with + * " used after released by Java" appended. + */ + protected String releasedMessage() + { + return identifierForMessage() + " used after released by Java"; + } + + /** + * Return the SQLSTATE appropriate for an attempt to use this object + * after its native state has been released. + *

    + * This implementation returns 55000, object not in prerequisite state. + */ + protected String invalidSqlState() + { + return "55000"; + } + + /** + * Return the SQLSTATE appropriate for an attempt to use this object + * after its Java state has been released. + *

    + * This implementation returns 55000, object not in prerequisite state. + */ + protected String releasedSqlState() + { + return "55000"; } /** @@ -461,7 +1395,7 @@ public String toString(Object o) int pnl = c.getPackage().getName().length(); return String.format("%s owner:%x %s", cn.substring(1 + pnl), m_resourceOwner, - nativeStateIsValid() ? "fresh" : "stale"); + z(m_state.get() & NATIVE_RELEASED) ? "fresh" : "stale"); } /** @@ -475,15 +1409,17 @@ public String toString(Object o) *

    * Some state subclasses may have their nativeStateReleased methods called * from Java code, when it is clear the native state is no longer needed in - * Java. That doesn't remove the state instance from s_liveInstances though, - * so it will still eventually be seen by this loop and efficiently removed - * by the iterator. Hence the nativeStateIsValid test, to avoid invoking - * nativeStateReleased more than once. + * Java. That doesn't remove the state instance from s_scopedInstances, + * though, so it will still eventually be seen by this loop and efficiently + * removed by the iterator. Hence the {@code NATIVE_RELEASED} test, to avoid + * invoking nativeStateReleased more than once. */ private static void resourceOwnerRelease(long resourceOwner) { long total = 0L, release = 0L; + assert Backend.threadMayEnterPG() : m("resourceOwnerRelease thread"); + DualState head = s_scopedInstances.remove(resourceOwner); if ( null == head ) return; @@ -495,25 +1431,29 @@ private static void resourceOwnerRelease(long resourceOwner) t = s.m_next; s.m_prev = s.m_next = null; ++ total; - if ( null == s.get() ) // Unreachable from Java, no action needed - continue; /* - * This synchronized() is part of DualState's contract with clients. - * They are responsible for synchronizing on the state instance - * whenever they obtain the wrapped native state (which is verified - * to still be valid at that time) and to hold that monitor for the - * duration of whatever operation needs access to that state. By - * acquiring the same monitor here, native state is blocked from - * vanishing while it is actively in use. + * This lock() is part of DualState's contract with clients. + * They are responsible for pinning the state instance + * whenever they need the wrapped native state (which is verified + * to still be valid at that time) and for the duration of whatever + * operation needs access to that state. Taking this lock here + * ensures the native state is blocked from vanishing while it is + * actively in use. */ - synchronized ( s ) + int state = s.lock(false); + try { - if ( s.nativeStateIsValid() ) + if ( z(NATIVE_RELEASED & state) ) { ++ release; - s.nativeStateReleased(); + s.nativeStateReleased( + z(JAVA_RELEASED & state) && null != s.get()); } } + finally + { + s.unlock(state, true); // true -> ensure NATIVE_RELEASED is set. + } } s_stats.resourceOwnerPoll(release, total); @@ -526,59 +1466,67 @@ private static void resourceOwnerRelease(long resourceOwner) * that were cleared and enqueued by the garbage collector; calls the * {@link #javaStateReleased javaStateReleased} method for instances that * have not yet been garbage collected, but were enqueued by Java code - * explicitly calling {@link #enqueue enqueue}. + * explicitly calling {@link #releaseFromJava releaseFromJava}. */ private static void cleanEnqueuedInstances() { - long total = 0L, delistScoped = 0L, delistUnscoped = 0L, release = 0L; - + long total = 0L, release = 0L; DualState s; - while ( null != (s = (DualState)s_releasedInstances.poll()) ) - { - ++ total; - if ( 0 != s.m_resourceOwner ) - { - if ( s.remove() ) - ++ delistScoped; - } - else + assert s_inCleanup.enter(); // no-op when assertions disabled + try + { + while ( null != (s = (DualState)s_releasedInstances.poll()) ) { - if ( null != s_unscopedInstances.remove(s) ) - ++ delistUnscoped; - } + ++ total; - try - { - if ( null == s.get() ) - s.javaStateUnreachable(); - else + s.delist(); + int state = s.m_state.get(); + try { - ++ release; - s.javaStateReleased(); + if ( !z(JAVA_RELEASED & state) ) + { + ++ release; + s.javaStateReleased(z(NATIVE_RELEASED & state)); + } + else if ( z(NATIVE_RELEASED & state) ) + s.javaStateUnreachable(z(NATIVE_RELEASED & state)); } + catch ( Throwable t ) { } /* JDK 9 Cleaner ignores exceptions */ } - catch ( Throwable t ) { } /* JDK 9 Cleaner ignores exceptions, so */ + } + finally + { + assert s_inCleanup.exit(); } - s_stats.referenceQueueDrain( - delistScoped, delistUnscoped, total - release, release, total); + s_stats.referenceQueueDrain(total - release, release, total); } /** - * Remove this instance from the per-resource-owner linked list holding it. - * @return true if this instance was on a list, and has been removed + * Remove this instance from the data structure holding it, for scoped + * instances if it has a non-zero resource owner, otherwise for unscoped + * instances. */ - private boolean remove() + private void delist() { + assert Backend.threadMayEnterPG() : m("DualState delist thread"); + + if ( 0 == m_resourceOwner ) + { + if ( null != s_unscopedInstances.remove(this) ) + s_stats.delistUnscoped(); + return; + } + if ( null == m_prev || null == m_next ) - return false; + return; if ( this == m_prev.m_next ) m_prev.m_next = m_next; if ( this == m_next.m_prev ) m_next.m_prev = m_prev; m_prev = m_next = null; - return true; + s_stats.delistScoped(); } /** @@ -611,17 +1559,19 @@ private static class ListHead extends DualState // because why not? * {@code DualState}, it can't help having a resource owner field, so * may as well use it to store the resource owner that the list is for, * in case it's of interest in debugging. - * @param ownr The resource owner + * @param owner The resource owner */ - private ListHead(long ownr) + private ListHead(long owner) { - super("", ownr); // An instance needs some object to be its referent - clear(); // ... but doesn't need it for long! - m_prev = m_next = this; + super("", owner); // An instance needs an object to be its referent } - protected boolean nativeStateIsValid() { return false; } - protected void nativeStateReleased() { } + @Override + public String toString(Object o) + { + return String.format( + "DualState.ListHead for resource owner %x", m_resourceOwner); + } } /** @@ -637,67 +1587,37 @@ protected void nativeStateReleased() { } */ public static abstract class SingleGuardedLong extends DualState { - private volatile long m_value; + private final long m_guardedLong; protected SingleGuardedLong( Key cookie, T referent, long resourceOwner, long guardedLong) { super(cookie, referent, resourceOwner); - m_value = guardedLong; + m_guardedLong = guardedLong; } @Override public String toString(Object o) { - return String.format( - "%s GuardedLong(%x)", super.toString(o), m_value); - } - - /** - * For this class, the native state is valid whenever the wrapped - * value is not null. - */ - @Override - protected boolean nativeStateIsValid() - { - return 0 != m_value; - } - - /** - * When the native state is released, the wrapped value is zeroed - * to indicate the state is no longer valid; no other action is taken, - * on the assumption that the resource owner's release will be - * followed by wholesale reclamation of the guarded state anyway. - */ - @Override - protected void nativeStateReleased() - { - m_value = 0; + return + String.format(formatString(), super.toString(o), m_guardedLong); } /** - * When the Java state is released, the wrapped pointer is zeroed to - * indicate the state is no longer valid; no other action is taken. - *

    - * This overrides the inherited default, which would have removed this - * instance from the live instances collection. Users of this class - * should not call this method directly, but simply call - * {@link #enqueue enqueue}, and let the reclamation happen when the - * queue is processed. + * Return a {@code printf} format string resembling + * {@code "%s something(%x)"} where the {@code %x} will be the value + * being guarded; the "something" should indicate what the value + * represents, or what will be done with it when released by Java. */ - @Override - protected void javaStateReleased() + protected String formatString() { - m_value = 0; + return "%s GuardedLong(%x)"; } - /** - * Allows a subclass to obtain the wrapped value. - */ - protected long getValue() throws SQLException + protected final long guardedLong() { - assertNativeStateIsValid(); - return m_value; + assert pinnedByCurrentThread() : m("guardedLong() without pin"); + return m_guardedLong; } } @@ -705,89 +1625,31 @@ protected long getValue() throws SQLException * A {@code DualState} subclass whose only native resource releasing action * needed is {@code pfree} of a single pointer. */ - public static abstract class SinglePfree extends DualState + public static abstract class SinglePfree extends SingleGuardedLong { - private volatile long m_pointer; - protected SinglePfree( Key cookie, T referent, long resourceOwner, long pfreeTarget) { - super(cookie, referent, resourceOwner); - m_pointer = pfreeTarget; - } - - @Override - public String toString(Object o) - { - return String.format("%s pfree(%x)", super.toString(o), m_pointer); - } - - /** - * For this class, the native state is valid whenever the wrapped - * pointer is not null. - */ - @Override - protected boolean nativeStateIsValid() - { - return 0 != m_pointer; + super(cookie, referent, resourceOwner, pfreeTarget); } - /** - * When the native state is released, the wrapped pointer is nulled - * to indicate the state is no longer valid; no {@code pfree} call is - * made, on the assumption that the resource owner's release will be - * followed by wholesale release of the containing memory context - * anyway. - */ @Override - protected void nativeStateReleased() + protected String formatString() { - m_pointer = 0; + return "%s pfree(%x)"; } /** - * When the Java state is released, the wrapped pointer is nulled to - * indicate the state is no longer valid, and a {@code pfree} + * When the Java state is released or unreachable, a {@code pfree} * call is made so the native memory is released without having to wait * for release of its containing context. - *

    - * This overrides the inherited default, which would have removed this - * instance from the live instances collection. Users of this class - * should not call this method directly, but simply call - * {@link #enqueue enqueue}, and let the reclamation happen when the - * queue is processed. - */ - @Override - protected void javaStateReleased() - { - synchronized(Backend.THREADLOCK) - { - long p = m_pointer; - m_pointer = 0; - if ( 0 != p ) - _pfree(p); - } - } - - /** - * This override simply calls - * {@link #javaStateReleased javaStateReleased}, so there is no - * difference in the effect of the Java object being explicitly - * released, or found unreachable by the garbage collector. */ @Override - protected void javaStateUnreachable() - { - javaStateReleased(); - } - - /** - * Allows a subclass to obtain the wrapped pointer value. - */ - protected long getPointer() throws SQLException + protected void javaStateUnreachable(boolean nativeStateLive) { - assertNativeStateIsValid(); - return m_pointer; + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _pfree(guardedLong()); } private native void _pfree(long pointer); @@ -803,92 +1665,33 @@ protected long getPointer() throws SQLException * as far as Java is concerned; the object is no longer accessible, and the * native code is responsible for whatever happens to it next. */ - public static abstract class SingleMemContextDelete extends DualState + public static abstract class SingleMemContextDelete + extends SingleGuardedLong { - private volatile long m_context; - protected SingleMemContextDelete( Key cookie, T referent, long resourceOwner, long memoryContext) { - super(cookie, referent, resourceOwner); - m_context = memoryContext; - } - - @Override - public String toString(Object o) - { - return String.format("%s MemoryContextDelete(%x)", - super.toString(o), m_context); - } - - /** - * For this class, the native state is valid whenever the wrapped - * context pointer is not null. - */ - @Override - protected boolean nativeStateIsValid() - { - return 0 != m_context; + super(cookie, referent, resourceOwner, memoryContext); } - /** - * When the native state is released, the wrapped pointer is nulled - * to indicate the state is no longer valid. No - * {@code MemoryContextDelete} call is - * made; this is important, as the native code may have other plans for - * the memory context, such as to relink it under a different parent - * context, etc. - */ @Override - protected void nativeStateReleased() + public String formatString() { - m_context = 0; + return "%s MemoryContextDelete(%x)"; } /** - * When the Java state is released, the wrapped pointer is nulled to - * indicate the state is no longer valid, and a + * When the Java state is released or unreachable, a * {@code MemoryContextDelete} * call is made so the native memory is released without having to wait * for release of its parent context. - *

    - * This overrides the inherited default, which would have removed this - * instance from the live instances collection. Users of this class - * should not call this method directly, but simply call - * {@link #enqueue enqueue}, and let the reclamation happen when the - * queue is processed. - */ - @Override - protected void javaStateReleased() - { - synchronized(Backend.THREADLOCK) - { - long p = m_context; - m_context = 0; - if ( 0 != p ) - _memContextDelete(p); - } - } - - /** - * This override simply calls - * {@link #javaStateReleased javaStateReleased}, so there is no - * difference in the effect of the Java object being explicitly - * released, or found unreachable by the garbage collector. */ @Override - protected void javaStateUnreachable() - { - javaStateReleased(); - } - - /** - * Allows a subclass to obtain the wrapped pointer value. - */ - protected long getMemoryContext() throws SQLException + protected void javaStateUnreachable(boolean nativeStateLive) { - assertNativeStateIsValid(); - return m_context; + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _memContextDelete(guardedLong()); } private native void _memContextDelete(long pointer); @@ -898,92 +1701,33 @@ protected long getMemoryContext() throws SQLException * A {@code DualState} subclass whose only native resource releasing action * needed is {@code FreeTupleDesc} of a single pointer. */ - public static abstract class SingleFreeTupleDesc extends DualState + public static abstract class SingleFreeTupleDesc + extends SingleGuardedLong { - private volatile long m_pointer; - protected SingleFreeTupleDesc( Key cookie, T referent, long resourceOwner, long ftdTarget) { - super(cookie, referent, resourceOwner); - m_pointer = ftdTarget; - } - - @Override - public String toString(Object o) - { - return String.format("%s FreeTupleDesc(%x)", super.toString(o), - m_pointer); + super(cookie, referent, resourceOwner, ftdTarget); } - /** - * For this class, the native state is valid whenever the wrapped - * pointer is not null. - */ - @Override - protected boolean nativeStateIsValid() - { - return 0 != m_pointer; - } - - /** - * When the native state is released, the wrapped pointer is nulled - * to indicate the state is no longer valid; no - * {@code FreeTupleDesc} call is - * made, on the assumption that the resource owner's release will be - * followed by wholesale release of the containing memory context - * anyway. - */ @Override - protected void nativeStateReleased() + public String formatString() { - m_pointer = 0; + return "%s FreeTupleDesc(%x)"; } /** - * When the Java state is released, the wrapped pointer is nulled to - * indicate the state is no longer valid, and a + * When the Java state is released or unreachable, a * {@code FreeTupleDesc} * call is made so the native memory is released without having to wait * for release of its containing context. - *

    - * This overrides the inherited default, which would have removed this - * instance from the live instances collection. Users of this class - * should not call this method directly, but simply call - * {@link #enqueue enqueue}, and let the reclamation happen when the - * queue is processed. - */ - @Override - protected void javaStateReleased() - { - synchronized(Backend.THREADLOCK) - { - long p = m_pointer; - m_pointer = 0; - if ( 0 != p ) - _freeTupleDesc(p); - } - } - - /** - * This override simply calls - * {@link #javaStateReleased javaStateReleased}, so there is no - * difference in the effect of the Java object being explicitly - * released, or found unreachable by the garbage collector. */ @Override - protected void javaStateUnreachable() - { - javaStateReleased(); - } - - /** - * Allows a subclass to obtain the wrapped pointer value. - */ - protected long getPointer() throws SQLException + protected void javaStateUnreachable(boolean nativeStateLive) { - assertNativeStateIsValid(); - return m_pointer; + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _freeTupleDesc(guardedLong()); } private native void _freeTupleDesc(long pointer); @@ -993,92 +1737,33 @@ protected long getPointer() throws SQLException * A {@code DualState} subclass whose only native resource releasing action * needed is {@code heap_freetuple} of a single pointer. */ - public static abstract class SingleHeapFreeTuple extends DualState + public static abstract class SingleHeapFreeTuple + extends SingleGuardedLong { - private volatile long m_pointer; - protected SingleHeapFreeTuple( Key cookie, T referent, long resourceOwner, long hftTarget) { - super(cookie, referent, resourceOwner); - m_pointer = hftTarget; - } - - @Override - public String toString(Object o) - { - return String.format("%s heap_freetuple(%x)", super.toString(o), - m_pointer); - } - - /** - * For this class, the native state is valid whenever the wrapped - * pointer is not null. - */ - @Override - protected boolean nativeStateIsValid() - { - return 0 != m_pointer; + super(cookie, referent, resourceOwner, hftTarget); } - /** - * When the native state is released, the wrapped pointer is nulled - * to indicate the state is no longer valid; no - * {@code heap_freetuple} call is - * made, on the assumption that the resource owner's release will be - * followed by wholesale release of the containing memory context - * anyway. - */ @Override - protected void nativeStateReleased() + public String formatString() { - m_pointer = 0; + return"%s heap_freetuple(%x)"; } /** - * When the Java state is released, the wrapped pointer is nulled to - * indicate the state is no longer valid, and a + * When the Java state is released or unreachable, a * {@code heap_freetuple} * call is made so the native memory is released without having to wait * for release of its containing context. - *

    - * This overrides the inherited default, which would have removed this - * instance from the live instances collection. Users of this class - * should not call this method directly, but simply call - * {@link #enqueue enqueue}, and let the reclamation happen when the - * queue is processed. - */ - @Override - protected void javaStateReleased() - { - synchronized(Backend.THREADLOCK) - { - long p = m_pointer; - m_pointer = 0; - if ( 0 != p ) - _heapFreeTuple(p); - } - } - - /** - * This override simply calls - * {@link #javaStateReleased javaStateReleased}, so there is no - * difference in the effect of the Java object being explicitly - * released, or found unreachable by the garbage collector. */ @Override - protected void javaStateUnreachable() - { - javaStateReleased(); - } - - /** - * Allows a subclass to obtain the wrapped pointer value. - */ - protected long getPointer() throws SQLException + protected void javaStateUnreachable(boolean nativeStateLive) { - assertNativeStateIsValid(); - return m_pointer; + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _heapFreeTuple(guardedLong()); } private native void _heapFreeTuple(long pointer); @@ -1088,92 +1773,33 @@ protected long getPointer() throws SQLException * A {@code DualState} subclass whose only native resource releasing action * needed is {@code FreeErrorData} of a single pointer. */ - public static abstract class SingleFreeErrorData extends DualState + public static abstract class SingleFreeErrorData + extends SingleGuardedLong { - private volatile long m_pointer; - protected SingleFreeErrorData( Key cookie, T referent, long resourceOwner, long fedTarget) { - super(cookie, referent, resourceOwner); - m_pointer = fedTarget; - } - - @Override - public String toString(Object o) - { - return String.format("%s FreeErrorData(%x)", super.toString(o), - m_pointer); - } - - /** - * For this class, the native state is valid whenever the wrapped - * pointer is not null. - */ - @Override - protected boolean nativeStateIsValid() - { - return 0 != m_pointer; + super(cookie, referent, resourceOwner, fedTarget); } - /** - * When the native state is released, the wrapped pointer is nulled - * to indicate the state is no longer valid; no - * {@code FreeErrorData} call is - * made, on the assumption that the resource owner's release will be - * followed by wholesale release of the containing memory context - * anyway. - */ @Override - protected void nativeStateReleased() + public String formatString() { - m_pointer = 0; + return "%s FreeErrorData(%x)"; } /** - * When the Java state is released, the wrapped pointer is nulled to - * indicate the state is no longer valid, and a + * When the Java state is released or unreachable, a * {@code FreeErrorData} * call is made so the native memory is released without having to wait * for release of its containing context. - *

    - * This overrides the inherited default, which would have removed this - * instance from the live instances collection. Users of this class - * should not call this method directly, but simply call - * {@link #enqueue enqueue}, and let the reclamation happen when the - * queue is processed. - */ - @Override - protected void javaStateReleased() - { - synchronized(Backend.THREADLOCK) - { - long p = m_pointer; - m_pointer = 0; - if ( 0 != p ) - _freeErrorData(p); - } - } - - /** - * This override simply calls - * {@link #javaStateReleased javaStateReleased}, so there is no - * difference in the effect of the Java object being explicitly - * released, or found unreachable by the garbage collector. */ @Override - protected void javaStateUnreachable() - { - javaStateReleased(); - } - - /** - * Allows a subclass to obtain the wrapped pointer value. - */ - protected long getPointer() throws SQLException + protected void javaStateUnreachable(boolean nativeStateLive) { - assertNativeStateIsValid(); - return m_pointer; + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _freeErrorData(guardedLong()); } private native void _freeErrorData(long pointer); @@ -1196,6 +1822,8 @@ public static interface StatisticsMBean long getResourceOwnerPasses(); long getReferenceQueuePasses(); long getReferenceQueueItems(); + long getContendedLocks(); + long getContendedPins(); } static class Statistics implements StatisticsMBean @@ -1211,6 +1839,8 @@ static class Statistics implements StatisticsMBean public long getResourceOwnerPasses() { return resourceOwnerPasses; } public long getReferenceQueuePasses() { return referenceQueuePasses; } public long getReferenceQueueItems() { return referenceQueueItems; } + public long getContendedLocks() { return contendedLocks; } + public long getContendedPins() { return contendedPins; } private long constructed = 0L; private long enlistedScoped = 0L; @@ -1223,6 +1853,8 @@ static class Statistics implements StatisticsMBean private long resourceOwnerPasses = 0L; private long referenceQueuePasses = 0L; private long referenceQueueItems = 0L; + private long contendedLocks = 0L; + private long contendedPins = 0L; final void construct(long scoped) { @@ -1246,15 +1878,37 @@ final void javaRelease(long scoped, long unscoped) } final void referenceQueueDrain( - long delistScoped, long delistUnscoped, long unreachable, long release, long total) { ++ referenceQueuePasses; referenceQueueItems += total; javaUnreachable += unreachable; javaReleased += release; - delistedScoped += delistScoped; - delistedUnscoped += delistUnscoped; + } + + final void delistScoped() + { + ++ delistedScoped; + } + + final void delistUnscoped() + { + ++ delistedUnscoped; + } + + final void javaRelease() + { + ++ javaReleased; + } + + final void lockContended(int n) + { + contendedLocks += n; + } + + final void pinContended(int n) + { + contendedPins += n; } } } From d5e3d45dfdb8b1414b3d51ef401ecdc37c727d32 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Apr 2019 14:49:42 -0400 Subject: [PATCH 0267/1087] Add unidirectional SQLXML test examples. Sometimes it's handy to test just one direction of VarlenaWrapper at a time. --- .../pljava/example/annotation/PassXML.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 f253a74b..fb146edc 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 @@ -128,6 +128,21 @@ public class PassXML implements SQLData static Map s_tpls = new HashMap(); + @Function(schema="javatest", implementor="postgresql_xml") + public static String inXMLoutString(SQLXML in) throws SQLException + { + return in.getString(); + } + + @Function(schema="javatest", implementor="postgresql_xml") + public static SQLXML inStringoutXML(String in) throws SQLException + { + Connection c = DriverManager.getConnection("jdbc:default:connection"); + SQLXML result = c.createSQLXML(); + result.setString(in); + return result; + } + /** * Echo an XML parameter back, exercising seven different ways * (howin => 1-7) of reading an SQLXML object, and seven From 82f8e7ba04a047c05388a84c2c2d72896fa87843 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Apr 2019 12:06:21 -0400 Subject: [PATCH 0268/1087] Adapt already-DualStated classes to use pin/unpin. --- pljava-so/src/main/c/SQLInputFromTuple.c | 2 +- pljava-so/src/main/c/type/ErrorData.c | 22 +- pljava-so/src/main/c/type/Relation.c | 2 +- pljava-so/src/main/c/type/SingleRowReader.c | 2 +- pljava-so/src/main/c/type/TriggerData.c | 2 +- pljava-so/src/main/c/type/Tuple.c | 2 +- pljava-so/src/main/c/type/TupleDesc.c | 2 +- .../internal/ByteBufferInputStream.java | 100 ++++- .../postgresql/pljava/internal/ErrorData.java | 29 +- .../postgresql/pljava/internal/Relation.java | 27 +- .../pljava/internal/TriggerData.java | 27 +- .../org/postgresql/pljava/internal/Tuple.java | 28 +- .../postgresql/pljava/internal/TupleDesc.java | 28 +- .../pljava/internal/VarlenaWrapper.java | 342 ++++++++++++------ .../pljava/jdbc/SingleRowReader.java | 27 +- 15 files changed, 441 insertions(+), 201 deletions(-) diff --git a/pljava-so/src/main/c/SQLInputFromTuple.c b/pljava-so/src/main/c/SQLInputFromTuple.c index 8aae6567..72a1bf82 100644 --- a/pljava-so/src/main/c/SQLInputFromTuple.c +++ b/pljava-so/src/main/c/SQLInputFromTuple.c @@ -36,7 +36,7 @@ jobject pljava_SQLInputFromTuple_create(HeapTupleHeader hth) p2lro.ptrVal = currentInvocation; result = - JNI_newObject(s_SQLInputFromTuple_class, s_SQLInputFromTuple_init, + JNI_newObjectLocked(s_SQLInputFromTuple_class, s_SQLInputFromTuple_init, pljava_DualState_key(), p2lro.longVal, p2lht.longVal, jtd); JNI_deleteLocalRef(jtd); diff --git a/pljava-so/src/main/c/type/ErrorData.c b/pljava-so/src/main/c/type/ErrorData.c index 70ac26ce..a2f443ab 100644 --- a/pljava-so/src/main/c/type/ErrorData.c +++ b/pljava-so/src/main/c/type/ErrorData.c @@ -40,7 +40,7 @@ jobject pljava_ErrorData_getCurrentError(void) * being made into JavaMemoryContext, which never gets reset, so only * unreachability from the Java side will free it. */ - jed = JNI_newObject(s_ErrorData_class, s_ErrorData_init, + jed = JNI_newObjectLocked(s_ErrorData_class, s_ErrorData_init, pljava_DualState_key(), NULL, p2l.longVal); return jed; } @@ -137,11 +137,6 @@ void pljava_ErrorData_initialize(void) "(J)I", Java_org_postgresql_pljava_internal_ErrorData__1getSavedErrno }, - { - "_free", - "(J)V", - Java_org_postgresql_pljava_internal_ErrorData__1free - }, { 0, 0, 0 } }; @@ -396,18 +391,3 @@ JNIEXPORT jint JNICALL Java_org_postgresql_pljava_internal_ErrorData__1getSavedE p2l.longVal = _this; return (jint)((ErrorData*)p2l.ptrVal)->saved_errno; } - -/* - * Class: org_postgresql_pljava_internal_ErrorData - * Method: _free - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_ErrorData__1free(JNIEnv* env, jobject _this, jlong pointer) -{ - BEGIN_NATIVE_NO_ERRCHECK - Ptr2Long p2l; - p2l.longVal = pointer; - FreeErrorData(p2l.ptrVal); - END_NATIVE -} diff --git a/pljava-so/src/main/c/type/Relation.c b/pljava-so/src/main/c/type/Relation.c index 0951acde..5eed4104 100644 --- a/pljava-so/src/main/c/type/Relation.c +++ b/pljava-so/src/main/c/type/Relation.c @@ -42,7 +42,7 @@ jobject pljava_Relation_create(Relation r) p2lr.longVal = 0L; p2lr.ptrVal = r; - return JNI_newObject( + return JNI_newObjectLocked( s_Relation_class, s_Relation_init, pljava_DualState_key(), diff --git a/pljava-so/src/main/c/type/SingleRowReader.c b/pljava-so/src/main/c/type/SingleRowReader.c index 40e0b796..f65c1b26 100644 --- a/pljava-so/src/main/c/type/SingleRowReader.c +++ b/pljava-so/src/main/c/type/SingleRowReader.c @@ -59,7 +59,7 @@ jobject pljava_SingleRowReader_create(HeapTupleHeader ht) p2lro.ptrVal = currentInvocation; result = - JNI_newObject(s_SingleRowReader_class, s_SingleRowReader_init, + JNI_newObjectLocked(s_SingleRowReader_class, s_SingleRowReader_init, pljava_DualState_key(), p2lro.longVal, p2lht.longVal, jtd); JNI_deleteLocalRef(jtd); diff --git a/pljava-so/src/main/c/type/TriggerData.c b/pljava-so/src/main/c/type/TriggerData.c index 0d005764..1dd6b563 100644 --- a/pljava-so/src/main/c/type/TriggerData.c +++ b/pljava-so/src/main/c/type/TriggerData.c @@ -39,7 +39,7 @@ jobject pljava_TriggerData_create(TriggerData* triggerData) p2ltd.longVal = 0L; p2ltd.ptrVal = triggerData; - return JNI_newObject( + return JNI_newObjectLocked( s_TriggerData_class, s_TriggerData_init, pljava_DualState_key(), diff --git a/pljava-so/src/main/c/type/Tuple.c b/pljava-so/src/main/c/type/Tuple.c index 1f92141f..73bb499b 100644 --- a/pljava-so/src/main/c/type/Tuple.c +++ b/pljava-so/src/main/c/type/Tuple.c @@ -71,7 +71,7 @@ jobject pljava_Tuple_internalCreate(HeapTuple ht, bool mustCopy) * unreachability from the Java side will free it. * XXX? this seems like a lot of tuple copying. */ - jht = JNI_newObject(s_Tuple_class, s_Tuple_init, + jht = JNI_newObjectLocked(s_Tuple_class, s_Tuple_init, pljava_DualState_key(), NULL, htH.longVal); return jht; } diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index 1f46c5e5..e2ec998b 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -61,7 +61,7 @@ jobject pljava_TupleDesc_internalCreate(TupleDesc td) * unreachability from the Java side will free it. * XXX what about invalidating if DDL alters the column layout? */ - jtd = JNI_newObject(s_TupleDesc_class, s_TupleDesc_init, + jtd = JNI_newObjectLocked(s_TupleDesc_class, s_TupleDesc_init, pljava_DualState_key(), NULL, tdH.longVal, (jint)td->natts); return jtd; } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java b/pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java index 0dd602db..1632d6c7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ByteBufferInputStream.java @@ -66,6 +66,14 @@ protected ByteBufferInputStream() m_open = true; } + protected void pin() throws IOException + { + } + + protected void unpin() + { + } + /** * Return the {@link ByteBuffer} being wrapped, or throw an exception if the * memory windowed by the buffer should no longer be accessed. @@ -76,59 +84,98 @@ protected ByteBufferInputStream() * It is called everywhere that should happen, so it is the perfect place * for the test, and allows the implementing class to use a customized * message in the exception. + *

    + * All uses of the buffer in this class are preceded by {@code pin()} and + * followed by {@code unpin()} (whose default implementations in this class + * do nothing). If a subclass overrides {@code pin} with a version that + * throws the appropriate exception in either case or both, it is then + * redundant and unnecessary for {@code buffer} to check the same + * conditions. */ protected abstract ByteBuffer buffer() throws IOException; @Override public int read() throws IOException { - synchronized ( m_state ) + pin(); + try { ByteBuffer src = buffer(); - if ( 0 < src.remaining() ) - return src.get(); - return -1; + synchronized ( m_state ) + { + if ( 0 < src.remaining() ) + return src.get(); + return -1; + } + } + finally + { + unpin(); } } @Override public int read(byte[] b, int off, int len) throws IOException { - synchronized ( m_state ) + pin(); + try { ByteBuffer src = buffer(); - int has = src.remaining(); - if ( len > has ) + synchronized ( m_state ) { - if ( 0 == has ) - return -1; - len = has; + int has = src.remaining(); + if ( len > has ) + { + if ( 0 == has ) + return -1; + len = has; + } + src.get(b, off, len); + return len; } - src.get(b, off, len); - return len; + } + finally + { + unpin(); } } @Override public long skip(long n) throws IOException { - synchronized ( m_state ) + pin(); + try { ByteBuffer src = buffer(); - int has = src.remaining(); - if ( n > has ) - n = has; - src.position(src.position() + (int)n); - return n; + synchronized ( m_state ) + { + int has = src.remaining(); + if ( n > has ) + n = has; + src.position(src.position() + (int)n); + return n; + } + } + finally + { + unpin(); } } @Override public int available() throws IOException { - synchronized ( m_state ) + pin(); + try + { + synchronized ( m_state ) + { + return buffer().remaining(); + } + } + finally { - return buffer().remaining(); + unpin(); } } @@ -150,8 +197,11 @@ public void mark(int readlimit) { if ( ! m_open ) return; + boolean gotPin = false; // Kludge to get pin() inside the try block try { + pin(); + gotPin = true; buffer().mark(); } catch ( IOException e ) @@ -163,6 +213,11 @@ public void mark(int readlimit) * throwing, method is then called. If not, no harm no foul. */ } + finally + { + if ( gotPin ) + unpin(); + } } } @@ -173,6 +228,7 @@ public void reset() throws IOException { if ( ! m_open ) return; + pin(); try { buffer().reset(); @@ -181,6 +237,10 @@ public void reset() throws IOException { throw new IOException("reset attempted when mark not set"); } + finally + { + unpin(); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java index 92fccca7..3030b79d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java @@ -42,17 +42,29 @@ private State( /** * Return the ErrorData pointer. *

    - * As long as this value is used in instance methods on ErrorData - * (or subclasses, or on something that holds a reference to this - * ErrorData) and only while they hold Backend.THREADLOCK, it isn't - * necessary to also hold the monitor on this State object. The state - * can't go java-unreachable while an instance method's on the stack, - * and as long as we're on the thread that's in PG, the Invocation that - * state is scoped to can't be popped before we return. + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * ErrorData, or subclasses, or something with a strong reference to + * this ErrorData, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while an instance method's on the call + * stack, and the {@code Invocation} marking this state's native scope + * can't be popped before return of any method using the value. */ private long getErrorDataPtr() throws SQLException { - return getPointer(); + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } } } @@ -264,5 +276,4 @@ public int getSavedErrno() private static native int _getInternalPos(long pointer); private static native String _getInternalQuery(long pointer); private static native int _getSavedErrno(long pointer); /* errno at entry */ - protected native void _free(long pointer); } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java index 04094eff..b78dd7c5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java @@ -36,16 +36,29 @@ private State( /** * Return the Relation pointer. *

    - * As long as this value is used in instance methods on Relation - * (or subclasses) and only while they hold Backend.THREADLOCK, it isn't - * necessary to also hold the monitor on this State object. The state - * can't go java-unreachable while an instance method's on the stack, - * and as long as we're on the thread that's in PG, the Invocation that - * state is scoped to can't be popped before we return. + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * Relation, or subclasses, or something with a strong reference to + * this Relation, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while an instance method's on the call + * stack, and the {@code Invocation} marking this state's native scope + * can't be popped before return of any method using the value. */ private long getRelationPtr() throws SQLException { - return getValue(); + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java index 29361932..5d70af30 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java @@ -50,16 +50,29 @@ private State( /** * Return the TriggerData pointer. *

    - * As long as this value is used in instance methods on TriggerData - * (or subclasses) and only while they hold Backend.THREADLOCK, it isn't - * necessary to also hold the monitor on this State object. The state - * can't go java-unreachable while an instance method's on the stack, - * and as long as we're on the thread that's in PG, the Invocation that - * state is scoped to can't be popped before we return. + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * TriggerData, or subclasses, or something with a strong reference + * to this TriggerData, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while an instance method's on the call + * stack, and the {@code Invocation} marking this state's native scope + * can't be popped before return of any method using the value. */ private long getTriggerDataPtr() throws SQLException { - return getValue(); + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java index ff97905f..3570d5fb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java @@ -41,17 +41,29 @@ private State( /** * Return the HeapTuple pointer. *

    - * As long as this value is used in instance methods on TupleDesc - * (or subclasses, or on something that holds a reference to this - * TupleDesc) and only while they hold Backend.THREADLOCK, it isn't - * necessary to also hold the monitor on this State object. The state - * can't go java-unreachable while an instance method's on the stack, - * and as long as we're on the thread that's in PG, the Invocation that - * state is scoped to can't be popped before we return. + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * Tuple, or subclasses, or something with a strong reference + * to this Tuple, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while an instance method's on the call + * stack, and the {@code Invocation} marking this state's native scope + * can't be popped before return of any method using the value. */ private long getHeapTuplePtr() throws SQLException { - return getPointer(); + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java b/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java index 1fbe84a3..c7200b99 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java @@ -45,17 +45,29 @@ private State( /** * Return the TupleDesc pointer. *

    - * As long as this value is used in instance methods on TupleDesc - * (or subclasses, or on something that holds a reference to this - * TupleDesc) and only while they hold Backend.THREADLOCK, it isn't - * necessary to also hold the monitor on this State object. The state - * can't go java-unreachable while an instance method's on the stack, - * and as long as we're on the thread that's in PG, the Invocation that - * state is scoped to can't be popped before we return. + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * TupleDesc, or subclasses, or something with a strong reference + * to this TupleDesc, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while an instance method's on the call + * stack, and the {@code Invocation} marking this state's native scope + * can't be popped before return of any method using the value. */ private long getTupleDescPtr() throws SQLException { - return getPointer(); + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 6945c43c..63c7a78a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -34,8 +34,6 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicReference; - /** * Interface that wraps a PostgreSQL native variable-length ("varlena") datum; * implementing classes present an existing one to Java as a readable @@ -117,6 +115,35 @@ private Input(DualState.Key cookie, long resourceOwner, context, snapshot, varlenaPtr, buf); } + /* + * Overrides {@code ByteBufferInputStream} method and throws the + * exception type declared there. For other uses of pin in this class + * where SQLException is expected, just use {@code m_state.pin} + * directly, + */ + @Override + protected void pin() throws IOException + { + try + { + ((State)m_state).pin(); + } + catch ( SQLException e ) + { + throw new IOException(e.getMessage(), e); + } + } + + /* + * Unpin for use in {@code ByteBufferInputStream} or here; no + * throws-clause difference to blotch things up. + */ + @Override + protected void unpin() + { + ((State)m_state).unpin(); + } + /** * Apply a {@code Verifier} to the input data. *

    @@ -132,22 +159,33 @@ private Input(DualState.Key cookie, long resourceOwner, */ public void verify(Verifier v) throws SQLException { + /* + * This is only called from some client code's adopt() method, calls + * to which are serialized through Backend.THREADLOCK anyway, so + * holding a pin here for the duration doesn't further limit + * concurrency. Hold m_state's monitor also to block any extraneous + * reading interleaved with the verifier. + */ + ((State)m_state).pin(); try { ByteBuffer buf = buffer(); - if ( 0 != buf.position() ) - throw new SQLException( - "Variable-length input data to be verified " + - " not positioned at start", - "55000"); - InputStream dontCloseMe = new FilterInputStream(this) + synchronized ( m_state ) { - @Override - public void close() throws IOException { } - }; - v.verify(dontCloseMe); - if ( 0 != buf.remaining() ) - throw new SQLException("Verifier finished prematurely"); + if ( 0 != buf.position() ) + throw new SQLException( + "Variable-length input data to be verified " + + " not positioned at start", + "55000"); + InputStream dontCloseMe = new FilterInputStream(this) + { + @Override + public void close() throws IOException { } + }; + v.verify(dontCloseMe); + if ( 0 != buf.remaining() ) + throw new SQLException("Verifier finished prematurely"); + } } catch ( SQLException sqe ) { @@ -163,6 +201,10 @@ public void close() throws IOException { } "Error verifying variable-length input data, " + "not otherwise provided for", "XX000", e); } + finally + { + ((State)m_state).unpin(); + } } @Override @@ -183,17 +225,23 @@ protected ByteBuffer buffer() throws IOException @Override public void close() throws IOException { - synchronized ( m_state ) + pin(); + try { super.close(); - ((State)m_state).enqueue(); + ((State)m_state).releaseFromJava(); + } + finally + { + unpin(); } } @Override public long adopt(DualState.Key cookie) throws SQLException { - synchronized ( m_state ) + ((State)m_state).pin(); + try { if ( ! m_open ) throw new SQLException( @@ -201,6 +249,10 @@ public long adopt(DualState.Key cookie) throws SQLException "55000"); return ((State)m_state).adopt(cookie); } + finally + { + ((State)m_state).unpin(); + } } @Override @@ -238,67 +290,71 @@ private State( private ByteBuffer buffer() throws SQLException { - long ctx = getMemoryContext(); - if ( null == m_buf ) + pin(); + try { + if ( null != m_buf ) + return m_buf; synchronized ( Backend.THREADLOCK ) { m_buf = _detoast( - m_varlena, ctx, m_snapshot, m_resourceOwner) - .asReadOnlyBuffer(); + m_varlena, guardedLong(), m_snapshot, + m_resourceOwner).asReadOnlyBuffer(); m_snapshot = 0; } + return m_buf; + } + finally + { + unpin(); } - return m_buf; } private long adopt(DualState.Key cookie) throws SQLException { - checkCookie(cookie); - long ctx = getMemoryContext(); - if ( 0 != m_snapshot ) /* fetch now, before snapshot released */ + adoptionLock(cookie); + try { - synchronized ( Backend.THREADLOCK ) + if ( 0 != m_snapshot ) { - m_varlena = _fetch(m_varlena, ctx); + /* fetch, before snapshot released */ + m_varlena = _fetch(m_varlena, guardedLong()); } + return m_varlena; + } + finally + { + adoptionUnlock(cookie); } - long varlena = m_varlena; - nativeStateReleased(); - return varlena; } @Override - protected void nativeStateReleased() + protected void nativeStateReleased(boolean javaStateLive) { - synchronized ( Backend.THREADLOCK ) - { - super.nativeStateReleased(); - /* - * You might not expect to have to explicitly unregister a - * snapshot from the resource owner that is at this very - * moment being released, and will happily unregister the - * snapshot itself in the course of so doing. Ah, but it - * also happily logs a warning when it does that, so we need - * to have our toys picked up before it gets the chance. - */ - if ( 0 != m_snapshot ) - _unregisterSnapshot(m_snapshot, m_resourceOwner); - m_snapshot = 0; - } + assert Backend.threadMayEnterPG(); + super.nativeStateReleased(javaStateLive); + /* + * You might not expect to have to explicitly unregister a + * snapshot from the resource owner that is at this very + * moment being released, and will happily unregister the + * snapshot itself in the course of so doing. Ah, but it + * also happily logs a warning when it does that, so we need + * to have our toys picked up before it gets the chance. + */ + if ( 0 != m_snapshot ) + _unregisterSnapshot(m_snapshot, m_resourceOwner); + m_snapshot = 0; m_buf = null; } @Override - protected void javaStateReleased() + protected void javaStateUnreachable(boolean nativeStateLive) { - synchronized ( Backend.THREADLOCK ) - { - super.javaStateReleased(); - if ( 0 != m_snapshot ) - _unregisterSnapshot(m_snapshot, m_resourceOwner); - m_snapshot = 0; - } + assert Backend.threadMayEnterPG(); + super.javaStateUnreachable(nativeStateLive); + if ( 0 != m_snapshot ) + _unregisterSnapshot(m_snapshot, m_resourceOwner); + m_snapshot = 0; m_buf = null; } @@ -422,20 +478,43 @@ private ByteBuffer buf(int desiredCapacity) throws IOException } } + /** + * Wrapper around the {@code pin} method of the native state, for sites + * where an {@code IOException} is needed rather than + * {@code SQLException}. + */ + private void pin() throws IOException + { + try + { + m_state.pin(); + } + catch ( SQLException e ) + { + throw new IOException(e.getMessage(), e); + } + } + @Override public void write(int b) throws IOException { - synchronized ( m_state ) + pin(); + try { ByteBuffer dst = buf(1); dst.put((byte)(b & 0xff)); } + finally + { + m_state.unpin(); + } } @Override public void write(byte[] b, int off, int len) throws IOException { - synchronized ( m_state ) + pin(); + try { while ( 0 < len ) { @@ -448,22 +527,28 @@ public void write(byte[] b, int off, int len) throws IOException len -= can; } } + finally + { + m_state.unpin(); + } } @Override public void close() throws IOException { - synchronized ( m_state ) + pin(); + try { if ( ! m_open ) return; buf(0); m_open = false; + m_state.verify(); + } + finally + { + m_state.unpin(); } - /* - * Outside of synchronized block to avoid deadlock with verifier. - */ - m_state.verify(); } /** @@ -477,13 +562,14 @@ public void close() throws IOException public void free() throws IOException { close(); - m_state.javaStateReleased(); + m_state.releaseFromJava(); } @Override public long adopt(DualState.Key cookie) throws SQLException { - synchronized ( m_state ) + m_state.pin(); + try { if ( m_open ) throw new SQLException( @@ -491,6 +577,10 @@ public long adopt(DualState.Key cookie) throws SQLException "55000"); return m_state.adopt(cookie); } + finally + { + m_state.unpin(); + } } @Override @@ -527,28 +617,47 @@ private State( private ByteBuffer buffer(int desiredCapacity) throws SQLException { - assertNativeStateIsValid(); - if ( 0 < m_buf.remaining() && 0 < desiredCapacity ) + pin(); + try + { + if ( 0 < m_buf.remaining() && 0 < desiredCapacity ) + return m_buf; + ByteBuffer filledBuf = m_buf; + synchronized ( Backend.THREADLOCK ) + { + int lstate = lock(true); // true -> upgrade my held pin + try + { + m_buf = _nextBuffer(m_varlena, m_buf.position(), + desiredCapacity); + } + finally + { + unlock(lstate); + } + } + m_verifier.update(this, filledBuf); + if ( 0 == desiredCapacity ) + m_verifier.update(MarkableSequenceInputStream.NO_MORE); return m_buf; - ByteBuffer filledBuf = m_buf; - synchronized ( Backend.THREADLOCK ) + } + finally { - m_buf = _nextBuffer(m_varlena, m_buf.position(), - desiredCapacity); + unpin(); } - m_verifier.update(this, filledBuf); - if ( 0 == desiredCapacity ) - m_verifier.update(MarkableSequenceInputStream.NO_MORE); - return m_buf; } private long adopt(DualState.Key cookie) throws SQLException { - checkCookie(cookie); - assertNativeStateIsValid(); - long varlena = m_varlena; - nativeStateReleased(); - return varlena; + adoptionLock(cookie); + try + { + return m_varlena; + } + finally + { + adoptionUnlock(cookie); + } } private void setVerifier(Verifier v) @@ -572,10 +681,6 @@ private void cancelVerifier() } } - /* - * Caller must NOT be in synchronized block, to make sure verifier - * thread can complete. - */ private void verify() throws IOException // because called in close { try @@ -599,19 +704,19 @@ public String toString(Object o) } @Override - protected void nativeStateReleased() + protected void nativeStateReleased(boolean javaStateLive) { m_buf = null; cancelVerifier(); - super.nativeStateReleased(); + super.nativeStateReleased(javaStateLive); } @Override - protected void javaStateReleased() + protected void javaStateUnreachable(boolean nativeStateLive) { m_buf = null; cancelVerifier(); - super.javaStateReleased(); + super.javaStateUnreachable(nativeStateLive); } private native ByteBuffer _nextBuffer( @@ -658,7 +763,7 @@ private native ByteBuffer _nextBuffer( public static abstract class Verifier implements Callable { private final BlockingQueue m_queue; - private final AtomicReference m_latch; + private final CountDownLatch m_latch; private volatile Future m_future; /* @@ -688,22 +793,17 @@ public static abstract class Verifier implements Callable * synchronization puzzle crops up around the convenient synchronization * tool. :( * - * So, this method returns the Future, if we have it yet, or throws an - * IllegalStateException if we shouldn't have it yet because schedule() - * hasn't been called (or hasn't installed the latch yet; it's races all - * the way down), or waits for the latch and /then/ returns the Future. + * So, this method returns the Future, if we have it, or waits + * for the latch and /then/ returns the Future. */ private Future future() throws SQLException { Future f = m_future; if ( null != f ) return f; - CountDownLatch cll = m_latch.get(); - if ( null == cll ) - throw new IllegalStateException("Verifier not yet scheduled"); try { - cll.await(); + m_latch.await(); } catch ( InterruptedException e ) { @@ -717,7 +817,7 @@ private Future future() throws SQLException */ private Verifier( BlockingQueue queue, - AtomicReference latch) + CountDownLatch latch) { m_queue = queue; m_latch = latch; @@ -730,7 +830,7 @@ private Verifier( private Verifier() { this(new LinkedBlockingQueue(), - new AtomicReference()); + new CountDownLatch(1)); } protected void verify(InputStream is) throws Exception @@ -840,11 +940,13 @@ public final void cancel() throws SQLException */ public Verifier schedule() { - CountDownLatch cll = new CountDownLatch(1); - if ( m_latch.compareAndSet(null, cll) ) + synchronized (m_latch) { - m_future = LazyExecutorService.INSTANCE.submit(this); - cll.countDown(); + if ( 1 == m_latch.getCount() ) + { + m_future = LazyExecutorService.INSTANCE.submit(this); + m_latch.countDown(); + } } return this; } @@ -990,31 +1092,55 @@ public Thread newThread(Runnable r) * {@link ByteBufferInputStream ByteBufferInputStream} subclass that * wraps a {@code ByteBuffer} and the {@link Output.State Output.State} * that protects it. + *

    + * {@code BufferWrapper} installs itself as the inherited + * {@code m_state} field, so {@code ByteBufferInputStream}'s methods + * synchronize on it rather than the {@code State} object, for no + * interference with the writing thread. The {@code pin} and + * {@code unpin} methods, of course, forward to those of the + * native state object. */ - static class BufferWrapper extends ByteBufferInputStream + static class BufferWrapper + extends ByteBufferInputStream { private ByteBuffer m_buf; + private Output.State m_nativeState; BufferWrapper(Output.State state, ByteBuffer buf) { - m_state = state; + m_state = this; + m_nativeState = state; m_buf = buf; } @Override - protected ByteBuffer buffer() throws IOException + protected void pin() throws IOException { - if ( ! m_open ) - throw new IOException( - "I/O operation on closed VarlenaWrapper.Verifier"); try { - ((Output.State)m_state).assertNativeStateIsValid(); + m_nativeState.pin(); } catch ( SQLException e ) { throw new IOException(e.getMessage(), e); } + } + + @Override + protected void unpin() + { + m_nativeState.unpin(); + } + + @Override + protected ByteBuffer buffer() throws IOException + { + if ( ! m_open ) + throw new IOException( + "I/O operation on closed VarlenaWrapper.Verifier"); + /* + * Caller holds a pin already. + */ return m_buf; } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java index 563a1430..ca9450da 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java @@ -44,16 +44,29 @@ private State( /** * Return the HeapTupleHeader pointer. *

    - * As long as this value is used in instance methods on SingleRowReader - * (or subclasses) and only while they hold Backend.THREADLOCK, it isn't - * necessary to also hold the monitor on this State object. The state - * can't go java-unreachable while an instance method's on the stack, - * and as long as we're on the thread that's in PG, the Invocation that - * state is scoped to can't be popped before we return. + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * SingleRowReader, or subclasses, or something with a strong reference + * to this SingleRowReader, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while an instance method's on the call + * stack, and the {@code Invocation} marking this state's native scope + * can't be popped before return of any method using the value. */ private long getHeapTupleHeaderPtr() throws SQLException { - return getValue(); + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } } } From 9ea077a83ce0b64a874c8c5f453866a283953964 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Apr 2019 13:53:51 -0400 Subject: [PATCH 0269/1087] DualState-ify ExecutionPlan. --- pljava-so/src/main/c/DualState.c | 51 ++++++- pljava-so/src/main/c/ExecutionPlan.c | 81 +++++----- pljava-so/src/main/c/type/Portal.c | 7 +- pljava-so/src/main/c/type/Type.c | 2 - .../src/main/include/pljava/type/Portal.h | 17 ++- .../postgresql/pljava/internal/DualState.java | 36 +++++ .../pljava/internal/ExecutionPlan.java | 138 ++++++++++++++---- .../postgresql/pljava/internal/Portal.java | 15 +- 8 files changed, 256 insertions(+), 91 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 6fd56f8c..feae7a8f 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -16,15 +16,22 @@ #include "org_postgresql_pljava_internal_DualState_SingleFreeTupleDesc.h" #include "org_postgresql_pljava_internal_DualState_SingleHeapFreeTuple.h" #include "org_postgresql_pljava_internal_DualState_SingleFreeErrorData.h" +#include "org_postgresql_pljava_internal_DualState_SingleSPIfreeplan.h" #include "pljava/DualState.h" +#include "pljava/Exception.h" #include "pljava/PgObject.h" #include "pljava/JNICalls.h" +#include "pljava/SPI.h" /* - * Includes for objects dependent on DualState, so they can be initialized here + * Includes for objects dependent on DualState, so they can be initialized here. + * (If there's a .c file that has no corresponding .h file because there would + * be only an ..._initialize method in it and nothing else at all, just declare + * its init method here.) */ #include "pljava/type/ErrorData.h" +extern void pljava_ExecutionPlan_initialize(void); #include "pljava/type/Relation.h" #include "pljava/type/SingleRowReader.h" #include "pljava/type/TriggerData.h" @@ -152,6 +159,16 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod singleSPIfreeplanMethods[] = + { + { + "_spiFreePlan", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SingleSPIfreeplan__1spiFreePlan + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( @@ -190,12 +207,18 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleFreeErrorDataMethods); JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SingleSPIfreeplan"); + PgObject_registerNatives2(clazz, singleSPIfreeplanMethods); + JNI_deleteLocalRef(clazz); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); /* * Call initialize() methods of known classes built upon DualState. */ pljava_ErrorData_initialize(); + pljava_ExecutionPlan_initialize(); pljava_Relation_initialize(); pljava_SingleRowReader_initialize(); pljava_SQLInputFromTuple_initialize(); @@ -314,3 +337,29 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleFreeErrorData__1freeErr FreeErrorData(p2l.ptrVal); END_NATIVE } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_SingleSPIfreeplan + * Method: _spiFreePlan + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SingleSPIfreeplan__1spiFreePlan( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + PG_TRY(); + { + SPI_freeplan(p2l.ptrVal); + } + PG_CATCH(); + { + Exception_throw_ERROR("SPI_freeplan"); + } + PG_END_TRY(); + END_NATIVE +} diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index 1846f806..f3725d5a 100644 --- a/pljava-so/src/main/c/ExecutionPlan.c +++ b/pljava-so/src/main/c/ExecutionPlan.c @@ -17,6 +17,7 @@ #include #include "org_postgresql_pljava_internal_ExecutionPlan.h" +#include "pljava/DualState.h" #include "pljava/Invocation.h" #include "pljava/Exception.h" #include "pljava/Function.h" @@ -37,10 +38,13 @@ #define SPI_READONLY_FORCED 1 #define SPI_READONLY_CLEARED 2 +static jclass s_ExecutionPlan_class; +static jmethodID s_ExecutionPlan_init; + /* Make this datatype available to the postgres system. */ -extern void ExecutionPlan_initialize(void); -void ExecutionPlan_initialize(void) +extern void pljava_ExecutionPlan_initialize(void); +void pljava_ExecutionPlan_initialize(void) { JNINativeMethod methods[] = { @@ -61,17 +65,19 @@ void ExecutionPlan_initialize(void) }, { "_prepare", - "(Ljava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)J", + "(Ljava/lang/Object;Ljava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)Lorg/postgresql/pljava/internal/ExecutionPlan;", Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare }, - { - "_invalidate", - "(J)V", - Java_org_postgresql_pljava_internal_ExecutionPlan__1invalidate - }, { 0, 0, 0 } }; PgObject_registerNatives("org/postgresql/pljava/internal/ExecutionPlan", methods); + + s_ExecutionPlan_class = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/ExecutionPlan"); + s_ExecutionPlan_init = PgObject_getJavaMethod(s_ExecutionPlan_class, + "", + "(Lorg/postgresql/pljava/internal/DualState$Key;J" + "Ljava/lang/Object;J)V"); } static bool coerceObjects(void* ePlan, jobjectArray jvalues, Datum** valuesPtr, char** nullsPtr) @@ -131,7 +137,7 @@ static bool coerceObjects(void* ePlan, jobjectArray jvalues, Datum** valuesPtr, * Signature: (JLjava/lang/String;[Ljava/lang/Object;S)Lorg/postgresql/pljava/internal/Portal; */ JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jclass clazz, jlong _this, jstring cursorName, jobjectArray jvalues, jshort readonly_spec) +Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jobject jplan, jlong _this, jstring cursorName, jobjectArray jvalues, jshort readonly_spec) { jobject jportal = 0; if(_this != 0) @@ -167,7 +173,7 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1cursorOpen(JNIEnv* env, jcla if(nulls != 0) pfree(nulls); - jportal = Portal_create(portal); + jportal = pljava_Portal_create(portal, jplan); } } PG_CATCH(); @@ -264,12 +270,15 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1execute(JNIEnv* env, jclass /* * Class: org_postgresql_pljava_internal_ExecutionPlan * Method: _prepare - * Signature: (Ljava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)J; + * Signature: (Ljava/lang/Object;Ljava/lang/String;[Lorg/postgresql/pljava/internal/Oid;)Lorg/postgresql/pljava/internal/ExecutionPlan; */ -JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass clazz, jstring jcmd, jobjectArray paramTypes) +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass clazz, jobject key, jstring jcmd, jobjectArray paramTypes) { - jlong result = 0; + jobject result = 0; +#if PG_VERSION_NUM >= 90200 + int spi_ret; +#endif BEGIN_NATIVE STACK_BASE_VARS STACK_BASE_PUSH(env) @@ -310,9 +319,20 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass /* Make the plan durable */ p2l.longVal = 0L; /* ensure that the rest is zeroed out */ +#if PG_VERSION_NUM >= 90200 + spi_ret = SPI_keepplan(ePlan); + if ( 0 == spi_ret ) + p2l.ptrVal = ePlan; + else + Exception_throwSPI("keepplan", spi_ret); +#else p2l.ptrVal = SPI_saveplan(ePlan); - result = p2l.longVal; - SPI_freeplan(ePlan); /* Get rid of the original, nobody can see it anymore */ + SPI_freeplan(ePlan); /* Get rid of original, nobody can see it */ +#endif + result = JNI_newObjectLocked( + s_ExecutionPlan_class, s_ExecutionPlan_init, + /* 0L as resource owner as the saved plan isn't transient */ + pljava_DualState_key(), 0L, key, p2l.longVal); } } PG_CATCH(); @@ -324,32 +344,3 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass END_NATIVE return result; } - -/* - * Class: org_postgresql_pljava_internal_ExecutionPlan - * Method: _invalidate - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_ExecutionPlan__1invalidate(JNIEnv* env, jclass clazz, jlong _this) -{ - /* The plan is not cached as a normal JavaHandle since its made - * persistent. - */ - if(_this != 0) - { - BEGIN_NATIVE_NO_ERRCHECK - PG_TRY(); - { - Ptr2Long p2l; - p2l.longVal = _this; - SPI_freeplan(p2l.ptrVal); - } - PG_CATCH(); - { - Exception_throw_ERROR("SPI_freeplan"); - } - PG_END_TRY(); - END_NATIVE - } -} diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index 60e9e099..c51e4227 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -64,7 +64,7 @@ static void _pljavaPortalCleanup(Portal portal) /* * org.postgresql.pljava.type.Tuple type. */ -jobject Portal_create(Portal portal) +jobject pljava_Portal_create(Portal portal, jobject jplan) { jobject jportal = 0; if(portal != 0) @@ -82,7 +82,8 @@ jobject Portal_create(Portal portal) if(s_originalCleanupProc == 0) s_originalCleanupProc = portal->cleanup; - jportal = JNI_newObject(s_Portal_class, s_Portal_init, p2l.longVal); + jportal = JNI_newObject( + s_Portal_class, s_Portal_init, p2l.longVal, jplan); HashMap_putByOpaque(s_portalMap, portal, JNI_newGlobalRef(jportal)); /* @@ -148,7 +149,7 @@ void Portal_initialize(void) s_Portal_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/Portal")); PgObject_registerNatives2(s_Portal_class, methods); - s_Portal_init = PgObject_getJavaMethod(s_Portal_class, "", "(J)V"); + s_Portal_init = PgObject_getJavaMethod(s_Portal_class, "", "(JLorg/postgresql/pljava/internal/ExecutionPlan;)V"); s_Portal_pointer = PgObject_getJavaField(s_Portal_class, "m_pointer", "J"); s_portalMap = HashMap_create(13, TopMemoryContext); } diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index a060c43c..d2be9d18 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -804,7 +804,6 @@ extern void AclId_initialize(void); extern void String_initialize(void); extern void byte_array_initialize(void); -extern void ExecutionPlan_initialize(void); extern void Portal_initialize(void); extern void TupleTable_initialize(void); @@ -843,7 +842,6 @@ void Type_initialize(void) byte_array_initialize(); - ExecutionPlan_initialize(); Portal_initialize(); TupleTable_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/Portal.h b/pljava-so/src/main/include/pljava/type/Portal.h index 23a48a13..d76cba08 100644 --- a/pljava-so/src/main/include/pljava/type/Portal.h +++ b/pljava-so/src/main/include/pljava/type/Portal.h @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * PostgreSQL Global Development Group + * Chapman Flack */ #ifndef __pljava_Portal_h #define __pljava_Portal_h @@ -27,7 +32,7 @@ extern "C" { /* * Create the org.postgresql.pljava.Portal instance */ -extern jobject Portal_create(Portal portal); +extern jobject pljava_Portal_create(Portal portal, jobject jplan); #ifdef __cplusplus } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 2c9ca659..00813fff 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -1805,6 +1805,42 @@ protected void javaStateUnreachable(boolean nativeStateLive) private native void _freeErrorData(long pointer); } + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code SPI_freeplan} of a single pointer. + */ + public static abstract class SingleSPIfreeplan + extends SingleGuardedLong + { + protected SingleSPIfreeplan( + Key cookie, T referent, long resourceOwner, long fpTarget) + { + super(cookie, referent, resourceOwner, fpTarget); + } + + @Override + public String formatString() + { + return "%s SPI_freeplan(%x)"; + } + + /** + * When the Java state is released or unreachable, an + * {@code SPI_freeplan} + * call is made so the native memory is released without having to wait + * for release of its containing context. + */ + @Override + protected void javaStateUnreachable(boolean nativeStateLive) + { + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _spiFreePlan(guardedLong()); + } + + private native void _spiFreePlan(long pointer); + } + /** * Bean exposing some {@code DualState} allocation and lifecycle statistics * for viewing in a JMX management client. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java index 10340740..77a6eca7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java @@ -18,8 +18,46 @@ import java.util.LinkedHashMap; /** - * The ExecutionPlan correspons to the execution plan obtained - * using an internal PostgreSQL SPI_prepare call. + * The {@code ExecutionPlan} corresponds to the execution plan obtained + * using an internal PostgreSQL {@code SPI_prepare} call. + *

    + * The {@code ExecutionPlan} is distinct from {@code SPIPreparedStatement} + * because of its greater specificity. The current {@code PreparedStatement} + * behavior (though it may, in future, change) is that the types of parameters + * are not inferred in a "PostgreSQL-up" manner (that is, by having PostgreSQL + * parse the SQL and report what types the parameters would need to have), but + * in a "Java-down" manner, driven by the types of the parameter values supplied + * to the {@code PreparedStatement} before executing it. An + * {@code ExecutionPlan} corresponds to a particular assignment of parameter + * types; a subsequent use of the same {@code PreparedStatement} with different + * parameter values may (depending on their types) lead to generation of a new + * plan, with the former plan displaced from the {@code PreparedStatement} and + * into the {@code PlanCache} implemented here. Another re-use of the same + * {@code PreparedStatement} with the original parameter types will displace the + * newer plan into the cache and retrieve the earlier one. + *

    + * The native state of a plan is not held in a transient context, so it is not + * subject to invalidation from the native side. The Java object is kept "live" + * (garbage-collection prevented) by being referenced either from the + * {@code Statement} that created it, or from the cache if it has been displaced + * there. The {@code close} method does not deallocate a plan, but simply moves + * it to the cache, where it may be found again if needed for the same SQL and + * parameter types. + *

    + * At no time (except in passing) is a plan referred to both by the cache and by + * a {@code Statement}. It is cached when displaced out of its statement, + * and removed from the cache if it is later found there and claimed again by + * a statement, so that one {@code ExecutionPlan} does not end up getting + * shared by multiple statement instances. (There is nothing, however, + * thread-safe about these machinations.) + *

    + * There are not many ways for an {@code ExecutionPlan} to actually be freed. + * That will happen if it is evicted from the cache, either because it is oldest + * and the cache limit is reached, or when another plan is cached for the same + * SQL and parameter types; it will also happen if a {@code PreparedStatement} + * using the plan becomes unreferenced and garbage-collected without + * {@code close} being called (which would have moved the plan back to the + * cache). * * @author Thomas Hallgren */ @@ -34,7 +72,45 @@ public class ExecutionPlan public static final short SPI_READONLY_FORCED = 1; public static final short SPI_READONLY_CLEARED = 2; - private long m_pointer; + private final State m_state; + + private static class State + extends DualState.SingleSPIfreeplan + { + private State( + DualState.Key cookie, ExecutionPlan jep, long ro, long ep) + { + super(cookie, jep, ro, ep); + } + + /** + * Return the SPI execution-plan pointer. + *

    + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * ExecutionPlan, or subclasses, or something with a strong reference to + * this ExecutionPlan, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while a reference is on the call stack, + * and as long as we're on the thread that's in PG, the saved plan won't + * be popped before we return. + */ + private long getExecutionPlanPtr() throws SQLException + { + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } + } + } /** * MRU cache for prepared plans. @@ -55,14 +131,9 @@ protected boolean removeEldestEntry(Map.Entry eldest) return false; ExecutionPlan evicted = (ExecutionPlan)eldest.getValue(); - synchronized(Backend.THREADLOCK) - { - if(evicted.m_pointer != 0) - { - _invalidate(evicted.m_pointer); - evicted.m_pointer = 0; - } - } + /* + * See close() below for why 'evicted' is not enqueue()d right here. + */ return true; } }; @@ -122,10 +193,11 @@ public int hashCode() : cacheSize)); } - private ExecutionPlan(Object key, long pointer) + private ExecutionPlan(DualState.Key cookie, long resourceOwner, + Object planKey, long spiPlan) { - m_key = key; - m_pointer = pointer; + m_key = planKey; + m_state = new State(cookie, this, resourceOwner, spiPlan); } /** @@ -134,14 +206,15 @@ private ExecutionPlan(Object key, long pointer) public void close() { ExecutionPlan old = (ExecutionPlan)s_planCache.put(m_key, this); - if(old != null && old.m_pointer != 0) - { - synchronized(Backend.THREADLOCK) - { - _invalidate(old.m_pointer); - old.m_pointer = 0; - } - } + /* + * For now, do NOT immediately enqueue() a non-null 'old'. It could + * still be live via a Portal that is still retrieving results. Java + * reachability will determine when it isn't, in the natural course of + * things. + * If that turns out to keep plans around too long, something more + * elaborate can be done, involving coordination with the reachability + * of any referencing Portal. + */ } /** @@ -164,7 +237,8 @@ public Portal cursorOpen( { synchronized(Backend.THREADLOCK) { - return _cursorOpen(m_pointer, cursorName, parameters, read_only); + return _cursorOpen(m_state.getExecutionPlanPtr(), + cursorName, parameters, read_only); } } @@ -181,7 +255,7 @@ public boolean isCursorPlan() throws SQLException { synchronized(Backend.THREADLOCK) { - return _isCursorPlan(m_pointer); + return _isCursorPlan(m_state.getExecutionPlanPtr()); } } @@ -204,7 +278,8 @@ public int execute(Object[] parameters, short read_only, int rowCount) { synchronized(Backend.THREADLOCK) { - return _execute(m_pointer, parameters, read_only, rowCount); + return _execute(m_state.getExecutionPlanPtr(), + parameters, read_only, rowCount); } } @@ -230,13 +305,17 @@ public static ExecutionPlan prepare(String statement, Oid[] argTypes) { synchronized(Backend.THREADLOCK) { - plan = new ExecutionPlan(key, _prepare(statement, argTypes)); + plan = _prepare(key, statement, argTypes); } } return plan; } - private static native Portal _cursorOpen(long pointer, + /* + * Not static, so the Portal can hold a live reference to us in case we are + * evicted from the cache while it is still using the plan. + */ + private native Portal _cursorOpen(long pointer, String cursorName, Object[] parameters, short read_only) throws SQLException; @@ -246,8 +325,7 @@ private static native boolean _isCursorPlan(long pointer) private static native int _execute(long pointer, Object[] parameters, short read_only, int rowCount) throws SQLException; - private static native long _prepare(String statement, Oid[] argTypes) + private static native ExecutionPlan _prepare( + Object key, String statement, Oid[] argTypes) throws SQLException; - - private static native void _invalidate(long pointer); } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java index 0617aa38..eb5b0a23 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -23,10 +23,17 @@ public class Portal { private long m_pointer; + /* + * Hold a reference to the Java ExecutionPlan object as long as we might be + * using it, just to make sure Java unreachability doesn't cause it to + * mop up its native plan state while the portal might still be using it. + */ + private ExecutionPlan m_plan; - Portal(long pointer) + Portal(long pointer, ExecutionPlan plan) { m_pointer = pointer; + m_plan = plan; } /** @@ -39,6 +46,7 @@ public void close() { _close(m_pointer); m_pointer = 0; + m_plan = null; } } @@ -134,9 +142,8 @@ public boolean isAtStart() } /** - * Checks if the portal is still active. I can be closed either explicitly - * using the {@link #close()} mehtod or implicitly due to a pop of invocation - * context. + * Checks if the portal is still active. It can be closed either explicitly + * using the {@link #close()} method or implicitly. */ public boolean isValid() { From f0082163890cd9299486c114d074c839159f1761 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Apr 2019 14:11:04 -0400 Subject: [PATCH 0270/1087] DualState-ify Portal As far as I can discern, the complexity in Portal.c involving a hash map from native to Java Portal objects was only there to support hooking the Portal cleanup callback for detecting native release. That callback is supplied the native Portal pointer, so the map was needed for the callback to find the corresponding Java object. I could find no indication that it served as a cache, or that a Portal_create call ever returned a retrieved object rather than a new one. As it happens, a Portal object has a ResourceOwner, so it should fit rather directly into the DualState scheme, with no need for such complexity of its own. The resource owner callback happens a bit later (at drop of the Portal, where the cleanup callback could happen as soon as it reached DONE or FAILED state). A portal can't be executed again after DONE or FAILED and will protest with an error message if that's attempted, so using the resource owner may leave a gap in which Java still has the Portal reference but the Portal will protest with appropriate errors logged if Java tries to use it. But a non-dangling pointer whose use leads to appropriate errors being logged seems tolerable, and we will know about the Portal's release in time to invalidate a dangling pointer. --- pljava-so/src/main/c/DualState.c | 54 ++++++++ pljava-so/src/main/c/type/Portal.c | 116 +++--------------- pljava-so/src/main/c/type/Type.c | 2 - .../src/main/include/pljava/type/Portal.h | 4 +- .../postgresql/pljava/internal/DualState.java | 49 ++++++++ .../postgresql/pljava/internal/Portal.java | 93 +++++++++----- .../postgresql/pljava/jdbc/SPIResultSet.java | 8 +- 7 files changed, 193 insertions(+), 133 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index feae7a8f..29ff1e45 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -17,9 +17,11 @@ #include "org_postgresql_pljava_internal_DualState_SingleHeapFreeTuple.h" #include "org_postgresql_pljava_internal_DualState_SingleFreeErrorData.h" #include "org_postgresql_pljava_internal_DualState_SingleSPIfreeplan.h" +#include "org_postgresql_pljava_internal_DualState_SingleSPIcursorClose.h" #include "pljava/DualState.h" #include "pljava/Exception.h" +#include "pljava/Invocation.h" #include "pljava/PgObject.h" #include "pljava/JNICalls.h" #include "pljava/SPI.h" @@ -32,6 +34,7 @@ */ #include "pljava/type/ErrorData.h" extern void pljava_ExecutionPlan_initialize(void); +#include "pljava/type/Portal.h" #include "pljava/type/Relation.h" #include "pljava/type/SingleRowReader.h" #include "pljava/type/TriggerData.h" @@ -169,6 +172,16 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod singleSPIcursorCloseMethods[] = + { + { + "_spiCursorClose", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SingleSPIcursorClose__1spiCursorClose + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( @@ -212,6 +225,11 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleSPIfreeplanMethods); JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SingleSPIcursorClose"); + PgObject_registerNatives2(clazz, singleSPIcursorCloseMethods); + JNI_deleteLocalRef(clazz); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); /* @@ -219,6 +237,7 @@ void pljava_DualState_initialize(void) */ pljava_ErrorData_initialize(); pljava_ExecutionPlan_initialize(); + pljava_Portal_initialize(); pljava_Relation_initialize(); pljava_SingleRowReader_initialize(); pljava_SQLInputFromTuple_initialize(); @@ -363,3 +382,38 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleSPIfreeplan__1spiFreePl PG_END_TRY(); END_NATIVE } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_SingleSPIcursorClose + * Method: _spiCursorClose + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SingleSPIcursorClose__1spiCursorClose( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + PG_TRY(); + { + /* + * This code copied from its former location in Portal.c, for reasons + * not really explained there, is different from most of the other + * javaStateReleased actions here, by virtue of being conditional; it + * does nothing if the current Invocation's errorOccurred flag is set, + * or during an end-of-expression-context callback from the executor. + */ + if ( NULL != currentInvocation && ! currentInvocation->errorOccurred + && ! currentInvocation->inExprContextCB ) + SPI_cursor_close(p2l.ptrVal); + } + PG_CATCH(); + { + Exception_throw_ERROR("SPI_cursor_close"); + } + PG_END_TRY(); + END_NATIVE +} diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index c51e4227..3cf9e5b8 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -33,74 +33,33 @@ static jclass s_Portal_class; static jmethodID s_Portal_init; -static jfieldID s_Portal_pointer; - -typedef void (*PortalCleanupProc)(Portal portal); - -static HashMap s_portalMap = 0; -static PortalCleanupProc s_originalCleanupProc = 0; - -static void _pljavaPortalCleanup(Portal portal) -{ - /* - * Remove this object from the cache and clear its - * handle. - */ - jobject jportal = (jobject)HashMap_removeByOpaque(s_portalMap, portal); - if(jportal) - { - - JNI_setLongField(jportal, s_Portal_pointer, 0); - JNI_deleteGlobalRef(jportal); - } - - portal->cleanup = s_originalCleanupProc; - if(s_originalCleanupProc != 0) - { - (*s_originalCleanupProc)(portal); - } -} /* - * org.postgresql.pljava.type.Tuple type. + * org.postgresql.pljava.type.Portal type. */ jobject pljava_Portal_create(Portal portal, jobject jplan) { - jobject jportal = 0; - if(portal != 0) - { - jportal = (jobject)HashMap_getByOpaque(s_portalMap, portal); - if(jportal == 0) - { - Ptr2Long p2l; - p2l.longVal = 0L; /* ensure that the rest is zeroed out */ - p2l.ptrVal = portal; + jobject jportal; + Ptr2Long p2l; + Ptr2Long p2lro; + if(portal == 0) + return NULL; - /* We need to know when a portal is dropped so that we - * don't attempt to drop it twice. - */ - if(s_originalCleanupProc == 0) - s_originalCleanupProc = portal->cleanup; + p2l.longVal = 0L; /* ensure that the rest is zeroed out */ + p2l.ptrVal = portal; - jportal = JNI_newObject( - s_Portal_class, s_Portal_init, p2l.longVal, jplan); - HashMap_putByOpaque(s_portalMap, portal, JNI_newGlobalRef(jportal)); + p2lro.longVal = 0L; + p2lro.ptrVal = portal->resowner; + + jportal = JNI_newObjectLocked(s_Portal_class, s_Portal_init, + pljava_DualState_key(), p2lro.longVal, p2l.longVal, jplan); - /* - * Fail the day the backend decides to utilize the pointer for multiple - * purposes. - */ - Assert(portal->cleanup == s_originalCleanupProc); - portal->cleanup = _pljavaPortalCleanup; - } - } return jportal; } /* Make this datatype available to the postgres system. */ -extern void Portal_initialize(void); -void Portal_initialize(void) +void pljava_Portal_initialize(void) { JNINativeMethod methods[] = { @@ -125,11 +84,6 @@ void Portal_initialize(void) Java_org_postgresql_pljava_internal_Portal__1fetch }, { - "_close", - "(J)V", - Java_org_postgresql_pljava_internal_Portal__1close - }, - { "_isAtEnd", "(J)Z", Java_org_postgresql_pljava_internal_Portal__1isAtEnd @@ -149,9 +103,8 @@ void Portal_initialize(void) s_Portal_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/Portal")); PgObject_registerNatives2(s_Portal_class, methods); - s_Portal_init = PgObject_getJavaMethod(s_Portal_class, "", "(JLorg/postgresql/pljava/internal/ExecutionPlan;)V"); - s_Portal_pointer = PgObject_getJavaField(s_Portal_class, "m_pointer", "J"); - s_portalMap = HashMap_create(13, TopMemoryContext); + s_Portal_init = PgObject_getJavaMethod(s_Portal_class, "", + "(Lorg/postgresql/pljava/internal/DualState$Key;JJLorg/postgresql/pljava/internal/ExecutionPlan;)V"); } /**************************************** @@ -261,43 +214,6 @@ Java_org_postgresql_pljava_internal_Portal__1getTupleDesc(JNIEnv* env, jclass cl return result; } -/* - * Class: org_postgresql_pljava_internal_Portal - * Method: _invalidate - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_Portal__1close(JNIEnv* env, jclass clazz, jlong _this) -{ - /* We don't use error checking here since we don't want an exception - * caused by another exception when we attempt to close. - */ - if(_this != 0) - { - Ptr2Long p2l; - p2l.longVal = _this; - BEGIN_NATIVE_NO_ERRCHECK - Portal portal = (Portal)p2l.ptrVal; - - /* Reset our own cleanup callback if needed. No need to come in - * the backway - */ - - jobject jportal = (jobject)HashMap_removeByOpaque(s_portalMap, portal); - if(jportal) - { - JNI_deleteGlobalRef(jportal); - } - - if(portal->cleanup == _pljavaPortalCleanup) - portal->cleanup = s_originalCleanupProc; - - if(!(currentInvocation->errorOccurred || currentInvocation->inExprContextCB)) - SPI_cursor_close(portal); - END_NATIVE - } -} - /* * Class: org_postgresql_pljava_internal_Portal * Method: _isAtStart diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index d2be9d18..961e49a6 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -804,7 +804,6 @@ extern void AclId_initialize(void); extern void String_initialize(void); extern void byte_array_initialize(void); -extern void Portal_initialize(void); extern void TupleTable_initialize(void); extern void Composite_initialize(void); @@ -842,7 +841,6 @@ void Type_initialize(void) byte_array_initialize(); - Portal_initialize(); TupleTable_initialize(); Composite_initialize(); diff --git a/pljava-so/src/main/include/pljava/type/Portal.h b/pljava-so/src/main/include/pljava/type/Portal.h index d76cba08..ed467a06 100644 --- a/pljava-so/src/main/include/pljava/type/Portal.h +++ b/pljava-so/src/main/include/pljava/type/Portal.h @@ -23,12 +23,14 @@ extern "C" { #include /***************************************************************** - * The Portal java class extends the NativeStruct and provides JNI + * The Portal java class provides JNI * access to some of the attributes of the Portal structure. * * @author Thomas Hallgren *****************************************************************/ +extern void pljava_Portal_initialize(); + /* * Create the org.postgresql.pljava.Portal instance */ diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 00813fff..e737a060 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -1841,6 +1841,55 @@ protected void javaStateUnreachable(boolean nativeStateLive) private native void _spiFreePlan(long pointer); } + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code SPI_cursor_close} of a single pointer. + */ + public static abstract class SingleSPIcursorClose + extends SingleGuardedLong + { + protected SingleSPIcursorClose( + Key cookie, T referent, long resourceOwner, long ccTarget) + { + super(cookie, referent, resourceOwner, ccTarget); + } + + @Override + public String formatString() + { + return "%s SPI_cursor_close(%x)"; + } + + /** + * When the Java state is released or unreachable, an + * {@code SPI_cursor_close} + * call is made so the native memory is released without having to wait + * for release of its containing context. + *

    + * For this class (and for reasons that weren't made + * obvious in the original code this reimplements), the native code will + * avoid calling {@code SPI_cursor_close} if the {@code Invocation}'s + * error-occurred flag is set, or during a callback from the executor + * through an {@code ExprContextCallbackFunction}. + */ + @Override + protected void javaStateUnreachable(boolean nativeStateLive) + { + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _spiCursorClose(guardedLong()); + } + + /* + * This code copied from its former location in Portal.c, for reasons + * not really explained there, is different from most of the other + * javaStateReleased actions here, by virtue of being conditional; it + * does nothing if the current Invocation's errorOccurred flag is set, + * or during an end-of-expression-context callback from the executor. + */ + private native void _spiCursorClose(long pointer); + } + /** * Bean exposing some {@code DualState} allocation and lifecycle statistics * for viewing in a JMX management client. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java index eb5b0a23..b3b36856 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -12,6 +12,8 @@ */ package org.postgresql.pljava.internal; +import org.postgresql.pljava.internal.SPI; // for javadoc + import java.sql.SQLException; /** @@ -22,7 +24,6 @@ */ public class Portal { - private long m_pointer; /* * Hold a reference to the Java ExecutionPlan object as long as we might be * using it, just to make sure Java unreachability doesn't cause it to @@ -30,12 +31,52 @@ public class Portal */ private ExecutionPlan m_plan; - Portal(long pointer, ExecutionPlan plan) + private final State m_state; + + Portal(DualState.Key cookie, long ro, long pointer, ExecutionPlan plan) { - m_pointer = pointer; + m_state = new State(cookie, this, ro, pointer); m_plan = plan; } + private static class State + extends DualState.SingleSPIcursorClose + { + private State( + DualState.Key cookie, Portal referent, long ro, long portal) + { + super(cookie, referent, ro, portal); + } + + /** + * Return the Portal pointer. + *

    + * This is a transitional implementation: ideally, each method requiring + * the native state would be moved to this class, and hold the pin for + * as long as the state is being manipulated. Simply returning the + * guarded value out from under the pin, as here, is not great practice, + * but as long as the value is only used in instance methods of + * Portal, or subclasses, or something with a strong reference to + * this Portal, and only on a thread for which + * {@code Backend.threadMayEnterPG()} is true, disaster will not strike. + * It can't go Java-unreachable while a reference is on the call stack, + * and as long as we're on the thread that's in PG, the saved plan won't + * be popped before we return. + */ + private long getPortalPtr() throws SQLException + { + pin(); + try + { + return guardedLong(); + } + finally + { + unpin(); + } + } + } + /** * Invalidates this structure and frees up memory using the * internal function SPI_cursor_close @@ -44,35 +85,34 @@ public void close() { synchronized(Backend.THREADLOCK) { - _close(m_pointer); - m_pointer = 0; + m_state.releaseFromJava(); m_plan = null; } } /** * Returns the name of this Portal. - * @throws SQLException if the handle to the native structur is stale. + * @throws SQLException if the handle to the native structure is stale. */ public String getName() throws SQLException { synchronized(Backend.THREADLOCK) { - return _getName(m_pointer); + return _getName(m_state.getPortalPtr()); } } /** * Returns the value of the portalPos attribute. - * @throws SQLException if the handle to the native structur is stale. + * @throws SQLException if the handle to the native structure is stale. */ public long getPortalPos() throws SQLException { synchronized(Backend.THREADLOCK) { - long pos = _getPortalPos(m_pointer); + long pos = _getPortalPos(m_state.getPortalPtr()); if ( pos < 0 ) throw new ArithmeticException( "portal position too large to report " + @@ -84,30 +124,36 @@ public long getPortalPos() /** * Returns the TupleDesc that describes the row Tuples for this * Portal. - * @throws SQLException if the handle to the native structur is stale. + * @throws SQLException if the handle to the native structure is stale. */ public TupleDesc getTupleDesc() throws SQLException { synchronized(Backend.THREADLOCK) { - return _getTupleDesc(m_pointer); + return _getTupleDesc(m_state.getPortalPtr()); } } /** * Performs an SPI_cursor_fetch. + *

    + * The fetched rows are parked at the C global {@code SPI_tuptable}; see + * {@link SPI#getTupTable SPI.getTupTable} for retrieving them. (While + * faithful to the way the C API works, this seems a bit odd as a Java API, + * and suggests that calls to this method and then {@code SPI.getTupTable} + * would ideally be done under one acquisition of the PG thread lock.) * @param forward Set to true for forward, false for backward. * @param count Maximum number of rows to fetch. * @return The actual number of fetched rows. - * @throws SQLException if the handle to the native structur is stale. + * @throws SQLException if the handle to the native structure is stale. */ public long fetch(boolean forward, long count) throws SQLException { synchronized(Backend.THREADLOCK) { - long fetched = _fetch(m_pointer, forward, count); + long fetched = _fetch(m_state.getPortalPtr(), forward, count); if ( fetched < 0 ) throw new ArithmeticException( "fetched too many rows to report in a Java signed long"); @@ -117,52 +163,43 @@ public long fetch(boolean forward, long count) /** * Returns the value of the atEnd attribute. - * @throws SQLException if the handle to the native structur is stale. + * @throws SQLException if the handle to the native structure is stale. */ public boolean isAtEnd() throws SQLException { synchronized(Backend.THREADLOCK) { - return _isAtEnd(m_pointer); + return _isAtEnd(m_state.getPortalPtr()); } } /** * Returns the value of the atStart attribute. - * @throws SQLException if the handle to the native structur is stale. + * @throws SQLException if the handle to the native structure is stale. */ public boolean isAtStart() throws SQLException { synchronized(Backend.THREADLOCK) { - return _isAtStart(m_pointer); + return _isAtStart(m_state.getPortalPtr()); } } - /** - * Checks if the portal is still active. It can be closed either explicitly - * using the {@link #close()} method or implicitly. - */ - public boolean isValid() - { - return m_pointer != 0; - } - /** * Performs an SPI_cursor_move. * @param forward Set to true for forward, false for backward. * @param count Maximum number of rows to fetch. * @return The actual number of rows moved. - * @throws SQLException if the handle to the native structur is stale. + * @throws SQLException if the handle to the native structure is stale. */ public long move(boolean forward, long count) throws SQLException { synchronized(Backend.THREADLOCK) { - long moved = _move(m_pointer, forward, count); + long moved = _move(m_state.getPortalPtr(), forward, count); if ( moved < 0 ) throw new ArithmeticException( "moved too many rows to report in a Java signed long"); diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java index 71dfe7d4..673fe5ee 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java @@ -43,6 +43,8 @@ public class SPIResultSet extends ResultSetBase private TupleTable m_table; private int m_tableRow; + private boolean m_open; + SPIResultSet(SPIStatement statement, Portal portal, long maxRows) throws SQLException { @@ -52,14 +54,16 @@ public class SPIResultSet extends ResultSetBase m_maxRows = maxRows; m_tupleDesc = portal.getTupleDesc(); m_tableRow = -1; + m_open = true; } @Override public void close() throws SQLException { - if(m_portal.isValid()) + if(m_open) { + m_open = false; m_portal.close(); m_statement.resultSetClosed(this); m_table = null; @@ -119,7 +123,7 @@ public Statement getStatement() protected final Portal getPortal() throws SQLException { - if(!m_portal.isValid()) + if(!m_open) throw new SQLException("ResultSet is closed"); return m_portal; } From 561c276186fa15b5783b621e6721a49bd3eafa89 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 18 Mar 2019 21:21:28 -0400 Subject: [PATCH 0271/1087] Test holdability of ResultSet from PreparedStatement. Commit be5e9db got transaction-duration holdability working for ResultSets produced by Statements, but a problem remains for those produced by PreparedStatements (issue #209): since cebbd72, those have been forcibly closed at invocation exit whether you want it or not, and closing a statement closes any dependent ResultSet. --- .../example/annotation/Holdability.java | 5 +- .../postgresql/pljava/jdbc/Invocation.java | 55 ++++--------------- .../postgresql/pljava/jdbc/SPIConnection.java | 1 - .../pljava/jdbc/SPIPreparedStatement.java | 1 - 4 files changed, 13 insertions(+), 49 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java index 73d4a354..709b27df 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Holdability.java @@ -14,6 +14,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -71,9 +72,9 @@ private Holdability(Statement s, ResultSet rs) public static void stashResultSet() throws SQLException { Connection c = DriverManager.getConnection("jdbc:default:connection"); - Statement s = c.createStatement(); - ResultSet rs = s.executeQuery( + PreparedStatement s = c.prepareStatement( "SELECT * FROM pg_catalog.pg_description"); + ResultSet rs = s.executeQuery(); s_stash = new Holdability(s, rs); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java index becb98d6..fe8a2816 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -55,29 +61,6 @@ final PgSavepoint getSavepoint() return m_savepoint; } - private ArrayList m_preparedStatements; - - final void manageStatement(PreparedStatement statement) - { - if(m_preparedStatements == null) - m_preparedStatements = new ArrayList(); - m_preparedStatements.add(statement); - } - - final void forgetStatement(PreparedStatement statement) - { - if(m_preparedStatements == null) - return; - - int idx = m_preparedStatements.size(); - while(--idx >= 0) - if(m_preparedStatements.get(idx) == statement) - { - m_preparedStatements.remove(idx); - return; - } - } - /** * @param savepoint The savepoint to set. */ @@ -97,24 +80,6 @@ public void onExit() { if(m_savepoint != null) m_savepoint.onInvocationExit(SPIDriver.getDefault()); - - if(m_preparedStatements != null) - { - int idx = m_preparedStatements.size(); - if(idx > 0) - { - Logger w = Logger.getAnonymousLogger(); - w.warning( - "Closing " + idx + " \"forgotten\" statement" - + ((idx > 1) ? "s" : "")); - while(--idx >= 0) - { - PreparedStatement stmt = (PreparedStatement)m_preparedStatements.get(idx); - w.finer("Closed: " + stmt); - stmt.close(); - } - } - } } finally { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 55cac482..75749d3d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -564,7 +564,6 @@ public PreparedStatement prepareStatement(String sql) int[] pcount = new int[] { 0 }; sql = this.nativeSQL(sql, pcount); PreparedStatement stmt = new SPIPreparedStatement(this, sql, pcount[0]); - Invocation.current().manageStatement(stmt); return stmt; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index 8e4a6e38..18b34285 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -75,7 +75,6 @@ public void close() } this.clearParameters(); super.close(); - Invocation.current().forgetStatement(this); } @Override From e4332ccdb33e5cfc64869ddef871e53641aebf73 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 7 Apr 2019 19:59:17 -0400 Subject: [PATCH 0272/1087] Allow pin waits to be interruptible. Interruptible /lock/ waits will have to be added another day, if at all; they would need both more implementation effort and a pretty good story for how to make sure the world is safe after interruption of a lock wait. (Consider: a waiting locker probably meant to deliver a notice that some native state was being reclaimed; if that wait is interrupted, the state probably is reclaimed anyway, while the notice isn't delivered.) Pin waits are comparatively easy. --- .../postgresql/pljava/internal/DualState.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index e737a060..9fdd5ddd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Queue; +import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import static java.util.concurrent.locks.LockSupport.park; @@ -727,6 +728,7 @@ public final void clear() * and throw the associated exception. * @throws SQLException if the native state or the Java state has been * released. + * @throws CancellationException if the thread is interrupted while waiting. */ public final void pin() throws SQLException { @@ -767,7 +769,9 @@ public final void pin() throws SQLException * If moving our bit leaves zero under PINNERS_MASK and it's the * MUTATOR_WANTS case, we promote and unpark the mutator before parking. */ - m_waiters.add(Thread.currentThread()); + + Thread thr = Thread.currentThread(); + m_waiters.add(thr); int t; /* * Top-of-loop invariant, s has either MUTATOR_HOLDS or MUTATOR_WANTS, @@ -817,9 +821,11 @@ public final void pin() throws SQLException */ for ( ;; t = s ) { - park(this); + if ( ! thr.isInterrupted() ) + park(this); s = m_state.get(); - if ( !z(s & (NATIVE_RELEASED | JAVA_RELEASED)) ) + if ( thr.isInterrupted() + || !z(s & (NATIVE_RELEASED | JAVA_RELEASED)) ) backoutPinAfterPark(s, t); // does not return if ( !z(s & MUTATOR_HOLDS) ) // can only be a spurious unpark continue; @@ -951,6 +957,7 @@ private void backoutPinAfterEnqueue(int s) * @param s the most recently fetched state * @param t prior state from before parking * @throws SQLException appropriate for the reason this method was called + * @throws CancellationException if the reason was thread interruption */ private void backoutPinAfterPark(int s, int t) throws SQLException { @@ -1014,6 +1021,10 @@ private void backoutPinAfterPark(int s, int t) throws SQLException * One of the following conditions was the reason this method was * called, so throw the appropriate exception. */ + if ( Thread.interrupted() ) + throw (CancellationException) + new CancellationException("Interrupted waiting for pin") + .initCause(new InterruptedException()); if ( !z(s & NATIVE_RELEASED) ) throw new SQLException(invalidMessage(), invalidSqlState()); if ( !z(s & JAVA_RELEASED) ) From 0c21b4bcccb615139de2d16677acc5941a97e37c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 11 Apr 2019 23:41:40 -0400 Subject: [PATCH 0273/1087] Simplify PgSavepoint. This does refer to a bit of what could be called native state, but not really enough to bother adapting it to DualState. Just simplify it instead. The PostgreSQL functions it relies on only need two integers (transaction ID and nest level), so those can be kept right in the Java object. It's very tempting to eliminate the special-case code in Invocation.java for remembering PgSavepoints, and the special code in SPIConnection to call the special code in Invocation to remember and forget PgSavepoints, and simply migrate PgSavepoint to using DualState like most other stuff, and have the Invocation as its resource owner. But because of the way savepoints nest, it is sufficient to do what the special-case code now does, and remember only the outermost savepoint that is set, so the effort of migrating to a different mechanism doesn't offer much benefit. It appears that at one time Thomas factored out the savepoint set/release/rollback logic into functions and a struct that could be offered upstream to core as new SPI_ functions, but that was a long time ago and it looks as if core didn't take up the offer. So, just merge the logic back into PgSavepoint so it isn't scattered all over creation. There are still the mildly incestuous arrangements involving SubXactListener, Invocation, and SPIConnection mentioned above. Both JDBC and PostgreSQL's BeginInternalSubTransaction allow for a savepoint to be unnamed, so no longer treat the no-arg setSavepoint as if it were setSavepoint("anonymous_savepoint"). --- pljava-so/src/main/c/JNICalls.c | 7 + pljava-so/src/main/c/PgSavepoint.c | 187 ++++++++---------- pljava-so/src/main/c/SPI.c | 55 ------ pljava-so/src/main/c/SubXactListener.c | 31 +-- pljava-so/src/main/include/pljava/JNICalls.h | 15 +- pljava-so/src/main/include/pljava/SPI.h | 39 +--- .../pljava/internal/PgSavepoint.java | 155 +++++++++++---- .../pljava/internal/SubXactListener.java | 18 +- .../postgresql/pljava/jdbc/SPIConnection.java | 4 +- 9 files changed, 255 insertions(+), 256 deletions(-) diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 3c7fbb9d..02da38a8 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -1276,6 +1276,13 @@ void JNI_setLongArrayRegion(jlongArray array, jsize start, jsize len, jlong* buf END_JAVA } +void JNI_setIntField(jobject object, jfieldID field, jint value) +{ + BEGIN_JAVA + (*env)->SetIntField(env, object, field, value); + END_JAVA +} + void JNI_setLongField(jobject object, jfieldID field, jlong value) { BEGIN_JAVA diff --git a/pljava-so/src/main/c/PgSavepoint.c b/pljava-so/src/main/c/PgSavepoint.c index e2d77967..bddeb037 100644 --- a/pljava-so/src/main/c/PgSavepoint.c +++ b/pljava-so/src/main/c/PgSavepoint.c @@ -1,53 +1,87 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Thomas Hallgren + * Chapman Flack */ #include +#include #include #include #include "org_postgresql_pljava_internal_PgSavepoint.h" #include "pljava/Exception.h" +#include "pljava/Invocation.h" #include "pljava/type/String.h" #include "pljava/SPI.h" +static jfieldID s_nestLevel; + extern void PgSavepoint_initialize(void); +static void unwind(void (*f)(void), int nestingLevel, int xid); +static void assertXid(SubTransactionId); + void PgSavepoint_initialize(void) { + StaticAssertStmt(sizeof(SubTransactionId) <= sizeof(jint), + "SubTransactionId wider than jint?!"); + JNINativeMethod methods[] = { { "_set", - "(Ljava/lang/String;)J", + "(Ljava/lang/String;)I", Java_org_postgresql_pljava_internal_PgSavepoint__1set }, { "_release", - "(J)V", + "(II)V", Java_org_postgresql_pljava_internal_PgSavepoint__1release }, { "_rollback", - "(J)V", + "(II)V", Java_org_postgresql_pljava_internal_PgSavepoint__1rollback }, - { - "_getName", - "(J)Ljava/lang/String;", - Java_org_postgresql_pljava_internal_PgSavepoint__1getName - }, - { - "_getId", - "(J)I", - Java_org_postgresql_pljava_internal_PgSavepoint__1getId - }, { 0, 0, 0 } }; - PgObject_registerNatives("org/postgresql/pljava/internal/PgSavepoint", methods); + PgObject_registerNatives("org/postgresql/pljava/internal/PgSavepoint", + methods); + + jclass cls = PgObject_getJavaClass( + "org/postgresql/pljava/internal/PgSavepoint"); + s_nestLevel = PgObject_getJavaField(cls, "m_nestLevel", "I"); +} + +static void unwind(void (*f)(void), jint xid, jint nestingLevel) +{ + while ( nestingLevel < GetCurrentTransactionNestLevel() ) + f(); + + if ( nestingLevel == GetCurrentTransactionNestLevel() ) + { + assertXid((SubTransactionId)xid); + f(); + } +} + +static void assertXid(SubTransactionId xid) +{ + if(xid != GetCurrentSubTransactionId()) + { + /* Oops. Rollback to top level transaction. + */ + ereport(ERROR, ( + errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION), + errmsg("Subtransaction mismatch at txlevel %d", + GetCurrentTransactionNestLevel()))); + } } /**************************************** @@ -56,119 +90,70 @@ void PgSavepoint_initialize(void) /* * Class: org_postgresql_pljava_internal_PgSavepoint * Method: _set - * Signature: (Ljava/lang/String;)J; + * Signature: (Ljava/lang/String;)I; */ -JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_PgSavepoint__1set(JNIEnv* env, jclass cls, jstring jname) +JNIEXPORT jint JNICALL +Java_org_postgresql_pljava_internal_PgSavepoint__1set(JNIEnv* env, jobject this, jstring jname) { - jlong result = 0; + jint xid = 0; BEGIN_NATIVE PG_TRY(); { - Ptr2Long p2l; char* name = String_createNTS(jname); - MemoryContext currCtx = MemoryContextSwitchTo(JavaMemoryContext); - p2l.longVal = 0L; /* ensure that the rest is zeroed out */ - p2l.ptrVal = SPI_setSavepoint(name); - result = p2l.longVal; - MemoryContextSwitchTo(currCtx); - pfree(name); + Invocation_assertConnect(); + JNI_setIntField(this, s_nestLevel, 1+GetCurrentTransactionNestLevel()); + BeginInternalSubTransaction(name); + xid = GetCurrentSubTransactionId(); + if ( NULL != name ) + pfree(name); } PG_CATCH(); { - Exception_throw_ERROR("SPI_setSavepoint"); + Exception_throw_ERROR("setSavepoint"); } PG_END_TRY(); END_NATIVE - return result; + return xid; } /* * Class: org_postgresql_pljava_internal_PgSavepoint - * Method: _getName - * Signature: (J)Ljava/lang/String; + * Method: _release + * Signature: (II)V */ -JNIEXPORT jstring JNICALL -Java_org_postgresql_pljava_internal_PgSavepoint__1getName(JNIEnv* env, jclass clazz, jlong _this) +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_PgSavepoint__1release(JNIEnv* env, jclass clazz, jint xid, jint nestLevel) { - jstring result = 0; - if(_this != 0) + BEGIN_NATIVE + PG_TRY(); { - BEGIN_NATIVE - Ptr2Long p2l; - p2l.longVal = _this; - result = String_createJavaStringFromNTS(((Savepoint*)p2l.ptrVal)->name); - END_NATIVE + unwind(ReleaseCurrentSubTransaction, xid, nestLevel); } - return result; -} - -/* - * Class: org_postgresql_pljava_internal_PgSavepoint - * Method: _getId - * Signature: (J)I - */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_PgSavepoint__1getId(JNIEnv* env, jclass clazz, jlong _this) -{ - jint result = (jint)InvalidSubTransactionId; - if(_this != 0) + PG_CATCH(); { - Ptr2Long p2l; - p2l.longVal = _this; - result = (jint)((Savepoint*)p2l.ptrVal)->xid; + Exception_throw_ERROR("releaseSavepoint"); } - return result; + PG_END_TRY(); + END_NATIVE } /* * Class: org_postgresql_pljava_internal_PgSavepoint - * Method: _release - * Signature: (J)V + * Method: _rollback + * Signature: (II)V */ JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_PgSavepoint__1release(JNIEnv* env, jclass clazz, jlong _this) +Java_org_postgresql_pljava_internal_PgSavepoint__1rollback(JNIEnv* env, jclass clazz, jint xid, jint nestLevel) { - if(_this != 0) + BEGIN_NATIVE + PG_TRY(); { - BEGIN_NATIVE - Ptr2Long p2l; - p2l.longVal = _this; - PG_TRY(); - { - SPI_releaseSavepoint((Savepoint*)p2l.ptrVal); - } - PG_CATCH(); - { - Exception_throw_ERROR("SPI_releaseSavepoint"); - } - PG_END_TRY(); - END_NATIVE + unwind(RollbackAndReleaseCurrentSubTransaction, xid, nestLevel); } -} - -/* - * Class: org_postgresql_pljava_internal_PgSavepoint - * Method: _rollback - * Signature: (J)V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_PgSavepoint__1rollback(JNIEnv* env, jclass clazz, jlong _this) -{ - if(_this != 0) + PG_CATCH(); { - BEGIN_NATIVE - Ptr2Long p2l; - p2l.longVal = _this; - PG_TRY(); - { - SPI_rollbackSavepoint((Savepoint*)p2l.ptrVal); - } - PG_CATCH(); - { - Exception_throw_ERROR("SPI_rollbackSavepoint"); - } - PG_END_TRY(); - END_NATIVE + Exception_throw_ERROR("rollbackSavepoint"); } + PG_END_TRY(); + END_NATIVE } diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index 5632dee2..8ea7648a 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -23,8 +23,6 @@ #include #endif -Savepoint* infant = 0; - extern void SPI_initialize(void); void SPI_initialize(void) { @@ -154,56 +152,3 @@ Java_org_postgresql_pljava_internal_SPI__1freeTupTable(JNIEnv* env, jclass cls) END_NATIVE } } - -static void assertXid(SubTransactionId xid) -{ - if(xid != GetCurrentSubTransactionId()) - { - /* Oops. Rollback to top level transaction. - */ - ereport(ERROR, ( - errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION), - errmsg("Subtransaction mismatch at txlevel %d", - GetCurrentTransactionNestLevel()))); - } -} - -Savepoint* SPI_setSavepoint(const char* name) -{ - Savepoint* sp = (Savepoint*)palloc(sizeof(Savepoint) + strlen(name)); - Invocation_assertConnect(); - sp->nestingLevel = GetCurrentTransactionNestLevel() + 1; - strcpy(sp->name, name); - infant = sp; - BeginInternalSubTransaction(sp->name); - infant = 0; - sp->xid = GetCurrentSubTransactionId(); - return sp; -} - -void SPI_releaseSavepoint(Savepoint* sp) -{ - while(sp->nestingLevel < GetCurrentTransactionNestLevel()) - ReleaseCurrentSubTransaction(); - - if(sp->nestingLevel == GetCurrentTransactionNestLevel()) - { - assertXid(sp->xid); - ReleaseCurrentSubTransaction(); - } - pfree(sp); -} - -void SPI_rollbackSavepoint(Savepoint* sp) -{ - while(sp->nestingLevel < GetCurrentTransactionNestLevel()) - RollbackAndReleaseCurrentSubTransaction(); - - if(sp->nestingLevel == GetCurrentTransactionNestLevel()) - { - assertXid(sp->xid); - RollbackAndReleaseCurrentSubTransaction(); - } - SPI_restore_connection(); - pfree(sp); -} diff --git a/pljava-so/src/main/c/SubXactListener.c b/pljava-so/src/main/c/SubXactListener.c index c07e7545..d1acd0b6 100644 --- a/pljava-so/src/main/c/SubXactListener.c +++ b/pljava-so/src/main/c/SubXactListener.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include "pljava/Backend.h" #include "pljava/Exception.h" @@ -26,19 +30,16 @@ static void subXactCB(SubXactEvent event, SubTransactionId mySubid, SubTransacti switch(event) { case SUBXACT_EVENT_START_SUB: - { - Ptr2Long infant2l; - infant->xid = mySubid; - infant2l.longVal = 0L; /* ensure that the rest is zeroed out */ - infant2l.ptrVal = infant; - JNI_callStaticVoidMethod(s_SubXactListener_class, s_SubXactListener_onStart, p2l.longVal, infant2l.longVal, parentSubid); - } + JNI_callStaticVoidMethod(s_SubXactListener_class, + s_SubXactListener_onStart, p2l.longVal, mySubid, parentSubid); break; case SUBXACT_EVENT_COMMIT_SUB: - JNI_callStaticVoidMethod(s_SubXactListener_class, s_SubXactListener_onCommit, p2l.longVal, mySubid, parentSubid); + JNI_callStaticVoidMethod(s_SubXactListener_class, + s_SubXactListener_onCommit, p2l.longVal, mySubid, parentSubid); break; case SUBXACT_EVENT_ABORT_SUB: - JNI_callStaticVoidMethod(s_SubXactListener_class, s_SubXactListener_onAbort, p2l.longVal, mySubid, parentSubid); + JNI_callStaticVoidMethod(s_SubXactListener_class, + s_SubXactListener_onAbort, p2l.longVal, mySubid, parentSubid); } } @@ -63,7 +64,7 @@ void SubXactListener_initialize(void) s_SubXactListener_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/SubXactListener")); s_SubXactListener_onAbort = PgObject_getStaticJavaMethod(s_SubXactListener_class, "onAbort", "(JII)V"); s_SubXactListener_onCommit = PgObject_getStaticJavaMethod(s_SubXactListener_class, "onCommit", "(JII)V"); - s_SubXactListener_onStart = PgObject_getStaticJavaMethod(s_SubXactListener_class, "onStart", "(JJI)V"); + s_SubXactListener_onStart = PgObject_getStaticJavaMethod(s_SubXactListener_class, "onStart", "(JII)V"); } /* diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index 7b37a9e0..37deb5bb 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -215,6 +221,7 @@ extern void JNI_setFloatArrayRegion(jfloatArray array, jsize start, jsiz extern void JNI_setIntArrayRegion(jintArray array, jsize start, jsize len, jint* buf); extern void JNI_setLongArrayRegion(jlongArray array, jsize start, jsize len, jlong* buf); extern void JNI_setShortArrayRegion(jshortArray array, jsize start, jsize len, jshort* buf); +extern void JNI_setIntField(jobject object, jfieldID field, jint value); extern void JNI_setLongField(jobject object, jfieldID field, jlong value); extern void JNI_setObjectArrayElement(jobjectArray array, jsize index, jobject value); extern void JNI_setThreadLock(jobject lockObject); diff --git a/pljava-so/src/main/include/pljava/SPI.h b/pljava-so/src/main/include/pljava/SPI.h index 9d3548db..cc8975c6 100644 --- a/pljava-so/src/main/include/pljava/SPI.h +++ b/pljava-so/src/main/include/pljava/SPI.h @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #ifndef __pljava_SPI_h #define __pljava_SPI_h @@ -17,31 +21,6 @@ extern "C" { #endif -/*********************************************************************** - * Some needed additions to the SPI set of functions. - * - * @author Thomas Hallgren - * - ***********************************************************************/ - -typedef struct -{ - SubTransactionId xid; - int nestingLevel; - char name[1]; -} Savepoint; - -/* infant is set to the savepoint that is being created durin a setSavepoint call. - * It is used by the onStart callback. - */ -extern Savepoint* infant; - -extern Savepoint* SPI_setSavepoint(const char* name); - -extern void SPI_releaseSavepoint(Savepoint* sp); - -extern void SPI_rollbackSavepoint(Savepoint* sp); - #ifdef __cplusplus } /* end of extern "C" declaration */ #endif diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java index 26477121..7116f056 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Thomas Hallgren + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -17,21 +23,77 @@ */ public class PgSavepoint implements java.sql.Savepoint { - private static final WeakHashMap s_knownSavepoints = new WeakHashMap(); - - private long m_pointer; - - PgSavepoint(long pointer) + private static final WeakHashMap s_knownSavepoints = + new WeakHashMap(); + + /* + * PostgreSQL allows an internal subtransaction to have a name, though it + * isn't used for much, and is allowed to be null. The JDBC Savepoint API + * also allows a name, so we will pass it along to PG if provided, and save + * it here for the API method getSavepointName. + */ + private final String m_name; + + /* + * The transaction ID assigned during BeginInternalSubTransaction is really + * the identifier that matters. An instance will briefly have the default + * value zero when constructed; the real value will be filled in during the + * native _set method. + */ + private int m_xactId; + + /* + * The nesting level will also have its default value briefly at + * construction, with the real value filled in by _set. Real values will + * be positive, so setting this back to zero can be used to signal + * onInvocationExit that nothing remains to do. + */ + private int m_nestLevel; + + /* + * A complication arises if a savepoint listener has been registered: + * PostgreSQL will make the callback before BeginInternalSubTransaction + * has returned. The correct xactId will be passed to the callback, but it + * won't have been set in this object yet. The forId() method below can + * handle that by finding the object in this static and setting its m_xactId + * so it's fully initialized by the time it is passed to the listener. + * + * As all of this action (constructing and setting a new savepoint, calling + * a savepoint listener) can only happen on the PG thread, this static is + * effectively confined to one thread. + */ + private static PgSavepoint s_nursery; + + private PgSavepoint(String name) { - m_pointer = pointer; + m_name = name; } + /** + * Establish a savepoint; only to be called by + * {@link Connection#setSavepoint Connection.setSavepoint}, which makes the + * arrangements for {@link #onInvocationExit onInvocationExit} to be + * called. + */ public static PgSavepoint set(String name) throws SQLException { synchronized(Backend.THREADLOCK) { - PgSavepoint sp = new PgSavepoint(_set(name)); + PgSavepoint sp = new PgSavepoint(name); + s_nursery = sp; + try + { + /* + * This assignment of m_xactId will be redundant if a listener + * already was called and filled in the ID, but harmless. + */ + sp.m_xactId = sp._set(name); + } + finally + { + s_nursery = null; + } s_knownSavepoints.put(sp, Boolean.TRUE); return sp; } @@ -41,99 +103,106 @@ static PgSavepoint forId(int savepointId) { if(savepointId != 0) { - synchronized(Backend.THREADLOCK) + assert Backend.threadMayEnterPG(); // this only happens on PG thread + if ( null != s_nursery ) // can only be the Savepoint being set + { + s_nursery.m_xactId = savepointId; + return s_nursery; + } + for ( PgSavepoint sp : s_knownSavepoints.keySet() ) { - Iterator itor = s_knownSavepoints.keySet().iterator(); - while(itor.hasNext()) - { - PgSavepoint sp = (PgSavepoint)itor.next(); - if(savepointId == _getId(sp.m_pointer)) - return sp; - } + if(savepointId == sp.m_xactId) + return sp; } } return null; } + @Override public int hashCode() { return this.getSavepointId(); } + @Override public boolean equals(Object o) { return (this == o); } + /** + * Release (commit) a savepoint; only to be called by + * {@link Connection#releaseSavepoint Connection.releaseSavepoint}. + */ public void release() throws SQLException { synchronized(Backend.THREADLOCK) { - _release(m_pointer); + _release(m_xactId, m_nestLevel); s_knownSavepoints.remove(this); - m_pointer = 0; + m_nestLevel = 0; } } + /** + * Roll back a savepoint; only to be called by + * {@link Connection#rollback(Savepoint) Connection.rollback}. + */ public void rollback() throws SQLException { synchronized(Backend.THREADLOCK) { - _rollback(m_pointer); + _rollback(m_xactId, m_nestLevel); s_knownSavepoints.remove(this); - m_pointer = 0; + m_nestLevel = 0; } } + @Override public String getSavepointName() throws SQLException { - synchronized(Backend.THREADLOCK) - { - return _getName(m_pointer); - } + // XXX per JDBC, this should throw an exception rather than return null + return m_name; } + @Override public int getSavepointId() { - synchronized(Backend.THREADLOCK) - { - return _getId(m_pointer); - } + // XXX per JDBC, this should throw an exception if m_name ISN'T null + return m_xactId; } public void onInvocationExit(Connection conn) throws SQLException { - if(m_pointer == 0) + if(m_nestLevel == 0) return; Logger logger = Logger.getAnonymousLogger(); if(Backend.isReleaseLingeringSavepoints()) { - logger.warning("Releasing savepoint '" + _getId(m_pointer) + - "' since its lifespan exceeds that of the function where it was set"); + logger.warning("Releasing savepoint '" + m_xactId + + "' since its lifespan exceeds that of the function where " + + "it was set"); conn.releaseSavepoint(this); } else { - logger.warning("Rolling back to savepoint '" + _getId(m_pointer) + - "' since its lifespan exceeds that of the function where it was set"); + logger.warning("Rolling back to savepoint '" + m_xactId + + "' since its lifespan exceeds that of the function where " + + "it was set"); conn.rollback(this); } } - private static native long _set(String name) + private native int _set(String name) throws SQLException; - private static native void _release(long pointer) + private static native void _release(int xid, int nestLevel) throws SQLException; - private static native void _rollback(long pointer) + private static native void _rollback(int xid, int nestLevel) throws SQLException; - - private static native String _getName(long pointer); - - private static native int _getId(long pointer); } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java index 88673c5b..b1c8e40b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Thomas Hallgren + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -36,11 +42,11 @@ static void onCommit(long listenerId, int spId, int parentSpId) throws SQLExcept listener.onCommit(Backend.getSession(), PgSavepoint.forId(spId), PgSavepoint.forId(parentSpId)); } - static void onStart(long listenerId, long spPointer, int parentSpId) throws SQLException + static void onStart(long listenerId, int spId, int parentSpId) throws SQLException { SavepointListener listener = (SavepointListener)s_listeners.get(new Long(listenerId)); if(listener != null) - listener.onStart(Backend.getSession(), new PgSavepoint(spPointer), PgSavepoint.forId(parentSpId)); + listener.onStart(Backend.getSession(), PgSavepoint.forId(spId), PgSavepoint.forId(parentSpId)); } static void addListener(SavepointListener listener) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 75749d3d..6be65662 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -650,7 +650,7 @@ public PreparedStatement prepareStatement(String sql, String[] columnNames) public Savepoint setSavepoint() throws SQLException { - return this.rememberSavepoint(PgSavepoint.set("anonymous_savepoint")); + return this.rememberSavepoint(PgSavepoint.set(null)); } @Override From 221f83dc1c21f7c14b59b10e2bfd60bbd24add8c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 12 Apr 2019 00:13:14 -0400 Subject: [PATCH 0274/1087] Guarantee threadMayEnterPG for subxact callbacks. The behavior of PgSavepoint.forId() presupposes that the callbacks are made on the PG thread, which is perfectly reasonable to suppose, but could be exposed to surprises in java_thread_pg_entry=allow mode. This change will make the callbacks behave as if in =block mode. Perhaps that is too drastic a change for 1.5.x, and the C code should just make two upcalls, one to forId() and then to the actual listener, with only the first one locked? --- pljava-so/src/main/c/SubXactListener.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/SubXactListener.c b/pljava-so/src/main/c/SubXactListener.c index d1acd0b6..f76aee5c 100644 --- a/pljava-so/src/main/c/SubXactListener.c +++ b/pljava-so/src/main/c/SubXactListener.c @@ -30,15 +30,15 @@ static void subXactCB(SubXactEvent event, SubTransactionId mySubid, SubTransacti switch(event) { case SUBXACT_EVENT_START_SUB: - JNI_callStaticVoidMethod(s_SubXactListener_class, + JNI_callStaticVoidMethodLocked(s_SubXactListener_class, s_SubXactListener_onStart, p2l.longVal, mySubid, parentSubid); break; case SUBXACT_EVENT_COMMIT_SUB: - JNI_callStaticVoidMethod(s_SubXactListener_class, + JNI_callStaticVoidMethodLocked(s_SubXactListener_class, s_SubXactListener_onCommit, p2l.longVal, mySubid, parentSubid); break; case SUBXACT_EVENT_ABORT_SUB: - JNI_callStaticVoidMethod(s_SubXactListener_class, + JNI_callStaticVoidMethodLocked(s_SubXactListener_class, s_SubXactListener_onAbort, p2l.longVal, mySubid, parentSubid); } } From 81deb0f507602793e76d4022adebdad007160e35 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 12 Apr 2019 22:30:26 -0400 Subject: [PATCH 0275/1087] Simplify XactListener. The former implementation using identity hash codes to identify listeners was not very convincing. While the PostgreSQL RegisterXactCallback does allow a passthrough arg to the callback function, it isn't necessary to use that method to keep track of what listener to call. PostgreSQL simply calls all the callbacks on the list anyway, and those calls all came through PL/Java's one xactCB to be dispatched to the right listener. May as well just register xactCB exactly once, and let it call a list of Java listeners. In simplifying this, found it to be unused within PL/Java itself; SavepointListeners have some coverage in the examples, but TransactionListeners, none. The documentation for Session describing transactional attribute semantics isn't accurate; the attributes are still there, but stopped having transactional semantics in 3ab90e5 (November 2005, released in PL/Java 1.2.0). There is probably no solution at this point better than deprecating the Session attribute methods, and advising anyone needing some transactional data store to implement it from scratch and register a TransactionListener. --- .../java/org/postgresql/pljava/Session.java | 52 ++++++++++++---- .../postgresql/pljava/example/SPIActions.java | 36 ++++++++++- pljava-so/src/main/c/XactListener.c | 59 ++++++++++--------- .../postgresql/pljava/internal/Session.java | 36 ++++++++++- .../pljava/internal/TransactionalMap.java | 15 ++++- .../pljava/internal/XactListener.java | 59 +++++++++++-------- 6 files changed, 192 insertions(+), 65 deletions(-) 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 4dd4c07b..7a7bc0f9 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Session.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Session.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava; @@ -10,13 +16,18 @@ import java.sql.SQLException; /** - * A Session maintains transaction coordinated in-memory data. The data - * added since the last commit will be lost on a transaction rollback, i.e. - * the Session state is synchronized with the transaction. + * A Session brings together some useful methods and data for the current + * database session. It provides a set of attributes (a + * {@code String} to {@code Object} map. Until PL/Java 1.2.0, its attribute + * store had transactional behavior (i.e., the data + * added since the last commit would be lost on a transaction rollback, or kept + * after a commit), but in 1.2.0 and later, it has not, and has functioned as a + * {@code Map} with no awareness of transactions. Java already provides those, + * so the attribute-related methods of {@code Session} are now deprecated. * - * Please note that if nested objects (such as lists and maps) are stored - * in the session, changes internal to those objects are not subject to - * the session semantics since the session is unaware of them. + * {@code TransactionListeners} and {@code SavepointListeners} are available for + * use by any code that needs to synchronize some state with PostgreSQL + * transactions. * * @author Thomas Hallgren */ @@ -40,6 +51,13 @@ public interface Session /** * Obtain an attribute from the current session. + * + * @deprecated {@code Session}'s attribute store once had a special, and + * possibly useful, transactional behavior, but since PL/Java 1.2.0 it has + * lacked that, and offers nothing you don't get with an ordinary + * {@code Map} (that forbids nulls). If some kind of store with + * transactional behavior is needed, it should be implemented in straight + * Java and kept in sync by using a {@link TransactionListener}. * @param attributeName The name of the attribute * @return The value of the attribute */ @@ -130,6 +148,13 @@ void executeAsSessionUser(Connection conn, String statement) /** * Remove an attribute previously stored in the session. If * no attribute is found, nothing happens. + * + * @deprecated {@code Session}'s attribute store once had a special, and + * possibly useful, transactional behavior, but since PL/Java 1.2.0 it has + * lacked that, and offers nothing you don't get with an ordinary + * {@code Map} (that forbids nulls). If some kind of store with + * transactional behavior is needed, it should be implemented in straight + * Java and kept in sync by using a {@link TransactionListener}. * @param attributeName The name of the attribute. */ void removeAttribute(String attributeName); @@ -152,6 +177,13 @@ void executeAsSessionUser(Connection conn, String statement) /** * Set an attribute to a value in the current session. + * + * @deprecated {@code Session}'s attribute store once had a special, and + * possibly useful, transactional behavior, but since PL/Java 1.2.0 it has + * lacked that, and offers nothing you don't get with an ordinary + * {@code Map} (that forbids nulls). If some kind of store with + * transactional behavior is needed, it should be implemented in straight + * Java and kept in sync by using a {@link TransactionListener}. * @param attributeName * @param value */ diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/SPIActions.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/SPIActions.java index c4604f3c..9101b621 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/SPIActions.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/SPIActions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -25,6 +25,7 @@ import org.postgresql.pljava.SavepointListener; import org.postgresql.pljava.Session; import org.postgresql.pljava.SessionManager; +import org.postgresql.pljava.TransactionListener; /** * Some methods used for testing the SPI JDBC driver. @@ -326,4 +327,37 @@ public static int transferPeopleWithSalary(int salary) throws SQLException { conn.close(); } } + + static TransactionListener s_tlstnr; + + public static void registerTransactionListener() throws SQLException + { + Session currentSession = SessionManager.current(); + if ( null == s_tlstnr ) + { + s_tlstnr = new XactListener(); + currentSession.addTransactionListener(s_tlstnr); + } + else + { + currentSession.removeTransactionListener(s_tlstnr); + s_tlstnr = null; + } + } + + static class XactListener implements TransactionListener + { + public void onAbort(Session s) + { + System.err.println("aborting a transaction"); + } + public void onCommit(Session s) + { + System.err.println("committing a transaction"); + } + public void onPrepare(Session s) + { + System.err.println("preparing a transaction"); + } + } } diff --git a/pljava-so/src/main/c/XactListener.c b/pljava-so/src/main/c/XactListener.c index ec96793f..a6e30dca 100644 --- a/pljava-so/src/main/c/XactListener.c +++ b/pljava-so/src/main/c/XactListener.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Thomas Hallgren + * Chapman Flack */ #include "pljava/Backend.h" #include "pljava/Exception.h" @@ -19,19 +23,19 @@ static jmethodID s_XactListener_onPrepare; static void xactCB(XactEvent event, void* arg) { - Ptr2Long p2l; - p2l.longVal = 0L; /* ensure that the rest is zeroed out */ - p2l.ptrVal = arg; switch(event) { case XACT_EVENT_ABORT: - JNI_callStaticVoidMethod(s_XactListener_class, s_XactListener_onAbort, p2l.longVal); + JNI_callStaticVoidMethod(s_XactListener_class, + s_XactListener_onAbort); break; case XACT_EVENT_COMMIT: - JNI_callStaticVoidMethod(s_XactListener_class, s_XactListener_onCommit, p2l.longVal); + JNI_callStaticVoidMethod(s_XactListener_class, + s_XactListener_onCommit); break; case XACT_EVENT_PREPARE: - JNI_callStaticVoidMethod(s_XactListener_class, s_XactListener_onPrepare, p2l.longVal); + JNI_callStaticVoidMethod(s_XactListener_class, + s_XactListener_onPrepare); break; } } @@ -42,38 +46,37 @@ void XactListener_initialize(void) JNINativeMethod methods[] = { { "_register", - "(J)V", - Java_org_postgresql_pljava_internal_XactListener__1register + "()V", + Java_org_postgresql_pljava_internal_XactListener__1register }, { "_unregister", - "(J)V", - Java_org_postgresql_pljava_internal_XactListener__1unregister + "()V", + Java_org_postgresql_pljava_internal_XactListener__1unregister }, - { 0, 0, 0 }}; + { 0, 0, 0 } + }; PgObject_registerNatives("org/postgresql/pljava/internal/XactListener", methods); s_XactListener_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/XactListener")); - s_XactListener_onAbort = PgObject_getStaticJavaMethod(s_XactListener_class, "onAbort", "(J)V"); - s_XactListener_onCommit = PgObject_getStaticJavaMethod(s_XactListener_class, "onCommit", "(J)V"); - s_XactListener_onPrepare = PgObject_getStaticJavaMethod(s_XactListener_class, "onPrepare", "(J)V"); + s_XactListener_onAbort = PgObject_getStaticJavaMethod(s_XactListener_class, "onAbort", "()V"); + s_XactListener_onCommit = PgObject_getStaticJavaMethod(s_XactListener_class, "onCommit", "()V"); + s_XactListener_onPrepare = PgObject_getStaticJavaMethod(s_XactListener_class, "onPrepare", "()V"); } /* * Class: org_postgresql_pljava_internal_XactListener * Method: _register - * Signature: (J)V + * Signature: ()V */ JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_XactListener__1register(JNIEnv* env, jclass cls, jlong listenerId) +Java_org_postgresql_pljava_internal_XactListener__1register(JNIEnv* env, jclass cls) { BEGIN_NATIVE PG_TRY(); { - Ptr2Long p2l; - p2l.longVal = listenerId; - RegisterXactCallback(xactCB, p2l.ptrVal); + RegisterXactCallback(xactCB, NULL); } PG_CATCH(); { @@ -86,17 +89,15 @@ Java_org_postgresql_pljava_internal_XactListener__1register(JNIEnv* env, jclass /* * Class: org_postgresql_pljava_internal_XactListener * Method: _unregister - * Signature: (J)V + * Signature: ()V */ JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_XactListener__1unregister(JNIEnv* env, jclass cls, jlong listenerId) +Java_org_postgresql_pljava_internal_XactListener__1unregister(JNIEnv* env, jclass cls) { BEGIN_NATIVE PG_TRY(); { - Ptr2Long p2l; - p2l.longVal = listenerId; - UnregisterXactCallback(xactCB, p2l.ptrVal); + UnregisterXactCallback(xactCB, NULL); } PG_CATCH(); { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java index f96a1b7a..89a89320 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java @@ -28,7 +28,11 @@ /** * An instance of this interface reflects the current session. The attribute - * store is transactional. + * store is deprecated. It had interesting transactional behavior until + * PL/Java 1.2.0, but since then it has behaved as any (non-null-allowing) Map. + * Anyone needing any sort of attribute store with transactional behavior will + * need to implement one and use a {@link TransactionListener} to keep it + * sync'd. * * @author Thomas Hallgren */ @@ -79,6 +83,16 @@ public void addSavepointListener(SavepointListener listener) SubXactListener.addListener(listener); } + /** + * Get an attribute from the session's attribute store. + * @deprecated {@code Session}'s attribute store once had a special, and + * possibly useful, transactional behavior, but since PL/Java 1.2.0 it has + * lacked that, and offers nothing you don't get with an ordinary + * {@code Map} (that forbids nulls). If some kind of store with + * transactional behavior is needed, it should be implemented in straight + * Java and kept in sync by using a {@link TransactionListener}. + */ + @Override public Object getAttribute(String attributeName) { return m_attributes.get(attributeName); @@ -108,11 +122,31 @@ public String getSessionUserName() } + /** + * Remove an attribute from the session's attribute store. + * @deprecated {@code Session}'s attribute store once had a special, and + * possibly useful, transactional behavior, but since PL/Java 1.2.0 it has + * lacked that, and offers nothing you don't get with an ordinary + * {@code Map} (that forbids nulls). If some kind of store with + * transactional behavior is needed, it should be implemented in straight + * Java and kept in sync by using a {@link TransactionListener}. + */ + @Override public void removeAttribute(String attributeName) { m_attributes.remove(attributeName); } + /** + * Set an attribute in the session's attribute store. + * @deprecated {@code Session}'s attribute store once had a special, and + * possibly useful, transactional behavior, but since PL/Java 1.2.0 it has + * lacked that, and offers nothing you don't get with an ordinary + * {@code Map} (that forbids nulls). If some kind of store with + * transactional behavior is needed, it should be implemented in straight + * Java and kept in sync by using a {@link TransactionListener}. + */ + @Override public void setAttribute(String attributeName, Object value) { m_attributes.put(attributeName, value); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TransactionalMap.java b/pljava/src/main/java/org/postgresql/pljava/internal/TransactionalMap.java index 3c562605..6a112d16 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TransactionalMap.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TransactionalMap.java @@ -15,6 +15,12 @@ import java.util.AbstractCollection; import java.util.NoSuchElementException; +/* + * The smallest quantum of reason for adding an import: to avoid a javadoc error + * in a deprecation note. + */ +import org.postgresql.pljava.TransactionListener; + /** * A TransactionalMap acts as a modifiable front for a backing map. All * modifications can be reverted by a call to abort or propagated to @@ -23,6 +29,13 @@ * The map is not synchronized so care should be taken if multiple threads * will access the map. * + * @deprecated This class (a) isn't exposed in {@code pljava-api}, (b) is only + * used to implement the once-transactional attribute map in {@code Session}, + * and (c) hasn't had transactional behavior even there, since 3ab90e5 + * (November 2005). Future code needing any kind of store sync'd to PostgreSQL + * transactions should implement that behavior with Java's ordinary tools, using + * a {@link TransactionListener} to be kept in sync with transactions. + * * @author Thomas Hallgren */ public class TransactionalMap extends HashMap @@ -386,4 +399,4 @@ public Object next() return TransactionalMap.this.get(super.next()); } } -} \ No newline at end of file +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java index e972e8da..269426aa 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java @@ -1,13 +1,20 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Thomas Hallgren + * Chapman Flack */ package org.postgresql.pljava.internal; import java.sql.SQLException; -import java.util.HashMap; +import java.util.ArrayDeque; +import java.util.Deque; import org.postgresql.pljava.TransactionListener; @@ -20,26 +27,29 @@ */ class XactListener { - private static final HashMap s_listeners = new HashMap(); + /* + * A non-thread-safe Deque; will be made safe by doing all mutations on the + * PG thread (even though actually calling into PG is necessary only when + * the size changes from 0 to 1 or 1 to 0). + */ + private static final Deque s_listeners = + new ArrayDeque(); - static void onAbort(long listenerId) throws SQLException + static void onAbort() throws SQLException { - TransactionListener listener = (TransactionListener)s_listeners.get(new Long(listenerId)); - if(listener != null) + for ( TransactionListener listener : s_listeners ) listener.onAbort(Backend.getSession()); } - static void onCommit(long listenerId) throws SQLException + static void onCommit() throws SQLException { - TransactionListener listener = (TransactionListener)s_listeners.get(new Long(listenerId)); - if(listener != null) + for ( TransactionListener listener : s_listeners ) listener.onCommit(Backend.getSession()); } - static void onPrepare(long listenerId) throws SQLException + static void onPrepare() throws SQLException { - TransactionListener listener = (TransactionListener)s_listeners.get(new Long(listenerId)); - if(listener != null) + for ( TransactionListener listener : s_listeners ) listener.onPrepare(Backend.getSession()); } @@ -47,9 +57,11 @@ static void addListener(TransactionListener listener) { synchronized(Backend.THREADLOCK) { - long key = System.identityHashCode(listener); - if(s_listeners.put(new Long(key), listener) != listener) - _register(key); + if ( s_listeners.contains(listener) ) + return; + s_listeners.push(listener); + if( 1 == s_listeners.size() ) + _register(); } } @@ -57,13 +69,14 @@ static void removeListener(TransactionListener listener) { synchronized(Backend.THREADLOCK) { - long key = System.identityHashCode(listener); - if(s_listeners.remove(new Long(key)) == listener) - _unregister(key); + if ( ! s_listeners.remove(listener) ) + return; + if ( 0 == s_listeners.size() ) + _unregister(); } } - private static native void _register(long listenerId); + private static native void _register(); - private static native void _unregister(long listenerId); + private static native void _unregister(); } From ea9c7dc0f8652eb5b88e1594dc770c4941a830cc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 12 Apr 2019 23:32:05 -0400 Subject: [PATCH 0276/1087] Simplify SubXactListener the same way. It is a tiny bit more involved, because the ids of savepoints are passed. To resolve the ids to PgSavepoint objects, the C code will upcall to the Java forId() method with the Backend.THREADLOCK monitor held (as in java_thread_pg_entry = block mode), so that method can get away with an assert Backend.threadMayEnterPG(). The upcall that actually calls the listeners will be made with the monitor released, as that's The Way It's Been Done. This still is the PG thread, but without the monitor held, Backend.threadMayEnterPG() won't know it, so the Java code shouldn't blindly assert it. --- pljava-so/src/main/c/PgSavepoint.c | 17 ++++- pljava-so/src/main/c/SubXactListener.c | 68 ++++++++++++------- .../src/main/include/pljava/PgSavepoint.h | 24 +++++++ .../pljava/internal/SubXactListener.java | 49 +++++++------ 4 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 pljava-so/src/main/include/pljava/PgSavepoint.h diff --git a/pljava-so/src/main/c/PgSavepoint.c b/pljava-so/src/main/c/PgSavepoint.c index bddeb037..d2f32955 100644 --- a/pljava-so/src/main/c/PgSavepoint.c +++ b/pljava-so/src/main/c/PgSavepoint.c @@ -16,17 +16,26 @@ #include #include "org_postgresql_pljava_internal_PgSavepoint.h" +#include "pljava/PgSavepoint.h" #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/type/String.h" #include "pljava/SPI.h" +static jclass s_PgSavepoint_class; +static jmethodID s_forId; static jfieldID s_nestLevel; extern void PgSavepoint_initialize(void); static void unwind(void (*f)(void), int nestingLevel, int xid); static void assertXid(SubTransactionId); +jobject pljava_PgSavepoint_forId(SubTransactionId subId) +{ + return JNI_callStaticObjectMethodLocked(s_PgSavepoint_class, s_forId, + (jint)subId); +} + void PgSavepoint_initialize(void) { StaticAssertStmt(sizeof(SubTransactionId) <= sizeof(jint), @@ -54,9 +63,13 @@ void PgSavepoint_initialize(void) PgObject_registerNatives("org/postgresql/pljava/internal/PgSavepoint", methods); - jclass cls = PgObject_getJavaClass( + jclass s_PgSavepoint_class = PgObject_getJavaClass( "org/postgresql/pljava/internal/PgSavepoint"); - s_nestLevel = PgObject_getJavaField(cls, "m_nestLevel", "I"); + s_forId = + PgObject_getStaticJavaMethod(s_PgSavepoint_class, "forId", + "(I)Lorg/postgresql/pljava/internal/PgSavepoint;"); + s_nestLevel = + PgObject_getJavaField(s_PgSavepoint_class, "m_nestLevel", "I"); } static void unwind(void (*f)(void), jint xid, jint nestingLevel) diff --git a/pljava-so/src/main/c/SubXactListener.c b/pljava-so/src/main/c/SubXactListener.c index f76aee5c..687196f3 100644 --- a/pljava-so/src/main/c/SubXactListener.c +++ b/pljava-so/src/main/c/SubXactListener.c @@ -24,22 +24,34 @@ static jmethodID s_SubXactListener_onAbort; static void subXactCB(SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, void* arg) { - Ptr2Long p2l; - p2l.longVal = 0L; /* ensure that the rest is zeroed out */ - p2l.ptrVal = arg; + /* + * Map the subids to PgSavepoints first - this function upcalls into Java + * without releasing the Backend.THREADLOCK monitor, so the called methods + * can know they're on the PG thread; Backend.threadMayEnterPG() is true. + */ + jobject sp = pljava_PgSavepoint_forId(mySubid); + jobject parent = pljava_PgSavepoint_forId(parentSubid); + + /* + * These upcalls are made with the monitor released. We are, of course, ON + * the PG thread, but this time with no monitor held to prevent another + * thread from stepping in. These methods should not blindly assert + * Backend.threadMayEnterPG(), as for some java_thread_pg_entry settings it + * won't be true. This is the legacy behavior, so not changed for 1.5.x. + */ switch(event) { case SUBXACT_EVENT_START_SUB: - JNI_callStaticVoidMethodLocked(s_SubXactListener_class, - s_SubXactListener_onStart, p2l.longVal, mySubid, parentSubid); + JNI_callStaticVoidMethod(s_SubXactListener_class, + s_SubXactListener_onStart, sp, parent); break; case SUBXACT_EVENT_COMMIT_SUB: - JNI_callStaticVoidMethodLocked(s_SubXactListener_class, - s_SubXactListener_onCommit, p2l.longVal, mySubid, parentSubid); + JNI_callStaticVoidMethod(s_SubXactListener_class, + s_SubXactListener_onCommit, sp, parent); break; case SUBXACT_EVENT_ABORT_SUB: - JNI_callStaticVoidMethodLocked(s_SubXactListener_class, - s_SubXactListener_onAbort, p2l.longVal, mySubid, parentSubid); + JNI_callStaticVoidMethod(s_SubXactListener_class, + s_SubXactListener_onAbort, sp, parent); } } @@ -49,38 +61,46 @@ void SubXactListener_initialize(void) JNINativeMethod methods[] = { { "_register", - "(J)V", + "()V", Java_org_postgresql_pljava_internal_SubXactListener__1register }, { "_unregister", - "(J)V", + "()V", Java_org_postgresql_pljava_internal_SubXactListener__1unregister }, - { 0, 0, 0 }}; + { 0, 0, 0 } + }; PgObject_registerNatives("org/postgresql/pljava/internal/SubXactListener", methods); s_SubXactListener_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/SubXactListener")); - s_SubXactListener_onAbort = PgObject_getStaticJavaMethod(s_SubXactListener_class, "onAbort", "(JII)V"); - s_SubXactListener_onCommit = PgObject_getStaticJavaMethod(s_SubXactListener_class, "onCommit", "(JII)V"); - s_SubXactListener_onStart = PgObject_getStaticJavaMethod(s_SubXactListener_class, "onStart", "(JII)V"); + s_SubXactListener_onAbort = + PgObject_getStaticJavaMethod(s_SubXactListener_class, "onAbort", + "(Lorg/postgresql/pljava/internal/PgSavepoint;" + "Lorg/postgresql/pljava/internal/PgSavepoint;)V"); + s_SubXactListener_onCommit = + PgObject_getStaticJavaMethod(s_SubXactListener_class, "onCommit", + "(Lorg/postgresql/pljava/internal/PgSavepoint;" + "Lorg/postgresql/pljava/internal/PgSavepoint;)V"); + s_SubXactListener_onStart = + PgObject_getStaticJavaMethod(s_SubXactListener_class, "onStart", + "(Lorg/postgresql/pljava/internal/PgSavepoint;" + "Lorg/postgresql/pljava/internal/PgSavepoint;)V"); } /* * Class: org_postgresql_pljava_internal_SubXactListener * Method: _register - * Signature: (J)V + * Signature: ()V */ JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_SubXactListener__1register(JNIEnv* env, jclass cls, jlong listenerId) +Java_org_postgresql_pljava_internal_SubXactListener__1register(JNIEnv* env, jclass cls) { BEGIN_NATIVE PG_TRY(); { - Ptr2Long p2l; - p2l.longVal = listenerId; - RegisterSubXactCallback(subXactCB, p2l.ptrVal); + RegisterSubXactCallback(subXactCB, NULL); } PG_CATCH(); { @@ -93,17 +113,15 @@ Java_org_postgresql_pljava_internal_SubXactListener__1register(JNIEnv* env, jcla /* * Class: org_postgresql_pljava_internal_SubXactListener * Method: _unregister - * Signature: (J)V + * Signature: ()V */ JNIEXPORT void JNICALL -Java_org_postgresql_pljava_internal_SubXactListener__1unregister(JNIEnv* env, jclass cls, jlong listenerId) +Java_org_postgresql_pljava_internal_SubXactListener__1unregister(JNIEnv* env, jclass cls) { BEGIN_NATIVE PG_TRY(); { - Ptr2Long p2l; - p2l.longVal = listenerId; - UnregisterSubXactCallback(subXactCB, p2l.ptrVal); + UnregisterSubXactCallback(subXactCB, NULL); } PG_CATCH(); { diff --git a/pljava-so/src/main/include/pljava/PgSavepoint.h b/pljava-so/src/main/include/pljava/PgSavepoint.h new file mode 100644 index 00000000..b5448f67 --- /dev/null +++ b/pljava-so/src/main/include/pljava/PgSavepoint.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019 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 + */ +#ifndef __pljava_PgSavepoint_h +#define __pljava_PgSavepoint_h + +#ifdef __cplusplus +extern "C" { +#endif + +extern jobject pljava_PgSavepoint_forId(SubTransactionId); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java index b1c8e40b..86c22f93 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java @@ -13,7 +13,8 @@ package org.postgresql.pljava.internal; import java.sql.SQLException; -import java.util.HashMap; +import java.util.ArrayDeque; +import java.util.Deque; import org.postgresql.pljava.SavepointListener; @@ -26,36 +27,39 @@ */ class SubXactListener { - private static final HashMap s_listeners = new HashMap(); + private static final Deque s_listeners = + new ArrayDeque(); - static void onAbort(long listenerId, int spId, int parentSpId) throws SQLException + static void onAbort(PgSavepoint sp, PgSavepoint parent) + throws SQLException { - SavepointListener listener = (SavepointListener)s_listeners.get(new Long(listenerId)); - if(listener != null) - listener.onAbort(Backend.getSession(), PgSavepoint.forId(spId), PgSavepoint.forId(parentSpId)); + for ( SavepointListener listener : s_listeners ) + listener.onAbort(Backend.getSession(), sp, parent); } - static void onCommit(long listenerId, int spId, int parentSpId) throws SQLException + static void onCommit(PgSavepoint sp, PgSavepoint parent) + throws SQLException { - SavepointListener listener = (SavepointListener)s_listeners.get(new Long(listenerId)); - if(listener != null) - listener.onCommit(Backend.getSession(), PgSavepoint.forId(spId), PgSavepoint.forId(parentSpId)); + for ( SavepointListener listener : s_listeners ) + listener.onCommit(Backend.getSession(), sp, parent); } - static void onStart(long listenerId, int spId, int parentSpId) throws SQLException + static void onStart(PgSavepoint sp, PgSavepoint parent) + throws SQLException { - SavepointListener listener = (SavepointListener)s_listeners.get(new Long(listenerId)); - if(listener != null) - listener.onStart(Backend.getSession(), PgSavepoint.forId(spId), PgSavepoint.forId(parentSpId)); + for ( SavepointListener listener : s_listeners ) + listener.onStart(Backend.getSession(), sp, parent); } static void addListener(SavepointListener listener) { synchronized(Backend.THREADLOCK) { - long key = System.identityHashCode(listener); - if(s_listeners.put(new Long(key), listener) != listener) - _register(key); + if ( s_listeners.contains(listener) ) + return; + s_listeners.push(listener); + if( 1 == s_listeners.size() ) + _register(); } } @@ -63,13 +67,14 @@ static void removeListener(SavepointListener listener) { synchronized(Backend.THREADLOCK) { - long key = System.identityHashCode(listener); - if(s_listeners.remove(new Long(key)) == listener) - _unregister(key); + if ( ! s_listeners.remove(listener) ) + return; + if ( 0 == s_listeners.size() ) + _unregister(); } } - private static native void _register(long listenerId); + private static native void _register(); - private static native void _unregister(long listenerId); + private static native void _unregister(); } From 9bb3f0ce1746aacc0756c609f841af69a48bbcc2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 13 Apr 2019 14:25:27 -0400 Subject: [PATCH 0277/1087] Iterate on a copy of Xact/SubXact handler list ... as was done back in 7d9eed3, just in case some listener's handler includes an unregister action. --- .../postgresql/pljava/internal/SubXactListener.java | 10 +++++++--- .../org/postgresql/pljava/internal/XactListener.java | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java index 86c22f93..799c8869 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java @@ -33,21 +33,25 @@ class SubXactListener static void onAbort(PgSavepoint sp, PgSavepoint parent) throws SQLException { - for ( SavepointListener listener : s_listeners ) + // Take a snapshot. Handlers might unregister during event processing + for ( SavepointListener listener : + s_listeners.toArray(new SavepointListener[s_listeners.size()]) ) listener.onAbort(Backend.getSession(), sp, parent); } static void onCommit(PgSavepoint sp, PgSavepoint parent) throws SQLException { - for ( SavepointListener listener : s_listeners ) + for ( SavepointListener listener : + s_listeners.toArray(new SavepointListener[s_listeners.size()]) ) listener.onCommit(Backend.getSession(), sp, parent); } static void onStart(PgSavepoint sp, PgSavepoint parent) throws SQLException { - for ( SavepointListener listener : s_listeners ) + for ( SavepointListener listener : + s_listeners.toArray(new SavepointListener[s_listeners.size()]) ) listener.onStart(Backend.getSession(), sp, parent); } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java index 269426aa..08cc6bef 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java @@ -37,19 +37,23 @@ class XactListener static void onAbort() throws SQLException { - for ( TransactionListener listener : s_listeners ) + // Take a snapshot. Handlers might unregister during event processing + for ( TransactionListener listener : + s_listeners.toArray(new TransactionListener[s_listeners.size()]) ) listener.onAbort(Backend.getSession()); } static void onCommit() throws SQLException { - for ( TransactionListener listener : s_listeners ) + for ( TransactionListener listener : + s_listeners.toArray(new TransactionListener[s_listeners.size()]) ) listener.onCommit(Backend.getSession()); } static void onPrepare() throws SQLException { - for ( TransactionListener listener : s_listeners ) + for ( TransactionListener listener : + s_listeners.toArray(new TransactionListener[s_listeners.size()]) ) listener.onPrepare(Backend.getSession()); } From 88d24ae5d59589848bf711afa7492729d80ef96e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 4 Jun 2019 22:00:19 -0400 Subject: [PATCH 0278/1087] Missed detail in 'simplify SubXactListener' Somehow overlooked the implicit-declaration warning. --- pljava-so/src/main/c/SubXactListener.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/SubXactListener.c b/pljava-so/src/main/c/SubXactListener.c index 687196f3..ee33bd48 100644 --- a/pljava-so/src/main/c/SubXactListener.c +++ b/pljava-so/src/main/c/SubXactListener.c @@ -12,7 +12,7 @@ */ #include "pljava/Backend.h" #include "pljava/Exception.h" -#include "pljava/SPI.h" +#include "pljava/PgSavepoint.h" #include "org_postgresql_pljava_internal_SubXactListener.h" #include From fb9d5820719b017e250c7780ddb7a47f7c7434a1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 12 Mar 2019 22:47:42 -0400 Subject: [PATCH 0279/1087] Factor out 'specialization' check in SQL generator. This will make it easier to support idioms where a Java parameter or return is typed A (or some C that directly or indirectly extends A) and the generator needs to find B. Fixes a latent (in this branch) bug where parameterized return types could be written into the AS string in all their glory, instead of properly as their erasures. It's latent in this branch because the only use made of parameterized return types is for SETOF with Iterator, and the return type isn't written for that case, but would become a real bug with any future addition of other idioms involving parameterized return types. First done in an explorative branch against master, with this generally-useful refactoring rebased here. A couple other minor differences from master are also included here, to keep future merges simpler. --- .../pljava/sqlgen/DDRProcessor.java | 79 ++++++++++++------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index 144fa241..e2d44433 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -572,7 +572,7 @@ else if ( ! defaultImplementor.equals( imp) ) if ( null == fwdSnips || null == revSnips ) return; // error already reported - + try { DDRWriter.emit( fwdSnips, revSnips, this); @@ -1262,7 +1262,7 @@ else if ( Called.INSTEAD_OF.equals( _called) ) refOld = ! "".equals( _tableOld); refNew = ! "".equals( _tableNew); - if ( ( refOld || refNew ) ) + if ( refOld || refNew ) { if ( ! Called.AFTER.equals( _called) ) msg( Kind.ERROR, func.func, origin, @@ -1371,7 +1371,7 @@ public String[] deployStrings() sb.append("DEFERRABLE INITIALLY DEFERRED"); break; } - } + } if ( refOld || refNew ) { sb.append( "\n\tREFERENCING"); @@ -1462,6 +1462,7 @@ class FunctionImpl boolean setof = false; TypeMirror setofComponent = null; boolean trigger = false; + TypeMirror returnTypeMapKey = null; FunctionImpl(ExecutableElement e) { @@ -1516,6 +1517,7 @@ public boolean characterize() ExecutableType et = (ExecutableType)func.asType(); List ptms = et.getParameterTypes(); + List typeArgs; int arity = ptms.size(); if ( ! "".equals( type()) @@ -1533,34 +1535,16 @@ public boolean characterize() return false; } } - else if ( typu.isAssignable( typu.erasure( ret), TY_ITERATOR) ) + else if ( null != (typeArgs = specialization( ret, TY_ITERATOR)) ) { setof = true; - List pending = new LinkedList(); - pending.add( ret); - while ( ! pending.isEmpty() ) + if ( 1 != typeArgs.size() ) { - TypeMirror tm = pending.remove( 0); - if ( typu.isSameType( typu.erasure( tm), TY_ITERATOR) ) - { - DeclaredType dt = (DeclaredType)tm; - List typeArgs = - dt.getTypeArguments(); - if ( 1 != typeArgs.size() ) - { - msg( Kind.ERROR, func, - "Need one type argument for Iterator " + - "return type"); - return false; - } - setofComponent = typeArgs.get( 0); - break; - } - else - { - pending.addAll( typu.directSupertypes( tm)); - } + msg( Kind.ERROR, func, + "Need one type argument for Iterator return type"); + return false; } + setofComponent = typeArgs.get( 0); if ( null == setofComponent ) { msg( Kind.ERROR, func, @@ -1583,6 +1567,8 @@ else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) trigger = true; } } + + returnTypeMapKey = ret; if ( ! setof && -1 != rows() ) msg( Kind.ERROR, func, @@ -1652,7 +1638,7 @@ void appendParams( StringBuilder sb, boolean dflts) void appendAS( StringBuilder sb) { if ( ! ( complexViaInOut || setof || trigger ) ) - sb.append( func.getReturnType()).append( '='); + sb.append( typu.erasure( func.getReturnType())).append( '='); Element e = func.getEnclosingElement(); if ( ! e.getKind().equals( ElementKind.CLASS) ) msg( Kind.ERROR, func, @@ -1682,7 +1668,7 @@ else if ( null != setofComponent ) else if ( setof ) sb.append( "pg_catalog.RECORD"); else - sb.append( tmpr.getSQLType( func.getReturnType(), func)); + sb.append( tmpr.getSQLType( returnTypeMapKey, func)); } sb.append( "\n\tLANGUAGE "); if ( Trust.SANDBOXED.equals( trust()) ) @@ -1741,6 +1727,39 @@ public String[] undeployStrings() rslt [ rslt.length - 1 ] = sb.toString(); return rslt; } + + /** + * Test whether the type {@code tm} is, directly or indirectly, + * a specialization of generic type {@code dt}. + * @param tm a type to be checked + * @param dt known generic type to check for + * @return null if {@code tm} does not extend {@code dt}, otherwise the + * list of type arguments with which it specializes {@code dt} + */ + List specialization( + TypeMirror tm, DeclaredType dt) + { + if ( ! typu.isAssignable( typu.erasure( tm), dt) ) + return null; + + List pending = new LinkedList(); + pending.add( tm); + while ( ! pending.isEmpty() ) + { + tm = pending.remove( 0); + if ( typu.isSameType( typu.erasure( tm), dt) ) + return ((DeclaredType)tm).getTypeArguments(); + pending.addAll( typu.directSupertypes( tm)); + } + /* + * This is a can't-happen: tm is assignable to dt but has no + * supertype that's dt? Could throw an AssertionError, but returning + * an empty list will lead the caller to report an error, and that + * will give more information about the location in the source being + * compiled. + */ + return Collections.emptyList(); + } } static enum BaseUDTFunctionID @@ -2676,7 +2695,7 @@ else if ( ! array ) // expression intended to match SQL types that are arrays static final Pattern arrayish = - Pattern.compile( "(?si:(?:\\[\\s*\\d*\\s*\\]|ARRAY)\\s*)$"); + Pattern.compile( "(?si:(?:\\[\\s*+\\d*+\\s*+\\]|ARRAY)\\s*+)$"); /** * Work around bizarre javac behavior that silently supplies an Error From 77ea9d377a98de9632eefeb58c20b851f84e6c68 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 15 Mar 2019 20:13:04 -0400 Subject: [PATCH 0280/1087] Use JNI header constant field values. Neither the javah manual page, nor the Java 8+ javac manual page, actually comes out and says that static final primitive constants in a class get #defines created for them in the JNI header, but they do. The new-in-Java-8 @Native annotation for fields might suggest that it is needed to get a field copied into the .h file, but really seems only to ensure that a header is generated even if the class has no native methods. As long as a header is generated, it seems to get all the static final primitive fields (that have constant-expression initializers) whether so annotated or not. Therefore, take advantage of those to avoid divergent values between Java and C, either by directly using the #defines from Java in the C code, or by using StaticAssertStmt to confirm the values match the existing C constants they are intended to match. In passing, update the SPI return codes to include the newer ones. --- pljava-so/src/main/c/ExecutionPlan.c | 10 +- pljava-so/src/main/c/SPI.c | 43 +++++ pljava-so/src/main/c/TypeOid.c | 52 ++++++ .../org/postgresql/pljava/internal/SPI.java | 152 ++++++++---------- .../org/postgresql/pljava/jdbc/TypeOid.java | 75 ++++++--- 5 files changed, 218 insertions(+), 114 deletions(-) create mode 100644 pljava-so/src/main/c/TypeOid.c diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index f3725d5a..219839da 100644 --- a/pljava-so/src/main/c/ExecutionPlan.c +++ b/pljava-so/src/main/c/ExecutionPlan.c @@ -33,10 +33,12 @@ /* Class 07 - Dynamic SQL Error */ #define ERRCODE_PARAMETER_COUNT_MISMATCH MAKE_SQLSTATE('0','7', '0','0','1') -/* These three values must match those in ExecutionPlan.java */ -#define SPI_READONLY_DEFAULT 0 -#define SPI_READONLY_FORCED 1 -#define SPI_READONLY_CLEARED 2 +#define SPI_READONLY_DEFAULT \ + org_postgresql_pljava_internal_ExecutionPlan_SPI_READONLY_FORCED +#define SPI_READONLY_FORCED \ + org_postgresql_pljava_internal_ExecutionPlan_SPI_READONLY_FORCED +#define SPI_READONLY_CLEARED \ + org_postgresql_pljava_internal_ExecutionPlan_SPI_READONLY_CLEARED static jclass s_ExecutionPlan_class; static jmethodID s_ExecutionPlan_init; diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index 8ea7648a..9db51fcb 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -23,9 +23,52 @@ #include #endif +#define CONFIRMCONST(c) \ +StaticAssertStmt((c) == (org_postgresql_pljava_internal_##c), \ + "Java/C value mismatch for " #c) + extern void SPI_initialize(void); void SPI_initialize(void) { + /* Statically assert that the Java code has the right values for these. */ + CONFIRMCONST(SPI_ERROR_CONNECT); + CONFIRMCONST(SPI_ERROR_COPY); + CONFIRMCONST(SPI_ERROR_OPUNKNOWN); + CONFIRMCONST(SPI_ERROR_UNCONNECTED); + CONFIRMCONST(SPI_ERROR_CURSOR); + CONFIRMCONST(SPI_ERROR_ARGUMENT); + CONFIRMCONST(SPI_ERROR_PARAM); + CONFIRMCONST(SPI_ERROR_TRANSACTION); + CONFIRMCONST(SPI_ERROR_NOATTRIBUTE); + CONFIRMCONST(SPI_ERROR_NOOUTFUNC); + CONFIRMCONST(SPI_ERROR_TYPUNKNOWN); +#if PG_VERSION_NUM >= 100000 + CONFIRMCONST(SPI_ERROR_REL_DUPLICATE); + CONFIRMCONST(SPI_ERROR_REL_NOT_FOUND); +#endif + + CONFIRMCONST(SPI_OK_CONNECT); + CONFIRMCONST(SPI_OK_FINISH); + CONFIRMCONST(SPI_OK_FETCH); + CONFIRMCONST(SPI_OK_UTILITY); + CONFIRMCONST(SPI_OK_SELECT); + CONFIRMCONST(SPI_OK_SELINTO); + CONFIRMCONST(SPI_OK_INSERT); + CONFIRMCONST(SPI_OK_DELETE); + CONFIRMCONST(SPI_OK_UPDATE); + CONFIRMCONST(SPI_OK_CURSOR); + CONFIRMCONST(SPI_OK_INSERT_RETURNING); + CONFIRMCONST(SPI_OK_DELETE_RETURNING); + CONFIRMCONST(SPI_OK_UPDATE_RETURNING); +#if PG_VERSION_NUM >= 80400 + CONFIRMCONST(SPI_OK_REWRITTEN); +#endif +#if PG_VERSION_NUM >= 100000 + CONFIRMCONST(SPI_OK_REL_REGISTER); + CONFIRMCONST(SPI_OK_REL_UNREGISTER); + CONFIRMCONST(SPI_OK_TD_REGISTER); +#endif + JNINativeMethod methods[] = { { "_exec", diff --git a/pljava-so/src/main/c/TypeOid.c b/pljava-so/src/main/c/TypeOid.c new file mode 100644 index 00000000..1e468627 --- /dev/null +++ b/pljava-so/src/main/c/TypeOid.c @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019 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 + */ + +#include +#include + +#include "org_postgresql_pljava_jdbc_TypeOid.h" + +/* + * A compilation unit with no run-time purpose, merely to hold a bunch of + * StaticAssertStmts to confirm at compile time that we haven't fat-fingered + * any of the OID constants that are known to the Java code. + */ + +#define CONFIRMCONST(c) \ +StaticAssertStmt((c) == (org_postgresql_pljava_jdbc_TypeOid_##c), \ + "Java/C value mismatch for " #c) + +/* + * Class: org_postgresql_pljava_jdbc_TypeOid + * Method: _dummy + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_org_postgresql_pljava_jdbc_TypeOid__1dummy(JNIEnv * env, jclass cls) +{ + CONFIRMCONST(InvalidOid); + CONFIRMCONST(INT2OID); + CONFIRMCONST(INT4OID); + CONFIRMCONST(INT8OID); + CONFIRMCONST(TEXTOID); + CONFIRMCONST(NUMERICOID); + CONFIRMCONST(FLOAT4OID); + CONFIRMCONST(FLOAT8OID); + CONFIRMCONST(BOOLOID); + CONFIRMCONST(DATEOID); + CONFIRMCONST(TIMEOID); + CONFIRMCONST(TIMESTAMPOID); + CONFIRMCONST(TIMESTAMPTZOID); + CONFIRMCONST(BYTEAOID); + CONFIRMCONST(VARCHAROID); + CONFIRMCONST(OIDOID); + CONFIRMCONST(BPCHAROID); +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java index 61346c18..282ac690 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -20,28 +20,37 @@ */ public class SPI { - public static final int ERROR_CONNECT = -1; - public static final int ERROR_COPY = -2; - public static final int ERROR_OPUNKNOWN = -3; - public static final int ERROR_UNCONNECTED = -4; - public static final int ERROR_CURSOR = -5; - public static final int ERROR_ARGUMENT = -6; - public static final int ERROR_PARAM = -7; - public static final int ERROR_TRANSACTION = -8; - public static final int ERROR_NOATTRIBUTE = -9; - public static final int ERROR_NOOUTFUNC = -10; - public static final int ERROR_TYPUNKNOWN = -11; + public static final int ERROR_CONNECT = -1; + public static final int ERROR_COPY = -2; + public static final int ERROR_OPUNKNOWN = -3; + public static final int ERROR_UNCONNECTED = -4; + public static final int ERROR_CURSOR = -5; + public static final int ERROR_ARGUMENT = -6; + public static final int ERROR_PARAM = -7; + public static final int ERROR_TRANSACTION = -8; + public static final int ERROR_NOATTRIBUTE = -9; + public static final int ERROR_NOOUTFUNC = -10; + public static final int ERROR_TYPUNKNOWN = -11; + public static final int ERROR_REL_DUPLICATE = -12; + public static final int ERROR_REL_NOT_FOUND = -13; - public static final int OK_CONNECT = 1; - public static final int OK_FINISH = 2; - public static final int OK_FETCH = 3; - public static final int OK_UTILITY = 4; - public static final int OK_SELECT = 5; - public static final int OK_SELINTO = 6; - public static final int OK_INSERT = 7; - public static final int OK_DELETE = 8; - public static final int OK_UPDATE = 9; - public static final int OK_CURSOR = 10; + public static final int OK_CONNECT = 1; + public static final int OK_FINISH = 2; + public static final int OK_FETCH = 3; + public static final int OK_UTILITY = 4; + public static final int OK_SELECT = 5; + public static final int OK_SELINTO = 6; + public static final int OK_INSERT = 7; + public static final int OK_DELETE = 8; + public static final int OK_UPDATE = 9; + public static final int OK_CURSOR = 10; + public static final int OK_INSERT_RETURNING = 11; + public static final int OK_DELETE_RETURNING = 12; + public static final int OK_UPDATE_RETURNING = 13; + public static final int OK_REWRITTEN = 14; + public static final int OK_REL_REGISTER = 15; + public static final int OK_REL_UNREGISTER = 16; + public static final int OK_TD_REGISTER = 17; /** * Execute a command using the internal SPI_exec function. @@ -111,79 +120,46 @@ public static TupleTable getTupTable(TupleDesc known) */ /* * XXX PG 11 introduces a real SPI_result_code_string function. + * The strings it returns are like these with SPI_ prepended. */ public static String getResultText(int resultCode) { - String s; switch(resultCode) { - case ERROR_CONNECT: - s = "ERROR_CONNECT"; - break; - case ERROR_COPY: - s = "ERROR_COPY"; - break; - case ERROR_OPUNKNOWN: - s = "ERROR_OPUNKNOWN"; - break; - case ERROR_UNCONNECTED: - s = "ERROR_UNCONNECTED"; - break; - case ERROR_CURSOR: - s = "ERROR_CURSOR"; - break; - case ERROR_ARGUMENT: - s = "ERROR_ARGUMENT"; - break; - case ERROR_PARAM: - s = "ERROR_PARAM"; - break; - case ERROR_TRANSACTION: - s = "ERROR_TRANSACTION"; - break; - case ERROR_NOATTRIBUTE: - s = "ERROR_NOATTRIBUTE"; - break; - case ERROR_NOOUTFUNC: - s = "ERROR_NOOUTFUNC"; - break; - case ERROR_TYPUNKNOWN: - s = "ERROR_TYPUNKNOWN"; - break; - case OK_CONNECT: - s = "OK_CONNECT"; - break; - case OK_FINISH: - s = "OK_FINISH"; - break; - case OK_FETCH: - s = "OK_FETCH"; - break; - case OK_UTILITY: - s = "OK_UTILITY"; - break; - case OK_SELECT: - s = "OK_SELECT"; - break; - case OK_SELINTO: - s = "OK_SELINTO"; - break; - case OK_INSERT: - s = "OK_INSERT"; - break; - case OK_DELETE: - s = "OK_DELETE"; - break; - case OK_UPDATE: - s = "OK_UPDATE"; - break; - case OK_CURSOR: - s = "OK_CURSOR"; - break; - default: - s = "Unkown result code: " + resultCode; + case ERROR_CONNECT: return "ERROR_CONNECT"; + case ERROR_COPY: return "ERROR_COPY"; + case ERROR_OPUNKNOWN: return "ERROR_OPUNKNOWN"; + case ERROR_UNCONNECTED: return "ERROR_UNCONNECTED"; + case ERROR_CURSOR: return "ERROR_CURSOR"; + case ERROR_ARGUMENT: return "ERROR_ARGUMENT"; + case ERROR_PARAM: return "ERROR_PARAM"; + case ERROR_TRANSACTION: return "ERROR_TRANSACTION"; + case ERROR_NOATTRIBUTE: return "ERROR_NOATTRIBUTE"; + case ERROR_NOOUTFUNC: return "ERROR_NOOUTFUNC"; + case ERROR_TYPUNKNOWN: return "ERROR_TYPUNKNOWN"; + case ERROR_REL_DUPLICATE: return "ERROR_REL_DUPLICATE"; + case ERROR_REL_NOT_FOUND: return "ERROR_REL_NOT_FOUND"; + + case OK_CONNECT: return "OK_CONNECT"; + case OK_FINISH: return "OK_FINISH"; + case OK_FETCH: return "OK_FETCH"; + case OK_UTILITY: return "OK_UTILITY"; + case OK_SELECT: return "OK_SELECT"; + case OK_SELINTO: return "OK_SELINTO"; + case OK_INSERT: return "OK_INSERT"; + case OK_DELETE: return "OK_DELETE"; + case OK_UPDATE: return "OK_UPDATE"; + case OK_CURSOR: return "OK_CURSOR"; + case OK_INSERT_RETURNING: return "OK_INSERT_RETURNING"; + case OK_DELETE_RETURNING: return "OK_DELETE_RETURNING"; + case OK_UPDATE_RETURNING: return "OK_UPDATE_RETURNING"; + case OK_REWRITTEN: return "OK_REWRITTEN"; + case OK_REL_REGISTER: return "OK_REL_REGISTER"; + case OK_REL_UNREGISTER: return "OK_REL_UNREGISTER"; + case OK_TD_REGISTER: return "OK_TD_REGISTER"; + + default: return "Unknown result code: " + resultCode; } - return s; } @Deprecated diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java index 12f86b88..407a9142 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -12,22 +18,47 @@ * Provides constants for well-known backend OIDs for the types we commonly * use. */ -public class TypeOid { - public static final Oid INVALID = new Oid(0); - public static final Oid INT2 = new Oid(21); - public static final Oid INT4 = new Oid(23); - public static final Oid INT8 = new Oid(20); - public static final Oid TEXT = new Oid(25); - public static final Oid NUMERIC = new Oid(1700); - public static final Oid FLOAT4 = new Oid(700); - public static final Oid FLOAT8 = new Oid(701); - public static final Oid BOOL = new Oid(16); - public static final Oid DATE = new Oid(1082); - public static final Oid TIME = new Oid(1083); - public static final Oid TIMESTAMP = new Oid(1114); - public static final Oid TIMESTAMPTZ = new Oid(1184); - public static final Oid BYTEA = new Oid(17); - public static final Oid VARCHAR = new Oid(1043); - public static final Oid OID = new Oid(26); - public static final Oid BPCHAR = new Oid(1042); +public class TypeOid +{ + public static final int InvalidOid = 0; + public static final int INT2OID = 21; + public static final int INT4OID = 23; + public static final int INT8OID = 20; + public static final int TEXTOID = 25; + public static final int NUMERICOID = 1700; + public static final int FLOAT4OID = 700; + public static final int FLOAT8OID = 701; + public static final int BOOLOID = 16; + public static final int DATEOID = 1082; + public static final int TIMEOID = 1083; + public static final int TIMESTAMPOID = 1114; + public static final int TIMESTAMPTZOID = 1184; + public static final int BYTEAOID = 17; + public static final int VARCHAROID = 1043; + public static final int OIDOID = 26; + public static final int BPCHAROID = 1042; + + public static final Oid INVALID = new Oid(InvalidOid); + public static final Oid INT2 = new Oid(INT2OID); + public static final Oid INT4 = new Oid(INT4OID); + public static final Oid INT8 = new Oid(INT8OID); + public static final Oid TEXT = new Oid(TEXTOID); + public static final Oid NUMERIC = new Oid(NUMERICOID); + public static final Oid FLOAT4 = new Oid(FLOAT4OID); + public static final Oid FLOAT8 = new Oid(FLOAT8OID); + public static final Oid BOOL = new Oid(BOOLOID); + public static final Oid DATE = new Oid(DATEOID); + public static final Oid TIME = new Oid(TIMEOID); + public static final Oid TIMESTAMP = new Oid(TIMESTAMPOID); + public static final Oid TIMESTAMPTZ = new Oid(TIMESTAMPTZOID); + public static final Oid BYTEA = new Oid(BYTEAOID); + public static final Oid VARCHAR = new Oid(VARCHAROID); + public static final Oid OID = new Oid(OIDOID); + public static final Oid BPCHAR = new Oid(BPCHAROID); + + /* + * Before Java 8 with the @Native annotation, a class needs at least one + * native method to trigger generation of a .h file. + */ + private static native void _dummy(); } From a79958a226d9bd523d9cd8a2f905d5d9200b3264 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 13 Apr 2019 14:54:07 -0400 Subject: [PATCH 0281/1087] Adopt generics in more places where applicable. A solid, if non-exhaustive, effort to bring the code up to date with Java generics and transparent boxing/unboxing, based on a simplistic mechanical search for places where unnecessary explicit casting/boxing/ unboxing was being done. In the files touched, also elide a bunch of 'this.' qualifications on instance method calls. And fix the spelling of 'Coercion' in three methods. Bonus: fix three very old thinkos in SPIPreparedStatement that no one ever seems to have run into or reported. --- .../org/postgresql/pljava/ObjectPool.java | 10 +- .../java/org/postgresql/pljava/Session.java | 2 +- .../pljava/internal/ExecutionPlan.java | 17 ++- .../pljava/internal/ObjectPoolImpl.java | 55 +++++--- .../org/postgresql/pljava/internal/Oid.java | 13 +- .../postgresql/pljava/internal/Session.java | 3 +- .../pljava/jdbc/ObjectResultSet.java | 120 ++++++++-------- .../postgresql/pljava/jdbc/SPIConnection.java | 44 +++--- .../pljava/jdbc/SPIPreparedStatement.java | 131 +++++++++--------- .../pljava/jdbc/SQLInputFromTuple.java | 52 +++---- .../pljava/jdbc/SQLOutputToTuple.java | 67 ++++----- .../pljava/jdbc/SingleRowWriter.java | 6 +- .../pljava/jdbc/SyntheticResultSet.java | 18 +-- .../pljava/jdbc/TriggerResultSet.java | 4 +- .../pljava/management/Commands.java | 8 +- .../org/postgresql/pljava/sqlj/Loader.java | 19 ++- 16 files changed, 303 insertions(+), 266 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/ObjectPool.java b/pljava-api/src/main/java/org/postgresql/pljava/ObjectPool.java index fb04ba0f..6a3cb21a 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/ObjectPool.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/ObjectPool.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -21,7 +21,7 @@ * for the class to be pooled, which must implement {@link PooledObject}. * @author Thomas Hallgren */ -public interface ObjectPool +public interface ObjectPool { /** * Obtain a pooled object, calling its {@link PooledObject#activate()} @@ -30,7 +30,7 @@ public interface ObjectPool * * @return A new object or an object found in the pool. */ - PooledObject activateInstance() + T activateInstance() throws SQLException; /** @@ -38,13 +38,13 @@ PooledObject activateInstance() * to the pool. * @param instance The instance to passivate. */ - void passivateInstance(PooledObject instance) + void passivateInstance(T instance) throws SQLException; /** * Call the {@link PooledObject#remove()} method and evict the object * from the pool. */ - void removeInstance(PooledObject instance) + void removeInstance(T instance) throws SQLException; } 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 7a7bc0f9..463d204d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Session.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Session.java @@ -70,7 +70,7 @@ public interface Session * constructor for one argument of type ObjectPool. * @return An object pool that pools object of the given class. */ - ObjectPool getObjectPool(Class cls); + ObjectPool getObjectPool(Class cls); /** * Return the current effective database user name. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java index 77a6eca7..3e232666 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java @@ -114,8 +114,11 @@ private long getExecutionPlanPtr() throws SQLException /** * MRU cache for prepared plans. + * The key type is Object, not PlanKey, because for a statement with no + * parameters, the statement itself is used as the key, rather than + * constructing a PlanKey. */ - static final class PlanCache extends LinkedHashMap + static final class PlanCache extends LinkedHashMap { private final int m_cacheSize; @@ -125,12 +128,14 @@ public PlanCache(int cacheSize) m_cacheSize = cacheSize; } - protected boolean removeEldestEntry(Map.Entry eldest) + @Override + protected boolean removeEldestEntry( + Map.Entry eldest) { if(this.size() <= m_cacheSize) return false; - ExecutionPlan evicted = (ExecutionPlan)eldest.getValue(); + ExecutionPlan evicted = eldest.getValue(); /* * See close() below for why 'evicted' is not enqueue()d right here. */ @@ -181,7 +186,7 @@ public int hashCode() } } - private static final Map s_planCache; + private static final Map s_planCache; private final Object m_key; @@ -205,7 +210,7 @@ private ExecutionPlan(DualState.Key cookie, long resourceOwner, */ public void close() { - ExecutionPlan old = (ExecutionPlan)s_planCache.put(m_key, this); + ExecutionPlan old = s_planCache.put(m_key, this); /* * For now, do NOT immediately enqueue() a non-null 'old'. It could * still be live via a Portal that is still retrieving results. Java @@ -300,7 +305,7 @@ public static ExecutionPlan prepare(String statement, Oid[] argTypes) ? (Object)statement : (Object)new PlanKey(statement, argTypes); - ExecutionPlan plan = (ExecutionPlan)s_planCache.remove(key); + ExecutionPlan plan = s_planCache.remove(key); if(plan == null) { synchronized(Backend.THREADLOCK) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java b/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java index 7b42c11f..3b132c88 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root directory of this distribution or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2019 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -14,26 +20,27 @@ import org.postgresql.pljava.ObjectPool; import org.postgresql.pljava.PooledObject; -class ObjectPoolImpl implements ObjectPool +class ObjectPoolImpl implements ObjectPool { /** * An InstanceHandle is a link in a single linked list that * holds on to a ResultSetProvider. */ - private static class PooledObjectHandle + private static class PooledObjectHandle { - private PooledObject m_instance; - private PooledObjectHandle m_next; + private T m_instance; + private PooledObjectHandle m_next; } private static Class[] s_ctorSignature = { ObjectPool.class }; private static PooledObjectHandle s_handlePool; - private static final IdentityHashMap s_poolCache = new IdentityHashMap(); + private static final IdentityHashMap,ObjectPoolImpl> + s_poolCache = new IdentityHashMap,ObjectPoolImpl>(); - private final Constructor m_ctor; - private PooledObjectHandle m_providerPool; + private final Constructor m_ctor; + private PooledObjectHandle m_providerPool; - private ObjectPoolImpl(Class c) + private ObjectPoolImpl(Class c) { if(!PooledObject.class.isAssignableFrom(c)) throw new IllegalArgumentException("Class " + c + " does not implement the " + @@ -59,9 +66,10 @@ private ObjectPoolImpl(Class c) * @return * @throws SQLException */ - public static ObjectPoolImpl getObjectPool(Class cls) + public static ObjectPoolImpl + getObjectPool(Class cls) { - ObjectPoolImpl pool = (ObjectPoolImpl)s_poolCache.get(cls); + ObjectPoolImpl pool = (ObjectPoolImpl)s_poolCache.get(cls); if(pool == null) { pool = new ObjectPoolImpl(cls); @@ -70,11 +78,11 @@ public static ObjectPoolImpl getObjectPool(Class cls) return pool; } - public PooledObject activateInstance() + public T activateInstance() throws SQLException { - PooledObject instance; - PooledObjectHandle handle = m_providerPool; + T instance; + PooledObjectHandle handle = m_providerPool; if(handle != null) { m_providerPool = handle.m_next; @@ -90,7 +98,7 @@ public PooledObject activateInstance() { try { - instance = (PooledObject)m_ctor.newInstance(new Object[] { this }); + instance = m_ctor.newInstance(new Object[] { this }); } catch(InvocationTargetException e) { @@ -125,7 +133,7 @@ public PooledObject activateInstance() return instance; } - public void passivateInstance(PooledObject instance) + public void passivateInstance(T instance) throws SQLException { try @@ -141,21 +149,22 @@ public void passivateInstance(PooledObject instance) // Obtain a handle from the pool of handles so that // we have something to wrap the instance in. // - PooledObjectHandle handle = s_handlePool; + PooledObjectHandle handle = (PooledObjectHandle)s_handlePool; if(handle != null) s_handlePool = handle.m_next; else - handle = new PooledObjectHandle(); + handle = new PooledObjectHandle(); handle.m_instance = instance; handle.m_next = m_providerPool; m_providerPool = handle; } - public void removeInstance(PooledObject instance) throws SQLException + public void removeInstance(T instance) throws SQLException { PooledObjectHandle prev = null; - for(PooledObjectHandle handle = m_providerPool; handle != null; handle = handle.m_next) + for(PooledObjectHandle handle = m_providerPool; + handle != null; handle = handle.m_next) { if(handle.m_instance == instance) { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java index 3e079e83..915c280f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -25,9 +25,12 @@ */ public class Oid extends Number { - private static final HashMap s_class2typeId = new HashMap(); + private static final HashMap,Oid> s_class2typeId = + new HashMap,Oid>(); + + private static final HashMap> s_typeId2class = + new HashMap>(); - private static final HashMap s_typeId2class = new HashMap(); static { try @@ -52,7 +55,7 @@ public static Oid forJavaObject(Object obj) throws SQLException { if ( obj instanceof SQLData ) return forTypeName(((SQLData)obj).getSQLTypeName()); - return (Oid)s_class2typeId.get(obj.getClass()); + return s_class2typeId.get(obj.getClass()); } /** @@ -145,7 +148,7 @@ public float floatValue() public Class getJavaClass() throws SQLException { - Class c = (Class)s_typeId2class.get(this); + Class c = s_typeId2class.get(this); if(c == null) { String className; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java index 89a89320..6bf0ce4d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java @@ -21,6 +21,7 @@ import java.util.HashMap; import org.postgresql.pljava.ObjectPool; +import org.postgresql.pljava.PooledObject; import org.postgresql.pljava.SavepointListener; import org.postgresql.pljava.TransactionListener; import org.postgresql.pljava.jdbc.SQLUtils; @@ -98,7 +99,7 @@ public Object getAttribute(String attributeName) return m_attributes.get(attributeName); } - public ObjectPool getObjectPool(Class cls) + public ObjectPool getObjectPool(Class cls) { return ObjectPoolImpl.getObjectPool(cls); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java index f877669b..53bcd5fb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -98,7 +98,7 @@ public ResultSetMetaData getMetaData() public Array getArray(int columnIndex) throws SQLException { - return (Array)this.getValue(columnIndex, Array.class); + return getValue(columnIndex, Array.class); } /** @@ -108,7 +108,7 @@ public Array getArray(int columnIndex) public InputStream getAsciiStream(int columnIndex) throws SQLException { - Clob c = this.getClob(columnIndex); + Clob c = getClob(columnIndex); return (c == null) ? null : c.getAsciiStream(); } @@ -119,7 +119,7 @@ public InputStream getAsciiStream(int columnIndex) public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - return (BigDecimal)this.getValue(columnIndex, BigDecimal.class); + return getValue(columnIndex, BigDecimal.class); } /** @@ -140,7 +140,7 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) public InputStream getBinaryStream(int columnIndex) throws SQLException { - Blob b = this.getBlob(columnIndex); + Blob b = getBlob(columnIndex); return (b == null) ? null : b.getBinaryStream(); } @@ -151,7 +151,7 @@ public InputStream getBinaryStream(int columnIndex) public Blob getBlob(int columnIndex) throws SQLException { - byte[] bytes = this.getBytes(columnIndex); + byte[] bytes = getBytes(columnIndex); return (bytes == null) ? null : new BlobValue(bytes); } @@ -162,7 +162,7 @@ public Blob getBlob(int columnIndex) public boolean getBoolean(int columnIndex) throws SQLException { - Boolean b = (Boolean)this.getValue(columnIndex, Boolean.class); + Boolean b = getValue(columnIndex, Boolean.class); return (b == null) ? false : b.booleanValue(); } @@ -173,7 +173,7 @@ public boolean getBoolean(int columnIndex) public byte getByte(int columnIndex) throws SQLException { - Number b = this.getNumber(columnIndex, byte.class); + Number b = getNumber(columnIndex, byte.class); return (b == null) ? 0 : b.byteValue(); } @@ -184,7 +184,7 @@ public byte getByte(int columnIndex) public byte[] getBytes(int columnIndex) throws SQLException { - return (byte[])this.getValue(columnIndex, byte[].class); + return getValue(columnIndex, byte[].class); } /** @@ -194,7 +194,7 @@ public byte[] getBytes(int columnIndex) public Reader getCharacterStream(int columnIndex) throws SQLException { - Clob c = this.getClob(columnIndex); + Clob c = getClob(columnIndex); return (c == null) ? null : c.getCharacterStream(); } @@ -205,7 +205,7 @@ public Reader getCharacterStream(int columnIndex) public Clob getClob(int columnIndex) throws SQLException { - String str = this.getString(columnIndex); + String str = getString(columnIndex); return (str == null) ? null : new ClobValue(str); } @@ -216,7 +216,7 @@ public Clob getClob(int columnIndex) public Date getDate(int columnIndex) throws SQLException { - return (Date)this.getValue(columnIndex, Date.class); + return getValue(columnIndex, Date.class); } /** @@ -226,7 +226,7 @@ public Date getDate(int columnIndex) public Date getDate(int columnIndex, Calendar cal) throws SQLException { - return (Date)this.getValue(columnIndex, Date.class, cal); + return getValue(columnIndex, Date.class, cal); } /** @@ -236,7 +236,7 @@ public Date getDate(int columnIndex, Calendar cal) public double getDouble(int columnIndex) throws SQLException { - Number d = this.getNumber(columnIndex, double.class); + Number d = getNumber(columnIndex, double.class); return (d == null) ? 0 : d.doubleValue(); } @@ -247,7 +247,7 @@ public double getDouble(int columnIndex) public float getFloat(int columnIndex) throws SQLException { - Number f = this.getNumber(columnIndex, float.class); + Number f = getNumber(columnIndex, float.class); return (f == null) ? 0 : f.floatValue(); } @@ -258,7 +258,7 @@ public float getFloat(int columnIndex) public int getInt(int columnIndex) throws SQLException { - Number i = this.getNumber(columnIndex, int.class); + Number i = getNumber(columnIndex, int.class); return (i == null) ? 0 : i.intValue(); } @@ -269,7 +269,7 @@ public int getInt(int columnIndex) public long getLong(int columnIndex) throws SQLException { - Number l = this.getNumber(columnIndex, long.class); + Number l = getNumber(columnIndex, long.class); return (l == null) ? 0 : l.longValue(); } @@ -281,7 +281,7 @@ public long getLong(int columnIndex) public final Object getObject(int columnIndex) throws SQLException { - Object value = this.getObjectValue(columnIndex); + Object value = getObjectValue(columnIndex); m_wasNull = (value == null); return value; } @@ -294,7 +294,7 @@ public final Object getObject(int columnIndex) public final Object getObject(int columnIndex, Map map) throws SQLException { - Object value = this.getObjectValue(columnIndex, map); + Object value = getObjectValue(columnIndex, map); m_wasNull = (value == null); return value; } @@ -306,7 +306,7 @@ public final Object getObject(int columnIndex, Map map) public Ref getRef(int columnIndex) throws SQLException { - return (Ref)this.getValue(columnIndex, Ref.class); + return getValue(columnIndex, Ref.class); } /** @@ -316,7 +316,7 @@ public Ref getRef(int columnIndex) public short getShort(int columnIndex) throws SQLException { - Number s = this.getNumber(columnIndex, short.class); + Number s = getNumber(columnIndex, short.class); return (s == null) ? 0 : s.shortValue(); } @@ -327,7 +327,7 @@ public short getShort(int columnIndex) public String getString(int columnIndex) throws SQLException { - return (String)this.getValue(columnIndex, String.class); + return getValue(columnIndex, String.class); } /** @@ -337,7 +337,7 @@ public String getString(int columnIndex) public Time getTime(int columnIndex) throws SQLException { - return (Time)this.getValue(columnIndex, Time.class); + return getValue(columnIndex, Time.class); } /** @@ -347,7 +347,7 @@ public Time getTime(int columnIndex) public Time getTime(int columnIndex, Calendar cal) throws SQLException { - return (Time)this.getValue(columnIndex, Time.class, cal); + return getValue(columnIndex, Time.class, cal); } /** @@ -357,7 +357,7 @@ public Time getTime(int columnIndex, Calendar cal) public Timestamp getTimestamp(int columnIndex) throws SQLException { - return (Timestamp)this.getValue(columnIndex, Timestamp.class); + return getValue(columnIndex, Timestamp.class); } /** @@ -367,7 +367,7 @@ public Timestamp getTimestamp(int columnIndex) public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { - return (Timestamp)this.getValue(columnIndex, Timestamp.class, cal); + return getValue(columnIndex, Timestamp.class, cal); } /** @@ -386,7 +386,7 @@ public InputStream getUnicodeStream(int columnIndex) @Override public URL getURL(int columnIndex) throws SQLException { - return (URL)this.getValue(columnIndex, URL.class); + return getValue(columnIndex, URL.class); } /** @@ -410,7 +410,7 @@ public void refreshRow() @Override public void updateArray(int columnIndex, Array x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -423,7 +423,7 @@ public void updateAsciiStream(int columnIndex, InputStream x, int length) { try { - this.updateObject(columnIndex, + updateObject(columnIndex, new ClobValue(new InputStreamReader(x, "US-ASCII"), length)); } catch(UnsupportedEncodingException e) @@ -440,7 +440,7 @@ public void updateAsciiStream(int columnIndex, InputStream x, int length) public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -451,7 +451,7 @@ public void updateBigDecimal(int columnIndex, BigDecimal x) public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { - this.updateBlob(columnIndex, (Blob) new BlobValue(x, length)); + updateBlob(columnIndex, (Blob) new BlobValue(x, length)); } /** @@ -461,7 +461,7 @@ public void updateBinaryStream(int columnIndex, InputStream x, int length) public void updateBlob(int columnIndex, Blob x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -471,7 +471,7 @@ public void updateBlob(int columnIndex, Blob x) public void updateBoolean(int columnIndex, boolean x) throws SQLException { - this.updateObject(columnIndex, x ? Boolean.TRUE : Boolean.FALSE); + updateObject(columnIndex, x); } /** @@ -481,7 +481,7 @@ public void updateBoolean(int columnIndex, boolean x) public void updateByte(int columnIndex, byte x) throws SQLException { - this.updateObject(columnIndex, new Byte(x)); + updateObject(columnIndex, x); } /** @@ -491,7 +491,7 @@ public void updateByte(int columnIndex, byte x) public void updateBytes(int columnIndex, byte[] x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -502,7 +502,7 @@ public void updateBytes(int columnIndex, byte[] x) public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { - this.updateClob(columnIndex, (Clob) new ClobValue(x, length)); + updateClob(columnIndex, (Clob) new ClobValue(x, length)); } /** @@ -512,7 +512,7 @@ public void updateCharacterStream(int columnIndex, Reader x, int length) public void updateClob(int columnIndex, Clob x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -522,7 +522,7 @@ public void updateClob(int columnIndex, Clob x) public void updateDate(int columnIndex, Date x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -532,7 +532,7 @@ public void updateDate(int columnIndex, Date x) public void updateDouble(int columnIndex, double x) throws SQLException { - this.updateObject(columnIndex, new Double(x)); + updateObject(columnIndex, x); } /** @@ -542,7 +542,7 @@ public void updateDouble(int columnIndex, double x) public void updateFloat(int columnIndex, float x) throws SQLException { - this.updateObject(columnIndex, new Float(x)); + updateObject(columnIndex, x); } /** @@ -552,7 +552,7 @@ public void updateFloat(int columnIndex, float x) public void updateInt(int columnIndex, int x) throws SQLException { - this.updateObject(columnIndex, new Integer(x)); + updateObject(columnIndex, x); } /** @@ -562,7 +562,7 @@ public void updateInt(int columnIndex, int x) public void updateLong(int columnIndex, long x) throws SQLException { - this.updateObject(columnIndex, new Long(x)); + updateObject(columnIndex, x); } /** @@ -572,7 +572,7 @@ public void updateLong(int columnIndex, long x) public void updateNull(int columnIndex) throws SQLException { - this.updateObject(columnIndex, null); + updateObject(columnIndex, null); } /** @@ -582,7 +582,7 @@ public void updateNull(int columnIndex) public void updateRef(int columnIndex, Ref x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -592,7 +592,7 @@ public void updateRef(int columnIndex, Ref x) public void updateShort(int columnIndex, short x) throws SQLException { - this.updateObject(columnIndex, new Short(x)); + updateObject(columnIndex, x); } /** @@ -602,7 +602,7 @@ public void updateShort(int columnIndex, short x) public void updateString(int columnIndex, String x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -612,7 +612,7 @@ public void updateString(int columnIndex, String x) public void updateTime(int columnIndex, Time x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } /** @@ -622,7 +622,7 @@ public void updateTime(int columnIndex, Time x) public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { - this.updateObject(columnIndex, x); + updateObject(columnIndex, x); } // ************************************************************ @@ -638,7 +638,7 @@ public void updateTimestamp(int columnIndex, Timestamp x) public final T getObject(int columnIndex, Class type) throws SQLException { - Object value = this.getObjectValue(columnIndex, type); + Object value = getObjectValue(columnIndex, type); m_wasNull = (value == null); if ( m_wasNull || type.isInstance(value) ) return type.cast(value); @@ -652,35 +652,35 @@ public final T getObject(int columnIndex, Class type) /** * Implemented over {@link #getObjectValue}, tracks {@code wasNull}, - * applies {@link SPIConnection#basicNumericCoersion} to {@code cls}. + * applies {@link SPIConnection#basicNumericCoercion} to {@code cls}. */ protected final Number getNumber(int columnIndex, Class cls) throws SQLException { - Object value = this.getObjectValue(columnIndex); + Object value = getObjectValue(columnIndex); m_wasNull = (value == null); - return SPIConnection.basicNumericCoersion(cls, value); + return SPIConnection.basicNumericCoercion(cls, value); } /** * Implemented over {@link #getObject}, - * applies {@link SPIConnection#basicCoersion} to {@code cls}. + * applies {@link SPIConnection#basicCoercion} to {@code cls}. */ - protected final Object getValue(int columnIndex, Class cls) + protected final T getValue(int columnIndex, Class cls) throws SQLException { - return SPIConnection.basicCoersion(cls, this.getObject(columnIndex)); + return SPIConnection.basicCoercion(cls, getObject(columnIndex)); } /** * Implemented over {@link #getObject}, - * applies {@link SPIConnection#basicCalendricalCoersion} to {@code cls}. + * applies {@link SPIConnection#basicCalendricalCoercion} to {@code cls}. */ - protected Object getValue(int columnIndex, Class cls, Calendar cal) + protected T getValue(int columnIndex, Class cls, Calendar cal) throws SQLException { - return SPIConnection.basicCalendricalCoersion(cls, - this.getObject(columnIndex), cal); + return SPIConnection.basicCalendricalCoercion(cls, + getObject(columnIndex), cal); } /** @@ -691,7 +691,7 @@ protected Object getObjectValue(int columnIndex, Map typeMap) throws SQLException { if(typeMap == null) - return this.getObjectValue(columnIndex); + return getObjectValue(columnIndex); throw new UnsupportedFeatureException( "Obtaining values using explicit Map"); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 6be65662..d2cefa1d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -88,7 +88,8 @@ public class SPIConnection implements Connection * which, in turn, is only used for * {@link PreparedStatement#setObject(int,Object)}. */ - private static final HashMap s_sqlType2Class = new HashMap(30); + private static final HashMap,Integer> s_class2sqlType = + new HashMap,Integer>(30); static { @@ -115,7 +116,7 @@ public class SPIConnection implements Connection private static final void addType(Class clazz, int sqlType) { - s_sqlType2Class.put(clazz, new Integer(sqlType)); + s_class2sqlType.put(clazz, sqlType); } /** @@ -127,9 +128,9 @@ static int getTypeForClass(Class c) if(c.isArray() && !c.equals(byte[].class)) return Types.ARRAY; - Integer sqt = (Integer)s_sqlType2Class.get(c); + Integer sqt = s_class2sqlType.get(c); if(sqt != null) - return sqt.intValue(); + return sqt; /* * This is not a well known JDBC type. @@ -827,11 +828,11 @@ public String getPGType(Oid oid) throws SQLException * in {@code ResultSet}s seems a bit suspect, as does its use in UDT input * but not output with composites. */ - static Object basicCoersion(Class cls, Object value) + static T basicCoercion(Class cls, Object value) throws SQLException { if(value == null || cls.isInstance(value)) - return value; + return (T)value; if(cls == String.class) { @@ -840,13 +841,13 @@ static Object basicCoersion(Class cls, Object value) || value instanceof Timestamp || value instanceof Date || value instanceof Time) - return value.toString(); + return (T)value.toString(); } else if(cls == URL.class && value instanceof String) { try { - return new URL((String)value); + return (T)new URL((String)value); } catch(MalformedURLException e) { @@ -870,8 +871,12 @@ else if(cls == URL.class && value instanceof String) * the use of the same coercion in both the retrieval and storage direction * in {@code ResultSet}s seems a bit suspect, as does its use in UDT input * but not output with composites. + *

    + * Oddly, this doesn't promise to return a subclass of its {@code cls} + * parameter: if {@code value} is a {@code Number}, it is returned directly + * no matter what {@code cls} was requested. */ - static Number basicNumericCoersion(Class cls, Object value) + static Number basicNumericCoercion(Class cls, Object value) throws SQLException { if(value == null || value instanceof Number) @@ -917,14 +922,15 @@ else if(cls == BigDecimal.class) * the use of the same coercion in both the retrieval and storage direction * in {@code ResultSet}s seems a bit suspect. */ - static Object basicCalendricalCoersion(Class cls, Object value, Calendar cal) + static T basicCalendricalCoercion( + Class cls, Object value, Calendar cal) throws SQLException { if(value == null) - return value; + return null; if(cls.isInstance(value)) - return value; + return (T)value; if(cls == Timestamp.class) { @@ -935,17 +941,17 @@ static Object basicCalendricalCoersion(Class cls, Object value, Calendar cal) cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); - return new Timestamp(cal.getTimeInMillis()); + return (T)new Timestamp(cal.getTimeInMillis()); } else if(value instanceof Time) { cal.setTime((Date)value); cal.set(1970, 0, 1); - return new Timestamp(cal.getTimeInMillis()); + return (T)new Timestamp(cal.getTimeInMillis()); } else if(value instanceof String) { - return Timestamp.valueOf((String)value); + return (T)Timestamp.valueOf((String)value); } } else if(cls == Date.class) @@ -958,11 +964,11 @@ else if(cls == Date.class) cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); - return new Date(cal.getTimeInMillis()); + return (T)new Date(cal.getTimeInMillis()); } else if(value instanceof String) { - return Date.valueOf((String)value); + return (T)Date.valueOf((String)value); } } else if(cls == Time.class) @@ -972,11 +978,11 @@ else if(cls == Time.class) Timestamp ts = (Timestamp)value; cal.setTime(ts); cal.set(1970, 0, 1); - return new Time(cal.getTimeInMillis()); + return (T)new Time(cal.getTimeInMillis()); } else if(value instanceof String) { - return Time.valueOf((String)value); + return (T)Time.valueOf((String)value); } } throw new SQLException("Cannot derive a value of class " + diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index 18b34285..6cf8fbc0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -73,7 +73,7 @@ public void close() m_plan.close(); m_plan = null; } - this.clearParameters(); + clearParameters(); super.close(); } @@ -81,101 +81,101 @@ public void close() public ResultSet executeQuery() throws SQLException { - this.execute(); - return this.getResultSet(); + execute(); + return getResultSet(); } @Override public int executeUpdate() throws SQLException { - this.execute(); - return this.getUpdateCount(); + execute(); + return getUpdateCount(); } @Override public void setNull(int columnIndex, int sqlType) throws SQLException { - this.setObject(columnIndex, null, sqlType); + setObject(columnIndex, null, sqlType); } @Override public void setBoolean(int columnIndex, boolean value) throws SQLException { - this.setObject(columnIndex, value ? Boolean.TRUE : Boolean.FALSE, Types.BOOLEAN); + setObject(columnIndex, value, Types.BOOLEAN); } @Override public void setByte(int columnIndex, byte value) throws SQLException { - this.setObject(columnIndex, new Byte(value), Types.TINYINT); + setObject(columnIndex, value, Types.TINYINT); } @Override public void setShort(int columnIndex, short value) throws SQLException { - this.setObject(columnIndex, new Short(value), Types.SMALLINT); + setObject(columnIndex, value, Types.SMALLINT); } @Override public void setInt(int columnIndex, int value) throws SQLException { - this.setObject(columnIndex, new Integer(value), Types.INTEGER); + setObject(columnIndex, value, Types.INTEGER); } @Override public void setLong(int columnIndex, long value) throws SQLException { - this.setObject(columnIndex, new Long(value), Types.BIGINT); + setObject(columnIndex, value, Types.BIGINT); } @Override public void setFloat(int columnIndex, float value) throws SQLException { - this.setObject(columnIndex, new Float(value), Types.FLOAT); + setObject(columnIndex, value, Types.FLOAT); } @Override public void setDouble(int columnIndex, double value) throws SQLException { - this.setObject(columnIndex, new Double(value), Types.DOUBLE); + setObject(columnIndex, value, Types.DOUBLE); } @Override public void setBigDecimal(int columnIndex, BigDecimal value) throws SQLException { - this.setObject(columnIndex, value, Types.DECIMAL); + setObject(columnIndex, value, Types.DECIMAL); } @Override public void setString(int columnIndex, String value) throws SQLException { - this.setObject(columnIndex, value, Types.VARCHAR); + setObject(columnIndex, value, Types.VARCHAR); } @Override public void setBytes(int columnIndex, byte[] value) throws SQLException { - this.setObject(columnIndex, value, Types.VARBINARY); + setObject(columnIndex, value, Types.VARBINARY); } @Override public void setDate(int columnIndex, Date value) throws SQLException { - this.setObject(columnIndex, value, Types.DATE); + setObject(columnIndex, value, Types.DATE); } @Override public void setTime(int columnIndex, Time value) throws SQLException { - this.setObject(columnIndex, value, Types.TIME); + setObject(columnIndex, value, Types.TIME); } @Override public void setTimestamp(int columnIndex, Timestamp value) throws SQLException { - this.setObject(columnIndex, value, Types.TIMESTAMP); + setObject(columnIndex, value, Types.TIMESTAMP); } @Override @@ -183,7 +183,7 @@ public void setAsciiStream(int columnIndex, InputStream value, int length) throw { try { - this.setObject(columnIndex, + setObject(columnIndex, new ClobValue(new InputStreamReader(value, "US-ASCII"), length), Types.CLOB); } @@ -205,7 +205,7 @@ public void setUnicodeStream(int columnIndex, InputStream value, int arg2) throw @Override public void setBinaryStream(int columnIndex, InputStream value, int length) throws SQLException { - this.setObject(columnIndex, new BlobValue(value, length), Types.BLOB); + setObject(columnIndex, new BlobValue(value, length), Types.BLOB); } @Override @@ -223,7 +223,7 @@ public void clearParameters() public void setObject(int columnIndex, Object value, int sqlType, int scale) throws SQLException { - this.setObject(columnIndex, value, sqlType); + setObject(columnIndex, value, sqlType); } @Override @@ -318,7 +318,7 @@ public void setObject(int columnIndex, Object value) else sqlType = Types.OTHER; - this.setObject(columnIndex, value, sqlType, vAlt); + setObject(columnIndex, value, sqlType, vAlt); } /** @@ -350,8 +350,8 @@ public boolean execute() if(m_plan == null) m_plan = ExecutionPlan.prepare(m_statement, m_typeIds); - boolean result = this.executePlan(m_plan, m_values); - this.clearParameters(); // Parameters are cleared upon successful completion. + boolean result = executePlan(m_plan, m_values); + clearParameters(); // Parameters are cleared upon successful completion. return result; } @@ -369,8 +369,8 @@ public boolean execute(String statement) public void addBatch() throws SQLException { - this.internalAddBatch(new Object[]{m_values.clone(), m_sqlTypes.clone(), m_typeIds.clone()}); - this.clearParameters(); // Parameters are cleared upon successful completion. + internalAddBatch(new Object[]{m_values.clone(), m_sqlTypes.clone(), m_typeIds.clone()}); + clearParameters(); // Parameters are cleared upon successful completion. } /** @@ -388,31 +388,31 @@ public void addBatch(String statement) public void setCharacterStream(int columnIndex, Reader value, int length) throws SQLException { - this.setObject(columnIndex, new ClobValue(value, length), Types.CLOB); + setObject(columnIndex, new ClobValue(value, length), Types.CLOB); } @Override public void setRef(int columnIndex, Ref value) throws SQLException { - this.setObject(columnIndex, value, Types.REF); + setObject(columnIndex, value, Types.REF); } @Override public void setBlob(int columnIndex, Blob value) throws SQLException { - this.setObject(columnIndex, value, Types.BLOB); + setObject(columnIndex, value, Types.BLOB); } @Override public void setClob(int columnIndex, Clob value) throws SQLException { - this.setObject(columnIndex, value, Types.CLOB); + setObject(columnIndex, value, Types.CLOB); } @Override public void setArray(int columnIndex, Array value) throws SQLException { - this.setObject(columnIndex, value, Types.ARRAY); + setObject(columnIndex, value, Types.ARRAY); } /** @@ -430,27 +430,30 @@ public ResultSetMetaData getMetaData() public void setDate(int columnIndex, Date value, Calendar cal) throws SQLException { - if(cal == null || cal == Calendar.getInstance()) - this.setObject(columnIndex, value, Types.DATE); - throw new UnsupportedFeatureException("Setting date using explicit Calendar"); + if(cal != null && cal != Calendar.getInstance()) + throw new UnsupportedFeatureException( + "Setting date using explicit Calendar"); + setObject(columnIndex, value, Types.DATE); } @Override public void setTime(int columnIndex, Time value, Calendar cal) throws SQLException { - if(cal == null || cal == Calendar.getInstance()) - this.setObject(columnIndex, value, Types.TIME); - throw new UnsupportedFeatureException("Setting time using explicit Calendar"); + if(cal != null && cal != Calendar.getInstance()) + throw new UnsupportedFeatureException( + "Setting time using explicit Calendar"); + setObject(columnIndex, value, Types.TIME); } @Override public void setTimestamp(int columnIndex, Timestamp value, Calendar cal) throws SQLException { - if(cal == null || cal == Calendar.getInstance()) - this.setObject(columnIndex, value, Types.TIMESTAMP); - throw new UnsupportedFeatureException("Setting time using explicit Calendar"); + if(cal != null && cal != Calendar.getInstance()) + throw new UnsupportedFeatureException( + "Setting time using explicit Calendar"); + setObject(columnIndex, value, Types.TIMESTAMP); } /** @@ -500,7 +503,7 @@ else if ( !op.equals(id) ) @Override public void setURL(int columnIndex, URL value) throws SQLException { - this.setObject(columnIndex, value, Types.DATALINK); + setObject(columnIndex, value, Types.DATALINK); } public String toString() @@ -521,7 +524,7 @@ public String toString() public ParameterMetaData getParameterMetaData() throws SQLException { - return new SPIParameterMetaData(this.getSqlTypes()); + return new SPIParameterMetaData(getSqlTypes()); } protected long executeBatchEntry(Object batchEntry) @@ -553,11 +556,11 @@ protected long executeBatchEntry(Object batchEntry) } } - if(this.execute()) - this.getResultSet().close(); + if(execute()) + getResultSet().close(); else { - long updCount = this.getUpdateCount(); + long updCount = getUpdateCount(); if(updCount >= 0) ret = updCount; } @@ -588,7 +591,7 @@ public void setNClob(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setNClob( int, Reader ) not implemented yet.", "0A000" ); @@ -600,7 +603,7 @@ public void setNClob(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setNClob( int, NClob ) not implemented yet.", "0A000" ); @@ -612,7 +615,7 @@ public void setNClob(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setNClob( int, Reader, long ) not " + "implemented yet.", "0A000" ); @@ -625,7 +628,7 @@ public void setBlob(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setBlob( int, InputStream ) not " + "implemented yet.", "0A000" ); @@ -637,7 +640,7 @@ public void setBlob(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setBlob( int, InputStream, long ) not " + "implemented yet.", "0A000" ); @@ -650,7 +653,7 @@ public void setClob(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setClob( int, Reader ) not implemented yet.", "0A000" ); @@ -662,7 +665,7 @@ public void setClob(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setClob( int, Reader, long ) not " + "implemented yet.", "0A000" ); @@ -675,7 +678,7 @@ public void setNCharacterStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setNCharacterStream( int, Reader ) not " + "implemented yet.", "0A000" ); @@ -688,7 +691,7 @@ public void setNCharacterStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setNCharacterStream( int, Reader, long ) not " + "implemented yet.", "0A000" ); @@ -701,7 +704,7 @@ public void setCharacterStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setCharacterStream( int, Reader ) not " + "implemented yet.", "0A000" ); @@ -714,7 +717,7 @@ public void setCharacterStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setCharacterStream( int, Reader, long ) not " + "implemented yet.", "0A000" ); @@ -727,7 +730,7 @@ public void setBinaryStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setBinaryStream( int, InputStream ) not " + "implemented yet.", "0A000" ); @@ -740,7 +743,7 @@ public void setBinaryStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setBinaryStream( int, InputStream, long ) not " + "implemented yet.", "0A000" ); @@ -753,7 +756,7 @@ public void setAsciiStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setAsciiStream( int, InputStream ) not " + "implemented yet.", "0A000" ); @@ -766,7 +769,7 @@ public void setAsciiStream(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setAsciiStream( int, InputStream, long ) not " + "implemented yet.", "0A000" ); @@ -779,7 +782,7 @@ public void setNString(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setNString( int, String ) not implemented yet.", "0A000" ); } @@ -790,7 +793,7 @@ public void setRowId(int parameterIndex, throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".setRowId( int, RowId ) not implemented yet.", "0A000" ); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java index 35f5310a..3356206f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java @@ -74,7 +74,7 @@ protected int nextIndex() throws SQLException @Override public Array readArray() throws SQLException { - return (Array)this.readValue(Array.class); + return readValue(Array.class); } /** @@ -83,7 +83,7 @@ public Array readArray() throws SQLException @Override public InputStream readAsciiStream() throws SQLException { - Clob c = this.readClob(); + Clob c = readClob(); return (c == null) ? null : c.getAsciiStream(); } @@ -93,7 +93,7 @@ public InputStream readAsciiStream() throws SQLException @Override public BigDecimal readBigDecimal() throws SQLException { - return (BigDecimal)this.readValue(BigDecimal.class); + return readValue(BigDecimal.class); } /** @@ -102,7 +102,7 @@ public BigDecimal readBigDecimal() throws SQLException @Override public InputStream readBinaryStream() throws SQLException { - Blob b = this.readBlob(); + Blob b = readBlob(); return (b == null) ? null : b.getBinaryStream(); } @@ -112,7 +112,7 @@ public InputStream readBinaryStream() throws SQLException @Override public Blob readBlob() throws SQLException { - byte[] bytes = this.readBytes(); + byte[] bytes = readBytes(); return (bytes == null) ? null : new BlobValue(bytes); } @@ -122,7 +122,7 @@ public Blob readBlob() throws SQLException @Override public boolean readBoolean() throws SQLException { - Boolean b = (Boolean)this.readValue(Boolean.class); + Boolean b = readValue(Boolean.class); return (b == null) ? false : b.booleanValue(); } @@ -132,7 +132,7 @@ public boolean readBoolean() throws SQLException @Override public byte readByte() throws SQLException { - Number b = this.readNumber(byte.class); + Number b = readNumber(byte.class); return (b == null) ? 0 : b.byteValue(); } @@ -142,7 +142,7 @@ public byte readByte() throws SQLException @Override public byte[] readBytes() throws SQLException { - return (byte[])this.readValue(byte[].class); + return readValue(byte[].class); } /** @@ -150,7 +150,7 @@ public byte[] readBytes() throws SQLException */ public Reader readCharacterStream() throws SQLException { - Clob c = this.readClob(); + Clob c = readClob(); return (c == null) ? null : c.getCharacterStream(); } @@ -159,7 +159,7 @@ public Reader readCharacterStream() throws SQLException */ public Clob readClob() throws SQLException { - String str = this.readString(); + String str = readString(); return (str == null) ? null : new ClobValue(str); } @@ -169,7 +169,7 @@ public Clob readClob() throws SQLException @Override public Date readDate() throws SQLException { - return (Date)this.readValue(Date.class); + return readValue(Date.class); } /** @@ -178,7 +178,7 @@ public Date readDate() throws SQLException @Override public double readDouble() throws SQLException { - Number d = this.readNumber(double.class); + Number d = readNumber(double.class); return (d == null) ? 0 : d.doubleValue(); } @@ -188,7 +188,7 @@ public double readDouble() throws SQLException @Override public float readFloat() throws SQLException { - Number f = this.readNumber(float.class); + Number f = readNumber(float.class); return (f == null) ? 0 : f.floatValue(); } @@ -198,7 +198,7 @@ public float readFloat() throws SQLException @Override public int readInt() throws SQLException { - Number i = this.readNumber(int.class); + Number i = readNumber(int.class); return (i == null) ? 0 : i.intValue(); } @@ -208,7 +208,7 @@ public int readInt() throws SQLException @Override public long readLong() throws SQLException { - Number l = this.readNumber(long.class); + Number l = readNumber(long.class); return (l == null) ? 0 : l.longValue(); } @@ -224,7 +224,7 @@ public Object readObject() throws SQLException @Override public Ref readRef() throws SQLException { - return (Ref)this.readValue(Ref.class); + return readValue(Ref.class); } /** @@ -233,7 +233,7 @@ public Ref readRef() throws SQLException @Override public short readShort() throws SQLException { - Number s = this.readNumber(short.class); + Number s = readNumber(short.class); return (s == null) ? 0 : s.shortValue(); } @@ -243,7 +243,7 @@ public short readShort() throws SQLException @Override public String readString() throws SQLException { - return (String)this.readValue(String.class); + return readValue(String.class); } /** @@ -252,7 +252,7 @@ public String readString() throws SQLException @Override public Time readTime() throws SQLException { - return (Time)this.readValue(Time.class); + return readValue(Time.class); } /** @@ -261,7 +261,7 @@ public Time readTime() throws SQLException @Override public Timestamp readTimestamp() throws SQLException { - return (Timestamp)this.readValue(Timestamp.class); + return readValue(Timestamp.class); } /** @@ -270,7 +270,7 @@ public Timestamp readTimestamp() throws SQLException @Override public URL readURL() throws SQLException { - return (URL)this.readValue(URL.class); + return readValue(URL.class); } // ************************************************************ @@ -284,7 +284,7 @@ public URL readURL() throws SQLException public SQLXML readSQLXML() throws SQLException { - return this.readObject(SQLXML.class); + return readObject(SQLXML.class); } // ************************************************************ @@ -297,7 +297,7 @@ public RowId readRowId() throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".readRowId() not implemented yet.", "0A000" ); } @@ -308,7 +308,7 @@ public String readNString() throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".readNString() not implemented yet.", "0A000" ); @@ -320,7 +320,7 @@ public NClob readNClob() throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".readNClob() not implemented yet.", "0A000" ); @@ -345,7 +345,7 @@ private Number readNumber(Class numberClass) throws SQLException return getNumber(nextIndex(), numberClass); } - private Object readValue(Class valueClass) throws SQLException + private T readValue(Class valueClass) throws SQLException { return getValue(nextIndex(), valueClass); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java index 985e70af..55b0ec2d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root directory of this distribution or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Tada AB + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -75,7 +80,7 @@ public long getTuple() public void writeArray(Array value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeAsciiStream(InputStream value) throws SQLException @@ -83,7 +88,7 @@ public void writeAsciiStream(InputStream value) throws SQLException try { Reader rdr = new BufferedReader(new InputStreamReader(value, "US-ASCII")); - this.writeClob(new ClobValue(rdr, ClobValue.getReaderLength(rdr))); + writeClob(new ClobValue(rdr, ClobValue.getReaderLength(rdr))); } catch(UnsupportedEncodingException e) { @@ -93,111 +98,111 @@ public void writeAsciiStream(InputStream value) throws SQLException public void writeBigDecimal(BigDecimal value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeBinaryStream(InputStream value) throws SQLException { if(!value.markSupported()) value = new BufferedInputStream(value); - this.writeBlob(new BlobValue(value, BlobValue.getStreamLength(value))); + writeBlob(new BlobValue(value, BlobValue.getStreamLength(value))); } public void writeBlob(Blob value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeBoolean(boolean value) throws SQLException { - this.writeValue(value ? Boolean.TRUE : Boolean.FALSE); + writeValue(value); } public void writeByte(byte value) throws SQLException { - this.writeValue(new Byte(value)); + writeValue(value); } public void writeBytes(byte[] value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeCharacterStream(Reader value) throws SQLException { if(!value.markSupported()) value = new BufferedReader(value); - this.writeClob(new ClobValue(value, ClobValue.getReaderLength(value))); + writeClob(new ClobValue(value, ClobValue.getReaderLength(value))); } public void writeClob(Clob value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeDate(Date value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeDouble(double value) throws SQLException { - this.writeValue(new Double(value)); + writeValue(value); } public void writeFloat(float value) throws SQLException { - this.writeValue(new Float(value)); + writeValue(value); } public void writeInt(int value) throws SQLException { - this.writeValue(new Integer(value)); + writeValue(value); } public void writeLong(long value) throws SQLException { - this.writeValue(new Long(value)); + writeValue(value); } public void writeObject(SQLData value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeRef(Ref value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeShort(short value) throws SQLException { - this.writeValue(new Short(value)); + writeValue(value); } public void writeString(String value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeStruct(Struct value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeTime(Time value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeTimestamp(Timestamp value) throws SQLException { - this.writeValue(value); + writeValue(value); } public void writeURL(URL value) throws SQLException { - this.writeValue(value.toString()); + writeValue(value.toString()); } // ************************************************************ @@ -210,7 +215,7 @@ public void writeURL(URL value) throws SQLException public void writeSQLXML(SQLXML x) throws SQLException { - this.writeValue(x); + writeValue(x); } // ************************************************************ @@ -221,7 +226,7 @@ public void writeNClob(NClob x) throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".writeNClob( NClob ) not implemented yet.", "0A000" ); } @@ -230,7 +235,7 @@ public void writeNString(String x) throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".writeNString( String ) not implemented yet.", "0A000" ); } @@ -239,7 +244,7 @@ public void writeRowId(RowId x) throws SQLException { throw new SQLFeatureNotSupportedException - ( this.getClass() + ( getClass() + ".writeRowId( RowId ) not implemented yet.", "0A000" ); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java index bb819baa..502b313a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java @@ -95,14 +95,14 @@ public void updateObject(int columnIndex, Object x) && !(c == byte[].class && (x instanceof BlobValue))) { if(Number.class.isAssignableFrom(c)) - x = SPIConnection.basicNumericCoersion(c, x); + x = SPIConnection.basicNumericCoercion(c, x); else if(Time.class.isAssignableFrom(c) || Date.class.isAssignableFrom(c) || Timestamp.class.isAssignableFrom(c)) - x = SPIConnection.basicCalendricalCoersion(c, x, Calendar.getInstance()); + x = SPIConnection.basicCalendricalCoercion(c, x, Calendar.getInstance()); else - x = SPIConnection.basicCoersion(c, x); + x = SPIConnection.basicCoercion(c, x); } m_values[columnIndex-1] = null == xAlt ? x : xAlt; } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java index 1603c805..ab1704e7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SyntheticResultSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -27,9 +27,9 @@ */ public class SyntheticResultSet extends ResultSetBase { - private final ResultSetField[] m_fields; - private final ArrayList m_tuples; - private final HashMap m_fieldIndexes; + private final ResultSetField[] m_fields; + private final ArrayList m_tuples; + private final HashMap m_fieldIndexes; /** * Construct a {@code SyntheticResultSet} whose column types are described @@ -41,7 +41,7 @@ public class SyntheticResultSet extends ResultSetBase * {@link ResultSetField#canContain canContain} method of the * {@code ResultSetField} instance at index j. */ - SyntheticResultSet(ResultSetField[] fields, ArrayList tuples) + SyntheticResultSet(ResultSetField[] fields, ArrayList tuples) throws SQLException { super(tuples.size()); @@ -50,7 +50,7 @@ public class SyntheticResultSet extends ResultSetBase m_fieldIndexes = new HashMap(); int i = m_fields.length; while(--i >= 0) - m_fieldIndexes.put(m_fields[i].getColumnLabel(), new Integer(i+1)); + m_fieldIndexes.put(m_fields[i].getColumnLabel(), i+1); Object[][] tupleTest = (Object[][]) m_tuples.toArray(new Object[0][]); Object value; @@ -84,10 +84,10 @@ public void close() public int findColumn(String columnName) throws SQLException { - Integer idx = (Integer)m_fieldIndexes.get(columnName.toUpperCase()); + Integer idx = m_fieldIndexes.get(columnName.toUpperCase()); if(idx != null) { - return idx.intValue(); + return idx; } throw new SQLException("No such field: '" + columnName + "'"); } @@ -112,7 +112,7 @@ protected final Object[] getCurrentRow() int row = this.getRow(); if(row < 1 || row > m_tuples.size()) throw new SQLException("ResultSet is not positioned on a valid row"); - return (Object[])m_tuples.get(row-1); + return m_tuples.get(row-1); } @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java index 89b3e6e3..3ec1bc01 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TriggerResultSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * * All rights reserved. This program and the accompanying materials @@ -94,7 +94,7 @@ public void updateObject(int columnIndex, Object x) if(m_tupleChanges == null) m_tupleChanges = new ArrayList(); - m_tupleChanges.add(new Integer(columnIndex)); + m_tupleChanges.add(columnIndex); m_tupleChanges.add(x); } diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 47f542d2..a374f800 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -821,13 +821,13 @@ public static void setClassPath(String schemaName, String path) } PreparedStatement stmt; - ArrayList entries = null; + ArrayList entries = null; if(path != null && path.length() > 0) { // Collect and verify that all entries in the path represents a // valid jar // - entries = new ArrayList(); + entries = new ArrayList(); stmt = SQLUtils.getDefaultConnection().prepareStatement( "SELECT jarId FROM sqlj.jar_repository " + "WHERE jarName OPERATOR(pg_catalog.=) ?"); @@ -850,7 +850,7 @@ public static void setClassPath(String schemaName, String path) throw new SQLNonTransientException( "No such jar: " + jarName, "46102"); - entries.add(new Integer(jarId)); + entries.add(jarId); if(colon < 0) break; } @@ -889,7 +889,7 @@ public static void setClassPath(String schemaName, String path) int top = entries.size(); for(int idx = 0; idx < top; ++idx) { - int jarId = ((Integer)entries.get(idx)).intValue(); + int jarId = entries.get(idx); stmt.setString(1, schemaName); stmt.setInt(2, idx + 1); stmt.setInt(3, jarId); diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 8c4502e8..da4070f6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2019 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 @@ -95,9 +95,11 @@ public Object nextElement() } private static final String PUBLIC_SCHEMA = "public"; - private static final Map s_schemaLoaders = new HashMap(); + private static final Map + s_schemaLoaders = new HashMap(); - private static final Map s_typeMap = new HashMap(); + private static final Map>> + s_typeMap = new HashMap>>(); /** * Removes all cached schema loaders, functions, and type maps. This @@ -154,7 +156,7 @@ public static ClassLoader getSchemaLoader(String schemaName) else schemaName = schemaName.toLowerCase(); - ClassLoader loader = (ClassLoader)s_schemaLoaders.get(schemaName); + ClassLoader loader = s_schemaLoaders.get(schemaName); if(loader != null) return loader; @@ -249,9 +251,12 @@ public static ClassLoader getSchemaLoader(String schemaName) * @param schema The schema * @return The Map, possibly empty but never null. */ - public static Map getTypeMap(final String schema) throws SQLException + public static Map> getTypeMap( + final String schema) + throws SQLException { - Map typesForSchema = (Map)s_typeMap.get(schema); + Map> typesForSchema = + s_typeMap.get(schema); if(typesForSchema != null) return typesForSchema; @@ -282,7 +287,7 @@ public Object get(Object key) throw new SQLException("Class " + javaClassName + " does not implement java.sql.SQLData"); Oid typeOid = Oid.forTypeName(sqlName); - typesForSchema.put(typeOid, cls); + typesForSchema.put(typeOid, cls.asSubclass(SQLData.class)); s_logger.finer("Adding type mapping for OID " + typeOid + " -> class " + cls.getName() + " for schema " + schema); } catch(ClassNotFoundException e) From 003f75de47e62d8faf789d0b7c4d08dab29ad275 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 10 Jun 2019 23:01:18 -0400 Subject: [PATCH 0282/1087] Re-java7ify after REL1_5_STABLE merge. --- .../org/postgresql/pljava/sqlgen/DDRProcessor.java | 2 +- .../java/org/postgresql/pljava/internal/DualState.java | 10 +++++----- .../org/postgresql/pljava/internal/ObjectPoolImpl.java | 4 ++-- .../main/java/org/postgresql/pljava/internal/Oid.java | 4 ++-- .../org/postgresql/pljava/internal/PgSavepoint.java | 2 +- .../postgresql/pljava/internal/SubXactListener.java | 2 +- .../org/postgresql/pljava/internal/XactListener.java | 2 +- .../java/org/postgresql/pljava/jdbc/SPIConnection.java | 2 +- .../org/postgresql/pljava/management/Commands.java | 2 +- .../main/java/org/postgresql/pljava/sqlj/Loader.java | 4 ++-- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java index e3313570..de55276f 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/DDRProcessor.java @@ -1740,7 +1740,7 @@ List specialization( if ( ! typu.isAssignable( typu.erasure( tm), dt) ) return null; - List pending = new LinkedList(); + List pending = new LinkedList<>(); pending.add( tm); while ( ! pending.isEmpty() ) { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 9fdd5ddd..59b28fed 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -258,7 +258,7 @@ public abstract class DualState extends WeakReference * in selected places where it makes sense to do so. */ private static final ReferenceQueue s_releasedInstances = - new ReferenceQueue(); + new ReferenceQueue<>(); /** * All instances in a non-transient native scope are added here upon @@ -270,7 +270,7 @@ public abstract class DualState extends WeakReference * searching, and can be removed from this structure in O(1). */ private static final IdentityHashMap - s_unscopedInstances = new IdentityHashMap(); + s_unscopedInstances = new IdentityHashMap<>(); /** * All native-scoped instances are added to this structure upon creation. @@ -283,7 +283,7 @@ public abstract class DualState extends WeakReference * already at hand. The list head is of a dummy {@code DualState} subclass. */ private static final Map s_scopedInstances = - new HashMap(); + new HashMap<>(); /** Backward link in per-resource-owner list. */ private DualState m_prev; @@ -349,7 +349,7 @@ static final class Holder extends ThreadLocal> @Override protected Deque initialValue() { - return new ArrayDeque(); + return new ArrayDeque<>(); } /** @@ -542,7 +542,7 @@ protected DualState(Key cookie, T referent, long resourceOwner) m_resourceOwner = resourceOwner; m_state = new AtomicInteger(); - m_waiters = new ConcurrentLinkedQueue(); + m_waiters = new ConcurrentLinkedQueue<>(); assert Backend.threadMayEnterPG() : m("DualState construction"); /* diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java b/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java index 3b132c88..d67040a9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ObjectPoolImpl.java @@ -35,7 +35,7 @@ private static class PooledObjectHandle private static Class[] s_ctorSignature = { ObjectPool.class }; private static PooledObjectHandle s_handlePool; private static final IdentityHashMap,ObjectPoolImpl> - s_poolCache = new IdentityHashMap,ObjectPoolImpl>(); + s_poolCache = new IdentityHashMap<>(); private final Constructor m_ctor; private PooledObjectHandle m_providerPool; @@ -153,7 +153,7 @@ public void passivateInstance(T instance) if(handle != null) s_handlePool = handle.m_next; else - handle = new PooledObjectHandle(); + handle = new PooledObjectHandle<>(); handle.m_instance = instance; handle.m_next = m_providerPool; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java index 06cf6f61..9706dfea 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Oid.java @@ -26,10 +26,10 @@ public class Oid extends Number { private static final HashMap,Oid> s_class2typeId = - new HashMap,Oid>(); + new HashMap<>(); private static final HashMap> s_typeId2class = - new HashMap>(); + new HashMap<>(); /** * Finds the PostgreSQL well known Oid for the given Java object. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java index 7116f056..998e90aa 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java @@ -24,7 +24,7 @@ public class PgSavepoint implements java.sql.Savepoint { private static final WeakHashMap s_knownSavepoints = - new WeakHashMap(); + new WeakHashMap<>(); /* * PostgreSQL allows an internal subtransaction to have a name, though it diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java index 799c8869..05ac584d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java @@ -28,7 +28,7 @@ class SubXactListener { private static final Deque s_listeners = - new ArrayDeque(); + new ArrayDeque<>(); static void onAbort(PgSavepoint sp, PgSavepoint parent) throws SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java index 08cc6bef..48402ae5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java @@ -33,7 +33,7 @@ class XactListener * the size changes from 0 to 1 or 1 to 0). */ private static final Deque s_listeners = - new ArrayDeque(); + new ArrayDeque<>(); static void onAbort() throws SQLException { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index 6f87f3ad..11b1d434 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -89,7 +89,7 @@ public class SPIConnection implements Connection * {@link PreparedStatement#setObject(int,Object)}. */ private static final HashMap,Integer> s_class2sqlType = - new HashMap,Integer>(30); + new HashMap<>(30); static { diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 3c079e9f..2c9402f1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -807,7 +807,7 @@ public static void setClassPath(String schemaName, String path) // Collect and verify that all entries in the path represents a // valid jar // - entries = new ArrayList(); + entries = new ArrayList<>(); try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT jarId FROM sqlj.jar_repository " + diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index da4070f6..fd414e3f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -96,10 +96,10 @@ public Object nextElement() private static final String PUBLIC_SCHEMA = "public"; private static final Map - s_schemaLoaders = new HashMap(); + s_schemaLoaders = new HashMap<>(); private static final Map>> - s_typeMap = new HashMap>>(); + s_typeMap = new HashMap<>(); /** * Removes all cached schema loaders, functions, and type maps. This From c0c14d9948a2e561a541d19d2d0065897589b94d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 19 Mar 2019 22:44:35 -0400 Subject: [PATCH 0283/1087] java7ify more lookups of standard charsets. --- .../postgresql/pljava/jdbc/ObjectResultSet.java | 14 +++----------- .../pljava/jdbc/SPIPreparedStatement.java | 15 ++++----------- .../postgresql/pljava/jdbc/SQLOutputToTuple.java | 13 +++---------- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java index 4e460993..1a014ebb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java @@ -31,7 +31,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.io.UnsupportedEncodingException; +import static java.nio.charset.StandardCharsets.US_ASCII; /** @@ -421,16 +421,8 @@ public void updateArray(int columnIndex, Array x) throws SQLException public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { - try - { - updateObject(columnIndex, - new ClobValue(new InputStreamReader(x, "US-ASCII"), length)); - } - catch(UnsupportedEncodingException e) - { - throw new SQLException( - "US-ASCII encoding is not supported by this JVM"); - } + updateObject(columnIndex, + new ClobValue(new InputStreamReader(x, US_ASCII), length)); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index 6cf8fbc0..f02c4a10 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -16,9 +16,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URL; +import static java.nio.charset.StandardCharsets.US_ASCII; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; @@ -181,16 +181,9 @@ public void setTimestamp(int columnIndex, Timestamp value) throws SQLException @Override public void setAsciiStream(int columnIndex, InputStream value, int length) throws SQLException { - try - { - setObject(columnIndex, - new ClobValue(new InputStreamReader(value, "US-ASCII"), length), - Types.CLOB); - } - catch(UnsupportedEncodingException e) - { - throw new SQLException("US-ASCII encoding is not supported by this JVM"); - } + setObject(columnIndex, + new ClobValue(new InputStreamReader(value, US_ASCII), length), + Types.CLOB); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java index 55b0ec2d..e2234634 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java @@ -18,9 +18,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URL; +import static java.nio.charset.StandardCharsets.US_ASCII; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; @@ -85,15 +85,8 @@ public void writeArray(Array value) throws SQLException public void writeAsciiStream(InputStream value) throws SQLException { - try - { - Reader rdr = new BufferedReader(new InputStreamReader(value, "US-ASCII")); - writeClob(new ClobValue(rdr, ClobValue.getReaderLength(rdr))); - } - catch(UnsupportedEncodingException e) - { - throw new SQLException(e.toString()); - } + Reader rdr = new BufferedReader(new InputStreamReader(value, US_ASCII)); + writeClob(new ClobValue(rdr, ClobValue.getReaderLength(rdr))); } public void writeBigDecimal(BigDecimal value) throws SQLException From 6887e339489267313069692486c6a4099c82589b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 13 Jun 2019 22:06:58 -0400 Subject: [PATCH 0284/1087] Remove the mystery bootclasspath in pom.xml It has never been clear to me why Thomas added this line in 2013 (c795b6b). I have no build difficulties without it. It certainly can't remain for Java 9+, where the runtime files have been rearranged and there is no rt.jar. Perhaps this will impair building on some other platform. We'll see. --- pom.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pom.xml b/pom.xml index 789c1a76..13630b38 100644 --- a/pom.xml +++ b/pom.xml @@ -90,9 +90,6 @@ 1.6 1.6 ${project.build.sourceEncoding} - - ${env.JAVA_HOME}/jre/lib/rt.jar - From 6b14f19042fb0fb7f09faeb5f599fec3e2b08d73 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 13 Jun 2019 22:10:57 -0400 Subject: [PATCH 0285/1087] Handle loss of 1.6 compatibility in JDK 12+ Java 12 bumps the earliest -source/-target that can be requested from 6 to 7, so change the request accordingly if building on 12 or later. Anyone with an actual need to run the resulting pljava.jar on a Java 6 runtime will have to build it with 11 or earlier. --- pom.xml | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 13630b38..69bcc36f 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,45 @@ + + + srctgt6 + + [1.6,12) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + + srctgt7 + + [12,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 7 + 7 + + + + + + + @@ -87,8 +126,6 @@ maven-compiler-plugin 2.5.1 - 1.6 - 1.6 ${project.build.sourceEncoding} From 6cd539b27cbb4b6a2b4a6db0d6a05abccd94487d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 13 Jun 2019 22:24:50 -0400 Subject: [PATCH 0286/1087] Keep the JavaScript build steps working in 9+ Java 9 moves the batteries-included Nashorn JavaScript engine from the boot classpath to the application classpath. Maven does such wacky things with class loader delegation that suddenly the script engine is no longer found by default when building on 9 or later. (https://issues.apache.org/jira/browse/MANTRUN-200) Workaround is to pass a classpathref of maven.plugin.classpath explicitly to + + + + + org.apache.maven.plugins maven-resources-plugin diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml new file mode 100644 index 00000000..54835c75 --- /dev/null +++ b/pljava-pgxs/pom.xml @@ -0,0 +1,85 @@ + + 4.0.0 + + org.postgresql + pljava.app + 1.6.0-SNAPSHOT + + + pljava-pgxs + 0.0.1-SNAPSHOT + maven-plugin + + PL/Java PGXS + The plugin to build native code used inside PL/Java + + + + 1.8 + 1.8 + + + + + org.apache.maven + maven-plugin-api + ${maven.version} + provided + + + org.apache.maven + maven-core + ${maven.version} + provided + + + org.apache.maven + maven-artifact + ${maven.version} + provided + + + org.apache.maven + maven-compat + ${maven.version} + test + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.0 + provided + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.0 + + true + + + + + mojo-descriptor + + descriptor + + + + help-goal + + helpmojo + + + + + + + + diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java new file mode 100644 index 00000000..ea3edd63 --- /dev/null +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020 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: + * Kartik Ohri + */ +package org.postgresql.pljava.pgxs; + +import org.apache.maven.project.MavenProject; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class PGXSUtils +{ + static Pattern mustBeQuotedForC = Pattern.compile( + "([\"\\\\]|(?<=\\?)\\?(?=[=(/)'-]))|" + // (just insert backslash) + "([\\a\b\\f\\n\\r\\t\\x0B])|" + // (use specific escapes) + "(\\p{Cc}((?=\\p{XDigit}))?)" + // use hex, note whether an XDigit follows + ); + + private PGXSUtils () + { + } + + /** + * @param s string to be escaped + * @return s wrapped in double quotes and with internal characters + * escaped where appropriate using the C conventions + */ + public static String quoteStringForC (String s) + { + Matcher m = mustBeQuotedForC.matcher(s); + StringBuffer b = new StringBuffer(); + while (m.find()) + { + if (-1 != m.start(1)) // things that just need a backslash + m.appendReplacement(b, "\\\\$1"); + else if (-1 != m.start(2)) // things with specific escapes + { + char ec = 0; + switch (m.group(2)) // switch/case uses === + { + case "\u0007": + ec = 'a'; + break; + case "\b": + ec = 'b'; + break; + case "\f": + ec = 'f'; + break; + case "\n": + ec = 'n'; + break; + case "\r": + ec = 'r'; + break; + case "\t": + ec = 't'; + break; + case "\u000B": + ec = 'v'; + break; + } + m.appendReplacement(b, "\\\\" + ec); + } + else // it's group 3, use hex escaping + { + m.appendReplacement(b, + "\\\\x" + Integer.toHexString( + m.group(3).codePointAt(0)) + + (-1 == m.start(4) ? "" : "\"\"")); // XDigit follows? + } + } + return "\"" + m.appendTail(b) + "\""; + } + + /** + * @param bytes byte array to be decoded + * @return string decoded from input bytes using default platform charset + * @throws CharacterCodingException if unable to decode bytes using + * default platform charset + */ + public static String defaultCharsetDecodeStrict (byte[] bytes) + throws CharacterCodingException + { + return Charset.defaultCharset().newDecoder() + .decode(ByteBuffer.wrap(bytes)).toString(); + } + + public static String getPgConfigProperty (String pgConfigCommand, + String pgConfigArgument) + throws IOException, InterruptedException + { + if (pgConfigCommand == null || pgConfigCommand.isEmpty()) + pgConfigCommand = "pg_config"; + + ProcessBuilder processBuilder = new ProcessBuilder(pgConfigCommand, + pgConfigArgument); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + Process process = processBuilder.start(); + process.getOutputStream().close(); + byte[] bytes = process.getInputStream().readAllBytes(); + + int exitCode = process.waitFor(); + if (exitCode != 0) + throw new InterruptedException("pg_config process failed and " + + "exited with " + exitCode); + String pgConfigOutput = defaultCharsetDecodeStrict(bytes); + return pgConfigOutput.substring(0, + pgConfigOutput.length() - System.lineSeparator().length()); + } + + /** + * + * @param project maven project for the property key-value pair is to be set + * @param property key for the property to set + * @param propertyValue value of the property + */ + public static void setPgConfigProperty (MavenProject project, + String property, + String propertyValue) + { + project.getProperties().setProperty(property, propertyValue); + } + +} diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java new file mode 100644 index 00000000..0b39cf7a --- /dev/null +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 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: + * Kartik Ohri + */ +package org.postgresql.pljava.pgxs; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +@Mojo(name = "scripting", defaultPhase = LifecyclePhase.INITIALIZE) +public class ScriptingMojo extends AbstractMojo +{ + ScriptEngine engine; + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + MavenProject project; + + @Override + public void execute () + { + ScriptEngineManager manager = new ScriptEngineManager(); + getLog().info("Script Engines"); + manager.getEngineFactories() + .forEach(it -> getLog().info(it.getEngineName())); + engine = manager.getEngineByName("nashorn"); + try + { + engine.getContext().setAttribute("plugin", this, + ScriptContext.GLOBAL_SCOPE); + engine.eval( + "function quoteStringForC(text)" + + "{" + + "return Packages.PGXSUtils.quoteStringForC(text);" + + "}"); + + engine.eval( + "function setProjectProperty(key, value)" + + "{" + + "plugin.setProjectProperty(key, value);" + + "}"); + + engine.eval( + "function getPgConfigProperty(key)" + + "{" + + "return plugin.getPgConfigProperty(key);" + + "}"); + } + catch (ScriptException e) + { + e.printStackTrace(); + } + } + + public void setProjectProperty (String property, String value) + { + PGXSUtils.setPgConfigProperty(project, property, value); + } + + public String getPgConfigProperty (String property) + { + try + { + String pgConfigCommand = project.getProperties() + .getProperty("pgsql.pgconfig"); + return PGXSUtils.getPgConfigProperty(pgConfigCommand, property); + } + catch (Exception e) + { + e.printStackTrace(); + return null; + } + } + + +} diff --git a/pljava-so/build.xml b/pljava-so/build.xml deleted file mode 100644 index 93e1620d..00000000 --- a/pljava-so/build.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 29a7d0f4..7ab74a5b 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -115,10 +115,6 @@ nar-maven-plugin - - - ${MSVC_RINT} - ${PGSQL_INCLUDEDIR-SERVER}/port/win32 ${PGSQL_INCLUDEDIR-SERVER}/port/win32_msvc @@ -277,140 +273,41 @@ - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.7 - - - - pg_config to pgsql.properties - initialize - - run - - - - - - - - - - - - - - - - - org.codehaus.mojo - properties-maven-plugin - 1.0.0 + org.postgresql + pljava-pgxs + 0.0.1-SNAPSHOT + test initialize - read-project-properties + scripting - - ${basedir}/target/pgsql.properties - + diff --git a/pom.xml b/pom.xml index 2bae0837..c1a9cc02 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ pljava-ant pljava-examples pljava-packaging + pljava-pgxs From 1a5811d067fb78b17a5fa19acf2aa2544242aa88 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 19 Jul 2020 13:28:13 +0530 Subject: [PATCH 0571/1087] Temporary fix for ClassLoader issue to make ScriptingMojo work Maven has weird classloading issues due to which it is not able to detect the Nashorn Script Engine. As a temporary fix, the System ClassLoader is passed to the ScriptEngineManager to ensure it is able to find Nashorn. --- .../postgresql/pljava/pgxs/ScriptingMojo.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java index 0b39cf7a..300cc6bc 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -11,18 +11,22 @@ */ package org.postgresql.pljava.pgxs; +import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import java.util.List; -@Mojo(name = "scripting", defaultPhase = LifecyclePhase.INITIALIZE) +@Mojo(name = "scripting", defaultPhase = LifecyclePhase.INITIALIZE, + requiresDependencyResolution = ResolutionScope.TEST) public class ScriptingMojo extends AbstractMojo { ScriptEngine engine; @@ -30,14 +34,22 @@ public class ScriptingMojo extends AbstractMojo @Parameter(defaultValue = "${project}", required = true, readonly = true) MavenProject project; + @Parameter(property = "script") + private String script; + + @Parameter(property = "plugin.artifacts", required = true, readonly = true) + private List pluginArtifacts; + @Override public void execute () { - ScriptEngineManager manager = new ScriptEngineManager(); - getLog().info("Script Engines"); - manager.getEngineFactories() - .forEach(it -> getLog().info(it.getEngineName())); - engine = manager.getEngineByName("nashorn"); + ScriptEngineManager manager = new ScriptEngineManager( + ClassLoader.getSystemClassLoader()); + engine = manager.getEngineByName("JavaScript"); + + getLog().debug(engine.toString()); + getLog().debug(script); + try { engine.getContext().setAttribute("plugin", this, @@ -59,6 +71,7 @@ public void execute () "{" + "return plugin.getPgConfigProperty(key);" + "}"); + engine.eval(script); } catch (ScriptException e) { From 8eb132f27a63f717eda11499a86c684a74f3e0d4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Jul 2020 16:15:41 -0400 Subject: [PATCH 0572/1087] Try a 2-parent classloader for ScriptEngineManager My understanding (since 6cd539b) has been that Java 9 moved Nashorn from the bootstrap classloader to the application classloader [1], and that Maven creates classloaders that do not delegate to the application classloader (a/k/a the system classloader, as various ClassLoader methods still call it). So, what seemed an easy solution would be to create a new ClassLoader as a child of the Maven-supplied loader, but that would also behave as if it had the application class loader as a second parent. It still doesn't find Nashorn though. Back to the drawing board. [1] https://issues.apache.org/jira/browse/MANTRUN-200?focusedCommentId=16138712&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-16138712 --- .../postgresql/pljava/pgxs/ScriptingMojo.java | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java index 300cc6bc..7825632d 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -23,12 +23,79 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; import java.util.List; @Mojo(name = "scripting", defaultPhase = LifecyclePhase.INITIALIZE, requiresDependencyResolution = ResolutionScope.TEST) public class ScriptingMojo extends AbstractMojo { + /** + * A {@code ClassLoader} with (effectively) two parents, the inherited one + * and Java's application class loader. + *

    + * This loader will be given to the {@code ScriptEngineManager}. The + * inherited loader supplied by Maven omits the application class loader, + * to insulate Maven builds from possible differences in the application + * class path; a reasonable idea, but the Nashorn script engine was moved to + * the application class path as of Java 9, so without checking there, we + * will not find it. This loader (like most) delegates first to its parent, + * which should be the Maven-supplied loader; only what is not found there + * will be sought from the application loader. + */ + static class ScriptEngineLoader extends ClassLoader + { + private ScriptEngineLoader(ClassLoader parent) + { + super("pgxsScriptLoader", parent); + } + + /** + * Delegate to the application loader. + *

    + * This is called by the {@code super} implementation of + * {@code loadClass} only after the parent loader has drawn a blank, + * so there is nothing left to do but see if the application loader + * has the class. + */ + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + return findSystemClass(name); + } + + /** + * Delegate to the application loader for finding a resource. + *

    + * This is called by the {@code super} implementation of + * {@code getResource} only after the parent loader has drawn a blank, + * so there is nothing left to do but see if the application loader + * has the resource. + */ + @Override + protected URL findResource(String name) + { + return getSystemResource(name); + } + + /** + * Delegate to the application loader for finding a resource. + *

    + * This is called by the {@code super} implementation of + * {@code getResources} after enumerating the resources available from + * the parent loader. This method needs only to return the resources + * available from the application loader; the caller will combine the + * two enumerations. + */ + @Override + protected Enumeration findResources(String name) throws IOException + { + return getSystemResources(name); + } + } + ScriptEngine engine; @Parameter(defaultValue = "${project}", required = true, readonly = true) @@ -44,7 +111,7 @@ public class ScriptingMojo extends AbstractMojo public void execute () { ScriptEngineManager manager = new ScriptEngineManager( - ClassLoader.getSystemClassLoader()); + new ScriptEngineLoader(ScriptingMojo.class.getClassLoader())); engine = manager.getEngineByName("JavaScript"); getLog().debug(engine.toString()); From f570bab229dd8204df48a22951f6f78e6507e16d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Jul 2020 17:19:49 -0400 Subject: [PATCH 0573/1087] Swap the two parents of ScriptEngineLoader My understanding of the reason Nashorn started failing to load in Java 9 has been somewhat mistaken since 6cd539b. It is not so much that the Maven-supplied class loader never delegates to Java's loader. The key change came with the Java 9 module system, and the way the ServiceLoader works for services in named modules. The former (and current, for unnamed modules) algorithm involved searching for META-INF/services/* resources to find the providers of services. For named modules, the bindings are computed from the module-info files, and the javadoc for ServiceLoader.load provides: "Step 1: Locate providers in named modules. Service providers are located in all named modules of the class loader or to any class loader reachable via parent delegation." That turns out to be literally true. The ServiceLoader is not guided by whatever delegation policy a class loader actually implements by the way it overrides ClassLoader's default search order. To identify reachable modules, ServiceLoader only knows to follow a ClassLoader's actual parent link, nothing else. That means this two-parent classloader idea wasn't wrong, but if it's to find providers in Java-supplied modules, it needs its parents swapped so the Java application class loader is the real one and the loader supplied by Maven is the 'extra' one. With that delegation order, it could present some risk of class version conflicts, if the Maven-supplied loader has defined some classes that should supersede those from the application loader. That could be fixed by overriding other ClassLoader methods to change the standard delegation strategy. But with the narrow, targeted use of this loader only with the ScriptEngineManager, the risk may be low enough. It is fortunate that, for now, Maven itself and Maven plugins are not heavy users of named modules. This headache could recur if it ever becomes necessary to load named-module services from *both* parents of this loader. At that point, it could become necessary to explore whether the comments in ServiceLoader.load about ModuleLayer searching would offer any solution. --- .../postgresql/pljava/pgxs/ScriptingMojo.java | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java index 7825632d..b188e217 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -37,62 +37,74 @@ public class ScriptingMojo extends AbstractMojo * and Java's application class loader. *

    * This loader will be given to the {@code ScriptEngineManager}. The - * inherited loader supplied by Maven omits the application class loader, - * to insulate Maven builds from possible differences in the application - * class path; a reasonable idea, but the Nashorn script engine was moved to - * the application class path as of Java 9, so without checking there, we - * will not find it. This loader (like most) delegates first to its parent, - * which should be the Maven-supplied loader; only what is not found there - * will be sought from the application loader. + * inherited loader supplied by Maven does not have Java's application + * class loader as its parent (or ancestor), which leaves Java's + * {@code ServiceLoader} mechanism unable to find Nashorn's script engine. + * Therefore, this loader will declare the Java application class loader + * as its actual parent, and search the Maven-supplied class loader for + * whatever the application class loader does not find. + *

    + * This could pose a risk of class version conflicts if the Maven-supplied + * loader has defined classes that are also on the application class path. + * It would be safer to delegate to Maven's loader first and the parent as + * fallback. That would require overriding more of {@code ClassLoader}'s + * default functionality, though. With any luck, the targeted use of this + * loader only with the {@code ScriptEngineManager} will minimize the risk. */ static class ScriptEngineLoader extends ClassLoader { - private ScriptEngineLoader(ClassLoader parent) + private final ClassLoader mavenLoader; + + private ScriptEngineLoader(ClassLoader mavenLoader) { - super("pgxsScriptLoader", parent); + super("pgxsScriptLoader", ClassLoader.getSystemClassLoader()); + this.mavenLoader = mavenLoader; } /** - * Delegate to the application loader. + * Delegate to the Maven-supplied loader. *

    * This is called by the {@code super} implementation of * {@code loadClass} only after the parent loader has drawn a blank, - * so there is nothing left to do but see if the application loader + * so there is nothing left to do but see if the Maven-supplied loader * has the class. */ @Override protected Class findClass(String name) throws ClassNotFoundException { - return findSystemClass(name); + Class rslt = mavenLoader.loadClass(name); + return rslt; } /** - * Delegate to the application loader for finding a resource. + * Delegate to the Maven-supplied loader for finding a resource. *

    * This is called by the {@code super} implementation of * {@code getResource} only after the parent loader has drawn a blank, - * so there is nothing left to do but see if the application loader + * so there is nothing left to do but see if the Maven-supplied loader * has the resource. */ @Override protected URL findResource(String name) { - return getSystemResource(name); + URL rslt = mavenLoader.getResource(name); + return rslt; } /** - * Delegate to the application loader for finding a resource. + * Delegate to the Maven-supplied loader for finding a resource. *

    * This is called by the {@code super} implementation of * {@code getResources} after enumerating the resources available from * the parent loader. This method needs only to return the resources - * available from the application loader; the caller will combine the + * available from the Maven-supplied loader; the caller will combine the * two enumerations. */ @Override protected Enumeration findResources(String name) throws IOException { - return getSystemResources(name); + Enumeration rslt = mavenLoader.getResources(name); + return rslt; } } From 8ef1c7c0c48ed034598543aa632b440ef7e05007 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 19 Jul 2020 17:27:54 -0400 Subject: [PATCH 0574/1087] It's the platform loader, not the application one I had been relying on [1] in believing that Nashorn had been moved onto the application class path, but it turns out to be found with no trouble by the platform class loader. If the Maven developers were concerned about stability if delegating to the application loader, that concern should be reduced by delegating just to the platform loader instead. [1] https://issues.apache.org/jira/browse/MANTRUN-200?focusedCommentId=16138712&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-16138712 --- .../postgresql/pljava/pgxs/ScriptingMojo.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java index b188e217..b6aa6f4a 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -34,22 +34,24 @@ public class ScriptingMojo extends AbstractMojo { /** * A {@code ClassLoader} with (effectively) two parents, the inherited one - * and Java's application class loader. + * and Java's platform class loader. *

    * This loader will be given to the {@code ScriptEngineManager}. The - * inherited loader supplied by Maven does not have Java's application + * inherited loader supplied by Maven does not have Java's platform * class loader as its parent (or ancestor), which leaves Java's * {@code ServiceLoader} mechanism unable to find Nashorn's script engine. - * Therefore, this loader will declare the Java application class loader + * Therefore, this loader will declare the Java platform class loader * as its actual parent, and search the Maven-supplied class loader for - * whatever the application class loader does not find. + * whatever the platform class loader does not find. *

    * This could pose a risk of class version conflicts if the Maven-supplied - * loader has defined classes that are also on the application class path. + * loader has defined classes that are also known to the platform loader. * It would be safer to delegate to Maven's loader first and the parent as * fallback. That would require overriding more of {@code ClassLoader}'s * default functionality, though. With any luck, the targeted use of this - * loader only with the {@code ScriptEngineManager} will minimize the risk. + * loader only with the {@code ScriptEngineManager} will minimize the risk, + * already low because it would be odd to override classes of the Java + * platform itself. */ static class ScriptEngineLoader extends ClassLoader { @@ -57,7 +59,7 @@ static class ScriptEngineLoader extends ClassLoader private ScriptEngineLoader(ClassLoader mavenLoader) { - super("pgxsScriptLoader", ClassLoader.getSystemClassLoader()); + super("pgxsScriptLoader", ClassLoader.getPlatformClassLoader()); this.mavenLoader = mavenLoader; } From e47ce41adfe39249baef21c34d6c54735c113996 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Wed, 22 Jul 2020 00:30:40 +0530 Subject: [PATCH 0575/1087] Allow script tag to specify script engine and mime type --- pljava-packaging/pom.xml | 3 +- .../postgresql/pljava/pgxs/ScriptingMojo.java | 92 ++++++++++--------- pljava-so/pom.xml | 4 +- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 990e3b81..0beefe58 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -81,12 +81,13 @@ 0.0.1-SNAPSHOT + execute-script initialize scripting - diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 27acbe12..c3bbf68d 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -82,21 +82,46 @@ + - org.apache.maven.plugins - maven-javadoc-plugin - - 3.0.1 + org.postgresql + pljava-pgxs + 0.0.1-SNAPSHOT + + + + + generate-javadoc + + + + - org.postgresql.pljava.example.saxon + + diff --git a/pljava/pom.xml b/pljava/pom.xml index 0c9f2fe1..d70a4f61 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -37,31 +37,43 @@ - org.apache.maven.plugins - maven-javadoc-plugin + org.postgresql + pljava-pgxs + 0.0.1-SNAPSHOT + + + + + generate-javadoc + + + + - ${project.name} ${project.version} - ${project.name} ${project.version} - - - --module - org.postgresql.pljava.internal - --module-source-path - org.postgresql.pljava.internal=${basedir}/src/main/java - --show-module-contents - all - --show-packages - all - - - **/org/**/*.java - + diff --git a/pom.xml b/pom.xml index c1a9cc02..9d5730a2 100644 --- a/pom.xml +++ b/pom.xml @@ -186,24 +186,6 @@ maven-project-info-reports-plugin 3.0.0 - - org.apache.maven.plugins - maven-javadoc-plugin - 3.2.0 - - - - javadoc-no-fork - - - - - false - - https://docs.oracle.com/javase/10/docs/api/ - - - From d6468be3493b86cbd6f6092801ce43d52dde45ce Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Tue, 28 Jul 2020 23:13:08 +0530 Subject: [PATCH 0580/1087] Replace explicit ProcessBuilder call with DocumentationTool --- ...avadocMojo.java => DocumentationMojo.java} | 10 +++---- .../org/postgresql/pljava/pgxs/PGXSUtils.java | 28 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) rename pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/{JavadocMojo.java => DocumentationMojo.java} (88%) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/JavadocMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/DocumentationMojo.java similarity index 88% rename from pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/JavadocMojo.java rename to pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/DocumentationMojo.java index 14dd857b..fd47655d 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/JavadocMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/DocumentationMojo.java @@ -28,7 +28,7 @@ @Mojo(name = "generate-javadoc") @Execute(phase = LifecyclePhase.NONE) -public class JavadocMojo extends AbstractMavenReport +public class DocumentationMojo extends AbstractMavenReport { @Parameter private PlexusConfiguration script; @@ -38,13 +38,13 @@ public class JavadocMojo extends AbstractMavenReport @Override public String getOutputName () { - return "'PL/Java' Javadoc Report"; + return "Documentation Report"; } @Override public String getName (Locale locale) { - return String.format(locale, "%s", "PL/Java Javadoc Report"); + return String.format(locale, "%s", "Documentation Report"); } @Override @@ -60,7 +60,7 @@ protected void executeReport (Locale locale) throws MavenReportException { ScriptEngine engine = PGXSUtils.getScriptEngine(script, getLog()); String scriptText = script.getValue(); - getLog().error(scriptText); + getLog().debug(scriptText); engine.getContext().setAttribute("report", this, ScriptContext.GLOBAL_SCOPE); @@ -68,7 +68,7 @@ protected void executeReport (Locale locale) throws MavenReportException (Consumer) this::addJavadocArgument); engine.eval(scriptText); - PGXSUtils.executeJavadocTool("javadoc", javadocArguments); + PGXSUtils.executeDocumentationTool(javadocArguments); } catch (Exception e) { diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index 7af46cdb..89ee0bd4 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -17,6 +17,8 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; +import javax.tools.DocumentationTool; +import javax.tools.ToolProvider; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; @@ -188,24 +190,16 @@ public static void setPgConfigProperty (MavenProject project, project.getProperties().setProperty(property, propertyValue); } - public static void executeJavadocTool(String javadocCommand, - List javadocArguments) - throws IOException, InterruptedException + /** + * + * @param javadocArguments arguments to be passed to the documentation tool + */ + public static void executeDocumentationTool(List javadocArguments) { - String[] args = new String[javadocArguments.size() + 1]; - int index = 0; - args[index++] = javadocCommand; - for (String arg : javadocArguments) - args[index++] = arg; - - ProcessBuilder processBuilder = new ProcessBuilder(args); - - processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); - Process process = processBuilder.start(); - int exitCode = process.waitFor(); - if (exitCode != 0) - throw new InterruptedException("javadoc tool failed to execute" + - "successfully and exited with " +exitCode); + DocumentationTool tool = ToolProvider.getSystemDocumentationTool(); + DocumentationTool.DocumentationTask task = tool.getTask(null, + null, null, null, javadocArguments, null); + task.call(); } } From 3b5b39d6c8137131227356bf2b2a12fa1837f9ac Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Wed, 29 Jul 2020 00:34:05 +0530 Subject: [PATCH 0581/1087] Move usage of javadoc tool from mojo to script The ReportScriptingMojo is now completely agnostic to what it does during the site phase. The Mojo loads the script and executes it. This opens it up to other potential use cases. --- pljava-api/pom.xml | 38 ++++++++------ pljava-examples/pom.xml | 44 +++++++++------- .../org/postgresql/pljava/pgxs/PGXSUtils.java | 15 ------ ...tionMojo.java => ReportScriptingMojo.java} | 25 ++------- pljava/pom.xml | 52 +++++++++++-------- 5 files changed, 79 insertions(+), 95 deletions(-) rename pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/{DocumentationMojo.java => ReportScriptingMojo.java} (65%) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 9c53118a..eecd67db 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -45,28 +45,34 @@ - generate-javadoc + scripting-report diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index c3bbf68d..26abb6cb 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -93,31 +93,37 @@ - generate-javadoc + scripting-report diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index 89ee0bd4..26a1711d 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -17,13 +17,10 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; -import javax.tools.DocumentationTool; -import javax.tools.ToolProvider; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; -import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -190,16 +187,4 @@ public static void setPgConfigProperty (MavenProject project, project.getProperties().setProperty(property, propertyValue); } - /** - * - * @param javadocArguments arguments to be passed to the documentation tool - */ - public static void executeDocumentationTool(List javadocArguments) - { - DocumentationTool tool = ToolProvider.getSystemDocumentationTool(); - DocumentationTool.DocumentationTask task = tool.getTask(null, - null, null, null, javadocArguments, null); - task.call(); - } - } diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/DocumentationMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java similarity index 65% rename from pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/DocumentationMojo.java rename to pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java index fd47655d..4edc78fa 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/DocumentationMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java @@ -16,25 +16,18 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.reporting.AbstractMavenReport; -import org.apache.maven.reporting.MavenReportException; import org.codehaus.plexus.configuration.PlexusConfiguration; -import javax.script.ScriptContext; import javax.script.ScriptEngine; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; -import java.util.function.Consumer; -@Mojo(name = "generate-javadoc") +@Mojo(name = "scripting-report") @Execute(phase = LifecyclePhase.NONE) -public class DocumentationMojo extends AbstractMavenReport +public class ReportScriptingMojo extends AbstractMavenReport { @Parameter private PlexusConfiguration script; - private final List javadocArguments = new ArrayList<>(); - @Override public String getOutputName () { @@ -54,21 +47,14 @@ public String getDescription (Locale locale) } @Override - protected void executeReport (Locale locale) throws MavenReportException + protected void executeReport (Locale locale) { try { ScriptEngine engine = PGXSUtils.getScriptEngine(script, getLog()); String scriptText = script.getValue(); getLog().debug(scriptText); - - engine.getContext().setAttribute("report", this, - ScriptContext.GLOBAL_SCOPE); - engine.put("addJavadocArgument", - (Consumer) this::addJavadocArgument); engine.eval(scriptText); - - PGXSUtils.executeDocumentationTool(javadocArguments); } catch (Exception e) { @@ -76,9 +62,4 @@ protected void executeReport (Locale locale) throws MavenReportException } } - public void addJavadocArgument(String argument) - { - javadocArguments.add(argument); - } - } diff --git a/pljava/pom.xml b/pljava/pom.xml index d70a4f61..157decba 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -44,35 +44,41 @@ - generate-javadoc + scripting-report From 62d7c34c62e3f8c2bf7146aff6974fba3aba81a0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 28 Jul 2020 22:51:26 -0400 Subject: [PATCH 0582/1087] A step or two further The javadoc for pljava/ doesn't fail now. It was the module-path for its pljava-api dependency. I've hardcoded it here, but really, I am pretty sure Maven has a method "org.postgresql:pljava-api is one of my dependencies, what is the path to that jar?" That method (or a convenience wrapper for it) should be exposed to the script engine so this can be done right. In pljava-examples/ there is also a dependency on the pljava-api jar; that should also come from Maven. There is still something very puzzling about pljava/ though. It is documenting only the mbeans package, even though it is being passed --show-module-contents all and --show-packages all, and it is complaining about "unmappable character ... for encoding US-ASCII" even though it is being passed both -encoding UTF-8 and -docencoding UTF-8. It is almost as if it is ignoring some arguments. I have not determined why. For pljava-examples it was complaining about non-ASCII characters and I added the -encoding and -docencoding and it stopped. So they aren't ignored there. Puzzling. To see the generated Maven reports, run mvn site site:stage and then use a web browser to open target/staging/index.html. You can click one of the links under Modules (pljava-api, pljava, pljava-examples), and then under Project Reports click Documentation Report. It's blank. The script needs to be able to get a reference to the Doxia Sink (the script engine used to get a 'report' entry referencing 'this'; probably it still should, and getSink() can be called on that. The script should output to the sink at least a heading and a link to apidocs/index.html. --- pljava-api/pom.xml | 36 +++++++++++-------------- pljava-examples/pom.xml | 59 +++++++++++++++++------------------------ pljava/pom.xml | 51 ++++++++++++++++------------------- 3 files changed, 63 insertions(+), 83 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index eecd67db..25306931 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -52,27 +52,23 @@ diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 26abb6cb..135f7a2d 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -33,17 +33,6 @@ - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - - - @@ -100,30 +89,30 @@ diff --git a/pljava/pom.xml b/pljava/pom.xml index 157decba..36d385a0 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -51,34 +51,29 @@ From 133f6a321e9c8be1f34a34fc3c9899face1152b4 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 30 Jul 2020 19:40:19 +0530 Subject: [PATCH 0583/1087] Expose path to pljava-api jar to scripts using ReportScriptingMojo --- pljava-examples/pom.xml | 4 ++-- .../postgresql/pljava/pgxs/ReportScriptingMojo.java | 11 +++++++++++ pljava/pom.xml | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 135f7a2d..fb07424e 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -103,8 +103,8 @@ var args = java.util.List.of( "-use", "-version", "-protected", - "-classpath", // XXX get this from Maven as pljava-api dependency classpath - "${basedir}/../pljava-api/target/pljava-api-${project.version}.jar", + "-classpath", + pljava_api_jar_path, "org.postgresql.pljava.example", "org.postgresql.pljava.example.annotation" ); diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java index 4edc78fa..c4dbfe7e 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java @@ -11,6 +11,7 @@ */ package org.postgresql.pljava.pgxs; +import org.apache.maven.artifact.Artifact; import org.apache.maven.plugins.annotations.Execute; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -20,6 +21,7 @@ import javax.script.ScriptEngine; import java.util.Locale; +import java.util.Set; @Mojo(name = "scripting-report") @Execute(phase = LifecyclePhase.NONE) @@ -51,7 +53,16 @@ protected void executeReport (Locale locale) { try { + String pljavaApiJar = null; + Set artifacts = project.getArtifacts(); + for (Artifact artifact : artifacts) + { + String path = artifact.getFile().getAbsolutePath(); + if (path.contains("pljava-api")) + pljavaApiJar = path; + } ScriptEngine engine = PGXSUtils.getScriptEngine(script, getLog()); + engine.put("pljava_api_jar_path", pljavaApiJar); String scriptText = script.getValue(); getLog().debug(scriptText); engine.eval(scriptText); diff --git a/pljava/pom.xml b/pljava/pom.xml index 36d385a0..167e5761 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -66,8 +66,8 @@ var args = java.util.List.of( + "${basedir}/src/main/java", "-doctitle", "${project.name} ${project.version}", "-windowtitle", "${project.name} ${project.version}", - "--module-path", // should be obtained from Maven for pljava-api dependency - "${basedir}/../pljava-api/target/pljava-api-${project.version}.jar" + "--module-path", + pljava_api_jar_path ); java.lang.System.out.println(args); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); From c796ac34c5a875ec5996156c4f714c16bd599333 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 30 Jul 2020 19:41:46 +0530 Subject: [PATCH 0584/1087] Use Doxia sink to generate report html --- pljava-api/pom.xml | 16 ++++++++++++++++ pljava-examples/pom.xml | 16 ++++++++++++++++ .../pljava/pgxs/ReportScriptingMojo.java | 1 + pljava/pom.xml | 16 ++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 25306931..a3ae39ec 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -68,6 +68,22 @@ var args = java.util.List.of( var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); var task = tool.getTask(null, null, null, null, args, null); task.call(); + +var sink = report.getSink(); +sink.head(); +sink.title(); +sink.text("${project.name} Documentation Report"); +sink.title_(); +sink.head_(); +sink.body(); +sink.section1(); +sink.sectionTitle(); +sink.link("apidocs/index.html"); +sink.text("Go to documentation"); +sink.sectionTitle_(); +sink.link_(); +sink.section1_(); +sink.body_(); ]]> diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index fb07424e..00b2b824 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -112,6 +112,22 @@ java.lang.System.out.println(args); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); var task = tool.getTask(null, null, null, null, args, null); task.call(); + +var sink = report.getSink(); +sink.head(); +sink.title(); +sink.text("${project.name} Documentation Report"); +sink.title_(); +sink.head_(); +sink.body(); +sink.section1(); +sink.sectionTitle(); +sink.link("apidocs/index.html"); +sink.text("Go to documentation"); +sink.sectionTitle_(); +sink.link_(); +sink.section1_(); +sink.body_(); ]]> diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java index c4dbfe7e..1c33dda3 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java @@ -62,6 +62,7 @@ protected void executeReport (Locale locale) pljavaApiJar = path; } ScriptEngine engine = PGXSUtils.getScriptEngine(script, getLog()); + engine.put("report", this); engine.put("pljava_api_jar_path", pljavaApiJar); String scriptText = script.getValue(); getLog().debug(scriptText); diff --git a/pljava/pom.xml b/pljava/pom.xml index 167e5761..c1d37cec 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -73,6 +73,22 @@ java.lang.System.out.println(args); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); var task = tool.getTask(null, null, null, null, args, null); task.call(); + +var sink = report.getSink(); +sink.head(); +sink.title(); +sink.text("${project.name} Documentation Report"); +sink.title_(); +sink.head_(); +sink.body(); +sink.section1(); +sink.sectionTitle(); +sink.link("apidocs/index.html"); +sink.text("Go to documentation"); +sink.sectionTitle_(); +sink.link_(); +sink.section1_(); +sink.body_(); ]]> From f87f204e721d10f3924858937f830130d945c06e Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 31 Jul 2020 02:22:54 +0530 Subject: [PATCH 0585/1087] Expose some more useful methods from ReportScriptingMojo for use in scripting --- pljava-api/pom.xml | 2 +- pljava-examples/pom.xml | 13 ++++++--- .../pljava/pgxs/ReportScriptingMojo.java | 27 +++++++++++-------- pljava/pom.xml | 16 ++++++----- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index a3ae39ec..6ecc61e2 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -80,8 +80,8 @@ sink.section1(); sink.sectionTitle(); sink.link("apidocs/index.html"); sink.text("Go to documentation"); -sink.sectionTitle_(); sink.link_(); +sink.sectionTitle_(); sink.section1_(); sink.body_(); ]]> diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 00b2b824..25c909c4 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -90,15 +90,20 @@ diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 25c909c4..54c91d6e 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -117,22 +117,6 @@ java.lang.System.out.println(args); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); var task = tool.getTask(null, null, null, null, args, null); task.call(); - -var sink = report.getSink(); -sink.head(); -sink.title(); -sink.text("${project.name} Documentation Report"); -sink.title_(); -sink.head_(); -sink.body(); -sink.section1(); -sink.sectionTitle(); -sink.link("apidocs/index.html"); -sink.text("Go to documentation"); -sink.link_(); -sink.sectionTitle_(); -sink.section1_(); -sink.body_(); ]]> diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java index cbc7f95e..8cb2593f 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java @@ -20,6 +20,7 @@ import org.codehaus.plexus.configuration.PlexusConfiguration; import javax.script.ScriptEngine; +import java.io.File; import java.util.Locale; @Mojo(name = "scripting-report") @@ -32,7 +33,13 @@ public class ReportScriptingMojo extends AbstractMavenReport @Override public String getOutputName () { - return "Documentation Report"; + return "apidocs" + File.separator + "index"; + } + + @Override + public boolean isExternalReport () + { + return true; } @Override diff --git a/pljava/pom.xml b/pljava/pom.xml index 11f7f90d..5b90e98c 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -77,22 +77,6 @@ java.lang.System.out.println(args); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); var task = tool.getTask(null, null, null, null, args, null); task.call(); - -var sink = report.getSink(); -sink.head(); -sink.title(); -sink.text("${project.name} Documentation Report"); -sink.title_(); -sink.head_(); -sink.body(); -sink.section1(); -sink.sectionTitle(); -sink.link("apidocs/index.html"); -sink.text("Go to documentation"); -sink.link_(); -sink.sectionTitle_(); -sink.section1_(); -sink.body_(); ]]> From 6ac9059352f1c831c0a300e849a44b40f49fcb09 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Tue, 4 Aug 2020 17:13:50 +0530 Subject: [PATCH 0587/1087] Create ReportScript interface for extensibility Some parameters like getOutputName and so on were hardcoded inside the ReportScriptingMojo. While this was enough to meet the current needs of generating javadoc, it could be problematic with other future potential use cases. Therefore, a ReportScript interface has been created with some relevant methods that are subset of AbstractMavenReport class. The script can extend the interface and override only the values it wants to. For the remaining, the defaults will be used. This new instance must then be passed to the ReportScriptingMojo using setReportScript. --- pljava-api/pom.xml | 8 +++ .../postgresql/pljava/pgxs/ReportScript.java | 63 +++++++++++++++++++ .../pljava/pgxs/ReportScriptingMojo.java | 49 +++++++++------ 3 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 25306931..e0e4c399 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -53,6 +53,14 @@ diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 54c91d6e..bb3c62e8 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -90,33 +90,57 @@ diff --git a/pljava/pom.xml b/pljava/pom.xml index 5b90e98c..87728892 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -52,31 +52,55 @@ From 1b557d93179892167ce320a1033667603feb2d6f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 4 Aug 2020 23:07:34 -0400 Subject: [PATCH 0591/1087] Improve pljava and pljava-api javadoc links When documenting modular code, javadoc generates an index.html with nothing much in it but a link to the module-summary.html for the module. We can eliminate one click for the person reading the docs by having getOutputName return the relative path to the module-summary file instead, so Maven generates a link directly to it. I am already liking this better than the maven-javadoc-plugin. For pljava/ though, it still seems to be ignoring --show-packages all, and for the life of me I still don't know why. --- pljava-api/pom.xml | 4 +++- pljava/pom.xml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 63488cb7..f5126d97 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -65,7 +65,9 @@ function getDescription(report, locale) function getOutputName(report) { - return java.nio.file.Paths.get("apidocs", "index").toString(); + return java.nio.file.Paths + .get("apidocs", "org.postgresql.pljava", "module-summary") + .toString(); } function isExternalReport(report) diff --git a/pljava/pom.xml b/pljava/pom.xml index 87728892..b20855de 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -64,7 +64,9 @@ function getDescription(report, locale) function getOutputName(report) { - return java.nio.file.Paths.get("apidocs", "index").toString(); + return java.nio.file.Paths + .get("apidocs", "org.postgresql.pljava.internal", "module-summary") + .toString(); } function isExternalReport(report) From 8b7f12f8094751b0efdff43d7850cb5d36d06a8b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 4 Aug 2020 23:17:48 -0400 Subject: [PATCH 0592/1087] Move each report script inside its reportSet ... opening the possibility to create other reportSets using other scripts to make other kinds of report. --- pljava-api/pom.xml | 12 ++++++------ pljava-examples/pom.xml | 12 ++++++------ pljava/pom.xml | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index f5126d97..e6a5fbd9 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -48,10 +48,8 @@ scripting-report - - - - - + + + + diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index bb3c62e8..79c51b89 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -85,10 +85,8 @@ scripting-report - - - - - + + + + diff --git a/pljava/pom.xml b/pljava/pom.xml index b20855de..7f511b0a 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -47,10 +47,8 @@ scripting-report - - - - - + + + + From 980c9d3caa6a33fec01e4aff111ddcb8224028da Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 5 Aug 2020 16:56:45 -0400 Subject: [PATCH 0593/1087] Use the logger rather than blatting to stdout --- pljava-api/pom.xml | 2 ++ pljava-examples/pom.xml | 3 ++- pljava/pom.xml | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index e6a5fbd9..8b966a05 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -88,7 +88,9 @@ function executeReport(report, locale) "-version" ); + report.log.debug(args.toString()); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); + report.log.debug(tool.toString()); var task = tool.getTask(null, null, null, null, args, null); task.call(); } diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 79c51b89..f8f9e008 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -134,8 +134,9 @@ function executeReport(report, locale) "org.postgresql.pljava.example", "org.postgresql.pljava.example.annotation" ); - java.lang.System.out.println(args); + report.log.debug(args.toString()); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); + report.log.debug(tool.toString()); var task = tool.getTask(null, null, null, null, args, null); task.call(); } diff --git a/pljava/pom.xml b/pljava/pom.xml index 7f511b0a..b8060364 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -96,8 +96,9 @@ function executeReport(report, locale) "-windowtitle", "${project.name} ${project.version}", "--module-path", pljava_api_jar_path ); - java.lang.System.out.println(args); + report.log.debug(args.toString()); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); + report.log.debug(tool.toString()); var task = tool.getTask(null, null, null, null, args, null); task.call(); } From 1ae9eb6d006cf17e2c408aa4de6f0bb9c8cbc540 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 5 Aug 2020 18:43:25 -0400 Subject: [PATCH 0594/1087] Ensure plugin works in Java 15 for reports too Profiles that add dependencies to pljava-pgxs can't be in the parent pom, as they would cause failure in a first-time build where that plugin isn't built yet. So they have to be duplicated in each submodule that uses pljava-pgxs, and there are more of those now, thanks to its use for javadocs. Not delightful, but bearable. In profiles that add dependencies bar and baz to plugin foo, it isn't necessary to specify the version of foo; that would just become a value duplicated in many places and probably overlooked for updates. The profile in the parent pom that adds the graaljs dependencies to maven-antrun-plugin still needs to be there, as long as we are still using that plugin to run any scripts. One day if all those scripts have been migrated to use the new plugin, that will become unnecessary. That profile is also responsible for setting the polyglot.js.nashorn-compat system property to true, without which the Graal engine behaves differently from the Nashorn one. That turns out not to be enough, because properties-maven-plugin doesn't run in the 'site' lifecycle, but the report scripts still need the property. So a bit of hardcoding in PGXSUtils will also set it, if it isn't set and this is Java 15 or later. Also not delightful, but it seems to be necessary. That means that the setting of that property via profile in the parent pom could also become unnecessary if all uses of script in antrun get migrated to the new plugin. In passing, fix an older apparent logic bug in PGXSUtils and put a comment back on the line where it belonged. --- pljava-api/pom.xml | 30 ++++++++++++ pljava-examples/pom.xml | 27 +++++++++++ pljava-packaging/pom.xml | 1 - .../org/postgresql/pljava/pgxs/PGXSUtils.java | 24 ++++++++-- pljava-so/pom.xml | 1 - pljava/pom.xml | 29 ++++++++++++ pom.xml | 47 +++++++++++++++++++ 7 files changed, 152 insertions(+), 7 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 8b966a05..b840ba58 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -9,6 +9,36 @@ pljava-api PL/Java API The API for Java stored procedures in PostgreSQL using PL/Java + + + + nashorngone + + [15,) + + + + + org.postgresql + pljava-pgxs + + + org.graalvm.js + js + 20.1.0 + + + org.graalvm.js + js-scriptengine + 20.1.0 + + + + + + + + diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index f8f9e008..0b3a5177 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -34,6 +34,33 @@ + + + nashorngone + + [15,) + + + + + org.postgresql + pljava-pgxs + + + org.graalvm.js + js + 20.1.0 + + + org.graalvm.js + js-scriptengine + 20.1.0 + + + + + + diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 63efcbd9..5804c840 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -50,7 +50,6 @@ org.postgresql pljava-pgxs - 0.0.1-SNAPSHOT org.graalvm.js diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index 26a1711d..c9f219dc 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -26,11 +26,10 @@ public final class PGXSUtils { - static Pattern mustBeQuotedForC = Pattern.compile( + static final Pattern mustBeQuotedForC = Pattern.compile( "([\"\\\\]|(?<=\\?)\\?(?=[=(/)'-]))|" + // (just insert backslash) - "([\\a\b\\f\\n\\r\\t\\x0B])|" + // (use specific escapes) - "(\\p{Cc}((?=\\p{XDigit}))?)" - // use hex, note whether an XDigit follows + "([\\a\b\\f\\n\\r\\t\\x0B])|" + // (use specific escapes) + "(\\p{Cc}((?=\\p{XDigit}))?)" // use hex, note whether an XDigit follows ); private PGXSUtils () @@ -47,6 +46,20 @@ private PGXSUtils () */ static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log) { + /* + * Set the polyglot.js.nashorn-compat system property to true if it is + * unset and this is Java >= 15. It would be preferable to set this in + * a pom profile rather than hardcoding it here; properties-maven-plugin + * can do it, but that doesn't happen in the 'site' lifecycle, and we + * use scripting in reports too. In Java >= 15, the Nashorn JavaScript + * engine isn't available, and a profile will have arranged for Graal's + * JavaScript engine to be on the classpath, but it doesn't behave + * compatibly with Nashorn unless this property is set. + */ + if ( 0 <= Runtime.version().compareTo(Runtime.Version.parse("15-ea")) ) + System.getProperties() + .putIfAbsent("polyglot.js.nashorn-compat", "true"); + ScriptEngine engine = null; try { @@ -67,7 +80,8 @@ static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log) if (mimeType != null) if (engine != null) { - if (engine.getFactory().getMimeTypes().contains(mimeType)) + if ( ! engine.getFactory().getMimeTypes() + .contains(mimeType) ) log.warn("Specified engine does " + "not have given mime type : " + mimeType); } diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 3941e04b..6b48b8e4 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -86,7 +86,6 @@ org.postgresql pljava-pgxs - 0.0.1-SNAPSHOT org.graalvm.js diff --git a/pljava/pom.xml b/pljava/pom.xml index b8060364..fbf5a9e6 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -17,6 +17,35 @@ + + + nashorngone + + [15,) + + + + + org.postgresql + pljava-pgxs + + + org.graalvm.js + js + 20.1.0 + + + org.graalvm.js + js-scriptengine + 20.1.0 + + + + + + + + diff --git a/pom.xml b/pom.xml index 9d5730a2..420b2a43 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,53 @@ + + + nashorngone + + [15,) + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + initialize + + set-system-properties + + + + + true + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + org.graalvm.js + js + 20.1.0 + + + org.graalvm.js + js-scriptengine + 20.1.0 + + + + + + From f413312ce06825749e31b5035dd15f3c1f1f6972 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 5 Aug 2020 21:40:36 -0400 Subject: [PATCH 0595/1087] Work on getting the javadocs right The javadoc for pljava/ was messing up in surprising ways, complaining about non-ASCII source characters even though -encoding was specified, and ignoring the --show-module-contents and --show-packages specifications. The ignoring of -encoding was, of all things, the result of including --release 9. Apparently there is some bug where using the --release option clobbers an -encoding option. The --show-* trouble was pilot error: avoid using the legacy one-stop-shopping options like -private/-package/-protected/ -public in combination with the newer -show-* options, because the former can clobber the latter. To aid comprehension and troubleshooting, group the javadoc options the same way they are in the tool's documentation: first the ones that match javac options, then the core javadoc options, then the ones that are interpreted by the standard doclet. Phase out interpolated strings like ${project.name} in the JavaScript. It was convenient for prototyping and pleasant to look at, but of course Maven knows nothing about JavaScript syntax and it would make a mess if an interpolated value had special characters in it. The same values are available via access to the 'report' parameter. Because the --module-source-path mod=path syntax is used, and that appeared in javadoc 12, override canGenerateReport and return false if the JVM is earlier. There may be other ways of getting the javadoc to generate in 9 through 11, but the extra complexity might be worse than just saying "you need 12 or later to build the javadocs". --- pljava/pom.xml | 52 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/pljava/pom.xml b/pljava/pom.xml index fbf5a9e6..2075afea 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -79,6 +79,19 @@ diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index c9f219dc..ca16ae2a 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -15,12 +15,19 @@ import org.apache.maven.project.MavenProject; import org.codehaus.plexus.configuration.PlexusConfiguration; +import javax.script.ScriptContext; +import static javax.script.ScriptContext.GLOBAL_SCOPE; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; + +import javax.tools.Diagnostic; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -92,10 +99,54 @@ static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log) throw new IllegalArgumentException("No suitable engine " + "found for specified engine name or mime type"); } - log.debug(engine.toString()); + log.debug("Loaded script engine " + engine); } catch (Exception e) { log.error(e); } + + /* + * Give the script some convenient methods for logging to the Maven log. + * Only supply the versions with one CharSequence parameter, in case of + * a script engine that might not handle overloads well. The script may + * have another way to get access to the Log instance and use its other + * methods; these are just for convenience. + */ + ScriptContext context = engine.getContext(); + context.setAttribute("debug", + (Consumer) log::debug, GLOBAL_SCOPE); + context.setAttribute("error", + (Consumer) log::error, GLOBAL_SCOPE); + context.setAttribute("warn", + (Consumer) log::warn, GLOBAL_SCOPE); + context.setAttribute("info", + (Consumer) log::info, GLOBAL_SCOPE); + + /* + * Also provide a specialized method useful for a script that may + * handle diagnostics from Java tools. + */ + context.setAttribute("diag", + (BiConsumer)((kind,content) -> + { + switch ( kind ) + { + case ERROR: + log.error(content); + break; + case MANDATORY_WARNING: + case WARNING: + log.warn(content); + break; + case NOTE: + log.info(content); + break; + case OTHER: + log.debug(content); + break; + } + } + ), GLOBAL_SCOPE); + return engine; } @@ -187,18 +238,4 @@ public static String getPgConfigProperty (String pgConfigCommand, return pgConfigOutput.substring(0, pgConfigOutput.length() - System.lineSeparator().length()); } - - /** - * - * @param project maven project for the property key-value pair is to be set - * @param property key for the property to set - * @param propertyValue value of the property - */ - public static void setPgConfigProperty (MavenProject project, - String property, - String propertyValue) - { - project.getProperties().setProperty(property, propertyValue); - } - } diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java index 80db3478..10955286 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -60,7 +60,7 @@ public void execute () public void setProjectProperty (String property, String value) { - PGXSUtils.setPgConfigProperty(project, property, value); + project.getProperties().setProperty(property, value); } public String getPgConfigProperty (String property) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 6b48b8e4..9823ded9 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -314,27 +314,21 @@ diff --git a/pljava/pom.xml b/pljava/pom.xml index 00372ed9..8e7a6fa3 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -88,7 +88,7 @@ function canGenerateReport(report) var v = java.lang.Runtime.version(); if ( 0 <= v.compareTo(java.lang.Runtime.Version.parse("12")) ) return true; - report.log.warn("Skipping JavaDocs; Java >= 12 required"); + warn("Skipping JavaDocs; Java >= 12 required"); return false; } @@ -181,9 +181,9 @@ function executeReport(report, locale) "-windowtitle", title ); - report.log.debug(args.toString()); + debug(args.toString()); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); - report.log.debug(tool.toString()); + debug(tool.toString()); var task = tool.getTask(null, null, null, null, args, null); task.call(); } From deb79fac3265c7a050e72344037d42fed1888db0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 7 Aug 2020 21:33:10 -0400 Subject: [PATCH 0602/1087] Integrate javadoc diagnostics into Maven log Use a DiagnosticListener so the mutterings from javadoc come out in Maven style. Can use the opportunity to relativize the source file pathnames against the source root directory, just to keep the messages shorter. --- pljava-api/pom.xml | 24 ++++++++++++++++++++---- pljava-examples/pom.xml | 23 ++++++++++++++++++++--- pljava/pom.xml | 24 ++++++++++++++++++++---- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 44e34798..dc1e5f57 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -126,6 +126,8 @@ function executeReport(report, locale) * The toString() here is a sop to a graaljs bug; nashorn doesn't need it. */ var srcroot = java.nio.file.Paths.get("src", "main", "java").toString(); + srcroot = basedir.resolve(srcroot); + var srcrooturi = srcroot.toUri(); var jdklink = "https://docs.oracle.com/" + locale.language + "/java/javase/12/docs/api"; @@ -144,8 +146,7 @@ function executeReport(report, locale) */ "-encoding", report.inputEncoding, "--module", "org.postgresql.pljava", - "--module-source-path", "org.postgresql.pljava=" - + basedir.resolve(srcroot), + "--module-source-path", "org.postgresql.pljava=" + srcroot, /* * Core javadoc options. * Avoid the legacy package/private/protected/public options; they can @@ -171,8 +172,23 @@ function executeReport(report, locale) debug(args.toString()); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); - debug(tool.toString()); - var task = tool.getTask(null, null, null, null, args, null); + + function diagListener(d) + { + var s = d.source; + + if ( null === s ) + s = ""; + else + { + s = srcrooturi.relativize(s.toUri()).toString(); + s += "[" + d.lineNumber + "," + d.columnNumber + "] "; + } + + diag(d.kind, s + d.getMessage(locale)); + } + + var task = tool.getTask(null, null, diagListener, null, args, null); task.call(); } ]]> diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 435c4ef5..61173cde 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -149,6 +149,8 @@ function executeReport(report, locale) * The toString() here is a sop to a graaljs bug; nashorn doesn't need it. */ var srcroot = java.nio.file.Paths.get("src", "main", "java").toString(); + srcroot = basedir.resolve(srcroot); + var srcrooturi = srcroot.toUri(); var jdklink = "https://docs.oracle.com/" + locale.language + "/java/javase/12/docs/api"; @@ -172,7 +174,7 @@ function executeReport(report, locale) */ "-encoding", report.inputEncoding, "-classpath", classpath, - "-sourcepath", basedir.resolve(srcroot).toString(), + "-sourcepath", srcroot.toString(), /* * Core javadoc options. * Avoid the legacy package/private/protected/public options; they can @@ -229,8 +231,23 @@ function executeReport(report, locale) debug(args.toString()); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); - debug(tool.toString()); - var task = tool.getTask(null, null, null, null, args, null); + + function diagListener(d) + { + var s = d.source; + + if ( null === s ) + s = ""; + else + { + s = srcrooturi.relativize(s.toUri()).toString(); + s += "[" + d.lineNumber + "," + d.columnNumber + "] "; + } + + diag(d.kind, s + d.getMessage(locale)); + } + + var task = tool.getTask(null, null, diagListener, null, args, null); task.call(); } ]]> diff --git a/pljava/pom.xml b/pljava/pom.xml index 8e7a6fa3..3a94ab49 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -129,6 +129,8 @@ function executeReport(report, locale) * The toString() here is a sop to a graaljs bug; nashorn doesn't need it. */ var srcroot = java.nio.file.Paths.get("src", "main", "java").toString(); + srcroot = basedir.resolve(srcroot); + var srcrooturi = srcroot.toUri(); var jdklink = "https://docs.oracle.com/" + locale.language + "/java/javase/12/docs/api"; @@ -151,8 +153,7 @@ function executeReport(report, locale) "-encoding", report.inputEncoding, "--module", "org.postgresql.pljava.internal", "--module-path", pljava_api_jar_path, - "--module-source-path", "org.postgresql.pljava.internal=" - + basedir.resolve(srcroot), + "--module-source-path", "org.postgresql.pljava.internal=" + srcroot, /* * Core javadoc options. * Avoid the legacy package/private/protected/public options; they can @@ -183,8 +184,23 @@ function executeReport(report, locale) debug(args.toString()); var tool = javax.tools.ToolProvider.getSystemDocumentationTool(); - debug(tool.toString()); - var task = tool.getTask(null, null, null, null, args, null); + + function diagListener(d) + { + var s = d.source; + + if ( null === s ) + s = ""; + else + { + s = srcrooturi.relativize(s.toUri()).toString(); + s += "[" + d.lineNumber + "," + d.columnNumber + "] "; + } + + diag(d.kind, s + d.getMessage(locale)); + } + + var task = tool.getTask(null, null, diagListener, null, args, null); task.call(); } ]]> From a8bbc6d0a0e8a7665cf1bfe7a4c48340f46a3b4e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 8 Aug 2020 09:33:24 -0400 Subject: [PATCH 0603/1087] Test for active profile an easier way I somehow completely overlooked that method on MavenProject. --- pljava-examples/pom.xml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 61173cde..3623a33f 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -214,12 +214,8 @@ function executeReport(report, locale) "org.postgresql.pljava.example.annotation" ]; - var me = - (function(p) {return [p.groupId, p.artifactId, p.version].join(":");}) - (report.project); - - if ( report.project.injectedProfileIds.get(me) - .stream().anyMatch(function(p) { return 'saxon-examples' == p; }) ) + if ( report.project.activeProfiles.stream() + .anyMatch(function(p) { return 'saxon-examples' == p.id; }) ) packages.push("org.postgresql.pljava.example.saxon"); /* From d3b16434eee3ce58f1718f593eb79e79247dac4d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 16 Aug 2020 10:58:41 -0400 Subject: [PATCH 0604/1087] Handle the empty-argument case for Windows There will probably never be an empty argument in any expected invocation of initdb or postgres from here, but make sure the case is not left unhandled in forWindowsCRuntime anyway, in case the code gets reused elsewhere. Kartik confirmed empirically that "" works, and that the Java runtime doesn't catch the case itself. --- pljava-packaging/src/main/java/Node.java | 26 +++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 20eea79f..cac15cea 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1705,9 +1705,9 @@ private boolean waitPrePG10() throws Exception /** * Adjust the command arguments of a {@code ProcessBuilder} so that they - * will be recovered correctly by a target C/C++ program using the argument - * parsing algorithm of the usual C run-time code, when it is known that - * the command will not be handled first by {@code cmd}. + * will be recovered correctly on Windows by a target C/C++ program using + * the argument parsing algorithm of the usual C run-time code, when it is + * known that the command will not be handled first by {@code cmd}. *

    * This transformation must account for the way the C runtime will * ultimately parse the parameters apart, and also for the behavior of @@ -1811,6 +1811,9 @@ public static ProcessBuilder forWindowsCRuntime(ProcessBuilder pb) * rule 5 below will ensure that any leading " has a \ added before, * and therefore the questionable Java code will never see from us * an arg that both starts and ends with a ". + * + * There is one edge case where this behavior of the Java runtime + * will be relied on (see rule 7 below). */ while ( args.hasNext() ) @@ -1855,6 +1858,23 @@ public static ProcessBuilder forWindowsCRuntime(ProcessBuilder pb) transformed = transformed.replaceFirst( "(\\\\)(\\\\*+)$", "$1$2$2"); + /* + * 7. If the argument is the empty string, it must be represented + * as "" or it will simply disappear. The Java runtime will not + * do that for us (after all, the empty string does not contain + * space, tab, <, or >), so it has to be done here, replacing the + * arg with exactly "". + * + * This is the one case where we produce a value that both starts + * and ends with a " character, thereby triggering the Java + * runtime behavior described in (4) above, so the Java runtime + * will avoid trying to further "protect" the string we have + * produced here. For this one case, that 'worrisome' behavior is + * just what we want. + */ + if ( transformed.isEmpty() ) + transformed = "\"\""; + if ( ! transformed.equals(arg) ) args.set(transformed); } From 98cc847af06d361230cd351e209783f564e99a7e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 16 Aug 2020 22:10:47 -0400 Subject: [PATCH 0605/1087] Factor out void result set tests Testing code may wish to examine these results, not merely print them. --- pljava-packaging/src/main/java/Node.java | 73 ++++++++++++++++++------ 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index cac15cea..bd95301d 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1344,29 +1344,17 @@ else if ( o instanceof Throwable ) * (but will have been read through the last row). *

    * A result set with no columns of type other than {@code void} will be - * printed in an abbreviated form. + * printed in an abbreviated form, showing its number of rows and columns + * as reported by {@link #voidResultSetDims voidResultSetDims}. */ public static void qp(ResultSet rs) throws Exception { - ResultSetMetaData md = rs.getMetaData(); - int cols = md.getColumnCount(); - - boolean allVoid = true; - - for ( int c = 1; c <= cols; ++c ) - { - if ( Types.OTHER == md.getColumnType(c) - && "void".equals(md.getColumnTypeName(c)) ) - continue; - allVoid = false; - break; - } + int[] dims = voidResultSetDims(rs); - if ( allVoid ) + if ( null != dims ) { - rs.last(); System.out.println( - ""); + ""); return; } @@ -1519,6 +1507,57 @@ else if ( '"' == c ) return delim + s + delim; } + /** + * Determines whether an object is a {@code ResultSet} with no columns of + * any type other than {@code void}, to allow abbreviated output of result + * sets produced by the common case of queries that call {@code void} + * functions. + *

    + * Returns null if o is not a {@code ResultSet}, or if its columns + * are not all of {@code void} type. Otherwise, returns a two-element + * integer array giving the rows (index 0 in the array) and columns (index + * 1) of the result set. + *

    + * If this method returns non-null, the result set is left positioned on its + * last row. + * @param o Object to check + * @return null or a two-element int[], as described above + */ + public static int[] voidResultSetDims(Object o) throws Exception + { + if ( ! (o instanceof ResultSet) ) + return null; + + ResultSet rs = (ResultSet)o; + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + + for ( int c = 1; c <= cols; ++c ) + if ( Types.OTHER != md.getColumnType(c) + || ! "void".equals(md.getColumnTypeName(c)) ) + return null; + + rs.last(); // last(), getRow() appears to work, in pgjdbc-ng + return new int[] { rs.getRow(), cols }; + } + + /** + * Predicate testing that an object is a {@code ResultSet} that has only + * columns of {@code void} type, and the expected number of rows + * and columns. + *

    + * The expected result of a query that calls one {@code void}-typed, + * non-set-returning function could be checked with + * {@code isVoid(rs, 1, 1)}. + */ + public static boolean isVoidResultSet(Object o, int rows, int columns) + throws Exception + { + int[] dims = voidResultSetDims(o); + + return null != dims && rows == dims[0] && columns == dims[1]; + } + /* * For parsing the postmaster.pid file, these have been the lines at least * back to 9.1, except PM_STATUS appeared in 10. That's too bad; before 10 From 01314b27cfe1b5f7f43a2119f70be88b88774de6 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Tue, 11 Aug 2020 02:24:42 +0530 Subject: [PATCH 0606/1087] Add utility methods buildPaths and isProfileActive buildPaths can be used to obtain the modulepath and classpath from a list of string paths. isProfileActive can be used to detect whether profile with given name is active for the current session. --- pljava-examples/pom.xml | 9 +- pljava-pgxs/pom.xml | 7 ++ .../org/postgresql/pljava/pgxs/PGXSUtils.java | 115 +++++++++++++++++- .../pljava/pgxs/ReportScriptingMojo.java | 5 +- .../postgresql/pljava/pgxs/ScriptingMojo.java | 5 +- 5 files changed, 128 insertions(+), 13 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 3623a33f..5984e018 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -137,9 +137,9 @@ function isExternalReport(report) function executeReport(report, locale) { - var classpath = java.lang.String.join( - java.lang.System.getProperty("path.separator"), - report.project.compileClasspathElements); + var paths = buildPaths(report.project.compileClasspathElements); + var classpath = paths['classpath']; + warn(classpath); var title = report.project.name + " " + report.project.version; @@ -214,8 +214,7 @@ function executeReport(report, locale) "org.postgresql.pljava.example.annotation" ]; - if ( report.project.activeProfiles.stream() - .anyMatch(function(p) { return 'saxon-examples' == p.id; }) ) + if ( isProfileActive('saxon-examples') ) packages.push("org.postgresql.pljava.example.saxon"); /* diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 9bfe14bb..caf8a94d 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -85,6 +85,13 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + + diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index ca16ae2a..fbb38e0c 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -7,6 +7,7 @@ * http://opensource.org/licenses/BSD-3-Clause * * Contributors: + * Chapman Flack * Kartik Ohri */ package org.postgresql.pljava.pgxs; @@ -16,21 +17,29 @@ import org.codehaus.plexus.configuration.PlexusConfiguration; import javax.script.ScriptContext; -import static javax.script.ScriptContext.GLOBAL_SCOPE; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; - import javax.tools.Diagnostic; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static javax.script.ScriptContext.GLOBAL_SCOPE; + public final class PGXSUtils { static final Pattern mustBeQuotedForC = Pattern.compile( @@ -48,10 +57,13 @@ private PGXSUtils () * @param script the script block element in the configuration block of * the plugin in the project * @param log the logger associated with the plugin + * @param project the maven project requesting the ScriptEngine + * * @return ScriptEngine based on the engine and mime type set by the user * in the script block */ - static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log) + static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log, + MavenProject project) { /* * Set the polyglot.js.nashorn-compat system property to true if it is @@ -121,6 +133,13 @@ static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log) context.setAttribute("info", (Consumer) log::info, GLOBAL_SCOPE); + context.setAttribute("isProfileActive", + (Function) id -> isProfileActive(log, project, id), + GLOBAL_SCOPE); + context.setAttribute("buildPaths", + (Function, Map>) elements -> buildPaths(log, elements), + GLOBAL_SCOPE); + /* * Also provide a specialized method useful for a script that may * handle diagnostics from Java tools. @@ -238,4 +257,92 @@ public static String getPgConfigProperty (String pgConfigCommand, return pgConfigOutput.substring(0, pgConfigOutput.length() - System.lineSeparator().length()); } + + /** + * @param logger plugin logger instance to log warnings + * @param project maven project in which to check profiles + * @param profileName name of profile to check + * @return true if profile exists and is active, false otherwise + */ + public static boolean isProfileActive(Log logger, + MavenProject project, + String profileName) + { + boolean isValidProfile = + project.getModel().getProfiles().stream() + .anyMatch(profile -> profile.getId().equals(profileName)); + + if (!isValidProfile) + { + logger.warn(profileName + " does not exist in " + project.getName()); + return false; + } + + return project.getActiveProfiles().stream() + .anyMatch(profile -> profile.getId().equals(profileName)); + } + + /** + * @param elements list of elements to build classpath and modulepath from + * @return a map with two elements, classpath and modulepath which can be + * accessed using the respective keys + */ + public static Map buildPaths(Log logger, + List elements) + { + List modulepathElements = new ArrayList<>(); + List classpathElements = new ArrayList<>(); + String pathSeparator = System.getProperty("path.separator"); + try + { + for (String element : elements) + { + if (element.contains(pathSeparator)) + logger.warn(String.format("cannot add %s to path because " + + "it contains path separator %s", element, pathSeparator)); + else if (shouldPlaceOnModulepath(element)) + modulepathElements.add(element); + else + classpathElements.add(element); + } + } + catch (Exception e) + { + logger.error(e); + } + String modulepath = String.join(pathSeparator, modulepathElements); + String classpath = String.join(pathSeparator, classpathElements); + return Map.of("classpath", classpath, "modulepath", modulepath); + } + + /** + * @param filePath the filepath to check whether is a module + * @return true if input path should go on modulepath, false otherwise + */ + public static boolean shouldPlaceOnModulepath(String filePath) + throws IOException + { + Path path = Paths.get(filePath); + if (Files.isDirectory(path)) + { + Path moduleInfoFile = path.resolve("module-info.class"); + return Files.exists(moduleInfoFile); + } + + if (path.endsWith(".jar")) + { + try(JarFile jarFile = new JarFile(path.toFile())) + { + if (jarFile.getEntry("module-info.class") != null) + return true; + Manifest manifest = jarFile.getManifest(); + jarFile.close(); + if (manifest == null) + return false; + return manifest.getMainAttributes() + .containsKey("Automatic-Module-Name"); + } + } + return false; + } } diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java index 5fb73183..d4265bb8 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java @@ -40,7 +40,8 @@ private void setReportScript() try { - ScriptEngine engine = PGXSUtils.getScriptEngine(script, getLog()); + ScriptEngine engine = + PGXSUtils.getScriptEngine(script, getLog(), project); String scriptText = script.getValue(); getLog().debug(scriptText); engine.eval(scriptText); @@ -48,7 +49,7 @@ private void setReportScript() } catch (Exception e) { - e.printStackTrace(); + getLog().error(e); } } diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java index 10955286..390d5e46 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -39,7 +39,8 @@ public void execute () try { String scriptText = script.getValue(); - ScriptEngine engine = PGXSUtils.getScriptEngine(script, getLog()); + ScriptEngine engine = + PGXSUtils.getScriptEngine(script, getLog(), project); getLog().debug(scriptText); engine.getContext().setAttribute("plugin", this, @@ -73,7 +74,7 @@ public String getPgConfigProperty (String property) } catch (Exception e) { - e.printStackTrace(); + getLog().error(e); return null; } } From 8ff8cd771ffe6e06633e8e383affd7358425ed68 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sat, 15 Aug 2020 09:44:09 +0530 Subject: [PATCH 0607/1087] Add missing javadocs --- pljava-pgxs/pom.xml | 2 +- .../org/postgresql/pljava/pgxs/PGXSUtils.java | 55 +++++++-- .../postgresql/pljava/pgxs/ReportScript.java | 48 +++++++- .../pljava/pgxs/ReportScriptingMojo.java | 111 ++++++++++++++++++ .../postgresql/pljava/pgxs/ScriptingMojo.java | 23 ++++ 5 files changed, 228 insertions(+), 11 deletions(-) diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index caf8a94d..39200788 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -13,7 +13,7 @@ maven-plugin PL/Java PGXS - The plugin to build native code used inside PL/Java + The maven plugin to build native code used inside PL/Java diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index fbb38e0c..66ab488c 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -40,6 +40,10 @@ import static javax.script.ScriptContext.GLOBAL_SCOPE; +/** + * Utility methods to simplify and hide the bland implementation details + * for writing JavaScript snippets. + */ public final class PGXSUtils { static final Pattern mustBeQuotedForC = Pattern.compile( @@ -53,14 +57,15 @@ private PGXSUtils () } /** + * Returns a ScriptEngine with some basic utilities for scripting. * * @param script the script block element in the configuration block of * the plugin in the project * @param log the logger associated with the plugin * @param project the maven project requesting the ScriptEngine * - * @return ScriptEngine based on the engine and mime type set by the user - * in the script block + * @return ScriptEngine based on the engine and mime type provided in the + * script block */ static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log, MavenProject project) @@ -170,9 +175,11 @@ static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log, } /** + * Returns the input wrapped in double quotes and with internal characters + * escaped where appropriate using the C conventions. + * * @param s string to be escaped - * @return s wrapped in double quotes and with internal characters - * escaped where appropriate using the C conventions + * @return a C compatible String enclosed in double quotes */ public static String quoteStringForC (String s) { @@ -223,8 +230,10 @@ else if (-1 != m.start(2)) // things with specific escapes } /** + * Returns the string decoded from input bytes using default platform charset. + * * @param bytes byte array to be decoded - * @return string decoded from input bytes using default platform charset + * @return string decoded from input bytes * @throws CharacterCodingException if unable to decode bytes using * default platform charset */ @@ -235,6 +244,22 @@ public static String defaultCharsetDecodeStrict (byte[] bytes) .decode(ByteBuffer.wrap(bytes)).toString(); } + /** + * Returns the output, decoded using default platform charset, of the input + * command executed with the input argument. + *

    + * If the input parameter {@code pgConfigCommand} is empty or null, + * {@code pg_config} is used as the default value. If multiple version of + * {@code pg_config} are available or {@code pg_config} is not present on + * the path, consider passing an absolute path to {@code pg_config}. It is + * also recommended that only a single property be passed at a time. + * + * @param pgConfigCommand pg_config command to execute + * @param pgConfigArgument argument to be passed to the command + * @return output of the input command executed with the input argument + * @throws IOException if unable to read output of the command + * @throws InterruptedException if command does not complete successfully + */ public static String getPgConfigProperty (String pgConfigCommand, String pgConfigArgument) throws IOException, InterruptedException @@ -259,6 +284,12 @@ public static String getPgConfigProperty (String pgConfigCommand, } /** + * Returns true if the profile with given name exists and is active, false + * otherwise. + *

    + * A warning is logged if the no profile with the input name exists in the + * current project. + * * @param logger plugin logger instance to log warnings * @param project maven project in which to check profiles * @param profileName name of profile to check @@ -283,9 +314,12 @@ public static boolean isProfileActive(Log logger, } /** + * Returns a map with two elements with {@code classpath} and {@code modulepath} + * as keys and their joined string paths as the respective values. + * * @param elements list of elements to build classpath and modulepath from - * @return a map with two elements, classpath and modulepath which can be - * accessed using the respective keys + * @return a map containing the {@code classpath} and {@code modulepath} + * as separate elements */ public static Map buildPaths(Log logger, List elements) @@ -316,6 +350,13 @@ else if (shouldPlaceOnModulepath(element)) } /** + * Returns true if the element should be placed on the module path. + *

    + * An file path element should be placed on the module path if it points to + * 1) a directory with a top level {@code module-info.class} file + * 2) a {@code JAR} file having a {@code module-info.class} entry or the + * {@code Automatic-Module-Name} as a manifest attribute + * * @param filePath the filepath to check whether is a module * @return true if input path should go on modulepath, false otherwise */ diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java index c383ef30..1bb59ed0 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java @@ -14,25 +14,67 @@ import java.util.Locale; +/** + * Provides reasonable defaults and other required methods for + * using JavaScript to during {@code Site} lifecycle phase to configure a + * {@code MavenReport}. + */ public interface ReportScript { - default boolean isExternalReport(ReportScriptingMojo report) { + /** + * @param report instance of {@link ReportScriptingMojo} + * @return whether the report is an external report + * @see ReportScriptingMojo#isExternalReport() + */ + default boolean isExternalReport(ReportScriptingMojo report) + { return report.isExternalReportDefault(); } - default String getCategoryName(ReportScriptingMojo report) { + /** + * @param report instance of {@link ReportScriptingMojo} + * @return category name of the report + * @see ReportScriptingMojo#getCategoryName() + */ + default String getCategoryName(ReportScriptingMojo report) + { return report.getCategoryNameDefault(); } - default boolean canGenerateReport(ReportScriptingMojo report) { + /** + * @param report instance of {@link ReportScriptingMojo} + * @return whether the report can be generated + * @see ReportScriptingMojo#canGenerateReport() + */ + default boolean canGenerateReport(ReportScriptingMojo report) + { return report.canGenerateReportDefault(); } + /** + * @param report instance of {@link ReportScriptingMojo} + * @return path of the report relative to target site directory + * @see ReportScriptingMojo#getCategoryName() + */ String getOutputName (ReportScriptingMojo report); + /** + * @param report instance of {@link ReportScriptingMojo} + * @return name of the report + * @see ReportScriptingMojo#getName(Locale) + */ String getName (ReportScriptingMojo report, Locale locale); + /** + * @param report instance of {@link ReportScriptingMojo} + * @return description of the report + * @see ReportScriptingMojo#getDescription(Locale) + */ String getDescription (ReportScriptingMojo report, Locale locale); + /** + * @param report instance of {@link ReportScriptingMojo} + * @see ReportScriptingMojo#executeReport(Locale) + */ void executeReport(ReportScriptingMojo report, Locale locale); } diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java index d4265bb8..e42a6191 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java @@ -12,6 +12,7 @@ */ package org.postgresql.pljava.pgxs; +import org.apache.maven.doxia.sink.Sink; import org.apache.maven.plugins.annotations.Execute; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -24,6 +25,15 @@ import javax.script.ScriptEngine; import java.util.Locale; +/** + * Maven plugin goal to use JavaScript for configuring + * {@link org.apache.maven.reporting.MavenReport} during the + * {@link LifecyclePhase#SITE}. + *

    + * This plugin goal intends to allow the use of JavaScript during {@code SITE} + * lifecycle phase with the help of {@link ReportScript}. The motivation behind + * this is the inability to use Maven AntRun during {@code SITE} phase. + */ @Mojo(name = "scripting-report") @Execute(phase = LifecyclePhase.NONE) public class ReportScriptingMojo extends AbstractMavenReport @@ -33,6 +43,11 @@ public class ReportScriptingMojo extends AbstractMavenReport private ReportScript reportScript; + /** + * Creates an instance of {@link ReportScript} using methods defined in + * the JavaScript snippet in configuration of the report in {@code pom.xml}. + * Does nothing if the instance is already initialized. + */ private void setReportScript() { if ( null != reportScript ) @@ -53,6 +68,17 @@ private void setReportScript() } } + /** + * Returns the path relative to the target site directory of the this report. + * This value will be used by {@code Maven} to provide a link to the report + * from {@code index.html}. + *

    + * Calls {@code setReportScript} to ensure that the instance of + * {@link ReportScript} is available. Invokes + * {@code fun getOutputName(report)} defined in the JavaScript snippet + * associated with the report. No default implementation is provided. User + * must implement the method in JavaScript. + */ @Override public String getOutputName () { @@ -60,6 +86,17 @@ public String getOutputName () return reportScript.getOutputName(this); } + /** + * Returns false if this report will produce output through a + * supplied {@link Sink}, true if it is 'external', producing its output + * some other way. + *

    + * Calls {@code setReportScript} to ensure that the instance of + * {@link ReportScript} is available. Invokes + * {@code fun isExternalReport(report)} if defined in the javascript + * snippet associated with the report. Otherwise, the {@code super} + * implementation is invoked effectively. + */ @Override public boolean isExternalReport () { @@ -67,6 +104,16 @@ public boolean isExternalReport () return reportScript.isExternalReport(this); } + /** + * Returns the name of this report used by {@code Maven} for displaying in + * {@code index.html}. + *

    + * Calls {@code setReportScript} to ensure that the instance of + * {@link ReportScript} is available. Invokes + * {@code fun getName(report, locale)} defined in the javascript + * snippet associated with the report. No default implementation is provided + * . User must implement the method in javascript. + */ @Override public String getName (Locale locale) { @@ -74,6 +121,16 @@ public String getName (Locale locale) return reportScript.getName(this, locale); } + /** + * Returns the description of this report, used by {@code Maven} to display + * report description in {@code index.html}. + *

    + * Calls {@code setReportScript} to ensure that the instance of + * {@link ReportScript} is available. Invokes + * {@code fun getDescription(report, locale)} defined in the javascript + * snippet associated with the report. No default implementation is provided + * . User must implement the method in javascript. + */ @Override public String getDescription (Locale locale) { @@ -81,6 +138,16 @@ public String getDescription (Locale locale) return reportScript.getDescription(this, locale); } + /** + * Returns the category name of this report, used by {@code Maven} to display + * the report under the correct in {@code index.html}. + *

    + * Calls {@code setReportScript} to ensure that the instance of + * {@link ReportScript} is available. Invokes + * {@code fun getCategoryName(report)} if defined in the javascript + * snippet associated with the report. Otherwise, the {@code super} + * implementation is invoked effectively. + */ @Override public String getCategoryName () { @@ -88,6 +155,15 @@ public String getCategoryName () return reportScript.getCategoryName(this); } + /** + * Returns true if a report can be generated, false otherwise. + *

    + * Calls {@code setReportScript} to ensure that the instance of + * {@link ReportScript} is available. Invokes + * {@code fun canGenerateReport(report)} if defined in the javascript + * snippet. Otherwise, the {@code super} implementation is invoked + * effectively. + */ @Override public boolean canGenerateReport () { @@ -95,6 +171,14 @@ public boolean canGenerateReport () return reportScript.canGenerateReport(this); } + /** + * {@inheritDoc} + *

    + * Calls {@code setReportScript} to ensure that the instance of + * {@link ReportScript} is available. Invokes the + * {@code fun executeReport(report, locale)} with the instance of the + * current report. + */ @Override protected void executeReport (Locale locale) { @@ -102,34 +186,61 @@ protected void executeReport (Locale locale) reportScript.executeReport(this, locale); } + /** + * {@inheritDoc} + */ @Override public MavenProject getProject () { return super.getProject(); } + /** + * {@inheritDoc} + */ @Override public String getInputEncoding () { return super.getInputEncoding(); } + /** + * {@inheritDoc} + */ @Override public String getOutputEncoding () { return super.getOutputEncoding(); } + /** + * Default implementation for + * {@link ReportScript#isExternalReport(ReportScriptingMojo)}. Invoked if + * {@code fun isExternalReport(report)} is not defined in the javascript + * snippet associated with the report. + */ boolean isExternalReportDefault () { return super.isExternalReport(); } + /** + * Default implementation of + * {@link ReportScript#getCategoryName(ReportScriptingMojo)}. Invoked if + * {@code fun getCategoryName(report)} is not defined in the javascript + * snippet associated with the report. + */ String getCategoryNameDefault () { return super.getCategoryName(); } + /** + * Default implementation of + * {@link ReportScript#canGenerateReport(ReportScriptingMojo)}. Invoked if + * {@code fun canGenerateReport(report)} is not defined in the javascript + * snippet associated with the report. + */ boolean canGenerateReportDefault () { return super.canGenerateReport(); diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java index 390d5e46..53328d7d 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java @@ -23,6 +23,13 @@ import java.util.function.BiConsumer; import java.util.function.Function; +/** + * Maven plugin goal to use JavaScript during any of build lifecycle phases. + *

    + * The Mojo provides a limited subset of the functionality provided Maven AntRun + * Plugin. This is intentional to simplify usage as this maven plugin is + * specifically targeted at building Pl/Java native code. + */ @Mojo(name = "scripting", defaultPhase = LifecyclePhase.INITIALIZE, requiresDependencyResolution = ResolutionScope.TEST) public class ScriptingMojo extends AbstractMojo @@ -33,6 +40,10 @@ public class ScriptingMojo extends AbstractMojo @Parameter private PlexusConfiguration script; + /** + * Executes the javascript code inside {@code script} tag inside plugin + * configuration. + */ @Override public void execute () { @@ -59,11 +70,23 @@ public void execute () } } + /** + * Sets the value of a property for the current project. + * + * @param property key to use for property + * @param value the value of property to set + */ public void setProjectProperty (String property, String value) { project.getProperties().setProperty(property, value); } + /** + * Returns the value of a pg_config property. + * + * @param property property whose value is to be retrieved from pg_config + * @return output of pg_config executed with the input property as argument + */ public String getPgConfigProperty (String property) { try From cb21e6da0cf6aa2e3ceba8fea5a5a9b9c94027ec Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Mon, 17 Aug 2020 14:09:33 +0530 Subject: [PATCH 0608/1087] Add more documentation to PL/Java PGXS --- pljava-packaging/pom.xml | 2 +- pljava-pgxs/pom.xml | 1 - .../postgresql/pljava/pgxs/package-info.java | 18 ++++++++++++++++++ pljava-pgxs/src/site/markdown/index.md | 6 ++++++ pljava-so/pom.xml | 2 +- 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/package-info.java create mode 100644 pljava-pgxs/src/site/markdown/index.md diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 9eddd1da..f678954b 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -105,7 +105,7 @@ org.postgresql pljava-pgxs - 0.0.1-SNAPSHOT + 1.6.0-SNAPSHOT execute-script diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 39200788..66ec9809 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -9,7 +9,6 @@ pljava-pgxs - 0.0.1-SNAPSHOT maven-plugin PL/Java PGXS diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/package-info.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/package-info.java new file mode 100644 index 00000000..68ca19d3 --- /dev/null +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2020 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 + */ +/** + * The PL/Java PGXS package provides the necessary maven plugin goals to build + * the PL/Java Native C code. It also provides a maven plugin goal for utilising + * JavaScript during maven SITE lifecycle phase. + */ +package org.postgresql.pljava.pgxs; \ No newline at end of file diff --git a/pljava-pgxs/src/site/markdown/index.md b/pljava-pgxs/src/site/markdown/index.md new file mode 100644 index 00000000..c68d90c7 --- /dev/null +++ b/pljava-pgxs/src/site/markdown/index.md @@ -0,0 +1,6 @@ +## About PL/Java PGXS + +The `pljava-pgxs` subproject is maven plugin that builds the +native C code in `pljava-so` and allows using JavaScript for +configuring Maven Reports; this machine-generated page is a +project summary for developers of PL/Java. \ No newline at end of file diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 9823ded9..a39a4bd0 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -304,7 +304,7 @@ org.postgresql pljava-pgxs - 0.0.1-SNAPSHOT + 1.6.0-SNAPSHOT execute-script From 00a82335571a7a277eee1609e3a37ed6b0a8de2e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 17 Aug 2020 08:25:06 -0400 Subject: [PATCH 0609/1087] Hardcode fewer references to pljava-pgxs version --- pljava-api/pom.xml | 2 +- pljava-examples/pom.xml | 2 +- pljava-packaging/pom.xml | 2 +- pljava-so/pom.xml | 2 +- pljava/pom.xml | 2 +- pom.xml | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index dc1e5f57..dae8379a 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -70,7 +70,7 @@ org.postgresql pljava-pgxs - 0.0.1-SNAPSHOT + ${pljava.pgxs.version} diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 5984e018..c3240a8c 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -104,7 +104,7 @@ org.postgresql pljava-pgxs - 0.0.1-SNAPSHOT + ${pljava.pgxs.version} diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index f678954b..265760ee 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -105,7 +105,7 @@ org.postgresql pljava-pgxs - 1.6.0-SNAPSHOT + ${pljava.pgxs.version} execute-script diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index a39a4bd0..7fc88a26 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -304,7 +304,7 @@ org.postgresql pljava-pgxs - 1.6.0-SNAPSHOT + ${pljava.pgxs.version} execute-script diff --git a/pljava/pom.xml b/pljava/pom.xml index 3a94ab49..1785ec53 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -68,7 +68,7 @@ org.postgresql pljava-pgxs - 0.0.1-SNAPSHOT + ${pljava.pgxs.version} diff --git a/pom.xml b/pom.xml index 420b2a43..420db23c 100644 --- a/pom.xml +++ b/pom.xml @@ -10,6 +10,7 @@ Java stored procedures for PostgreSQL UTF-8 + ${project.version} From 1d9a3759ab250911c3f21ff4772cb5899381e2ce Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 17 Aug 2020 08:26:33 -0400 Subject: [PATCH 0610/1087] Explicit close no longer needed in try-resources --- .../src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index 66ab488c..f9210988 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -377,7 +377,6 @@ public static boolean shouldPlaceOnModulepath(String filePath) if (jarFile.getEntry("module-info.class") != null) return true; Manifest manifest = jarFile.getManifest(); - jarFile.close(); if (manifest == null) return false; return manifest.getMainAttributes() From 40bb1a55eb788e3f22dd824c579174c4d21d491a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 13 Aug 2020 18:16:45 -0400 Subject: [PATCH 0611/1087] Make classpath-setting Graal-friendly Nashorn would let you treat a Java map like a JS map, and index it with map[key]. Graal does not; must use get(key). --- pljava-examples/pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index c3240a8c..40b416e3 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -138,8 +138,7 @@ function isExternalReport(report) function executeReport(report, locale) { var paths = buildPaths(report.project.compileClasspathElements); - var classpath = paths['classpath']; - warn(classpath); + var classpath = paths.get('classpath'); var title = report.project.name + " " + report.project.version; From 2d3181b30835f574e73326293b861b61c30cdd8c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 13 Aug 2020 19:12:10 -0400 Subject: [PATCH 0612/1087] Get classpath/modulepath segregation working The pljava-examples javadoc was working, which sounds good, but was actually weird, because it was only passing -classpath to javadoc, and buildPaths() should have been detecting that pljava-api is modular, and putting it on the module path instead, where javadoc would not have been able to find it. That was a small matter in buildPaths(): the endsWith() method on a Path does not mean what endsWith() on a String means. On a Path, it tests for an ending sequence of intact path elements. If you want to match just the ending /portion/ of the last path element against some /characters/, it has to be a String first. With that working, of course the -examples javadoc fails, which could be fixed two ways: either smash paths['classpath'] and paths['modulepath'] back into a single string and call it -classpath (which is back to what was happening before, and works because the examples code isn't modular, so even the dependencies that are modules don't have to be treated as such), or, pass the class path and the module path separately, and name org.postgresql.pljava in an --add-modules option (which is needed because the examples code, being ignorant of modules, has no way of saying "hey, I use that module"). Done here the second way, as it seems more in the spirit of the module system. --- pljava-examples/pom.xml | 5 +++-- .../main/java/org/postgresql/pljava/pgxs/PGXSUtils.java | 2 +- pljava/pom.xml | 7 ++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 40b416e3..b9fb9126 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -138,7 +138,6 @@ function isExternalReport(report) function executeReport(report, locale) { var paths = buildPaths(report.project.compileClasspathElements); - var classpath = paths.get('classpath'); var title = report.project.name + " " + report.project.version; @@ -172,8 +171,10 @@ function executeReport(report, locale) * 12 through 15, anyway). */ "-encoding", report.inputEncoding, - "-classpath", classpath, + "--class-path", paths.get("classpath"), + "--module-path",paths.get("modulepath"), "-sourcepath", srcroot.toString(), + "--add-modules","org.postgresql.pljava", /* * Core javadoc options. * Avoid the legacy package/private/protected/public options; they can diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index f9210988..68661898 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -370,7 +370,7 @@ public static boolean shouldPlaceOnModulepath(String filePath) return Files.exists(moduleInfoFile); } - if (path.endsWith(".jar")) + if (path.getFileName().toString().endsWith(".jar")) { try(JarFile jarFile = new JarFile(path.toFile())) { diff --git a/pljava/pom.xml b/pljava/pom.xml index 1785ec53..0fee5e5d 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -116,10 +116,7 @@ function isExternalReport(report) function executeReport(report, locale) { - var pljava_api_jar_path; - for each (var artifact in report.project.artifacts) - if (artifact.artifactId == "pljava-api") - pljava_api_jar_path = artifact.file.path; + var paths = buildPaths(report.project.compileClasspathElements); var title = report.project.name + " " + report.project.version; @@ -152,7 +149,7 @@ function executeReport(report, locale) */ "-encoding", report.inputEncoding, "--module", "org.postgresql.pljava.internal", - "--module-path", pljava_api_jar_path, + "--module-path", paths.get("modulepath"), "--module-source-path", "org.postgresql.pljava.internal=" + srcroot, /* * Core javadoc options. From c789065c4e591bf0ada772e387d93ab77a2df916 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 13 Aug 2020 19:32:52 -0400 Subject: [PATCH 0613/1087] Avoid overriding Java release set in parent pom ... unless there appears some compelling reason, which should then be explained in a commit comment. --- pljava-pgxs/pom.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 66ec9809..6a516264 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -15,11 +15,6 @@ The maven plugin to build native code used inside PL/Java - - 1.8 - 1.8 - - org.apache.maven @@ -84,13 +79,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - 9 - - From 87831e4fdcc75d3de21f5bf1f86779eae35ad0c2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2020 22:13:37 -0400 Subject: [PATCH 0614/1087] Start on RelativizingFileManager --- .../pljava/pgxs/RelativizingFileManager.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java new file mode 100644 index 00000000..2d79ed45 --- /dev/null +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2020 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.pgxs; + +import java.io.File; +import java.io.IOException; + +import java.nio.file.Path; + +import java.util.Collection; + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; + +public class RelativizingFileManager +extends ForwardingJavaFileManager +implements StandardJavaFileManager +{ + public RelativizingFileManager(StandardJavaFileManager fileManager) + { + super(fileManager); + } + + @Override + public Iterable + getJavaFileObjectsFromFiles(Iterable files) + { + return fileManager.getJavaFileObjectsFromFiles(files); + } + + @Override + public Iterable + getJavaFileObjectsFromPaths(Iterable paths) + { + return fileManager.getJavaFileObjectsFromPaths(paths); + } + + @Override + public Iterable + getJavaFileObjects(File... files) + { + return fileManager.getJavaFileObjects(files); + } + + @Override + public Iterable + getJavaFileObjects(Path... paths) + { + return fileManager.getJavaFileObjects(paths); + } + + @Override + public Iterable + getJavaFileObjectsFromStrings(Iterable names) + { + return fileManager.getJavaFileObjectsFromStrings(names); + } + + @Override + public Iterable + getJavaFileObjects(String... names) + { + return fileManager.getJavaFileObjects(names); + } + + @Override + public void setLocation(Location location, Iterable files) + throws IOException + { + fileManager.setLocation(location, files); + } + + @Override + public void setLocationFromPaths( + Location location, + Collection paths) + throws IOException + { + fileManager.setLocationFromPaths(location, paths); + } + + @Override + public void setLocationForModule( + Location location, + String moduleName, + Collection paths) + throws IOException + { + fileManager.setLocationForModule(location, moduleName, paths); + } + + @Override + public Iterable getLocation(Location location) + { + return fileManager.getLocation(location); + } + + @Override + public Iterable getLocationAsPaths(Location location) + { + return fileManager.getLocationAsPaths(location); + } + + @Override + public Path asPath(FileObject file) + { + return fileManager.asPath(file); + } + + @Override + public void setPathFactory(PathFactory f) + { + fileManager.setPathFactory(f); + } +} From e99f844532279c669356205b94a33828ae165177 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 11 Aug 2020 20:20:34 -0400 Subject: [PATCH 0615/1087] Default methods solve problems and cause others Before default methods, API updates could prevent recompiling your code until you added implementations, possibly causing pain. With default methods, if you have a forwarding/proxy implementation and an API update adds a default method, everything compiles and now you have a forwarding implementation that doesn't forward every method, possibly causing pain. No silver bullets. --- .../postgresql/pljava/pgxs/RelativizingFileManager.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java index 2d79ed45..6c4bbf2d 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java @@ -41,6 +41,13 @@ public RelativizingFileManager(StandardJavaFileManager fileManager) return fileManager.getJavaFileObjectsFromFiles(files); } + // @Override only when support horizon advances to >= Java 13 + public Iterable + getJavaFileObjectsFromPaths(Collection paths) + { + return fileManager.getJavaFileObjectsFromPaths(paths); + } + @Override public Iterable getJavaFileObjectsFromPaths(Iterable paths) From 2c6053acac2c67a207af43c85efa67896ae0413e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2020 22:15:38 -0400 Subject: [PATCH 0616/1087] Do Repeat Yourself If you pass your own file manager to DocumentationTool's getTask, it won't find any of your files. This even if the file manager you supply is a forwarding wrapper around the exact file manager that getStandardFileManager() gave you. The reason turns out to be that the location options you pass to the DocumentationTool (-classpath, -sourcepath, etc.) do not get passed on to your file manager unless it's of an actual subclass of the internal, unextendable file manager class that you got from getStandardFileManager.[1] A forwarding wrapper isn't one of those, so those options don't get propagated to it, and therefore you must pass it the same location-defining options that you're already passing to the DocumentationTool itself. Who ever said DRY had to stand for *Don't* Repeat Yourself? This only seems to be needed for the location-defining options that are in the 'standard options' (inherited from javac) group. Locations specific to the doclet (like -d) do get passed to the file manager without extra attention here, by the doclet I guess. [1] http://hg.openjdk.java.net/jdk9/jdk9/langtools/file/65bfdabaab9c/src/jdk.javadoc/share/classes/jdk/javadoc/internal/tool/Start.java#l540 --- pljava-examples/pom.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index b9fb9126..104da1e2 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -242,7 +242,17 @@ function executeReport(report, locale) diag(d.kind, s + d.getMessage(locale)); } - var task = tool.getTask(null, null, diagListener, null, args, null); + var smgr = tool.getStandardFileManager( + diagListener, + locale, + java.nio.charset.Charset.forName(report.inputEncoding) + ); + + var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager(smgr); + rmgr.handleOption("-sourcepath", of(srcroot.toString()).iterator()); + rmgr.handleOption("-classpath", of(classpath).iterator()); + + var task = tool.getTask(null, rmgr, diagListener, null, args, null); task.call(); } ]]> From 0a6c14f48181e9c0065420e2f17110a6f3b9e4ae Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 13 Aug 2020 19:21:27 -0400 Subject: [PATCH 0617/1087] Add a handleFirstOptions method As the options recognized by the standard file manager are generally those among the "Standard Options" that javadoc inherits from javac (including the various location-setting options such as -classpath, as well as -encoding), with a little care to place those first in the argument list to be passed to the tool itself, the same list can be passed to a method on RelativizingFileManager that simply calls handleOption() repeatedly until it doesn't recognize an option, and thereby configure the file manager without repeating yourself much. --- pljava-examples/pom.xml | 6 ++-- .../pljava/pgxs/RelativizingFileManager.java | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 104da1e2..e55b21e3 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -174,13 +174,14 @@ function executeReport(report, locale) "--class-path", paths.get("classpath"), "--module-path",paths.get("modulepath"), "-sourcepath", srcroot.toString(), + // ^^^ Options recognized by the file manager end here ^^^ "--add-modules","org.postgresql.pljava", /* * Core javadoc options. * Avoid the legacy package/private/protected/public options; they can * clobber the effects of the newer -show-...=... options. */ - "-locale", locale.toString(), + "-locale", locale.toString(), "-quiet", /* * Options that are passed to the doclet. @@ -249,8 +250,7 @@ function executeReport(report, locale) ); var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager(smgr); - rmgr.handleOption("-sourcepath", of(srcroot.toString()).iterator()); - rmgr.handleOption("-classpath", of(classpath).iterator()); + rmgr.handleFirstOptions(args); var task = tool.getTask(null, rmgr, diagListener, null, args, null); task.call(); diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java index 6c4bbf2d..d39b96be 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java @@ -18,6 +18,7 @@ import java.nio.file.Path; import java.util.Collection; +import java.util.Iterator; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; @@ -34,6 +35,34 @@ public RelativizingFileManager(StandardJavaFileManager fileManager) super(fileManager); } + /** + * Call {@link #handleOption handleOption} on as many of the first supplied + * options as the file manager recognizes. + *

    + * Returns when {@link #handleOption handleOption} first returns false, + * indicating an option the file manager does not recognize. + *

    + * As the options recognized by the standard file manager are generally + * those among the "Standard Options" that javadoc inherits from javac + * (including the various location-setting options such as + * {@code -classpath}, as well as {@code -encoding}), with a little care to + * place those first in the argument list to be passed to the tool itself, + * the same list can be passed to this method to configure the file manager, + * without any more complicated option recognition needed here. + */ + public void handleFirstOptions(Iterable firstOptions) + { + Iterator it = firstOptions.iterator(); + + while ( it.hasNext() ) + if ( ! handleOption(it.next(), it) ) + break; + } + + /* + * The boilerplate StandardJavaFileManager forwards follow. + */ + @Override public Iterable getJavaFileObjectsFromFiles(Iterable files) From 5168b88e134d62482adfe0dd2df7c6d0117c5148 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2020 21:59:41 -0400 Subject: [PATCH 0618/1087] Introduce a RelativizingFileManager This is a workaround for javadoc's -linkoffline behavior, where following the documented instruction to supply a URL relative to the -d destination results in broken links, because javadoc forgets to add the right number of ../ components when writing files in subdirectories, as it would have to for that rule to work. It also fails to expand {@docRoot} in a -linkoffline URL, which would otherwise be a simpler workaround. This file manager simply interposes on output HTML files as they are written, and rewrites URLs containing RELDOTS to replace that token with the appropriate number of ../ components, exactly as {@docRoot} would be replaced if it were honored. A supplied -linkoffline URL must have its own ../ components, if any, placed before RELDOTS and not after, because javadoc normalizes the URL, such that if RELDOTS were followed by any ../ component, both would disappear, and this file manager would find nothing to rewrite. --- pljava-examples/pom.xml | 5 +- .../pljava/pgxs/RelativizingFileManager.java | 161 +++++++++++++++++- 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index e55b21e3..c66e423f 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -208,7 +208,7 @@ function executeReport(report, locale) var v = java.lang.Runtime.version(); if ( 0 <= v.compareTo(java.lang.Runtime.Version.parse("15-ea")) ) args.addAll(of("-linkoffline", - "../../pljava-api/apidocs", apioffline.toString())); + "../../RELDOTS/pljava-api/apidocs", apioffline.toString())); var packages = [ "org.postgresql.pljava.example", @@ -249,7 +249,8 @@ function executeReport(report, locale) java.nio.charset.Charset.forName(report.inputEncoding) ); - var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager(smgr); + var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager( + smgr, java.nio.charset.Charset.forName(report.outputEncoding)); rmgr.handleFirstOptions(args); var task = tool.getTask(null, rmgr, diagListener, null, args, null); diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java index d39b96be..55cbb54b 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java @@ -12,27 +12,180 @@ package org.postgresql.pljava.pgxs; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; import java.nio.file.Path; import java.util.Collection; import java.util.Iterator; +import java.util.regex.Pattern; + +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.stream.StreamSupport.stream; + +import javax.tools.DocumentationTool; // mentioned in javadoc import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; +import javax.tools.ForwardingJavaFileObject; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; +import static javax.tools.JavaFileObject.Kind.HTML; import javax.tools.StandardJavaFileManager; +/** + * A {@link ForwardingJavaFileManager} that interposes when asked for an output + * file of type {@code HTML}, and rewrites {@code href} URLs that contain + * {@code RELDOTS} as a component. + *

    Purpose

    + *

    + * This file manager is intended for use with the {@link DocumentationTool} + * when {@code -linkoffline} is used to generate links between subprojects + * (for example, {@code pljava-examples} to {@code pljava-api}). Maven's + * {@code site:stage} will copy the generated per-subproject documentation trees + * into a single {@code staging} directory, which can be relocated, deployed to + * web servers, etc. Therefore, it is both reasonable and desirable for the + * subproject API docs to refer to each other by relative links. However, the + * documentation for {@code -linkoffline} states that the relative links should + * be given as if from the output destination ({@code -d}) directory. That + * implies that the tool will add the right number of {@code ../} components, + * when generating links in a file some levels below the {@code -d} directory, + * so that the resulting relative URL will be correct. And it doesn't. The tool + * simply doesn't. + *

    + * As a workaround, the {@code -linkoffline} option can be told to produce URLs + * that contain {@code RELDOTS}, for example, + * {@code ../../RELDOTS/pljava-api/apidocs}, and this file manager can be used + * when running the tool. As the HTML files are written, any {@code href} URL + * that begins with zero or more {@code ../} followed by {@code RELDOTS} will + * have the {@code RELDOTS} replaced with the right number of {@code ../} to + * ascend from that file's containing directory to the output destination + * directory, resulting in relative URLs that are correct in files at any depth + * in the API docs tree. + *

    + * An alert reader will notice that {@code RELDOTS} is expanded to exactly what + * {@code {@docRoot}} is supposed to expand to. But experiment showed that + * {@code {@docRoot}} does not get expanded in a {@code -linkoffline} URL. + *

    Limitations

    + * The postprocessing is done blindly to any rules of HTML syntax. It will + * simply replace {@code RELDOTS} in any substring of the content resembling + * href="../RELDOTS/ (with any number, zero or more, of + * {@code ../} before the {@code RELDOTS}). The example in the preceding + * sentence was written carefully to avoid being rewritten in this comment. + *

    + * Only the form with a double quote is recognized, as the javadoc tool does not + * appear to generate the single-quoted form. + */ public class RelativizingFileManager extends ForwardingJavaFileManager implements StandardJavaFileManager { - public RelativizingFileManager(StandardJavaFileManager fileManager) + private final Charset outputEncoding; + + /** + * Construct a {@code RelativizingFileManager}, given the underlying file + * manager from {@link DocumentationTool#getStandardFileManager}, and the + * output encoding to be used. + *

    + * The javadoc tool requests {@link OutputStream}s for its output files, and + * supplies content already encoded, so the encoding is needed in order to + * decode them here for simple processing (as {@code java.util.regex} does + * not offer byte-domain flavors of patterns and matchers), then re-encode + * the result. + *

    + * The file manager constructed here must still be configured by passing + * the necessary subset of the desired javadoc options to + * {@link #handleFirstOptions handleFirstOptions}. + */ + public RelativizingFileManager( + StandardJavaFileManager fileManager, + Charset outputEncoding) { super(fileManager); + this.outputEncoding = outputEncoding; + } + + static final Pattern toReplace = Pattern.compile( + "(\\shref=\"(?:\\.\\./)*+)RELDOTS/"); + + /** + * Overridden to return the superclass result unchanged unless the requested + * file is of kind {@code HTML}, and in that case to return a file object + * that will interpose on the {@code OutputStream} and apply the rewriting. + */ + @Override + public FileObject getFileForOutput( + Location location /* location */, + String packageName, + String relativePath, + FileObject sibling) + throws IOException + { + FileObject fo = fileManager.getFileForOutput( + location, packageName, relativePath, sibling); + if ( ! (fo instanceof JavaFileObject) ) + return fo; + JavaFileObject jfo = (JavaFileObject)fo; + if ( ! (HTML == jfo.getKind()) ) + return fo; + + Path fp = asPath(fo); + Path r = + stream(getLocationAsPaths(location).spliterator(), false) + .filter(p -> fp.startsWith(p)).findAny().get(); + + int depth = r.relativize(fp).getNameCount() - 1; + + final String dots = Stream.generate(() -> "../").limit(depth) + .collect(Collectors.joining()); + + return new ForwardingJavaFileObject(jfo) + { + @Override + public OutputStream openOutputStream() throws IOException + { + final OutputStream os = fileObject.openOutputStream(); + + return new ByteArrayOutputStream() + { + private boolean closed = false; + + @Override + public void close() throws IOException + { + if ( closed ) + return; + closed = true; + super.close(); + + try (os; Writer w = + new OutputStreamWriter(os, + outputEncoding.newEncoder())) + { + ByteBuffer bb = ByteBuffer.wrap(buf, 0, count); + CharBuffer cb = + outputEncoding.newDecoder().decode(bb); + String fixed = toReplace.matcher(cb).replaceAll( + "$1" + dots); + w.append(fixed); + } + } + }; + } + }; } /** @@ -60,7 +213,11 @@ public void handleFirstOptions(Iterable firstOptions) } /* - * The boilerplate StandardJavaFileManager forwards follow. + * The file manager supplied by the tool is an instance of + * StandardJavaFileManager. There is no forwarding version of that, so we + * must extend ForwardingJavaFileManager and then supply forwarding versions + * of all methods added in StandardJavaFileManager. Those boilerplate + * forwarding methods follow. */ @Override From 5f4673f435b0d6f2dfe5f83b9e9729ba5c07ae9b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2020 23:21:36 -0400 Subject: [PATCH 0619/1087] Extra .. needed when linking from modular code The RelativizingFileManager has to check whether the output location is module-oriented, and add another ../ if so, to account for the module-name directory level at the top of the tree. For some reason, javadoc in pljava/ will complain if the -d option isn't passed to the file manager. In pljava-examples/ it wasn't being passed, and nothing complained. Anyway, moving -d up to the start of args in both places for consistency. --- pljava-examples/pom.xml | 20 ++++++++--- .../pljava/pgxs/RelativizingFileManager.java | 5 ++- pljava/pom.xml | 33 +++++++++++++++---- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index c66e423f..78b7148a 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -168,8 +168,12 @@ function executeReport(report, locale) /* * The 'standard options' that javadoc inherits from javac. * Do not add --release: it causes -encoding to be ignored (in javadoc - * 12 through 15, anyway). + * 12 through 15, anyway). -d is documented as a doclet option, but + * included here to be seen by the RelativizingFileManager (which may + * otherwise complain that no class output location has been set). */ + "-d", report.reportOutputDirectory.toPath() + .resolve("apidocs").toString(), "-encoding", report.inputEncoding, "--class-path", paths.get("classpath"), "--module-path",paths.get("modulepath"), @@ -188,8 +192,6 @@ function executeReport(report, locale) */ "-author", "-bottom", bottom, - "-d", report.reportOutputDirectory.toPath() - .resolve("apidocs").toString(), "-docencoding", report.outputEncoding, "-doctitle", title, "-link", jdklink, @@ -243,14 +245,22 @@ function executeReport(report, locale) diag(d.kind, s + d.getMessage(locale)); } + var Charset = Java.type("java.nio.charset.Charset"); + var smgr = tool.getStandardFileManager( diagListener, locale, - java.nio.charset.Charset.forName(report.inputEncoding) + Charset.forName(report.inputEncoding) ); + /* + * A special file manager that will rewrite the RELDOTS seen in -linkoffline + * above. The options a file manager recognizes must be the first ones in + * args; handleFirstOptions below returns at the first one the file manager + * doesn't know what to do with. + */ var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager( - smgr, java.nio.charset.Charset.forName(report.outputEncoding)); + smgr, Charset.forName(report.outputEncoding)); rmgr.handleFirstOptions(args); var task = tool.getTask(null, rmgr, diagListener, null, args, null); diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java index 55cbb54b..49e81a36 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/RelativizingFileManager.java @@ -147,7 +147,10 @@ public FileObject getFileForOutput( stream(getLocationAsPaths(location).spliterator(), false) .filter(p -> fp.startsWith(p)).findAny().get(); - int depth = r.relativize(fp).getNameCount() - 1; + int depth = r.relativize(fp).getNameCount() - 1; // -1 for file name + + if ( location.isModuleOrientedLocation() ) + ++ depth; final String dots = Stream.generate(() -> "../").limit(depth) .collect(Collectors.joining()); diff --git a/pljava/pom.xml b/pljava/pom.xml index 0fee5e5d..88f4b415 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -145,12 +145,17 @@ function executeReport(report, locale) /* * The 'standard options' that javadoc inherits from javac. * Do not add --release: it causes -encoding to be ignored (in javadoc - * 12 through 15, anyway). + * 12 through 15, anyway). -d is documented as a doclet option, but + * included here to be seen by the RelativizingFileManager (which may + * otherwise complain that no class output location has been set). */ + "-d", report.reportOutputDirectory.toPath() + .resolve("apidocs").toString(), "-encoding", report.inputEncoding, - "--module", "org.postgresql.pljava.internal", "--module-path", paths.get("modulepath"), "--module-source-path", "org.postgresql.pljava.internal=" + srcroot, + // ^^^ Options recognized by the file manager end here ^^^ + "--module", "org.postgresql.pljava.internal", /* * Core javadoc options. * Avoid the legacy package/private/protected/public options; they can @@ -165,13 +170,11 @@ function executeReport(report, locale) */ "-author", "-bottom", bottom, - "-d", report.reportOutputDirectory.toPath() - .resolve("apidocs").toString(), "-docencoding", report.outputEncoding, "-doctitle", title, "-link", jdklink, "-linkoffline", - "../../pljava-api/apidocs", + "../../RELDOTS/pljava-api/apidocs", apioffline.toString(), "-sourcetab", "4", "-use", @@ -197,7 +200,25 @@ function executeReport(report, locale) diag(d.kind, s + d.getMessage(locale)); } - var task = tool.getTask(null, null, diagListener, null, args, null); + var Charset = Java.type("java.nio.charset.Charset"); + + var smgr = tool.getStandardFileManager( + diagListener, + locale, + Charset.forName(report.inputEncoding) + ); + + /* + * A special file manager that will rewrite the RELDOTS seen in -linkoffline + * above. The options a file manager recognizes must be the first ones in + * args; handleFirstOptions below returns at the first one the file manager + * doesn't know what to do with. + */ + var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager( + smgr, Charset.forName(report.outputEncoding)); + rmgr.handleFirstOptions(args); + + var task = tool.getTask(null, rmgr, diagListener, null, args, null); task.call(); } ]]> From b6e60530554c266a334657f931e9007f0b140073 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2020 23:46:04 -0400 Subject: [PATCH 0620/1087] Only supply -sourcetab 4 for examples The option was added on speculation that it might improve the formatting of some generated documentation, by matching the project's conventional tab width. However, the option implicitly turns on -linksource, to generate linked HTMLified versions of the source code, which I'm not sure is necessary (GitHub does a fair job of that). It is not clear whether -sourcetab has any effect anywhere else but the HTMLified sources. But there is not any -nolinksource option to reverse its implicit -linksource, so simply remove it again. Leave it for -examples, where perusing the source is the point. --- pljava-api/pom.xml | 2 +- pljava/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index dae8379a..67cf6d63 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -164,7 +164,7 @@ function executeReport(report, locale) "-docencoding", report.outputEncoding, "-doctitle", title, "-link", jdklink, - "-sourcetab", "4", + //"-sourcetab", "4", // seemed good idea but implies -linksource "-use", "-version", "-windowtitle", title diff --git a/pljava/pom.xml b/pljava/pom.xml index 88f4b415..a1c02781 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -176,7 +176,7 @@ function executeReport(report, locale) "-linkoffline", "../../RELDOTS/pljava-api/apidocs", apioffline.toString(), - "-sourcetab", "4", + //"-sourcetab", "4",//seemed good idea but implies -linksource "-use", "-version", "-windowtitle", title From ef223ff4c042f18233ddf25cbf0cb59d6c1c72c9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 17 Aug 2020 08:41:21 -0400 Subject: [PATCH 0621/1087] Have pljava-pgxs use itself to make its javadoc This requires, again, a profile declaring Graal's JavaScript engine as a dependency of the plugin for Java >= 15. In this case, though, this IS the plugin, so a simpler profile can be used that directly declares the dependencies (unlike the profiles added in other subprojects, which had to use the more elaborate syntax to declare dependencies of a plugin they used). A convenient consequence of a profile in the plugin's own pom, declaring straight dependencies, is that the profiles added in other subprojects' poms earlier, to add the same dependencies, are no longer needed. I never liked having so many copies of those. This amounts to the pljava-pgxs plugin making a commitment that JavaScript will always be an available language for scripting. And that is pretty much the intent. In passing, remove three declared dependencies that seem unnecessary to building the plugin. If they need to be added again as more of its functionality is developed, so be it, but for now they seem unneeded. If we ever want to make its javadoc link offline to the corresponding Maven docs, it will seem less daunting without such a long list of dependencies. --- pljava-api/pom.xml | 29 ------- pljava-examples/pom.xml | 28 +------ pljava-pgxs/pom.xml | 171 ++++++++++++++++++++++++++++++++++++---- pljava/pom.xml | 29 ------- 4 files changed, 156 insertions(+), 101 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 67cf6d63..e2982385 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -10,35 +10,6 @@ PL/Java API The API for Java stored procedures in PostgreSQL using PL/Java - - - nashorngone - - [15,) - - - - - org.postgresql - pljava-pgxs - - - org.graalvm.js - js - 20.1.0 - - - org.graalvm.js - js-scriptengine - 20.1.0 - - - - - - - - diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 78b7148a..6273b0f0 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -34,33 +34,6 @@ - - - nashorngone - - [15,) - - - - - org.postgresql - pljava-pgxs - - - org.graalvm.js - js - 20.1.0 - - - org.graalvm.js - js-scriptengine - 20.1.0 - - - - - - @@ -70,6 +43,7 @@ ${project.version} + diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 6a516264..dc5bb9a5 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -21,22 +21,6 @@ maven-plugin-api ${maven.version} - - org.apache.maven - maven-core - ${maven.version} - - - org.apache.maven - maven-artifact - ${maven.version} - - - org.apache.maven - maven-compat - ${maven.version} - test - org.apache.maven.plugin-tools maven-plugin-annotations @@ -54,6 +38,27 @@ + + + nashorngone + + [15,) + + + + org.graalvm.js + js + 20.1.0 + + + org.graalvm.js + js-scriptengine + 20.1.0 + + + + + @@ -82,4 +87,138 @@ + + + + org.postgresql + pljava-pgxs + ${project.version} + + + + + scripting-report + + + + + + + + + + + diff --git a/pljava/pom.xml b/pljava/pom.xml index a1c02781..3f6c7591 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -17,35 +17,6 @@ - - - nashorngone - - [15,) - - - - - org.postgresql - pljava-pgxs - - - org.graalvm.js - js - 20.1.0 - - - org.graalvm.js - js-scriptengine - 20.1.0 - - - - - - - - From 49764b5140b7abb531f1c86be764aecf4793e4ae Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 17 Aug 2020 19:38:48 -0400 Subject: [PATCH 0622/1087] Tweak names of new goal and report scripting-report -> scripted-report (the first way sounded like it could be a report about use of scripting in the project). scripting -> scripted-goal for consistency. The id of a scripted goal or report should be used to say something about what it is intended to do. --- pljava-api/pom.xml | 4 +--- pljava-examples/pom.xml | 4 +--- pljava-packaging/pom.xml | 4 ++-- pljava-pgxs/pom.xml | 4 +--- .../java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java | 2 +- .../main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java | 2 +- pljava-so/pom.xml | 4 ++-- pljava/pom.xml | 4 +--- 8 files changed, 10 insertions(+), 18 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index e2982385..e4998769 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -45,9 +45,7 @@ - - scripting-report - + scripted-report + + + + + + diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 7720f5f0..0f74b6c8 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -119,6 +119,10 @@ *

    * As in JarX itself, some liberties with coding style may be taken here to keep * this one extra {@code .class} file from proliferating into a bunch of them. + *

    + * As the testing-related methods here are intended for ad-hoc or scripted use + * in {@code jshell}, they are typically declared to throw any checked + * exception, without further specifics. */ public class Node extends JarX { @@ -131,6 +135,11 @@ public class Node extends JarX { private static boolean s_jarProcessed = false; private static String s_examplesJar; + /** + * Perform an ordinary installation, using {@code pg_config} or the + * corresponding system properties to learn where the files belong, and + * unpacking the files (not including this class or its ancestors) there. + */ public static void main(String[] args) throws Exception { if ( args.length > 0 ) @@ -171,6 +180,13 @@ public void prepareResolver(String v) throws Exception m_lineSep = getProperty("line.separator"); } + /** + * Replaces a prefix {@code pljava/}key in a path to be extracted + * with the value of the {@code pgconfig.}key system property, or + * the result of invoking {@code pg_config} (or the exact executable named + * in the {@code pgconfig} system property, if present) with the option + * {@code --}key. + */ @Override public String resolve(String storedPath, String platformPath) throws Exception @@ -241,6 +257,17 @@ public String resolve(String storedPath, String platformPath) * of this class that is acting as a "Node" rather than as the JarX helper. */ + /** + * True if the platform is determined to be Windows. + *

    + * On Windows, {@link #forWindowsCRuntime forWindowsCRuntime} should be + * applied to any {@code ProcessBuilder} before invoking it; the details of + * the transformation applied by + * {@link #asPgCtlInvocation asPgCtlInvocation} change, and + * {@link #use_pg_ctl use_pg_ctl} may prove useful, as {@code pg_ctl} on + * Windows is able to drop administrative privileges that would otherwise + * prevent {@code postgres} from starting. + */ public static final boolean s_isWindows = getProperty("os.name").startsWith("Windows"); @@ -509,9 +536,8 @@ public void init() throws Exception } /** - * Invoke {@code initdb} for the node, passing default options appropriate - * for this setting, and optionally one or more tweaks to be applied to the - * {@code ProcessBuilder} before it is started. + * Invoke {@code initdb} for the node, with suppliedOptions + * overriding or supplementing the ones that would be passed by default. */ public void init(Map suppliedOptions) throws Exception { @@ -530,7 +556,9 @@ public void init(UnaryOperator tweaks) throws Exception /** * Invoke {@code initdb} for the node, with suppliedOptions - * overriding or supplementing the ones that would be passed by default. + * overriding or supplementing the ones that would be passed by default, + * and tweaks to be applied to the {@code ProcessBuilder} + * before it is started. *

    * By default, {@code postgres} will be the name of the superuser, UTF-8 * will be the encoding, {@code auth-local} will be {@code peer} and @@ -677,7 +705,9 @@ public void start(UnaryOperator tweaks) throws Exception /** * Start a PostgreSQL server for the node, with suppliedOptions - * overriding or supplementing the ones that would be passed by default. + * overriding or supplementing the ones that would be passed by default, and + * tweaks to be applied to the {@code ProcessBuilder} before it + * is started. *

    * By default, the server will listen only on the loopback interface and * not on any Unix-domain socket, on the port selected when this Node was @@ -964,141 +994,6 @@ public static Stream q(Connection c, String sql) throws Exception return q(s, () -> s.execute(sql)); } - /** - * Execute some arbitrary SQL and pass - * the {@link #q(Statement,Callable) result stream} - * to {@link #qp(Stream)} for printing to standard output. - */ - public static void qp(Connection c, String sql) throws Exception - { - qp(q(c, sql)); - } - - /** - * Invoke some {@code execute} method on a {@code Statement} and pass - * the {@link #q(Statement,Callable) result stream} - * to {@link #qp(Stream)} for printing to standard output. - *

    - * This is how, for example, to prepare, then print the results of, a - * {@code PreparedStatement}: - *

    -	 * PreparedStatement ps = conn.prepareStatement("select foo(?,?)");
    -	 * ps.setInt(1, 42);
    -	 * ps.setString(2, "surprise!");
    -	 * qp(ps, ps::execute);
    -	 *
    - * The {@code Statement} will be closed. - */ - public static void qp(Statement s, Callable work) throws Exception - { - qp(q(s, work)); - } - - /** - * Return true if the examples jar includes the - * {@code org.postgresql.pljava.example.saxon.S9} class (meaning the - * appropriate Saxon jar must be installed and on the classpath first before - * the examples jar can be deployed, unless {@code check_function_bodies} - * is {@code off} to skip dependency checking. - */ - public static boolean examplesNeedSaxon() throws Exception - { - dryExtract(); - try ( JarFile jf = new JarFile(s_examplesJar) ) - { - return jf.stream().anyMatch(e -> - "org/postgresql/pljava/example/saxon/S9.class" - .equals(e.getName())); - } - } - - /** - * Install the examples jar, under the name {@code examples}. - *

    - * The jar is specified by a {@code file:} URI and the path is the one where - * this installer installed (or would have installed) it. - * @return a {@link #q(Statement,Callable) result stream} from executing - * the statement - */ - public static Stream installExamples(Connection c, boolean deploy) - throws Exception - { - dryExtract(); - return installJar(c, "file:"+s_examplesJar, "examples", deploy); - } - - /** - * Install the examples jar, under the name {@code examples}, and place it - * on the class path for schema {@code public}. - *

    - * The return of a concatenated result stream from two consecutive - * statements might be likely to fail in cases where the first - * statement has any appreciable data to return, but pgjdbc-ng seems to - * handle it at least in this case where each statement just returns one - * row / one column of {@code void}. And it is convenient. - * @return a combined {@link #q(Statement,Callable) result stream} from - * executing the statements - */ - public static Stream installExamplesAndPath( - Connection c, boolean deploy) - throws Exception - { - Stream s1 = installExamples(c, deploy); - Stream s2 = setClasspath(c, "public", "examples"); - return Stream.concat(s1, s2); - } - - /** - * Install a Saxon jar under the name {@code saxon}, given the path to a - * local Maven repo and the needed version of Saxon, assuming the jar has - * been downloaded there already. - * @return a {@link #q(Statement,Callable) result stream} from executing - * the statement - */ - public static Stream installSaxon( - Connection c, String repo, String version) - throws Exception - { - Path p = Paths.get( - repo, "net", "sf", "saxon", "Saxon-HE", version, - "Saxon-HE-" + version + ".jar"); - return installJar(c, "file:" + p, "saxon", false); - } - - /** - * Install a Saxon jar under the name {@code saxon}, and place it on the - * class path for schema {@code public}. - * @return a combined {@link #q(Statement,Callable) result stream} from - * executing the statements - */ - public static Stream installSaxonAndPath( - Connection c, String repo, String version) - throws Exception - { - Stream s1 = installSaxon(c, repo, version); - Stream s2 = setClasspath(c, "public", "saxon"); - return Stream.concat(s1, s2); - } - - /** - * A four-fer: install Saxon, add it to the class path, then install the - * examples jar, and update the classpath to include both. - * @param repo the base directory of a local Maven repository into which the - * Saxon jar has been downloaded - * @param version the needed version of Saxon - * @param deploy whether to run the example jar's deployment code - * @return a combined {@link #q(Statement,Callable) result stream} from - * executing the statements - */ - public static Stream installSaxonAndExamplesAndPath( - Connection c, String repo, String version, boolean deploy) - throws Exception - { - Stream s1 = installSaxonAndPath(c, repo, version); - Stream s2 = installExamplesAndPath(c, deploy); - return Stream.concat(s1, s2); - } - /** * Produce a {@code Stream} of the (in JDBC, possibly multiple) results * from some {@code execute} method on a {@code Statement}. @@ -1269,6 +1164,141 @@ public static Stream q(final Statement s, Callable work) ); } + /** + * Execute some arbitrary SQL and pass + * the {@link #q(Statement,Callable) result stream} + * to {@link #qp(Stream)} for printing to standard output. + */ + public static void qp(Connection c, String sql) throws Exception + { + qp(q(c, sql)); + } + + /** + * Invoke some {@code execute} method on a {@code Statement} and pass + * the {@link #q(Statement,Callable) result stream} + * to {@link #qp(Stream)} for printing to standard output. + *

    + * This is how, for example, to prepare, then print the results of, a + * {@code PreparedStatement}: + *

    +	 * PreparedStatement ps = conn.prepareStatement("select foo(?,?)");
    +	 * ps.setInt(1, 42);
    +	 * ps.setString(2, "surprise!");
    +	 * qp(ps, ps::execute);
    +	 *
    + * The {@code Statement} will be closed. + */ + public static void qp(Statement s, Callable work) throws Exception + { + qp(q(s, work)); + } + + /** + * Return true if the examples jar includes the + * {@code org.postgresql.pljava.example.saxon.S9} class (meaning the + * appropriate Saxon jar must be installed and on the classpath first before + * the examples jar can be deployed, unless {@code check_function_bodies} + * is {@code off} to skip dependency checking). + */ + public static boolean examplesNeedSaxon() throws Exception + { + dryExtract(); + try ( JarFile jf = new JarFile(s_examplesJar) ) + { + return jf.stream().anyMatch(e -> + "org/postgresql/pljava/example/saxon/S9.class" + .equals(e.getName())); + } + } + + /** + * Install the examples jar, under the name {@code examples}. + *

    + * The jar is specified by a {@code file:} URI and the path is the one where + * this installer installed (or would have installed) it. + * @return a {@link #q(Statement,Callable) result stream} from executing + * the statement + */ + public static Stream installExamples(Connection c, boolean deploy) + throws Exception + { + dryExtract(); + return installJar(c, "file:"+s_examplesJar, "examples", deploy); + } + + /** + * Install the examples jar, under the name {@code examples}, and place it + * on the class path for schema {@code public}. + *

    + * The return of a concatenated result stream from two consecutive + * statements might be likely to fail in cases where the first + * statement has any appreciable data to return, but pgjdbc-ng seems to + * handle it at least in this case where each statement just returns one + * row / one column of {@code void}. And it is convenient. + * @return a combined {@link #q(Statement,Callable) result stream} from + * executing the statements + */ + public static Stream installExamplesAndPath( + Connection c, boolean deploy) + throws Exception + { + Stream s1 = installExamples(c, deploy); + Stream s2 = setClasspath(c, "public", "examples"); + return Stream.concat(s1, s2); + } + + /** + * Install a Saxon jar under the name {@code saxon}, given the path to a + * local Maven repo and the needed version of Saxon, assuming the jar has + * been downloaded there already. + * @return a {@link #q(Statement,Callable) result stream} from executing + * the statement + */ + public static Stream installSaxon( + Connection c, String repo, String version) + throws Exception + { + Path p = Paths.get( + repo, "net", "sf", "saxon", "Saxon-HE", version, + "Saxon-HE-" + version + ".jar"); + return installJar(c, "file:" + p, "saxon", false); + } + + /** + * Install a Saxon jar under the name {@code saxon}, and place it on the + * class path for schema {@code public}. + * @return a combined {@link #q(Statement,Callable) result stream} from + * executing the statements + */ + public static Stream installSaxonAndPath( + Connection c, String repo, String version) + throws Exception + { + Stream s1 = installSaxon(c, repo, version); + Stream s2 = setClasspath(c, "public", "saxon"); + return Stream.concat(s1, s2); + } + + /** + * A four-fer: install Saxon, add it to the class path, then install the + * examples jar, and update the classpath to include both. + * @param repo the base directory of a local Maven repository into which the + * Saxon jar has been downloaded + * @param version the needed version of Saxon + * @param deploy whether to run the example jar's deployment code + * @return a combined {@link #q(Statement,Callable) result stream} from + * executing the statements + */ + public static Stream installSaxonAndExamplesAndPath( + Connection c, String repo, String version, boolean deploy) + throws Exception + { + Stream s1 = installSaxonAndPath(c, repo, version); + Stream s2 = installExamplesAndPath(c, deploy); + return Stream.concat(s1, s2); + } + /** * A flat-mapping function to expand any {@code SQLException} or * {@code SQLWarning} instance in a result stream into the stream of @@ -1276,6 +1306,10 @@ public static Stream q(final Statement s, Callable work) * of the {@code SQLException} iterator. *

    * Any other object is returned in a singleton stream. + *

    + * To flatten just the chain of {@code SQLWarning} or {@code SQLException} + * but with each of those retaining its own list of {@code cause}s, see + * {@link #semiFlattenDiagnostics semiFlattenDiagnostics}. */ public static Stream flattenDiagnostics(Object oneResult) { @@ -1590,7 +1624,7 @@ public static int[] voidResultSetDims(Object o) throws Exception *

    * The expected result of a query that calls one {@code void}-typed, * non-set-returning function could be checked with - * {@code isVoid(rs, 1, 1)}. + * {@code isVoidResultSet(rs, 1, 1)}. */ public static boolean isVoidResultSet(Object o, int rows, int columns) throws Exception @@ -1792,13 +1826,14 @@ private boolean waitPrePG10() throws Exception *

    * This transformation must account for the way the C runtime will * ultimately parse the parameters apart, and also for the behavior of - * Java's runtime in assembling the command line that the C code + * Java's runtime in assembling the command line that the invoked process * will receive. * @param pb a ProcessBuilder whose command has been set to an executable * that parses parameters using the C runtime rules, and arguments as they * should result from parsing. * @return The same ProcessBuilder, with the argument list rewritten as - * necessary to produce the original list as a result of C runtime parsing, + * necessary to produce the original list as a result of Windows C runtime + * parsing, * @throws IllegalArgumentException if the ProcessBuilder does not have at * least the first command element (the executable to run) * @throws UnsupportedOperationException if the arguments passed, or system @@ -1975,8 +2010,8 @@ public static ProcessBuilder forWindowsCRuntime(ProcessBuilder pb) * correctly for {@code sh} or {@code cmd} as appropriate. *

    * The result of this transformation still has to be received intact by - * {@code pg_ctl} itself, which requires (on Windows) an application of - * {@code forWindowsCRuntime} as well. + * {@code pg_ctl} itself, which requires (on Windows) a subsequent + * application of {@code forWindowsCRuntime} as well. * @param pb a ProcessBuilder whose command has been set to an executable * path for {@code postgres}, with only {@code -D} and {@code -c} options. * @return The same ProcessBuilder, with the argument list rewritten to diff --git a/pljava-packaging/src/site/markdown/index.md b/pljava-packaging/src/site/markdown/index.md new file mode 100644 index 00000000..8a1e332d --- /dev/null +++ b/pljava-packaging/src/site/markdown/index.md @@ -0,0 +1,36 @@ +## About PL/Java packaging + +The `pljava-packaging` subproject builds a single `jar` file that contains +the files (including the API, implementation, and examples `jar` files, +native code shared object, and PostgreSQL extension control files) that must +be unpacked into a PostgreSQL installation so PL/Java can be used. These files +could have been wrapped in a `tar` or `zip` format instead, but any site where +PL/Java will be used necessarily has Java installed, and therefore support for +the `jar` format, so it is an obvious choice. + +The resulting `jar` can be simply extracted using the `jar` tool, and the files +moved to the proper locations, or it can be run with `java -jar`. It contains +two extra `.class` files to give it a very simple self-extracting behavior: +it will run `pg_config` to learn where PostgreSQL is installed, and extract +PL/Java's files into the correct locations. See [Installing PL/Java][install] +for the details. + +If the file is simply extracted using the `jar` tool, those two added class +files will also be extracted, and can be deleted; they are not needed for +PL/Java's operation. + +### Use with `jshell` as a testing environment + +The added classes supply some additional methods, unused during a simple +installation with `java -jar`, but accessible from Java's [JShell][] +scripting tool if it is launched with this `jar` on its classpath. +That allows `jshell` to serve as an environment for scripting tests +of PL/Java in a running PostgreSQL instance, with capabilities similar to +(and modeled on) the [PostgresNode][] Perl module distributed with PostgreSQL. + +See the javadoc for [the Node class][node] for details. + +[install]: ../install/install.html +[JShell]: https://docs.oracle.com/javase/9/jshell/introduction-jshell.htm +[PostgresNode]: https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/test/perl/PostgresNode.pm;h=aec3b9a;hb=e640093 +[node]: apidocs/org/postgresql/pljava/packaging/Node.html From 6f02f340d5549f3a1eefd0dc26faca3361e767e4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Aug 2020 20:45:53 -0400 Subject: [PATCH 0631/1087] Add a rudimentary DFA for checking a result stream The caller can supply a list of lambdas representing numbered states of a DFA, where the first in the list is considered state 1 and the initial state. Repeatedly, the current state will be applied to the current item of input, and may return the number of the next state, true to indicate a successful match, or false for inability to proceed. When moving to a new state, the current input item may be consumed, or retained and examined again in the new state. Also add a method peek(Object), which will print an object largely the same way qp would, but intended to be passed to Stream.peek to create a stream that will visibly print its members while also being passed to dfa() for matching. Unlike qp, peek declares no checked exceptions (as the Stream API requires), and will print only the metadata of a ResultSet, avoiding closing it or disturbing its cursor, so it can still be examined in the DFA. --- pljava-packaging/src/main/java/Node.java | 311 +++++++++++++++++++++-- 1 file changed, 289 insertions(+), 22 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 0f74b6c8..75113d73 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -31,6 +31,7 @@ */ import static java.lang.ProcessBuilder.Redirect.INHERIT; +import java.lang.reflect.InvocationHandler; // flexible SAM allowing exceptions import static java.lang.Thread.interrupted; import static java.net.InetAddress.getLoopbackAddress; @@ -74,6 +75,7 @@ import java.util.ArrayDeque; import java.util.Base64; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -90,6 +92,7 @@ import java.util.concurrent.Callable; // like a Supplier but allows exceptions! import static java.util.concurrent.TimeUnit.MILLISECONDS; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -1167,7 +1170,7 @@ public static Stream q(final Statement s, Callable work) /** * Execute some arbitrary SQL and pass * the {@link #q(Statement,Callable) result stream} - * to {@link #qp(Stream)} for printing to standard output. + * to {@link #qp(Stream)} for printing to standard output. */ public static void qp(Connection c, String sql) throws Exception { @@ -1177,7 +1180,7 @@ public static void qp(Connection c, String sql) throws Exception /** * Invoke some {@code execute} method on a {@code Statement} and pass * the {@link #q(Statement,Callable) result stream} - * to {@link #qp(Stream)} for printing to standard output. + * to {@link #qp(Stream)} for printing to standard output. *

    * This is how, for example, to prepare, then print the results of, a * {@code PreparedStatement}: @@ -1411,6 +1414,40 @@ else if ( o instanceof Throwable ) + " in result stream -->"); } + /** + * Prints an object in the manner of {@link #qp(Object) qp}, but in a way + * suitable for use in {@link Stream#peek Stream.peek}. + *

    + * If o is a {@code ResultSet}, only its metadata will be printed; + * its position will not be disturbed and it will not be closed. This method + * throws no checked exceptions, as the {@code Stream} API requires; any + * that is caught will be printed as if by {@link #qp(Throwable) qp}. + */ + public static void peek(Object o) + { + try + { + int[] dims = voidResultSetDims(o); + + if ( null != dims ) + { + System.out.printf(voidResultSet, dims[0], dims[1]); + return; + } + + if ( o instanceof ResultSet ) + qp(((ResultSet)o).getMetaData()); + else + qp(o); + } + catch ( Exception e ) + { + qp(e); + } + } + + private static final String voidResultSet = "%n"; + /** * Overload of {@code qp} for direct application to a {@code ResultSet}. *

    @@ -1429,8 +1466,7 @@ public static void qp(ResultSet rs) throws Exception if ( null != dims ) { - System.out.println( - ""); + System.out.printf(voidResultSet, dims[0], dims[1]); return; } @@ -1459,26 +1495,73 @@ public static void qp(ResultSet rs) throws Exception */ public static void qp(ParameterMetaData md) throws Exception { - WebRowSet wrs = RowSetProvider.newFactory().createWebRowSet(); - try + RowSetMetaDataImpl mdi = new RowSetMetaDataImpl(); + mdi.setColumnCount(md.getParameterCount()); + for ( int i = 1; i <= md.getParameterCount(); ++ i ) { - RowSetMetaDataImpl mdi = new RowSetMetaDataImpl(); - mdi.setColumnCount(md.getParameterCount()); - for ( int i = 1; i <= md.getParameterCount(); ++ i ) - { - mdi.setColumnType(i, md.getParameterType(i)); - mdi.setColumnTypeName(i, md.getParameterTypeName(i)); - mdi.setPrecision(i, md.getPrecision(i)); - mdi.setScale(i, md.getScale(i)); - mdi.setNullable(i, md.isNullable(i)); - mdi.setSigned(i, md.isSigned(i)); - } - wrs.setMetaData(mdi); - wrs.writeXml(System.out); + mdi.setColumnType(i, md.getParameterType(i)); + mdi.setColumnTypeName(i, md.getParameterTypeName(i)); + mdi.setPrecision(i, md.getPrecision(i)); + mdi.setScale(i, md.getScale(i)); + mdi.setNullable(i, md.isNullable(i)); + mdi.setSigned(i, md.isSigned(i)); } - finally + qp(mdi); + } + + /** + * Overload of {@code qp} for examining {@code ResultSetMetaData}. + *

    + * This makes an empty {@code WebRowSet} with the copied metadata, and dumps + * it with {@code writeXml}. Owing to a few missing setters on Java's + * {@link RowSetMetaDataImpl}, a few {@code ResultSetMetaData} attributes + * will not have been copied; they'll be wrong (unless the real values + * happen to match the defaults). That could be fixed by extending that + * class, but that would require yet another extra class file added to the + * installer jar. + */ + public static void qp(ResultSetMetaData md) throws Exception + { + RowSetMetaDataImpl mdi = new RowSetMetaDataImpl(); + mdi.setColumnCount(md.getColumnCount()); + for ( int i = 1; i <= md.getColumnCount(); ++ i ) { - wrs.close(); + mdi.setColumnType(i, md.getColumnType(i)); + mdi.setColumnTypeName(i, md.getColumnTypeName(i)); + mdi.setPrecision(i, md.getPrecision(i)); + mdi.setScale(i, md.getScale(i)); + mdi.setNullable(i, md.isNullable(i)); + mdi.setSigned(i, md.isSigned(i)); + + mdi.setAutoIncrement(i, md.isAutoIncrement(i)); + mdi.setCaseSensitive(i, md.isCaseSensitive(i)); + mdi.setCatalogName(i, md.getCatalogName(i)); + mdi.setColumnDisplaySize(i, md.getColumnDisplaySize(i)); + mdi.setColumnLabel(i, md.getColumnLabel(i)); + mdi.setColumnName(i, md.getColumnName(i)); + mdi.setCurrency(i, md.isCurrency(i)); + mdi.setSchemaName(i, md.getSchemaName(i)); + mdi.setSearchable(i, md.isSearchable(i)); + mdi.setTableName(i, md.getTableName(i)); + + /* + * Attributes that RowSetMetaDataImpl simply forgets to provide + * setters for. It is what it is. + columnClassName + isDefinitelyWritable + isReadOnly + isWritable + */ + } + qp(mdi); + } + + private static void qp(RowSetMetaDataImpl mdi) throws Exception + { + try (WebRowSet wrs = RowSetProvider.newFactory().createWebRowSet()) + { + wrs.setMetaData(mdi); + wrs.writeXml(System.out); } } @@ -1487,7 +1570,7 @@ public static void qp(ParameterMetaData md) throws Exception * special handling for {@code SQLException} and {@code SQLWarning}. *

    * In keeping with the XMLish vibe established by - * {@link #qp(Stream) qp} for other items in a result + * {@link #qp(Stream) qp} for other items in a result * stream, this will render a {@code Throwable} as an {@code error}, * {@code warning}, or {@code info} element (PostgreSQL's finer * distinctions of severity are not exposed by pgjdbc-ng's API.) @@ -1634,6 +1717,190 @@ public static boolean isVoidResultSet(Object o, int rows, int columns) return null != dims && rows == dims[0] && columns == dims[1]; } + /** + * Executes a deterministic finite automaton (DFA) specified in the form of + * a list of lambdas representing states of the DFA, to verify that a + * {@link #q(Statement,Callable) result stream} is as expected. + *

    + * Treats the list of lambdas as a set of consecutively-numbered states + * (the first in the list is state number 1, and is the initial state). + * At each automaton step, the current state is applied to the current + * input object, and may return an {@code Integer} or a {@code Boolean}. + *

    + * If an integer, its absolute value selects the next state. A positive + * integer consumes the current input item, so the next state will be + * applied to the next item of input. A negative integer transitions to the + * selected next state without consuming the current input item, so it will + * be examined again in the newly selected state. + *

    + * If boolean, {@code false} indicates that the DFA cannot proceed; the + * supplied reporter will be passed an explanatory string and this + * method returns false. A state that returns {@code true} indicates the + * automaton has reached an accepting state. + *

    + * No item of input is allowed to be null; null is reserved to be the + * end-of-input symbol. If a state returns {@code true} (accept) + * when applied to null at the end of input, the DFA has matched and this + * method returns true. A state may also return a negative integer in + * this case, to shift to another state while looking at the end of input. + * A positive integer (attempting to consume the end of input), or a false, + * return will cause an explanatory message to the reporter and a + * false return from this method. + *

    + * A state may return {@code true} (accept) when looking at a non-null + * input item, but the input will be checked to confirm it has no more + * elements. Otherwise, the automaton has tried to accept before matching + * all the input, and this method will return false. + *

    + * To avoid defining a new functional interface, each state is represented + * by {@link InvocationHandler}, an existing functional interface with a + * versatile argument list and permissive {@code throws} clause. Each state + * must be represented as a lambda with three parameters (the convention + * {@code (o,p,q)} is suggested), of which only the first is used. If Java + * ever completes the transition to {@code _} as an unused-parameter marker, + * the suggested convention will be {@code (o,_,_)}. + *

    + * As the input item passed to each state is typed {@code Object}, and as + * null can only represent the end of input, it may be common for a state to + * both cast an input to an expected type and confirm it is not null. + * The {@link #as as} method combines those operations. If its argument + * either is null or cannot be cast to the wanted type, {@code as} will + * throw a specific instance of {@code ClassCastException}, which will be + * treated, when caught by {@code dfa}, just as if the state had returned + * {@code false}. + * @param name A name for this dfa, used only in exception messages if it + * fails to match + * @param reporter a Consumer to accept a diagnostic string if the DFA fails + * to match, defaulting if null to System.err::println + * @param input A Stream of input items, of which none may be null + * @param states Lambdas representing states of the DFA + * @return true if an accepting state was reached coinciding with the end + * of input + * @throws Exception Anything that could be thrown during evaluation of the + * input stream or any state + */ + public static boolean dfa( + String name, Consumer reporter, Stream input, + InvocationHandler... states) + throws Exception + { + if ( null == reporter ) + reporter = System.err::println; + + try ( input ) + { + Iterator in = input.iterator(); + int currentState = 0; + int stepCount = 0; + int inputCount = 0; + Object currentInput = null; + boolean hasCurrent = false; + Object result; + + while ( hasCurrent || in.hasNext() ) + { + ++ stepCount; + if ( ! hasCurrent ) + { + currentInput = in.next(); + ++ inputCount; + if ( null == currentInput ) + throw new UnsupportedOperationException( + "Input to dfa() must not contain null values"); + hasCurrent = true; + } + + result = invoke(states[currentState], currentInput); + + if ( result instanceof Boolean ) + { + if ( (Boolean)result && ! in.hasNext() ) + return true; + reporter.accept(String.format( + "dfa \"%s\" in state %d at step %d: %s", + name, 1 + currentState, stepCount, (Boolean)result + ? String.format( + "transitioned to ACCEPT after %d input items but " + + "with input remaining", inputCount) + : String.format( + "could not proceed, looking at input %d: %s", + inputCount, currentInput))); + return false; + } + + currentState = (Integer)result; + if ( currentState > 0 ) + hasCurrent = false; + else + currentState *= -1; + + -- currentState; + } + + for ( ;; ) + { + ++ stepCount; + result = invoke(states[currentState], null); + if ( result instanceof Boolean && (Boolean)result ) + return true; + else if ( result instanceof Integer && 0 > (Integer)result ) + { + currentState = -1 - (Integer)result; + continue; + } + break; + } + + reporter.accept(String.format( + "dfa \"%s\" in state %d at step %d: " + + "does not accept at end of input after %d items", + name, 1 + currentState, stepCount, inputCount)); + return false; + } + } + + /** + * Casts o to class clazz, testing it also for null. + *

    + * This is meant as a shorthand in implementing states for + * {@link #dfa dfa}. If o either is null or is not castable to the + * desired type, a distinguished instance of {@code ClassCastException} will + * be thrown, which is treated specially if caught by {@code dfa} while + * evaluating a state. + */ + public static T as(Class clazz, Object o) + { + if ( clazz.isInstance(o) ) + return clazz.cast(o); + throw failedAsException; + } + + private static final ClassCastException failedAsException = + new ClassCastException(); + + private static Object invoke(InvocationHandler h, Object o) + throws Exception + { + try + { + return h.invoke(o, null, null); + } + catch ( ClassCastException e ) + { + if ( failedAsException == e ) + return false; + throw e; + } + catch ( Exception e ) + { + throw e; + } + catch ( Throwable t ) + { + throw (Error)t; + } + } + /* * For parsing the postmaster.pid file, these have been the lines at least * back to 9.1, except PM_STATUS appeared in 10. That's too bad; before 10 From 4bd5b55fb81dc21fcb5bc2c47a6999766b36237c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Aug 2020 11:27:45 -0400 Subject: [PATCH 0632/1087] Add q flavors for ResultSet rows and columns These allow extending the "Stream driving a DFA" idiom so it can also be used to check ResultSet row contents, or column and parameter metadata. --- pljava-packaging/src/main/java/Node.java | 198 ++++++++++++++++++++++- 1 file changed, 196 insertions(+), 2 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 75113d73..5f340cc9 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -100,6 +100,7 @@ import java.util.jar.JarFile; +import java.util.stream.IntStream; import java.util.stream.Stream; import static java.util.stream.StreamSupport.stream; @@ -125,7 +126,9 @@ *

    * As the testing-related methods here are intended for ad-hoc or scripted use * in {@code jshell}, they are typically declared to throw any checked - * exception, without further specifics. + * exception, without further specifics. There are many overloads of methods + * named {@code q} and {@code qp} (mnemonic of query and query-print), to make + * interactive use in {@code jshell} comfortable with just a few static imports. */ public class Node extends JarX { @@ -998,7 +1001,7 @@ public static Stream q(Connection c, String sql) throws Exception } /** - * Produce a {@code Stream} of the (in JDBC, possibly multiple) results + * Produces a {@code Stream} of the (in JDBC, possibly multiple) results * from some {@code execute} method on a {@code Statement}. *

    * Each result in the stream will be an instance of one of: @@ -1167,6 +1170,197 @@ public static Stream q(final Statement s, Callable work) ); } + /** + * Analogously to {@link #q(Statement,Callable) q(Statement,...)}, produces + * a {@code Stream} with an element for each row of a {@code ResultSet}, + * interleaved with any {@code SQLWarning}s reported on the result set, or + * an {@code SQLException} if one is thrown. + *

    + * This is supplied chiefly for use driving a {@link #dfa DFA} to verify + * contents of a result set. For each row, the element in the stream will be + * an instance of {@code Long}, counting up from 1 (intended to match the + * result set's {@code getRow} but without relying on it, as JDBC does not + * require every implementation to support it). By itself, of course, this + * does not convey any of the content of the row; the lambdas representing + * the DFA states should close over the result set and query it for content, + * perhaps not even using the object supplied here (except to detect when it + * is a warning or exception rather than a row number). + * The row position of the result set will have been updated, and should not + * be otherwise modified when this method is being used to walk through the + * results. + *

    + * For the same reason, don't try any funny business like sorting the stream + * in any way. The {@code ResultSet} will only be read forward, and each row + * only once. Simple filtering, {@code dropWhile}/{@code takeWhile}, and so + * on will work, but may be more conveniently rolled into the design of a + * {@link #dfa DFA}, as nearly any use of a {@code ResultSet} can throw + * {@code SQLException} and therefore isn't convenient in the stream API. + *

    + * Passing this result to {@code qp} as if it came from a {@code Statement} + * could lead to confusion, as the {@code Long} elements would be printed as + * update counts rather than row numbers. + * @param rs a ResultSet + * @return a Stream as described above + */ + public static Stream q(final ResultSet rs) + throws Exception + { + final Object[] nextHolder = new Object [ 1 ]; + final long[] nextRowNumber = new long [ 1 ]; + nextRowNumber [ 0 ] = 1L; + Object seed; + + /* + * The ResultSet must not be closed in a finally, or a + * try-with-resources, because if successful it needs to remain open + * as long as the result stream is being read. It will be closed when + * the stream is. + * + * However, in any exceptional exit, the ResultSet must be closed here. + */ + + final Supplier row = () -> + { + try + { + if ( rs.next() ) + return nextRowNumber [ 0 ] ++; + return null; + } + catch ( SQLException e ) + { + return e; + } + }; + + final Supplier warnings = () -> + { + try + { + SQLWarning w = rs.getWarnings(); + if ( null != w ) + { + try + { + rs.clearWarnings(); + } + catch ( SQLException e ) + { + nextHolder [ 0 ] = e; + } + } + return w; + } + catch ( SQLException e ) + { + return e; + } + }; + + /* + * First get warnings, if any. + * There is a remote chance this can return an exception rather than a + * warning, an even more remote chance it returns a warning and leaves + * an exception in nextHolder. + * Only if it did neither is there any point in proceeding to get a row. + * If we do, and there was a warning, we use the warning as the seed and + * save the first row in nextHolder. + */ + seed = warnings.get(); + if ( (null == seed || seed instanceof SQLWarning) + && null == nextHolder [ 0 ] ) + { + Object t = row.get(); + if ( null == seed ) + seed = t; + else + nextHolder [ 0 ] = t; + } + + UnaryOperator next = o -> + { + if ( o instanceof SQLException && !(o instanceof SQLWarning) ) + return null; + + o = nextHolder [ 0 ]; + if ( null != o ) + { + nextHolder [ 0 ] = null; + return o; + } + + o = warnings.get(); + if ( null != o ) + return o; + + return row.get(); + }; + + return Stream.iterate(seed, Objects::nonNull, next) + .onClose(() -> + { + try + { + rs.close(); + } + catch ( SQLException e ) + { + } + } + ); + } + + /** + * Produces a {@code Stream} with an element for each column of a + * {@code ResultSet}. + *

    + * This is another convenience method for use chiefly in driving a + * {@link #dfa DFA} to check per-column values or metadata for a + * {@code ResultSet}. It is, in fact, nothing other than + * {@code IntStream.rangeClosed(1, rsmd.getColumnCount()).boxed()} but typed + * as {@code Stream}. + *

    + * As with {@link #q(ResultSet) q(ResultSet)}, the column number supplied + * here conveys no actual column data or metadata. The lambdas representing + * the DFA states should close over the {@code ResultSetMetaData} or + * corresponding {@code ResultSet} object, or both, and use the column + * number from this stream to index them. + * @param rsmd a ResultSetMetaData object + * @return a Stream as described above + */ + public static Stream q(final ResultSetMetaData rsmd) + throws Exception + { + return + IntStream.rangeClosed(1, rsmd.getColumnCount()) + .mapToObj(i -> (Object)i); + } + + /** + * Produces a {@code Stream} with an element for each parameter of a + * {@code PreparedStatement}. + *

    + * This is another convenience method for use chiefly in driving a + * {@link #dfa DFA} to check per-parameter metadata. It is, in fact, + * nothing other than + * {@code IntStream.rangeClosed(1, rsmd.getParameterCount()).boxed()} + * but typed as {@code Stream}. + *

    + * As with {@link #q(ResultSet) q(ResultSet)}, the column number supplied + * here conveys no actual parameter metadata. The lambdas representing + * the DFA states should close over the {@code ParameterMetaData} object + * and use the parameter number from this stream to index it. + * @param pmd a ParameterMetaData object + * @return a Stream as described above + */ + public static Stream q(final ParameterMetaData pmd) + throws Exception + { + return + IntStream.rangeClosed(1, pmd.getParameterCount()) + .mapToObj(i -> (Object)i); + } + /** * Execute some arbitrary SQL and pass * the {@link #q(Statement,Callable) result stream} From 16ae3714c29a24bfa57fc86508a9d1e6be5a2bcb Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 17 May 2020 14:37:51 +0530 Subject: [PATCH 0633/1087] Setup Travis CI --- .travis.yml | 49 ++++++++++++++++++++++++++++ .travis/travis_install_openssl.sh | 10 ++++++ .travis/travis_install_postgresql.sh | 45 +++++++++++++++++++++++++ .travis/travis_setup_pljava.sh | 29 ++++++++++++++++ .travis/travis_setup_postgresql.sh | 19 +++++++++++ .travis/travis_test_pljava.sh | 33 +++++++++++++++++++ 6 files changed, 185 insertions(+) create mode 100644 .travis.yml create mode 100755 .travis/travis_install_openssl.sh create mode 100755 .travis/travis_install_postgresql.sh create mode 100755 .travis/travis_setup_pljava.sh create mode 100755 .travis/travis_setup_postgresql.sh create mode 100755 .travis/travis_test_pljava.sh diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..3fbbf5e0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: java +os: + - linux + - osx +dist: bionic +osx_image: xcode11 +jdk: + - openjdk14 + - openjdk13 + - openjdk12 + - openjdk11 + - openjdk10 + - openjdk9 +env: + - POSTGRESQL_VERSION=12 +cache: + directories: + - $HOME/.m2 +before_install: + - . .travis/travis_install_postgresql.sh + - . .travis/travis_install_openssl.sh +install: mvn clean install -Dnar.cores=1 -Psaxon-examples -Pwnosign --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn +script: + - .travis/travis_setup_postgresql.sh + - .travis/travis_setup_pljava.sh + - .travis/travis_test_pljava.sh +jobs: + include: + - os: linux + jdk: openjdk14 + env: POSTGRESQL_VERSION=11 + - os: linux + jdk: openjdk14 + env: POSTGRESQL_VERSION=10 + - os: linux + jdk: openjdk14 + env: POSTGRESQL_VERSION=9.5 + - os: osx + jdk: openjdk14 + env: POSTGRESQL_VERSION=11 + - os: osx + jdk: openjdk14 + env: POSTGRESQL_VERSION=10 + - os: osx + jdk: openjdk14 + env: POSTGRESQL_VERSION=9.5 + - os: linux + jdk: openjdk14 + env: POSTGRESQL_VERSION=SOURCE \ No newline at end of file diff --git a/.travis/travis_install_openssl.sh b/.travis/travis_install_openssl.sh new file mode 100755 index 00000000..28a6d417 --- /dev/null +++ b/.travis/travis_install_openssl.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +if [ "$TRAVIS_OS_NAME" = "osx" ]; then + curl -o ../macports.pkg -L https://github.com/macports/macports-base/releases/download/v2.6.2/MacPorts-2.6.2-10.14-Mojave.pkg + sudo installer -pkg ../macports.pkg -target / + export PATH=/opt/local/bin:/opt/local/sbin:$PATH + yes | sudo port install openssl + export CPATH=/opt/local/include:$CPATH + export LIBRARY_PATH=/opt/local/lib:$LIBRARY_PATH +fi diff --git a/.travis/travis_install_postgresql.sh b/.travis/travis_install_postgresql.sh new file mode 100755 index 00000000..28da2761 --- /dev/null +++ b/.travis/travis_install_postgresql.sh @@ -0,0 +1,45 @@ +if [ "$TRAVIS_OS_NAME" = "osx" ]; then + brew uninstall postgis postgresql + + if [ "$POSTGRESQL_VERSION" = "12" ]; then + unset POSTGRESQL_VERSION + else + POSTGRESQL_VERSION="@$POSTGRESQL_VERSION" + fi + brew install "postgresql${POSTGRESQL_VERSION}" + + export PATH="/usr/local/opt/postgresql${POSTGRESQL_VERSION}/bin:${PATH}" + export LDFLAGS="-L/usr/local/opt/postgresql${POSTGRESQL_VERSION}/lib" + export CPPFLAGS="-I/usr/local/opt/postgresql${POSTGRESQL_VERSION}/include" + export PKG_CONFIG_PATH="/usr/local/opt/postgresql${POSTGRESQL_VERSION}/lib/pkgconfig" +else + sudo chmod 777 /home/travis + sudo service postgresql stop + sudo apt-get -qq remove postgresql libpq-dev libpq5 postgresql-client-common postgresql-common --purge + if [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then + sudo apt-get -qq install build-essential libreadline-dev zlib1g-dev flex bison libxml2-dev libxslt-dev libssl-dev libxml2-utils xsltproc + + git clone git://git.postgresql.org/git/postgresql.git ../postgresql + cd ../postgresql + git checkout REL_12_STABLE + + ./configure --with-libxml --enable-cassert --enable-debug CFLAGS='-ggdb -Og -g3 -fno-omit-frame-pointer' --quiet + make --silent && sudo make install + + cd contrib + make --silent && sudo make install + + export LD_LIBRARY_PATH="/usr/local/pgsql/lib" + sudo /sbin/ldconfig /usr/local/pgsql/lib + export PATH="/usr/local/pgsql/bin:${PATH}" + + cd ../../pljava + else + . /etc/lsb-release + echo "deb http://apt.postgresql.org/pub/repos/apt/ $DISTRIB_CODENAME-pgdg main ${POSTGRESQL_VERSION}" > ../pgdg.list + sudo mv ../pgdg.list /etc/apt/sources.list.d/ + wget --quiet -O - https://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get -qq update + sudo apt-get -qq install "postgresql-${POSTGRESQL_VERSION}" "postgresql-server-dev-${POSTGRESQL_VERSION}" libecpg-dev libkrb5-dev + fi +fi \ No newline at end of file diff --git a/.travis/travis_setup_pljava.sh b/.travis/travis_setup_pljava.sh new file mode 100755 index 00000000..25307837 --- /dev/null +++ b/.travis/travis_setup_pljava.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +if [ "$TRAVIS_OS_NAME" = "osx" ]; then + libjvm_path=$(find "$JAVA_HOME" -mindepth 2 -name "libjli.dylib" | head -n 1) +else + libjvm_path=$(find "$JAVA_HOME" -mindepth 2 -name "libjvm.so" | head -n 1) +fi +echo $libjvm_path +vmoptions='-enableassertions:org.postgresql.pljava... -Xcheck:jni' + +if [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then + find pljava-packaging -name "pljava-pg*.jar" | sudo xargs java -jar -Dpgconfig="/usr/local/pgsql/bin/pg_config" +else + find pljava-packaging -name "pljava-pg*.jar" | sudo xargs java -jar +fi + +if [ "$TRAVIS_OS_NAME" = "osx" ]; then + printf '%s\n' "SET pljava.libjvm_location TO :'path'; ALTER SYSTEM SET pljava.libjvm_location TO :'path'; SELECT pg_reload_conf();" | psql -v path="$libjvm_path" -U postgres + printf '%s\n' "SET pljava.vmoptions TO :'options'; ALTER SYSTEM SET pljava.vmoptions TO :'options'; SELECT pg_reload_conf();" | psql -v options="$vmoptions" -U postgres + psql -c "CREATE EXTENSION pljava;" -U postgres +elif [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then + printf '%s\n' "SET pljava.libjvm_location TO :'path'; ALTER SYSTEM SET pljava.libjvm_location TO :'path'; SELECT pg_reload_conf();" | sudo -u postgres /usr/local/pgsql/bin/psql -v path="$libjvm_path" -U postgres + printf '%s\n' "SET pljava.vmoptions TO :'options'; ALTER SYSTEM SET pljava.vmoptions TO :'options'; SELECT pg_reload_conf();" | sudo -u postgres /usr/local/pgsql/bin/psql -v options="$vmoptions" -U postgres + sudo -u postgres /usr/local/pgsql/bin/psql -c "CREATE EXTENSION pljava;" -U postgres +else + printf '%s\n' "SET pljava.libjvm_location TO :'path'; ALTER SYSTEM SET pljava.libjvm_location TO :'path'; SELECT pg_reload_conf();" | sudo -u postgres psql -v path="$libjvm_path" -U postgres + printf '%s\n' "SET pljava.vmoptions TO :'options'; ALTER SYSTEM SET pljava.vmoptions TO :'options'; SELECT pg_reload_conf();" | sudo -u postgres psql -v options="$vmoptions" -U postgres + sudo -u postgres psql -c "CREATE EXTENSION pljava;" -U postgres +fi diff --git a/.travis/travis_setup_postgresql.sh b/.travis/travis_setup_postgresql.sh new file mode 100755 index 00000000..993f4c75 --- /dev/null +++ b/.travis/travis_setup_postgresql.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +if [ "$TRAVIS_OS_NAME" = "osx" ]; then + export PGDATA="${HOME}/postgres" + + initdb -D "$PGDATA" + pg_ctl -w -t 300 -c -o '-p 5432' -l /tmp/postgres.log start + wait + sleep 5 && createuser -s postgres +else + if [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then + export PGDATA="/home/travis/postgres" + /usr/local/pgsql/bin/pg_ctl -D ${PGDATA} -U postgres initdb + /usr/local/pgsql/bin/pg_ctl -D ${PGDATA} -w -t 300 -c -o '-p 5432' -l /tmp/postgres.log start + wait + sleep 5 && createuser -s postgres + else + sudo service postgresql start ${POSTGRESQL_VERSION} + fi +fi diff --git a/.travis/travis_test_pljava.sh b/.travis/travis_test_pljava.sh new file mode 100755 index 00000000..ec69ee8c --- /dev/null +++ b/.travis/travis_test_pljava.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +saxon_jar_name=$(find "${HOME}/.m2/repository/net/sf/saxon/Saxon-HE/" -name "Saxon-HE-*.jar" | head -n 1) +saxon_jar="file:${saxon_jar_name}" +examples_jar_name=$(find pljava-examples -name "pljava-examples*.jar") +examples_jar="file:${PWD}/${examples_jar_name}" + +if [ "$TRAVIS_OS_NAME" = "osx" ]; then + printf '%s\n' "SELECT sqlj.install_jar(:'path','saxon',true);" | psql -v path="$saxon_jar" -U postgres + psql -c "SELECT sqlj.set_classpath('public', 'saxon');" -U postgres + printf '%s\n' "SELECT sqlj.install_jar(:'path','examples',true);" | psql -v path="$examples_jar" -U postgres + psql -c "SELECT sqlj.get_classpath('javatest');" -U postgres + psql -c "SELECT sqlj.set_classpath('javatest', 'examples');" -U postgres + psql -c "SELECT javatest.java_addone(3);" -U postgres +elif [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then + sudo setfacl -m u:postgres:rwx /home/travis/.m2/ + printf '%s\n' "SELECT sqlj.install_jar(:'path','saxon',true);" | sudo -u postgres /usr/local/pgsql/bin/psql -v path="$saxon_jar" -U postgres + sudo -u postgres /usr/local/pgsql/bin/psql -c "SELECT sqlj.set_classpath('public', 'saxon');" -U postgres + printf '%s\n' "SELECT sqlj.install_jar(:'path','examples',true);" | sudo -u postgres /usr/local/pgsql/bin/psql -v path="$examples_jar" -U postgres + sudo -u postgres /usr/local/pgsql/bin/psql -c "SELECT sqlj.get_classpath('javatest');" -U postgres + sudo -u postgres /usr/local/pgsql/bin/psql -c "SELECT sqlj.set_classpath('javatest', 'examples');" -U postgres + sudo -u postgres /usr/local/pgsql/bin/psql -c "SELECT javatest.java_addone(3);" -U postgres +else + sudo setfacl -m u:postgres:rwx /home/travis/.m2/ + printf '%s\n' "SELECT sqlj.install_jar(:'path','saxon',true);" | sudo -u postgres psql -v path="$saxon_jar" -U postgres + sudo -u postgres psql -c "SELECT sqlj.set_classpath('public', 'saxon');" -U postgres + printf '%s\n' "SELECT sqlj.install_jar(:'path','examples',true);" | sudo -u postgres psql -v path="$examples_jar" -U postgres 2> test.log + grep -w "WARNING" test.log + grep -w "ERROR" test.log + sudo -u postgres psql -c "SELECT sqlj.get_classpath('javatest');" -U postgres + sudo -u postgres psql -c "SELECT sqlj.set_classpath('javatest', 'examples');" -U postgres + sudo -u postgres psql -c "SELECT javatest.java_addone(3);" -U postgres +fi From b91ebd27fb1e4baeb5227aabdd8732e3c474112a Mon Sep 17 00:00:00 2001 From: amCap1712 Date: Sun, 5 Jul 2020 15:28:19 +0530 Subject: [PATCH 0634/1087] Setup AppVeyor CI (#2) Setup AppVeyor CI --- .appveyor/appveyor_download_java.ps1 | 20 +++++++ .appveyor/appveyor_mingw.sh | 7 +++ appveyor.yml | 89 ++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 .appveyor/appveyor_download_java.ps1 create mode 100644 .appveyor/appveyor_mingw.sh create mode 100644 appveyor.yml diff --git a/.appveyor/appveyor_download_java.ps1 b/.appveyor/appveyor_download_java.ps1 new file mode 100644 index 00000000..18eb2272 --- /dev/null +++ b/.appveyor/appveyor_download_java.ps1 @@ -0,0 +1,20 @@ +Import-Module BitsTransfer +Install-Package -Force 7Zip4Powershell -ProviderName PowerShellGet +if ($Env:JDK -eq 9) +{ + $url = "https://download.java.net/java/GA/jdk9/9.0.4/binaries/openjdk-9.0.4_windows-x64_bin.tar.gz" + $out = "C:\Program Files\Java\jdk9" + Start-BitsTransfer -Source $url -Destination "$out.tar.gz" + Expand-7Zip "$out.tar.gz" "$out" + Expand-7Zip "$out\jdk9.tar" "$out" + Move-Item "$out\jdk-9.0.4\*" "$out" +} +if ($Env:JDK -eq 10) +{ + $url = "https://download.java.net/java/GA/jdk10/10/binaries/openjdk-10_windows-x64_bin.tar.gz" + $out = "C:\Program Files\Java\jdk10" + Start-BitsTransfer -Source $url -Destination "$out.tar.gz" + Expand-7Zip "$out.tar.gz" "$out" + Expand-7Zip "$out\jdk10.tar" "$out" + Move-Item "$out\jdk-10\*" "$out" +} \ No newline at end of file diff --git a/.appveyor/appveyor_mingw.sh b/.appveyor/appveyor_mingw.sh new file mode 100644 index 00000000..f4b1535b --- /dev/null +++ b/.appveyor/appveyor_mingw.sh @@ -0,0 +1,7 @@ +set -e +JAVA_HOME="/c/Program Files/Java/jdk$1" +PATH=$JAVA_HOME/bin:$PATH +javac -version +pacman -S mingw-w64-x86_64-postgresql --noconfirm +cd /c/projects/pljava +mvn clean install -Dnar.cores=1 -Psaxon-examples -Pwnosign --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..2cf448b6 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,89 @@ +image: Visual Studio 2019 +environment: + APPVEYOR_RDP_PASSWORD: MrRobot@2020 + VCVARSALL: C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat + PGUSER: postgres + PGPASSWORD: Password12! + matrix: + - SYS: MINGW + JDK: 9 + PG: 12 + - SYS: MINGW + JDK: 10 + PG: 12 + - SYS: MINGW + JDK: 14 + PG: 12 + - SYS: MINGW + JDK: 13 + PG: 12 + - SYS: MINGW + JDK: 12 + PG: 12 + - SYS: MINGW + JDK: 11 + PG: 12 + - SYS: MSVC + JDK: 14 + PG: 12 + - SYS: MSVC + JDK: 13 + PG: 12 + - SYS: MSVC + JDK: 12 + PG: 12 + - SYS: MSVC + JDK: 11 + PG: 12 + - SYS: MSVC + JDK: 10 + PG: 12 + - SYS: MSVC + JDK: 9 + PG: 12 + - SYS: MSVC + JDK: 14 + PG: 11 + - SYS: MSVC + JDK: 14 + PG: 10 + - SYS: MSVC + JDK: 14 + PG: 9.6 +before_build: + - ps: .appveyor/./appveyor_download_java.ps1 + - set JAVA_HOME=%ProgramFiles%\Java\jdk%JDK% + - set PGROOT=%ProgramFiles%\PostgreSQL\%PG% + - path %JAVA_HOME%\bin;%PGROOT%\bin;%PATH% + - '"%VCVARSALL%" x86' + - '"%VCVARSALL%" amd64' + - ps: $Env:JAVA_HOME = "C:\Program Files\Java\jdk$Env:JDK" + - ps: $Env:PGROOT = "C:\Program Files\PostgreSQL\$Env:PG" + - ps: $Env:Path = "$Env:JAVA_HOME\bin;$Env:PGROOT\bin;" + $Env:Path + - ps: $Env:PGDATA = "$Env:PGROOT\data" + - ps: Start-Process -FilePath "$Env:PGROOT\bin\pg_ctl.exe" -ArgumentList "start" -RedirectStandardError "error.txt" +build_script: + - IF %SYS%==MINGW ( + C:\msys64\usr\bin\env MSYSTEM=MINGW64 C:\msys64\usr\bin\bash -l -c "/c/projects/pljava/.appveyor/appveyor_mingw.sh %JDK%") + ELSE ( + mvn clean install -Dnar.cores=1 -Psaxon-examples --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + ) +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) +before_test: + - ps: $libjvm = "$Env:JAVA_HOME\bin\server\jvm.dll" + - ps: $vmoptions= "-enableassertions:org.postgresql.pljava... -Xcheck:jni" + - ps: Get-ChildItem -Recurse -Path pljava-packaging -Filter pljava-pg*.jar | %{java -jar $_.fullName} + - ps: psql -c "SET pljava.libjvm_location to '$libjvm'; ALTER USER postgres SET pljava.libjvm_location to '$libjvm'; SELECT pg_reload_conf();" -U postgres + - ps: psql -c "SET pljava.vmoptions to '$vmoptions'; ALTER USER postgres SET pljava.vmoptions to '$vmoptions'; SELECT pg_reload_conf();" -U postgres + - ps: psql -c "CREATE EXTENSION pljava;" -U postgres +test_script: + - ps: $saxonJar = Get-ChildItem -Recurse -Path "$env:UserProfile\.m2\repository\net\sf\saxon\Saxon-HE\" -Filter Saxon-HE-*.jar | Select-Object -First 1 | %{$_.fullName} + - ps: $examplesJar = Get-ChildItem -Recurse -Path "pljava-examples" -Filter pljava-*.jar | %{$_.fullName} + - ps: $saxon = "file:$saxonJar" + - ps: $examples = "file:$examplesJar" + - ps: psql -c "SELECT sqlj.install_jar('$saxon','saxon',true);" -U postgres + - ps: psql -c "SELECT sqlj.set_classpath('public', 'saxon');" -U postgres + - ps: psql -c "SET client_min_messages TO WARNING; SELECT sqlj.install_jar('$examples','examples',true);" -U postgres + - ps: psql -c "SELECT sqlj.get_classpath('javatest');" -U postgres + - ps: psql -c "SELECT sqlj.set_classpath('javatest', 'examples');" -U postgres + - ps: psql -c "SELECT javatest.java_addone(3);" -U postgres \ No newline at end of file From 78fb7e01e6e73eacaec071d92a2d01bf4a491fb5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 18 Jul 2020 17:20:11 -0400 Subject: [PATCH 0635/1087] A first attempt at using Node, through PowerShell Note to self: the yml mustn't have leading tabs; expand them to spaces. What happens when an external command writes info on stderr (even innocuous progress info) is not pretty; PowerShell has a hyperliteral idea of what 'stderr' is for, and believes anything written there should be shown in red and converted to Error Records. The PowerShell 2>&1 operator, as much as it looks like the POSIX one, isn't a lot better. It does join the content into the output stream, where it no longer appears in red, but only after still trying to make Error Records out of it anyway. Discussion: https://help.appveyor.com/discussions/questions/48697 --- .appveyor/appveyor_mingw.sh | 2 +- appveyor.yml | 82 ++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/.appveyor/appveyor_mingw.sh b/.appveyor/appveyor_mingw.sh index f4b1535b..40e18a75 100644 --- a/.appveyor/appveyor_mingw.sh +++ b/.appveyor/appveyor_mingw.sh @@ -4,4 +4,4 @@ PATH=$JAVA_HOME/bin:$PATH javac -version pacman -S mingw-w64-x86_64-postgresql --noconfirm cd /c/projects/pljava -mvn clean install -Dnar.cores=1 -Psaxon-examples -Pwnosign --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ No newline at end of file +mvn clean install -Dnar.cores=1 -Psaxon-examples -Ppgjdbc-ng -Pwnosign --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn diff --git a/appveyor.yml b/appveyor.yml index 2cf448b6..a6f45408 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -60,30 +60,74 @@ before_build: - ps: $Env:JAVA_HOME = "C:\Program Files\Java\jdk$Env:JDK" - ps: $Env:PGROOT = "C:\Program Files\PostgreSQL\$Env:PG" - ps: $Env:Path = "$Env:JAVA_HOME\bin;$Env:PGROOT\bin;" + $Env:Path - - ps: $Env:PGDATA = "$Env:PGROOT\data" - - ps: Start-Process -FilePath "$Env:PGROOT\bin\pg_ctl.exe" -ArgumentList "start" -RedirectStandardError "error.txt" build_script: - IF %SYS%==MINGW ( C:\msys64\usr\bin\env MSYSTEM=MINGW64 C:\msys64\usr\bin\bash -l -c "/c/projects/pljava/.appveyor/appveyor_mingw.sh %JDK%") ELSE ( - mvn clean install -Dnar.cores=1 -Psaxon-examples --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + mvn clean install -Dnar.cores=1 -Psaxon-examples -Ppgjdbc-ng --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn ) # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) before_test: - - ps: $libjvm = "$Env:JAVA_HOME\bin\server\jvm.dll" - - ps: $vmoptions= "-enableassertions:org.postgresql.pljava... -Xcheck:jni" - - ps: Get-ChildItem -Recurse -Path pljava-packaging -Filter pljava-pg*.jar | %{java -jar $_.fullName} - - ps: psql -c "SET pljava.libjvm_location to '$libjvm'; ALTER USER postgres SET pljava.libjvm_location to '$libjvm'; SELECT pg_reload_conf();" -U postgres - - ps: psql -c "SET pljava.vmoptions to '$vmoptions'; ALTER USER postgres SET pljava.vmoptions to '$vmoptions'; SELECT pg_reload_conf();" -U postgres - - ps: psql -c "CREATE EXTENSION pljava;" -U postgres + - ps: | + $packageJar = 'pljava-packaging' | + Get-ChildItem -Recurse -Filter pljava-pg*.jar + $packageJar = $packageJar.fullName + java -jar $packageJar 2>&1 test_script: - - ps: $saxonJar = Get-ChildItem -Recurse -Path "$env:UserProfile\.m2\repository\net\sf\saxon\Saxon-HE\" -Filter Saxon-HE-*.jar | Select-Object -First 1 | %{$_.fullName} - - ps: $examplesJar = Get-ChildItem -Recurse -Path "pljava-examples" -Filter pljava-*.jar | %{$_.fullName} - - ps: $saxon = "file:$saxonJar" - - ps: $examples = "file:$examplesJar" - - ps: psql -c "SELECT sqlj.install_jar('$saxon','saxon',true);" -U postgres - - ps: psql -c "SELECT sqlj.set_classpath('public', 'saxon');" -U postgres - - ps: psql -c "SET client_min_messages TO WARNING; SELECT sqlj.install_jar('$examples','examples',true);" -U postgres - - ps: psql -c "SELECT sqlj.get_classpath('javatest');" -U postgres - - ps: psql -c "SELECT sqlj.set_classpath('javatest', 'examples');" -U postgres - - ps: psql -c "SELECT javatest.java_addone(3);" -U postgres \ No newline at end of file + - ps: | + $mavenRepo = "$env:UserProfile\.m2\repository" + $saxonVer = Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | + Get-ChildItem + $jdbcJar = Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | + Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar + $jdbcJar = $jdbcJar.fullName + @' + import static java.nio.file.Paths.get + import java.sql.Connection + import org.postgresql.pljava.packaging.Node + import static org.postgresql.pljava.packaging.Node.qp + + String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" + + Node n1 = Node.get_new_node("TestNode1") + + try ( + AutoCloseable t1 = n1.initialized_cluster(); + AutoCloseable t2 = n1.started_server(Map.of( + "client_min_messages", "info", + "pljava.vmoptions", vmopts, + "pljava.libjvm_location", + get(System.getProperty("java.home"), "bin", "server", "jvm.dll") + .toString() + )); + ) + { + try ( Connection c = n1.connect() ) + { + qp(c, "create extension pljava"); + } + + /* + * 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() ) + { + qp(Node.installSaxonAndExamplesAndPath(c, + System.getProperty("mavenRepo"), + System.getProperty("saxonVer"), + true)); + } + } + /exit + '@ | + jshell ` + --execution local ` + "-J--class-path=$packageJar;$jdbcJar" ` + "--class-path=$packageJar" ` + "-J--add-modules=java.sql,java.sql.rowset" ` + "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` + "-J-DmavenRepo=$mavenRepo" ` + "-J-DsaxonVer=$saxonVer" From 03eff027dfd3194c7b83cb3610bdb4570a9970e1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 16 Jul 2020 00:32:41 -0400 Subject: [PATCH 0636/1087] Try an external PowerShell script As suggested by Feodor Fitsner in [1], maybe the behavior of a separate script will be better than when the script is in the .yml file. The staff seems to be strangely cagey about exactly what's happening that makes this stuff not work. There are other threads on the same topic (e.g. [2]). "you can try playing with StdErr redirection ... but ... not always work" ?! Seriously? [1] https://help.appveyor.com/discussions/questions/48697#comment_48398341 [2] https://help.appveyor.com/discussions/problems/10014#comment_44099248 --- .appveyor/test_script.ps1 | 60 ++++++++++++++++++++++++++++++++++++ appveyor.yml | 64 +-------------------------------------- 2 files changed, 61 insertions(+), 63 deletions(-) create mode 100644 .appveyor/test_script.ps1 diff --git a/.appveyor/test_script.ps1 b/.appveyor/test_script.ps1 new file mode 100644 index 00000000..82d45da7 --- /dev/null +++ b/.appveyor/test_script.ps1 @@ -0,0 +1,60 @@ +$packageJar = 'pljava-packaging' | + Get-ChildItem -Recurse -Filter pljava-pg*.jar +$packageJar = $packageJar.fullName +java -jar $packageJar +$mavenRepo = "$env:UserProfile\.m2\repository" +$saxonVer = Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | + Get-ChildItem +$jdbcJar = Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | + Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar +$jdbcJar = $jdbcJar.fullName +@' +import static java.nio.file.Paths.get +import java.sql.Connection +import org.postgresql.pljava.packaging.Node +import static org.postgresql.pljava.packaging.Node.qp + +String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" + +Node n1 = Node.get_new_node("TestNode1") + +try ( + AutoCloseable t1 = n1.initialized_cluster(); + AutoCloseable t2 = n1.started_server(Map.of( + "client_min_messages", "info", + "pljava.vmoptions", vmopts, + "pljava.libjvm_location", + get(System.getProperty("java.home"), "bin", "server", "jvm.dll") + .toString() + )); +) +{ + try ( Connection c = n1.connect() ) + { + qp(c, "create extension pljava"); + } + + /* + * 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() ) + { + qp(Node.installSaxonAndExamplesAndPath(c, + System.getProperty("mavenRepo"), + System.getProperty("saxonVer"), + true)); + } +} +/exit +'@ | +jshell ` + -execution local ` + "-J--class-path=$packageJar;$jdbcJar" ` + "--class-path=$packageJar" ` + "-J--add-modules=java.sql,java.sql.rowset" ` + "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` + "-J-DmavenRepo=$mavenRepo" ` + "-J-DsaxonVer=$saxonVer" - diff --git a/appveyor.yml b/appveyor.yml index a6f45408..9b88f16c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -67,67 +67,5 @@ build_script: mvn clean install -Dnar.cores=1 -Psaxon-examples -Ppgjdbc-ng --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn ) # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -before_test: - - ps: | - $packageJar = 'pljava-packaging' | - Get-ChildItem -Recurse -Filter pljava-pg*.jar - $packageJar = $packageJar.fullName - java -jar $packageJar 2>&1 test_script: - - ps: | - $mavenRepo = "$env:UserProfile\.m2\repository" - $saxonVer = Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | - Get-ChildItem - $jdbcJar = Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | - Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar - $jdbcJar = $jdbcJar.fullName - @' - import static java.nio.file.Paths.get - import java.sql.Connection - import org.postgresql.pljava.packaging.Node - import static org.postgresql.pljava.packaging.Node.qp - - String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" - - Node n1 = Node.get_new_node("TestNode1") - - try ( - AutoCloseable t1 = n1.initialized_cluster(); - AutoCloseable t2 = n1.started_server(Map.of( - "client_min_messages", "info", - "pljava.vmoptions", vmopts, - "pljava.libjvm_location", - get(System.getProperty("java.home"), "bin", "server", "jvm.dll") - .toString() - )); - ) - { - try ( Connection c = n1.connect() ) - { - qp(c, "create extension pljava"); - } - - /* - * 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() ) - { - qp(Node.installSaxonAndExamplesAndPath(c, - System.getProperty("mavenRepo"), - System.getProperty("saxonVer"), - true)); - } - } - /exit - '@ | - jshell ` - --execution local ` - "-J--class-path=$packageJar;$jdbcJar" ` - "--class-path=$packageJar" ` - "-J--add-modules=java.sql,java.sql.rowset" ` - "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` - "-J-DmavenRepo=$mavenRepo" ` - "-J-DsaxonVer=$saxonVer" + - powershell .appveyor\test_script.ps1 From d3c9bd3ce81cfbf650d482f92de19a6763b9c710 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 16 Jul 2020 01:12:36 -0400 Subject: [PATCH 0637/1087] Does -File help PowerShell recognize a file? --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 9b88f16c..bbd957b9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -68,4 +68,4 @@ build_script: ) # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) test_script: - - powershell .appveyor\test_script.ps1 + - powershell -File .appveyor\test_script.ps1 From 2e5883393715dce4dc3bb1eebcbbdd70615365e8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 16 Jul 2020 01:30:05 -0400 Subject: [PATCH 0638/1087] Limit number of builds while flailing --- appveyor.yml | 78 ++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index bbd957b9..51f66ed2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,51 +5,51 @@ environment: PGUSER: postgres PGPASSWORD: Password12! matrix: - - SYS: MINGW - JDK: 9 - PG: 12 - - SYS: MINGW - JDK: 10 - PG: 12 - - SYS: MINGW - JDK: 14 - PG: 12 - - SYS: MINGW - JDK: 13 - PG: 12 - - SYS: MINGW - JDK: 12 - PG: 12 +# - SYS: MINGW +# JDK: 9 +# PG: 12 +# - SYS: MINGW +# JDK: 10 +# PG: 12 +# - SYS: MINGW +# JDK: 14 +# PG: 12 +# - SYS: MINGW +# JDK: 13 +# PG: 12 +# - SYS: MINGW +# JDK: 12 +# PG: 12 - SYS: MINGW JDK: 11 PG: 12 - - SYS: MSVC - JDK: 14 - PG: 12 - - SYS: MSVC - JDK: 13 - PG: 12 - - SYS: MSVC - JDK: 12 - PG: 12 +# - SYS: MSVC +# JDK: 14 +# PG: 12 +# - SYS: MSVC +# JDK: 13 +# PG: 12 +# - SYS: MSVC +# JDK: 12 +# PG: 12 - SYS: MSVC JDK: 11 PG: 12 - - SYS: MSVC - JDK: 10 - PG: 12 - - SYS: MSVC - JDK: 9 - PG: 12 - - SYS: MSVC - JDK: 14 - PG: 11 - - SYS: MSVC - JDK: 14 - PG: 10 - - SYS: MSVC - JDK: 14 - PG: 9.6 +# - SYS: MSVC +# JDK: 10 +# PG: 12 +# - SYS: MSVC +# JDK: 9 +# PG: 12 +# - SYS: MSVC +# JDK: 14 +# PG: 11 +# - SYS: MSVC +# JDK: 14 +# PG: 10 +# - SYS: MSVC +# JDK: 14 +# PG: 9.6 before_build: - ps: .appveyor/./appveyor_download_java.ps1 - set JAVA_HOME=%ProgramFiles%\Java\jdk%JDK% From 6a4611027a11c3cbc93e368728aaf0f0b1b0770a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 18 Jul 2020 17:16:06 -0400 Subject: [PATCH 0639/1087] Don't rely on PowerShell's redirection Because PowerShell makes such a mess of stderr output from an external command, just setIn(out), and use redirectErrorStream(true) when starting outside processes. Do the file extraction from within jshell just by calling main(), rather than having that done in a separate earlier step with java -jar. --- .appveyor/test_script.ps1 | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.appveyor/test_script.ps1 b/.appveyor/test_script.ps1 index 82d45da7..59c82660 100644 --- a/.appveyor/test_script.ps1 +++ b/.appveyor/test_script.ps1 @@ -1,32 +1,41 @@ $packageJar = 'pljava-packaging' | Get-ChildItem -Recurse -Filter pljava-pg*.jar + $packageJar = $packageJar.fullName -java -jar $packageJar + $mavenRepo = "$env:UserProfile\.m2\repository" + $saxonVer = Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | Get-ChildItem + $jdbcJar = Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar + $jdbcJar = $jdbcJar.fullName + @' import static java.nio.file.Paths.get import java.sql.Connection import org.postgresql.pljava.packaging.Node import static org.postgresql.pljava.packaging.Node.qp +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" Node n1 = Node.get_new_node("TestNode1") try ( - AutoCloseable t1 = n1.initialized_cluster(); + AutoCloseable t1 = n1.initialized_cluster(p->p.redirectErrorStream(true)); AutoCloseable t2 = n1.started_server(Map.of( "client_min_messages", "info", "pljava.vmoptions", vmopts, "pljava.libjvm_location", get(System.getProperty("java.home"), "bin", "server", "jvm.dll") .toString() - )); + ), p->p.redirectErrorStream(true)); ) { try ( Connection c = n1.connect() ) From 1f5190eb23e5fda405e49f0a50c40e8fc524ab7e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 16 Jul 2020 21:55:05 -0400 Subject: [PATCH 0640/1087] Will it run that far, put back in appveyor.yml? On the last run it got as far as postgres's "I refuse to run as an adminstrative user" check. It will take some more doing to get past that. There seem to be relevant AppVeyor threads in [1] and [2]. But anyway, meanwhile, can it be put back into appveyor.yml, doing away with the separate .ps1 file, and still succeed as far? [1] https://help.appveyor.com/discussions/questions/1888-running-tests-with-reduced-privileges [2] https://help.appveyor.com/discussions/questions/3530-is-it-possible-to-somehow-specify-the-usernamehostname-with-which-ci-tests-are-run --- .appveyor/test_script.ps1 | 69 ------------------------------------- appveyor.yml | 72 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 70 deletions(-) delete mode 100644 .appveyor/test_script.ps1 diff --git a/.appveyor/test_script.ps1 b/.appveyor/test_script.ps1 deleted file mode 100644 index 59c82660..00000000 --- a/.appveyor/test_script.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -$packageJar = 'pljava-packaging' | - Get-ChildItem -Recurse -Filter pljava-pg*.jar - -$packageJar = $packageJar.fullName - -$mavenRepo = "$env:UserProfile\.m2\repository" - -$saxonVer = Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | - Get-ChildItem - -$jdbcJar = Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | - Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar - -$jdbcJar = $jdbcJar.fullName - -@' -import static java.nio.file.Paths.get -import java.sql.Connection -import org.postgresql.pljava.packaging.Node -import static org.postgresql.pljava.packaging.Node.qp - -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" - -Node n1 = Node.get_new_node("TestNode1") - -try ( - AutoCloseable t1 = n1.initialized_cluster(p->p.redirectErrorStream(true)); - AutoCloseable t2 = n1.started_server(Map.of( - "client_min_messages", "info", - "pljava.vmoptions", vmopts, - "pljava.libjvm_location", - get(System.getProperty("java.home"), "bin", "server", "jvm.dll") - .toString() - ), p->p.redirectErrorStream(true)); -) -{ - try ( Connection c = n1.connect() ) - { - qp(c, "create extension pljava"); - } - - /* - * 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() ) - { - qp(Node.installSaxonAndExamplesAndPath(c, - System.getProperty("mavenRepo"), - System.getProperty("saxonVer"), - true)); - } -} -/exit -'@ | -jshell ` - -execution local ` - "-J--class-path=$packageJar;$jdbcJar" ` - "--class-path=$packageJar" ` - "-J--add-modules=java.sql,java.sql.rowset" ` - "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` - "-J-DmavenRepo=$mavenRepo" ` - "-J-DsaxonVer=$saxonVer" - diff --git a/appveyor.yml b/appveyor.yml index 51f66ed2..8ca64e0e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -68,4 +68,74 @@ build_script: ) # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) test_script: - - powershell -File .appveyor\test_script.ps1 + - ps: | + $packageJar = 'pljava-packaging' | + Get-ChildItem -Recurse -Filter pljava-pg*.jar + + $packageJar = $packageJar.fullName + + $mavenRepo = "$env:UserProfile\.m2\repository" + + $saxonVer = Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | + Get-ChildItem + + $jdbcJar = Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | + Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar + + $jdbcJar = $jdbcJar.fullName + + @' + import static java.nio.file.Paths.get + import java.sql.Connection + import org.postgresql.pljava.packaging.Node + import static org.postgresql.pljava.packaging.Node.qp + + 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" + + Node n1 = Node.get_new_node("TestNode1") + + try ( + AutoCloseable t1 = n1.initialized_cluster( + p->p.redirectErrorStream(true)); + AutoCloseable t2 = n1.started_server(Map.of( + "client_min_messages", "info", + "pljava.vmoptions", vmopts, + "pljava.libjvm_location", + get(System.getProperty("java.home"), "bin", "server", "jvm.dll") + .toString() + ), p->p.redirectErrorStream(true)); + ) + { + try ( Connection c = n1.connect() ) + { + qp(c, "create extension pljava"); + } + + /* + * 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() ) + { + qp(Node.installSaxonAndExamplesAndPath(c, + System.getProperty("mavenRepo"), + System.getProperty("saxonVer"), + true)); + } + } + /exit + '@ | + jshell ` + -execution local ` + "-J--class-path=$packageJar;$jdbcJar" ` + "--class-path=$packageJar" ` + "-J--add-modules=java.sql,java.sql.rowset" ` + "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` + "-J-DmavenRepo=$mavenRepo" ` + "-J-DsaxonVer=$saxonVer" - From eff24983379a4c36ad27852547a0fd4ad708328b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 18 Jul 2020 20:42:09 -0400 Subject: [PATCH 0641/1087] Try using pg_ctl to start/stop the server This may allow the server to start under AppVeyor's testing account, which has admin privileges; pg_ctl has a Windows-specific ability to drop those. --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 8ca64e0e..b24a6f2a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -98,6 +98,8 @@ test_script: Node n1 = Node.get_new_node("TestNode1") + n1.use_pg_ctl(true) + try ( AutoCloseable t1 = n1.initialized_cluster( p->p.redirectErrorStream(true)); From e2c231b9baf94f5d236a7ce1ab86d6ad62805d67 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Jul 2020 22:21:03 -0400 Subject: [PATCH 0642/1087] Why would an isolated build download >1 Saxon? --- appveyor.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index b24a6f2a..f5e677b9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -69,20 +69,20 @@ build_script: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) test_script: - ps: | - $packageJar = 'pljava-packaging' | + $packageJar = ('pljava-packaging' | Get-ChildItem -Recurse -Filter pljava-pg*.jar - - $packageJar = $packageJar.fullName + ).FullName $mavenRepo = "$env:UserProfile\.m2\repository" - $saxonVer = Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | - Get-ChildItem + $saxonVer = (Join-Path $mavenRepo "net\sf\saxon\Saxon-HE" | + Get-ChildItem -Recurse -Filter Saxon-HE-*.jar | + Select-Object -Last 1 + ).Directory.Name - $jdbcJar = Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | + $jdbcJar = (Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar - - $jdbcJar = $jdbcJar.fullName + ).FullName @' import static java.nio.file.Paths.get From 23e8acb4a8c8f748caf208a93f08646c02c64499 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Aug 2020 17:38:06 -0400 Subject: [PATCH 0643/1087] Use Node.dfa to confirm expected results ... and arrange for jshell to exit with nonzero status as appropriate. In jshell 10 and later, /exit can take a value, but in 9 it can't, so System.exit must be used (and works ok in this setting with --execution local). An uncaught throwable (1) doesn't result in a nonzero exit, or any exit at all, and (2) gets printed more concisely than it would be by printStackTrace. So in the catch block, just record that it happened, and rethrow it. --- appveyor.yml | 88 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f5e677b9..2f5f462b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -85,10 +85,14 @@ test_script: ).FullName @' + boolean succeeding = false; // begin pessimistic + import static java.nio.file.Paths.get import java.sql.Connection import org.postgresql.pljava.packaging.Node - import static org.postgresql.pljava.packaging.Node.qp + import static org.postgresql.pljava.packaging.Node.q + import static org.postgresql.pljava.packaging.Node.dfa + import static org.postgresql.pljava.packaging.Node.isVoidResultSet System.setErr(System.out); // PowerShell makes a mess of stderr output @@ -100,6 +104,30 @@ test_script: 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]; + results.compute(type, (k,v) -> 1 + v); + if ( whatIsNG.contains(type) ) + results.compute("ng", (k,v) -> 1 + v); + return true; + } + + succeeding = true; // become optimistic, will be using &= below + try ( AutoCloseable t1 = n1.initialized_cluster( p->p.redirectErrorStream(true)); @@ -114,7 +142,20 @@ test_script: { try ( Connection c = n1.connect() ) { - qp(c, "create extension pljava"); + succeeding &= dfa( + "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 + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + + // state 2: must be end of input + (o,p,q) -> null == o + ); } /* @@ -125,13 +166,46 @@ test_script: */ try ( Connection c = n1.connect() ) { - qp(Node.installSaxonAndExamplesAndPath(c, - System.getProperty("mavenRepo"), - System.getProperty("saxonVer"), - true)); + succeeding &= dfa( + "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 + ); } + } catch ( Throwable t ) + { + succeeding = false; + throw t; } - /exit + + System.out.println(results); + succeeding &= (0 == results.get("ng")); + System.exit(succeeding ? 0 : 1) '@ | jshell ` -execution local ` From 0991f313cdd0cbf7eef81ce3d594dcf565c59805 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 24 Jul 2020 20:58:41 -0400 Subject: [PATCH 0644/1087] A first attempt at using Node.class in Travis --- .travis.yml | 98 ++++++++++++++++++++++++++++-- .travis/travis_setup_pljava.sh | 29 --------- .travis/travis_setup_postgresql.sh | 19 ------ 3 files changed, 92 insertions(+), 54 deletions(-) delete mode 100755 .travis/travis_setup_pljava.sh delete mode 100755 .travis/travis_setup_postgresql.sh diff --git a/.travis.yml b/.travis.yml index 3fbbf5e0..4075e34c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,97 @@ cache: before_install: - . .travis/travis_install_postgresql.sh - . .travis/travis_install_openssl.sh -install: mvn clean install -Dnar.cores=1 -Psaxon-examples -Pwnosign --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -script: - - .travis/travis_setup_postgresql.sh - - .travis/travis_setup_pljava.sh - - .travis/travis_test_pljava.sh + +install: | + mvn clean install --batch-mode \ + -Dnar.cores=1 -Pwnosign -Psaxon-examples -Ppgjdbc-ng \ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + +script: | + if [[ "$POSTGRESQL_VERSION" == "SOURCE" ]]; then + pgConfig=/usr/local/pgsql/bin/pg_config + else + pgConfig=pg_config + fi + + 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 + ) + + sudo java -Dpgconfig="$pgConfig" -jar "$packageJar" + + jshell \ + -execution local \ + "-J--class-path=$packageJar:$jdbcJar" \ + "--class-path=$packageJar" \ + "-J--add-modules=java.sql,java.sql.rowset" \ + "-J-Dpgconfig=$pgConfig" \ + "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" \ + "-J-DmavenRepo=$mavenRepo" \ + "-J-DsaxonVer=$saxonVer" - <<\ENDJSHELL + + import static java.nio.file.Paths.get + import java.sql.Connection + import org.postgresql.pljava.packaging.Node + import static org.postgresql.pljava.packaging.Node.qp + + Path javaLibDir = get(System.getProperty("java.home"), "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("lib/server/libjvm.so") + ); + + String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" + + Node n1 = Node.get_new_node("TestNode1") + + 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() ) + { + qp(c, "create extension pljava"); + } + + /* + * 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() ) + { + qp(Node.installSaxonAndExamplesAndPath(c, + System.getProperty("mavenRepo"), + System.getProperty("saxonVer"), + true)); + } + } + /exit + ENDJSHELL + jobs: include: - os: linux @@ -46,4 +132,4 @@ jobs: env: POSTGRESQL_VERSION=9.5 - os: linux jdk: openjdk14 - env: POSTGRESQL_VERSION=SOURCE \ No newline at end of file + env: POSTGRESQL_VERSION=SOURCE diff --git a/.travis/travis_setup_pljava.sh b/.travis/travis_setup_pljava.sh deleted file mode 100755 index 25307837..00000000 --- a/.travis/travis_setup_pljava.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -if [ "$TRAVIS_OS_NAME" = "osx" ]; then - libjvm_path=$(find "$JAVA_HOME" -mindepth 2 -name "libjli.dylib" | head -n 1) -else - libjvm_path=$(find "$JAVA_HOME" -mindepth 2 -name "libjvm.so" | head -n 1) -fi -echo $libjvm_path -vmoptions='-enableassertions:org.postgresql.pljava... -Xcheck:jni' - -if [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then - find pljava-packaging -name "pljava-pg*.jar" | sudo xargs java -jar -Dpgconfig="/usr/local/pgsql/bin/pg_config" -else - find pljava-packaging -name "pljava-pg*.jar" | sudo xargs java -jar -fi - -if [ "$TRAVIS_OS_NAME" = "osx" ]; then - printf '%s\n' "SET pljava.libjvm_location TO :'path'; ALTER SYSTEM SET pljava.libjvm_location TO :'path'; SELECT pg_reload_conf();" | psql -v path="$libjvm_path" -U postgres - printf '%s\n' "SET pljava.vmoptions TO :'options'; ALTER SYSTEM SET pljava.vmoptions TO :'options'; SELECT pg_reload_conf();" | psql -v options="$vmoptions" -U postgres - psql -c "CREATE EXTENSION pljava;" -U postgres -elif [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then - printf '%s\n' "SET pljava.libjvm_location TO :'path'; ALTER SYSTEM SET pljava.libjvm_location TO :'path'; SELECT pg_reload_conf();" | sudo -u postgres /usr/local/pgsql/bin/psql -v path="$libjvm_path" -U postgres - printf '%s\n' "SET pljava.vmoptions TO :'options'; ALTER SYSTEM SET pljava.vmoptions TO :'options'; SELECT pg_reload_conf();" | sudo -u postgres /usr/local/pgsql/bin/psql -v options="$vmoptions" -U postgres - sudo -u postgres /usr/local/pgsql/bin/psql -c "CREATE EXTENSION pljava;" -U postgres -else - printf '%s\n' "SET pljava.libjvm_location TO :'path'; ALTER SYSTEM SET pljava.libjvm_location TO :'path'; SELECT pg_reload_conf();" | sudo -u postgres psql -v path="$libjvm_path" -U postgres - printf '%s\n' "SET pljava.vmoptions TO :'options'; ALTER SYSTEM SET pljava.vmoptions TO :'options'; SELECT pg_reload_conf();" | sudo -u postgres psql -v options="$vmoptions" -U postgres - sudo -u postgres psql -c "CREATE EXTENSION pljava;" -U postgres -fi diff --git a/.travis/travis_setup_postgresql.sh b/.travis/travis_setup_postgresql.sh deleted file mode 100755 index 993f4c75..00000000 --- a/.travis/travis_setup_postgresql.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -if [ "$TRAVIS_OS_NAME" = "osx" ]; then - export PGDATA="${HOME}/postgres" - - initdb -D "$PGDATA" - pg_ctl -w -t 300 -c -o '-p 5432' -l /tmp/postgres.log start - wait - sleep 5 && createuser -s postgres -else - if [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then - export PGDATA="/home/travis/postgres" - /usr/local/pgsql/bin/pg_ctl -D ${PGDATA} -U postgres initdb - /usr/local/pgsql/bin/pg_ctl -D ${PGDATA} -w -t 300 -c -o '-p 5432' -l /tmp/postgres.log start - wait - sleep 5 && createuser -s postgres - else - sudo service postgresql start ${POSTGRESQL_VERSION} - fi -fi From 71b3f85326d73d5ca7e91cdd3dcc411f5af59dc9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 25 Jul 2020 21:51:16 -0400 Subject: [PATCH 0645/1087] What's being passed when driver's not found? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4075e34c..d3dbdcf8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ script: | ) sudo java -Dpgconfig="$pgConfig" -jar "$packageJar" - + set -x jshell \ -execution local \ "-J--class-path=$packageJar:$jdbcJar" \ From 4356d94a318cb2259ae5e1312baf3348701729d1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 25 Jul 2020 22:09:52 -0400 Subject: [PATCH 0646/1087] The multiple-dependency-download thing again --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d3dbdcf8..80bfea37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,11 +46,13 @@ script: | jdbcJar=$( find "$mavenRepo/com/impossibl/pgjdbc-ng/pgjdbc-ng-all" \ - -name 'pgjdbc-ng-all-*.jar' -print + -name 'pgjdbc-ng-all-*.jar' -print | + sort | + tail -n 1 ) sudo java -Dpgconfig="$pgConfig" -jar "$packageJar" - set -x + jshell \ -execution local \ "-J--class-path=$packageJar:$jdbcJar" \ From 293b97d5fa59d2ddd145d804b7cc9d02ee78f380 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 25 Jul 2020 22:23:11 -0400 Subject: [PATCH 0647/1087] One lib/ too many --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 80bfea37..d1cc8181 100644 --- a/.travis.yml +++ b/.travis.yml @@ -74,7 +74,7 @@ script: | "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("lib/server/libjvm.so") + : javaLibDir.resolve("server/libjvm.so") ); String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" From 35c31740ef7cf939dc5140978d9a9bbcef8bb315 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Aug 2020 17:40:55 -0400 Subject: [PATCH 0648/1087] Use Node.dfa to confirm expected results ... and arrange for jshell to exit with nonzero status as appropriate. In jshell 10 and later, /exit can take a value, but in 9 it can't, so System.exit must be used (and works ok in this setting with --execution local). An uncaught throwable (1) doesn't result in a nonzero exit, or any exit at all, and (2) gets printed more concisely than it would be by printStackTrace. So in the catch block, just record that it happened, and rethrow it. --- .travis.yml | 88 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index d1cc8181..43979001 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,10 +63,14 @@ script: | "-J-DmavenRepo=$mavenRepo" \ "-J-DsaxonVer=$saxonVer" - <<\ENDJSHELL + boolean succeeding = false; // begin pessimistic + import static java.nio.file.Paths.get import java.sql.Connection import org.postgresql.pljava.packaging.Node - import static org.postgresql.pljava.packaging.Node.qp + import static org.postgresql.pljava.packaging.Node.q + import static org.postgresql.pljava.packaging.Node.dfa + import static org.postgresql.pljava.packaging.Node.isVoidResultSet Path javaLibDir = get(System.getProperty("java.home"), "lib") @@ -81,6 +85,30 @@ script: | 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]; + results.compute(type, (k,v) -> 1 + v); + if ( whatIsNG.contains(type) ) + results.compute("ng", (k,v) -> 1 + v); + return true; + } + + succeeding = true; // become optimistic, will be using &= below + try ( AutoCloseable t1 = n1.initialized_cluster(); AutoCloseable t2 = n1.started_server(Map.of( @@ -92,7 +120,20 @@ script: | { try ( Connection c = n1.connect() ) { - qp(c, "create extension pljava"); + succeeding &= dfa( + "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 + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + + // state 2: must be end of input + (o,p,q) -> null == o + ); } /* @@ -103,13 +144,46 @@ script: | */ try ( Connection c = n1.connect() ) { - qp(Node.installSaxonAndExamplesAndPath(c, - System.getProperty("mavenRepo"), - System.getProperty("saxonVer"), - true)); + succeeding &= dfa( + "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 + ); } + } catch ( Throwable t ) + { + succeeding = false; + throw t; } - /exit + + System.out.println(results); + succeeding &= (0 == results.get("ng")); + System.exit(succeeding ? 0 : 1) ENDJSHELL jobs: From 546f26b87309527a407c90ead1e91f4a4f2904d7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Aug 2020 10:51:23 -0400 Subject: [PATCH 0649/1087] Don't be thrown by multiple pgjdbc-ng versions This change was needed in the travis config (4356d94) so assume it could be needed here also. --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 2f5f462b..124823f6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -81,7 +81,8 @@ test_script: ).Directory.Name $jdbcJar = (Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | - Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar + Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar | + Select-Object -Last 1 ).FullName @' From 5ddaf5a0b8571e22d32442a269a641b2223e99cc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Aug 2020 11:22:20 -0400 Subject: [PATCH 0650/1087] Revert 'limit number of builds while flailing' annnd we'll see what happens. --- appveyor.yml | 78 ++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 124823f6..530370f1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,51 +5,51 @@ environment: PGUSER: postgres PGPASSWORD: Password12! matrix: -# - SYS: MINGW -# JDK: 9 -# PG: 12 -# - SYS: MINGW -# JDK: 10 -# PG: 12 -# - SYS: MINGW -# JDK: 14 -# PG: 12 -# - SYS: MINGW -# JDK: 13 -# PG: 12 -# - SYS: MINGW -# JDK: 12 -# PG: 12 + - SYS: MINGW + JDK: 9 + PG: 12 + - SYS: MINGW + JDK: 10 + PG: 12 + - SYS: MINGW + JDK: 14 + PG: 12 + - SYS: MINGW + JDK: 13 + PG: 12 + - SYS: MINGW + JDK: 12 + PG: 12 - SYS: MINGW JDK: 11 PG: 12 -# - SYS: MSVC -# JDK: 14 -# PG: 12 -# - SYS: MSVC -# JDK: 13 -# PG: 12 -# - SYS: MSVC -# JDK: 12 -# PG: 12 + - SYS: MSVC + JDK: 14 + PG: 12 + - SYS: MSVC + JDK: 13 + PG: 12 + - SYS: MSVC + JDK: 12 + PG: 12 - SYS: MSVC JDK: 11 PG: 12 -# - SYS: MSVC -# JDK: 10 -# PG: 12 -# - SYS: MSVC -# JDK: 9 -# PG: 12 -# - SYS: MSVC -# JDK: 14 -# PG: 11 -# - SYS: MSVC -# JDK: 14 -# PG: 10 -# - SYS: MSVC -# JDK: 14 -# PG: 9.6 + - SYS: MSVC + JDK: 10 + PG: 12 + - SYS: MSVC + JDK: 9 + PG: 12 + - SYS: MSVC + JDK: 14 + PG: 11 + - SYS: MSVC + JDK: 14 + PG: 10 + - SYS: MSVC + JDK: 14 + PG: 9.6 before_build: - ps: .appveyor/./appveyor_download_java.ps1 - set JAVA_HOME=%ProgramFiles%\Java\jdk%JDK% From b556722af97fbd34b13ca2845d26e585c53e9f0a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Aug 2020 11:49:02 -0400 Subject: [PATCH 0651/1087] Skip Windows JDK9 builds jshell 9 did not yet understand the file name - to mean stdin, read non-interactively. On Linux, that just means it complains about the file name - and then reads from stdin anyway, printing all prompts and input as if it were interactive, which looks goofy but otherwise works. On Windows, it seems to complain about the file name - and then sit forever waiting for nonexistent console input. A JDK 9 Windows test would probably require writing the jshell script into a temp file and putting that name on the jshell command line. --- appveyor.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 530370f1..a388f051 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,9 +5,9 @@ environment: PGUSER: postgres PGPASSWORD: Password12! matrix: - - SYS: MINGW - JDK: 9 - PG: 12 +# - SYS: MINGW +# JDK: 9 +# PG: 12 - SYS: MINGW JDK: 10 PG: 12 @@ -38,9 +38,9 @@ environment: - SYS: MSVC JDK: 10 PG: 12 - - SYS: MSVC - JDK: 9 - PG: 12 +# - SYS: MSVC +# JDK: 9 +# PG: 12 - SYS: MSVC JDK: 14 PG: 11 From 124ae8025c40786bf97686b9ff2d72e83aacfbc7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Aug 2020 17:38:53 -0400 Subject: [PATCH 0652/1087] Avoid spurious failure when using pg_ctl The test for whether the server process has exited during the ready wait must not use the passed Process p; that's just the pg_ctl process, and would be expected to have exited. --- pljava-packaging/src/main/java/Node.java | 31 +++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 5f340cc9..2f182b38 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -92,6 +92,7 @@ import java.util.concurrent.Callable; // like a Supplier but allows exceptions! import static java.util.concurrent.TimeUnit.MILLISECONDS; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -2145,6 +2146,25 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) } ); + /* + * The isAlive check is a simple check on p in the m_usePostgres case. + * Otherwise, p is the pg_ctl process and probably has exited already; + * the handle assigned to m_serverHandle must be checked. If no handle + * has been assigned yet, just assume alive. The prospect of an + * unbounded wait (server process exiting before its pid could be + * collected from the pid file) should not be realizable, as long as + * pg_ctl itself waits long enough for the file to be present. + */ + BooleanSupplier isAlive = + m_usePostgres + ? (() -> p.isAlive()) + : (() -> null != m_serverHandle ? m_serverHandle.isAlive() : true); + + if ( ! m_usePostgres ) + if ( 0 != p.waitFor() ) + throw new IllegalStateException( + "pg_ctl exited with status " + p.exitValue()); + /* * Initialize a watch service just in case the postmaster.pid file * isn't there or has the wrong contents when we first look, @@ -2188,10 +2208,15 @@ && waitPrePG10() ) */ for ( ;; ) { - if ( ! p.isAlive() ) + if ( ! isAlive.getAsBoolean() ) throw new IllegalStateException( - "Server process exited while awaiting \"ready\" " + - "with status " + p.exitValue()); + "Server process exited while awaiting \"ready\"" + + ( + m_usePostgres + ? " with status " + p.exitValue() + : "" + ) + ); WatchKey k = watcher.poll(250, MILLISECONDS); if ( interrupted() ) throw new InterruptedException(); From 319de27bab0312fe92fbdadd421afe4a3b84227a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Aug 2020 17:40:55 -0400 Subject: [PATCH 0653/1087] Make sure MinGW tests run the MinGW PostgreSQL I had been wondering about the server startup log entries mentioning MSVC. --- appveyor.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index a388f051..a1d947e0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -69,6 +69,12 @@ build_script: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) test_script: - ps: | + if ($Env:SYS -eq 'MINGW') { + $pgConfig = 'C:\msys64\mingw64\bin\pg_config' + } else { + $pgConfig = 'pg_config' + } + $packageJar = ('pljava-packaging' | Get-ChildItem -Recurse -Filter pljava-pg*.jar ).FullName @@ -214,5 +220,6 @@ test_script: "--class-path=$packageJar" ` "-J--add-modules=java.sql,java.sql.rowset" ` "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` + "-J-Dpgconfig=$pgConfig" ` "-J-DmavenRepo=$mavenRepo" ` "-J-DsaxonVer=$saxonVer" - From 756f6f39fd21246e6f981ba3913dfb628c397d5e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Aug 2020 23:56:32 -0400 Subject: [PATCH 0654/1087] Don't try to exec MinGW's initdb bash script MinGW-w64 packaging of PostgreSQL seems to require a rather platform-specific wart added in Node.java. --- pljava-packaging/src/main/java/Node.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 2f182b38..66f3c029 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -43,6 +43,7 @@ import static java.nio.file.Files.createTempFile; import static java.nio.file.Files.createTempDirectory; import static java.nio.file.Files.deleteIfExists; +import static java.nio.file.Files.exists; import static java.nio.file.Files.getLastModifiedTime; import static java.nio.file.Files.lines; import static java.nio.file.Files.walk; @@ -590,6 +591,21 @@ public void init( * that expands keys like pljava/bindir to pg_config --bindir output. */ String initdb = resolve("pljava/bindir/initdb"); + + if ( s_isWindows ) + { + /* + * This is irksome. The mingw64 postgresql package has both + * initdb.exe and initdb, a bash script that runs it under winpty. + * If the script were not there, the .exe suffix would be added + * implicitly, but with both there, we try to exec the bash script. + */ + Path p1 = Paths.get(initdb); + Path p2 = Paths.get(initdb + ".exe"); + if ( exists(p1) && exists(p2) ) + initdb = p2.toString(); + } + Path pwfile = createTempFile(m_basedir, "pw", ""); Map options = new HashMap<>(suppliedOptions); From b6f56d707a6094ac379c1cbca8ec362261c08dd0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 24 Aug 2020 19:10:46 -0400 Subject: [PATCH 0655/1087] Pre-PG10, the SHMEM_KEY line wasn't on Windows Expecting it was causing wait_for_pid_file to hang on 9.6 Windows startup. https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/miscadmin.h;hb=8c55244#l444 --- pljava-packaging/src/main/java/Node.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 66f3c029..415f9270 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -2210,7 +2210,12 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) && PM_STATUS_READY.equals( status[LOCK_FILE_LINE_PM_STATUS - 1]) ) return; - if ( (status.length == LOCK_FILE_LINE_SHMEM_KEY) + if ( + ( + status.length == LOCK_FILE_LINE_SHMEM_KEY + || s_isWindows + && status.length == LOCK_FILE_LINE_LISTEN_ADDR + ) && checkPid.test(status) && waitPrePG10() ) return; From 159073192089f544fe5f9d9e0babb3fae612ee2b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 25 Aug 2020 20:32:58 -0400 Subject: [PATCH 0656/1087] Work around the pg_ctl race condition on Windows There is a race condition known for pg_ctl on Windows: https://www.postgresql.org/message-id/16922.1520722108%40sss.pgh.pa.us The same issue can also affect Java's deleteIfExists method and cause an AccessDeniedException during clean_node() here. Workarounds needed: - if ProcessBuilder tweaks were applied when starting the server, and pg_ctl is used, apply the same tweaks when running pg_ctl stop. That way, if a tweak was used to merge stderr into stdout and avoid the mess PowerShell makes of stderr, the mess will also be avoided for the message written by pg_ctl when tripped by this race. - if pg_ctl stop exits with a nonzero status, wait a bit and then check that the server has gone away; if it has, consider the exit status benign. - in clean_node(), if AccessDeniedException is caught when trying to delete postmaster.pid, wait a bit and try to delete it again. --- pljava-packaging/src/main/java/Node.java | 78 ++++++++++++++++++++---- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 415f9270..5a0392d2 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -55,6 +55,7 @@ import java.nio.file.WatchKey; import java.nio.file.WatchService; +import java.nio.file.AccessDeniedException; import java.nio.file.NoSuchFileException; import java.sql.Connection; @@ -430,7 +431,23 @@ public void clean_node(boolean keepRoot) throws Exception for ( Path p : (Iterable)walk(m_basedir)::iterator ) { while ( ! stk.isEmpty() && ! p.startsWith(stk.peek()) ) - deleteIfExists(stk.pop()); + { + Path toDelete = stk.pop(); + try + { + deleteIfExists(toDelete); + } + catch ( AccessDeniedException e ) + { + if (!toDelete.equals(data_dir().resolve("postmaster.pid"))) + throw e; + /* + * See comments for stopViaPgCtl regarding this weirdness. + */ + Thread.sleep(500); + deleteIfExists(toDelete); + } + } stk.push(p); } if ( keepRoot ) @@ -635,10 +652,10 @@ public void init( new ProcessBuilder(args) .redirectOutput(INHERIT) .redirectError(INHERIT); - tweaks.apply(pb); + pb = tweaks.apply(pb); if ( s_isWindows ) - forWindowsCRuntime(pb); + pb = forWindowsCRuntime(pb); Process p = pb.start(); p.getOutputStream().close(); @@ -685,6 +702,10 @@ public AutoCloseable started_server(UnaryOperator tweaks) /** * Like {@code start()} but returns an {@code AutoCloseable} that will * stop the server on the exit of a calling try-with-resources scope. + *

    + * Supplied tweaks will be applied to the {@code ProcessBuilder} + * used to start the server; if {@code pg_ctl} is being used, they will also + * be applied when running {@code pg_ctl stop} to stop it. */ public AutoCloseable started_server( Map suppliedOptions, @@ -694,7 +715,7 @@ public AutoCloseable started_server( start(suppliedOptions, tweaks); return () -> { - stop(); + stop(tweaks); }; } @@ -797,12 +818,12 @@ public void start( .redirectError(INHERIT); if ( ! m_usePostgres ) - asPgCtlInvocation(pb); + pb = asPgCtlInvocation(pb); - tweaks.apply(pb); + pb = tweaks.apply(pb); if ( s_isWindows ) - forWindowsCRuntime(pb); + pb = forWindowsCRuntime(pb); Process p = pb.start(); p.getOutputStream().close(); @@ -821,20 +842,34 @@ public void start( } } + + /** + * Stop the server instance associated with this Node. + *

    + * Has the effect of {@link #stop(UnaryOperator) stop(tweaks)} without + * any tweaks. + */ + public void stop() throws Exception + { + stop(UnaryOperator.identity()); + } + /** * Stop the server instance associated with this Node. *

    * No effect if it has not been started or has already been stopped, but * a message to standard error is logged if the server had been started and * the process is found to have exited unexpectedly. + * @param tweaks tweaks to apply to a ProcessBuilder; unused unless pg_ctl + * will be used to stop the server */ - public void stop() throws Exception + public void stop(UnaryOperator tweaks) throws Exception { if ( null == ( m_usePostgres ? m_server : m_serverHandle ) ) return; if ( ! m_usePostgres ) { - stopViaPgCtl(); + stopViaPgCtl(tweaks); return; } if ( m_server.isAlive() ) @@ -849,7 +884,8 @@ public void stop() throws Exception m_server = null; } - private void stopViaPgCtl() throws Exception + private void stopViaPgCtl(UnaryOperator tweaks) + throws Exception { if ( ! m_serverHandle.isAlive() ) { @@ -863,11 +899,29 @@ private void stopViaPgCtl() throws Exception pg_ctl, "stop", "-D", data_dir().toString(), "-m", "fast") .redirectOutput(INHERIT) .redirectError(INHERIT); + pb = tweaks.apply(pb); Process p = pb.start(); p.getOutputStream().close(); + if ( 0 != p.waitFor() ) - throw new AssertionError( - "Nonzero pg_ctl stop result: " + p.waitFor()); + { + /* + * Here is a complication. On Windows, pg_ctl suffers from a race + * condition that can occasionally cause it to exit with a nonzero + * status and a "permission denied" message about postmaster.pid, + * while the server is otherwise successfully stopped: + * www.postgresql.org/message-id/16922.1520722108%40sss.pgh.pa.us + * + * Without capturing the stderr of the process (too much bother), we + * won't know for sure if that is the message, but if the exit value + * was nonzero, just wait a bit and see if the server has gone away; + * if it has, don't worry about it. + */ + Thread.sleep(1000); + if ( m_serverHandle.isAlive() ) + throw new AssertionError( + "Nonzero pg_ctl stop result: " + p.waitFor()); + } m_serverHandle = null; } From 2c7402436ffd30760d335ed6f03b279205c911d2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 25 Aug 2020 21:43:55 -0400 Subject: [PATCH 0657/1087] Update maven-project-info-reports to 3.1.0 Continuing in the vein of the recent updates in 6fabe39. 3.1.0 reduces the volume of the overwhelming useless stack traces described in https://issues.apache.org/jira/browse/MPIR-373 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 420db23c..a63b9ef8 100644 --- a/pom.xml +++ b/pom.xml @@ -232,7 +232,7 @@ org.apache.maven.plugins maven-project-info-reports-plugin - 3.0.0 + 3.1.0 From 9cdbd5ea212a3fd1839bae7b34c2226c14de52eb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 26 Aug 2020 11:54:40 -0400 Subject: [PATCH 0658/1087] Ensure lines(pidfile) will be closed There is an API node for Files.lines() that I had overlooked. --- pljava-packaging/src/main/java/Node.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 5a0392d2..264c0d66 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -2258,7 +2258,11 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) * for it to change will be the right thing to do. */ - String[] status = lines(pidfile).toArray(String[]::new); + String[] status; + try ( Stream lines = lines(pidfile) ) + { + status = lines.toArray(String[]::new); + } if ( (status.length == LOCK_FILE_LINE_PM_STATUS) && checkPid.test(status) && PM_STATUS_READY.equals( From 9d6e1e0c5a141dedfb6c8d00c99cedd6942635b3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 26 Aug 2020 11:56:20 -0400 Subject: [PATCH 0659/1087] Allow different granularity file/process times With OS X and Java 9 there could be a frequent hang because the modification time on postmaster.pid appeared truncated to seconds, and would look earlier than the process start time, which wasn't. As the time check is just a heuristic, add a second to the file time before comparing. Caught with some logic to detect cycles in the wait_for_pid_file routine, which will be left in place as a hedge against future similar puzzles. --- pljava-packaging/src/main/java/Node.java | 32 +++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 264c0d66..6f5dda11 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -92,6 +92,7 @@ import static java.util.Spliterators.spliteratorUnknownSize; import java.util.concurrent.Callable; // like a Supplier but allows exceptions! +import java.util.concurrent.CancellationException; import static java.util.concurrent.TimeUnit.MILLISECONDS; import java.util.function.BooleanSupplier; @@ -2230,10 +2231,28 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) ? (() -> p.isAlive()) : (() -> null != m_serverHandle ? m_serverHandle.isAlive() : true); + StringBuilder tracepoints = new StringBuilder(); + Matcher dejavu = compile("(.+?)(?:\\1){16,}").matcher(tracepoints); + Consumer trace = c -> + { + tracepoints.insert(0, c); + if ( ! dejavu.reset().lookingAt() ) + return; + tracepoints.reverse(); + String preamble = + tracepoints.substring(0, tracepoints.length() - dejavu.end()); + String cycle = + tracepoints.substring(tracepoints.length() - dejavu.end(1)); + throw new CancellationException( + "Guru Meditation #" + preamble + "." + cycle); + }; + + trace.accept('A'); if ( ! m_usePostgres ) if ( 0 != p.waitFor() ) throw new IllegalStateException( "pg_ctl exited with status " + p.exitValue()); + trace.accept('B'); /* * Initialize a watch service just in case the postmaster.pid file @@ -2247,9 +2266,10 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) for ( ;; ) { + trace.accept('C'); try { - if ( getLastModifiedTime(pidfile).toInstant() + if ( getLastModifiedTime(pidfile).toInstant().plusSeconds(1) .isBefore(info.startInstant().get()) ) throw new NoSuchFileException("honest!"); /* @@ -2258,6 +2278,7 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) * for it to change will be the right thing to do. */ + trace.accept('D'); String[] status; try ( Stream lines = lines(pidfile) ) { @@ -2268,6 +2289,7 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) && PM_STATUS_READY.equals( status[LOCK_FILE_LINE_PM_STATUS - 1]) ) return; + trace.accept('E'); if ( ( status.length == LOCK_FILE_LINE_SHMEM_KEY @@ -2277,9 +2299,11 @@ private void wait_for_pid_file(Process p, ProcessHandle.Info info) && checkPid.test(status) && waitPrePG10() ) return; + trace.accept('F'); } catch (NoSuchFileException e) { + trace.accept('G'); } /* @@ -2296,11 +2320,15 @@ && waitPrePG10() ) : "" ) ); + trace.accept('H'); WatchKey k = watcher.poll(250, MILLISECONDS); + trace.accept('I'); if ( interrupted() ) throw new InterruptedException(); + trace.accept('J'); if ( null == k ) break; // timed out; check again just in case + trace.accept('K'); assert key.equals(k); // it's the only one we registered boolean recheck = k.pollEvents().stream() .anyMatch(e -> @@ -2319,8 +2347,10 @@ && waitPrePG10() ) return false; } ); + trace.accept('L'); if ( recheck ) break; + trace.accept('M'); k.reset(); } } From 9188f867bded42d435695b4f2f6a4b78d7d2afe6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 26 Aug 2020 19:47:35 -0400 Subject: [PATCH 0660/1087] Try to prove PGROOT and similar vars unnecessary The key is to find the right pg_config for the PostgreSQL version to be built and tested against, and be sure to pass that on the command line to mvn for building and to the package jar for installing and testing. If any of those other environment manipulations are needed, there's a bug somewhere. Also run the chosen pg_config, so its output is in the log in case of questions about what was actually being built. Ended up breaking out the appveyor build_script to a separate BAT file, because if you put a multiline CMD block in appveyor.yml it gets passed to CMD line-by-line and breaks ... unlike a multiline PowerShell block, which is handled as one would expect ... but PowerShell messes up what external commands write to standard error, so would be awkward to use here. --- .appveyor/appveyor_mingw.sh | 7 ++++++- .appveyor/appveyor_mingw_or_msvc.bat | 20 ++++++++++++++++++++ .travis.yml | 9 +++------ .travis/travis_install_postgresql.sh | 12 ++++-------- appveyor.yml | 20 ++++---------------- 5 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 .appveyor/appveyor_mingw_or_msvc.bat diff --git a/.appveyor/appveyor_mingw.sh b/.appveyor/appveyor_mingw.sh index 40e18a75..c9de750f 100644 --- a/.appveyor/appveyor_mingw.sh +++ b/.appveyor/appveyor_mingw.sh @@ -3,5 +3,10 @@ JAVA_HOME="/c/Program Files/Java/jdk$1" PATH=$JAVA_HOME/bin:$PATH javac -version pacman -S mingw-w64-x86_64-postgresql --noconfirm +pgConfig='C:\msys64\mingw64\bin\pg_config' +"$pgConfig" cd /c/projects/pljava -mvn clean install -Dnar.cores=1 -Psaxon-examples -Ppgjdbc-ng -Pwnosign --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn +mvn clean install \ + -Dpgsql.pgconfig="$pgConfig" \ + -Dnar.cores=1 -Psaxon-examples -Ppgjdbc-ng -Pwnosign --batch-mode \ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn diff --git a/.appveyor/appveyor_mingw_or_msvc.bat b/.appveyor/appveyor_mingw_or_msvc.bat new file mode 100644 index 00000000..f5e508dc --- /dev/null +++ b/.appveyor/appveyor_mingw_or_msvc.bat @@ -0,0 +1,20 @@ +REM a bat file because PowerShell makes a mess of stderr output, and a multiline +REM command intended for CMD in appveyor.yml gets broken up. + +IF %SYS%==MINGW ( + set pgConfig=C:\msys64\mingw64\bin\pg_config +) ELSE ( + set pgConfig=%ProgramFiles%\PostgreSQL\%PG%\bin\pg_config +) + +IF %SYS%==MINGW ( + C:\msys64\usr\bin\env MSYSTEM=MINGW64 ^ + C:\msys64\usr\bin\bash -l ^ + -c "/c/projects/pljava/.appveyor/appveyor_mingw.sh %JDK%" +) ELSE ( + "%pgConfig%" + mvn clean install ^ + -Dpgsql.pgconfig="%pgConfig%" ^ + -Dnar.cores=1 -Psaxon-examples -Ppgjdbc-ng --batch-mode ^ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn +) diff --git a/.travis.yml b/.travis.yml index 43979001..45fd672d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,17 +21,14 @@ before_install: - . .travis/travis_install_openssl.sh install: | + $pgConfig + mvn clean install --batch-mode \ + -Dpgsql.pgconfig="$pgConfig" \ -Dnar.cores=1 -Pwnosign -Psaxon-examples -Ppgjdbc-ng \ -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn script: | - if [[ "$POSTGRESQL_VERSION" == "SOURCE" ]]; then - pgConfig=/usr/local/pgsql/bin/pg_config - else - pgConfig=pg_config - fi - packageJar=$(find pljava-packaging -name pljava-pg*.jar -print) mavenRepo="$HOME/.m2/repository" diff --git a/.travis/travis_install_postgresql.sh b/.travis/travis_install_postgresql.sh index 28da2761..2957002d 100755 --- a/.travis/travis_install_postgresql.sh +++ b/.travis/travis_install_postgresql.sh @@ -8,10 +8,7 @@ if [ "$TRAVIS_OS_NAME" = "osx" ]; then fi brew install "postgresql${POSTGRESQL_VERSION}" - export PATH="/usr/local/opt/postgresql${POSTGRESQL_VERSION}/bin:${PATH}" - export LDFLAGS="-L/usr/local/opt/postgresql${POSTGRESQL_VERSION}/lib" - export CPPFLAGS="-I/usr/local/opt/postgresql${POSTGRESQL_VERSION}/include" - export PKG_CONFIG_PATH="/usr/local/opt/postgresql${POSTGRESQL_VERSION}/lib/pkgconfig" + pgConfig="/usr/local/opt/postgresql${POSTGRESQL_VERSION}/bin/pg_config" else sudo chmod 777 /home/travis sudo service postgresql stop @@ -29,9 +26,7 @@ else cd contrib make --silent && sudo make install - export LD_LIBRARY_PATH="/usr/local/pgsql/lib" - sudo /sbin/ldconfig /usr/local/pgsql/lib - export PATH="/usr/local/pgsql/bin:${PATH}" + pgConfig="/usr/local/pgsql/bin/pg_config" cd ../../pljava else @@ -41,5 +36,6 @@ else wget --quiet -O - https://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | sudo apt-key add - sudo apt-get -qq update sudo apt-get -qq install "postgresql-${POSTGRESQL_VERSION}" "postgresql-server-dev-${POSTGRESQL_VERSION}" libecpg-dev libkrb5-dev + pgConfig=pg_config fi -fi \ No newline at end of file +fi diff --git a/appveyor.yml b/appveyor.yml index a1d947e0..faf44b01 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,8 +2,6 @@ image: Visual Studio 2019 environment: APPVEYOR_RDP_PASSWORD: MrRobot@2020 VCVARSALL: C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat - PGUSER: postgres - PGPASSWORD: Password12! matrix: # - SYS: MINGW # JDK: 9 @@ -53,27 +51,17 @@ environment: before_build: - ps: .appveyor/./appveyor_download_java.ps1 - set JAVA_HOME=%ProgramFiles%\Java\jdk%JDK% - - set PGROOT=%ProgramFiles%\PostgreSQL\%PG% - - path %JAVA_HOME%\bin;%PGROOT%\bin;%PATH% + - path %JAVA_HOME%\bin;%PATH% - '"%VCVARSALL%" x86' - '"%VCVARSALL%" amd64' - ps: $Env:JAVA_HOME = "C:\Program Files\Java\jdk$Env:JDK" - - ps: $Env:PGROOT = "C:\Program Files\PostgreSQL\$Env:PG" - - ps: $Env:Path = "$Env:JAVA_HOME\bin;$Env:PGROOT\bin;" + $Env:Path + - ps: $Env:Path = "$Env:JAVA_HOME\bin;" + $Env:Path build_script: - - IF %SYS%==MINGW ( - C:\msys64\usr\bin\env MSYSTEM=MINGW64 C:\msys64\usr\bin\bash -l -c "/c/projects/pljava/.appveyor/appveyor_mingw.sh %JDK%") - ELSE ( - mvn clean install -Dnar.cores=1 -Psaxon-examples -Ppgjdbc-ng --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn - ) + - .appveyor\appveyor_mingw_or_msvc.bat # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) test_script: - ps: | - if ($Env:SYS -eq 'MINGW') { - $pgConfig = 'C:\msys64\mingw64\bin\pg_config' - } else { - $pgConfig = 'pg_config' - } + $pgConfig = $Env:pgConfig $packageJar = ('pljava-packaging' | Get-ChildItem -Recurse -Filter pljava-pg*.jar From 66134d23adbb3cd532d132fc265cba51405b79fb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 27 Aug 2020 00:50:21 -0400 Subject: [PATCH 0661/1087] Minor polishing, skip updating homebrew ... assuming whatever homebrew cache is initially on the VM image probably is up-to-date enough? That should at least slim the log by the large lists of updated formulae. (Maybe that is an indication that what's on the VM image is not up-to-date enough. Have to see.) Eliminating the homebrew auto update also eliminates the auto-upgrade it was doing to dozens of packages we have no interest in, cutting that part of the build from over 9 minutes down to half a minute or so. --- .travis.yml | 6 +++--- .travis/travis_install_postgresql.sh | 3 +++ appveyor.yml | 6 +++--- pljava-packaging/src/main/java/Node.java | 9 +++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 45fd672d..6847de78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -104,8 +104,6 @@ script: | return true; } - succeeding = true; // become optimistic, will be using &= below - try ( AutoCloseable t1 = n1.initialized_cluster(); AutoCloseable t2 = n1.started_server(Map.of( @@ -117,6 +115,8 @@ script: | { try ( Connection c = n1.connect() ) { + succeeding = true; // become optimistic, will be using &= below + succeeding &= dfa( "create extension no result", null, @@ -125,7 +125,7 @@ script: | .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), - // state 1: consume any diagnostics, or go to state 2 + // 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 diff --git a/.travis/travis_install_postgresql.sh b/.travis/travis_install_postgresql.sh index 2957002d..d2d62b16 100755 --- a/.travis/travis_install_postgresql.sh +++ b/.travis/travis_install_postgresql.sh @@ -1,4 +1,7 @@ if [ "$TRAVIS_OS_NAME" = "osx" ]; then + HOMEBREW_NO_AUTO_UPDATE=1 + export HOMEBREW_NO_AUTO_UPDATE + brew uninstall postgis postgresql if [ "$POSTGRESQL_VERSION" = "12" ]; then diff --git a/appveyor.yml b/appveyor.yml index faf44b01..5a88a329 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -121,8 +121,6 @@ test_script: return true; } - succeeding = true; // become optimistic, will be using &= below - try ( AutoCloseable t1 = n1.initialized_cluster( p->p.redirectErrorStream(true)); @@ -137,6 +135,8 @@ test_script: { try ( Connection c = n1.connect() ) { + succeeding = true; // become optimistic, will be using &= below + succeeding &= dfa( "create extension no result", null, @@ -145,7 +145,7 @@ test_script: .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), - // state 1: consume any diagnostics, or go to state 2 + // state 1: consume any diagnostics, or show same item to state 2 (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, // state 2: must be end of input diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 6f5dda11..8153ab24 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1076,6 +1076,15 @@ public static Stream q(Connection c, String sql) throws Exception * Produces a {@code Stream} of the (in JDBC, possibly multiple) results * from some {@code execute} method on a {@code Statement}. *

    + * This is how, for example, to prepare, then examine the results of, a + * {@code PreparedStatement}: + *

    +	 * PreparedStatement ps = conn.prepareStatement("select foo(?,?)");
    +	 * ps.setInt(1, 42);
    +	 * ps.setString(2, "surprise!");
    +	 * q(ps, ps::execute);
    +	 *
    + *

    * Each result in the stream will be an instance of one of: * {@code ResultSet}, {@code Long} (an update count, positive or zero), * {@code SQLWarning}, or some other {@code SQLException}. A warning or From 7f281cd20d9fe9c98f1b89e60c29e07a24456828 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 28 Aug 2020 20:56:43 -0400 Subject: [PATCH 0662/1087] Ambiguous null to Java variadic parameter --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 70c1d176..5699f259 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 @@ -2021,7 +2021,7 @@ static enum BaseUDTFunctionID INPUT("in", null, "pg_catalog.cstring", "pg_catalog.oid", "integer"), OUTPUT("out", "pg_catalog.cstring", (String[])null), RECEIVE("recv", null, "pg_catalog.internal","pg_catalog.oid","integer"), - SEND("send", "pg_catalog.bytea", null); + SEND("send", "pg_catalog.bytea", (String[])null); BaseUDTFunctionID( String suffix, String ret, String... param) { this.suffix = suffix; From e38307e9a3ec08775b9c9b44464940f0b9ede94f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 28 Aug 2020 21:07:51 -0400 Subject: [PATCH 0663/1087] Declaration after statement --- pljava-so/src/main/c/Function.c | 6 ++- pljava-so/src/main/c/PgSavepoint.c | 12 +++-- pljava-so/src/main/c/SPI.c | 66 +++++++++++++------------ pljava-so/src/main/c/type/TriggerData.c | 36 +++++++++----- 4 files changed, 71 insertions(+), 49 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 91502d64..05de6c7a 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -790,6 +790,7 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) * it is created in the upper context (even after connecting SPI, should * that be necessary). */ + MemoryContext currCtx; #if PG_VERSION_NUM >= 100000 /* If the invoked trigger function didn't connect SPI, do that here * (getTriggerReturnTuple now needs it), but there will be no need to @@ -798,7 +799,7 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) currentInvocation->triggerData = NULL; Invocation_assertConnect(); #endif - MemoryContext currCtx = Invocation_switchToUpperContext(); + currCtx = Invocation_switchToUpperContext(); ret = PointerGetDatum( pljava_TriggerData_getTriggerReturnTuple( jtd, &fcinfo->isnull)); @@ -929,9 +930,10 @@ JNIEXPORT jboolean JNICALL if ( 0 < numParams ) { + jint *paramOids; self->func.nonudt.paramTypes = (Type *)MemoryContextAlloc(ctx, numParams * sizeof (Type)); - jint *paramOids = JNI_getIntArrayElements(paramTypes, NULL); + paramOids = JNI_getIntArrayElements(paramTypes, NULL); for ( i = 0 ; i < numParams ; ++ i ) { if ( NULL != paramJTypes ) diff --git a/pljava-so/src/main/c/PgSavepoint.c b/pljava-so/src/main/c/PgSavepoint.c index fd5d91ab..b6bbb17f 100644 --- a/pljava-so/src/main/c/PgSavepoint.c +++ b/pljava-so/src/main/c/PgSavepoint.c @@ -38,9 +38,6 @@ jobject pljava_PgSavepoint_forId(SubTransactionId subId) void PgSavepoint_initialize(void) { - StaticAssertStmt(sizeof(SubTransactionId) <= sizeof(jint), - "SubTransactionId wider than jint?!"); - JNINativeMethod methods[] = { { @@ -63,7 +60,14 @@ void PgSavepoint_initialize(void) PgObject_registerNatives("org/postgresql/pljava/internal/PgSavepoint", methods); - jclass s_PgSavepoint_class = JNI_newGlobalRef(PgObject_getJavaClass( + /* + * I would rather put this at the top, but it counts as a statement, and + * would trigger a declaration-after-statement warning. + */ + StaticAssertStmt(sizeof(SubTransactionId) <= sizeof(jint), + "SubTransactionId wider than jint?!"); + + s_PgSavepoint_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/PgSavepoint")); s_forId = PgObject_getStaticJavaMethod(s_PgSavepoint_class, "forId", diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index 9db51fcb..ea104bd9 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -30,7 +30,41 @@ StaticAssertStmt((c) == (org_postgresql_pljava_internal_##c), \ extern void SPI_initialize(void); void SPI_initialize(void) { - /* Statically assert that the Java code has the right values for these. */ + JNINativeMethod methods[] = { + { + "_exec", + "(Ljava/lang/String;I)I", + Java_org_postgresql_pljava_internal_SPI__1exec + }, + { + "_getProcessed", + "()J", + Java_org_postgresql_pljava_internal_SPI__1getProcessed + }, + { + "_getResult", + "()I", + Java_org_postgresql_pljava_internal_SPI__1getResult + }, + { + "_getTupTable", + "(Lorg/postgresql/pljava/internal/TupleDesc;)Lorg/postgresql/pljava/internal/TupleTable;", + Java_org_postgresql_pljava_internal_SPI__1getTupTable + }, + { + "_freeTupTable", + "()V", + Java_org_postgresql_pljava_internal_SPI__1freeTupTable + }, + { 0, 0, 0 }}; + + PgObject_registerNatives("org/postgresql/pljava/internal/SPI", methods); + + /* + * Statically assert that the Java code has the right values for these. + * I would rather have this at the top, but these count as statements and + * would trigger a declaration-after-statment warning. + */ CONFIRMCONST(SPI_ERROR_CONNECT); CONFIRMCONST(SPI_ERROR_COPY); CONFIRMCONST(SPI_ERROR_OPUNKNOWN); @@ -68,36 +102,6 @@ void SPI_initialize(void) CONFIRMCONST(SPI_OK_REL_UNREGISTER); CONFIRMCONST(SPI_OK_TD_REGISTER); #endif - - JNINativeMethod methods[] = { - { - "_exec", - "(Ljava/lang/String;I)I", - Java_org_postgresql_pljava_internal_SPI__1exec - }, - { - "_getProcessed", - "()J", - Java_org_postgresql_pljava_internal_SPI__1getProcessed - }, - { - "_getResult", - "()I", - Java_org_postgresql_pljava_internal_SPI__1getResult - }, - { - "_getTupTable", - "(Lorg/postgresql/pljava/internal/TupleDesc;)Lorg/postgresql/pljava/internal/TupleTable;", - Java_org_postgresql_pljava_internal_SPI__1getTupTable - }, - { - "_freeTupTable", - "()V", - Java_org_postgresql_pljava_internal_SPI__1freeTupTable - }, - { 0, 0, 0 }}; - - PgObject_registerNatives("org/postgresql/pljava/internal/SPI", methods); } /**************************************** diff --git a/pljava-so/src/main/c/type/TriggerData.c b/pljava-so/src/main/c/type/TriggerData.c index da6f9e3e..9f05542c 100644 --- a/pljava-so/src/main/c/type/TriggerData.c +++ b/pljava-so/src/main/c/type/TriggerData.c @@ -165,9 +165,10 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getRelation(JNIEnv* env, jclass clazz, jlong _this) { jobject result = 0; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -186,9 +187,10 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getTriggerTuple(JNIEnv* env, jclass clazz, jlong _this) { jobject result = 0; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -207,9 +209,10 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getNewTuple(JNIEnv* env, jclass clazz, jlong _this) { jobject result = 0; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -228,9 +231,10 @@ JNIEXPORT jobjectArray JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getArguments(JNIEnv* env, jclass clazz, jlong _this) { jobjectArray result = 0; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) { char** cpp; @@ -261,9 +265,10 @@ JNIEXPORT jstring JNICALL Java_org_postgresql_pljava_internal_TriggerData__1getName(JNIEnv* env, jclass clazz, jlong _this) { jstring result = 0; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) { BEGIN_NATIVE @@ -282,9 +287,10 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredAfter(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_AFTER(self->tg_event); return result; @@ -299,9 +305,10 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredBefore(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BEFORE(self->tg_event); return result; @@ -316,9 +323,10 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredForEachRow(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_FOR_ROW(self->tg_event); return result; @@ -333,9 +341,10 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredForStatement(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_FOR_STATEMENT(self->tg_event); return result; @@ -350,9 +359,10 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredByDelete(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BY_DELETE(self->tg_event); return result; @@ -367,9 +377,10 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredByInsert(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BY_INSERT(self->tg_event); return result; @@ -384,9 +395,10 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_TriggerData__1isFiredByUpdate(JNIEnv* env, jclass clazz, jlong _this) { jboolean result = JNI_FALSE; + TriggerData* self; Ptr2Long p2l; p2l.longVal = _this; - TriggerData* self = (TriggerData*)p2l.ptrVal; + self = (TriggerData*)p2l.ptrVal; if(self != 0) result = (jboolean)TRIGGER_FIRED_BY_UPDATE(self->tg_event); return result; From 7ec80dd4cf218667e5b04b7bfffff0e004f3a0bb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 28 Aug 2020 21:15:22 -0400 Subject: [PATCH 0664/1087] Missing prototypes --- pljava-so/src/main/c/InstallHelper.c | 2 +- pljava-so/src/main/include/pljava/Function.h | 2 +- pljava-so/src/main/include/pljava/InstallHelper.h | 14 +++++++------- pljava-so/src/main/include/pljava/type/Portal.h | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index bf948a8b..a2b87526 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -119,7 +119,7 @@ static jmethodID s_InstallHelper_groundwork; static bool extensionExNihilo = false; static void checkLoadPath( bool *livecheck); -static void getExtensionLoadPath(); +static void getExtensionLoadPath(void); static char *origUserName(); char const *pljavaLoadPath = NULL; diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 5671abcd..0d5b30fb 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -87,7 +87,7 @@ extern void pljava_Function_setParameter(Function self, int idx, jvalue val); /* * Not intended for any caller other than Invocation_popInvocation. */ -extern void pljava_Function_popFrame(); +extern void pljava_Function_popFrame(void); /* * These actually invoke a target Java method (returning, respectively, a diff --git a/pljava-so/src/main/include/pljava/InstallHelper.h b/pljava-so/src/main/include/pljava/InstallHelper.h index c3478fd7..b923eebf 100644 --- a/pljava-so/src/main/include/pljava/InstallHelper.h +++ b/pljava-so/src/main/include/pljava/InstallHelper.h @@ -76,13 +76,13 @@ extern bool InstallHelper_isPLJavaFunction(Oid fn); * In a background worker, there's no MyProcPort, and the name is found another * way and strdup'd in TopMemoryContext, it'll keep, don't bother freeing it. */ -extern char *pljavaDbName(); +extern char *pljavaDbName(void); /* * Return the name of the cluster if it has been set (only possible in 9.5+), * or an empty string, never NULL. */ -extern char const *pljavaClusterName(); +extern char const *pljavaClusterName(void); /* * Construct a default for pljava.module_path ($sharedir/pljava/pljava-$VER.jar @@ -113,7 +113,7 @@ extern char const *InstallHelper_defaultModulePath(char *, char); * and disrupt the abort. The trickiest bit was finding available API to * recognize the ABORT_PENDING cases. */ -extern bool pljavaViableXact(); +extern bool pljavaViableXact(void); /* * Backend's initsequencer needs to know whether it's being called in a 9.3+ @@ -123,10 +123,10 @@ extern bool pljavaViableXact(); * version-specific Windows visibility issues, so the ugly details are in * InstallHelper, and Backend just asks this nice function. */ -extern bool InstallHelper_shouldDeferInit(); +extern bool InstallHelper_shouldDeferInit(void); -extern char *InstallHelper_hello(); +extern char *InstallHelper_hello(void); -extern void InstallHelper_groundwork(); +extern void InstallHelper_groundwork(void); -extern void InstallHelper_initialize(); +extern void InstallHelper_initialize(void); diff --git a/pljava-so/src/main/include/pljava/type/Portal.h b/pljava-so/src/main/include/pljava/type/Portal.h index ed467a06..d523ecc3 100644 --- a/pljava-so/src/main/include/pljava/type/Portal.h +++ b/pljava-so/src/main/include/pljava/type/Portal.h @@ -29,7 +29,7 @@ extern "C" { * @author Thomas Hallgren *****************************************************************/ -extern void pljava_Portal_initialize(); +extern void pljava_Portal_initialize(void); /* * Create the org.postgresql.pljava.Portal instance From 35eda7bd53b363f186b8f6b4899a18f671f6d1d6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 28 Aug 2020 21:33:23 -0400 Subject: [PATCH 0665/1087] Maybe uninitialized The BEGIN/END_NATIVE family of macros conceal 'if' statements: if certain conditions are pending, the underlying check 'throws' an exception and returns false. Ordinarily, a thrown exception would mean no code after the 'if' ever executes, and there would be no concern about using a variable whose initialization was contained in the if block. In JNI, though, an exception isn't really thrown until the function returns. It just has to skip the actions inside the block and return something, anything, as directly as possible. At that point, the JVM throws the exception and no use of the return value is made, but the C compiler still sees that the function could return an uninitialized value in some circumstances. Simply initializing it, even to an arbitrary value, quiets the warning. --- pljava-so/src/main/c/VarlenaWrapper.c | 8 ++++---- pljava-so/src/main/c/type/Oid.c | 2 +- pljava-so/src/main/c/type/SQLXMLImpl.c | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 9cbe5be1..6be9abb4 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -513,7 +513,7 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa Ptr2Long p2ldetoasted; _VL_TYPE detoasted; MemoryContext prevcxt; - jobject dbb; + jobject dbb = NULL; BEGIN_NATIVE_NO_ERRCHECK @@ -570,18 +570,18 @@ JNIEXPORT jlong JNICALL Java_org_postgresql_pljava_internal_VarlenaWrapper_00024 MemoryContext prevcxt; _VL_TYPE fetched; - BEGIN_NATIVE_NO_ERRCHECK; p2lvl.longVal = varlena; p2lcxt.longVal = memContext; + BEGIN_NATIVE_NO_ERRCHECK; prevcxt = MemoryContextSwitchTo((MemoryContext) p2lcxt.ptrVal); fetched = heap_tuple_fetch_attr((_VL_TYPE) p2lvl.ptrVal); pfree(p2lvl.ptrVal); p2lvl.longVal = 0L; p2lvl.ptrVal = fetched; MemoryContextSwitchTo(prevcxt); - END_NATIVE; + return p2lvl.longVal; } @@ -599,7 +599,7 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Output_00024State__1next ExpandedVarlenaOutputStreamNode *node; Ptr2Long p2l; Datum d; - jobject dbb; + jobject dbb = NULL; p2l.longVal = varlenaPtr; d = PointerGetDatum(p2l.ptrVal); diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index 7638dbe4..aecb67e0 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -316,7 +316,7 @@ Java_org_postgresql_pljava_internal_Oid__1getJavaClassName(JNIEnv* env, jclass c JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_Oid__1getCurrentLoader(JNIEnv *env, jclass cls) { - jobject result; + jobject result = NULL; BEGIN_NATIVE result = Function_currentLoader(); END_NATIVE diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 5fc73860..8f8bd989 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -236,7 +236,7 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_jdbc_SQLXMLImpl__1newWritable (JNIEnv *env, jclass sqlxml_class) { - jobject sqlxml; + jobject sqlxml = NULL; jobject vwo; BEGIN_NATIVE vwo = pljava_VarlenaWrapper_Output( From 172b97d9dac74fb4048c44b5595d519f66aeacd9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 28 Aug 2020 22:09:49 -0400 Subject: [PATCH 0666/1087] Sometimes uninitialized Mac clang's version of gcc's "maybe uninitialized" ... which finds a case that Linux gcc missed. --- pljava-so/src/main/c/Function.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 05de6c7a..b80ba2b6 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -902,7 +902,7 @@ JNIEXPORT jboolean JNICALL int i = 0; uint16 refParams = 0; uint16 primParams = 0; - bool returnTypeIsOutParameter; + bool returnTypeIsOutParameter = false; p2l.longVal = wrappedPtr; self = (Function)p2l.ptrVal; @@ -983,7 +983,6 @@ JNIEXPORT jboolean JNICALL Exception_throw_ERROR(PG_FUNCNAME_MACRO); } PG_END_TRY(); - END_NATIVE if ( returnTypeIsOutParameter && JNI_TRUE != isMultiCall ) ++ refParams; @@ -991,6 +990,7 @@ JNIEXPORT jboolean JNICALL self->func.nonudt.numRefParams = refParams; self->func.nonudt.numPrimParams = primParams; + END_NATIVE return returnTypeIsOutParameter; } From 96484124a70069749f65f0d70bb8fe0e3e589cf9 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Wed, 19 Aug 2020 17:10:29 +0530 Subject: [PATCH 0667/1087] Add JavaScript snippet to build pljava-so --- .../org/postgresql/pljava/pgxs/PGXSUtils.java | 36 ++++++++++ pljava-so/pom.xml | 65 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java index 5614a30d..faa2c57c 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java @@ -20,6 +20,7 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.tools.Diagnostic; +import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.jar.JarFile; @@ -145,6 +147,11 @@ static ScriptEngine getScriptEngine(PlexusConfiguration script, Log log, (Function, Map>) elements -> buildPaths(log, elements), GLOBAL_SCOPE); + context.setAttribute("runCommand", + (BiFunction, ProcessBuilder>) + (command, args) -> runCommand(project, command, args), + GLOBAL_SCOPE); + /* * Also provide a specialized method useful for a script that may * handle diagnostics from Java tools. @@ -283,6 +290,35 @@ public static String getPgConfigProperty (String pgConfigCommand, pgConfigOutput.length() - System.lineSeparator().length()); } + /** + * @param project maven project invoking the method + * @param command the command to be executed + * @param argumentsList list of arguments to pass to the command + * + * @return ProcessBuilder with input command and arguments + */ + public static ProcessBuilder runCommand(MavenProject project, + String command, + List argumentsList) + { + List commandList = new ArrayList<>(); + commandList.add(command); + commandList.addAll(argumentsList); + ProcessBuilder processBuilder = new ProcessBuilder(commandList); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + File outputDirectory = new File(project.getBuild().getDirectory(), "pljava-pgxs"); + try + { + Files.createDirectories(outputDirectory.toPath()); + processBuilder.directory(outputDirectory); + } catch (IOException e) { + e.printStackTrace(); + } + + return processBuilder; + } + /** * Returns true if the profile with given name exists and is active, false * otherwise. diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 88bbeab7..8dbca519 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -301,6 +301,71 @@ var jvmdfltQuoted = quoteStringForC(jvmdflt); setProjectProperty("pljava.qlibjvmdefault", jvmdfltQuoted); } + + var source_path = java.nio.file.Paths.get("${basedir}", "src", "main", "c"); + var files = java.nio.file.Files + .walk(source_path) + .parallel() + .filter(function(e) java.nio.file.Files.isRegularFile(e)) + .map(function(e) e.toAbsolutePath().toString()) + .filter(function(e) e.endsWith(".c")) + .collect(java.util.stream.Collectors.toList()); + + var includes = java.util.List.of( + "-I${java.home}/include", + "-I${java.home}/include/linux", + "-I" + getPgConfigProperty("--includedir"), + "-I" + getPgConfigProperty("--includedir-server"), + "-I${basedir}/src/main/include/", + "-I${basedir}/src/main/include/fallback/jdbc", + "-I${javah.include}"); + + var defines = java.util.List.of( + "-DGNU_GCC", + "-DLinux", + "-DPLJAVA_SO_VERSION=${project.parent.version}"); + + var flags = java.util.List.of( + "-Wall", + "-Wno-long-long", + "-Wpointer-arith", + "-Wconversion", + "-fPIC", + "-c", + "-fPIC"); + + var ArrayList = Java.type("java.util.ArrayList"); + + var compileArgs = new ArrayList(); + compileArgs.addAll(flags); + compileArgs.addAll(defines); + compileArgs.addAll(includes); + compileArgs.addAll(files); + + var compileProcess = runCommand("gcc", compileArgs); + var outputDirectory = compileProcess.directory(); + var process = compileProcess.start(); + process.waitFor(); + + var objectFiles = java.nio.file.Files + .walk(outputDirectory.toPath()) + .parallel() + .map(function(e) e.toAbsolutePath().toString()) + .filter(function(e) e.endsWith(".o")) + .collect(java.util.stream.Collectors.toList()); + + var linkingArgs = new ArrayList(); + linkingArgs.add("-shared"); + linkingArgs.add("-o"); + linkingArgs.add("libpljava-so-1.6.0-SNAPSHOT.so"); + linkingArgs.addAll(objectFiles); + linkingArgs.add("-shared-libgcc"); + + var linkingProcess = runCommand("g++", linkingArgs); + linkingProcess.directory(outputDirectory); + var exit = linkingProcess.start().waitFor(); + error("Exit code" + exit); + ]]> From 41c67fef59d93bbd89737130437986f486d937ec Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 20 Aug 2020 15:15:54 +0530 Subject: [PATCH 0668/1087] Delete nar maven from build process --- pljava-packaging/build.xml | 31 ++++------------------ pljava-packaging/pom.xml | 2 +- pljava-so/pom.xml | 54 +------------------------------------- 3 files changed, 7 insertions(+), 80 deletions(-) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 530f1921..dffd8174 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -1,28 +1,7 @@ - + - - - - - - - - - - - - - - - - - - - - - + - + - diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index b4eefcfe..8cfeeaf4 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -31,7 +31,7 @@ org.postgresql pljava-so ${project.version} - nar + pom diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 8dbca519..41072643 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -11,11 +11,9 @@ Generates the pljava (.so, .dll, etc.) library which gets loaded by the PostgreSQL backend - nar + pom - - 0 ${basedir}/../pljava/target/javah-include/ @@ -273,7 +271,6 @@ - org.postgresql pljava-pgxs @@ -373,55 +370,6 @@ - - - com.github.maven-nar - nar-maven-plugin - 3.2.3 - true - - - ${nar.cores} - - - - - - PLJAVA_SO_VERSION=${project.version} - - - - ${PGSQL_INCLUDEDIR} - ${PGSQL_INCLUDEDIR-SERVER} - ${basedir}/src/main/include/ - ${basedir}/src/main/include/fallback/jdbc - ${javah.include} - - ${so.debug} - ${so.optimize} - - - - true - - - - - - - - - - plugin - - false - - - - - - - haslibjvmdefault - - - pljava.libjvmdefault - - - - - - com.github.maven-nar - nar-maven-plugin - - - - PLJAVA_LIBJVMDEFAULT=${pljava.qlibjvmdefault} - - - - - - - needsdefaultpgconfig @@ -281,13 +140,6 @@ From 3d1f58d65ec77c60d06bb0678f4a794e8041693f Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 30 Aug 2020 01:59:02 +0530 Subject: [PATCH 0684/1087] Delete references to deleted maven profiles --- src/site/markdown/build/build.md | 25 +--------------------- src/site/markdown/build/linkpglibs.md | 28 ------------------------- src/site/markdown/build/macosx.md.vm | 11 ---------- src/site/markdown/build/package.md | 16 -------------- src/site/markdown/build/runpath.md | 21 ------------------- src/site/markdown/install/install.md.vm | 3 --- 6 files changed, 1 insertion(+), 103 deletions(-) delete mode 100644 src/site/markdown/build/linkpglibs.md diff --git a/src/site/markdown/build/build.md b/src/site/markdown/build/build.md index 75fba91c..d4810335 100644 --- a/src/site/markdown/build/build.md +++ b/src/site/markdown/build/build.md @@ -82,8 +82,6 @@ Please review any of the following that apply to your situation: * Building on an EnterpriseDB PostgreSQL distribution that bundles system libraries, or other situations where [a linker runpath](runpath.html) can help -* Building on a platform that - [requires PostgreSQL libraries at link time](linkpglibs.html) * Building if you are [making a package for a software distribution](package.html) * Building [with debugging or optimization options](debugopt.html) @@ -248,33 +246,12 @@ type/String.c:132:43: warning: conversion to 'jlong' from 'Size' may change If the compiler reports any actual errors, the build will fail. -#### Disable nuisance warnings where possible - -The Maven plugin that drives the C compiler enables, by default, many -types of warning that would be impractical to fix. Those can clutter the -output (especially with Maven tagging them with `[ERROR]`) so that if the -build does fail because of an actual error, it is difficult to read back -through the `[ERROR]`s that were not errors, to find the one that was. - -If the compiler is `gcc`, an extra option `-Pwnosign` can be given on the -`mvn` command line, and will suppress the most voluminous and least useful -warnings. It adds the compiler option `-Wno-sign-conversion` which might not -be understood by other compilers, so may not have the intended effect if the -compiler is not `gcc`. - -#### Compile with a single core, for clarity of messages - -On a machine with many cores, messages from several compilation threads may be -intermingled in the output so that related messages are hard to identify. -The option `-Dnar.cores=1` will force the messages into a sequential order -(and has little effect on the speed of a PL/Java build). - #### Capture the output of `mvn -X` The `-X` option will add a lot of information on the details of Maven's build activities. - mvn -X -Pwnosign -Dnar.cores=1 clean install + mvn -X clean install #### Avoid capturing the first run of Maven diff --git a/src/site/markdown/build/linkpglibs.md b/src/site/markdown/build/linkpglibs.md deleted file mode 100644 index 70bab635..00000000 --- a/src/site/markdown/build/linkpglibs.md +++ /dev/null @@ -1,28 +0,0 @@ -# Including `pgtypes`, `pq`, `ecpg` libraries at link time - -The PL/Java build process has, for some time, explicitly included these -three PostgreSQL libraries when linking PL/Java. In many cases this is -unnecessary and, in fact, better avoided. Leaving these out at link time -eliminates the chance of certain run-time library version mismatches. - -However, there may be some platforms where these libraries must be included -in the link. If you have a failing build, especially if the failure involves -undefined symbol errors, try the build again, adding - - -Plinkpglibs - -on the `mvn` command line. If that helps, please report your platform and -configuration so we know which platforms require it. - -If it doesn't help, the problem lies somewhere else. - -## Library version mismatches when using `-Plinkpglibs` - -If you must use this option when building, and you will use PL/Java on a -system where several PostgreSQL versions are installed and one has been marked -as the system default, it is possible to see version-mismatch problems where -PL/Java running in one of the non-default PostgreSQL versions will have found -the libraries from the default version. - -That problem and its solutions are described near the end of the -[Building PL/Java with a `RUNPATH`](runpath.html) page. diff --git a/src/site/markdown/build/macosx.md.vm b/src/site/markdown/build/macosx.md.vm index e7665c1a..7baafc9c 100644 --- a/src/site/markdown/build/macosx.md.vm +++ b/src/site/markdown/build/macosx.md.vm @@ -73,17 +73,6 @@ Note that on Mac OS X, the variable should point to a `libjli.dylib` file if it is available (Java 7 and later), not to a `libjvm.dylib` as you would otherwise expect. See `No Java runtime present` below for details. -$h2 Troubleshooting the build - -$h3 Aggressive compiler warnings - -On OS X, it seems the `-Pwnosign` Maven build option does not succeed in -suppressing the many useless sign-conversion warnings, so if the build does -fail, it can be difficult to find the real problem because of so many -surrounding messages. One technique is to direct the `mvn -X -Dnar.cores=1 clean -install` output into a file, then search that file for the strings `fatal error` -or `error generated`. - $h2 Troubleshooting installation $h3 `No Java runtime present, requesting install` or `Java SE 6` download dialog diff --git a/src/site/markdown/build/package.md b/src/site/markdown/build/package.md index 454834d5..e61c80d9 100644 --- a/src/site/markdown/build/package.md +++ b/src/site/markdown/build/package.md @@ -149,22 +149,6 @@ version. (The same effect was always possible by making sure that `bin` directory was at the front of the `PATH` when invoking `mvn`, but this option on the `mvn` command makes it more explicit.) -`-Pwnosign` -: If `gcc` is used in the build, adding this option on the `mvn` command line -will suppress a large number of nuisance warnings `gcc` would otherwise -produce, keeping the build log manageable in case any actual problem needs to -be found. (The nuisance warnings can be traced upstream to PostgreSQL headers, -and have been [deemed not pressing to fix there][notgoingthere], as upstream -also builds with those warnings suppressed.) - -`-Dnar.cores=1` -: This option also helps to make the build log more intelligible in case any -problem needs to be found, by avoiding the interleaving of messages from -simultaneous compilations. PL/Java builds quickly enough that there is little -cost in wall time to do the compiles on one core. - -[notgoingthere]: https://www.postgresql.org/message-id/19039.1496353285%40sss.pgh.pa.us - ## Patching PL/Java If your packaging project requires patches to PL/Java, and not simply the diff --git a/src/site/markdown/build/runpath.md b/src/site/markdown/build/runpath.md index 44879054..fcbf90b7 100644 --- a/src/site/markdown/build/runpath.md +++ b/src/site/markdown/build/runpath.md @@ -81,24 +81,3 @@ Other platform-specific options for solving the same problem, such as `ldconfig` on Linux, may be available. Before there was `pljava.libjvm_location`, it used to be common to have to know these tricks. Now it should be uncommon, but in rare cases can still be useful. - -## Other situations where a RUNPATH may be needed - -On a system where several versions of PostgreSQL are installed, and one of -them has been made the default, there may be entries in the standard system -library directories that point to PostgreSQL-specific libraries (like -`libpgtypes`, `libpq`, `libecpg`) *for the version selected as default*. - -It is possible that when you load PL/Java into one of the non-default -PostgreSQL installations, it will find the wrong versions of those libraries -by looking in the system locations first. - -*This should only be possible if, for some reason, you needed the -`-Plinkpglibs` option when building PL/Java.* Without that option, those -PostgreSQL-specific libraries should be resolved within the PostgreSQL -backend itself, where the correct versions will be found. - -If your platform requires you to use `-Plinkpglibs`, and a problem with the -wrong library versions being found at run time results, it also can be solved -by explicitly using the `pg_config --libdir` value from the appropriate -PostgreSQL version, using any of the methods described above. diff --git a/src/site/markdown/install/install.md.vm b/src/site/markdown/install/install.md.vm index 7e41050c..8e812c1a 100644 --- a/src/site/markdown/install/install.md.vm +++ b/src/site/markdown/install/install.md.vm @@ -268,9 +268,6 @@ If PL/Java loading fails with undefined-symbol errors that seem to refer to common system libraries (`libldap`, for example), see [Building PL/Java with a `RUNPATH`](../build/runpath.html). -In case of errors that seem to refer to symbols of PostgreSQL itself, -see [this page](../build/linkpglibs.html). - $h2 More background and special considerations These last sections cover a little more of what happens under the hood. From 30cbd1d4b5222ee577174c6bf7d9c73aed4a8e60 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 30 Aug 2020 02:43:13 +0530 Subject: [PATCH 0685/1087] Remove travis_install_openssl.sh The only need for this file was to install an openssl that could be detected by the compiler while building. However, considering flags from pg_config is enough for using the correct openssl from homebrew. --- .travis.yml | 1 - .travis/travis_install_openssl.sh | 10 ---------- 2 files changed, 11 deletions(-) delete mode 100755 .travis/travis_install_openssl.sh diff --git a/.travis.yml b/.travis.yml index 54047d0e..e8dc6e16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ cache: - $HOME/.m2 before_install: - . .travis/travis_install_postgresql.sh - - . .travis/travis_install_openssl.sh install: | $pgConfig diff --git a/.travis/travis_install_openssl.sh b/.travis/travis_install_openssl.sh deleted file mode 100755 index 28a6d417..00000000 --- a/.travis/travis_install_openssl.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -if [ "$TRAVIS_OS_NAME" = "osx" ]; then - curl -o ../macports.pkg -L https://github.com/macports/macports-base/releases/download/v2.6.2/MacPorts-2.6.2-10.14-Mojave.pkg - sudo installer -pkg ../macports.pkg -target / - export PATH=/opt/local/bin:/opt/local/sbin:$PATH - yes | sudo port install openssl - export CPATH=/opt/local/include:$CPATH - export LIBRARY_PATH=/opt/local/lib:$LIBRARY_PATH -fi From a48df1a4522d81fefab0d93c9768e20f2381d825 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 30 Aug 2020 02:43:29 +0530 Subject: [PATCH 0686/1087] Consider CPPFLAGS while building pljava-so --- pljava-so/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 0612cab2..042d70d3 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -40,6 +40,7 @@ var cc = getPgConfigProperty("--cc"); var cflags = getPgConfigProperty("--cflags"); + var cppflags = getPgConfigProperty("--cppflags"); var cflags_sl = getPgConfigProperty("--cflags_sl"); var ldflags = getPgConfigProperty("--ldflags"); var ldflags_sl = getPgConfigProperty("--ldflags_sl"); @@ -276,6 +277,7 @@ var files = utils.getFilesWithExtension(source_path, ".c"); var compile_flags = new ArrayList(); compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags)); + compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cppflags)); compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags_sl)); var exitCode = pgxs.compile(cc, files, target_path, base_includes, base_defines, compile_flags); if (exitCode != 0) From f8e475b861152987ec797eb7ccb6af6b7df02c05 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 30 Aug 2020 11:43:57 +0530 Subject: [PATCH 0687/1087] Fix implementation of getPgConfigPropertyAsList The earlier implementation using regex was unable to correctly parse the string into a list when single quotes were part of the word. A few unit tests have also been added to ensure that a new implementation of the method does not break existing functionality. --- .../postgresql/pljava/pgxs/AbstractPGXS.java | 27 +++++++-- .../src/test/java/AbstractPGXSMock.java | 34 +++++++++++ .../test/java/PgConfigPropertyAsListTest.java | 58 +++++++++++++++++++ 3 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 pljava-pgxs/src/test/java/AbstractPGXSMock.java create mode 100644 pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java index 6d2a9560..ea3fda5d 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java @@ -1,11 +1,9 @@ package org.postgresql.pljava.pgxs; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.regex.MatchResult; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; public abstract class AbstractPGXS @@ -35,8 +33,25 @@ public List formatDefines(Map definesMap) } public List getPgConfigPropertyAsList(String properties) { - Pattern pattern = Pattern.compile("[^\\s']+|'([^']*)'"); - Matcher matcher = pattern.matcher(properties); - return matcher.results().map(MatchResult::group).collect(Collectors.toList()); + List propertyList = new ArrayList<>(); + StringBuilder builder = new StringBuilder(); + boolean isInsideQuotes = false; + + for (char x : properties.toCharArray()) + { + if (x == '\'') + isInsideQuotes = !isInsideQuotes; + else if (!isInsideQuotes && x == ' ') + { + propertyList.add(builder.toString()); + builder.setLength(0); + } + else + builder.append(x); + } + + if (builder.length() != 0) + propertyList.add(builder.toString()); + return propertyList; } } diff --git a/pljava-pgxs/src/test/java/AbstractPGXSMock.java b/pljava-pgxs/src/test/java/AbstractPGXSMock.java new file mode 100644 index 00000000..69c8ec11 --- /dev/null +++ b/pljava-pgxs/src/test/java/AbstractPGXSMock.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 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: + * Kartik Ohri + */ +import org.postgresql.pljava.pgxs.AbstractPGXS; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +public class AbstractPGXSMock extends AbstractPGXS +{ + @Override + public int compile(String compiler, List files, Path targetPath, + List includes, Map defines, + List flags) + { + throw new UnsupportedOperationException(); + } + + @Override + public int link(String linker, List flags, List files, + Path targetPath) + { + throw new UnsupportedOperationException(); + } +} diff --git a/pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java b/pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java new file mode 100644 index 00000000..38116d93 --- /dev/null +++ b/pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 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: + * Kartik Ohri + */ + +import org.junit.Before; +import org.junit.Test; +import org.postgresql.pljava.pgxs.AbstractPGXS; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class PgConfigPropertyAsListTest { + + AbstractPGXS pgxs; + @Before + public void setup() { + pgxs = new AbstractPGXSMock(); + } + + @Test + public void testSimpleExample() + { + List actualResult = pgxs.getPgConfigPropertyAsList( + "foo 'bar' 'foo bar'"); + List expectedResult = List.of("foo", "bar", "foo bar"); + assertEquals(expectedResult, actualResult); + } + + @Test + public void testPracticalExample() + { + List actualResult = pgxs.getPgConfigPropertyAsList( + "-Wl,--as-needed -Wl,-rpath,'/usr/local/pgsql/lib',--enable-new-dtags"); + List expectedResult = List.of("-Wl,--as-needed", + "-Wl,-rpath,/usr/local/pgsql/lib,--enable-new-dtags"); + assertEquals(expectedResult, actualResult); + } + + @Test + public void testWhitespaceInQuotes() + { + List actualResult = pgxs.getPgConfigPropertyAsList( + "-Wl,--as-needed -Wl,-rpath,'/usr/local test/pgsql/lib',--enable-new-dtags"); + List expectedResult = List.of("-Wl,--as-needed", + "-Wl,-rpath,/usr/local test/pgsql/lib,--enable-new-dtags"); + assertEquals(expectedResult, actualResult); + } + +} From 5d71fb871f10653709b041d2c76f58ffe494474c Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 30 Aug 2020 13:27:46 +0530 Subject: [PATCH 0688/1087] Document AbstractPGXS --- .../postgresql/pljava/pgxs/AbstractPGXS.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java index ea3fda5d..4b51e26e 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2020 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: + * Kartik Ohri + */ package org.postgresql.pljava.pgxs; import java.nio.file.Path; @@ -6,20 +17,49 @@ import java.util.Map; import java.util.stream.Collectors; +/** + * Class to act as a blueprint for platform specific build configurations in + * pljava-so/pom.xml + */ public abstract class AbstractPGXS { + + /** + * Add instructions for compiling the pljava-so C files on your platform + * by implementing this method in your configuration block. + */ public abstract int compile(String compiler, List files, Path targetPath, List includes, Map defines, List flags); + /** + * Add instructions for linking and producing the pljava-so shared library + * artifact on your platform by implementing this method in your + * configuration block. + */ public abstract int link(String linker, List flags, List files, Path targetPath); + /** + * Returns a list with all items prefixed with correct include flag symbol. + * + * This is the default implementation for formatting the list of includes, + * and prefixes the includes with -I. For compilers like MSVC that require + * different symbols, override this method in your configuration block. + */ public List formatIncludes(List includesList) { return includesList.stream().map(s -> "-I" + s) .collect(Collectors.toList()); } + /** + * Returns a list with all defines prefixed correctly. + * + * This is the default implementation for formatting the list of defines. + * Each item is prefixed with -D. If the define is associated with a value, + * adds equal symbol also followed by the value. If your linker expects a + * different format, override the method in your configuration block. + */ public List formatDefines(Map definesMap) { return definesMap.entrySet().stream() @@ -32,6 +72,10 @@ public List formatDefines(Map definesMap) .collect(Collectors.toList()); } + /** + * Returns the input pg_config property as a list of individual flags split + * at whitespace, except when quoted, and the quotes removed. + */ public List getPgConfigPropertyAsList(String properties) { List propertyList = new ArrayList<>(); StringBuilder builder = new StringBuilder(); From a2c5f2aa544e7f208030856d115055c1c2997940 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sun, 30 Aug 2020 15:17:59 +0530 Subject: [PATCH 0689/1087] Fix MinGW build by removing -DBUILDING_DLL Windows has a particular global symbol filtering mechanism. The -DBUILDING_DLL flag is present in CPPFLAGS used by PostgreSQL to build its dlls. In context of the PostgreSQL build, the flag is used to mark the symbols that should be exposed to other object files. However, PL/Java is a PostgreSQL extension. It only needs to import the symbols from PostgreSQL header files and not export those. What happens is that -DBUILDING_DLL causes PGDLLIMPORT marker to expand to __declspec(dllexport) instead of the usual __declspec(dllimport). This leads to a failure during linking of object files. Removing this flag from CPPFLAGS fixes the issue. --- pljava-so/pom.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 042d70d3..11367002 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -9,7 +9,7 @@ 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 @@ -27,7 +27,7 @@ diff --git a/src/site/markdown/install/install.md.vm b/src/site/markdown/install/install.md.vm index 8e812c1a..734a332b 100644 --- a/src/site/markdown/install/install.md.vm +++ b/src/site/markdown/install/install.md.vm @@ -20,7 +20,7 @@ $h2 For the impatient After completing the [build][bld]: - java -jar pljava-packaging/target/pljava-pgX.Y-arch-os-link.jar + java -jar pljava-packaging/target/pljava-pgX.jar (run the above with sufficient privilege to write in the PostgreSQL installation directories, or read further for how to install in @@ -30,8 +30,8 @@ After completing the [build][bld]: CREATE EXTENSION pljava; GRANT USAGE ON LANGUAGE java TO ...; -- see "Usage permission" below -where *pgX.Y* represents the PostgreSQL version, and *arch*, *os*, and -*link* are ... wait, you're impatient, just look in the directory, you'll +where *pgX* represents the PostgreSQL version, and +... wait, you're impatient, just look in the directory, you'll see the jar file there. *Upgrading an older PL/Java installation? Use From f0e8509a0ef7c4b354d475c457289cdcc636cb1c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 13 Sep 2020 14:35:27 -0400 Subject: [PATCH 0694/1087] Use more casual term 'state machine' than 'dfa' There was already another one added in 4e001ae, and there the less-jargon-y term 'state machine' was used; for consistency, use that in Node also. Anyway, because the states can be arbitrary lambdas, it doesn't have to be strictly a DFA; it would be simple to implement a pushdown automaton, for example (or just call another stateMachine from a state). It is easy to foresee tests that might do such things (to check a sequence of results by descending into the individual ResultSets and into individual rows of those, for example). So stateMachine is just a better name. --- .travis.yml | 6 +- appveyor.yml | 6 +- pljava-packaging/src/main/java/Node.java | 73 ++++++++++++------------ 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/.travis.yml b/.travis.yml index 91423699..521c494d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -73,7 +73,7 @@ script: | import java.sql.Connection import org.postgresql.pljava.packaging.Node import static org.postgresql.pljava.packaging.Node.q - import static org.postgresql.pljava.packaging.Node.dfa + import static org.postgresql.pljava.packaging.Node.stateMachine import static org.postgresql.pljava.packaging.Node.isVoidResultSet String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" @@ -114,7 +114,7 @@ script: | { succeeding = true; // become optimistic, will be using &= below - succeeding &= dfa( + succeeding &= stateMachine( "create extension no result", null, @@ -138,7 +138,7 @@ script: | */ try ( Connection c = n1.connect() ) { - succeeding &= dfa( + succeeding &= stateMachine( "saxon path examples path", null, diff --git a/appveyor.yml b/appveyor.yml index 24a111a8..0ee84b15 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -86,7 +86,7 @@ test_script: import java.sql.Connection import org.postgresql.pljava.packaging.Node import static org.postgresql.pljava.packaging.Node.q - import static org.postgresql.pljava.packaging.Node.dfa + import static org.postgresql.pljava.packaging.Node.stateMachine import static org.postgresql.pljava.packaging.Node.isVoidResultSet System.setErr(System.out); // PowerShell makes a mess of stderr output @@ -134,7 +134,7 @@ test_script: { succeeding = true; // become optimistic, will be using &= below - succeeding &= dfa( + succeeding &= stateMachine( "create extension no result", null, @@ -158,7 +158,7 @@ test_script: */ try ( Connection c = n1.connect() ) { - succeeding &= dfa( + succeeding &= stateMachine( "saxon path examples path", null, diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 7546fa30..2c65be89 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1257,15 +1257,16 @@ public static Stream q(final Statement s, Callable work) * interleaved with any {@code SQLWarning}s reported on the result set, or * an {@code SQLException} if one is thrown. *

    - * This is supplied chiefly for use driving a {@link #dfa DFA} to verify + * This is supplied chiefly for use driving a + * {@link #stateMachine state machine} to verify * contents of a result set. For each row, the element in the stream will be * an instance of {@code Long}, counting up from 1 (intended to match the * result set's {@code getRow} but without relying on it, as JDBC does not * require every implementation to support it). By itself, of course, this * does not convey any of the content of the row; the lambdas representing - * the DFA states should close over the result set and query it for content, - * perhaps not even using the object supplied here (except to detect when it - * is a warning or exception rather than a row number). + * the machine states should close over the result set and query it for + * content, perhaps not even using the object supplied here (except to + * detect when it is a warning or exception rather than a row number). * The row position of the result set will have been updated, and should not * be otherwise modified when this method is being used to walk through the * results. @@ -1274,8 +1275,9 @@ public static Stream q(final Statement s, Callable work) * in any way. The {@code ResultSet} will only be read forward, and each row * only once. Simple filtering, {@code dropWhile}/{@code takeWhile}, and so * on will work, but may be more conveniently rolled into the design of a - * {@link #dfa DFA}, as nearly any use of a {@code ResultSet} can throw - * {@code SQLException} and therefore isn't convenient in the stream API. + * {@link #stateMachine state machine}, as nearly any use of a + * {@code ResultSet} can throw {@code SQLException} and therefore isn't + * convenient in the stream API. *

    * Passing this result to {@code qp} as if it came from a {@code Statement} * could lead to confusion, as the {@code Long} elements would be printed as @@ -1396,14 +1398,14 @@ public static Stream q(final ResultSet rs) * {@code ResultSet}. *

    * This is another convenience method for use chiefly in driving a - * {@link #dfa DFA} to check per-column values or metadata for a - * {@code ResultSet}. It is, in fact, nothing other than + * {@link #stateMachine state machine} to check per-column values + * or metadata for a {@code ResultSet}. It is, in fact, nothing other than * {@code IntStream.rangeClosed(1, rsmd.getColumnCount()).boxed()} but typed * as {@code Stream}. *

    * As with {@link #q(ResultSet) q(ResultSet)}, the column number supplied * here conveys no actual column data or metadata. The lambdas representing - * the DFA states should close over the {@code ResultSetMetaData} or + * the machine states should close over the {@code ResultSetMetaData} or * corresponding {@code ResultSet} object, or both, and use the column * number from this stream to index them. * @param rsmd a ResultSetMetaData object @@ -1422,14 +1424,14 @@ public static Stream q(final ResultSetMetaData rsmd) * {@code PreparedStatement}. *

    * This is another convenience method for use chiefly in driving a - * {@link #dfa DFA} to check per-parameter metadata. It is, in fact, - * nothing other than + * {@link #stateMachine state machine} to check per-parameter metadata. + * It is, in fact, nothing other than * {@code IntStream.rangeClosed(1, rsmd.getParameterCount()).boxed()} * but typed as {@code Stream}. *

    * As with {@link #q(ResultSet) q(ResultSet)}, the column number supplied * here conveys no actual parameter metadata. The lambdas representing - * the DFA states should close over the {@code ParameterMetaData} object + * the machine states should close over the {@code ParameterMetaData} object * and use the parameter number from this stream to index it. * @param pmd a ParameterMetaData object * @return a Stream as described above @@ -1993,13 +1995,13 @@ public static boolean isVoidResultSet(Object o, int rows, int columns) } /** - * Executes a deterministic finite automaton (DFA) specified in the form of - * a list of lambdas representing states of the DFA, to verify that a + * Executes a state machine specified in the form of + * a list of lambdas representing its states, to verify that a * {@link #q(Statement,Callable) result stream} is as expected. *

    * Treats the list of lambdas as a set of consecutively-numbered states * (the first in the list is state number 1, and is the initial state). - * At each automaton step, the current state is applied to the current + * At each step of the machine, the current state is applied to the current * input object, and may return an {@code Integer} or a {@code Boolean}. *

    * If an integer, its absolute value selects the next state. A positive @@ -2008,15 +2010,15 @@ public static boolean isVoidResultSet(Object o, int rows, int columns) * selected next state without consuming the current input item, so it will * be examined again in the newly selected state. *

    - * If boolean, {@code false} indicates that the DFA cannot proceed; the + * If boolean, {@code false} indicates that the machine cannot proceed; the * supplied reporter will be passed an explanatory string and this * method returns false. A state that returns {@code true} indicates the - * automaton has reached an accepting state. + * machine has reached an accepting state. *

    * No item of input is allowed to be null; null is reserved to be the * end-of-input symbol. If a state returns {@code true} (accept) - * when applied to null at the end of input, the DFA has matched and this - * method returns true. A state may also return a negative integer in + * when applied to null at the end of input, the machine has matched and + * this method returns true. A state may also return a negative integer in * this case, to shift to another state while looking at the end of input. * A positive integer (attempting to consume the end of input), or a false, * return will cause an explanatory message to the reporter and a @@ -2024,7 +2026,7 @@ public static boolean isVoidResultSet(Object o, int rows, int columns) *

    * A state may return {@code true} (accept) when looking at a non-null * input item, but the input will be checked to confirm it has no more - * elements. Otherwise, the automaton has tried to accept before matching + * elements. Otherwise, the machine has tried to accept before matching * all the input, and this method will return false. *

    * To avoid defining a new functional interface, each state is represented @@ -2041,20 +2043,20 @@ public static boolean isVoidResultSet(Object o, int rows, int columns) * The {@link #as as} method combines those operations. If its argument * either is null or cannot be cast to the wanted type, {@code as} will * throw a specific instance of {@code ClassCastException}, which will be - * treated, when caught by {@code dfa}, just as if the state had returned - * {@code false}. - * @param name A name for this dfa, used only in exception messages if it - * fails to match - * @param reporter a Consumer to accept a diagnostic string if the DFA fails - * to match, defaulting if null to System.err::println + * treated, when caught by {@code stateMachine}, just as if the state + * had returned {@code false}. + * @param name A name for this state machine, used only in exception + * messages if it fails to match all the input + * @param reporter a Consumer to accept a diagnostic string if the machine + * fails to match, defaulting if null to System.err::println * @param input A Stream of input items, of which none may be null - * @param states Lambdas representing states of the DFA + * @param states Lambdas representing states of the machine * @return true if an accepting state was reached coinciding with the end * of input * @throws Exception Anything that could be thrown during evaluation of the * input stream or any state */ - public static boolean dfa( + public static boolean stateMachine( String name, Consumer reporter, Stream input, InvocationHandler... states) throws Exception @@ -2081,7 +2083,8 @@ public static boolean dfa( ++ inputCount; if ( null == currentInput ) throw new UnsupportedOperationException( - "Input to dfa() must not contain null values"); + "Input to stateMachine() must " + + "not contain null values"); hasCurrent = true; } @@ -2092,7 +2095,7 @@ public static boolean dfa( if ( (Boolean)result && ! in.hasNext() ) return true; reporter.accept(String.format( - "dfa \"%s\" in state %d at step %d: %s", + "stateMachine \"%s\" in state %d at step %d: %s", name, 1 + currentState, stepCount, (Boolean)result ? String.format( "transitioned to ACCEPT after %d input items but " + @@ -2127,7 +2130,7 @@ else if ( result instanceof Integer && 0 > (Integer)result ) } reporter.accept(String.format( - "dfa \"%s\" in state %d at step %d: " + + "stateMachine \"%s\" in state %d at step %d: " + "does not accept at end of input after %d items", name, 1 + currentState, stepCount, inputCount)); return false; @@ -2138,10 +2141,10 @@ else if ( result instanceof Integer && 0 > (Integer)result ) * Casts o to class clazz, testing it also for null. *

    * This is meant as a shorthand in implementing states for - * {@link #dfa dfa}. If o either is null or is not castable to the - * desired type, a distinguished instance of {@code ClassCastException} will - * be thrown, which is treated specially if caught by {@code dfa} while - * evaluating a state. + * {@link #stateMachine stateMachine}. If o either is null or + * is not castable to the desired type, a distinguished instance of + * {@code ClassCastException} will be thrown, which is treated specially + * if caught by {@code stateMachine} while evaluating a state. */ public static T as(Class clazz, Object o) { From 1d809835fbdd2801a0e9eefffd19f447ffe0a47c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 13 Sep 2020 19:58:04 -0400 Subject: [PATCH 0695/1087] Accept possible flags in pg_config --cc In some cases, pg_config can sneak flags into the reported CC value. For example, a simple source build of PostgreSQL 12 in my Linux environment results in "gcc -std=gnu99" as the value of CC. So it seems it has to be treated as a list, then its first element used as the executable, and any others as flags. --- pljava-so/pom.xml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index 45baa969..c8830e32 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -286,7 +286,19 @@ var pgxs = new org.postgresql.pljava.pgxs.AbstractPGXS(implementation); var files = utils.getFilesWithExtension(source_path, ".c"); + var compile_flags = new ArrayList(); + var link_flags = new ArrayList(); + /* + * pg_config can sometimes sneak options into the value of CC (for example, + * gcc -std=gnu99). Add it to compile_flags as a list, then snatch the first + * element back out to use as the CC executable. Copy any remaining flags + * to link_flags also. + */ + compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cc)); + cc = compile_flags.remove(0); + link_flags.addAll(compile_flags); + compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags)); compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cppflags)); compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags_sl)); @@ -296,7 +308,6 @@ .MojoExecutionException("Compilation failed with exit code: " + exitCode); var object_files = utils.getFilesWithExtension(target_path, extension); - var link_flags = new ArrayList(); link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags)); link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_sl)); link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_ex)); From 531503e353bd6753c2a2983e22a7b8829c2c9d93 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 13 Sep 2020 19:59:51 -0400 Subject: [PATCH 0696/1087] Mark my_vfprintf as a printf-like function Not a bug, but silences a warning that may appear in some environments (and is visible now that the old warning chaff is so much reduced!). --- pljava-so/src/main/c/Backend.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index a950faae..5ae65f1a 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -169,7 +169,8 @@ static void JVMOptList_addVisualVMName(JVMOptList*); static void JVMOptList_addModuleMain(JVMOptList*); static void addUserJVMOptions(JVMOptList*); static char* getModulePath(const char*); -static jint JNICALL my_vfprintf(FILE*, const char*, va_list); +static jint JNICALL my_vfprintf(FILE*, const char*, va_list) + pg_attribute_printf(2, 0); static void _destroyJavaVM(int, Datum); static void initPLJavaClasses(void); static void initJavaSession(void); From b7c01e83b70d7e66b0ab7187b442305f3de68464 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 13 Sep 2020 20:46:22 -0400 Subject: [PATCH 0697/1087] Fix level of a debugging message This internal muttering was being logged at INFO, and so showing up in CI test output. --- .../main/java/org/postgresql/pljava/management/Commands.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 549d0ef2..a64848e5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -1026,7 +1026,7 @@ private static String getFullSqlNameOwned(String sqlTypeName) throws SQLException { Oid typeId = Oid.forTypeName(sqlTypeName); - s_logger.info("Type id = " + typeId.toString()); + s_logger.finer("Type id = " + typeId.toString()); AclId invoker = AclId.getOuterUser(); From d75030a7568d06f80d61cc44c240ccab9268c0f2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 14 Jul 2020 22:50:15 -0400 Subject: [PATCH 0698/1087] Make Checked more nearly orthogonal Complete the set of ToFooFunction interfaces, for Foo in byte, short, char, float, plus Predicate, so that the reference and all primitive types are covered; complete the in(...) methods to cover the full set of those functions. Let the new functional interfaces that do not have Java API checked-exception-less counterparts, and therefore do not strictly need to have ederWrap methods, inherit a no-op default one anyway. That eliminates arbitrary limits on which ones can participate in the use(...).in(...) idiom. --- .../postgresql/pljava/internal/Checked.java | 187 +++++++++++++++++- 1 file changed, 184 insertions(+), 3 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java index c4b53cc2..bd9dafde 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java @@ -29,7 +29,7 @@ * {@code boolean}. To allow a more orthogonal API for access to datum values, * those are provided here, again supporting checked exceptions. Because these * "bonus" types do not have checked-exception-less counterparts in the Java - * API, they have no need for the wrapper methods described next. + * API, they do not strictly need the wrapper methods described next. *

    * For interoperating with Java APIs that require the Java no-checked-exceptions * versions of these interfaces, each checked interface here (for which a Java @@ -46,7 +46,7 @@ * Writer w = ...; * try { * Checked.Consumer.use((String s) -> w.write(s)) // throws IOException! - * .in(c -> strs.forEach(c)); + * .in(c -> strs.forEach(c)); * } * catch ( IOException e ) { ... } * @@ -63,11 +63,18 @@ * cannot accept them, it can be useful as long as the intervening code through * which the exception may be 'flown' is simple and short. *

    + * The functional interfaces defined here that do not correspond to a + * Java API no-checked version, while not strictly needing an {@code ederWrap} + * method, have one anyway, a no-op identity function. That avoids arbitrary + * limits on which ones can participate in the {@code use(...).in(...)} idiom. + *

    * Static {@code composed()} methods are provided here in place of the instance * {@code compose} or {@code andThen} methods in Java's function API, which seem * to challenge {@code javac}'s type inference when exception types are thrown * in. A static {@code composed} method can substitute for {@code compose} or - * {@code andThen}, by ordering the parameters as desired. + * {@code andThen}, by ordering the parameters as desired. Likewise, static + * {@code and} and {@code or} methods are provided in place of the instance + * methods on Java's {@code Predicate}. *

    * Each functional interface declared here has a static {@code use(...)} method * that can serve, as a concise alternative to casting, to constrain the type @@ -83,6 +90,26 @@ static E ederThrow(Throwable t) throws E WT ederWrap(); + /** + * Superinterface of the functional interfaces declared here that do + * not have checked-exception-less counterparts in the Java API. + *

    + * These can all inherit a no-op default {@code ederWrap} that returns the + * instance unchanged, allowing them also to participate in the + * {@code use(...).in(...)} idiom for stylistic consistency even if it is + * not strictly necessary. + */ + interface Trivial, EX extends Throwable> + extends Checked + { + @Override + @SuppressWarnings("unchecked") + default WT ederWrap() + { + return (WT) this; + } + } + default void in(Consumer c) throws EX, RX @@ -118,6 +145,61 @@ long in(ToLongFunction f) return f.apply(ederWrap()); } + default + boolean in(Predicate f) + throws EX, RX + { + return f.test(ederWrap()); + } + + default + byte in(ToByteFunction f) + throws EX, RX + { + return f.apply(ederWrap()); + } + + default + short in(ToShortFunction f) + throws EX, RX + { + return f.apply(ederWrap()); + } + + default + char in(ToCharFunction f) + throws EX, RX + { + return f.apply(ederWrap()); + } + + default + float in(ToFloatFunction f) + throws EX, RX + { + return f.apply(ederWrap()); + } + + /* + * Short-circuiting predicate combinators. + */ + + static + Predicate and( + Predicate first, + Predicate after) + { + return t -> first.test(t) && after.test(t); + } + + static + Predicate or( + Predicate first, + Predicate after) + { + return t -> first.test(t) || after.test(t); + } + /* * composed() methods. */ @@ -361,6 +443,7 @@ static LongSupplier use(LongSupplier o) @FunctionalInterface interface ByteSupplier + extends Trivial, E> { byte getAsByte() throws E; @@ -372,6 +455,7 @@ static ByteSupplier use(ByteSupplier o) @FunctionalInterface interface ShortSupplier + extends Trivial, E> { short getAsShort() throws E; @@ -383,6 +467,7 @@ static ShortSupplier use(ShortSupplier o) @FunctionalInterface interface CharSupplier + extends Trivial, E> { char getAsChar() throws E; @@ -394,6 +479,7 @@ static CharSupplier use(CharSupplier o) @FunctionalInterface interface FloatSupplier + extends Trivial, E> { float getAsFloat() throws E; @@ -523,6 +609,96 @@ ToLongFunction use(ToLongFunction o) } } + @FunctionalInterface + interface Predicate + extends Checked, E> + { + boolean test(T t) throws E; + + default Predicate negate() + { + return t -> ! test(t); + } + + @Override + default java.util.function.Predicate ederWrap() + { + return (t) -> + { + try + { + return test(t); + } + catch ( Throwable thw ) + { + throw Checked.ederThrow(thw); + } + }; + } + + static + Predicate use(Predicate o) + { + return o; + } + } + + /* + * Functions without checked-exception-less Java API counterparts. + */ + + @FunctionalInterface + interface ToByteFunction + extends Trivial, E> + { + byte apply(T t) throws E; + + static + ToByteFunction use(ToByteFunction o) + { + return o; + } + } + + @FunctionalInterface + interface ToShortFunction + extends Trivial, E> + { + short apply(T t) throws E; + + static + ToShortFunction use(ToShortFunction o) + { + return o; + } + } + + @FunctionalInterface + interface ToCharFunction + extends Trivial, E> + { + char apply(T t) throws E; + + static + ToCharFunction use(ToCharFunction o) + { + return o; + } + } + + @FunctionalInterface + interface ToFloatFunction + extends Trivial, E> + { + char apply(T t) throws E; + + static + ToFloatFunction use(ToFloatFunction o) + { + return o; + } + } + /* * Consumers that have checked-exception-less counterparts in the Java API. */ @@ -645,6 +821,7 @@ static LongConsumer use(LongConsumer o) @FunctionalInterface interface BooleanConsumer + extends Trivial, E> { void accept(boolean value) throws E; @@ -657,6 +834,7 @@ BooleanConsumer use(BooleanConsumer o) @FunctionalInterface interface ByteConsumer + extends Trivial, E> { void accept(byte value) throws E; @@ -668,6 +846,7 @@ static ByteConsumer use(ByteConsumer o) @FunctionalInterface interface ShortConsumer + extends Trivial, E> { void accept(short value) throws E; @@ -679,6 +858,7 @@ static ShortConsumer use(ShortConsumer o) @FunctionalInterface interface CharConsumer + extends Trivial, E> { void accept(char value) throws E; @@ -690,6 +870,7 @@ static CharConsumer use(CharConsumer o) @FunctionalInterface interface FloatConsumer + extends Trivial, E> { void accept(float value) throws E; From c4dc35b889bc8de0437d2bc145a11ae525223acb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Jul 2020 20:45:04 -0400 Subject: [PATCH 0699/1087] An AutoCloseable with exception-type parameter Java's AutoCloseable does not have a parameter for the thrown exception type. It is declared Exception, and while subinterfaces are "strongly encouraged" to narrow that declaration, that's of no use to the compiler except in code that explicitly uses the subinterface. In passing move the Trivial interface inside a class (arbitrarily, the Closing class just added here), only so it can be made private; it has a default method that performs an unchecked cast, and should only be extended by other interfaces in this compilation unit. --- .../postgresql/pljava/internal/Checked.java | 200 +++++++++++++++--- 1 file changed, 167 insertions(+), 33 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java index bd9dafde..2063e5a3 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java @@ -12,6 +12,8 @@ package org.postgresql.pljava.internal; import java.util.NoSuchElementException; +import static java.util.Objects.requireNonNull; +import java.util.stream.BaseStream; /** * Functional interfaces handling checked exceptions. @@ -79,6 +81,11 @@ * Each functional interface declared here has a static {@code use(...)} method * that can serve, as a concise alternative to casting, to constrain the type * of a lambda expression when the compiler won't infer it. + *

    + * A {@link AutoCloseable variant of AutoCloseable} with an exception-type + * parameter, and some {@link #closing(AutoCloseable) closing} methods (inspired + * by Python, for use with resources that do not already implement + * {@code AutoCloseable}), are also provided. */ public interface Checked { @@ -90,26 +97,6 @@ static E ederThrow(Throwable t) throws E WT ederWrap(); - /** - * Superinterface of the functional interfaces declared here that do - * not have checked-exception-less counterparts in the Java API. - *

    - * These can all inherit a no-op default {@code ederWrap} that returns the - * instance unchanged, allowing them also to participate in the - * {@code use(...).in(...)} idiom for stylistic consistency even if it is - * not strictly necessary. - */ - interface Trivial, EX extends Throwable> - extends Checked - { - @Override - @SuppressWarnings("unchecked") - default WT ederWrap() - { - return (WT) this; - } - } - default void in(Consumer c) throws EX, RX @@ -260,6 +247,153 @@ LongConsumer composed( }; } + /** + * Version of {@link java.lang.AutoCloseable} with an exception-type + * parameter. + *

    + * This does not need {@code use} or {@code ederWrap} methods because Java's + * {@code AutoCloseable} already allows checked exceptions. The only trouble + * with the Java one is it can't be parameterized to narrow the thrown type + * from {@code Exception}. In Java's API docs, implementers are "strongly + * encouraged" to narrow their {@code throws} clauses, but that's only + * helpful where the compiler sees the specific implementing class. + */ + @FunctionalInterface + interface AutoCloseable + extends java.lang.AutoCloseable + { + @Override + void close() throws E; + } + + /** + * Returns its argument; shorthand for casting a suitable lambda to + * {@code AutoCloseable}. + *

    + * Where some resource does not itself implement {@code AutoCloseable}, an + * idiom like the following, inspired by Python, can be used in Java 10 or + * later, and the compiler will infer that it can throw whatever + * {@code thing.release()} can throw: + *

    +	 *  try(var ac = closing(() -> thing.release()))
    +	 *  {
    +	 *    ...
    +	 *  }
    +	 *
    + *

    + * Pre-Java 10, without {@code var}, you have to specify the exception type, + * but you still get the simple idiom without needing to declare some new + * interface: + *

    +	 *  try(Checked.AutoCloseable<ThingException> ac =
    +	 *		closing(() -> thing.release()))
    +	 *  {
    +	 *    ...
    +	 *  }
    +	 *
    + */ + static + AutoCloseable closing(AutoCloseable o) + { + return o; + } + + /** + * Wrap some payload and a 'closer' lambda as a {@code Closing} instance + * that can supply the payload and implements {@code AutoCloseable} using + * the lambda; useful in a {@code try}-with-resources when the payload + * itself does not implement {@code AutoCloseable}. + */ + static + Closing closing(T payload, AutoCloseable closer) + { + return new Closing<>(payload, closer); + } + + /** + * Given a stream and a lambda that should be invoked when it is closed, + * construct a new stream that runs that lambda when closed, and return a + * {@code Closing} instance with the new stream as its payload, which will + * be closed by the {@code close} action. + *

    + * Intended for use in a {@code try}-with-resources. Any checked exception + * throwable by closer will be remembered as throwable by the + * {@code close} method of the returned {@code Closing} instance (and + * therefore will be considered throwable by the {@code try}-with-resources + * in which it is used. Any other code that calls {@code close} directly on + * the returned stream could be surprised by the checked exception, as a + * stream's {@code close} method is not declared to throw any. When used as + * intended in a {@code try}-with-resources, any such surprise is bounded + * by the scope of that statement. + */ + static , E extends Exception> + Closing closing(S stream, Runnable closer) + { + S newStream = stream.onClose(closer.ederWrap()); + return new Closing<>(newStream, newStream::close); + } + + /** + * A class that can supply a {@code T} while also implementing + * {@code AutoCloseable}; suitable for use in a + * {@code try}-with-resources to wrap some value that does not itself + * implement {@code AutoCloseable}. + *

    + * Obtained via one of the {@code closing} methods above. + */ + /* + * This class also encloses the private interface Trivial, simply to make it + * private (a private interface can only exist within a class) to ensure it + * is only extended by other interfaces in this compilation unit (its + * default method includes an unchecked cast). It did not seem worth + * creating another entire class only to enclose a private interface. + */ + class Closing + implements java.util.function.Supplier, AutoCloseable + { + private final T m_payload; + private final AutoCloseable m_closer; + + private Closing(T payload, AutoCloseable closer) + { + m_payload = payload; + m_closer = requireNonNull(closer); + } + + @Override + public T get() + { + return m_payload; + } + + @Override + public void close() throws E + { + m_closer.close(); + } + + /** + * Superinterface of the functional interfaces declared here that do + * not have checked-exception-less counterparts in Java's API. + *

    + * These can all inherit a no-op default {@code ederWrap} that returns + * the instance unchanged, allowing them also to participate in the + * {@code use(...).in(...)} idiom for stylistic consistency even if it + * is not strictly necessary. + */ + private interface Trivial + , EX extends Throwable> + extends Checked + { + @Override + @SuppressWarnings("unchecked") + default WT ederWrap() + { + return (WT) this; + } + } + } + /* * Runnable. */ @@ -443,7 +577,7 @@ static LongSupplier use(LongSupplier o) @FunctionalInterface interface ByteSupplier - extends Trivial, E> + extends Closing.Trivial, E> { byte getAsByte() throws E; @@ -455,7 +589,7 @@ static ByteSupplier use(ByteSupplier o) @FunctionalInterface interface ShortSupplier - extends Trivial, E> + extends Closing.Trivial, E> { short getAsShort() throws E; @@ -467,7 +601,7 @@ static ShortSupplier use(ShortSupplier o) @FunctionalInterface interface CharSupplier - extends Trivial, E> + extends Closing.Trivial, E> { char getAsChar() throws E; @@ -479,7 +613,7 @@ static CharSupplier use(CharSupplier o) @FunctionalInterface interface FloatSupplier - extends Trivial, E> + extends Closing.Trivial, E> { float getAsFloat() throws E; @@ -649,7 +783,7 @@ Predicate use(Predicate o) @FunctionalInterface interface ToByteFunction - extends Trivial, E> + extends Closing.Trivial, E> { byte apply(T t) throws E; @@ -662,7 +796,7 @@ ToByteFunction use(ToByteFunction o) @FunctionalInterface interface ToShortFunction - extends Trivial, E> + extends Closing.Trivial, E> { short apply(T t) throws E; @@ -675,7 +809,7 @@ ToShortFunction use(ToShortFunction o) @FunctionalInterface interface ToCharFunction - extends Trivial, E> + extends Closing.Trivial, E> { char apply(T t) throws E; @@ -688,7 +822,7 @@ ToCharFunction use(ToCharFunction o) @FunctionalInterface interface ToFloatFunction - extends Trivial, E> + extends Closing.Trivial, E> { char apply(T t) throws E; @@ -821,7 +955,7 @@ static LongConsumer use(LongConsumer o) @FunctionalInterface interface BooleanConsumer - extends Trivial, E> + extends Closing.Trivial, E> { void accept(boolean value) throws E; @@ -834,7 +968,7 @@ BooleanConsumer use(BooleanConsumer o) @FunctionalInterface interface ByteConsumer - extends Trivial, E> + extends Closing.Trivial, E> { void accept(byte value) throws E; @@ -846,7 +980,7 @@ static ByteConsumer use(ByteConsumer o) @FunctionalInterface interface ShortConsumer - extends Trivial, E> + extends Closing.Trivial, E> { void accept(short value) throws E; @@ -858,7 +992,7 @@ static ShortConsumer use(ShortConsumer o) @FunctionalInterface interface CharConsumer - extends Trivial, E> + extends Closing.Trivial, E> { void accept(char value) throws E; @@ -870,7 +1004,7 @@ static CharConsumer use(CharConsumer o) @FunctionalInterface interface FloatConsumer - extends Trivial, E> + extends Closing.Trivial, E> { void accept(float value) throws E; From b284fbfe4d8d382e04966898eaceead4b2f83c57 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Jul 2020 21:04:28 -0400 Subject: [PATCH 0700/1087] New Optionals missing equals/hashCode/ofNullable They're also all missing stream(), but fixing that would require implementing the missing primitive stream types. Provide ofNullable (with a boxed parameter) for primitive-typed Optionals (Java's as well as these). Provide them as overloaded static methods in a single class, so one import static gets them all. --- .../postgresql/pljava/internal/Checked.java | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java index 2063e5a3..6f7b356e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java @@ -11,6 +11,9 @@ */ package org.postgresql.pljava.internal; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.NoSuchElementException; import static java.util.Objects.requireNonNull; import java.util.stream.BaseStream; @@ -1016,6 +1019,12 @@ static FloatConsumer use(FloatConsumer o) /* * Optionals without checked-exception-less counterparts in the Java API. + * + * Rather than following Java's odd "Value-Based Class" conventions (which + * would require each class to be final and therefore preclude a None/Some + * implementation), these all have private constructors and constitute an + * effectively sealed hierarchy. Client code can and should treat them as + * value-based classes, and they will behave. */ abstract class OptionalBase @@ -1025,11 +1034,76 @@ public boolean isPresent() return false; } + @Override + public boolean equals(Object obj) + { + /* + * This is the equals() inherited by every EMPTY instance, and + * therefore can only return true when obj is an instance of the + * exact same type. + */ + return null != obj && getClass().equals(obj.getClass()); + } + + @Override + public int hashCode() + { + return 0; + } + @Override public String toString() { return getClass().getSimpleName() + ".empty"; } + + public static OptionalDouble ofNullable(Double value) + { + return null == value ? + OptionalDouble.empty() : OptionalDouble.of(value); + } + + public static OptionalInt ofNullable(Integer value) + { + return null == value ? + OptionalInt.empty() : OptionalInt.of(value); + } + + public static OptionalLong ofNullable(Long value) + { + return null == value ? + OptionalLong.empty() : OptionalLong.of(value); + } + + public static OptionalBoolean ofNullable(Boolean value) + { + return null == value ? + OptionalBoolean.EMPTY : OptionalBoolean.of(value); + } + + public static OptionalByte ofNullable(Byte value) + { + return null == value ? + OptionalByte.EMPTY : OptionalByte.of(value); + } + + public static OptionalShort ofNullable(Short value) + { + return null == value ? + OptionalShort.EMPTY : OptionalShort.of(value); + } + + public static OptionalChar ofNullable(Character value) + { + return null == value ? + OptionalChar.EMPTY : OptionalChar.of(value); + } + + public static OptionalFloat ofNullable(Float value) + { + return null == value ? + OptionalFloat.EMPTY : OptionalFloat.of(value); + } } class OptionalBoolean extends OptionalBase @@ -1097,6 +1171,17 @@ public boolean isPresent() return true; } + /* + * The inherited equals() works here too; this and obj must be both + * of class False or both of class True. + */ + + @Override + public int hashCode() + { + return Boolean.hashCode(getAsBoolean()); + } + @Override public String toString() { @@ -1238,6 +1323,19 @@ public boolean isPresent() return true; } + @Override + public boolean equals(Object obj) + { + return obj instanceof Present + && (m_value == ((Present)obj).m_value); + } + + @Override + public int hashCode() + { + return Byte.hashCode(m_value); + } + @Override public String toString() { @@ -1357,6 +1455,19 @@ public boolean isPresent() return true; } + @Override + public boolean equals(Object obj) + { + return obj instanceof Present + && (m_value == ((Present)obj).m_value); + } + + @Override + public int hashCode() + { + return Short.hashCode(m_value); + } + @Override public String toString() { @@ -1475,6 +1586,19 @@ public boolean isPresent() return true; } + @Override + public boolean equals(Object obj) + { + return obj instanceof Present + && (m_value == ((Present)obj).m_value); + } + + @Override + public int hashCode() + { + return Character.hashCode(m_value); + } + @Override public String toString() { @@ -1593,6 +1717,19 @@ public boolean isPresent() return true; } + @Override + public boolean equals(Object obj) + { + return obj instanceof Present + && (0 == Float.compare(m_value, ((Present)obj).m_value)); + } + + @Override + public int hashCode() + { + return Float.hashCode(m_value); + } + @Override public String toString() { From cf688f8e55d51928edaff339ac5b92d0fd46b388 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Jul 2020 22:51:49 -0400 Subject: [PATCH 0701/1087] Add a test class for Checked Even compile-time only 'tests' with no runtime behavior are enough to flush out some typos and usability issues. The several overloaded in() methods seem to require the compiler to infer more than it always can. Give those distinct names. Much is not yet tested. Everything won't be ('everything' would include the Cartesian product of everything with a use() method and the ten flavors of in...() method ... fuhgeddaboudit. --- .../postgresql/pljava/internal/Checked.java | 20 +-- pljava/src/test/java/CheckedTest.java | 124 ++++++++++++++++++ 2 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 pljava/src/test/java/CheckedTest.java diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java index 6f7b356e..44ac2ded 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java @@ -108,63 +108,63 @@ void in(Consumer c) } default - RT in(Function f) + RT inReturning(Function f) throws EX, RX { return f.apply(ederWrap()); } default - double in(ToDoubleFunction f) + double inDoubleReturning(ToDoubleFunction f) throws EX, RX { return f.apply(ederWrap()); } default - int in(ToIntFunction f) + int inIntReturning(ToIntFunction f) throws EX, RX { return f.apply(ederWrap()); } default - long in(ToLongFunction f) + long inLongReturning(ToLongFunction f) throws EX, RX { return f.apply(ederWrap()); } default - boolean in(Predicate f) + boolean inBooleanReturning(Predicate f) throws EX, RX { return f.test(ederWrap()); } default - byte in(ToByteFunction f) + byte inByteReturning(ToByteFunction f) throws EX, RX { return f.apply(ederWrap()); } default - short in(ToShortFunction f) + short inShortReturning(ToShortFunction f) throws EX, RX { return f.apply(ederWrap()); } default - char in(ToCharFunction f) + char inCharReturning(ToCharFunction f) throws EX, RX { return f.apply(ederWrap()); } default - float in(ToFloatFunction f) + float inFloatReturning(ToFloatFunction f) throws EX, RX { return f.apply(ederWrap()); @@ -827,7 +827,7 @@ ToCharFunction use(ToCharFunction o) interface ToFloatFunction extends Closing.Trivial, E> { - char apply(T t) throws E; + float apply(T t) throws E; static ToFloatFunction use(ToFloatFunction o) diff --git a/pljava/src/test/java/CheckedTest.java b/pljava/src/test/java/CheckedTest.java new file mode 100644 index 00000000..2d1da5d9 --- /dev/null +++ b/pljava/src/test/java/CheckedTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020 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.internal; + +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import java.util.stream.Stream; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +import static org.postgresql.pljava.internal.Checked.closing; +import static org.postgresql.pljava.internal.Checked.OptionalBase.ofNullable; + +public class CheckedTest +{ + public void compilability() + { + try + { + Checked.Consumer + .use((String n) -> { Class.forName(n); }) + .in(l -> { Stream.of("Foo").forEach(l); }); + } + catch ( ClassNotFoundException e ) + { + } + + try + { + Checked.DoubleConsumer + .use(v -> {throw new IllegalAccessException();}) + .in(l -> { DoubleStream.of(4.2).forEach(l); }); + + Checked.IntConsumer + .use(v -> {throw new IllegalAccessException();}) + .in(l -> { IntStream.of(42).forEach(l); }); + + Checked.LongConsumer + .use(v -> {throw new IllegalAccessException();}) + .in(l -> { LongStream.of(4).forEach(l); }); + + Checked.Runnable + .use(() -> {throw new IllegalAccessException();}) + .in(r -> { Stream.of("").forEach(o -> {r.run();}); }); + } + catch ( IllegalAccessException e ) + { + } + + Boolean zl = + Checked.Supplier + .use(() -> Boolean.TRUE) + .inReturning(s -> s.get()); + + boolean z = + Checked.BooleanSupplier + .use(() -> true) + .inBooleanReturning(zs -> zs.getAsBoolean()); + + double d = + Checked.DoubleSupplier + .use(() -> 4.2) + .inDoubleReturning(ds -> ds.getAsDouble()); + + int i = + Checked.IntSupplier + .use(() -> 4) + .inIntReturning(is -> is.getAsInt()); + + long j = + Checked.LongSupplier + .use(() -> 4) + .inLongReturning(ls -> ls.getAsLong()); + + byte b = + Checked.ByteSupplier + .use(() -> 4) + .inByteReturning(bs -> bs.getAsByte()); + + short s = + Checked.ShortSupplier + .use(() -> 4) + .inShortReturning(ss -> ss.getAsShort()); + + char c = + Checked.CharSupplier + .use(() -> 4) + .inCharReturning(cs -> cs.getAsChar()); + + float f = + Checked.FloatSupplier + .use(() -> 2.4f) + .inFloatReturning(fs -> fs.getAsFloat()); + + try (Checked.AutoCloseable ac = // Java 10: var + closing(() -> {throw new IllegalAccessException();})) + { + } + catch ( IllegalAccessException e ) + { + } + + OptionalDouble opd = ofNullable(4.2); + OptionalInt opi = ofNullable(4); + OptionalLong opj = ofNullable(4L); + Checked.OptionalBoolean opz = ofNullable(true); + Checked.OptionalByte opb = ofNullable((byte)2); + Checked.OptionalShort ops = ofNullable((short)2); + Checked.OptionalChar opc = ofNullable('2'); + Checked.OptionalFloat opf = ofNullable(2f); + } +} From 0a83c39f434ced663c055f5028575f53f020601a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Sep 2020 18:40:49 -0400 Subject: [PATCH 0702/1087] Avoid MSVC TZ workaround in PG 10.2 and later In 10.2, session_timezone gained a PGDLLIMPORT marking, making the workaround unnecessary. This typo'd conditional caused it to be used in later 10.x versions anyway. Tempting to blame the change in PG_VERSION_NUM format with PG 10 (in 958fe54), but the value here was wrong for the old format too. Addresses #297. Apparently the workaround wasn't a perfect one, either. --- pljava-so/src/main/c/type/Timestamp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 1963194f..2cbdba9e 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -406,7 +406,7 @@ static Datum _Timestamptz_coerceObject(Type self, jobject ts) static int32 Timestamp_getTimeZone(pg_time_t time) { #if defined(_MSC_VER) && ( \ - 100000<=PG_VERSION_NUM && PG_VERSION_NUM<102000 || \ + 100000<=PG_VERSION_NUM && PG_VERSION_NUM<100002 || \ 90600<=PG_VERSION_NUM && PG_VERSION_NUM< 90607 || \ 90500<=PG_VERSION_NUM && PG_VERSION_NUM< 90511 || \ 90400<=PG_VERSION_NUM && PG_VERSION_NUM< 90416 || \ From 07499e37c3f6f58d8b24289461f5c16472c25d48 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Sep 2020 20:30:51 -0400 Subject: [PATCH 0703/1087] Avoid MSVC TZ workaround in PG 10.2 and later In 10.2, session_timezone gained a PGDLLIMPORT marking, making the workaround unnecessary. This typo'd conditional caused it to be used in later 10.x versions anyway. Tempting to blame the change in PG_VERSION_NUM format with PG 10 (in 958fe54), but the value here was wrong for the old format too. Addresses #297. Apparently the workaround wasn't a perfect one, either. Backpatched from 0a83c39. --- pljava-so/src/main/c/type/Timestamp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 1963194f..2cbdba9e 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -406,7 +406,7 @@ static Datum _Timestamptz_coerceObject(Type self, jobject ts) static int32 Timestamp_getTimeZone(pg_time_t time) { #if defined(_MSC_VER) && ( \ - 100000<=PG_VERSION_NUM && PG_VERSION_NUM<102000 || \ + 100000<=PG_VERSION_NUM && PG_VERSION_NUM<100002 || \ 90600<=PG_VERSION_NUM && PG_VERSION_NUM< 90607 || \ 90500<=PG_VERSION_NUM && PG_VERSION_NUM< 90511 || \ 90400<=PG_VERSION_NUM && PG_VERSION_NUM< 90416 || \ From b1a550cc5d404c0af3de9fb801562da3cf9c286f Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 11 Sep 2020 17:57:12 +0530 Subject: [PATCH 0704/1087] Return MavenReportException from executeReport If an exception occurs during execution of ReportScriptingMojo, it should be thrown so that Maven knows about it and stops the execution immediately. However, Nashorn and Graal wrap exceptions thrown during execution in different ways. Nashorn wraps the original exception twice whereas Graal consumes the original exception and always throws a Polyglot exception wrapped in ScriptException. Therefore, directly throwing an exception can obscure the actual cause. Hence, we return the actual exception, if one occurred, from the method. If execution completes successfully, null is returned instead. --- pljava-api/pom.xml | 8 ++++++++ pljava-examples/pom.xml | 7 +++++++ pljava-packaging/pom.xml | 8 ++++++++ pljava-pgxs/pom.xml | 8 ++++++++ .../java/org/postgresql/pljava/pgxs/ReportScript.java | 6 +++++- .../org/postgresql/pljava/pgxs/ReportScriptingMojo.java | 7 +++++-- pljava/pom.xml | 8 ++++++++ 7 files changed, 49 insertions(+), 3 deletions(-) diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index cc2b1bc9..99a5f981 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -86,6 +86,8 @@ function isExternalReport(report) } function executeReport(report, locale) +{ +try { var title = report.project.name + " " + report.project.version; @@ -156,6 +158,12 @@ function executeReport(report, locale) var task = tool.getTask(null, null, diagListener, null, args, null); task.call(); + return null; +} +catch(e) +{ + return new org.apache.maven.reporting.MavenReportException(e); +} } ]]> diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 478eba6c..dcd10b92 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -109,6 +109,7 @@ function isExternalReport(report) function executeReport(report, locale) { +try { var paths = buildPaths(report.project.compileClasspathElements); var title = report.project.name + " " + report.project.version; @@ -234,6 +235,12 @@ function executeReport(report, locale) var task = tool.getTask(null, rmgr, diagListener, null, args, null); task.call(); + return null; +} +catch(e) +{ + return new org.apache.maven.reporting.MavenReportException(e); +} } ]]> diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 4fe702b0..b021c872 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -220,6 +220,8 @@ function isExternalReport(report) } function executeReport(report, locale) +{ +try { var title = report.project.name + " " + report.project.version; @@ -295,6 +297,12 @@ function executeReport(report, locale) var task = tool.getTask(null, null, diagListener, null, args, null); task.call(); + return null; +} +catch(e) +{ + return new org.apache.maven.reporting.MavenReportException(e); +} } ]]> diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 5ba994ed..77b55310 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -128,6 +128,8 @@ function isExternalReport(report) } function executeReport(report, locale) +{ +try { var paths = buildPaths(report.project.compileClasspathElements); @@ -212,6 +214,12 @@ function executeReport(report, locale) var task = tool.getTask(null, null, diagListener, null, args, null); task.call(); + return null; +} +catch(e) +{ + return new org.apache.maven.reporting.MavenReportException(e); +} } ]]> diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java index 5423fefe..8194920c 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java @@ -12,6 +12,8 @@ */ package org.postgresql.pljava.pgxs; +import org.apache.maven.reporting.MavenReportException; + import java.util.Locale; /** @@ -78,7 +80,9 @@ default boolean canGenerateReport(ReportScriptingMojo report) * @param report instance of {@link ReportScriptingMojo} * @param locale Locale to use for any locale-sensitive content in * the report + * @return null if execution completed successfully, Exception that occurred + * during execution otherwise * @see ReportScriptingMojo#executeReport(Locale) */ - void executeReport(ReportScriptingMojo report, Locale locale); + MavenReportException executeReport(ReportScriptingMojo report, Locale locale); } diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java index 55a28aee..921ca5e4 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java @@ -19,6 +19,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.reporting.AbstractMavenReport; +import org.apache.maven.reporting.MavenReportException; import org.codehaus.plexus.configuration.PlexusConfiguration; import javax.script.Invocable; @@ -191,10 +192,12 @@ public boolean canGenerateReport () * current report. */ @Override - protected void executeReport (Locale locale) + protected void executeReport (Locale locale) throws MavenReportException { setReportScript(); - reportScript.executeReport(this, locale); + MavenReportException exception = reportScript.executeReport(this, locale); + if (exception != null) + throw exception; } /** diff --git a/pljava/pom.xml b/pljava/pom.xml index 259fea59..f1005dfb 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -84,6 +84,8 @@ function isExternalReport(report) } function executeReport(report, locale) +{ +try { var paths = buildPaths(report.project.compileClasspathElements); @@ -186,6 +188,12 @@ function executeReport(report, locale) var task = tool.getTask(null, rmgr, diagListener, null, args, null); task.call(); + return null; +} +catch(e) +{ + return new org.apache.maven.reporting.MavenReportException(e); +} } ]]> From 4f5899b20d4ba08bcc7a42b76c50f44ff4a33481 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 15 Sep 2020 17:29:46 -0400 Subject: [PATCH 0705/1087] Corrections to two examples in javadoc --- .../src/main/java/org/postgresql/pljava/internal/Checked.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java index 44ac2ded..064818e6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java @@ -278,7 +278,7 @@ interface AutoCloseable * later, and the compiler will infer that it can throw whatever * {@code thing.release()} can throw: *

    -	 *  try(var ac = closing(() -> thing.release()))
    +	 *  try(var ac = closing(() -> { thing.release(); }))
     	 *  {
     	 *    ...
     	 *  }
    @@ -289,7 +289,7 @@ interface AutoCloseable
     	 * interface:
     	 *
     	 *  try(Checked.AutoCloseable<ThingException> ac =
    -	 *		closing(() -> thing.release()))
    +	 *		closing(() -> { thing.release(); }))
     	 *  {
     	 *    ...
     	 *  }
    
    From 2cf7becf96dd8548a9a926eb8035faf67e94f82c Mon Sep 17 00:00:00 2001
    From: Kartik Ohri 
    Date: Sat, 12 Sep 2020 01:48:57 +0530
    Subject: [PATCH 0706/1087] Add exceptionWrap method to ReportScript
    
    JavaScript allows to arbitrarily throw anything. Therefore, instead of
    directly wrapping the thrown object inside a MavenReportException, we
    wrap pass it to exceptionWrap to handle it properly.
    ---
     pljava-api/pom.xml                            |  2 +-
     pljava-examples/pom.xml                       |  5 ++-
     pljava-packaging/pom.xml                      |  2 +-
     pljava-pgxs/pom.xml                           |  2 +-
     .../postgresql/pljava/pgxs/ReportScript.java  | 38 +++++++++++++++++++
     pljava/pom.xml                                |  2 +-
     6 files changed, 45 insertions(+), 6 deletions(-)
    
    diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml
    index 99a5f981..eebac123 100644
    --- a/pljava-api/pom.xml
    +++ b/pljava-api/pom.xml
    @@ -162,7 +162,7 @@ try
     }
     catch(e)
     {
    -	return new org.apache.maven.reporting.MavenReportException(e);
    +	return report.exceptionWrap(e);
     }
     }
     ]]>
    diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml
    index dcd10b92..945c0c61 100644
    --- a/pljava-examples/pom.xml
    +++ b/pljava-examples/pom.xml
    @@ -109,7 +109,8 @@ function isExternalReport(report)
     
     function executeReport(report, locale)
     {
    -try {
    +try
    +{
     	var paths = buildPaths(report.project.compileClasspathElements);
     
     	var title = report.project.name + " " + report.project.version;
    @@ -239,7 +240,7 @@ try {
     }
     catch(e)
     {
    -	return new org.apache.maven.reporting.MavenReportException(e);
    +	return report.exceptionWrap(e);
     }
     }
     ]]>
    diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml
    index b021c872..a6cb3faf 100644
    --- a/pljava-packaging/pom.xml
    +++ b/pljava-packaging/pom.xml
    @@ -301,7 +301,7 @@ try
     }
     catch(e)
     {
    -	return new org.apache.maven.reporting.MavenReportException(e);
    +	return report.exceptionWrap(e);
     }
     }
     ]]>
    diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml
    index 77b55310..df9c7c28 100644
    --- a/pljava-pgxs/pom.xml
    +++ b/pljava-pgxs/pom.xml
    @@ -218,7 +218,7 @@ try
     }
     catch(e)
     {
    -	return new org.apache.maven.reporting.MavenReportException(e);
    +	return report.exceptionWrap(e);
     }
     }
     ]]>
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java
    index 8194920c..6ab34aff 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java
    @@ -85,4 +85,42 @@ default boolean canGenerateReport(ReportScriptingMojo report)
     	 * @see ReportScriptingMojo#executeReport(Locale)
     	 */
     	MavenReportException executeReport(ReportScriptingMojo report, Locale locale);
    +
    +	/**
    +	 * Wraps the input object in a {@link MavenReportException}.
    +	 *
    +	 * The exception returned is constructed as follows:
    +	 * 1) If {@code object} is null, the exception message indicates the same.
    +	 * 2) If {@code object} is already a {@link MavenReportException}, return it
    +	 * as is.
    +	 * 3) If {@code object} is any other {@link Throwable}, set it as the cause
    +	 * for the exception.
    +	 * {@link MavenReportException} with {@code object} as its cause.
    +	 * 4) If {@code object} is a {@link String}, set it as the message of the
    +	 * exception.
    +	 * 5) For all other case, the message of the exception is set in this format
    +	 * , Class Name of object: String representation of object.
    +	 *
    +	 * @param object to wrap in MavenReportException
    +	 * @return object wrapped inside a {@link MavenReportException}
    +	 */
    +	default MavenReportException exceptionWrap(Object object)
    +	{
    +		if (object == null)
    +			return new MavenReportException("Script threw a null value");
    +		else if (object instanceof MavenReportException)
    +			return (MavenReportException) object;
    +		else if (object instanceof Throwable)
    +		{
    +			Throwable t = (Throwable) object;
    +			MavenReportException exception = new MavenReportException(t.getMessage());
    +			exception.initCause(t);
    +			return exception;
    +		}
    +		else if (object instanceof String)
    +			return new MavenReportException((String) object);
    +		else
    +			return new MavenReportException(object.getClass().getCanonicalName()
    +				+ ": " + object.toString());
    +	}
     }
    diff --git a/pljava/pom.xml b/pljava/pom.xml
    index f1005dfb..53975222 100644
    --- a/pljava/pom.xml
    +++ b/pljava/pom.xml
    @@ -192,7 +192,7 @@ try
     }
     catch(e)
     {
    -	return new org.apache.maven.reporting.MavenReportException(e);
    +	return report.exceptionWrap(e);
     }
     }
     ]]>
    
    From 1032e68c9f5aefd02109b3c03f321c19e9816db1 Mon Sep 17 00:00:00 2001
    From: Kartik Ohri 
    Date: Tue, 15 Sep 2020 17:41:19 +0530
    Subject: [PATCH 0707/1087] Add GoalScript to execute script using Invocable
    
    Nashorn wraps exceptions thrown during Javascript execution whereas
    Graal consumes them and throws an altogether new exception. In order to
    correctly handle exceptions thrown during execution of the script, we
    return the exception after wrapping it inside MojoExecutionException.
    
    As it is not possible to return a value from the script directly, we use
    the GoalScript interface to execute the script from Java and obtain the
    return value. A non null value indicates unsuccessful completion.
    ---
     pljava-packaging/pom.xml                      |  4 ++
     .../postgresql/pljava/pgxs/GoalScript.java    | 18 ++++++
     .../org/postgresql/pljava/pgxs/PGXSUtils.java | 58 +++++++++++++++++++
     .../postgresql/pljava/pgxs/ScriptingMojo.java | 16 ++++-
     pljava-so/pom.xml                             | 12 +++-
     5 files changed, 106 insertions(+), 2 deletions(-)
     create mode 100644 pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/GoalScript.java
    
    diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml
    index a6cb3faf..b1d3ac81 100644
    --- a/pljava-packaging/pom.xml
    +++ b/pljava-packaging/pom.xml
    @@ -106,6 +106,8 @@
     						
     							
     						
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/GoalScript.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/GoalScript.java
    new file mode 100644
    index 00000000..828dd1d3
    --- /dev/null
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/GoalScript.java
    @@ -0,0 +1,18 @@
    +package org.postgresql.pljava.pgxs;
    +
    +import org.apache.maven.plugin.AbstractMojoExecutionException;
    +
    +/**
    + * Enables obtaining an interface from the script using
    + * {@link javax.script.Invocable} in order to correctly handle errors.
    + */
    +public interface GoalScript {
    +
    +	/**
    +	 * Executes the driver code for running the script.
    +	 * @return MojoExecutionException or MojoFailureException in case of error,
    +	 * null in case of successful execution
    +	 */
    +	AbstractMojoExecutionException execute();
    +
    +}
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    index eac54fca..a17b60e3 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    @@ -12,6 +12,9 @@
      */
     package org.postgresql.pljava.pgxs;
     
    +import org.apache.maven.plugin.AbstractMojoExecutionException;
    +import org.apache.maven.plugin.MojoExecutionException;
    +import org.apache.maven.plugin.MojoFailureException;
     import org.apache.maven.plugin.logging.Log;
     import org.apache.maven.project.MavenProject;
     import org.codehaus.plexus.configuration.PlexusConfiguration;
    @@ -663,4 +666,59 @@ public ProcessBuilder forWindowsCRuntime(ProcessBuilder pb)
     		return pb;
     	}
     
    +	/**
    +	 * Wraps the input object in a {@link MojoExecutionException}.
    +	 *
    +	 * The returned exception is constructed as follows:
    +	 * 1) If {@code object} is null, the exception message indicates the same.
    +	 * 2) If {@code object} is already a {@link MojoExecutionException}, return it
    +	 * as is.
    +	 * 3) If {@code object} is any other {@link Throwable}, set it as the cause
    +	 * for the exception.
    +	 * {@link MojoExecutionException} with {@code object} as its cause.
    +	 * 4) If {@code object} is a {@link String}, set it as the message of the
    +	 * exception.
    +	 * 5) For all other case, the message of the exception is set in this format
    +	 * , Class Name of object: String representation of object.
    +	 *
    +	 * @param object to wrap in MojoExecutionException
    +	 * @return object wrapped inside a {@link MojoExecutionException}
    +	 */
    +	public AbstractMojoExecutionException exceptionWrap(Object object,
    +														boolean scriptFailure)
    +	{
    +		AbstractMojoExecutionException exception;
    +		if (object == null)
    +			exception = new MojoExecutionException("Script threw a null value");
    +		else if (object instanceof MojoExecutionException)
    +			exception = (MojoExecutionException) object;
    +		else if (object instanceof MojoFailureException)
    +			exception = (MojoFailureException) object;
    +		else if (object instanceof Throwable)
    +		{
    +			Throwable t = (Throwable) object;
    +			if (scriptFailure)
    +				exception = new MojoExecutionException(t.getMessage(), t);
    +			else
    +				exception = new MojoFailureException(t.getMessage(), t);
    +		}
    +		else if (object instanceof String)
    +		{
    +			if (scriptFailure)
    +				exception = new MojoExecutionException((String) object);
    +			else
    +				exception = new MojoFailureException((String) object);
    +		}
    +		else
    +		{
    +			String message = object.getClass().getCanonicalName()
    +				+ ": " + object.toString();
    +			if (scriptFailure)
    +				exception = new MojoExecutionException(message);
    +			else
    +				exception = new MojoFailureException(message);
    +		}
    +		return exception;
    +	}
    +
     }
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java
    index f78f95d9..07e7b3a7 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java
    @@ -13,6 +13,9 @@
     package org.postgresql.pljava.pgxs;
     
     import org.apache.maven.plugin.AbstractMojo;
    +import org.apache.maven.plugin.AbstractMojoExecutionException;
    +import org.apache.maven.plugin.MojoExecutionException;
    +import org.apache.maven.plugin.MojoFailureException;
     import org.apache.maven.plugins.annotations.LifecyclePhase;
     import org.apache.maven.plugins.annotations.Mojo;
     import org.apache.maven.plugins.annotations.Parameter;
    @@ -20,6 +23,7 @@
     import org.apache.maven.project.MavenProject;
     import org.codehaus.plexus.configuration.PlexusConfiguration;
     
    +import javax.script.Invocable;
     import javax.script.ScriptContext;
     import javax.script.ScriptEngine;
     import javax.script.ScriptException;
    @@ -50,7 +54,7 @@ public class ScriptingMojo extends AbstractMojo
     	 * configuration.
     	 */
     	@Override
    -	public void execute ()
    +	public void execute () throws MojoExecutionException, MojoFailureException
     	{
     		try
     		{
    @@ -68,6 +72,16 @@ public void execute ()
     			engine.put("getPgConfigProperty",
     				(Function) this::getPgConfigProperty);
     			engine.eval(scriptText);
    +
    +			GoalScript goal = ((Invocable) engine).getInterface(GoalScript.class);
    +			AbstractMojoExecutionException exception = goal.execute();
    +			if (exception != null)
    +			{
    +				if (exception instanceof MojoExecutionException)
    +					throw (MojoExecutionException) exception;
    +				else if (exception instanceof MojoFailureException)
    +					throw (MojoFailureException) exception;
    +			}
     		}
     		catch (ScriptException e)
     		{
    diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml
    index c8830e32..20a48926 100644
    --- a/pljava-so/pom.xml
    +++ b/pljava-so/pom.xml
    @@ -285,6 +285,10 @@
     
     	var pgxs = new org.postgresql.pljava.pgxs.AbstractPGXS(implementation);
     
    +function execute()
    +{
    +try
    +{
     	var files = utils.getFilesWithExtension(source_path, ".c");
     
     	var compile_flags = new ArrayList();
    @@ -315,7 +319,13 @@
     	if (exitCode != 0)
     		throw new org.apache.maven.plugin
     			.MojoExecutionException("Linking failed with exit code: " + exitCode);
    -
    +	return null;
    +}
    +catch(e)
    +{
    +	return utils.exceptionWrap(e);
    +}
    +}
     ]]>
     							
     						
    
    From b16a686bb7c438652ba3bb9de800d3f5a37ca7a2 Mon Sep 17 00:00:00 2001
    From: Kartik Ohri 
    Date: Tue, 15 Sep 2020 13:48:01 +0530
    Subject: [PATCH 0708/1087] Indentation only
    
    ---
     pljava-api/pom.xml       | 144 +++++++++++------------
     pljava-examples/pom.xml  | 238 +++++++++++++++++++--------------------
     pljava-packaging/pom.xml | 142 +++++++++++------------
     pljava-pgxs/pom.xml      | 172 ++++++++++++++--------------
     pljava-so/pom.xml        |  73 ++++++------
     pljava/pom.xml           | 196 ++++++++++++++++----------------
     6 files changed, 481 insertions(+), 484 deletions(-)
    
    diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml
    index eebac123..64ef5edd 100644
    --- a/pljava-api/pom.xml
    +++ b/pljava-api/pom.xml
    @@ -87,83 +87,83 @@ function isExternalReport(report)
     
     function executeReport(report, locale)
     {
    -try
    -{
    -	var title = report.project.name + " " + report.project.version;
    -
    -	var basedir = report.project.basedir.toPath();
    -
    -	var srcroot = java.nio.file.Paths.get("src", "main", "java");
    -	srcroot = resolve(basedir, srcroot);
    -	var srcrooturi = srcroot.toUri();
    -
    -	var jdklink = "https://docs.oracle.com/" +
    -		locale.language + "/java/javase/12/docs/api";
    -
    -	var bottom =
    -		"Copyright © " +
    -		report.project.inceptionYear + "–" + new Date().getFullYear() +
    -		"" +
    -		report.project.organization.name + "";
    -
    -	var args = java.util.List.of(
    -		/*
    -		 * The 'standard options' that javadoc inherits from javac.
    -		 * Do not add --release: it causes -encoding to be ignored (in javadoc
    -		 * 12 through 15, anyway).
    -		 */
    -		"-encoding",            report.inputEncoding,
    -		"--module",             "org.postgresql.pljava",
    -		"--module-source-path", "org.postgresql.pljava=" + srcroot,
    -		/*
    -		 * Core javadoc options.
    -		 * Avoid the legacy package/private/protected/public options; they can
    -		 * clobber the effects of the newer -show-...=... options.
    -		 */
    -		"-locale",              locale.toString(),
    -		"-quiet",
    -		/*
    -		 * Options that are passed to the doclet.
    -		 */
    -		"-author",
    -		"-bottom",              bottom,
    -		"-d",                   report.reportOutputDirectory.toPath()
    -									.resolve("apidocs").toString(),
    -		"-docencoding",         report.outputEncoding,
    -		"-doctitle",            title,
    -		"-link",                jdklink,
    -		//"-sourcetab",         "4", // seemed good idea but implies -linksource
    -		"-use",
    -		"-version",
    -		"-windowtitle",         title
    -	);
    -
    -	debug(args.toString());
    -	var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    -
    -	function diagListener(d)
    +	try
     	{
    -		var s = d.source;
    -
    -		if ( null === s )
    -			s = "";
    -		else
    +		var title = report.project.name + " " + report.project.version;
    +
    +		var basedir = report.project.basedir.toPath();
    +
    +		var srcroot = java.nio.file.Paths.get("src", "main", "java");
    +		srcroot = resolve(basedir, srcroot);
    +		var srcrooturi = srcroot.toUri();
    +
    +		var jdklink = "https://docs.oracle.com/" +
    +			locale.language + "/java/javase/12/docs/api";
    +
    +		var bottom =
    +			"Copyright © " +
    +			report.project.inceptionYear + "–" + new Date().getFullYear() +
    +			"" +
    +			report.project.organization.name + "";
    +
    +		var args = java.util.List.of(
    +			/*
    +			 * The 'standard options' that javadoc inherits from javac.
    +			 * Do not add --release: it causes -encoding to be ignored (in javadoc
    +			 * 12 through 15, anyway).
    +			 */
    +			"-encoding",            report.inputEncoding,
    +			"--module",             "org.postgresql.pljava",
    +			"--module-source-path", "org.postgresql.pljava=" + srcroot,
    +			/*
    +			 * Core javadoc options.
    +			 * Avoid the legacy package/private/protected/public options; they can
    +			 * clobber the effects of the newer -show-...=... options.
    +			 */
    +			"-locale",              locale.toString(),
    +			"-quiet",
    +			/*
    +			 * Options that are passed to the doclet.
    +			 */
    +			"-author",
    +			"-bottom",              bottom,
    +			"-d",                   report.reportOutputDirectory.toPath()
    +										.resolve("apidocs").toString(),
    +			"-docencoding",         report.outputEncoding,
    +			"-doctitle",            title,
    +			"-link",                jdklink,
    +			//"-sourcetab",         "4", // seemed good idea but implies -linksource
    +			"-use",
    +			"-version",
    +			"-windowtitle",         title
    +		);
    +
    +		debug(args.toString());
    +		var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    +
    +		function diagListener(d)
     		{
    -			s = srcrooturi.relativize(s.toUri()).toString();
    -			s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			var s = d.source;
    +
    +			if ( null === s )
    +				s = "";
    +			else
    +			{
    +				s = srcrooturi.relativize(s.toUri()).toString();
    +				s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			}
    +
    +			diag(d.kind, s + d.getMessage(locale));
     		}
     
    -		diag(d.kind, s + d.getMessage(locale));
    +		var task = tool.getTask(null, null, diagListener, null, args, null);
    +		task.call();
    +		return null;
    +	}
    +	catch(e)
    +	{
    +		return report.exceptionWrap(e);
     	}
    -
    -	var task = tool.getTask(null, null, diagListener, null, args, null);
    -	task.call();
    -	return null;
    -}
    -catch(e)
    -{
    -	return report.exceptionWrap(e);
    -}
     }
     ]]>
     							
    diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml
    index 945c0c61..d5b67719 100644
    --- a/pljava-examples/pom.xml
    +++ b/pljava-examples/pom.xml
    @@ -109,139 +109,139 @@ function isExternalReport(report)
     
     function executeReport(report, locale)
     {
    -try
    -{
    -	var paths = buildPaths(report.project.compileClasspathElements);
    -
    -	var title = report.project.name + " " + report.project.version;
    -
    -	var basedir = report.project.basedir.toPath();
    -
    -	var srcroot = java.nio.file.Paths.get("src", "main", "java");
    -	srcroot = resolve(basedir, srcroot);
    -	var srcrooturi = srcroot.toUri();
    +	try
    +	{
    +		var paths = buildPaths(report.project.compileClasspathElements);
    +
    +		var title = report.project.name + " " + report.project.version;
    +
    +		var basedir = report.project.basedir.toPath();
    +
    +		var srcroot = java.nio.file.Paths.get("src", "main", "java");
    +		srcroot = resolve(basedir, srcroot);
    +		var srcrooturi = srcroot.toUri();
    +
    +		var jdklink = "https://docs.oracle.com/" +
    +			locale.language + "/java/javase/12/docs/api";
    +
    +		var apioffline =
    +			basedir.toUri().resolve("../pljava-api/target/site/apidocs");
    +
    +		var bottom =
    +			"Copyright © " +
    +			report.project.inceptionYear + "–" + new Date().getFullYear() +
    +			"" +
    +			report.project.organization.name + "";
    +
    +		var of = java.util.List.of;
    +
    +		var args = of(
    +			/*
    +			 * The 'standard options' that javadoc inherits from javac.
    +			 * Do not add --release: it causes -encoding to be ignored (in javadoc
    +			 * 12 through 15, anyway). -d is documented as a doclet option, but
    +			 * included here to be seen by the RelativizingFileManager (which may
    +			 * otherwise complain that no class output location has been set).
    +			 */
    +			"-d",           report.reportOutputDirectory.toPath()
    +								.resolve("apidocs").toString(),
    +			"-encoding",    report.inputEncoding,
    +			"--class-path", paths.get("classpath"),
    +			"--module-path",paths.get("modulepath"),
    +			"-sourcepath",  srcroot.toString(),
    +			// ^^^ Options recognized by the file manager end here ^^^
    +			"--add-modules","org.postgresql.pljava",
    +			/*
    +			 * Core javadoc options.
    +			 * Avoid the legacy package/private/protected/public options; they can
    +			 * clobber the effects of the newer -show-...=... options.
    +			 */
    +			"-locale",      locale.toString(),
    +			"-quiet",
    +			/*
    +			 * Options that are passed to the doclet.
    +			 */
    +			"-author",
    +			"-bottom",      bottom,
    +			"-docencoding", report.outputEncoding,
    +			"-doctitle",    title,
    +			"-link",        jdklink,
    +			"-sourcetab",   "4",
    +			"-use",
    +			"-version",
    +			"-windowtitle", title
    +		);
     
    -	var jdklink = "https://docs.oracle.com/" +
    -		locale.language + "/java/javase/12/docs/api";
    +		args = new java.util.ArrayList(args);
     
    -	var apioffline =
    -		basedir.toUri().resolve("../pljava-api/target/site/apidocs");
    +		/*
    +		 * Javadoc doesn't learn how to link to modular docs from nonmodular code
    +		 * until 15.
    +		 */
    +		var v = java.lang.Runtime.version();
    +		if ( 0 <= v.compareTo(java.lang.Runtime.Version.parse("15-ea")) )
    +			args.addAll(of("-linkoffline",
    +				"../../RELDOTS/pljava-api/apidocs", apioffline.toString()));
     
    -	var bottom =
    -		"Copyright © " +
    -		report.project.inceptionYear + "–" + new Date().getFullYear() +
    -		"" +
    -		report.project.organization.name + "";
    +		var packages = [
    +			"org.postgresql.pljava.example",
    +			"org.postgresql.pljava.example.annotation"
    +		];
     
    -	var of = java.util.List.of;
    +		if ( isProfileActive('saxon-examples') )
    +			packages.push("org.postgresql.pljava.example.saxon");
     
    -	var args = of(
    -		/*
    -		 * The 'standard options' that javadoc inherits from javac.
    -		 * Do not add --release: it causes -encoding to be ignored (in javadoc
    -		 * 12 through 15, anyway). -d is documented as a doclet option, but
    -		 * included here to be seen by the RelativizingFileManager (which may
    -		 * otherwise complain that no class output location has been set).
    -		 */
    -		"-d",           report.reportOutputDirectory.toPath()
    -							.resolve("apidocs").toString(),
    -		"-encoding",    report.inputEncoding,
    -		"--class-path", paths.get("classpath"),
    -		"--module-path",paths.get("modulepath"),
    -		"-sourcepath",  srcroot.toString(),
    -		// ^^^ Options recognized by the file manager end here ^^^
    -		"--add-modules","org.postgresql.pljava",
     		/*
    -		 * Core javadoc options.
    -		 * Avoid the legacy package/private/protected/public options; they can
    -		 * clobber the effects of the newer -show-...=... options.
    +		 * Add the packages to be documented to args. They could perhaps also
    +		 * be supplied as JavaFileObjects in the compilationUnits parameter,
    +		 * but they are accepted among the option args, so why work any harder?
     		 */
    -		"-locale",      locale.toString(),
    -		"-quiet",
    -		/*
    -		 * Options that are passed to the doclet.
    -		 */
    -		"-author",
    -		"-bottom",      bottom,
    -		"-docencoding", report.outputEncoding,
    -		"-doctitle",    title,
    -		"-link",        jdklink,
    -		"-sourcetab",   "4",
    -		"-use",
    -		"-version",
    -		"-windowtitle", title
    -	);
    -
    -	args = new java.util.ArrayList(args);
    -
    -	/*
    -	 * Javadoc doesn't learn how to link to modular docs from nonmodular code
    -	 * until 15.
    -	 */
    -	var v = java.lang.Runtime.version();
    -	if ( 0 <= v.compareTo(java.lang.Runtime.Version.parse("15-ea")) )
    -		args.addAll(of("-linkoffline",
    -			"../../RELDOTS/pljava-api/apidocs", apioffline.toString()));
    -
    -	var packages = [
    -		"org.postgresql.pljava.example",
    -		"org.postgresql.pljava.example.annotation"
    -	];
    -
    -	if ( isProfileActive('saxon-examples') )
    -		packages.push("org.postgresql.pljava.example.saxon");
    -
    -	/*
    -	 * Add the packages to be documented to args. They could perhaps also
    -	 * be supplied as JavaFileObjects in the compilationUnits parameter,
    -	 * but they are accepted among the option args, so why work any harder?
    -	 */
    -	packages.forEach(function(p) { args.add(p); });
    -
    -	debug(args.toString());
    -	var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    -
    -	function diagListener(d)
    -	{
    -		var s = d.source;
    +		packages.forEach(function(p) { args.add(p); });
     
    -		if ( null === s )
    -			s = "";
    -		else
    +		debug(args.toString());
    +		var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    +
    +		function diagListener(d)
     		{
    -			s = srcrooturi.relativize(s.toUri()).toString();
    -			s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			var s = d.source;
    +
    +			if ( null === s )
    +				s = "";
    +			else
    +			{
    +				s = srcrooturi.relativize(s.toUri()).toString();
    +				s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			}
    +
    +			diag(d.kind, s + d.getMessage(locale));
     		}
     
    -		diag(d.kind, s + d.getMessage(locale));
    -	}
    +		var Charset = Java.type("java.nio.charset.Charset");
     
    -	var Charset = Java.type("java.nio.charset.Charset");
    +		var smgr = tool.getStandardFileManager(
    +				diagListener,
    +				locale,
    +				Charset.forName(report.inputEncoding)
    +			);
     
    -	var smgr = tool.getStandardFileManager(
    -			diagListener,
    -			locale,
    -			Charset.forName(report.inputEncoding)
    -		);
    +		/*
    +		 * A special file manager that will rewrite the RELDOTS seen in -linkoffline
    +		 * above. The options a file manager recognizes must be the first ones in
    +		 * args; handleFirstOptions below returns at the first one the file manager
    +		 * doesn't know what to do with.
    +		 */
    +		var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager(
    +			smgr, Charset.forName(report.outputEncoding));
    +		rmgr.handleFirstOptions(args);
     
    -	/*
    -	 * A special file manager that will rewrite the RELDOTS seen in -linkoffline
    -	 * above. The options a file manager recognizes must be the first ones in
    -	 * args; handleFirstOptions below returns at the first one the file manager
    -	 * doesn't know what to do with.
    -	 */
    -	var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager(
    -		smgr, Charset.forName(report.outputEncoding));
    -	rmgr.handleFirstOptions(args);
    -
    -	var task = tool.getTask(null, rmgr, diagListener, null, args, null);
    -	task.call();
    -	return null;
    -}
    -catch(e)
    -{
    -	return report.exceptionWrap(e);
    -}
    +		var task = tool.getTask(null, rmgr, diagListener, null, args, null);
    +		task.call();
    +		return null;
    +	}
    +	catch(e)
    +	{
    +		return report.exceptionWrap(e);
    +	}
     }
     ]]>
     							
    diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml
    index b1d3ac81..ba56f68a 100644
    --- a/pljava-packaging/pom.xml
    +++ b/pljava-packaging/pom.xml
    @@ -225,88 +225,88 @@ function isExternalReport(report)
     
     function executeReport(report, locale)
     {
    -try
    -{
    -	var title = report.project.name + " " + report.project.version;
    -
    -	var basedir = report.project.basedir.toPath();
    +	try
    +	{
    +		var title = report.project.name + " " + report.project.version;
     
    -	var srcroot = java.nio.file.Paths.get("src", "main", "java");
    -	srcroot = resolve(basedir, srcroot);
    -	var srcrooturi = srcroot.toUri();
    +		var basedir = report.project.basedir.toPath();
     
    -	var jdklink = "https://docs.oracle.com/" +
    -		locale.language + "/java/javase/12/docs/api";
    +		var srcroot = java.nio.file.Paths.get("src", "main", "java");
    +		srcroot = resolve(basedir, srcroot);
    +		var srcrooturi = srcroot.toUri();
     
    -	var bottom =
    -		"Copyright © " +
    -		report.project.inceptionYear + "–" + new Date().getFullYear() +
    -		"" +
    -		report.project.organization.name + "";
    +		var jdklink = "https://docs.oracle.com/" +
    +			locale.language + "/java/javase/12/docs/api";
     
    -	var args = java.util.List.of(
    -		/*
    -		 * The 'standard options' that javadoc inherits from javac.
    -		 * Do not add --release: it causes -encoding to be ignored (in javadoc
    -		 * 12 through 15, anyway). -d is documented as a doclet option, but
    -		 * included here to be seen by the RelativizingFileManager (which may
    -		 * otherwise complain that no class output location has been set).
    -		 */
    -		"-d",           report.reportOutputDirectory.toPath()
    -							.resolve("apidocs").toString(),
    -		"-encoding",    report.inputEncoding,
    -		"-sourcepath",  srcroot.toString(),
    -		/*
    -		 * Core javadoc options.
    -		 * Avoid the legacy package/private/protected/public options; they can
    -		 * clobber the effects of the newer -show-...=... options.
    -		 */
    -		"-locale",      locale.toString(),
    -		"-quiet",
    -		/*
    -		 * Options that are passed to the doclet.
    -		 */
    -		"-author",
    -		"-bottom",      bottom,
    -		"-docencoding", report.outputEncoding,
    -		"-doctitle",    title,
    -		"-link",        jdklink,
    -		"-use",
    -		"-version",
    -		"-windowtitle", title,
    -		/*
    -		 * Source files to document
    -		 */
    -		srcroot.resolve("Node.java").toString(),
    -		srcroot.resolve("JarX.java").toString()
    -	);
    +		var bottom =
    +			"Copyright © " +
    +			report.project.inceptionYear + "–" + new Date().getFullYear() +
    +			"" +
    +			report.project.organization.name + "";
     
    -	debug(args.toString());
    -	var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    +		var args = java.util.List.of(
    +			/*
    +			 * The 'standard options' that javadoc inherits from javac.
    +			 * Do not add --release: it causes -encoding to be ignored (in javadoc
    +			 * 12 through 15, anyway). -d is documented as a doclet option, but
    +			 * included here to be seen by the RelativizingFileManager (which may
    +			 * otherwise complain that no class output location has been set).
    +			 */
    +			"-d",           report.reportOutputDirectory.toPath()
    +								.resolve("apidocs").toString(),
    +			"-encoding",    report.inputEncoding,
    +			"-sourcepath",  srcroot.toString(),
    +			/*
    +			 * Core javadoc options.
    +			 * Avoid the legacy package/private/protected/public options; they can
    +			 * clobber the effects of the newer -show-...=... options.
    +			 */
    +			"-locale",      locale.toString(),
    +			"-quiet",
    +			/*
    +			 * Options that are passed to the doclet.
    +			 */
    +			"-author",
    +			"-bottom",      bottom,
    +			"-docencoding", report.outputEncoding,
    +			"-doctitle",    title,
    +			"-link",        jdklink,
    +			"-use",
    +			"-version",
    +			"-windowtitle", title,
    +			/*
    +			 * Source files to document
    +			 */
    +			srcroot.resolve("Node.java").toString(),
    +			srcroot.resolve("JarX.java").toString()
    +		);
     
    -	function diagListener(d)
    -	{
    -		var s = d.source;
    +		debug(args.toString());
    +		var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
     
    -		if ( null === s )
    -			s = "";
    -		else
    +		function diagListener(d)
     		{
    -			s = srcrooturi.relativize(s.toUri()).toString();
    -			s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			var s = d.source;
    +
    +			if ( null === s )
    +				s = "";
    +			else
    +			{
    +				s = srcrooturi.relativize(s.toUri()).toString();
    +				s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			}
    +
    +			diag(d.kind, s + d.getMessage(locale));
     		}
     
    -		diag(d.kind, s + d.getMessage(locale));
    +		var task = tool.getTask(null, null, diagListener, null, args, null);
    +		task.call();
    +		return null;
    +	}
    +	catch(e)
    +	{
    +		return report.exceptionWrap(e);
     	}
    -
    -	var task = tool.getTask(null, null, diagListener, null, args, null);
    -	task.call();
    -	return null;
    -}
    -catch(e)
    -{
    -	return report.exceptionWrap(e);
    -}
     }
     ]]>
     							
    diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml
    index df9c7c28..bd624856 100644
    --- a/pljava-pgxs/pom.xml
    +++ b/pljava-pgxs/pom.xml
    @@ -129,97 +129,97 @@ function isExternalReport(report)
     
     function executeReport(report, locale)
     {
    -try
    -{
    -	var paths = buildPaths(report.project.compileClasspathElements);
    -
    -	var title = report.project.name + " " + report.project.version;
    -
    -	var basedir = report.project.basedir.toPath();
    -
    -	var srcroot = java.nio.file.Paths.get("src", "main", "java");
    -	srcroot = resolve(basedir, srcroot);
    -	var srcrooturi = srcroot.toUri();
    -
    -	var jdklink = "https://docs.oracle.com/" +
    -		locale.language + "/java/javase/12/docs/api";
    -
    -	var bottom =
    -		"Copyright © " +
    -		report.project.inceptionYear + "–" + new Date().getFullYear() +
    -		"" +
    -		report.project.organization.name + "";
    -
    -	var of = java.util.List.of;
    -
    -	var args = of(
    -		/*
    -		 * The 'standard options' that javadoc inherits from javac.
    -		 * Do not add --release: it causes -encoding to be ignored (in javadoc
    -		 * 12 through 15, anyway). -d is documented as a doclet option, but
    -		 * included here to be seen by the RelativizingFileManager (which may
    -		 * otherwise complain that no class output location has been set).
    -		 */
    -		"-d",           report.reportOutputDirectory.toPath()
    -							.resolve("apidocs").toString(),
    -		"-encoding",    report.inputEncoding,
    -		"--class-path", paths.get("classpath"),
    -		"--module-path",paths.get("modulepath"),
    -		"-sourcepath",  srcroot.toString(),
    -		/*
    -		 * Core javadoc options.
    -		 * Avoid the legacy package/private/protected/public options; they can
    -		 * clobber the effects of the newer -show-...=... options.
    -		 */
    -		"-locale",      locale.toString(),
    -		"-quiet",
    -		/*
    -		 * Options that are passed to the doclet.
    -		 */
    -		"-author",
    -		"-bottom",      bottom,
    -		"-docencoding", report.outputEncoding,
    -		"-doctitle",    title,
    -		"-link",        jdklink,
    -		"-use",
    -		"-version",
    -		"-windowtitle", title
    -	);
    -
    -	args = new java.util.ArrayList(args);
    -
    -	var packages = [
    -		"org.postgresql.pljava.pgxs"
    -	];
    -
    -	packages.forEach(function(p) { args.add(p); });
    -
    -	debug(args.toString());
    -	var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    -
    -	function diagListener(d)
    +	try
     	{
    -		var s = d.source;
    -
    -		if ( null === s )
    -			s = "";
    -		else
    +		var paths = buildPaths(report.project.compileClasspathElements);
    +
    +		var title = report.project.name + " " + report.project.version;
    +
    +		var basedir = report.project.basedir.toPath();
    +
    +		var srcroot = java.nio.file.Paths.get("src", "main", "java");
    +		srcroot = resolve(basedir, srcroot);
    +		var srcrooturi = srcroot.toUri();
    +
    +		var jdklink = "https://docs.oracle.com/" +
    +			locale.language + "/java/javase/12/docs/api";
    +
    +		var bottom =
    +			"Copyright © " +
    +			report.project.inceptionYear + "–" + new Date().getFullYear() +
    +			"" +
    +			report.project.organization.name + "";
    +
    +		var of = java.util.List.of;
    +
    +		var args = of(
    +			/*
    +			 * The 'standard options' that javadoc inherits from javac.
    +			 * Do not add --release: it causes -encoding to be ignored (in javadoc
    +			 * 12 through 15, anyway). -d is documented as a doclet option, but
    +			 * included here to be seen by the RelativizingFileManager (which may
    +			 * otherwise complain that no class output location has been set).
    +			 */
    +			"-d",           report.reportOutputDirectory.toPath()
    +								.resolve("apidocs").toString(),
    +			"-encoding",    report.inputEncoding,
    +			"--class-path", paths.get("classpath"),
    +			"--module-path",paths.get("modulepath"),
    +			"-sourcepath",  srcroot.toString(),
    +			/*
    +			 * Core javadoc options.
    +			 * Avoid the legacy package/private/protected/public options; they can
    +			 * clobber the effects of the newer -show-...=... options.
    +			 */
    +			"-locale",      locale.toString(),
    +			"-quiet",
    +			/*
    +			 * Options that are passed to the doclet.
    +			 */
    +			"-author",
    +			"-bottom",      bottom,
    +			"-docencoding", report.outputEncoding,
    +			"-doctitle",    title,
    +			"-link",        jdklink,
    +			"-use",
    +			"-version",
    +			"-windowtitle", title
    +		);
    +
    +		args = new java.util.ArrayList(args);
    +
    +		var packages = [
    +			"org.postgresql.pljava.pgxs"
    +		];
    +
    +		packages.forEach(function(p) { args.add(p); });
    +
    +		debug(args.toString());
    +		var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    +
    +		function diagListener(d)
     		{
    -			s = srcrooturi.relativize(s.toUri()).toString();
    -			s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			var s = d.source;
    +
    +			if ( null === s )
    +				s = "";
    +			else
    +			{
    +				s = srcrooturi.relativize(s.toUri()).toString();
    +				s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			}
    +
    +			diag(d.kind, s + d.getMessage(locale));
     		}
     
    -		diag(d.kind, s + d.getMessage(locale));
    +		var task = tool.getTask(null, null, diagListener, null, args, null);
    +		task.call();
    +		return null;
    +	}
    +	catch(e)
    +	{
    +		return report.exceptionWrap(e);
     	}
    -
    -	var task = tool.getTask(null, null, diagListener, null, args, null);
    -	task.call();
    -	return null;
    -}
    -catch(e)
    -{
    -	return report.exceptionWrap(e);
    -}
     }
     ]]>
     							
    diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml
    index 20a48926..4de8edd5 100644
    --- a/pljava-so/pom.xml
    +++ b/pljava-so/pom.xml
    @@ -287,44 +287,41 @@
     
     function execute()
     {
    -try
    -{
    -	var files = utils.getFilesWithExtension(source_path, ".c");
    -
    -	var compile_flags = new ArrayList();
    -	var link_flags = new ArrayList();
    -	/*
    -	 * pg_config can sometimes sneak options into the value of CC (for example,
    -	 * gcc -std=gnu99). Add it to compile_flags as a list, then snatch the first
    -	 * element back out to use as the CC executable. Copy any remaining flags
    -	 * to link_flags also.
    -	 */
    -	compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cc));
    -	cc = compile_flags.remove(0);
    -	link_flags.addAll(compile_flags);
    -
    -	compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags));
    -	compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cppflags));
    -	compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags_sl));
    -	var exitCode = pgxs.compile(cc, files, target_path, base_includes, base_defines, compile_flags);
    -	if (exitCode != 0)
    -		throw new org.apache.maven.plugin
    -			.MojoExecutionException("Compilation failed with exit code: " + exitCode);
    -
    -	var object_files =  utils.getFilesWithExtension(target_path, extension);
    -	link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags));
    -	link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_sl));
    -	link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_ex));
    -	exitCode = pgxs.link(cc, link_flags, object_files, target_path);
    -	if (exitCode != 0)
    -		throw new org.apache.maven.plugin
    -			.MojoExecutionException("Linking failed with exit code: " + exitCode);
    -	return null;
    -}
    -catch(e)
    -{
    -	return utils.exceptionWrap(e);
    -}
    +	try
    +	{
    +		var files = utils.getFilesWithExtension(source_path, ".c");
    +		var compile_flags = new ArrayList();var link_flags = new ArrayList();
    +		/*
    +	 	* pg_config can sometimes sneak options into the value of CC (for example,
    +	 	* gcc -std=gnu99). Add it to compile_flags as a list, then snatch the first
    +	 	* element back out to use as the CC executable. Copy any remaining flags
    +	 	* to link_flags also.
    +	 	*/
    +		compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cc));
    +		cc = compile_flags.remove(0);
    +		link_flags.addAll(compile_flags);
    +		compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags));
    +		compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cppflags));
    +		compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags_sl));
    +		var exitCode = pgxs.compile(cc, files, target_path, base_includes, base_defines, compile_flags);
    +		if (exitCode != 0)
    +			throw new org.apache.maven.plugin
    +				.MojoExecutionException("Compilation failed with exit code: " + exitCode);
    +
    +		var object_files =  utils.getFilesWithExtension(target_path, extension);
    +		link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags));
    +		link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_sl));
    +		link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_ex));
    +		exitCode = pgxs.link(cc, link_flags, object_files, target_path);
    +		if (exitCode != 0)
    +			throw new org.apache.maven.plugin
    +				.MojoExecutionException("Linking failed with exit code: " + exitCode);
    +		return null;
    +	}
    +	catch(e)
    +	{
    +		return utils.exceptionWrap(e);
    +	}
     }
     ]]>
     							
    diff --git a/pljava/pom.xml b/pljava/pom.xml
    index 53975222..c6fa9fa4 100644
    --- a/pljava/pom.xml
    +++ b/pljava/pom.xml
    @@ -85,115 +85,115 @@ function isExternalReport(report)
     
     function executeReport(report, locale)
     {
    -try
    -{
    -	var paths = buildPaths(report.project.compileClasspathElements);
    +	try
    +	{
    +		var paths = buildPaths(report.project.compileClasspathElements);
    +
    +		var title = report.project.name + " " + report.project.version;
    +
    +		var basedir = report.project.basedir.toPath();
    +
    +		var srcroot = java.nio.file.Paths.get("src", "main", "java");
    +		srcroot = resolve(basedir, srcroot);
    +		var srcrooturi = srcroot.toUri();
    +
    +		var jdklink = "https://docs.oracle.com/" +
    +			locale.language + "/java/javase/12/docs/api";
    +
    +		var apioffline =
    +			basedir.toUri().resolve("../pljava-api/target/site/apidocs");
    +
    +		var bottom =
    +			"Copyright © " +
    +			report.project.inceptionYear + "–" + new Date().getFullYear() +
    +			"" +
    +			report.project.organization.name + "";
    +
    +		var args = java.util.List.of(
    +			/*
    +			 * The 'standard options' that javadoc inherits from javac.
    +			 * Do not add --release: it causes -encoding to be ignored (in javadoc
    +			 * 12 through 15, anyway). -d is documented as a doclet option, but
    +			 * included here to be seen by the RelativizingFileManager (which may
    +			 * otherwise complain that no class output location has been set).
    +			 */
    +			"-d",                     report.reportOutputDirectory.toPath()
    +										.resolve("apidocs").toString(),
    +			"-encoding",              report.inputEncoding,
    +			"--module-path",          paths.get("modulepath"),
    +			"--module-source-path",   "org.postgresql.pljava.internal=" + srcroot,
    +			// ^^^ Options recognized by the file manager end here ^^^
    +			"--module",               "org.postgresql.pljava.internal",
    +			/*
    +			 * Core javadoc options.
    +			 * Avoid the legacy package/private/protected/public options; they can
    +			 * clobber the effects of the newer -show-...=... options.
    +			 */
    +			"-locale",                locale.toString(),
    +			"-quiet",
    +			"--show-module-contents", "all",
    +			"--show-packages",        "all",
    +			/*
    +			 * Options that are passed to the doclet.
    +			 */
    +			"-author",
    +			"-bottom",                bottom,
    +			"-docencoding",           report.outputEncoding,
    +			"-doctitle",              title,
    +			"-link",                  jdklink,
    +			"-linkoffline",
    +									  "../../RELDOTS/pljava-api/apidocs",
    +									  apioffline.toString(),
    +			//"-sourcetab",           "4",//seemed good idea but implies -linksource
    +			"-use",
    +			"-version",
    +			"-windowtitle",           title
    +		);
     
    -	var title = report.project.name + " " + report.project.version;
    +		debug(args.toString());
    +		var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
     
    -	var basedir = report.project.basedir.toPath();
    +		function diagListener(d)
    +		{
    +			var s = d.source;
     
    -	var srcroot = java.nio.file.Paths.get("src", "main", "java");
    -	srcroot = resolve(basedir, srcroot);
    -	var srcrooturi = srcroot.toUri();
    +			if ( null === s )
    +				s = "";
    +			else
    +			{
    +				s = srcrooturi.relativize(s.toUri()).toString();
    +				s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    +			}
     
    -	var jdklink = "https://docs.oracle.com/" +
    -		locale.language + "/java/javase/12/docs/api";
    +			diag(d.kind, s + d.getMessage(locale));
    +		}
     
    -	var apioffline =
    -		basedir.toUri().resolve("../pljava-api/target/site/apidocs");
    +		var Charset = Java.type("java.nio.charset.Charset");
     
    -	var bottom =
    -		"Copyright © " +
    -		report.project.inceptionYear + "–" + new Date().getFullYear() +
    -		"" +
    -		report.project.organization.name + "";
    +		var smgr = tool.getStandardFileManager(
    +				diagListener,
    +				locale,
    +				Charset.forName(report.inputEncoding)
    +			);
     
    -	var args = java.util.List.of(
    -		/*
    -		 * The 'standard options' that javadoc inherits from javac.
    -		 * Do not add --release: it causes -encoding to be ignored (in javadoc
    -		 * 12 through 15, anyway). -d is documented as a doclet option, but
    -		 * included here to be seen by the RelativizingFileManager (which may
    -		 * otherwise complain that no class output location has been set).
    -		 */
    -		"-d",                     report.reportOutputDirectory.toPath()
    -									.resolve("apidocs").toString(),
    -		"-encoding",              report.inputEncoding,
    -		"--module-path",          paths.get("modulepath"),
    -		"--module-source-path",   "org.postgresql.pljava.internal=" + srcroot,
    -		// ^^^ Options recognized by the file manager end here ^^^
    -		"--module",               "org.postgresql.pljava.internal",
     		/*
    -		 * Core javadoc options.
    -		 * Avoid the legacy package/private/protected/public options; they can
    -		 * clobber the effects of the newer -show-...=... options.
    +		 * A special file manager that will rewrite the RELDOTS seen in -linkoffline
    +		 * above. The options a file manager recognizes must be the first ones in
    +		 * args; handleFirstOptions below returns at the first one the file manager
    +		 * doesn't know what to do with.
     		 */
    -		"-locale",                locale.toString(),
    -		"-quiet",
    -		"--show-module-contents", "all",
    -		"--show-packages",        "all",
    -		/*
    -		 * Options that are passed to the doclet.
    -		 */
    -		"-author",
    -		"-bottom",                bottom,
    -		"-docencoding",           report.outputEncoding,
    -		"-doctitle",              title,
    -		"-link",                  jdklink,
    -		"-linkoffline",
    -								  "../../RELDOTS/pljava-api/apidocs",
    -								  apioffline.toString(),
    -		//"-sourcetab",           "4",//seemed good idea but implies -linksource
    -		"-use",
    -		"-version",
    -		"-windowtitle",           title
    -	);
    -
    -	debug(args.toString());
    -	var tool = javax.tools.ToolProvider.getSystemDocumentationTool();
    -
    -	function diagListener(d)
    -	{
    -		var s = d.source;
    -
    -		if ( null === s )
    -			s = "";
    -		else
    -		{
    -			s = srcrooturi.relativize(s.toUri()).toString();
    -			s += "[" + d.lineNumber + "," + d.columnNumber + "] ";
    -		}
    +		var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager(
    +			smgr, Charset.forName(report.outputEncoding));
    +		rmgr.handleFirstOptions(args);
     
    -		diag(d.kind, s + d.getMessage(locale));
    +		var task = tool.getTask(null, rmgr, diagListener, null, args, null);
    +		task.call();
    +		return null;
    +	}
    +	catch(e)
    +	{
    +		return report.exceptionWrap(e);
     	}
    -
    -	var Charset = Java.type("java.nio.charset.Charset");
    -
    -	var smgr = tool.getStandardFileManager(
    -			diagListener,
    -			locale,
    -			Charset.forName(report.inputEncoding)
    -		);
    -
    -	/*
    -	 * A special file manager that will rewrite the RELDOTS seen in -linkoffline
    -	 * above. The options a file manager recognizes must be the first ones in
    -	 * args; handleFirstOptions below returns at the first one the file manager
    -	 * doesn't know what to do with.
    -	 */
    -	var rmgr = new org.postgresql.pljava.pgxs.RelativizingFileManager(
    -		smgr, Charset.forName(report.outputEncoding));
    -	rmgr.handleFirstOptions(args);
    -
    -	var task = tool.getTask(null, rmgr, diagListener, null, args, null);
    -	task.call();
    -	return null;
    -}
    -catch(e)
    -{
    -	return report.exceptionWrap(e);
    -}
     }
     ]]>
     							
    
    From b3bdd3b9920686f474aa294deab93f2d538d6ece Mon Sep 17 00:00:00 2001
    From: Kartik Ohri 
    Date: Fri, 18 Sep 2020 01:47:23 +0530
    Subject: [PATCH 0709/1087] Check if javadoc completed successfully
    
    If execution fails, MavenReportException is thrown.
    ---
     pljava-api/pom.xml                                    |  5 +++--
     pljava-examples/pom.xml                               |  5 +++--
     pljava-packaging/pom.xml                              |  5 +++--
     pljava-pgxs/pom.xml                                   |  5 +++--
     .../postgresql/pljava/pgxs/ReportScriptingMojo.java   | 11 +++++++++++
     pljava/pom.xml                                        |  5 +++--
     6 files changed, 26 insertions(+), 10 deletions(-)
    
    diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml
    index 64ef5edd..c0757c00 100644
    --- a/pljava-api/pom.xml
    +++ b/pljava-api/pom.xml
    @@ -157,8 +157,9 @@ function executeReport(report, locale)
     		}
     
     		var task = tool.getTask(null, null, diagListener, null, args, null);
    -		task.call();
    -		return null;
    +		if (task.call())
    +			return null;
    +		return report.exceptionWrap("Javadoc errors were reported");
     	}
     	catch(e)
     	{
    diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml
    index d5b67719..df34dd62 100644
    --- a/pljava-examples/pom.xml
    +++ b/pljava-examples/pom.xml
    @@ -235,8 +235,9 @@ function executeReport(report, locale)
     		rmgr.handleFirstOptions(args);
     
     		var task = tool.getTask(null, rmgr, diagListener, null, args, null);
    -		task.call();
    -		return null;
    +		if (task.call())
    +			return null;
    +		return report.exceptionWrap("Javadoc errors were reported");
     	}
     	catch(e)
     	{
    diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml
    index ba56f68a..dd336142 100644
    --- a/pljava-packaging/pom.xml
    +++ b/pljava-packaging/pom.xml
    @@ -300,8 +300,9 @@ function executeReport(report, locale)
     		}
     
     		var task = tool.getTask(null, null, diagListener, null, args, null);
    -		task.call();
    -		return null;
    +		if (task.call())
    +			return null;
    +		return report.exceptionWrap("Javadoc errors were reported");
     	}
     	catch(e)
     	{
    diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml
    index bd624856..5793c089 100644
    --- a/pljava-pgxs/pom.xml
    +++ b/pljava-pgxs/pom.xml
    @@ -213,8 +213,9 @@ function executeReport(report, locale)
     		}
     
     		var task = tool.getTask(null, null, diagListener, null, args, null);
    -		task.call();
    -		return null;
    +		if (task.call())
    +			return null;
    +		return report.exceptionWrap("Javadoc errors were reported");
     	}
     	catch(e)
     	{
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java
    index 921ca5e4..02857f81 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java
    @@ -259,4 +259,15 @@ boolean canGenerateReportDefault ()
     	{
     		return super.canGenerateReport();
     	}
    +
    +	/**
    +	 * Default implementation of
    +	 * {@link ReportScript#exceptionWrap(Object)}. Invoked if
    +	 * {@code fun exceptionWrap(object)} is not defined in the javascript
    +	 * snippet associated with the report.
    +	 */
    +	public MavenReportException exceptionWrap(Object object)
    +	{
    +		return reportScript.exceptionWrap(object);
    +	}
     }
    diff --git a/pljava/pom.xml b/pljava/pom.xml
    index c6fa9fa4..8b13cf8e 100644
    --- a/pljava/pom.xml
    +++ b/pljava/pom.xml
    @@ -187,8 +187,9 @@ function executeReport(report, locale)
     		rmgr.handleFirstOptions(args);
     
     		var task = tool.getTask(null, rmgr, diagListener, null, args, null);
    -		task.call();
    -		return null;
    +		if (task.call())
    +			return null;
    +		return report.exceptionWrap("Javadoc errors were reported");
     	}
     	catch(e)
     	{
    
    From 44e724e2285357634e00f8914d49868d20eed35f Mon Sep 17 00:00:00 2001
    From: Kartik Ohri 
    Date: Fri, 18 Sep 2020 19:18:20 +0530
    Subject: [PATCH 0710/1087] Fix exceptionWrap method in PGXSUtils
    
    The documentation and usage has been updated to consider the effect of
    the scriptFailure parameter.
    ---
     .../org/postgresql/pljava/pgxs/PGXSUtils.java | 28 +++++++++++++------
     pljava-so/pom.xml                             |  8 ++----
     2 files changed, 22 insertions(+), 14 deletions(-)
    
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    index a17b60e3..d0db55a1 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    @@ -667,22 +667,32 @@ public ProcessBuilder forWindowsCRuntime(ProcessBuilder pb)
     	}
     
     	/**
    -	 * Wraps the input object in a {@link MojoExecutionException}.
    +	 * Wraps the input object in a {@link AbstractMojoExecutionException}.
     	 *
     	 * The returned exception is constructed as follows:
    -	 * 1) If {@code object} is null, the exception message indicates the same.
    -	 * 2) If {@code object} is already a {@link MojoExecutionException}, return it
    +	 * 1) If {@code object} is null, then {@link MojoExecutionException} is used
    +	 * to wrap and the message indicates that null value was thrown by the script.
    +	 * 2) If {@code object} is already a {@link MojoExecutionException}, return
    +	 * it as is.
    +	 * 3) If {@code object} is already a {@link MojoFailureException}, return it
     	 * as is.
    -	 * 3) If {@code object} is any other {@link Throwable}, set it as the cause
    +	 *
    +	 * For the steps, below the wrapping exception is chosen according to the
    +	 * the value of {@code scriptFailure} parameter.
    +	 *
    +	 * 4) If {@code object} is any other {@link Throwable}, set it as the cause
     	 * for the exception.
    -	 * {@link MojoExecutionException} with {@code object} as its cause.
    -	 * 4) If {@code object} is a {@link String}, set it as the message of the
    +	 * 5) If {@code object} is a {@link String}, set it as the message of the
     	 * exception.
    -	 * 5) For all other case, the message of the exception is set in this format
    +	 * 6) For all other case, the message of the exception is set in this format
     	 * , Class Name of object: String representation of object.
     	 *
    -	 * @param object to wrap in MojoExecutionException
    -	 * @return object wrapped inside a {@link MojoExecutionException}
    +	 * @param object to wrap in AbstractMojoExecutionException
    +	 * @param scriptFailure if true, use a MojoExecutionException for wrapping
    +	 *                      otherwise use MojoFailureException. this parameter
    +	 *                      is ignored, if the object is null or instance of
    +	 *                      MojoExecutionException or MojoFailureException
    +	 * @return object wrapped inside a {@link AbstractMojoExecutionException}
     	 */
     	public AbstractMojoExecutionException exceptionWrap(Object object,
     														boolean scriptFailure)
    diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml
    index 4de8edd5..435254bf 100644
    --- a/pljava-so/pom.xml
    +++ b/pljava-so/pom.xml
    @@ -305,8 +305,7 @@ function execute()
     		compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags_sl));
     		var exitCode = pgxs.compile(cc, files, target_path, base_includes, base_defines, compile_flags);
     		if (exitCode != 0)
    -			throw new org.apache.maven.plugin
    -				.MojoExecutionException("Compilation failed with exit code: " + exitCode);
    +			throw "Compilation failed with exit code: " + exitCode;
     
     		var object_files =  utils.getFilesWithExtension(target_path, extension);
     		link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags));
    @@ -314,13 +313,12 @@ function execute()
     		link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_ex));
     		exitCode = pgxs.link(cc, link_flags, object_files, target_path);
     		if (exitCode != 0)
    -			throw new org.apache.maven.plugin
    -				.MojoExecutionException("Linking failed with exit code: " + exitCode);
    +			throw "Linking failed with exit code: " + exitCode;
     		return null;
     	}
     	catch(e)
     	{
    -		return utils.exceptionWrap(e);
    +		return utils.exceptionWrap(e, false);
     	}
     }
     ]]>
    
    From 3f13426adae8f5b9594425027946cad560adab24 Mon Sep 17 00:00:00 2001
    From: Kartik Ohri 
    Date: Sat, 19 Sep 2020 17:19:52 +0530
    Subject: [PATCH 0711/1087] Move exceptionWrap method to ReportScriptingMojo
    
    There seems to be no benefit of allowing users to override exceptionWrap
    . Hence, move it to ReportScriptingMojo to avoid unneeded indirection.
    ---
     .../postgresql/pljava/pgxs/ReportScript.java  | 38 -------------------
     .../pljava/pgxs/ReportScriptingMojo.java      | 37 +++++++++++++++---
     2 files changed, 32 insertions(+), 43 deletions(-)
    
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java
    index 6ab34aff..8194920c 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScript.java
    @@ -85,42 +85,4 @@ default boolean canGenerateReport(ReportScriptingMojo report)
     	 * @see ReportScriptingMojo#executeReport(Locale)
     	 */
     	MavenReportException executeReport(ReportScriptingMojo report, Locale locale);
    -
    -	/**
    -	 * Wraps the input object in a {@link MavenReportException}.
    -	 *
    -	 * The exception returned is constructed as follows:
    -	 * 1) If {@code object} is null, the exception message indicates the same.
    -	 * 2) If {@code object} is already a {@link MavenReportException}, return it
    -	 * as is.
    -	 * 3) If {@code object} is any other {@link Throwable}, set it as the cause
    -	 * for the exception.
    -	 * {@link MavenReportException} with {@code object} as its cause.
    -	 * 4) If {@code object} is a {@link String}, set it as the message of the
    -	 * exception.
    -	 * 5) For all other case, the message of the exception is set in this format
    -	 * , Class Name of object: String representation of object.
    -	 *
    -	 * @param object to wrap in MavenReportException
    -	 * @return object wrapped inside a {@link MavenReportException}
    -	 */
    -	default MavenReportException exceptionWrap(Object object)
    -	{
    -		if (object == null)
    -			return new MavenReportException("Script threw a null value");
    -		else if (object instanceof MavenReportException)
    -			return (MavenReportException) object;
    -		else if (object instanceof Throwable)
    -		{
    -			Throwable t = (Throwable) object;
    -			MavenReportException exception = new MavenReportException(t.getMessage());
    -			exception.initCause(t);
    -			return exception;
    -		}
    -		else if (object instanceof String)
    -			return new MavenReportException((String) object);
    -		else
    -			return new MavenReportException(object.getClass().getCanonicalName()
    -				+ ": " + object.toString());
    -	}
     }
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java
    index 02857f81..f37577fd 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ReportScriptingMojo.java
    @@ -261,13 +261,40 @@ boolean canGenerateReportDefault ()
     	}
     
     	/**
    -	 * Default implementation of
    -	 * {@link ReportScript#exceptionWrap(Object)}. Invoked if
    -	 * {@code fun exceptionWrap(object)} is not defined in the javascript
    -	 * snippet associated with the report.
    +	 * Wraps the input object in a {@link MavenReportException}.
    +	 *
    +	 * The exception returned is constructed as follows:
    +	 * 1) If {@code object} is null, the exception message indicates the same.
    +	 * 2) If {@code object} is already a {@link MavenReportException}, return it
    +	 * as is.
    +	 * 3) If {@code object} is any other {@link Throwable}, set it as the cause
    +	 * for the exception.
    +	 * {@link MavenReportException} with {@code object} as its cause.
    +	 * 4) If {@code object} is a {@link String}, set it as the message of the
    +	 * exception.
    +	 * 5) For all other case, the message of the exception is set in this format
    +	 * , Class Name of object: String representation of object.
    +	 *
    +	 * @param object to wrap in MavenReportException
    +	 * @return object wrapped inside a {@link MavenReportException}
     	 */
     	public MavenReportException exceptionWrap(Object object)
     	{
    -		return reportScript.exceptionWrap(object);
    +		if (object == null)
    +			return new MavenReportException("Script threw a null value");
    +		else if (object instanceof MavenReportException)
    +			return (MavenReportException) object;
    +		else if (object instanceof Throwable)
    +		{
    +			Throwable t = (Throwable) object;
    +			MavenReportException exception = new MavenReportException(t.getMessage());
    +			exception.initCause(t);
    +			return exception;
    +		}
    +		else if (object instanceof String)
    +			return new MavenReportException((String) object);
    +		else
    +			return new MavenReportException(object.getClass().getCanonicalName()
    +				+ ": " + object.toString());
     	}
     }
    
    From e642b0f1677671ac00a4fbd5e4bc076946146768 Mon Sep 17 00:00:00 2001
    From: Kartik Ohri 
    Date: Sat, 19 Sep 2020 21:15:03 +0530
    Subject: [PATCH 0712/1087] Move exceptionWrap method to ScriptingMojo
    
    In the previous commit, exceptionWrap method was moved from ReportScript
    to ReportScriptingMojo. In line with the changes, move the exceptionWrap
    for ScriptingMojo from PGXSUtils to it.
    
    In addition, catch and throw any other exceptions that may arise during
    execution of the plugin as MojoExecutionException.
    ---
     .../org/postgresql/pljava/pgxs/PGXSUtils.java | 65 ----------------
     .../postgresql/pljava/pgxs/ScriptingMojo.java | 76 ++++++++++++++++---
     pljava-so/pom.xml                             |  8 +-
     3 files changed, 72 insertions(+), 77 deletions(-)
    
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    index d0db55a1..b9321a22 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/PGXSUtils.java
    @@ -666,69 +666,4 @@ public ProcessBuilder forWindowsCRuntime(ProcessBuilder pb)
     		return pb;
     	}
     
    -	/**
    -	 * Wraps the input object in a {@link AbstractMojoExecutionException}.
    -	 *
    -	 * The returned exception is constructed as follows:
    -	 * 1) If {@code object} is null, then {@link MojoExecutionException} is used
    -	 * to wrap and the message indicates that null value was thrown by the script.
    -	 * 2) If {@code object} is already a {@link MojoExecutionException}, return
    -	 * it as is.
    -	 * 3) If {@code object} is already a {@link MojoFailureException}, return it
    -	 * as is.
    -	 *
    -	 * For the steps, below the wrapping exception is chosen according to the
    -	 * the value of {@code scriptFailure} parameter.
    -	 *
    -	 * 4) If {@code object} is any other {@link Throwable}, set it as the cause
    -	 * for the exception.
    -	 * 5) If {@code object} is a {@link String}, set it as the message of the
    -	 * exception.
    -	 * 6) For all other case, the message of the exception is set in this format
    -	 * , Class Name of object: String representation of object.
    -	 *
    -	 * @param object to wrap in AbstractMojoExecutionException
    -	 * @param scriptFailure if true, use a MojoExecutionException for wrapping
    -	 *                      otherwise use MojoFailureException. this parameter
    -	 *                      is ignored, if the object is null or instance of
    -	 *                      MojoExecutionException or MojoFailureException
    -	 * @return object wrapped inside a {@link AbstractMojoExecutionException}
    -	 */
    -	public AbstractMojoExecutionException exceptionWrap(Object object,
    -														boolean scriptFailure)
    -	{
    -		AbstractMojoExecutionException exception;
    -		if (object == null)
    -			exception = new MojoExecutionException("Script threw a null value");
    -		else if (object instanceof MojoExecutionException)
    -			exception = (MojoExecutionException) object;
    -		else if (object instanceof MojoFailureException)
    -			exception = (MojoFailureException) object;
    -		else if (object instanceof Throwable)
    -		{
    -			Throwable t = (Throwable) object;
    -			if (scriptFailure)
    -				exception = new MojoExecutionException(t.getMessage(), t);
    -			else
    -				exception = new MojoFailureException(t.getMessage(), t);
    -		}
    -		else if (object instanceof String)
    -		{
    -			if (scriptFailure)
    -				exception = new MojoExecutionException((String) object);
    -			else
    -				exception = new MojoFailureException((String) object);
    -		}
    -		else
    -		{
    -			String message = object.getClass().getCanonicalName()
    -				+ ": " + object.toString();
    -			if (scriptFailure)
    -				exception = new MojoExecutionException(message);
    -			else
    -				exception = new MojoFailureException(message);
    -		}
    -		return exception;
    -	}
    -
     }
    diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java
    index 07e7b3a7..11f0ddac 100644
    --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java
    +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/ScriptingMojo.java
    @@ -26,8 +26,8 @@
     import javax.script.Invocable;
     import javax.script.ScriptContext;
     import javax.script.ScriptEngine;
    -import javax.script.ScriptException;
     import java.util.function.BiConsumer;
    +import java.util.function.BiFunction;
     import java.util.function.Function;
     
     /**
    @@ -76,16 +76,15 @@ public void execute () throws MojoExecutionException, MojoFailureException
     			GoalScript goal = ((Invocable) engine).getInterface(GoalScript.class);
     			AbstractMojoExecutionException exception = goal.execute();
     			if (exception != null)
    -			{
    -				if (exception instanceof MojoExecutionException)
    -					throw (MojoExecutionException) exception;
    -				else if (exception instanceof MojoFailureException)
    -					throw (MojoFailureException) exception;
    -			}
    +				throw exception;
     		}
    -		catch (ScriptException e)
    +		catch (MojoFailureException | MojoExecutionException e)
     		{
    -			getLog().error(e);
    +			throw e;
    +		}
    +		catch (Exception e)
    +		{
    +			throw (MojoExecutionException) exceptionWrap(e, true);
     		}
     	}
     
    @@ -119,4 +118,63 @@ public String getPgConfigProperty (String property)
     			return null;
     		}
     	}
    +
    +	/**
    +	 * Wraps the input object in a {@link AbstractMojoExecutionException}.
    +	 *
    +	 * The returned exception is constructed as follows:
    +	 * 1) If {@code object} is null, then {@link MojoExecutionException} is used
    +	 * to wrap and the message indicates that null value was thrown by the script.
    +	 * 2) If {@code object} is already a {@link MojoExecutionException}, return
    +	 * it as is.
    +	 * 3) If {@code object} is already a {@link MojoFailureException}, return it
    +	 * as is.
    +	 *
    +	 * For the steps, below the wrapping exception is chosen according to the
    +	 * the value of {@code scriptFailure} parameter.
    +	 *
    +	 * 4) If {@code object} is any other {@link Throwable}, set it as the cause
    +	 * for the exception.
    +	 * 5) If {@code object} is a {@link String}, set it as the message of the
    +	 * exception.
    +	 * 6) For all other case, the message of the exception is set in this format
    +	 * , Class Name of object: String representation of object.
    +	 *
    +	 * @param object to wrap in AbstractMojoExecutionException
    +	 * @param scriptFailure if true, use a MojoExecutionException for wrapping
    +	 *                      otherwise use MojoFailureException. this parameter
    +	 *                      is ignored, if the object is null or instance of
    +	 *                      MojoExecutionException or MojoFailureException
    +	 * @return object wrapped inside a {@link AbstractMojoExecutionException}
    +	 */
    +	public AbstractMojoExecutionException exceptionWrap(Object object,
    +														boolean scriptFailure)
    +	{
    +		BiFunction
    +			createException = scriptFailure ? MojoExecutionException::new :
    +			MojoFailureException::new;
    +
    +		AbstractMojoExecutionException exception;
    +		if (object == null)
    +			exception = new MojoExecutionException("Script threw a null value");
    +		else if (object instanceof MojoExecutionException)
    +			exception = (MojoExecutionException) object;
    +		else if (object instanceof MojoFailureException)
    +			exception = (MojoFailureException) object;
    +		else if (object instanceof Throwable)
    +		{
    +			Throwable t = (Throwable) object;
    +			exception = createException.apply(t.getMessage(), t);
    +		}
    +		else if (object instanceof String)
    +			exception = createException.apply((String) object, null);
    +		else
    +		{
    +			String message = object.getClass().getCanonicalName() + ": "
    +				+ object.toString();
    +			exception = createException.apply(message, null);
    +		}
    +		return exception;
    +	}
    +
     }
    diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml
    index 435254bf..8f2fb63c 100644
    --- a/pljava-so/pom.xml
    +++ b/pljava-so/pom.xml
    @@ -305,7 +305,8 @@ function execute()
     		compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags_sl));
     		var exitCode = pgxs.compile(cc, files, target_path, base_includes, base_defines, compile_flags);
     		if (exitCode != 0)
    -			throw "Compilation failed with exit code: " + exitCode;
    +			return plugin.exceptionWrap("Compilation failed with exit code: "
    +				+ exitCode, false);
     
     		var object_files =  utils.getFilesWithExtension(target_path, extension);
     		link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags));
    @@ -313,12 +314,13 @@ function execute()
     		link_flags.addAll(pgxs.getPgConfigPropertyAsList(ldflags_ex));
     		exitCode = pgxs.link(cc, link_flags, object_files, target_path);
     		if (exitCode != 0)
    -			throw "Linking failed with exit code: " + exitCode;
    +			return plugin.exceptionWrap("Linking failed with exit code: "
    +				+ exitCode, false);
     		return null;
     	}
     	catch(e)
     	{
    -		return utils.exceptionWrap(e, false);
    +		return plugin.exceptionWrap(e, true);
     	}
     }
     ]]>
    
    From fc7291d8b6ce23fc11c19b9b9915b1f033c044f8 Mon Sep 17 00:00:00 2001
    From: Chapman Flack 
    Date: Sat, 26 Sep 2020 19:05:27 -0400
    Subject: [PATCH 0713/1087] Add linux-ppc64le to builds for Travis CI
    
    The language: java runner environment isn't able to install the same
    selection of JDKs on ppc64le as on amd64, so switch instead to the
    language: minimal runner, and explicitly get Java from adoptopenjdk.
    
    That can be done with the install-jdk.sh script, which already exists
    in /usr/local/bin on and amd64 linux runner ... but not on ppc64le
    (or osx, for that matter). So, get that script from GitHub (at a
    specific SHA after reviewing it) when it isn't already installed.
    (It might be just as well to download it unconditionally, and never
    wonder what version the runner has.)
    
    The install-jdk.sh script needs a patch to compute the right
    JAVA_HOME value on osx.
    
    In constructing the URL for downloading from adoptopenjdk, the
    Travis arch value 'amd64' must be 'x64', and the Travis OS 'osx'
    must be 'mac'.
    
    Naturally, the minimal runner doesn't have Maven preinstalled
    either. It's not a bad thing to have to fetch Maven; it is good
    to confirm the build works back to 3.5.2, as the documentation
    claims.
    
    The minimal runner doesn't have PostgreSQL installed, which
    doesn't change much, except travis_install_postgresql.sh needs
    to be undeterred if 'service postgresql stop' fails. (I don't
    know what that chmod was doing there; builds seem to work
    without it.)
    
    For Java 9, the --cacerts option has to be given to the
    install-jdk.sh script, or Maven won't be able to download
    dependencies because it won't be able to verify connections
    to repository hosts.
    
    Use the matrix now to generate linux builds for amd64 and ppc64le
    with various PostgreSQL versions, Java feature versions, JVM
    implementations (hotspot, openj9) and Maven versions. There isn't
    support for osx-ppc64le, so remove osx from the matrix, and just
    use include: to add some osx builds manually.
    
    The env: section in the matrix is also kind of manually populated
    for a plausible subset of possible builds. The full Cartesian
    product of {arch}x{pg version}x{java version}x{hotspot,openj9}x
    {maven version} would be a test of Travis's generosity.
    
    It is good to have added OpenJ9 into the matrix, and discovered
    a segfault. So comment that back out of the matrix for now.
    ---
     .travis.yml                          | 134 +++++++++++++++++++--------
     .travis/travis_install_postgresql.sh |   3 +-
     2 files changed, 94 insertions(+), 43 deletions(-)
    
    diff --git a/.travis.yml b/.travis.yml
    index 521c494d..9a38771c 100644
    --- a/.travis.yml
    +++ b/.travis.yml
    @@ -1,23 +1,98 @@
    -language: java
    +language: minimal
     os:
       - linux
    -  - osx
    +arch:
    +  - amd64
    +  - ppc64le
     dist: bionic
    -osx_image: xcode11
    -jdk:
    -  - openjdk14
    -  - openjdk13
    -  - openjdk12
    -  - openjdk11
    -  - openjdk10
    -  - openjdk9
     env:
    -  - POSTGRESQL_VERSION=12
    +  - POSTGRESQL_VERSION: 12
    +    JAVA_VERSION: 14
    +    JVM_IMPL: hotspot
    +    MVN_VERSION: 3.5.2
    +# - POSTGRESQL_VERSION: 12
    +#   JAVA_VERSION: 14
    +#   JVM_IMPL: openj9
    +#   MVN_VERSION: 3.6.3
    +  - POSTGRESQL_VERSION: 12
    +    JAVA_VERSION: 11
    +    JVM_IMPL: hotspot
    +    MVN_VERSION: 3.6.3
    +  - POSTGRESQL_VERSION: 12
    +    JAVA_VERSION: 9
    +    JVM_IMPL: hotspot
    +    MVN_VERSION: 3.6.3
    +  - POSTGRESQL_VERSION: 10
    +    JAVA_VERSION: 14
    +    JVM_IMPL: hotspot
    +    MVN_VERSION: 3.6.3
    +  - POSTGRESQL_VERSION: 9.5
    +    JAVA_VERSION: 14
    +    JVM_IMPL: hotspot
    +    MVN_VERSION: 3.6.3
    +
    +jobs:
    +  include:
    +    - os: osx
    +      osx_image: xcode11
    +      arch: amd64
    +      env:
    +        - POSTGRESQL_VERSION: 11
    +          JAVA_VERSION: 14
    +          JVM_IMPL: hotspot
    +          MVN_VERSION: 3.6.3
    +    - os: osx
    +      osx_image: xcode11
    +      arch: amd64
    +      env:
    +        - POSTGRESQL_VERSION: 10
    +          JAVA_VERSION: 14
    +          JVM_IMPL: hotspot
    +          MVN_VERSION: 3.6.3
    +    - os: osx
    +      osx_image: xcode11
    +      arch: amd64
    +      env:
    +        - POSTGRESQL_VERSION: 9.5
    +          JAVA_VERSION: 14
    +          JVM_IMPL: hotspot
    +          MVN_VERSION: 3.6.3
    +
     cache:
       directories:
         - $HOME/.m2
    -before_install:
    -  - . .travis/travis_install_postgresql.sh
    +
    +before_install: |
    +  javaUrl=https://api.adoptopenjdk.net/v3/binary/latest
    +  javaUrl="$javaUrl/$JAVA_VERSION/ga/${TRAVIS_OS_NAME//osx/mac}"
    +  javaUrl="$javaUrl/${TRAVIS_CPU_ARCH//amd64/x64}/jdk"
    +  javaUrl="$javaUrl/$JVM_IMPL/normal/adoptopenjdk"
    +
    +  installJdk=$(which install-jdk.sh) || {
    +    wget https://raw.githubusercontent.com/sormuras/bach/8c457fd6e46bd9f3f575867dd0c9af1d7edfd5b4/install-jdk.sh
    +    installJdk=./install-jdk.sh
    +
    +    printf '%s\n%s\n%s\n%s\n%s\n' \
    +      '--- install-jdk.sh' \
    +      '+++ install-jdk.sh' \
    +      '@@ -257 +257 @@' \
    +      '-            target="${workspace}"/$(tar --list ${tar_options} | head -2 | tail -1 | cut -f 2 -d '"'/' -)/Contents/Home" \
    +      '+            target="${workspace}"/$(tar --list ${tar_options} | sed -n '"'/\/bin\/javac/s///p')" \
    +    | patch "$installJdk"
    +  }
    +
    +  [[ $JAVA_VERSION == 9 ]] && certs=--cacerts || unset certs
    +
    +  . "$installJdk" --url "$javaUrl" ${certs+"$certs"}
    +
    +  mvnUrl=https://archive.apache.org/dist/maven/maven-3
    +  mvnUrl="$mvnUrl/$MVN_VERSION/binaries/apache-maven-$MVN_VERSION-bin.tar.gz"
    +
    +  wget --no-verbose "$mvnUrl" && tar xzf "apache-maven-$MVN_VERSION-bin.tar.gz"
    +  mvn="./apache-maven-$MVN_VERSION/bin/mvn"
    +  "$mvn" --version
    +
    +  . .travis/travis_install_postgresql.sh
     
     install: |
       $pgConfig
    @@ -29,7 +104,7 @@ install: |
       fi
       libjvm=$(find "$JAVA_HOME" -mindepth 2 -name $libjvm_name | head -n 1)
     
    -  mvn clean install --batch-mode \
    +  "$mvn" clean install --batch-mode \
        -Dpgsql.pgconfig="$pgConfig" \
        -Dpljava.libjvmdefault="$libjvm" \
        -Psaxon-examples -Ppgjdbc-ng \
    @@ -55,9 +130,9 @@ script: |
         tail -n 1
       )
     
    -  sudo java -Dpgconfig="$pgConfig" -jar "$packageJar"
    +  sudo "$JAVA_HOME"/bin/java -Dpgconfig="$pgConfig" -jar "$packageJar"
     
    -  jshell \
    +  "$JAVA_HOME"/bin/jshell \
         -execution local \
         "-J--class-path=$packageJar:$jdbcJar" \
         "--class-path=$packageJar" \
    @@ -65,7 +140,7 @@ script: |
         "-J-Dpgconfig=$pgConfig" \
         "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" \
         "-J-DmavenRepo=$mavenRepo" \
    -    "-J-DsaxonVer=$saxonVer" - <<\ENDJSHELL
    +    "-J-DsaxonVer=$saxonVer" - <<\ENDJSHELL && # continues after here document
     
       boolean succeeding = false; // begin pessimistic
     
    @@ -179,27 +254,4 @@ script: |
       succeeding &= (0 == results.get("ng"));
       System.exit(succeeding ? 0 : 1)
       ENDJSHELL
    -
    -jobs:
    -  include:
    -    - os: linux
    -      jdk: openjdk14
    -      env: POSTGRESQL_VERSION=11
    -    - os: linux
    -      jdk: openjdk14
    -      env: POSTGRESQL_VERSION=10
    -    - os: linux
    -      jdk: openjdk14
    -      env: POSTGRESQL_VERSION=9.5
    -    - os: osx
    -      jdk: openjdk14
    -      env: POSTGRESQL_VERSION=11
    -    - os: osx
    -      jdk: openjdk14
    -      env: POSTGRESQL_VERSION=10
    -    - os: osx
    -      jdk: openjdk14
    -      env: POSTGRESQL_VERSION=9.5
    -    - os: linux
    -      jdk: openjdk14
    -      env: POSTGRESQL_VERSION=SOURCE
    +  : travis wants something after the end of the here document
    diff --git a/.travis/travis_install_postgresql.sh b/.travis/travis_install_postgresql.sh
    index d2d62b16..42b4442f 100755
    --- a/.travis/travis_install_postgresql.sh
    +++ b/.travis/travis_install_postgresql.sh
    @@ -13,8 +13,7 @@ if [ "$TRAVIS_OS_NAME" = "osx" ]; then
     
         pgConfig="/usr/local/opt/postgresql${POSTGRESQL_VERSION}/bin/pg_config"
     else
    -    sudo chmod 777 /home/travis
    -    sudo service postgresql stop
    +    sudo sh -c 'service postgresql stop || true'
         sudo apt-get -qq remove postgresql libpq-dev libpq5 postgresql-client-common postgresql-common --purge
         if [ "$POSTGRESQL_VERSION" = "SOURCE" ]; then
             sudo apt-get -qq install build-essential libreadline-dev zlib1g-dev flex bison libxml2-dev libxslt-dev libssl-dev libxml2-utils xsltproc
    
    From f58d60d73ff9d2fcb63ca858c184fd871374e0af Mon Sep 17 00:00:00 2001
    From: Chapman Flack 
    Date: Sat, 26 Sep 2020 19:25:27 -0400
    Subject: [PATCH 0714/1087] No cacerts for Java 9 on ppc64le
    
    ---
     .travis.yml | 7 +++++++
     1 file changed, 7 insertions(+)
    
    diff --git a/.travis.yml b/.travis.yml
    index 9a38771c..ca07960a 100644
    --- a/.travis.yml
    +++ b/.travis.yml
    @@ -32,6 +32,13 @@ env:
         MVN_VERSION: 3.6.3
     
     jobs:
    +  exclude:
    +    - arch: ppc64le
    +      env:
    +        POSTGRESQL_VERSION: 12
    +        JAVA_VERSION: 9
    +        JVM_IMPL: hotspot
    +        MVN_VERSION: 3.6.3
       include:
         - os: osx
           osx_image: xcode11
    
    From 2d6b5eb51c5fd7860f652dffd1736d81af689af9 Mon Sep 17 00:00:00 2001
    From: Chapman Flack 
    Date: Sat, 26 Sep 2020 21:29:11 -0400
    Subject: [PATCH 0715/1087] Fix one JNI method-type mismatch
    
    ... introduced in cdea77f, using CallStaticObjectMethod on
    a method that returns void. Hotspot's -Xcheck:jni never complained,
    but OpenJ9's catches it, and unless given as -Xcheck:jni:nonfatal,
    OpenJ9's will abort the process, reported as a SIGSEGV, making it
    perhaps more alarming than it needed to be.
    ---
     pljava-so/src/main/c/Function.c | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c
    index b80ba2b6..6fc47ba7 100644
    --- a/pljava-so/src/main/c/Function.c
    +++ b/pljava-so/src/main/c/Function.c
    @@ -371,7 +371,7 @@ jdouble pljava_Function_doubleInvoke(Function self)
     
     void pljava_Function_udtWriteInvoke(jobject value, jobject stream)
     {
    -	JNI_callStaticObjectMethod(s_EntryPoints_class,
    +	JNI_callStaticVoidMethod(s_EntryPoints_class,
     		s_EntryPoints_udtWriteInvoke, value, stream);
     }
     
    
    From d2cba055f72e7d9f980c9056ff6ec90082e6f4ac Mon Sep 17 00:00:00 2001
    From: Chapman Flack 
    Date: Sun, 27 Sep 2020 11:56:41 -0400
    Subject: [PATCH 0716/1087] PG13 drops CREATE EXTENSION ... FROM unpackaged
    
    PostgreSQL 13 drops support for CREATE EXTENSION ... FROM unpackaged; on
    the rationale that any sensible site has already updated old unpackaged
    extensions to their extension versions. For PL/Java, though, there is
    still a realistic scenario where it ends up installed as 'unpackaged':
    if a CREATE EXTENSION failed because a setting needed adjustment, the
    admin supplied the right setting, and the installation then succeeded.
    That leaves PL/Java installed, but not as a packaged extension. The old
    CREATE EXTENSION ... FROM unpackaged; syntax was the perfect recovery
    method for that. It will still work in versions < 13.
    
    For PostgreSQL 13, recovery now requires two steps instead. The first
    step is CREATE EXTENSION pljava VERSION unpackaged; which will use this
    script to simply confirm the unpackaged installation has already
    happened, and otherwise do absolutely nothing. The second step (which
    must happen in a new session) is ALTER EXTENSION pljava UPDATE; which
    will package it as the latest extension version, even running the exact
    script that CREATE EXTENSION ... FROM unpackaged; would've run to do it.
    ---
     pljava-packaging/build.xml                    |  4 +-
     .../main/resources/pljava--unpackaged--.sql   | 80 +++++++++++++++
     .../src/main/resources/pljava--unpackaged.sql | 97 ++++---------------
     pljava-so/src/main/c/Backend.c                |  9 +-
     4 files changed, 111 insertions(+), 79 deletions(-)
     create mode 100644 pljava-packaging/src/main/resources/pljava--unpackaged--.sql
    
    diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml
    index 7eb80a8b..53526662 100644
    --- a/pljava-packaging/build.xml
    +++ b/pljava-packaging/build.xml
    @@ -273,7 +273,7 @@ jos.close();
     			
    -			
    @@ -331,7 +331,7 @@ jos.close();
     			/>
     						
    +				excludes="pljava.sql pljava--.sql pljava--unpackaged--.sql"/>			
     
    +			
     			
     		org.postgresql
     		pljava.app
    -		1.5.6-SNAPSHOT
    +		1.5-SNAPSHOT
     	
     	pljava-packaging
     	PL/Java packaging
    diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml
    index 8ce37c61..588b10e0 100644
    --- a/pljava-so/pom.xml
    +++ b/pljava-so/pom.xml
    @@ -4,7 +4,7 @@
     	
     		org.postgresql
     		pljava.app
    -		1.5.6-SNAPSHOT
    +		1.5-SNAPSHOT
     	
     	pljava-so
     	PL/Java backend native code
    diff --git a/pljava/pom.xml b/pljava/pom.xml
    index 16f4aa49..691a6c42 100644
    --- a/pljava/pom.xml
    +++ b/pljava/pom.xml
    @@ -4,7 +4,7 @@
     	
     		org.postgresql
     		pljava.app
    -		1.5.6-SNAPSHOT
    +		1.5-SNAPSHOT
     	
     	pljava
     	PL/Java backend Java code
    diff --git a/pom.xml b/pom.xml
    index 20b8cda9..1d5c46e5 100644
    --- a/pom.xml
    +++ b/pom.xml
    @@ -3,7 +3,7 @@
     	4.0.0
     	org.postgresql
     	pljava.app
    -	1.5.6-SNAPSHOT
    +	1.5-SNAPSHOT
     	pom
     	PostgreSQL PL/Java
     	https://tada.github.io/pljava/
    
    From c198a5e4d283fb4bf6f3ab4665441bb79f8f294c Mon Sep 17 00:00:00 2001
    From: Chapman Flack 
    Date: Sun, 4 Oct 2020 17:38:55 -0400
    Subject: [PATCH 0739/1087] Remove ordering constraint on variadic test
    
    As expected, changing Type.c's s_obtainerByJavaName cache to use
    name+oid as the key eliminated the ordering-dependent failure that
    was seen in 6745166 and required the ordering constraint that was
    added here, so that can now be removed.
    ---
     .../pljava/example/annotation/AnyTest.java    | 19 +++++--------------
     1 file changed, 5 insertions(+), 14 deletions(-)
    
    diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java
    index d36c0aa1..45645141 100644
    --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java
    +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java
    @@ -27,20 +27,11 @@
      * This SQLAction declares a function that refers to Java's own String.format
      * method, which is variadic, and tests its behavior.
      *
    - * It has a 'provides' string, and makeArray below 'requires' it, because there
    - * is a bug in the C Type caching that will make this test fail if the
    - * makeArray function has been initialized first. (Until that bug is fixed, the
    - * time is not ripe to outright advertise that variadic functions like this are
    - * supported. But this test is here to ensure at least the support does not
    - * regress.)
    - *
    - * The bug is that PL/Java treats java.lang.Object[] as anyarray, intended to be
    - * adaptable to whatever array type is passed, but once the Type caching logic
    - * has already associated the Oid for anyarray upon seeing the return type of
    - * makeArray below, it cannot later supply a Type for text[] as seen in the
    - * parameter list of format().
    + * Its presence in this file is an artifact of history: it isn't much related
    + * to the functions otherwise defined here, but once had an ordering dependency
    + * with one of them because of a bug in the type system.
      */
    -@SQLAction(provides = "String.format tested",
    +@SQLAction(
     	install = {
     		"CREATE FUNCTION javatest.format(" +
     		"  format pg_catalog.text," +
    @@ -98,7 +89,7 @@ public static Object logAnyElement(
     	 * one-element array with the object's class as its element type.
     	 */
     	@Function(schema="javatest", effects=IMMUTABLE, onNullInput=RETURNS_NULL,
    -		type="pg_catalog.anyarray", requires="String.format tested")
    +		type="pg_catalog.anyarray")
     	public static Object[] makeArray(
     		@SQLType("pg_catalog.anyelement") Object param)
     	{
    
    From 81c704d3b37283abd8e6d493b7f2e1a9b7f78e7a Mon Sep 17 00:00:00 2001
    From: Chapman Flack 
    Date: Fri, 9 Oct 2020 18:18:40 -0400
    Subject: [PATCH 0740/1087] Make Markdown rendering work, again
    
    The last time around this tree was 6fabe39, when the site plugin
    had to be downgraded to 3.8.2 because 3.9.0 wouldn't work.
    
    But today all the `code` markup in the Markdown documentation is
    getting ignored, which seems to match the topic of this report:
    https://issues.apache.org/jira/browse/DOXIA-597
    
    which apparently can't be fixed by bumping the version of the
    module that does that, so try instead eliminating the version
    peg for that module and bumping the site plugin to 3.9.1. So
    far, it seems that 3.9.1 doesn't fail (and still works in Maven
    3.5.2) and the `code` markup works again.
    ---
     pom.xml | 7 +------
     1 file changed, 1 insertion(+), 6 deletions(-)
    
    diff --git a/pom.xml b/pom.xml
    index a63b9ef8..289cb805 100644
    --- a/pom.xml
    +++ b/pom.xml
    @@ -200,13 +200,8 @@
     			
     				org.apache.maven.plugins
     				maven-site-plugin
    -				3.8.2
    +				3.9.1
     				
    -					
    -						org.apache.maven.doxia
    -						doxia-module-markdown
    -						1.6
    -					
     					
     						net.trajano.wagon
     						wagon-git
    
    From b7bc38b3a0d852c73d34ee00fb2d496619abfb50 Mon Sep 17 00:00:00 2001
    From: Chapman Flack 
    Date: Sat, 10 Oct 2020 12:52:29 -0400
    Subject: [PATCH 0741/1087] Teach the SQL generator about variadic functions
    
    ---
     .../pljava/annotation/Function.java           | 10 +++-
     .../annotation/processing/DDRProcessor.java   | 48 ++++++++++++++-----
     2 files changed, 46 insertions(+), 12 deletions(-)
    
    diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java
    index 31bea59d..fac1ae1a 100644
    --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java
    +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java
    @@ -92,6 +92,14 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE };
     	 */
     	String schema() default "";
     
    +	/**
    +	 * Whether PostgreSQL should gather trailing arguments into an array that
    +	 * will be bound to the last (non-output) Java parameter (which must have an
    +	 * array type).
    +	 * Appeared in PostgreSQL 8.4.
    +	 */
    +	boolean variadic() default false;
    +
     	/**
     	 * Estimated cost in units of cpu_operator_cost.
     	 *
    @@ -161,7 +169,7 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE };
     	 * please see the users' guide.
     	 *

    - * Appeared in 9.6. + * Appeared in PostgreSQL 9.6. */ Parallel parallel() default Parallel.UNSAFE; 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 5699f259..a193c091 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 @@ -1568,6 +1568,7 @@ class FunctionImpl public String type() { return _type; } public String name() { return _name; } public String schema() { return _schema; } + public boolean variadic() { return _variadic; } public OnNullInput onNullInput() { return _onNullInput; } public Security security() { return _security; } public Effects effects() { return _effects; } @@ -1586,6 +1587,7 @@ class FunctionImpl public String _type; public String _name; public String _schema; + public boolean _variadic; public OnNullInput _onNullInput; public Security _security; public Effects _effects; @@ -1736,6 +1738,15 @@ else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) */ resolveParameterAndReturnTypes(); + if ( _variadic ) + { + int last = parameterTypes.length - 1; + if ( 0 > last || ! parameterTypes[last].isArray() ) + msg( Kind.ERROR, func, + "VARIADIC function must have a last, non-output " + + "parameter that is an array"); + } + recordImplicitTags(); recordExplicitTags(_provides, _requires); @@ -1880,17 +1891,26 @@ void appendNameAndParams( StringBuilder sb, boolean dflts) void appendParams( StringBuilder sb, boolean dflts) { - sb.append(parameterInfo() - .map( - i -> - { - String name = null == i.st ? null : i.st.name(); - if ( null == name ) - name = i.ve.getSimpleName().toString(); - return "\n\t" + name + " " + i.dt.toString(dflts); - }) - .collect(joining(",")) - ); + int count = parameterTypes.length; + for ( ParameterInfo i + : (Iterable)parameterInfo()::iterator ) + { + -- count; + + String name = null == i.st ? null : i.st.name(); + if ( null == name ) + name = i.ve.getSimpleName().toString(); + + sb.append("\n\t"); + + if ( _variadic && 0 == count ) + sb.append("VARIADIC "); + + sb.append(name).append(' ').append(i.dt.toString(dflts)); + + if ( 0 < count ) + sb.append(','); + } } void appendAS( StringBuilder sb) @@ -2123,6 +2143,12 @@ public void setType( Object o, boolean explicit, Element e) "The type of a UDT function may not be changed"); } + public void setVariadic( Object o, boolean explicit, Element e) + { + if ( explicit ) + msg( Kind.ERROR, e, "A UDT function is not variadic"); + } + public void setRows( Object o, boolean explicit, Element e) { if ( explicit ) From e515408fe6bf2278712cb4e580c4456abe0d831b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Oct 2020 17:41:05 -0400 Subject: [PATCH 0742/1087] Give variadic functions their own example class --- .../pljava/example/annotation/AnyTest.java | 29 ---- .../pljava/example/annotation/Variadic.java | 139 ++++++++++++++++++ 2 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java index 45645141..f1f7515b 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/AnyTest.java @@ -23,35 +23,6 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; -/* - * This SQLAction declares a function that refers to Java's own String.format - * method, which is variadic, and tests its behavior. - * - * Its presence in this file is an artifact of history: it isn't much related - * to the functions otherwise defined here, but once had an ordering dependency - * with one of them because of a bug in the type system. - */ -@SQLAction( - install = { - "CREATE FUNCTION javatest.format(" + - " format pg_catalog.text," + - " VARIADIC args pg_catalog.text[])" + - " RETURNS pg_catalog.text" + - " LANGUAGE java" + - " AS 'java.lang.String=" + - " java.lang.String.format(java.lang.String,java.lang.Object[])'", - - "SELECT" + - " CASE" + - " WHEN result OPERATOR(pg_catalog.=) 'Hello, world'" + - " THEN javatest.logmessage('INFO', 'variadic call ok')" + - " ELSE javatest.logmessage('WARNING', 'variadic call ng')" + - " END" + - " FROM" + - " javatest.format('Hello, %s', 'world') AS result" - }, - remove = "DROP FUNCTION javatest.format(pg_catalog.text,pg_catalog.text[])" -) /** * Provides example methods to illustrate the polymorphic types {@code any}, * {@code anyarray}, and {@code anyelement}. diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java new file mode 100644 index 00000000..725198c1 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2020 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 static java.util.Arrays.stream; +import java.util.Objects; + +import org.postgresql.pljava.annotation.Function; +import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; +import static + org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL; +import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLType; + +/* + * This SQLAction declares a function that refers to Java's own String.format + * method, which is variadic, and tests its behavior, as well as the locally- + * defined sumOfSquares and sumOfSquaresBoxed functions below. + */ +@SQLAction( + requires = { "sumOfSquares", "sumOfSquaresBoxed" }, + install = { + "CREATE FUNCTION javatest.javaformat(" + + " format pg_catalog.text," + + " VARIADIC args pg_catalog.anyarray DEFAULT CAST(ARRAY[] AS text[]))" + + " RETURNS pg_catalog.text" + + " RETURNS NULL ON NULL INPUT" + + " LANGUAGE java" + + " AS 'java.lang.String=" + + " java.lang.String.format(java.lang.String,java.lang.Object[])'", + + "COMMENT ON FUNCTION javatest.javaformat(" + + " pg_catalog.text, VARIADIC pg_catalog.anyarray) IS '" + + "Invoke Java''s String.format with a format string and any number of " + + "arguments. This is not quite as general as the Java method implies, " + + "because, while the variadic argument is declared ''anyarray'' and " + + "its members can have any type, PostgreSQL requires all of them to " + + "have the same type in any given call. Furthermore, in the VARIADIC " + + "anyarray case, as here, the actual arguments must not all have " + + "''unknown'' type; if supplying bare literals, one must be cast to " + + "a type. PostgreSQL will not recognize a call of a variadic function " + + "unless at least one argument to populate the variadic parameter is " + + "supplied; to allow calls that don''t pass any, give the variadic " + + "parameter an empty-array default, as done here.'", + + "SELECT" + + " CASE" + + " WHEN s.ok AND d.ok" + + " THEN javatest.logmessage('INFO', 'variadic calls ok')" + + " ELSE javatest.logmessage('WARNING', 'variadic calls ng')" + + " END" + + " FROM" + + " (SELECT" + + " every(expect IS NOT DISTINCT FROM got)" + + " FROM" + + " (VALUES" + + " (" + + " 'Hello, world'," + + " javatest.javaformat('Hello, %s', 'world'::text)" + + " )" + + " ) AS t(expect, got)" + + " ) AS s(ok)," + + " (SELECT" + + " every(expect IS NOT DISTINCT FROM got)" + + " FROM" + + " (VALUES" + + " (14.0, javatest.sumOfSquares(1, 2, 3))," + + " (14.0, javatest.sumOfSquares(1, 2, null, 3))," + + " ( 0.0, javatest.sumOfSquares())," + + " (14.0, javatest.sumOfSquaresBoxed(1, 2, 3))," + + " (null, javatest.sumOfSquaresBoxed(1, 2, null, 3))" + + " ) AS t(expect, got)" + + " ) AS d(ok)" + }, + + remove = "DROP FUNCTION javatest.javaformat(pg_catalog.text,anyarray)" +) +/** + * Provides example methods to illustrate variadic functions. + *

    + * The key is the {@code @Function} annotation declaring the function variadic + * to PostgreSQL. The Java method parameter is declared as an ordinary array, + * not with Java's {@code ...} syntax; in fact, that would be impossible for a + * function with a composite return type (where the Java signature would have to + * include a {@code ResultSet} parameter after the variadic input parameter). + */ +public class Variadic { + /** + * Compute a double-precision sum of squares, returning null if any input + * value is null. + *

    + * The {@code RETURNS_NULL} annotation does not mean the array collecting + * the variadic arguments cannot have null entries; it only means PostgreSQL + * will never call this function with null for the array itself. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL, + variadic = true, provides = "sumOfSquaresBoxed" + ) + public static Double sumOfSquaresBoxed(Double[] vals) + { + if ( stream(vals).anyMatch(Objects::isNull) ) + return null; + + return + stream(vals).mapToDouble(Double::doubleValue).map(v -> v*v).sum(); + } + + /** + * Compute a double-precision sum of squares, treating any null input + * as zero. + *

    + * The {@code RETURNS_NULL} annotation does not mean the array collecting + * the variadic arguments cannot have null entries; it only means PostgreSQL + * will never call this function with null for the array itself. Because + * the Java parameter type here is primitive and cannot represent nulls, + * PL/Java will have silently replaced any nulls in the input with zeros. + *

    + * This version can be called with no arguments (returning zero, naturally), + * because it is given an empty-array default. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL, + variadic = true, provides = "sumOfSquares" + ) + public static double sumOfSquares(@SQLType(defaultValue={}) double[] vals) + { + return stream(vals).map(v -> v*v).sum(); + } +} From 03e1d36a38b505f152e35c558ae7db9bac6adb20 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Oct 2020 20:58:17 -0400 Subject: [PATCH 0743/1087] Follow existing style in BaseUDTFunctionImpl It initializes its instance fields. A new boolean would be false by default anyway, but ... follow the style. In the diagnostic, "never" rather than "not" better conveys why just mentioning the element at all merits a diagnostic. --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a193c091..7996615a 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 @@ -2087,6 +2087,7 @@ class BaseUDTFunctionImpl extends FunctionImpl _name = Identifier.Simple.fromJava(ui.name()) .concat("_", id.getSuffix()).toString(); _schema = ui.schema(); + _variadic = false; _cost = -1; _rows = -1; _onNullInput = OnNullInput.CALLED; @@ -2146,7 +2147,7 @@ public void setType( Object o, boolean explicit, Element e) public void setVariadic( Object o, boolean explicit, Element e) { if ( explicit ) - msg( Kind.ERROR, e, "A UDT function is not variadic"); + msg( Kind.ERROR, e, "A UDT function is never variadic"); } public void setRows( Object o, boolean explicit, Element e) From 698ea9153e32262e7e670bea5ec27375c9be13db Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Oct 2020 21:15:32 -0400 Subject: [PATCH 0744/1087] More schema-qualification in the new example The older examples have never been gone through to schema-qualify things, but perhaps a new one should set a better, hmm, example. This is not an easy habit to cultivate. --- .../org/postgresql/pljava/example/annotation/Variadic.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java index 725198c1..5a5c7cbb 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java @@ -31,7 +31,8 @@ install = { "CREATE FUNCTION javatest.javaformat(" + " format pg_catalog.text," + - " VARIADIC args pg_catalog.anyarray DEFAULT CAST(ARRAY[] AS text[]))" + + " VARIADIC args pg_catalog.anyarray" + + " DEFAULT CAST(ARRAY[] AS pg_catalog.text[]))" + " RETURNS pg_catalog.text" + " RETURNS NULL ON NULL INPUT" + " LANGUAGE java" + @@ -60,7 +61,7 @@ " END" + " FROM" + " (SELECT" + - " every(expect IS NOT DISTINCT FROM got)" + + " pg_catalog.every(expect IS NOT DISTINCT FROM got)" + " FROM" + " (VALUES" + " (" + @@ -70,7 +71,7 @@ " ) AS t(expect, got)" + " ) AS s(ok)," + " (SELECT" + - " every(expect IS NOT DISTINCT FROM got)" + + " pg_catalog.every(expect IS NOT DISTINCT FROM got)" + " FROM" + " (VALUES" + " (14.0, javatest.sumOfSquares(1, 2, 3))," + From 0eb0494ef09c8b31b472fe0c09c25599d93c33d1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Oct 2020 12:28:10 -0400 Subject: [PATCH 0745/1087] Expand the example's javadoc comments --- .../pljava/example/annotation/Variadic.java | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java index 5a5c7cbb..58e8c83a 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Variadic.java @@ -21,14 +21,24 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; -/* - * This SQLAction declares a function that refers to Java's own String.format - * method, which is variadic, and tests its behavior, as well as the locally- - * defined sumOfSquares and sumOfSquaresBoxed functions below. +/** + * Provides example methods to illustrate variadic functions. + *

    + * The key is the {@code @Function} annotation declaring the function variadic + * to PostgreSQL. The Java method parameter is declared as an ordinary array, + * not with Java's {@code ...} syntax; in fact, that would be impossible for a + * function with a composite return type (where the Java signature would have to + * include a {@code ResultSet} parameter after the variadic input parameter). */ @SQLAction( requires = { "sumOfSquares", "sumOfSquaresBoxed" }, install = { + /* + * In addition to the two sumOfSquares functions that are defined in + * this class using annotations, emit some direct SQL to declare a + * javaformat function that refers directly to java.lang.String.format, + * which is a function declared variadic in Java. + */ "CREATE FUNCTION javatest.javaformat(" + " format pg_catalog.text," + " VARIADIC args pg_catalog.anyarray" + @@ -53,6 +63,9 @@ "supplied; to allow calls that don''t pass any, give the variadic " + "parameter an empty-array default, as done here.'", + /* + * Test a bunch of variadic calls. + */ "SELECT" + " CASE" + " WHEN s.ok AND d.ok" + @@ -85,16 +98,9 @@ remove = "DROP FUNCTION javatest.javaformat(pg_catalog.text,anyarray)" ) -/** - * Provides example methods to illustrate variadic functions. - *

    - * The key is the {@code @Function} annotation declaring the function variadic - * to PostgreSQL. The Java method parameter is declared as an ordinary array, - * not with Java's {@code ...} syntax; in fact, that would be impossible for a - * function with a composite return type (where the Java signature would have to - * include a {@code ResultSet} parameter after the variadic input parameter). - */ public class Variadic { + private Variadic() { } // do not instantiate + /** * Compute a double-precision sum of squares, returning null if any input * value is null. @@ -126,8 +132,11 @@ public static Double sumOfSquaresBoxed(Double[] vals) * the Java parameter type here is primitive and cannot represent nulls, * PL/Java will have silently replaced any nulls in the input with zeros. *

    - * This version can be called with no arguments (returning zero, naturally), - * because it is given an empty-array default. + * This version also demonstrates using {@link SQLType @SQLType} to give + * the variadic parameter an empty-array default, so PostgreSQL will allow + * the function to be called with no corresponding arguments. Without that, + * PostgreSQL won't recognize a call to the function unless at least one + * argument corresponding to the variadic parameter is supplied. */ @Function( schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL, From 442c7455100e40adb7b46ed175c05578112b08dc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Oct 2020 20:36:50 -0400 Subject: [PATCH 0746/1087] Try never using the runner's install-jdk.sh It is an older version, and has been messing jobs up--not consistently, but I haven't seen any fail with the downloaded version ... yet .... In passing, fix one case of overlooked quotes, and simplify (very slightly) the jshell invocation: the --add-modules doesn't need to mention java.sql, because that's a transitive requirement of java.sql.rowset. --- .travis.yml | 6 +++--- appveyor.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca3f636d..ab996476 100644 --- a/.travis.yml +++ b/.travis.yml @@ -75,7 +75,7 @@ before_install: | javaUrl="$javaUrl/${TRAVIS_CPU_ARCH//amd64/x64}/jdk" javaUrl="$javaUrl/$JVM_IMPL/normal/adoptopenjdk" - installJdk=$(which install-jdk.sh) || { + installJdk=$(false && which install-jdk.sh) || { wget https://raw.githubusercontent.com/sormuras/bach/8c457fd6e46bd9f3f575867dd0c9af1d7edfd5b4/install-jdk.sh installJdk=./install-jdk.sh @@ -102,7 +102,7 @@ before_install: | . .travis/travis_install_postgresql.sh install: | - $pgConfig + "$pgConfig" if [ "$TRAVIS_OS_NAME" = "osx" ]; then libjvm_name="libjli.dylib" @@ -143,7 +143,7 @@ script: | -execution local \ "-J--class-path=$packageJar:$jdbcJar" \ "--class-path=$packageJar" \ - "-J--add-modules=java.sql,java.sql.rowset" \ + "-J--add-modules=java.sql.rowset" \ "-J-Dpgconfig=$pgConfig" \ "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" \ "-J-DmavenRepo=$mavenRepo" \ diff --git a/appveyor.yml b/appveyor.yml index 0ee84b15..8fab1166 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -203,7 +203,7 @@ test_script: -execution local ` "-J--class-path=$packageJar;$jdbcJar" ` "--class-path=$packageJar" ` - "-J--add-modules=java.sql,java.sql.rowset" ` + "-J--add-modules=java.sql.rowset" ` "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` "-J-Dpgconfig=$pgConfig" ` "-J-DmavenRepo=$mavenRepo" ` From 4a6cad3610024d4356e980aa97607d3120c6c1b6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Oct 2020 20:03:13 -0400 Subject: [PATCH 0747/1087] Mistrust localized punctuation (issue #312) Instead of looking for an element name inside some kind of punctuation, just use negative lookahead/lookbehind to match the expected element name neither preceded nor followed by another XML NameChar. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index a9bf4a45..e6cdb54e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -4273,8 +4273,40 @@ public T loadExternalDTD(boolean v) static class SAXDOMErrorHandler implements ErrorHandler { static final SAXDOMErrorHandler INSTANCE = new SAXDOMErrorHandler(); - static final Pattern s_wrapelement = Pattern.compile( - "^cvc-elt\\.1(?:\\.a)?+:.*'pljava-content-wrap'"); + static final String s_xmlNameChar = + /* + * Issue #312: localized error messages from the schema validator + * don't always use the same punctuation around the offending + * element name! One option is to ignore the punctuation; the + * wrapper element name is unlikely to appear as part of some other + * element name by chance. But then ... a validation error matched + * by s_wrapelement will be ignored. Could anything devious be + * achieved by faking an element name that gets its validation error + * ignored? Not that I can think of, but am I devious enough? + * + * Rather than looking for punctuation, just use negative lookbehind + * and lookahead to ensure that the element name in this pattern + * is neither preceded nor followed by another XML NameChar. Simple + * enough, except XML NameChar isn't a category built in to + * java.util.regex, and I haven't found anything in the XML APIs + * exporting it (even though I bet it's buried in there in a dozen + * different places), and it has a fairly messy definition: + * + * https://www.w3.org/TR/REC-xml/#NT-NameStartChar + * https://www.w3.org/TR/xml11/#NT-NameStartChar + */ + "[:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02FF\\u0370-\\u037D" + + "\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF" + + "\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD" + + "\\x{10000}-\\x{EFFFF}" + + /* + * https://www.w3.org/TR/REC-xml/#NT-NameChar + * https://www.w3.org/TR/xml11/#NT-NameChar + */ + "\\x2D.0-9\\xB7\\u0300-\\u036F\\u203F-\\u2040]"; + static final Pattern s_wrapelement = Pattern.compile(String.format( + "^cvc-elt\\.1(?:\\.a)?+:.*(? Date: Tue, 13 Oct 2020 00:41:05 +0000 Subject: [PATCH 0748/1087] Bump junit from 4.12 to 4.13.1 Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.1. - [Release notes](https://github.com/junit-team/junit4/releases) - [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md) - [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.1) Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 289cb805..cc8b1d68 100644 --- a/pom.xml +++ b/pom.xml @@ -157,7 +157,7 @@ junit junit - 4.12 + 4.13.1 test From fc3a0028fd6dc82961270d273c2141be234caa57 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 14 Oct 2020 20:11:31 -0400 Subject: [PATCH 0749/1087] Rethought issue #312 approach The previous approach, replacing the punctuation check with a messy non-XML-NameChar check, persisted in an ill-thought-out belief that it's important to avoid giving a pass on error reporting to an element name that happens to contain the name of our wrapping element. Anybody who wanted to sneak an element past the error reporting would not be prevented from using exactly the name of our wrapping element to do it. So the extra work to avoid the containing-name case achieves nothing. Better to just count the exceptions. If we are using a wrapping element, it will be first. No special treatment after that one (or at all, if we are not wrapping). --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 115 ++++++++++++------ 1 file changed, 77 insertions(+), 38 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index e6cdb54e..1b2a5e54 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -1501,6 +1501,11 @@ static class Verifier extends VarlenaWrapper.Verifier.Base { private XMLReader m_xr; + /** + * Constructor called only from {@code adopt()} when an untouched + * {@code Readable} is being bounced back to PostgreSQL with a type Oid + * different from its original. + */ Verifier() throws SQLException { try @@ -1518,6 +1523,11 @@ static class Verifier extends VarlenaWrapper.Verifier.Base } } + /** + * Constructor called with an already-constructed {@code XMLReader}. + *

    + * Adjustments may have been made to the {@code XMLReader}. + */ Verifier(XMLReader xr) { m_xr = xr; @@ -1529,6 +1539,16 @@ protected void verify(InputStream is) throws Exception boolean[] wrapped = { false }; is = correctedDeclStream( is, false, implServerCharset(), wrapped); + + /* + * The supplied XMLReader is never set up to do unwrapping, which is + * ok; it never needs to. But it will have had its error handler set + * on that assumption, which must be changed here if wrapping is in + * effect, just in case schema validation has been requested. + */ + if ( wrapped[0] ) + m_xr.setErrorHandler(SAXDOMErrorHandler.instance(true)); + /* * What does an XMLReader do if no handlers have been set for * content events? Parses everything and discards the events. @@ -3955,6 +3975,11 @@ static class Parsing extends XMLCopier { super(tgt); InputSource is = src.getInputSource(); + /* + * No correctedDeclStream, no check for unwrapping: if some + * random {@code SQLXML} implementation is passing a stream + * to parse, it had better make sense to a vanilla parser. + */ m_source = new AdjustingSAXSource(is, false); } @@ -4272,45 +4297,32 @@ public T loadExternalDTD(boolean v) */ static class SAXDOMErrorHandler implements ErrorHandler { - static final SAXDOMErrorHandler INSTANCE = new SAXDOMErrorHandler(); - static final String s_xmlNameChar = - /* - * Issue #312: localized error messages from the schema validator - * don't always use the same punctuation around the offending - * element name! One option is to ignore the punctuation; the - * wrapper element name is unlikely to appear as part of some other - * element name by chance. But then ... a validation error matched - * by s_wrapelement will be ignored. Could anything devious be - * achieved by faking an element name that gets its validation error - * ignored? Not that I can think of, but am I devious enough? - * - * Rather than looking for punctuation, just use negative lookbehind - * and lookahead to ensure that the element name in this pattern - * is neither preceded nor followed by another XML NameChar. Simple - * enough, except XML NameChar isn't a category built in to - * java.util.regex, and I haven't found anything in the XML APIs - * exporting it (even though I bet it's buried in there in a dozen - * different places), and it has a fairly messy definition: - * - * https://www.w3.org/TR/REC-xml/#NT-NameStartChar - * https://www.w3.org/TR/xml11/#NT-NameStartChar - */ - "[:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02FF\\u0370-\\u037D" + - "\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF" + - "\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD" + - "\\x{10000}-\\x{EFFFF}" + - /* - * https://www.w3.org/TR/REC-xml/#NT-NameChar - * https://www.w3.org/TR/xml11/#NT-NameChar - */ - "\\x2D.0-9\\xB7\\u0300-\\u036F\\u203F-\\u2040]"; - static final Pattern s_wrapelement = Pattern.compile(String.format( - "^cvc-elt\\.1(?:\\.a)?+:.*(? Date: Fri, 16 Oct 2020 21:39:28 -0400 Subject: [PATCH 0750/1087] Fix some lint warnings in the S9 example Though the normal build is without -Xlint, the unchecked warning summary message was showing up in CI build logs. Some of the iterator games can be simplified by checking size() of an XdmValue, generally in a context where it's known to be zero-or-one, so not an expensive test. Add javadoc for one method where it isn't inherited because the source is in a different subproject. --- .../postgresql/pljava/example/saxon/S9.java | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java index 7cc6be19..8b03cff9 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java @@ -291,7 +291,7 @@ public class S9 implements ResultSetProvider { private S9( - XdmSequenceIterator xsi, + XdmSequenceIterator xsi, XQueryEvaluator[] columnXQEs, SequenceType[] columnStaticTypes, XMLBinary enc) @@ -303,7 +303,7 @@ private S9( m_xmlbinary = enc; } - final XdmSequenceIterator m_sequenceIterator; + final XdmSequenceIterator m_sequenceIterator; final XQueryEvaluator[] m_columnXQEs; final SequenceType[] m_columnStaticTypes; final SequenceType s_01untypedAtomic = makeSequenceType( @@ -529,7 +529,7 @@ public static boolean xmlcast( } ItemType xsbt = mapSQLDataTypeToXMLSchemaDataType(op, enc, Nulls.ABSENT); - XdmSequenceIterator tv = (XdmSequenceIterator) + Iterator tv = xmlCastAsSequence(v, enc, xsbt).iterator(); try { @@ -558,19 +558,14 @@ public static boolean xmlcast( PredefinedQueryHolders.DocumentWrapUnwrap.INSTANCE.load(); xqe.setExternalVariable(PredefinedQueryHolders.s_qEXPR, xv); xv = xqe.evaluate(); - XdmSequenceIterator si = (XdmSequenceIterator)xv.iterator(); - if ( ! si.hasNext() ) + /* + * It's zero-or-one, or XPTY0004 was thrown here. + */ + if ( 0 == xv.size() ) { target.updateNull(1); return true; } - xv = si.next(); - if ( si.hasNext() ) - { - si.close(); - throw new XPathException( - "Atomized sequence has more than one item", "XPTY0004"); - } XdmAtomicValue av = (XdmAtomicValue)xv; xmlCastAsNonXML( av, ItemType.UNTYPED_ATOMIC, tg, target, 1, enc); @@ -756,7 +751,7 @@ private static XdmSequenceIterator evalXQuery( * as {@code $EXPR} to {@code document{$EXPR}}. */ private static SQLXML returnContent( - XdmSequenceIterator x, boolean nullOnEmpty) + Iterator x, boolean nullOnEmpty) throws SQLException, SaxonApiException, XPathException { if ( nullOnEmpty && ! x.hasNext() ) @@ -895,9 +890,9 @@ public static ResultSetProvider xmltable( } XQueryEvaluator rowXQE = rowXQX.load(); - XdmSequenceIterator rowIterator; + XdmSequenceIterator rowIterator; if ( storePassedValuesInDynamicContext(rowXQE, rowBindings, true) ) - rowIterator = (XdmSequenceIterator) + rowIterator = (XdmSequenceIterator) XdmEmptySequence.getInstance().iterator(); else rowIterator = rowXQE.iterator(); @@ -909,6 +904,9 @@ public static ResultSetProvider xmltable( } } + /** + * Called when PostgreSQL has no need for more rows of the tabular result. + */ @Override public void close() { @@ -1207,10 +1205,26 @@ public boolean assignRowValues(ResultSet receive, int currentRow) */ if ( null == m_columnXQEs [ i ] ) continue; + if ( Types.SQLXML == p.typeJDBC() ) continue; + /* - * Ok, the output column type is non-XML. If the column + * Ok, the output column type is non-XML; choose an atomizer, + * either a simple identity if the result type is statically + * known to be zero-or-one atomic, or the long way through the + * general-purpose one. If the type is statically known to be + * the empty sequence (weird, but not impossible), the identity + * atomizer suffices and we're on to the next column. + */ + OccurrenceIndicator occur = staticType.getOccurrenceIndicator(); + if ( OccurrenceIndicator.ZERO == occur ) + { + m_atomize [ i ] = (v, col) -> v; + continue; + } + + /* So, it isn't known to be empty. If the column * expression type isn't known to be atomic, or isn't known to * be zero-or-one, then the general-purpose atomizer--a trip * through data(document { ... } / child::node())--must be used. @@ -1221,9 +1235,9 @@ public boolean assignRowValues(ResultSet receive, int currentRow) * BUT NO ... Saxon is more likely to find a converter from * xs:untypedAtomic than from xs:anyAtomicType. */ - if ( staticType.getOccurrenceIndicator().allowsMany() - || ! ItemType.ANY_ATOMIC_VALUE.subsumes( - staticType.getItemType()) + ItemType itemType = staticType.getItemType(); + if ( occur.allowsMany() + || ! ItemType.ANY_ATOMIC_VALUE.subsumes(itemType) /* * The following tests may be punctilious to a fault. If we * have a bare Saxon atomic type of either xs:base64Binary @@ -1237,9 +1251,9 @@ public boolean assignRowValues(ResultSet receive, int currentRow) * succeed) in the cases where the specified, unoptimized * behavior would be to fail. */ - || ItemType.HEX_BINARY.subsumes(staticType.getItemType()) + || ItemType.HEX_BINARY.subsumes(itemType) && (XMLBinary.HEX != m_xmlbinary) - || ItemType.BASE64_BINARY.subsumes(staticType.getItemType()) + || ItemType.BASE64_BINARY.subsumes(itemType) && (XMLBinary.BASE64 != m_xmlbinary) ) { @@ -1252,17 +1266,10 @@ public boolean assignRowValues(ResultSet receive, int currentRow) docWrapUnwrap.setExternalVariable( PredefinedQueryHolders.s_qEXPR, v); v = docWrapUnwrap.evaluate(); - XdmSequenceIterator si = - (XdmSequenceIterator)v.iterator(); - if ( ! si.hasNext() ) - return v; - v = si.next(); - if ( ! si.hasNext() ) - return v; - si.close(); - throw new XPathException( - "Atomized sequence has more than one item " + - "(column " + col + ")", "XPTY0004"); + /* + * It's already zero-or-one, or XPTY0004 was thrown + */ + return v; }; } m_atomize [ i ] = atomizer; @@ -1280,7 +1287,7 @@ public boolean assignRowValues(ResultSet receive, int currentRow) * We know we'll be getting zero-or-one atomic value, so * the atomizing function can be the identity. */ - m_atomize [ i ] = (v, col) -> v; + m_atomize [ i ] = (v, col) -> v; } } } @@ -1586,6 +1593,7 @@ else if ( b.knownNonNull() ) return xftn; } + @SuppressWarnings("fallthrough") private static ItemType mapSQLDataTypeToXMLSchemaDataType( Binding b, XMLBinary xmlbinary, Nulls nulls) throws SQLException @@ -1645,6 +1653,7 @@ private static ItemType mapSQLDataTypeToXMLSchemaDataType( return ItemType.FLOAT; // could check P, MINEXP, MAXEXP here. case Types.FLOAT: assert false; // PG should always report either REAL or DOUBLE + /*FALLTHROUGH*/ case Types.DOUBLE: return ItemType.DOUBLE; @@ -2442,6 +2451,7 @@ static class BindingsFromResultSet extends Binding.Assemblage * @throws SQLException if numbers of columns and expressions don't * match, or there is an ordinality column and its type is not suitable. */ + @SuppressWarnings("fallthrough") BindingsFromResultSet(ResultSet rs, XQueryEvaluator[] exprs) throws SQLException { @@ -2474,6 +2484,7 @@ static class BindingsFromResultSet extends Binding.Assemblage int scale = p.scale(); if ( 0 == scale || -1 == scale ) break; + /*FALLTHROUGH*/ default: throw new SQLSyntaxErrorException( "Column FOR ORDINALITY must have an exact numeric" + From f226e32bee96b48af6651c5011af768504b76750 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 17 Oct 2020 17:42:21 -0400 Subject: [PATCH 0751/1087] Add javadoc in some old classes --- .../pljava/internal/PgSavepoint.java | 9 +++++-- .../pljava/internal/ResultSetPicker.java | 15 +++++++++++ .../pljava/internal/SPIException.java | 15 ++++++++--- .../pljava/internal/ServerException.java | 15 ++++++++--- .../pljava/internal/SyntheticXMLReader.java | 3 +++ .../pljava/internal/VarlenaXMLRenderer.java | 6 ++++- .../org/postgresql/pljava/jdbc/BlobValue.java | 16 ++++++++---- .../org/postgresql/pljava/jdbc/ClobValue.java | 16 ++++++++---- .../pljava/jdbc/PgNodeTreeAsXML.java | 8 +++++- .../pljava/jdbc/ResultSetField.java | 2 +- .../pljava/jdbc/SPIDatabaseMetaData.java | 7 ++--- .../org/postgresql/pljava/jdbc/SPIDriver.java | 26 ++++++++++++------- .../pljava/jdbc/SPIParameterMetaData.java | 4 +-- .../pljava/jdbc/SPIPreparedStatement.java | 2 +- .../postgresql/pljava/jdbc/SPIStatement.java | 2 +- .../pljava/jdbc/SQLOutputToTuple.java | 3 ++- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 3 +++ .../pljava/jdbc/StatementClosedException.java | 15 ++++++++--- .../jdbc/UnsupportedFeatureException.java | 20 +++++++++----- .../org/postgresql/pljava/sqlj/Loader.java | 2 ++ 20 files changed, 138 insertions(+), 51 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java index 74399271..cbe5e8e6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -16,15 +16,20 @@ import java.sql.Connection; import java.sql.SQLException; +import java.sql.Savepoint; import java.sql.SQLNonTransientException; import java.util.Iterator; import java.util.WeakHashMap; import java.util.logging.Logger; /** + * Implementation of {@link Savepoint} for the SPI connection. + *

    + * It is an historical oddity that this is in the {@code .internal} package + * rather than {@code .jdbc}. * @author Thomas Hallgren */ -public class PgSavepoint implements java.sql.Savepoint +public class PgSavepoint implements Savepoint { /* * Instances that might be live are tracked here in a WeakHashMap. The diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java b/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java index 36871d7a..6752e93d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java @@ -13,6 +13,21 @@ import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.jdbc.SingleRowWriter; +/** + * An adapter class used internally when a set-returning user function returns + * a {@code ResultSetHandle}, presenting it as a {@link ResultSetProvider} + * instead. + *

    + * Note on the current implementation: + * this class operates by fetching every field of every row of the result set + * as a Java object via the one-argument {@code getObject}, then storing it into + * the writable result set supplied by PL/Java. Apart from being rather + * inefficient, this can involve conversions through legacy types (such as + * {@code java.sql.Timestamp} when the JSR 310 {@code java.time} conversions are + * better specified). In cases where that isn't acceptable, the user function + * should be declared to return {@code ResultSetProvider} and do this work + * itself. + */ public class ResultSetPicker implements ResultSetProvider { private final ResultSetHandle m_resultSetHandle; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPIException.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPIException.java index 2c6072c5..3a0d5220 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPIException.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPIException.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Thomas Hallgren + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -10,6 +16,7 @@ /** + * A Java exception constructed from a PostgreSQL SPI result code. * @author Thomas Hallgren */ public class SPIException extends SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java b/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java index 24f703bc..4f9cc3f0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java @@ -1,14 +1,21 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Thomas Hallgren + * Chapman Flack */ package org.postgresql.pljava.internal; import java.sql.SQLException; /** + * A Java exception constructed over a PostgreSQL error report. * @author Thomas Hallgren */ public class ServerException extends SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SyntheticXMLReader.java b/pljava/src/main/java/org/postgresql/pljava/internal/SyntheticXMLReader.java index 93242459..ca590c63 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SyntheticXMLReader.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SyntheticXMLReader.java @@ -782,6 +782,9 @@ public void toSAX() } } + /** + * An immutable and empty collection of attributes. + */ public static class EmptyAttributes2 extends Attributes2Impl { @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java index 9db0bb1c..11bc2754 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2020 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 @@ -20,6 +20,10 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +/** + * Class adapting a {@code ByteBufferXMLReader} to a + * {@code VarlenaWrapper.Input}. + */ public abstract class VarlenaXMLRenderer extends ByteBufferXMLReader implements VarlenaWrapper { diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java index 9c4c0657..26e46238 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -19,6 +24,7 @@ import java.sql.SQLFeatureNotSupportedException; /** + * Implementation of {@link Blob} for the SPI connection. * @author Thomas Hallgren */ public class BlobValue extends InputStream implements Blob diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java index eda4bd08..9e594967 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Copyright (c) 2010, 2011 PostgreSQL Global Development Group + * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. * - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://wiki.tada.se/index.php?title=PLJava_License + * 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: + * Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ package org.postgresql.pljava.jdbc; @@ -21,6 +26,7 @@ import java.sql.SQLFeatureNotSupportedException; /** + * Implementation of {@link Clob} for the SPI connection. * @author Thomas Hallgren */ public class ClobValue extends Reader implements Clob diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java index ce129bad..8d5de171 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below. * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * @@ -31,6 +31,12 @@ import org.postgresql.pljava.internal.VarlenaWrapper; import org.postgresql.pljava.internal.VarlenaXMLRenderer; +/** + * An adapter presenting PostgreSQL's {@code pg_node_tree} type (a serialized + * representation of a tree data structure) through the XML API (in, currently, + * an ad-hoc, schemaless rendering, but one with which some practical use might + * be made of the information, after a little study). + */ public class PgNodeTreeAsXML extends VarlenaXMLRenderer { PgNodeTreeAsXML(VarlenaWrapper.Input vwi) throws SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetField.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetField.java index 0d52b8d3..fd8d69b7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetField.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ResultSetField.java @@ -17,7 +17,7 @@ import org.postgresql.pljava.internal.Oid; /** - * + * Representation of a field to be presented in a {@link SyntheticResultSet}. * @author Filip Hrbek */ diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java index 10bed99f..1b5de43d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDatabaseMetaData.java @@ -14,9 +14,6 @@ package org.postgresql.pljava.jdbc; -/** - * @author Filip Hrbek - */ import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; @@ -33,6 +30,10 @@ import org.postgresql.pljava.internal.Backend; import org.postgresql.pljava.internal.Oid; +/** + * Implementation of {@link DatabaseMetaData} for the SPI connection. + * @author Filip Hrbek + */ public class SPIDatabaseMetaData implements DatabaseMetaData { public SPIDatabaseMetaData(SPIConnection conn) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDriver.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDriver.java index 1246cc95..a8eb168e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDriver.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIDriver.java @@ -1,8 +1,13 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Thomas Hallgren */ package org.postgresql.pljava.jdbc; @@ -16,12 +21,13 @@ import java.util.logging.Logger; /** - * + * Implementation of {@link Driver} for the SPI connection. * @author Thomas Hallgren */ public class SPIDriver implements Driver { - private static final Logger s_logger = Logger.getLogger( "org.postgresql.pljava.jdbc" ); + private static final Logger s_logger = Logger.getLogger( + "org.postgresql.pljava.jdbc" ); private static final String s_defaultURL = "jdbc:default:connection"; private static final int s_defaultURLLen = s_defaultURL.length(); @@ -88,8 +94,8 @@ static Connection getDefault() return s_defaultConn; } - public Logger getParentLogger() throws SQLFeatureNotSupportedException - { - return s_logger; - } + public Logger getParentLogger() throws SQLFeatureNotSupportedException + { + return s_logger; + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java index 08ed90a8..5fe27784 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIParameterMetaData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -18,7 +18,7 @@ import java.sql.SQLFeatureNotSupportedException; /** - * + * Implementation of {@link ParameterMetaData} for the SPI connection. * @author Thomas Hallgren */ public class SPIParameterMetaData implements ParameterMetaData diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java index 1a51597f..02ccd407 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -43,7 +43,7 @@ import org.postgresql.pljava.internal.Oid; /** - * + * Implementation of {@link PreparedStatement} for the SPI connection. * @author Thomas Hallgren */ public class SPIPreparedStatement extends SPIStatement implements PreparedStatement diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java index 6a1cb46e..6197c81f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIStatement.java @@ -27,7 +27,7 @@ import org.postgresql.pljava.internal.SPIException; /** - * + * Implementation of {@link Statement} for the SPI connection. * @author Thomas Hallgren */ public class SPIStatement implements Statement, SPIReadOnlyControl diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java index e2234634..ec2365bb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * * All rights reserved. This program and the accompanying materials @@ -41,6 +41,7 @@ import org.postgresql.pljava.internal.TupleDesc; /** + * Implementation of {@link SQLOutput} for the case of a composite data type. * @author Thomas Hallgren */ public class SQLOutputToTuple implements SQLOutput diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 1b2a5e54..865b8c83 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -176,6 +176,9 @@ import org.postgresql.pljava.internal.VarlenaXMLRenderer; import static org.postgresql.pljava.jdbc.TypeOid.PGNODETREEOID; +/** + * Implementation of {@link SQLXML} for the SPI connection. + */ public abstract class SQLXMLImpl implements SQLXML { private static final VarHandle s_backingVH; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/StatementClosedException.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/StatementClosedException.java index 3f532de6..300c78ba 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/StatementClosedException.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/StatementClosedException.java @@ -1,14 +1,21 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Thomas Hallgren */ package org.postgresql.pljava.jdbc; import java.sql.SQLException; /** + * An {@code SQLException} specific to the case of attempted use of a + * {@code Statement} that has been closed. * @author Thomas Hallgren */ public class StatementClosedException extends SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/UnsupportedFeatureException.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/UnsupportedFeatureException.java index dff8d6c0..6b4e822c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/UnsupportedFeatureException.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/UnsupportedFeatureException.java @@ -1,17 +1,25 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Thomas Hallgren + * Chapman Flack */ package org.postgresql.pljava.jdbc; -import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; /** + * An {@code SQLException} specific to the case of attempted use of + * an unsupported feature. * @author Thomas Hallgren */ -public class UnsupportedFeatureException extends SQLException +public class UnsupportedFeatureException extends SQLFeatureNotSupportedException { private static final long serialVersionUID = 7956037664745636982L; diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 8acd36a6..859eecb2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -73,6 +73,8 @@ import org.postgresql.pljava.jdbc.SPIReadOnlyControl; /** + * Class loader to load from jars installed in the database with + * {@code SQLJ.INSTALL_JAR}. * @author Thomas Hallgren */ public class Loader extends ClassLoader From 63d8a5e467a9c0f626c48e9ee134a58ac308fd8e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 17 Oct 2020 18:43:23 -0400 Subject: [PATCH 0752/1087] Begin drafting 1.6 release notes and new docs Remove hyperlink from org.postgresql.pljava.annotation package overview to the DDR generator package, as that isn't really considered API (though provided in pljava-api's module so it can operate at compile time), so isn't exported from the module. --- .../postgresql/pljava/sqlgen/Lexicals.java | 50 +++ .../example/annotation/package-info.java | 6 +- pljava-packaging/src/main/java/Node.java | 4 + src/site/markdown/build/versions.md | 14 +- src/site/markdown/develop/develop.md | 1 + src/site/markdown/develop/node.md | 391 ++++++++++++++++++ src/site/markdown/releasenotes.md.vm | 133 +++++- 7 files changed, 575 insertions(+), 24 deletions(-) create mode 100644 src/site/markdown/develop/node.md diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 6a124e77..20f175c5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -45,6 +45,8 @@ */ public abstract class Lexicals { + private Lexicals() { } // do not instantiate + /** Allowed as the first character of a regular identifier by ISO. */ public static final Pattern ISO_REGULAR_IDENTIFIER_START = Pattern.compile( @@ -438,6 +440,8 @@ public static Identifier.Simple identifierFrom(Matcher m) */ public static abstract class Identifier implements Serializable { + Identifier() { } // not API + /** * This Identifier represented as it would be in SQL source. *

    @@ -449,6 +453,9 @@ public static abstract class Identifier implements Serializable */ public abstract String deparse(Charset cs); + /** + * Indicates whether some other object is "equal to" this one. + */ @Override public boolean equals(Object other) { @@ -498,9 +505,16 @@ private void readObject(ObjectInputStream in) + c.getName()); } + /** + * Class representing a non-schema-qualified identifier, either the + * {@link Simple Simple} form used for naming most things, or the + * {@link Operator Operator} form specific to PostgreSQL operators. + */ public static abstract class Unqualified> extends Identifier { + Unqualified() { } // not API + /** * Produce the deparsed form of a qualified identifier with the * given qualifier and this as the local part. @@ -514,6 +528,11 @@ public static abstract class Unqualified> public abstract Qualified withQualifier(Simple qualifier); } + /** + * Class representing an unqualified identifier in the form of a name + * (whether a case-insensitive "regular identifier" without quotes, + * or a delimited form). + */ public static class Simple extends Unqualified { protected final String m_nonFolded; @@ -979,8 +998,22 @@ public boolean equals(Object other, Messager msgr) */ public static class Pseudo extends Simple { + /** + * Instance intended to represent {@code PUBLIC} when used as a + * privilege grantee. + *

    + * It would not be correct to use this instance for other special + * things that happen to be named {@code PUBLIC}, such as the + * {@code PUBLIC} schema. That is a real catalog object that has + * the actual name {@code PUBLIC}, and should be represented as a + * {@code Simple} with that name. + */ public static final Pseudo PUBLIC = new Pseudo("PUBLIC"); + /** + * A {@code Pseudo} identifier instance is only equal + * to itself. + */ @Override public boolean equals(Object other) { @@ -1084,6 +1117,9 @@ public Qualified withQualifier(Simple qualifier) return new Qualified<>(qualifier, this); } + /** + * Returns a hash code value for the object. + */ @Override public int hashCode() { @@ -1412,6 +1448,13 @@ public String deparse(Charset cs) return m_local.deparse(m_qualifier, cs); } + /** + * Combines the hash codes of the qualifier and local part. + *

    + * Equal to the local part's hash if the qualifier is null, though a + * {@code Qualified} with null qualifier is still not considered + * "equal" to an {@code Unqualified} with the same name. + */ @Override public int hashCode() { @@ -1432,11 +1475,18 @@ public boolean equals(Object other, Messager msgr) && m_local.equals(oi.m_local, msgr); } + /** + * Returns the qualifier, possibly null, as a {@code Simple}. + */ public Simple qualifier() { return m_qualifier; } + /** + * Returns the local part, a {@code Simple} or an {@code Operator}, + * as the case may be. + */ public T local() { return m_local; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/package-info.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/package-info.java index f8c64f3e..20ea8b40 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/package-info.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/package-info.java @@ -1,8 +1,6 @@ /** - * The first examples that were converted to test the annotation-driven SQL generator instead of using hand-written SQL - * deployment code. + * The first examples that were converted to test the annotation-driven + * SQL generator instead of using hand-written SQL deployment code. * @author Thomas Hallgren * @author Chapman Flack */ diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 2c65be89..f1cb0117 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -121,6 +121,10 @@ * from {@code jshell} if its classpath includes the installer jar (and * pgjdbc-ng). *

    + * An + * introduction with examples + * is available. + *

    * Unlike the many capabilities of {@code PostgresNode.pm}, this only deals in * TCP sockets bound to {@code localhost} (Java doesn't have Unix sockets out of * the box yet) and only a few of the most basic operations. diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index d4e7fd02..52ab63f6 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -62,17 +62,13 @@ versions 4.3.0 or later are recommended in order to avoid a ## PostgreSQL -PL/Java does not currently support PostgreSQL releases before 8.2. -Recent work is known to have introduced dependencies on 8.2 features. - -The current aim is to avoid deliberately breaking compatibility back -to 8.2. (A former commercial fork of PostgreSQL 8.2 recently returned -to the open-source fold with a *really* old version of PL/Java, so -the aim is that the current PL/Java should be a possible upgrade there.) +PL/Java 1.6.0 does not commit to support PostgreSQL earlier than 9.5. +(Support for 9.4 or even 9.3 might be feasible to add if there is a pressing +need.) More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. -PL/Java 1.5.1 has been successfully built and run on at least one platform -with PostgreSQL versions from 11 to 8.2, the latest maintenance +PL/Java 1.6.0 has been successfully built and run on at least one platform +with PostgreSQL versions from 13 to 9.5, the latest maintenance release for each. diff --git a/src/site/markdown/develop/develop.md b/src/site/markdown/develop/develop.md index d092cefb..e09d8f59 100644 --- a/src/site/markdown/develop/develop.md +++ b/src/site/markdown/develop/develop.md @@ -5,4 +5,5 @@ PL/Java's implementation. They will be most often of interest to those developing PL/Java itself, but may also be useful when a deeper understanding of some PL/Java behavior is needed. +* [The testing harness `Node.class` in PL/Java's self-installer jar](node.html) * [Passing of data types between PostgreSQL and Java](coercion.html) diff --git a/src/site/markdown/develop/node.md b/src/site/markdown/develop/node.md new file mode 100644 index 00000000..abd7f185 --- /dev/null +++ b/src/site/markdown/develop/node.md @@ -0,0 +1,391 @@ +# The testing harness `Node.class` in PL/Java's self-installer jar + +The end product of a PL/Java build is a jar file containing the actual +files (including other jars) that need to be installed on a target system, +plus some logic allowing it to be run with `java -jar` and extract itself, +consulting the target system's `pg_config` to learn where to put the files. +That is unchanged from PL/Java 1.5. + +In 1.6, however, the class added in the jar to support the self-extraction +has a number of new methods useful for integration testing. + +The new methods are unused in a simple extraction with `java -jar`, but are +available, for example, to Java's [jshell][] scriptable interpreter. +Starting `jshell` with PL/Java's installer jar on its class path creates +a rather versatile environment for scripting tests of PL/Java in one or more +temporary database instances. + +This is currently done in the multi-platform CI test configurations in the +project's repository, as a way to keep as much as possible of the testing code +common across platforms. + +The overall flavor, and even some of the method names, follow the `PostgresNode` +Perl module that has been part of PostgreSQL's "PGXS" extension-building tools +since 2015, so a quick review of that follows. + +## Similarities to PostgreSQL's `PostgresNode` Perl module + +When used from a testing script written in Perl, the methods of `PostgresNode` +make it easy to spin up and tear down one or more PostgreSQL instances, running +in temporary directories, listening on temporary ports, non-interfering with +each other or with production instances using the standard locations and ports, +and without needing the permissions that guard those 'real' locations and ports. +A Perl test script might be simply: + +```perl +my $n1 = get_new_node("TestNode1"); +$n1->init(); # run initdb in n1's temporary location +$n1->start(); # start a server listening on n1's temporary port +$n1->safe_psql("postgres", "select 42"); +$n1->stop(); # stop the server +$n1->clean_node(); # recursively delete the temporary location +``` + +`PostgresNode.pm` illustrates the immense utility of making just a few +well-chosen methods available, when there is already an expressive scripting +language at hand (Perl) for putting those methods to use. + +Early Java versions lacked any batteries-included support for scripting, but the +arrival of `jshell` with Java 9 changed that. Start up `jshell` with PL/Java's +installer jar on its classpath, and you have an interactive, scriptable version +of Java, with the methods of `Node.class` available in it. + +The ones that correspond to the Perl example above have the same names, for +consistency (right down to the Perlish spelling with underscores rather than +Javaish camelCase): + +```java +import org.postgresql.pljava.packaging.Node +Node n1 = Node.get_new_node("TestNode1") +n1.init() +n1.start() +/* ... */ +n1.stop() +n1.clean_node() +``` + +`jshell` has to be run with a rather lengthy command line to get to this point; +more on that later. But once started, it presents a familiar `PostgresNode`-like +environment. As the example shows, `jshell` is lenient about statement-ending +semicolons. + +## `Node.class` in detail + +A `Node` will register a VM shutdown hook to make sure `stop` and `clean_node` +happen if you forget and exit `jshell`, though forgetting is not recommended. +For using `jshell` interactively, these methods are convenient. If writing a +script, the equivalent `try`-with-resources forms may be tidier: + +```java +try (AutoCloseable t1 = n1.initialized_cluster()) +{ + try (AutoCloseable t2 = n1.started_server()) + { + /* ... */ + } +} +``` + +The server will be stopped when `t2` goes out of scope, and the file tree +created by `initdb` will be removed when `t1` goes out of scope. + +The `try` form is less convenient for interactive use, because `jshell` is not +very interactive when gathering a compound statement like a `try`. None of your +actions actually happen until you supply the final closing brace, and then they +all happen at once and the instance is torn down. But for any sort of finished +test script, the `try` form will be natural. + +The full set of `Node` methods available can be seen +[in its javadocs][nodeapi]. + +### Connecting to the server + +Running `initdb` and starting a server are all well and good, but sooner or +later a test may need to connect to it. That requires a JDBC driver to be on the +classpath also: specifically `pgjdbc-ng` (at least, that is the one that's been +tested and whose URL syntax is built in to `Node`). The older `pgjdbc` punts on +the correct handling of warning/notice responses from the server, which seems +rather disqualifying for a testing environment. In `pgjdbc-ng`, notices and +warnings (any PostgreSQL severity less than `ERROR` and at or above the +`client_min_messages` setting) are chained together as `SQLWarning` instances, +as JDBC provides. + +A new profile has been added to PL/Java's Maven build, and can be activated with +`-Ppgjdbc-ng` on the `mvn` command line. It has no effect but to declare an +extra dependency on the `pgjdbc-ng` dependencies-included jar. It is not used in +the build, but Maven will have downloaded it to the local repository, and that +location can be added to `jshell`'s classpath to make the driver available. + +That addition leads to the final long unwieldy command line needed to start +`jshell`, which can be seen in all its glory toward the end of this page. +Once that is copied and pasted into a terminal and any local paths changed, the +rest is easy: + +```java +import org.postgresql.pljava.packaging.Node +Node n1 = Node.get_new_node("TestNode1") +n1.init() +n1.start() +import java.sql.Connection +Connection c1 = n1.connect() +``` + +Once you have an open connection (or several), the convenience methods `Node` +provides for using them are `static`. A connection is already to a specific +`Node`, so there is no need for the convenience methods to be invoked on a +`Node` instance. They are `static`, and simply take a `Connection` as the first +parameter. + +```java +import static org.postgresql.pljava.packaging.Node.qp; // query-print +qp(c1, "create table foo (bar int, baz text)") +qp(c1, "insert into foo (values (1, 'Howdy!'))") +qp(c1, "select 1/0") +qp(c1, "select pg_sleep(1.5)") +qp(c1, "select * from foo") +``` + +This example shows `qp` used several different ways: with a DDL statement that +returns no result, a DML statement that returns an update count, a statement +that returns an error, one that calls a `void`-returning function (and therefore +produces a one-row result with one column typed `void` and always null), and one +that returns a general query result. What it prints: + +``` +jshell> qp(c1, "create table foo (bar int, baz text)") + +jshell> qp(c1, "insert into foo (values (1, 'Howdy!'))") + + +jshell> qp(c1, "select 1/0") + + +jshell> qp(c1, "select pg_sleep(1.5)") + + +jshell> qp(c1, "select * from foo") + ... + text + + + + + 1 + Howdy! + + + + +jshell> +``` + +The XMLish output style comes from using Java's built-in `WebRowSet.writeXml` +method for dumping general result sets. It is more verbose than one would like, +and easily flummoxed by unusual or PG-specific column types, but it is as useful +a way to readably dump a typical result set as one could hope to write in four +lines of Java. (This is meant as a _small_ class useful for testing, not as a +reimplementation of `psql`!) + +The writing of update counts and diagnostics as +`success`/`error`/`warning`/`info` XML elements naturally follows to keep +the output format consistent. The `void` output is special treatment for +the common case of a result set with only the `void` column type, to spare the +effort of generating a whole `WebRowSet` XML that only shows nothing is there. + +#### `qp` dissected + +`qp` is for interactive, exploratory use, generating printed output. For +scripting purposes, `q` gives direct access to result objects; `qp` is nothing +but a wrapper that calls `q` with the same arguments, and what `q` returns is +passed directly to another method (in fact, another of several overloads of +`qp`) to be printed. + +What `q` returns is a `Stream`. Although declared with element type +`Object`, the stream will only deliver instances of: `ResultSet`, `Long` (an +update count), or throwables (caught exceptions, or `SQLWarning` instances). The +JDBC `Statement` is polled for new `SQLWarning`s before checking for each next +result (`ResultSet` or update count). An error or exception that is thrown and +caught will be placed on the stream when caught (and will be the last thing on +the stream). + +All 'notices' from PostgreSQL (severity below `ERROR` but at or above +`client_min_messages`) are turned into `SQLWarning` instances by `pgjdbc-ng`, +which does not provide any API to get the original PostgreSQL severity, or any +of the details other than the message and SQLState code. `Node` classifies them +as `info` if the SQLState 'class' (leftmost two positions) is `00`, otherwise as +`warning`. Exceptions of any other kind are classified as `error`. + +As it happens, there is also an overload of `qp` with just one `Stream` +parameter. If you have already run a query with `q` and have the result stream, +and decide you just want to print that, just pass it to `qp`. There are other +overloads of `qp` for the individual objects you might encounter in a result +stream. One static import of the name `qp` will allow printing many things. + +#### More specialized convenience methods + +`Node` also supplies several more specialized methods: `setConfig` for +PostgreSQL configuration variables (`qp` is fine for a literal string "set foo +to bar" command, but for computed values, `setConfig` uses a prepared statement +and binding), and wrappers for PL/Java `install_jar`, `remove_jar`, and +`set_classpath`. + +The `installExamplesJar` method supplies the correct as-installed path to the +jar file (which we know, because this _is_ the self-installer code, remember?). +The boolean method `examplesNeedSaxon` introspects in the examples jar to see if +it includes the Saxon examples, and therefore needs the Saxon jar in place +before it can be deployed. + +As the Saxon jar is probably already in a local Maven repository, `installSaxon` +will install it from there, given a path to the repository root and the desired +version of Saxon-HE. Not to be outdone, `installSaxonAndExamplesAndPath` +combines the steps in correct order to install the Saxon jar, place it on the +classpath, install and deploy the examples jar, and set a final classpath that +installs both. + +```java +import static java.nio.file.Paths.get +import java.sql.Connection +import org.postgresql.pljava.packaging.Node +import static org.postgresql.pljava.packaging.Node.qp + +Node n1 = Node.get_new_node("TestNode1") + +try ( + AutoCloseable t1 = n1.initialized_cluster(); + AutoCloseable t2 = n1.started_server(Map.of( + "client_min_messages", "info", + "pljava.vmoptions", + "-Xcheck:jni -enableassertions:org.postgresql.pljava..." + )); +) +{ + try ( Connection c = n1.connect() ) + { + qp(c, "create extension pljava"); + } + + /* + * 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() ) + { + qp(Node.installSaxonAndExamplesAndPath(c, + get(System.getProperty("user.home"), ".m2", "repository").toString(), + "10.2", + true)) + } +} +/exit +``` + +The above example puts together most of the ideas covered here. While it does +demonstrate installing and deploying the examples jar (which runs all of the +tests contained in its deployment code), this example merely prints the output, +rather than examining it programmatically to evaluate success, and it does not +use its exit status to communicate success or failure to its invoker, as one +would expect of a test. + +Worked-out examples that do the rest of that can be seen in the project +repository in the configuration files for the CI testing services. + +One last `Node` method most useful for checking returned results +programmatically is `stateMachine` (full description in +[the javadocs][nodeapi]). For example, the `installSaxonAndExamplesAndPath` call +above returns a concatenation of four object streams: one from +`sqlj.install_jar` loading the Saxon jar, one from `sqlj.set_classpath` adding +it to the path, one from `sqlj.install_jar` loading the PL/Java examples jar +(which runs the tests in its deployment descriptor), and finally the +`sqlj.set_classpath` placing both jars on the path. + +Each of those streams should end with a `void` result set, preceded by zero +or more `info` or `warning` (any `error` should be counted as test failure). +In the third stream, from installing the examples jar, any `warning` should +also be counted as a test failure: the tests in the deployment descriptor +report failures that way to avoid aborting the query, so more results can be +reported. + +Using `stateMachine`, that can be expressed as a small set of states and +transitions that match that expected sequence. A state returns a positive +number _n_ to consume the input object it is looking at and transition to +state _n_ for the next item of input, or a negative number _-n_ to go to +state _n_ still with the same input item. The final accepting state returns +`true`; `false` from any state reports a mismatch. A `null` is supplied by +`stateMachine` after the last item on the input `Stream` (which therefore +must not contain nulls): + +```java +succeeding &= stateMachine( + "descriptive string for this state machine", + null, + + Node.installSaxonAndExamplesAndPath(c, + System.getProperty("mavenRepo"), + System.getProperty("saxonVer"), + true) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), // so they also appear in the log + + // 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 +); + +``` + +The `isDiagnostic` method shown above isn't part of the `Node` class; in the +actual test configurations in the repository, it is trivially defined in +`jshell` a few lines earlier. Not everything needs to be built in. + +## Invoking `jshell` to use `Node.class` + +As hinted above, the command needed to get `jshell` started so all the foregoing +goodness can happen is a bit unwieldy with options. It looks like this: + +```sh +jshell \ + --execution local \ + "-J--class-path=$packageJar:$jdbcJar" \ + "--class-path=$packageJar" \ + "-J--add-modules=java.sql.rowset" \ + "-J-Dpgconfig=$pgConfig" \ + "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" +``` + +where _$packageJar_ is a PL/Java self-installer jar, _$jdbcJar_ should point +to a `pgjdbc-ng` "fat jar" (`pgjdbc-ng-all`), and _$pgConfig_ should point to +the `pg_config` executable for the PostgreSQL installation that should be used. +(If there is only one PostgreSQL installation or the right `pg_config` will be +found on the search path, it doesn't have to be specified.) + +The `-J--add-modules` is needed because even though `jshell` treats +`java.sql.rowset` as available by default, the local JVM it is running on +(because of `--execution local`) wouldn't know that without being told. + +The path given to `jshell` itself (`--class-path` without the `-J`) does not +need to mention the `pgjdbc-ng` jar, because it can be a provider of the +`java.sql.Driver` service without having to be visible. If the script will +want to use `pgjdbc-ng`-specific classes, then the jar does have to be +on `jshell`'s class path too. + +The `noUnsafe` setting silences a complaint from the `netty` library +about Java (correctly!) denying it access to private internals. + +[jshell]: https://docs.oracle.com/en/java/javase/15/jshell/introduction-jshell.html +[nodeapi]: ../pljava-packaging/apidocs/org/postgresql/pljava/packaging/Node.html#method.summary diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index b6c205a6..24e08d3b 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,6 +10,128 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') +$h2 PL/Java 1.6.0 + +This is the first release of a significantly refactored PL/Java 1.6 branch +with a number of new features and changes. It requires Java 9 or later at +build and run time, but retains the ability to run PL/Java application code +built for earlier versions. It should be used with PostgreSQL 9.5 or later. +For applications _requiring_ an older Java or PostgreSQL version, the latest +release in the PL/Java 1.5 line remains an option. + +**Note to package maintainers**: these release notes should be reviewed before +an installation moves to 1.6.0 from a 1.5 or earlier version, so it is best +packaged in a way that requires an affirmative choice to upgrade. + +$h3 Version compatibility + +PL/Java 1.6.0 can be built against recent PostgreSQL versions including 13, and +older ones back to 9.5, using Java SE 9 or later. The Java version used at +runtime does not have to be the same version used for building. PL/Java itself +can run on any Java version 9 or later if built with Java 9 or with 12 or later +(bugs in the Java 10 and 11 compilers prevent running on 9 if built with 10, +or on 9 or 10 if built with 11). PL/Java functions can be +written for, and use features of, whatever Java version will be loaded at run +time. See [version compatibility][versions] for more detail. + +When used with GraalVM as the runtime VM, PL/Java functions can use Graal's +"polyglot" capabilities to execute code in any other language available on +GraalVM. In this release, it is not yet possible to directly declare a function +in a language other than Java. + +$h3 Changes + +$h4 Validation at `CREATE FUNCTION` time + +PL/Java can now detect problems with a function declaration, including missing +dependencies, at the time of `CREATE FUNCTION`, rather than allowing the +function to be created and reporting failure later when it is called. + +This change may have an impact on some established procedures. For example, +when installing a jar that contains deployment commands, deployment may +fail if another required jar has not been installed and added to the class +path first; in the past, the order did not matter. For details, see +[this section][linkage] in the documentation for the supplied examples, +and the description of `check_function_bodies` in the +[configuration variable reference](use/variables.html). + +$h4 Improvements to the annotation-driven SQL generator + +$h5 Infers additional implicit ordering dependencies + +The SQL generator can now respect the implicit ordering constraints among +user-defined types and functions that either use the types or are used in their +definitions, which can eliminate many `provides`/`requires` annotation elements +that had to be added by hand for PL/Java 1.5. The reduction in boilerplate +needed for a realistic example can be seen by comparing the annotated version +of Bear Giles's `pljava-udt-type-extension` example at +[this commit][udtd32f84e] (pre-1.6) and [this one][udt0066a1e] (1.6.0). + +$h5 Generates variadic function declarations + +PL/Java 1.6 can declare functions that can be called from SQL with varying +numbers of arguments. [Example code][variadic] is provided. + +$h4 Build system and continuous integration + +* The `nar-maven-plugin` formerly used in the build has been replaced with + a newly-developed Maven plugin tailored to PostgreSQL extension building. + +* The new plugin respects the flags reported by `pg_config` when building + the native library. + +* Building with the same flags used for PostgreSQL has eliminated the flood + of uninformative warnings that, in prior versions, made troubleshooting + actual build problems difficult. + +* Travis-CI and AppVeyor now regularly build and test PL/Java for + Linux (x86_64 and ppc64le), Mac OS, and Windows (using MSVC + and MinGW-w64), with results visible at GitHub. + +* PL/Java's self-installer jar now includes utilities to simplify + integration testing, similar to the `PostgresNode` Perl module provided + with PostgreSQL. It is used in the Travis and AppVeyor builds to keep + platform-specific code to a minimum, and may be useful for other purposes. + Some [documentation](develop/node.html) is included. + +$h3 Enhancement requests addressed + +* [Add regression testing](${ghbug}11) +* [`CFLAGS` from `pg_config` when building `pljava-so`](${ghbug}152) + +$h3 Bugs fixed + +* [`-Dpljava.libjvmlocation` breaks Windows build](${ghbug}190) +* [XML Schema regression-test failure in de_DE locale](${ghbug}312) + +$h3 Credits + +There is a PL/Java 1.6.0 thanks in part to +Christoph Berg, +Chapman Flack, +Kartik Ohri, +original creator Thomas Hallgren, +and the many contributors to earlier versions. + +The work of Kartik Ohri in summer 2020 on the build system renovation and +continuous integration was supported by Google Summer of Code. + +[linkage]: examples/examples.html#Exception_resolving_class_or_method_.28message_when_installing_examples.29 +[udtd32f84e]: https://github.com/jcflack/pljava-udt-type-extension/commit/d32f84e +[udt0066a1e]: https://github.com/jcflack/pljava-udt-type-extension/commit/0066a1e +[variadic]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/Variadic.html#method.detail + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + $h2 PL/Java 1.5.6 This release adds support for PostgreSQL 13. @@ -128,17 +250,6 @@ The work of Kartik Ohri in summer 2020 was supported by Google Summer of Code. [xqtdf]: https://www.w3.org/TR/xquery-31/#id-typed-data-feature [xqstf]: https://www.w3.org/TR/xquery-31/#id-static-typing-feature -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 PL/Java 1.5.5 (4 November 2019) This bug-fix release fixes runtime issues reported in 32-bit `i386` builds, some From fdf7d62ede1791091fa791dcc4cc1747d0b880ae Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 10 Feb 2020 02:05:43 -0500 Subject: [PATCH 0753/1087] Small tweak: defineClass with ProtectionDomain Define a new URL protocol handler "sqlj:" to represent loaded jars by jarname. The protocol handler must exist for such URLs to be successfully constructed. It might not need to do much more than that; in particular, its openConnection method just throws an exception. --- pljava/src/main/java/module-info.java | 3 + .../org/postgresql/pljava/sqlj/Handler.java | 73 +++++++++++++++++++ .../org/postgresql/pljava/sqlj/Loader.java | 54 ++++++++++++-- 3 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 29c0942e..18385c21 100644 --- a/pljava/src/main/java/module-info.java +++ b/pljava/src/main/java/module-info.java @@ -23,6 +23,9 @@ exports org.postgresql.pljava.elog to java.logging; + provides java.net.spi.URLStreamHandlerProvider + with org.postgresql.pljava.sqlj.Handler; + provides java.sql.Driver with org.postgresql.pljava.jdbc.SPIDriver; provides org.postgresql.pljava.Session diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java new file mode 100644 index 00000000..a0bd626b --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 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.sqlj; + +import java.io.IOException; + +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.spi.URLStreamHandlerProvider; + +/** + * Provider for an {@code sqlj:jarname} URL stream handler. + *

    + * This is only used to allow the security policy to grant permissions to jars + * by name. The handler is otherwise nonfunctional; its {@code openConnection} + * method throws an exception. + */ +public class Handler extends URLStreamHandlerProvider +{ + private static final Handler INSTANCE = new Handler(); + + public URLStreamHandlerProvider provider() + { + return INSTANCE; + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) + { + switch ( protocol ) + { + case "sqlj": + return SQLJ.INSTANCE; + default: + return null; + } + } + + static class SQLJ extends URLStreamHandler + { + static final SQLJ INSTANCE = new SQLJ(); + + @Override + protected URLConnection openConnection(URL u) throws IOException + { + throw new IOException( + "URL of sqlj: protocol can't really be opened"); + } + + @Override + protected void parseURL(URL u, String spec, int start, int limit) + { + if ( spec.length() > limit ) + throw new IllegalArgumentException( + "sqlj: URL should not contain #"); + if ( spec.length() == start ) + throw new IllegalArgumentException( + "sqlj: URL should not have empty path part"); + setURL(u, u.getProtocol(), null, -1, null, null, + spec.substring(start), null, null); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 859eecb2..7910a9ef 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -22,6 +22,11 @@ import java.net.MalformedURLException; import java.net.URL; +import java.security.CodeSigner; +import java.security.CodeSource; +import java.security.Principal; +import java.security.ProtectionDomain; + import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -38,6 +43,8 @@ import java.util.logging.Level; import java.util.logging.Logger; +import static java.util.stream.Collectors.groupingBy; + import org.postgresql.pljava.internal.Backend; import org.postgresql.pljava.internal.Checked; import org.postgresql.pljava.internal.Oid; @@ -175,13 +182,24 @@ public static ClassLoader getSchemaLoader(String schemaName) if(loader != null) return loader; + /* + * Under-construction map from an entry name to an array of integer + * surrogate keys for entries with matching names in jars on the path. + */ Map classImages = new HashMap<>(); + + /* + * Under-construction map from an integer entry key to a + * CodeSource representing the jar it belongs to. + */ + Map codeSources = new HashMap<>(); + Connection conn = getDefaultConnection(); try ( // Read the entries so that the one with highest prio is read last. // PreparedStatement outer = conn.prepareStatement( - "SELECT r.jarId" + + "SELECT r.jarId, r.jarName" + " FROM" + " sqlj.jar_repository r" + " INNER JOIN sqlj.classpath_entry c" + @@ -200,6 +218,9 @@ public static ClassLoader getSchemaLoader(String schemaName) { while(rs.next()) { + URL jarUrl = new URL("sqlj:" + rs.getString(2)); + CodeSource cs = new CodeSource(jarUrl, (CodeSigner[])null); + inner.setInt(1, rs.getInt(1)); try ( ResultSet rs2 = inner.executeQuery() ) { @@ -207,6 +228,7 @@ public static ClassLoader getSchemaLoader(String schemaName) { int entryId = rs2.getInt(1); String entryName = rs2.getString(2); + codeSources.put(entryId, cs); int[] oldEntry = classImages.get(entryName); if(oldEntry == null) classImages.put(entryName, new int[] { entryId }); @@ -222,6 +244,10 @@ public static ClassLoader getSchemaLoader(String schemaName) } } } + catch ( MalformedURLException e ) + { + throw unchecked(e); + } } ClassLoader parent = ClassLoader.getSystemClassLoader(); @@ -234,7 +260,7 @@ public static ClassLoader getSchemaLoader(String schemaName) loader = schemaName.equals(PUBLIC_SCHEMA) ? parent : getSchemaLoader(PUBLIC_SCHEMA); else - loader = new Loader(classImages, parent); + loader = new Loader(classImages, codeSources, parent); s_schemaLoaders.put(schemaName, loader); return loader; @@ -330,17 +356,33 @@ private static URL entryURL(int entryId) * loader's jar path that contain entries matching the name. */ private final Map m_entries; + private final Map m_domains; /** * Create a new Loader. * @param entries * @param parent */ - Loader(Map entries, ClassLoader parent) + Loader( + Map entries, + Map sources, ClassLoader parent) { super(parent); m_entries = entries; m_j9Helper = ifJ9getHelper(); // null if not under OpenJ9 with sharing + + Principal[] noPrincipals = new Principal[0]; + + m_domains = new HashMap<>(); + + sources.entrySet().stream() + .collect(groupingBy(Map.Entry::getValue)) + .entrySet().stream().forEach(e -> + { + ProtectionDomain pd = new ProtectionDomain( + e.getKey(), null /* no permissions */, this, noPrincipals); + e.getValue().forEach(ee -> m_domains.put(ee.getKey(), pd)); + }); } @Override @@ -351,6 +393,8 @@ protected Class findClass(final String name) int[] entryId = m_entries.get(path); if(entryId != null) { + ProtectionDomain pd = m_domains.get(entryId[0]); + /* * Check early whether running on OpenJ9 JVM and the shared cache * has the class. It is possible this early because the entryId is @@ -365,7 +409,7 @@ protected Class findClass(final String name) if ( o instanceof byte[] ) { byte[] img = (byte[]) o; - return defineClass(name, img, 0, img.length); + return defineClass(name, img, 0, img.length, pd); } String ifJ9token = (String) o; // used below when storing class @@ -396,7 +440,7 @@ protected Class findClass(final String name) { byte[] img = rs.getBytes(1); - Class cls = this.defineClass(name, img, 0, img.length); + Class cls = defineClass(name, img, 0, img.length, pd); ifJ9storeSharedClass(ifJ9token, cls); // noop for null token return cls; From 18abf79ffff154cd712e5cbecea0a7dcb2da0d41 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 25 Jan 2020 22:53:32 -0500 Subject: [PATCH 0754/1087] Ax the custom SecurityManager In the process, move the method from Backend that belongs in Commands (as observed in refactor/master/createfunc) so public can be removed from everything in Commands except what is meant to be exposed to CREATE FUNCTION. Warning: as of this commit, all distinction between the PostgreSQL language categories TRUSTED and UNTRUSTED is wiped out, until a later commit restores it by maintaining the distinction in AccessControlContext, allowing permissions to be granted in policy. --- pljava-so/src/main/c/Backend.c | 45 +---- pljava-so/src/main/c/InstallHelper.c | 2 +- pljava-so/src/main/c/Invocation.c | 14 +- pljava-so/src/main/c/type/Type.c | 11 +- pljava-so/src/main/include/pljava/Backend.h | 17 +- .../src/main/include/pljava/Invocation.h | 7 +- .../postgresql/pljava/internal/Backend.java | 185 ------------------ .../pljava/internal/InstallHelper.java | 21 ++ .../pljava/management/Commands.java | 53 ++++- 9 files changed, 87 insertions(+), 268 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 0a8d4a45..24b584f1 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -108,7 +108,6 @@ MemoryContext JavaMemoryContext; static JavaVM* s_javaVM = 0; static jclass s_Backend_class; -static jmethodID s_setTrusted; /* * GUC states @@ -128,7 +127,6 @@ static int java_thread_pg_entry; static char* java_thread_pg_entry; #endif -static bool s_currentTrust; static int s_javaLogLevel; #if PG_VERSION_NUM < 100000 @@ -1007,33 +1005,6 @@ static void initPLJavaClasses(void) SQLOutputToTuple_initialize(); InstallHelper_initialize(); - - s_setTrusted = PgObject_getStaticJavaMethod(s_Backend_class, "setTrusted", "(Z)V"); -} - -/** - * Initialize security - */ -void Backend_setJavaSecurity(bool trusted) -{ - if(trusted != s_currentTrust) - { - /* GCJ has major issues here. Real work on SecurityManager and - * related classes has just started in version 4.0.0. - */ -#ifndef GCJ - JNI_callStaticVoidMethod(s_Backend_class, s_setTrusted, (jboolean)trusted); - if(JNI_exceptionCheck()) - { - JNI_exceptionDescribe(); - JNI_exceptionClear(); - ereport(ERROR, ( - errcode(ERRCODE_INTERNAL_ERROR), - errmsg("Unable to initialize java security"))); - } -#endif - s_currentTrust = trusted; - } } int Backend_setJavaLogLevel(int logLevel) @@ -1313,7 +1284,7 @@ static void _destroyJavaVM(int status, Datum dummy) pqsigfunc saveSigAlrm; #endif - Invocation_pushInvocation(&ctx, false); + Invocation_pushInvocation(&ctx); if(sigsetjmp(recoverBuf, 1) != 0) { elog(DEBUG2, @@ -1340,7 +1311,7 @@ static void _destroyJavaVM(int status, Datum dummy) #endif #else - Invocation_pushInvocation(&ctx, false); + Invocation_pushInvocation(&ctx); elog(DEBUG2, "shutting down the Java virtual machine"); JNI_destroyVM(s_javaVM); #endif @@ -1801,13 +1772,9 @@ static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS) { deferInit = false; initsequencer( initstage, false); - - /* Force initial setting - */ - s_currentTrust = !trusted; } - Invocation_pushInvocation(&ctx, trusted); + Invocation_pushInvocation(&ctx); PG_TRY(); { Function function = @@ -1886,13 +1853,9 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) if ( IS_PLJAVA_INSTALLING > initstage ) PG_RETURN_VOID(); } - - /* Force initial setting - */ - s_currentTrust = !trusted; } - Invocation_pushInvocation(&ctx, trusted); + Invocation_pushInvocation(&ctx); PG_TRY(); { Function_getFunction(funcoid, false, true, check_function_bodies); diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index a2b87526..efe62942 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -595,7 +595,7 @@ void InstallHelper_groundwork() { Invocation ctx; bool snapshot_set = false; - Invocation_pushInvocation(&ctx, false); + Invocation_pushInvocation(&ctx); ctx.function = Function_INIT_WRITER; #if PG_VERSION_NUM >= 80400 if ( ! ActiveSnapshotSet() ) diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 31aa05f9..56689ddf 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -107,7 +107,6 @@ void Invocation_pushBootContext(Invocation* ctx) ctx->invocation = 0; ctx->function = 0; ctx->pushedFrame = false; - ctx->trusted = false; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; @@ -127,13 +126,12 @@ void Invocation_popBootContext(void) --s_callLevel; } -void Invocation_pushInvocation(Invocation* ctx, bool trusted) +void Invocation_pushInvocation(Invocation* ctx) { JNI_pushLocalFrame(LOCAL_FRAME_SIZE); ctx->invocation = 0; ctx->function = 0; ctx->pushedFrame = false; - ctx->trusted = trusted; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; @@ -143,7 +141,6 @@ void Invocation_pushInvocation(Invocation* ctx, bool trusted) ctx->triggerData = 0; #endif currentInvocation = ctx; - Backend_setJavaSecurity(trusted); ++s_callLevel; } @@ -184,15 +181,6 @@ void Invocation_popInvocation(bool wasException) JNI_popLocalFrame(0); if(ctx != 0) { - PG_TRY(); - { - Backend_setJavaSecurity(ctx->trusted); - } - PG_CATCH(); - { - elog(FATAL, "Failed to reinstate untrusted security after a trusted call or vice versa"); - } - PG_END_TRY(); MemoryContextSwitchTo(ctx->upperContext); } diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index ed18d3aa..29d871d4 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -126,18 +126,12 @@ typedef struct */ MemoryContext spiContext; bool hasConnected; - /* - * Copy of Invocation's 'trusted' flag. In normal calls through the handler, - * the value is known (implicit in which handler entry point was called), - * but that isn't available to the _endOfSetCB, so must be remembered here. - */ - bool trusted; } CallContextData; /* * Called during evaluation of a set-returning function, at various points after * calls into Java code could have instantiated an Invocation, or connected SPI. - * Does not stash elemType, rowProducer, rowCollector, or trusted; those are all + * Does not stash elemType, rowProducer, or rowCollector; those are all * unconditionally set in the first-call initialization, and spiContext to zero. */ static void stashCallContext(CallContextData *ctxData) @@ -202,7 +196,7 @@ static void _endOfSetCB(Datum arg) bool saveInExprCtxCB; CallContextData* ctxData = (CallContextData*)DatumGetPointer(arg); if(currentInvocation == 0) - Invocation_pushInvocation(&topCall, ctxData->trusted); + Invocation_pushInvocation(&topCall); saveInExprCtxCB = currentInvocation->inExprContextCB; currentInvocation->inExprContextCB = true; @@ -518,7 +512,6 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) JNI_deleteLocalRef(tmp); } - ctxData->trusted = currentInvocation->trusted; stashCallContext(ctxData); /* Register callback to be called when the function ends diff --git a/pljava-so/src/main/include/pljava/Backend.h b/pljava-so/src/main/include/pljava/Backend.h index fda7b67f..cb31b289 100644 --- a/pljava-so/src/main/include/pljava/Backend.h +++ b/pljava-so/src/main/include/pljava/Backend.h @@ -1,10 +1,15 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB - Thomas Hallgren + * PostgreSQL Global Development Group + * Chapman Flack */ #ifndef __pljava_Backend_h #define __pljava_Backend_h @@ -25,8 +30,6 @@ extern "C" { extern bool integerDateTimes; #endif -void Backend_setJavaSecurity(bool trusted); - int Backend_setJavaLogLevel(int logLevel); #ifdef PG_GETCONFIGOPTION diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index 63ca0586..686257e8 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -51,11 +51,6 @@ struct Invocation_ */ bool inExprContextCB; - /** - * Set to true if the executing function is trusted - */ - bool trusted; - /** * Whether nested invocation of a Function has required pushing a parameter * frame that will have to be popped when the Invocation is. @@ -102,7 +97,7 @@ extern void Invocation_pushBootContext(Invocation* ctx); extern void Invocation_popBootContext(void); -extern void Invocation_pushInvocation(Invocation* ctx, bool trusted); +extern void Invocation_pushInvocation(Invocation* ctx); extern void Invocation_popInvocation(bool wasException); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index b1abaf1a..29f59e62 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -12,28 +12,16 @@ */ package org.postgresql.pljava.internal; -import java.io.File; -import java.io.FilePermission; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.security.Permission; import java.sql.SQLException; import java.sql.SQLDataException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.PropertyPermission; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.postgresql.pljava.elog.ELogHandler; // for javadoc -import org.postgresql.pljava.management.Commands; - import org.postgresql.pljava.sqlgen.Lexicals.Identifier; import static org.postgresql.pljava.sqlgen.Lexicals.identifierFrom; import static @@ -282,158 +270,6 @@ public static void log(int logLevel, String str) doInPG(() -> _log(logLevel, str)); } - private static class PLJavaSecurityManager extends SecurityManager - { - private boolean m_recursion = false; - - public void checkPermission(Permission perm) - { - this.nonRecursiveCheck(perm); - } - - public void checkPermission(Permission perm, Object context) - { - this.nonRecursiveCheck(perm); - } - - private synchronized void nonRecursiveCheck(Permission perm) - { - if(m_recursion) - // - // Something, probably a ClassLoader - // loading one of the referenced - // classes, caused a recursion. Well - // everything done within this method - // is permitted so we just return - // here. - // - return; - - m_recursion = true; - try - { - this.assertPermission(perm); - } - finally - { - m_recursion = false; - } - } - - void assertPermission(Permission perm) - { - if(perm instanceof RuntimePermission) - { - String name = perm.getName(); - if("*".equals(name) || "exitVM".equals(name)) - throw new SecurityException(); - else if("setSecurityManager".equals(name) - && !s_inSetTrusted) - // - // Attempt to set another - // security manager while not - // in the setTrusted method - // - throw new SecurityException(); - } - else if(perm instanceof PropertyPermission) - { - if(perm.getActions().indexOf("write") >= 0) - { - // We never allow this to be changed. - // As for UDT byteorder, the classes that use it only check - // once so it would be misleading to allow runtime changes; - // use pljava.vmoptions to provide an initial value. - // - String propName = perm.getName(); - if ( propName.equals("java.home") || propName.matches( - "org\\.postgresql\\.pljava\\.udt\\.byteorder(?:\\..*)?") - ) - throw new SecurityException(); - } - } - } - } - - private static boolean s_inSetTrusted = false; - - private static final SecurityManager s_untrustedSecurityManager = new PLJavaSecurityManager(); - - /** - * This security manager will block all attempts to access the file system - */ - private static final SecurityManager s_trustedSecurityManager = new PLJavaSecurityManager() - { - void assertPermission(Permission perm) - { - if(perm instanceof FilePermission) - { - String actions = perm.getActions(); - if("read".equals(actions)) - { - // Allow read of /dev/random - // and /dev/urandom - - String fileName = perm.getName(); - - if ( "/dev/random".equals( fileName ) - || - "/dev/urandom".equals( fileName ) - ) - return; - - // Must be able to read - // timezone info etc. in the - // java installation - // directory. - // - File javaHome = new File(System.getProperty("java.home")); - File accessedFile = new File(perm.getName()); - File fileDir = accessedFile.getParentFile(); - while(fileDir != null) - { - if(fileDir.equals(javaHome)) - return; - fileDir = fileDir.getParentFile(); - } - } - throw new SecurityException(perm.getActions() + " on " + perm.getName()); - } - super.assertPermission(perm); - } - }; - - public static void addClassImages(int jarId, String urlString) - throws SQLException - { - InputStream urlStream = null; - boolean wasTrusted = (System.getSecurityManager() == s_trustedSecurityManager); - - if(wasTrusted) - setTrusted(false); - - try - { - URL url = new URL(urlString); - URLConnection uc = url.openConnection(); - uc.connect(); - int sz = uc.getContentLength(); // once java6 obsolete, use ...Long - urlStream = uc.getInputStream(); - Commands.addClassImages(jarId, urlStream, sz); - } - catch(IOException e) - { - throw new SQLException("I/O exception reading jar file: " + e.getMessage()); - } - finally - { - if(urlStream != null) - try { urlStream.close(); } catch(IOException e) {} - if(wasTrusted) - setTrusted(true); - } - } - public static void clearFunctionCache() { doInPG(Backend::_clearFunctionCache); @@ -444,27 +280,6 @@ public static boolean isCreatingExtension() return doInPG(Backend::_isCreatingExtension); } - /** - * Called when the JVM is first booted and then everytime a switch - * is made between calling a trusted function versus an untrusted - * function. - */ - private static void setTrusted(boolean trusted) - { - s_inSetTrusted = true; - try - { - Logger log = Logger.getAnonymousLogger(); - if(log.isLoggable(Level.FINER)) - log.finer("Using SecurityManager for " + (trusted ? "trusted" : "untrusted") + " language"); - System.setSecurityManager(trusted ? s_trustedSecurityManager : s_untrustedSecurityManager); - } - finally - { - s_inSetTrusted = false; - } - } - /** * Returns true if the backend is awaiting a return from a * call into the JVM. This method will only return false diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 966cf24f..4003b471 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -14,7 +14,10 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; +import java.net.URL; +import java.net.MalformedURLException; import java.nio.charset.Charset; +import java.security.Security; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; @@ -121,6 +124,11 @@ public static String hello( System.clearProperty(encodingKey); } + /* so it can be granted permissions in the pljava policy */ + System.setProperty( "org.postgresql.pljava.codesource", + InstallHelper.class.getProtectionDomain().getCodeSource() + .getLocation().toString()); + /* * Construct the strings announcing the versions in use. */ @@ -137,6 +145,19 @@ public static String hello( String vmVer = System.getProperty( "java.vm.version"); String vmInfo = System.getProperty( "java.vm.info"); + try + { + new URL("sqlj:x"); // sqlj: scheme must exist before reading policy + } + catch ( MalformedURLException e ) + { + throw new SecurityException( + "failed to create sqlj: URL scheme needed for security policy", + e); + } + + System.setSecurityManager( new SecurityManager()); + StringBuilder sb = new StringBuilder(); sb.append( "PL/Java native code (").append( nativeVer).append( ")\n"); sb.append( "PL/Java common code (").append( implVersion).append( ")\n"); diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index a64848e5..bea99db9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; import java.nio.ByteBuffer; import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetDecoder; @@ -48,6 +50,7 @@ import org.postgresql.pljava.internal.AclId; import org.postgresql.pljava.internal.Backend; import org.postgresql.pljava.internal.Oid; +import static org.postgresql.pljava.internal.Privilege.doPrivileged; import org.postgresql.pljava.jdbc.SQLUtils; import org.postgresql.pljava.sqlj.Loader; @@ -334,13 +337,49 @@ public class Commands * jar_entry table. * * @param jarId The id used for the foreign key to the jar_repository table + * @param urlString The url to be read + */ + static void addClassImages(int jarId, String urlString) + throws SQLException + { + try + { + URL url = new URL(urlString); + URLConnection uc = url.openConnection(); + long[] sz = new long[1]; + + /* + * Do uc.connect() with PL/Java implementation's permissions, but + * narrowed to only what uc says it needs to make this connection. + */ + try ( + InputStream urlStream = doPrivileged(() -> + { + uc.connect(); + sz[0] = uc.getContentLengthLong(); + return uc.getInputStream(); + }, null, uc.getPermission()) + ) + { + addClassImages(jarId, urlStream, sz[0]); + } + } + catch(IOException e) + { + throw new SQLException("I/O exception reading jar file: " + + e.getMessage()); + } + } + + /** + * Add class images from an already opened stream. * @param urlStream An InputStream (opened on what may have been a URL) * @param sz The expected size of the stream, used as a worst-case * mark/reset limit. The caller might pass -1 if the URLConnection can't * determine a size in advance (a generous guess will be made in that case). * @throws SQLException */ - public static void addClassImages(int jarId, InputStream urlStream, int sz) + static void addClassImages(int jarId, InputStream urlStream, long sz) throws SQLException { PreparedStatement stmt = null; @@ -486,12 +525,14 @@ public static void addClassImages(int jarId, InputStream urlStream, int sz) * leaves little choice but to sneak in ahead of the JarInputStream and * pluck out the original manifest as a zip entry. */ - private static String rawManifest( BufferedInputStream bis, int markLimit) + private static String rawManifest( BufferedInputStream bis, long markLimit) throws IOException { + if ( Integer.MAX_VALUE < markLimit ) + markLimit = -1; // just pretend it wasn't specified // If the caller can't say how long the stream is, this mark() limit // should be plenty - bis.mark( markLimit > 0 ? markLimit : 32*1024*1024); + bis.mark( markLimit > 0 ? (int)markLimit : 32*1024*1024); ZipInputStream zis = new ZipInputStream( bis); for ( ZipEntry ze; null != (ze = zis.getNextEntry()); ) { @@ -1160,7 +1201,7 @@ private static void installJar(String urlString, String jarName, throw new SQLException("Unable to obtain id of '" + jarName + "'"); if(image == null) - Backend.addClassImages(jarId, urlString); + addClassImages(jarId, urlString); else { InputStream imageStream = new ByteArrayInputStream(image); @@ -1231,7 +1272,7 @@ private static void replaceJar(String urlString, String jarName, SQLUtils.close(stmt); } if(image == null) - Backend.addClassImages(jarId, urlString); + addClassImages(jarId, urlString); else { InputStream imageStream = new ByteArrayInputStream(image); From f014f4fb9fd9dd1f617de2df56275f5612897407 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 18 Sep 2020 18:39:39 -0400 Subject: [PATCH 0755/1087] Add a pljava.policy adequate to run the examples --- pljava-packaging/build.xml | 3 + .../src/main/resources/pljava.policy | 58 +++++++++++++++++++ .../pljava/internal/InstallHelper.java | 3 + 3 files changed, 64 insertions(+) create mode 100644 pljava-packaging/src/main/resources/pljava.policy diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 1c47fa3b..ea612df8 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -310,6 +310,9 @@ jos.close(); + + 0 ; ) - depDesc[i].remove(SQLUtils.getDefaultConnection()); - try + withJarInPath(jarName, true, () -> { - if (classpathChanged) - setClassPath(originalSchemaAndPath[0],originalSchemaAndPath[1]); - } - catch ( SQLException sqle ) - { - if ( ! "3F000".equals(sqle.getSQLState()) ) - throw sqle; - } + for ( int i = depDesc.length ; i --> 0 ; ) + depDesc[i].remove(SQLUtils.getDefaultConnection()); + }); } private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) @@ -1145,14 +1165,14 @@ private static int getJarId(String jarName, AclId[] ownerRet) * schema is found. * @throws SQLException */ - private static Oid getSchemaId(String schemaName) throws SQLException + private static Oid getSchemaId(Identifier.Simple schema) throws SQLException { try(PreparedStatement stmt = SQLUtils.getDefaultConnection() .prepareStatement( "SELECT oid FROM pg_catalog.pg_namespace " + "WHERE nspname OPERATOR(pg_catalog.=) ?")) { - stmt.setString(1, schemaName); + stmt.setString(1, schema.pgFolded()); try(ResultSet rs = stmt.executeQuery()) { if(!rs.next()) diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index e8b483d5..032ce8f7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -45,6 +45,8 @@ import static java.util.stream.Collectors.groupingBy; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; + import org.postgresql.pljava.internal.Backend; import org.postgresql.pljava.internal.Checked; import org.postgresql.pljava.internal.Oid; @@ -120,13 +122,15 @@ public URL nextElement() return entryURL(m_entryIds[m_top++]); } } - private static final String PUBLIC_SCHEMA = "public"; + private static final Identifier.Simple PUBLIC_SCHEMA = + Identifier.Simple.fromCatalog("public"); - private static final Map + private static final Map s_schemaLoaders = new HashMap<>(); - private static final Map>> - s_typeMap = new HashMap<>(); + private static final + Map>> + s_typeMap = new HashMap<>(); /** * Removes all cached schema loaders, functions, and type maps. This @@ -161,25 +165,23 @@ public static ClassLoader getCurrentLoader() throw new SQLException("Unable to determine current schema"); schema = rs.getString(1); } - return getSchemaLoader(schema); + return getSchemaLoader(Identifier.Simple.fromCatalog(schema)); } /** * Obtain a loader that has been configured for the class path of the * schema named schemaName. Class paths are defined using the * SQL procedure sqlj.set_classpath. - * @param schemaName The name of the schema. + * @param schema The name of the schema as an Identifier.Simple. * @return A loader. */ - public static ClassLoader getSchemaLoader(String schemaName) + public static ClassLoader getSchemaLoader(Identifier.Simple schema) throws SQLException { - if(schemaName == null || schemaName.length() == 0) - schemaName = PUBLIC_SCHEMA; - else - schemaName = schemaName.toLowerCase(); + if(schema == null ) + schema = PUBLIC_SCHEMA; - ClassLoader loader = s_schemaLoaders.get(schemaName); + ClassLoader loader = s_schemaLoaders.get(schema); if(loader != null) return loader; @@ -214,7 +216,7 @@ public static ClassLoader getSchemaLoader(String schemaName) { outer.unwrap(SPIReadOnlyControl.class).clearReadOnly(); inner.unwrap(SPIReadOnlyControl.class).clearReadOnly(); - outer.setString(1, schemaName); + outer.setString(1, schema.pgFolded()); try ( ResultSet rs = outer.executeQuery() ) { while(rs.next()) @@ -258,13 +260,13 @@ public static ClassLoader getSchemaLoader(String schemaName) // classpath of public schema or to the system classloader if the // request already is for the public schema. // - loader = schemaName.equals(PUBLIC_SCHEMA) + loader = schema.equals(PUBLIC_SCHEMA) ? parent : getSchemaLoader(PUBLIC_SCHEMA); else loader = doPrivileged(() -> new Loader(classImages, codeSources, parent)); - s_schemaLoaders.put(schemaName, loader); + s_schemaLoaders.put(schema, loader); return loader; } @@ -278,7 +280,7 @@ public static ClassLoader getSchemaLoader(String schemaName) * @return The Map, possibly empty but never null. */ public static Map> getTypeMap( - final String schema) + final Identifier.Simple schema) throws SQLException { Map> typesForSchema = @@ -291,7 +293,8 @@ public static Map> getTypeMap( { public Class get(Oid key) { - s_logger.finer("Obtaining type mapping for OID " + key + " for schema " + schema); + s_logger.finer("Obtaining type mapping for OID " + key + + " for schema " + schema); return super.get(key); } }; From bbbb75c8c1631a788039ecf66efa6a4d60d0f772 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 22 Sep 2020 22:28:27 -0400 Subject: [PATCH 0760/1087] Retrofit UDT-related entry points, now with lid --- pljava-so/src/main/c/Function.c | 166 ++++++++++++------ pljava-so/src/main/c/Invocation.c | 46 ++++- pljava-so/src/main/c/type/Type.c | 3 +- pljava-so/src/main/c/type/UDT.c | 22 ++- pljava-so/src/main/include/pljava/Function.h | 12 +- .../src/main/include/pljava/Invocation.h | 27 ++- pljava-so/src/main/include/pljava/type/UDT.h | 24 ++- .../src/main/include/pljava/type/UDT_priv.h | 24 ++- .../pljava/internal/EntryPoints.java | 161 ++++++++++++++--- .../postgresql/pljava/internal/Function.java | 138 +++++++++++---- 10 files changed, 475 insertions(+), 148 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 25606195..019d8187 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -59,6 +59,8 @@ static jmethodID s_Function_create; static jmethodID s_Function_getClassIfUDT; static jmethodID s_Function_udtReadHandle; static jmethodID s_Function_udtParseHandle; +static jmethodID s_Function_udtWriteHandle; +static jmethodID s_Function_udtToStringHandle; static jmethodID s_ParameterFrame_push; static jmethodID s_ParameterFrame_pop; static jmethodID s_EntryPoints_invoke; @@ -210,7 +212,11 @@ void Function_initialize(void) { "_storeToUDT", "(JLjava/lang/ClassLoader;Ljava/lang/Class;ZII" - "Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;)V", + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + ")V", Java_org_postgresql_pljava_internal_Function__1storeToUDT }, { @@ -260,24 +266,41 @@ void Function_initialize(void) s_EntryPoints_udtWriteInvoke = PgObject_getStaticJavaMethod( s_EntryPoints_class, - "udtWriteInvoke", "(Ljava/sql/SQLData;Ljava/sql/SQLOutput;" + "udtWriteInvoke", + "(Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + "Ljava/sql/SQLData;Ljava/sql/SQLOutput;" ")V"); s_EntryPoints_udtToStringInvoke = PgObject_getStaticJavaMethod( s_EntryPoints_class, - "udtToStringInvoke", "(Ljava/sql/SQLData;)Ljava/lang/String;"); + "udtToStringInvoke", + "(Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + "Ljava/sql/SQLData;)Ljava/lang/String;"); s_EntryPoints_udtReadInvoke = PgObject_getStaticJavaMethod( s_EntryPoints_class, - "udtReadInvoke", "(Ljava/lang/invoke/MethodHandle;Ljava/sql/SQLInput;" + "udtReadInvoke", + "(Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + "Ljava/sql/SQLInput;" "Ljava/lang/String;)Ljava/sql/SQLData;"); s_EntryPoints_udtParseInvoke = PgObject_getStaticJavaMethod( s_EntryPoints_class, - "udtParseInvoke", "(Ljava/lang/invoke/MethodHandle;Ljava/lang/String;" + "udtParseInvoke", + "(Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" + "Ljava/lang/String;" "Ljava/lang/String;)Ljava/sql/SQLData;"); s_Function_udtReadHandle = PgObject_getStaticJavaMethod(s_Function_class, - "udtReadHandle", "(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandle;"); + "udtReadHandle", "(Ljava/lang/Class;)" + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); s_Function_udtParseHandle = PgObject_getStaticJavaMethod(s_Function_class, - "udtParseHandle", "(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandle;"); + "udtParseHandle", "(Ljava/lang/Class;)" + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); + s_Function_udtWriteHandle = PgObject_getStaticJavaMethod(s_Function_class, + "udtWriteHandle", "(Ljava/lang/Class;)" + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); + s_Function_udtToStringHandle = + PgObject_getStaticJavaMethod(s_Function_class, + "udtToStringHandle", "(Ljava/lang/Class;)" + "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); PgObject_registerNatives2(s_Function_class, functionMethods); @@ -354,30 +377,80 @@ jdouble pljava_Function_doubleInvoke(Function self) return s_primitiveParameters[0].d; } -void pljava_Function_udtWriteInvoke(jobject value, jobject stream) +/* + * 'Reserve' the static parameter frame for (refArgCount,primArgCount) reference + * and primitive parameters, respectively, pushing temporarily out of the way + * any current contents, detected by a non-(0,0) existing reservation. + * + * The corresponding pop of the earlier contents will happen at + * Invocation_popInvocation time, so this scheme is only appropriately used for + * calls that happen within the scope of an Invocation, as conventional PL/Java + * function calls do. + * + * It is possible to reserve (0,0) space, though no existing frame will be + * saved/restored in that case. Caution: the two count arguments here count only + * parameters, not the possibility that a primitive-returning function uses a + * slot in the frame for its return. The primitive call wrappers must make their + * own arrangements to save the typically-only-one-jvalue affected by that use + * and restore it on both normal and exceptional return paths. To streamline the + * most common case, Invocation_{push,pop}Invocation will unconditionally save + * the first jvalue slot, and restore it if the more heavyweight frame-pushing + * has not been used. That spares a primitive call wrapper the cycles of + * managing another PG_TRY block. Any wrapper that will use more than the first + * jvalue slot for returns, though, must handle its own normal and exceptional + * cleanup. + */ +static void reserveParameterFrame(jsize refArgCount, jsize primArgCount) +{ + jshort newCounts = COUNTCHECK(refArgCount, primArgCount); + + /* The *s_countCheck field in the parameter area will be zero unless + * this is a recursive invocation (believed only possible via a UDT + * function called while converting the parameters for some outer + * invocation). It could also be zero if this is a recursive invocation + * but the outer one involves no parameters, which won't happen if UDT + * conversion for a parameter is the only way to get here, and even if + * it happens, we still don't need to save its frame because there is + * nothing there that we'll clobber. + */ + if ( 0 != newCounts && 0 != *s_countCheck ) + { + JNI_callStaticVoidMethodLocked( + s_ParameterFrame_class, s_ParameterFrame_push); + /* Record, in currentInvocation, that a frame was pushed; the pop + * will happen in Invocation_popInvocation, which our caller + * arranges for both normal return and PG_CATCH cases. + */ + currentInvocation->frameLimits = FRAME_LIMITS_PUSHED; + } + *s_countCheck = newCounts; +} + +void pljava_Function_udtWriteInvoke( + jobject invocable, jobject value, jobject stream) { JNI_callStaticVoidMethod(s_EntryPoints_class, - s_EntryPoints_udtWriteInvoke, value, stream); + s_EntryPoints_udtWriteInvoke, invocable, value, stream); } -jstring pljava_Function_udtToStringInvoke(jobject value) +jstring pljava_Function_udtToStringInvoke(jobject invocable, jobject value) { return JNI_callStaticObjectMethod(s_EntryPoints_class, - s_EntryPoints_udtToStringInvoke, value); + s_EntryPoints_udtToStringInvoke, invocable, value); } jobject pljava_Function_udtReadInvoke( - jobject readMH, jobject stream, jstring typeName) + jobject invocable, jobject stream, jstring typeName) { return JNI_callStaticObjectMethod(s_EntryPoints_class, - s_EntryPoints_udtReadInvoke, readMH, stream, typeName); + s_EntryPoints_udtReadInvoke, invocable, stream, typeName); } jobject pljava_Function_udtParseInvoke( - jobject parseMH, jstring stringRep, jstring typeName) + jobject parseInvocable, jstring stringRep, jstring typeName) { return JNI_callStaticObjectMethod(s_EntryPoints_class, - s_EntryPoints_udtParseInvoke, parseMH, stringRep, typeName); + s_EntryPoints_udtParseInvoke, parseInvocable, stringRep, typeName); } jobject pljava_Function_udtReadHandle(jclass clazz) @@ -392,6 +465,18 @@ jobject pljava_Function_udtParseHandle(jclass clazz) s_Function_udtParseHandle, clazz); } +jobject pljava_Function_udtWriteHandle(jclass clazz) +{ + return JNI_callStaticObjectMethod(s_Function_class, + s_Function_udtWriteHandle, clazz); +} + +jobject pljava_Function_udtToStringHandle(jclass clazz) +{ + return JNI_callStaticObjectMethod(s_Function_class, + s_Function_udtToStringHandle, clazz); +} + static jstring getSchemaName(int namespaceOid) { HeapTuple nspTup = PgObject_getValidTuple(NAMESPACEOID, namespaceOid, "namespace"); @@ -433,8 +518,8 @@ Type Function_checkTypeUDT(Oid typeId, Form_pg_type typeStruct) ReleaseSysCache(procTup); if ( NULL != clazz ) - t = (Type) - UDT_registerUDT(clazz, typeId, typeStruct, 0, true, NULL, NULL); + t = (Type)UDT_registerUDT(clazz, typeId, typeStruct, 0, true, + NULL, NULL, NULL, NULL); return t; } @@ -653,31 +738,8 @@ Datum Function_invoke(Function self, PG_FUNCTION_ARGS) } if ( ! skipParameterConversion ) - { - jsize refArgCount = self->func.nonudt.numRefParams; - jsize primArgCount = self->func.nonudt.numPrimParams; - - /* The *s_countCheck field in the parameter area will be zero unless - * this is a recursive invocation (believed only possible via a UDT - * function called while converting the parameters for some outer - * invocation). It could also be zero if this is a recursive invocation - * but the outer one involves no parameters, which won't happen if UDT - * conversion for a parameter is the only way to get here, and even if - * it happens, we still don't need to save its frame because there is - * nothing there that we'll clobber. - */ - if ( 0 != *s_countCheck ) - { - JNI_callStaticVoidMethodLocked( - s_ParameterFrame_class, s_ParameterFrame_push); - /* Record, in currentInvocation, that a frame was pushed; the pop - * will happen in Invocation_popInvocation, which our caller - * arranges for both normal return and PG_CATCH cases. - */ - currentInvocation->pushedFrame = true; - } - *s_countCheck = COUNTCHECK(refArgCount, primArgCount); - } + reserveParameterFrame( + self->func.nonudt.numRefParams, self->func.nonudt.numPrimParams); invokerType = self->func.nonudt.returnType; @@ -743,16 +805,7 @@ Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) if(jtd == 0) return 0; - /* - * See comments for this block in Function_invoke. - */ - if ( 0 != *s_countCheck ) - { - JNI_callStaticVoidMethodLocked( - s_ParameterFrame_class, s_ParameterFrame_push); - currentInvocation->pushedFrame = true; - } - *s_countCheck = COUNTCHECK(1, 0); + reserveParameterFrame(1, 0); JNI_setObjectArrayElement(s_referenceParameters, 0, jtd); @@ -865,6 +918,7 @@ JNIEXPORT jobject JNICALL * it is only called once in early initialization on the primordial thread. */ s_referenceParameters = (*env)->NewGlobalRef(env, referenceParams); + pljava_Invocation_shareFrame(s_primitiveParameters, s_countCheck); return (*env)->NewDirectByteBuffer( env, &s_primitiveParameters, sizeof s_primitiveParameters); } @@ -983,13 +1037,13 @@ JNIEXPORT jboolean JNICALL /* * Class: org_postgresql_pljava_internal_Function * Method: _storeToUDT - * Signature: (JLjava/lang/ClassLoader;Ljava/lang/Class;ZIILjava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;)V + * Signature: (JLjava/lang/ClassLoader;Ljava/lang/Class;ZIILorg/postgresql/pljava/internal/EntryPoints$Invocable;Lorg/postgresql/pljava/internal/EntryPoints$Invocable;Lorg/postgresql/pljava/internal/EntryPoints$Invocable;Lorg/postgresql/pljava/internal/EntryPoints$Invocable;)V */ JNIEXPORT void JNICALL Java_org_postgresql_pljava_internal_Function__1storeToUDT( JNIEnv *env, jclass jFunctionClass, jlong wrappedPtr, jobject schemaLoader, jclass clazz, jboolean readOnly, jint funcInitial, jint udtId, - jobject parseMH, jobject readMH) + jobject parseMH, jobject readMH, jobject writeMH, jobject toStringMH) { Ptr2Long p2l; Function self; @@ -1022,8 +1076,8 @@ JNIEXPORT void JNICALL self->clazz = JNI_newGlobalRef(clazz); self->func.udt.udt = - UDT_registerUDT( - self->clazz, udtId, pgType, 0, true, parseMH, readMH); + UDT_registerUDT(self->clazz, udtId, pgType, 0, true, + parseMH, readMH, writeMH, toStringMH); switch ( funcInitial ) { diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 56689ddf..1154c8e5 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -29,6 +29,32 @@ static unsigned int s_callLevel = 0; Invocation* currentInvocation; +/* + * Two features of the calling convention for PL/Java functions will be handled + * here in Invocation to keep wrappers in Function simple. A PL/Java function + * may use static primitive slot 0 to return a primitive value, so that will + * always be saved in an Invocation struct and restored on both normal and + * exceptional return paths, when the heavier-weight full pushing of a Java + * ParameterFrame has not occurred. Likewise, the heavy full push is skipped if + * either the current or the new frame limits are (0,0), which means for such + * cases the frame limits themselves must be saved and restored the same way. + */ +static jvalue *s_primSlot0; +static jshort *s_frameLimits; + +/* + * To keep these values somewhat encapsulated, Function.c calls this function + * during its initialization to share them, rather than simply making them + * global. + */ +void pljava_Invocation_shareFrame(jvalue *slot0, jshort *limits) +{ + if ( 0 != s_primSlot0 || 0 != s_frameLimits ) + return; + s_primSlot0 = slot0; + s_frameLimits = limits; +} + extern void Invocation_initialize(void); void Invocation_initialize(void) { @@ -106,7 +132,8 @@ void Invocation_pushBootContext(Invocation* ctx) JNI_pushLocalFrame(LOCAL_FRAME_SIZE); ctx->invocation = 0; ctx->function = 0; - ctx->pushedFrame = false; + ctx->frameLimits = 0; + ctx->primSlot0.j = 0L; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; @@ -131,7 +158,8 @@ void Invocation_pushInvocation(Invocation* ctx) JNI_pushLocalFrame(LOCAL_FRAME_SIZE); ctx->invocation = 0; ctx->function = 0; - ctx->pushedFrame = false; + ctx->frameLimits = *s_frameLimits; + ctx->primSlot0 = *s_primSlot0; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; @@ -148,8 +176,19 @@ void Invocation_popInvocation(bool wasException) { Invocation* ctx = currentInvocation->previous; - if ( currentInvocation->pushedFrame ) + /* + * If the more heavyweight parameter-frame push got done, undo it. + */ + if ( FRAME_LIMITS_PUSHED == currentInvocation->frameLimits ) pljava_Function_popFrame(); + else + { + /* + * The lighter-weight cleanup. + */ + *s_frameLimits = currentInvocation->frameLimits; + *s_primSlot0 = currentInvocation->primSlot0; + } /* * If a Java Invocation instance was created and associated with this @@ -179,6 +218,7 @@ void Invocation_popInvocation(bool wasException) SPI_finish(); JNI_popLocalFrame(0); + if(ctx != 0) { MemoryContextSwitchTo(ctx->upperContext); diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 29d871d4..f01c7395 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -684,7 +684,8 @@ Type Type_fromOid(Oid typeId, jobject typeMap) if ( hasTupleDesc ) ReleaseTupleDesc(tupleDesc); type = (Type)UDT_registerUDT( - typeClass, typeId, typeStruct, hasTupleDesc, false, NULL, NULL); + typeClass, typeId, typeStruct, hasTupleDesc, false, + NULL, NULL, NULL, NULL); JNI_deleteLocalRef(typeClass); goto finally; } diff --git a/pljava-so/src/main/c/type/UDT.c b/pljava-so/src/main/c/type/UDT.c index 277f4431..e4a7aa25 100644 --- a/pljava-so/src/main/c/type/UDT.c +++ b/pljava-so/src/main/c/type/UDT.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -151,7 +151,7 @@ static Datum coerceScalarObject(UDT self, jobject value) * produce the human-used external representation, is being called here * to produce this UDT's internal representation. */ - jstring jstr = pljava_Function_udtToStringInvoke(value); + jstring jstr = pljava_Function_udtToStringInvoke(self->toString, value); char* tmp = String_createNTS(jstr); result = CStringGetDatum(tmp); JNI_deleteLocalRef(jstr); @@ -176,7 +176,7 @@ static Datum coerceScalarObject(UDT self, jobject value) enlargeStringInfo(&buffer, dataLen); outputStream = SQLOutputToChunk_create(&buffer, isJavaBasedScalar); - pljava_Function_udtWriteInvoke(value, outputStream); + pljava_Function_udtWriteInvoke(self->writeSQL, value, outputStream); SQLOutputToChunk_close(outputStream); if(dataLen < 0) @@ -223,7 +223,7 @@ static Datum coerceTupleObject(UDT self, jobject value) TupleDesc tupleDesc = lookup_rowtype_tupdesc_noerror(typeId, -1, true); jobject sqlOutput = SQLOutputToTuple_create(tupleDesc); ReleaseTupleDesc(tupleDesc); - pljava_Function_udtWriteInvoke(value, sqlOutput); + pljava_Function_udtWriteInvoke(self->writeSQL, value, sqlOutput); tuple = SQLOutputToTuple_getTuple(sqlOutput); if(tuple != 0) result = HeapTupleGetDatum(tuple); @@ -355,7 +355,7 @@ Datum UDT_output(UDT udt, PG_FUNCTION_ARGS) * toString to get the external representation from the object. */ jobject value = _UDT_coerceDatum((Type)udt, PG_GETARG_DATUM(0)).l; - jstring jstr = pljava_Function_udtToStringInvoke(value); + jstring jstr = pljava_Function_udtToStringInvoke(udt->toString, value); MemoryContext currCtx = Invocation_switchToUpperContext(); txt = String_createNTS(jstr); @@ -429,7 +429,8 @@ bool UDT_isScalar(UDT udt) /* Make this datatype available to the postgres system. */ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, - bool hasTupleDesc, bool isJavaBasedScalar, jobject parseMH, jobject readMH) + bool hasTupleDesc, bool isJavaBasedScalar, jobject parseMH, jobject readMH, + jobject writeMH, jobject toStringMH) { jstring jcn; MemoryContext currCtx; @@ -527,19 +528,28 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, */ if ( NULL == parseMH ) parseMH = pljava_Function_udtParseHandle(clazz); + if ( NULL == toStringMH ) + toStringMH = pljava_Function_udtToStringHandle(clazz); udt->parse = JNI_newGlobalRef(parseMH); + udt->toString = JNI_newGlobalRef(toStringMH); JNI_deleteLocalRef(parseMH); + JNI_deleteLocalRef(toStringMH); } else { udt->parse = NULL; + udt->toString = NULL; } udt->hasTupleDesc = hasTupleDesc; if ( NULL == readMH ) readMH = pljava_Function_udtReadHandle(clazz); + if ( NULL == writeMH ) + writeMH = pljava_Function_udtWriteHandle(clazz); udt->readSQL = JNI_newGlobalRef(readMH); + udt->writeSQL = JNI_newGlobalRef(writeMH); JNI_deleteLocalRef(readMH); + JNI_deleteLocalRef(writeMH); Type_registerType(className, (Type)udt); return udt; } diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 0d5b30fb..dd76fb09 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -107,13 +107,17 @@ extern jfloat pljava_Function_floatInvoke(Function self); extern jlong pljava_Function_longInvoke(Function self); extern jdouble pljava_Function_doubleInvoke(Function self); -extern void pljava_Function_udtWriteInvoke(jobject value, jobject stream); -extern jstring pljava_Function_udtToStringInvoke(jobject value); +extern void pljava_Function_udtWriteInvoke( + jobject invocable, jobject value, jobject stream); +extern jstring pljava_Function_udtToStringInvoke( + jobject invocable, jobject value); extern jobject pljava_Function_udtReadInvoke( - jobject readMH, jobject stream, jstring typeName); + jobject invocable, jobject stream, jstring typeName); extern jobject pljava_Function_udtParseInvoke( - jobject parseMH, jstring stringRep, jstring typeName); + jobject invocable, jstring stringRep, jstring typeName); +extern jobject pljava_Function_udtWriteHandle(jclass clazz); +extern jobject pljava_Function_udtToStringHandle(jclass clazz); extern jobject pljava_Function_udtReadHandle(jclass clazz); extern jobject pljava_Function_udtParseHandle(jclass clazz); diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index 686257e8..e3226a4e 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -52,10 +52,24 @@ struct Invocation_ bool inExprContextCB; /** - * Whether nested invocation of a Function has required pushing a parameter - * frame that will have to be popped when the Invocation is. + * The saved limits reserved in Function.c's static parameter frame, as a + * count of reference and primitive parameters combined in a short. + * FRAME_LIMITS_PUSHED is an otherwise invalid value used to record that the + * more heavyweight saving of the frame as a Java ParameterFrame instance + * has occurred. Otherwise, this value (and the primitive slot 0 value + * below) are simply restored when this Invocation is exited normally or + * exceptionally. */ - bool pushedFrame; + jshort frameLimits; +#define FRAME_LIMITS_PUSHED ((jshort)-1) + + /** + * The saved value of the first primitive slot in Function's static + * parameter frame. Unless frameLimits above is FRAME_LIMITS_PUSHED, this + * value is simply restored when this Invocation is exited normally or + * exceptionally. + */ + jvalue primSlot0; /** * The currently executing Function. @@ -113,6 +127,13 @@ extern jobject Invocation_getTypeMap(void); */ extern MemoryContext Invocation_switchToUpperContext(void); +/* + * Called only during Function's initialization to supply these values, making + * them cheap to access during pushInvocation/popInvocation, while still a bit + * more encapsulated than if they were made global. + */ +extern void pljava_Invocation_shareFrame(jvalue *slot0, jshort *limits); + #ifdef __cplusplus } #endif diff --git a/pljava-so/src/main/include/pljava/type/UDT.h b/pljava-so/src/main/include/pljava/type/UDT.h index 517a48cc..9323f027 100644 --- a/pljava-so/src/main/include/pljava/type/UDT.h +++ b/pljava-so/src/main/include/pljava/type/UDT.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -40,13 +46,15 @@ extern bool UDT_isScalar(UDT udt); * argument is only used in the scalar case. A readMH is needed for the scalar * or the composite case. * - * If null is supplied for readMH, or for parseMH in the scalar case, an upcall - * to Java will be made to obtain the handle. Handles can be passed as arguments + * Non-null values for {parse,read,write,toString}MH can be passed as arguments * here as a shortcut in case the registration is coming from Function.c and the - * handles are already known. + * handles are already known (they are in fact Invocables, but were method + * handles before, and MH still suggests their purpose and makes shorter names). + * If passed as NULL and needed, upcalls will be made to obtain them. */ extern UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, - bool hasTupleDesc, bool isJavaBasedScalar, jobject parseMH, jobject readMH); + bool hasTupleDesc, bool isJavaBasedScalar, jobject parseMH, jobject readMH, + jobject writeMH, jobject toStringMH); typedef Datum (*UDTFunction)(UDT udt, PG_FUNCTION_ARGS); diff --git a/pljava-so/src/main/include/pljava/type/UDT_priv.h b/pljava-so/src/main/include/pljava/type/UDT_priv.h index 39ab334e..89ea80c0 100644 --- a/pljava-so/src/main/include/pljava/type/UDT_priv.h +++ b/pljava-so/src/main/include/pljava/type/UDT_priv.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -33,6 +39,16 @@ struct UDT_ bool hasTupleDesc; jobject parse; jobject readSQL; + + /* + * At first glance, one might not retain writeSQL and toString handles + * per-UDT, as they are both inherited methods common to all UDTs and so + * do not depend on the class of the receiver. What these jobjects hold, + * though, is an Invocable, which carries an AccessControlContext, which is + * chosen at resolution time per-UDT or per-function, so they must be here. + */ + jobject writeSQL; + jobject toString; }; extern Datum _UDT_coerceObject(Type self, jobject jstr); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java index ea38e435..c9ce3592 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java @@ -20,6 +20,7 @@ import java.security.PrivilegedAction; import java.sql.SQLData; +import java.sql.SQLException; import java.sql.SQLInput; import java.sql.SQLOutput; @@ -70,7 +71,10 @@ private EntryPoints() { } - private static final MethodType s_expectedType = methodType(Object.class); + private static final MethodType s_generalType = methodType(Object.class); + private static final MethodType s_udtCtor = methodType(SQLData.class); + private static final MethodType s_udtParse = + methodType(SQLData.class, String.class, String.class); /** * Wrap a {@code MethodHandle} in an {@code Invocable} suitable for @@ -83,11 +87,15 @@ private EntryPoints() * constructed to return null, storing any primitive value returned into * the first static primitive-parameter slot. */ - static Invocable invocable(MethodHandle mh, AccessControlContext acc) + static Invocable invocable(MethodHandle mh, AccessControlContext acc) { - if ( ! s_expectedType.equals(mh.type()) ) + if ( null == mh + || s_udtCtor.equals(mh.type()) || s_udtParse.equals(mh.type()) ) + return new Invocable(mh, acc); + + if ( ! s_generalType.equals(mh.type()) ) throw new IllegalArgumentException( - "invocable() requires a MethodHandle with type ()Object"); + "invocable() passed a MethodHandle with unexpected type"); /* * The EntryPoints class is specially loaded early in PL/Java's startup @@ -113,7 +121,7 @@ static Invocable invocable(MethodHandle mh, AccessControlContext acc) } }; - return new Invocable(a, acc); + return new Invocable>(a, acc); } /** @@ -124,61 +132,121 @@ static Invocable invocable(MethodHandle mh, AccessControlContext acc) * has void type or returns a primitive (which will have been returned in * the first static primitive parameter slot). */ - private static Object invoke(Invocable target) + private static Object invoke(Invocable> target) throws Throwable { - try - { - return doPrivileged(target.action, target.acc); - } - catch ( UncheckedException e ) - { - throw e.unwrap(); - } + assert PrivilegedAction.class.isInstance(target.payload); + + return doPrivilegedAndUnwrap(target.payload, target.acc); } /** * Entry point for calling the {@code writeSQL} method of a UDT. + *

    + * Like {@code udtReadInvoke}, this is a distinct entry point in order to + * avoid use of the static parameter area. While this operation is not + * expected during preparation of a function's parameter list, it can occur + * during a function's execution, if it stores values of user-defined type + * into result sets, prepared-statement bindings, etc. Such uses are not + * individually surrounded by {@code pushInvocation}/{@code popInvocation} + * as ordinary function calls are, and the {@code ParameterFrame} save and + * restore mechanism relies on those, so it is better for this entry point + * also to be handled specially. + * @param t Invocable carrying the appropriate AccessControlContext (t's + * action is unused and expected to be null) * @param o the UDT instance * @param stream the SQLOutput stream on which the type's internal * representation will be written */ - private static void udtWriteInvoke(SQLData o, SQLOutput stream) + private static void udtWriteInvoke( + Invocable target, SQLData o, SQLOutput stream) throws Throwable { - o.writeSQL(stream); + PrivilegedAction action = () -> + { + try + { + o.writeSQL(stream); + return null; + } + catch ( SQLException e ) + { + throw unchecked(e); + } + }; + + doPrivilegedAndUnwrap(action, target.acc); } /** * Entry point for calling the {@code toString} method of a UDT. + *

    + * This can be called during transformation of a UDT that has a + * NUL-terminated storage form, and without being separately wrapped in + * {@code pushInvocation}/{@code popInvocation}, so it gets its own entry + * point here to avoid use of the static parameter area. + * @param target Invocable carrying the appropriate AccessControlContext + * (target's action is unused and expected to be null) * @param o the UDT instance * @return the UDT's text representation */ - private static String udtToStringInvoke(SQLData o) + private static String udtToStringInvoke(Invocable target, SQLData o) { - return o.toString(); + PrivilegedAction action = () -> + { + return o.toString(); + }; + + return doPrivileged(action, target.acc); } /** * Entry point for calling the {@code readSQL} method of a UDT, after * constructing an instance first. - * @param mh a MethodHandle that composes the allocation of a new instance - * by its no-arg constructor with the call of readSQL. + *

    + * This gets its own entry point so parameters can be passed to it + * independently of the static parameter area used for ordinary function + * invocations. Should an ordinary function have a parameter that is of a + * user-defined type, this entry point is used to instantiate the Java + * form of that parameter during the assembly of the function's + * parameter list, so the static area is not touched here. + * @param target an Invocable that returns a new instance, on which readSQL + * will then be called. The Invocable's access control context will be in + * effect for both operations. * @param stream the SQLInput stream from which to read the UDT's internal * representation * @param typeName the SQL type name to be associated with the instance * @return the allocated and initialized instance */ private static SQLData udtReadInvoke( - MethodHandle mh, SQLInput stream, String typeName) + Invocable target, SQLInput stream, String typeName) throws Throwable { - return (SQLData)mh.invokeExact(stream, typeName); + PrivilegedAction action = () -> + { + try + { + SQLData o = (SQLData)target.payload.invokeExact(); + o.readSQL(stream, typeName); + return o; + } + catch ( Throwable t ) + { + throw unchecked(t); + } + }; + + return doPrivilegedAndUnwrap(action, target.acc); } /** * Entry point for calling the {@code parse} method of a UDT, which will * construct an instance given its text representation. + *

    + * This can be called during transformation of a UDT that has a + * NUL-terminated storage form, and without being separately wrapped in + * {@code pushInvocation}/{@code popInvocation}, so it gets its own entry + * point here to avoid use of the static parameter area. * @param mh a MethodHandle to the class's static parse method, which will * allocate and return an instance. * @param textRep the text representation @@ -186,20 +254,57 @@ private static SQLData udtReadInvoke( * @return the allocated and initialized instance */ private static SQLData udtParseInvoke( - MethodHandle mh, String textRep, String typeName) + Invocable target, String textRep, String typeName) + throws Throwable + { + PrivilegedAction action = () -> + { + try + { + return (SQLData)target.payload.invokeExact(textRep, typeName); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + }; + + return doPrivilegedAndUnwrap(action, target.acc); + } + + /** + * Factors out the common {@code doPrivileged} and unwrapping of possible + * wrapped checked exceptions for the above entry points. + */ + private static T doPrivilegedAndUnwrap( + PrivilegedAction action, AccessControlContext context) throws Throwable { - return (SQLData)mh.invokeExact(textRep, typeName); + try + { + return doPrivileged(action, context); + } + catch ( UncheckedException e ) + { + throw e.unwrap(); + } } - static final class Invocable + /** + * A class carrying a payload of some kind and an access control context + * to impose when it is invoked. + *

    + * The type of the payload will be specific to which entry point above + * will be used to invoke it. + */ + static final class Invocable { - final PrivilegedAction action; + final T payload; final AccessControlContext acc; - Invocable(PrivilegedAction action, AccessControlContext acc) + Invocable(T payload, AccessControlContext acc) { - this.action = requireNonNull(action); + this.payload = payload; this.acc = acc; } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 5b264a63..c7e5f510 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -565,7 +565,6 @@ else if ( rt.isPrimitive() ) private static final MethodHandle s_paramCountsAre; private static final MethodHandle s_countsZeroer; private static final int s_sizeof_jvalue = 8; // Function.c StaticAssertStmt - private static final MethodHandle s_readSQL_mh; /** * An {@code AccessControlContext} representing "nobody special": it should @@ -592,7 +591,10 @@ else if ( rt.isPrimitive() ) * desired invocation targets. Such a constructed method handle will take * care of pushing the arguments from the appropriate static slots. Because * that happens before the target is invoked, and calls must happen on the - * PG thread, the static areas are safe for reentrant calls. + * PG thread, the static areas are safe for reentrant calls (but for an edge + * case involving UDTs among the parameters and a UDT function that incurs + * a reentrant call; it may never happen, but that's what the ParameterFrame + * class is for, below). * * Such a constructed method handle will be passed, wrapped in * an Invocable, to EntryPoints.invoke, which is declared to return @@ -791,9 +793,6 @@ private static void pop() insertArguments(mh, 0, (byte)1), 0, boolean.class), dropArguments( insertArguments(mh, 0, (byte)0), 0, boolean.class)); - - s_readSQL_mh = l.findVirtual(SQLData.class, "readSQL", - methodType(void.class, SQLInput.class, String.class)); } catch ( ReflectiveOperationException e ) { @@ -864,14 +863,53 @@ private static void paramCountsAre(short counts) } /** - * Return a "construct+readSQL" method handle for a given UDT class. + * Return an {@code Invocable} for the {@code writeSQL} method of + * a given UDT class. + *

    + * While this is not expected to be used while transforming parameters for + * another function call as the UDT-read handle would be, it can still be + * used during a function's execution and without being separately wrapped + * in {@code pushInvocation}/{@code popInvocation}. The pushing and popping + * of {@code ParameterFrame} rely on invocation scoping, so it is better for + * the UDT-write method also to avoid using the static parameter area. + *

    + * The access control context of the {@code Invocable} returned here is used + * at the corresponding entry point; the payload is not. + */ + private static Invocable udtWriteHandle(Class clazz) + throws SQLException + { + return invocable(null, accessControlContextFor(clazz)); + } + + /** + * Return an {@code Invocable} for the {@code toString} method of + * a given UDT class (or any class, really). *

    - * The method handle will expect the two non-receiver arguments for - * {@code readSQL}, construct a new instance of the class, invoke - * {@code readSQL} on that instance and the two supplied arguments, - * and return the instance. + * The access control context of the {@code Invocable} returned here is used + * at the corresponding entry point; the payload is not. */ - private static MethodHandle udtReadHandle(Class clazz) + private static Invocable udtToStringHandle(Class clazz) + throws SQLException + { + return invocable(null, accessControlContextFor(clazz)); + } + + /** + * Return a special {@code Invocable} for the {@code readSQL} method of + * a given UDT class. + *

    + * Because this can commonly be invoked while transforming parameters for + * another function call, it has a dedicated corresponding + * {@code EntryPoints} method and does not use the static parameter area. + * The {@code Invocable} created here is bound to the constructor of the + * type, takes no parameters, and simply returns the constructed instance; + * the {@code EntryPoints} method will then call {@code readSQL} on it and + * pass the stream and type-name arguments. The {@code AccessControlContext} + * assigned here will be in effect for both the constructor and the + * {@code readSQL} call. + */ + private static Invocable udtReadHandle(Class clazz) throws SQLException { Lookup l = lookupFor(clazz); @@ -879,7 +917,9 @@ private static MethodHandle udtReadHandle(Class clazz) try { - ctor = l.findConstructor(clazz, methodType(void.class)); + ctor = + l.findConstructor(clazz, methodType(void.class)) + .asType(methodType(SQLData.class)); // invocable() enforces this } catch ( ReflectiveOperationException e ) { @@ -888,29 +928,32 @@ private static MethodHandle udtReadHandle(Class clazz) " must have a no-argument public constructor", "38000", e); } - MethodHandle mh = identity(SQLData.class); // o -> o - mh = collectArguments(mh, 1, s_readSQL_mh); // (o, o, stream, type) -> o - mh = permuteArguments(mh, methodType( - SQLData.class, SQLData.class, SQLInput.class, String.class), - 0, 0, 1, 2); // (o, stream, type) -> o - ctor = ctor.asType(methodType(SQLData.class)); - mh = collectArguments(mh, 0, ctor); // (stream, type) -> o - return mh; + return invocable(ctor, accessControlContextFor(clazz)); } /** - * Return a "parse" method handle for a given UDT class. + * Return a "parse" {@code Invocable} for a given UDT class. + *

    + * The method can be invoked during the preparation of a parameter that has + * a NUL-terminated storage form, so it gets its own dedicated entry point + * and does not use the static parameter area. */ - private static MethodHandle udtParseHandle(Class clazz) + private static Invocable udtParseHandle(Class clazz) throws SQLException { Lookup l = lookupFor(clazz); try { - return l.findStatic(clazz, "parse", - methodType(clazz, String.class, String.class)) - .asType(methodType(SQLData.class, String.class, String.class)); + MethodHandle mh = + l.findStatic(clazz, "parse", + methodType(clazz, String.class, String.class)); + + return + invocable( + mh.asType(mh.type().changeReturnType(SQLData.class)), + accessControlContextFor(clazz) + ); } catch ( ReflectiveOperationException e ) { @@ -976,7 +1019,6 @@ private static Invocable init( Map> typeMap = null; String className = info.group("udtcls"); boolean isUDT = (null != className); - AccessControlContext acc = null; // assume no lid needed on permissions if ( ! isUDT ) { @@ -989,10 +1031,6 @@ private static Invocable init( ClassLoader schemaLoader = Loader.getSchemaLoader(schema); Class clazz = loadClass(schemaLoader, className); - if ( clazz != Commands.class - && ! (clazz.getClassLoader() instanceof Loader) ) - acc = s_lid; // put a lid on permissions if calling JRE directly - if ( isUDT ) { setupUDT(wrappedPtr, info, procTup, schemaLoader, @@ -1032,7 +1070,7 @@ private static Invocable init( resolvedTypes, retTypeIsOutParameter, isMultiCall) .asFixedArity() ), - acc + accessControlContextFor(clazz) ); } @@ -1051,6 +1089,27 @@ private static boolean isTrigger(ResultSet procTup) procTup.getInt("prorettype"); // type Oid, but implements Number } + /** + * Select the {@code AccessControlContext} to be in effect when invoking + * a function. + *

    + * At present, the only choices are null (no additional restrictions) when + * the target class is in a PL/Java-loaded jar file, or the 'lid' when + * invoking anything else (such as code of the JRE itself, which would + * otherwise have all permissions). The 'lid' is constructed to be 'nobody + * special', so will have only those permissions the policy grants without + * conditions. No exception is made here for the few functions supplied by + * PL/Java's own {@code Commands} class; they get a lid. It is reasonable to + * ask them to use {@code doPrivileged} when appropriate. + */ + private static AccessControlContext accessControlContextFor(Class clazz) + { + if ( clazz.getClassLoader() instanceof Loader ) + return null; // policy already applies appropriate permissions + + return s_lid; // put a lid on permissions if calling JRE directly + } + /** * The initialization specific to a UDT function. */ @@ -1077,12 +1136,20 @@ private static void setupUDT( throw new SQLException("internal error in PL/Java UDT parsing"); } - MethodHandle parseMH = 'i' == udtInitial ? udtParseHandle(clazz) : null; - MethodHandle readMH = udtReadHandle(clazz); + Invocable readMH = udtReadHandle(clazz); + Invocable writeMH = udtWriteHandle(clazz); + /* + * A Base UDT will have an 'i' (parse) and an 'o' (toString) function + * to register. We'll look them both up here when resolving the 'i' + * case, and they'll be registered at one time, saving upcalls if the + * 'i' case is first to be resolved. + */ + Invocable parseMH = 'i' == udtInitial? udtParseHandle(clazz) :null; + Invocable toStringMH = 'i' == udtInitial? udtToStringHandle(clazz):null; doInPG(() -> _storeToUDT(wrappedPtr, schemaLoader, clazz, readOnly, udtInitial, udtId.intValue(), - parseMH, readMH)); + parseMH, readMH, writeMH, toStringMH)); } /** @@ -1568,7 +1635,8 @@ private static native void _storeToUDT( long wrappedPtr, ClassLoader schemaLoader, Class clazz, boolean readOnly, int funcInitial, int udtOid, - MethodHandle readMH, MethodHandle parseMH); + Invocable readMH, Invocable parseMH, + Invocable writeMH, Invocable toStringMH); private static native void _reconcileTypes( long wrappedPtr, String[] resolvedTypes, String[] explicitTypes, int i); From cccf995627d70c2bcc8f3633d635b0c2332ba5fe Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 18 Sep 2020 18:25:04 -0400 Subject: [PATCH 0761/1087] Bring Iterator-based SRFs into the scheme The Invocable for such an SRF will return another Invocable. That's what will be called through the iterate/close steps of SRF evaluation; its AccessControlContext will be the same one that was selected for the SRF itself. Still to do: the same trick for ResultSetProvider-based SRFs (and for ResultSetHandle-based ones, which just need an addition in the MethodHandle tree to instantiate a ResultSetPicker). Completed, this will obviate the need for Type_hasNextSRF/ nextSRF/closeSRF to be "virtual methods" of Type; the choice of behavior will just be made up front when building the method handle. --- pljava-so/src/main/c/Function.c | 20 ++ pljava-so/src/main/c/type/Type.c | 14 +- pljava-so/src/main/include/pljava/Function.h | 4 + .../pljava/internal/EntryPoints.java | 16 +- .../postgresql/pljava/internal/Function.java | 185 ++++++++++++++++-- 5 files changed, 212 insertions(+), 27 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 019d8187..0bac98db 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -426,6 +426,26 @@ static void reserveParameterFrame(jsize refArgCount, jsize primArgCount) *s_countCheck = newCounts; } +/* + * Invoke an Invocable that was obtained by invoking an Invocable for a + * set-returning-function that returns results in value-per-call style. + * Pass true for 'close' when no more results are wanted. Will always overwrite + * *result; check the boolean return value to determine whether that is a real + * result (true) or the end of results was reached (false). + */ +jboolean pljava_Function_vpcInvoke( + jobject invocable, jobject rowcollect, jlong call_cntr, jboolean close, + jobject *result) +{ + reserveParameterFrame(1, 2); + JNI_setObjectArrayElement(s_referenceParameters, 0, rowcollect); + s_primitiveParameters[0].j = call_cntr; + s_primitiveParameters[1].z = close; + *result = JNI_callStaticObjectMethod(s_EntryPoints_class, + s_EntryPoints_invoke, invocable); + return s_primitiveParameters[0].z; +} + void pljava_Function_udtWriteInvoke( jobject invocable, jobject value, jobject stream) { diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index f01c7395..83bb1e05 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -775,22 +775,26 @@ static jobject _Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) return 0; } +static jobject testingStash; + static bool _Type_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jlong callCounter) { - return (JNI_callBooleanMethod(rowProducer, s_Iterator_hasNext) == JNI_TRUE); + return (JNI_TRUE == pljava_Function_vpcInvoke( + rowProducer, rowCollector, callCounter, JNI_FALSE, &testingStash)); } static Datum _Type_nextSRF(Type self, jobject rowProducer, jobject rowCollector) { - /* XXX make an entry point */ - jobject tmp = JNI_callObjectMethod(rowProducer, s_Iterator_next); - Datum result = Type_coerceObject(self, tmp); - JNI_deleteLocalRef(tmp); + /* XXX make an entry point + jobject tmp = JNI_callObjectMethod(rowProducer, s_Iterator_next); */ + Datum result = Type_coerceObject(self, testingStash); + JNI_deleteLocalRef(testingStash); return result; } static void _Type_closeSRF(Type self, jobject rowProducer) { + pljava_Function_vpcInvoke(rowProducer, NULL, 0, JNI_TRUE, &testingStash); } jobject Type_getSRFProducer(Type self, Function fn) diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index dd76fb09..5df70478 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -107,6 +107,10 @@ extern jfloat pljava_Function_floatInvoke(Function self); extern jlong pljava_Function_longInvoke(Function self); extern jdouble pljava_Function_doubleInvoke(Function self); +extern jboolean pljava_Function_vpcInvoke( + jobject invocable, jobject rowcollect, jlong call_cntr, jboolean close, + jobject *result); + extern void pljava_Function_udtWriteInvoke( jobject invocable, jobject value, jobject stream); extern jstring pljava_Function_udtToStringInvoke( diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java index c9ce3592..cc29e21b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java @@ -71,7 +71,8 @@ private EntryPoints() { } - private static final MethodType s_generalType = methodType(Object.class); + private static final MethodType s_generalType = + methodType(Object.class, AccessControlContext.class); private static final MethodType s_udtCtor = methodType(SQLData.class); private static final MethodType s_udtParse = methodType(SQLData.class, String.class, String.class); @@ -80,12 +81,19 @@ private EntryPoints() * Wrap a {@code MethodHandle} in an {@code Invocable} suitable for * passing directly to {@link #invoke invoke()}. *

    - * The supplied method handle must have type {@code ()Object}, and fetch any + * The supplied method handle must have type + * {@code (AccessControlContext)Object}, and fetch any * parameter values needed by its target from bound-in references to the * static reference and primitive parameter areas. If its ultimate target * has {@code void} or a primitive return type, the handle must be * constructed to return null, storing any primitive value returned into * the first static primitive-parameter slot. + *

    + * The {@code AccessControlContext} passed to the handle will be the same + * one under which it will be invoked, and so can be ignored in most cases. + * A handle for a value-per-call set-returning function can copy it into the + * {@code Invocable} that it creates from this function's result, to be used + * for iteratively retrieving the results. */ static Invocable invocable(MethodHandle mh, AccessControlContext acc) { @@ -113,7 +121,7 @@ static Invocable invocable(MethodHandle mh, AccessControlContext acc) { try { - return mh.invokeExact(); + return mh.invokeExact(acc); } catch ( Throwable t ) { @@ -126,7 +134,7 @@ static Invocable invocable(MethodHandle mh, AccessControlContext acc) /** * Entry point for a general PL/Java function. - * @param target PrivilegedAction obtained from Function.create that will + * @param target Invocable obtained from Function.create that will * push the actual parameters and call the target method. * @return The value returned by the target method, or null if the method * has void type or returns a primitive (which will have been returned in diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index c7e5f510..3258f213 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -19,6 +19,7 @@ import static java.lang.invoke.MethodHandles.collectArguments; import static java.lang.invoke.MethodHandles.dropArguments; import static java.lang.invoke.MethodHandles.empty; +import static java.lang.invoke.MethodHandles.exactInvoker; import static java.lang.invoke.MethodHandles.filterArguments; import static java.lang.invoke.MethodHandles.filterReturnValue; import static java.lang.invoke.MethodHandles.foldArguments; @@ -63,6 +64,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.util.regex.Pattern.compile; @@ -72,6 +74,7 @@ import org.postgresql.pljava.sqlgen.Lexicals.Identifier; import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.EntryPoints; import org.postgresql.pljava.internal.EntryPoints.Invocable; import static org.postgresql.pljava.internal.EntryPoints.invocable; import static org.postgresql.pljava.jdbc.TypeOid.INVALID; @@ -359,7 +362,7 @@ private static SQLException memberException( * incoming parameters into an {@code Object} array for all those of * reference type, and a C array of {@code jvalue} for the primitives. * Those arrays are static, and will be bound into the method handle - * produced here, from which it will fetch values when invoked. + * produced here, which will fetch values from them when invoked. *

    * The job of this method is to take any static method handle {@code mh} and * return a method handle that takes no parameters, and invokes the original @@ -564,6 +567,15 @@ else if ( rt.isPrimitive() ) private static final MethodHandle s_primitiveZeroer; private static final MethodHandle s_paramCountsAre; private static final MethodHandle s_countsZeroer; + private static final MethodHandle s_nonNull; + + /* + * Handles that fit the SFRM_ValuePerCall entry point, from a function that + * returns an Iterator or ResultSetProvider, respectively. + */ + private static final MethodHandle s_iteratorVPC; + private static final MethodHandle s_resultSetProviderVPC; + private static final int s_sizeof_jvalue = 8; // Function.c StaticAssertStmt /** @@ -793,21 +805,154 @@ private static void pop() insertArguments(mh, 0, (byte)1), 0, boolean.class), dropArguments( insertArguments(mh, 0, (byte)0), 0, boolean.class)); + + s_referenceNuller = + insertArguments( + arrayElementSetter(Object[].class), 2, (Object)null) + .bindTo(s_referenceParameters); + + s_primitiveZeroer = insertArguments(longSetter, 1, 0L); + + s_countsZeroer = + insertArguments(s_primitiveZeroer, 0, s_offset_paramCounts); + + s_nonNull = l.findStatic(Objects.class, "nonNull", + methodType(boolean.class, Object.class)); + + /* + * Build a bit of MethodHandle tree for invoking a set-returning + * user function that will implement the ValuePerCall protocol. + * Such a function will return either an Iterator or a + * ResultSetProvider (or a ResultSetHandle, more on that further + * below). Its MethodHandle, as obtained from AdaptHandle, of course + * has type ()Object (changed to (acc)Object before init() returns + * it, but ordinarily it will ignore the acc passed to it). + * + * The handle tree being built here will go on top of that, and will + * also ultimately have type (acc)Object. What it returns will be + * a new Invocable, carrying the same acc, and a handle tree built + * over the Iterator or ResultSetProvider that the user function + * returned. Part of that tree must depend on whether the return + * type is Iterator or ResultSet; the part being built here is the + * common part. It will have an extra first argument of type + * MethodHandle that can be bound to the ResultSetProvider- or + * Iterator-specific handle. If the user function returns null, so + * will this. + */ + + MethodHandle invocableMH = + myL.findStatic(EntryPoints.class, "invocable", methodType( + Invocable.class, + MethodHandle.class, AccessControlContext.class)); + + mh = l.findVirtual(MethodHandle.class, "bindTo", + methodType(MethodHandle.class, Object.class)); + + mh = collectArguments(invocableMH, 0, mh); + // (hdl, obj, acc) -> Invocable(hdl-bound-to-obj, acc) + + mh = guardWithTest(dropArguments(s_nonNull, 0, MethodHandle.class), + mh, empty(methodType(Invocable.class, + MethodHandle.class, Object.class, AccessControlContext.class + )) + ); + + mh = filterArguments(mh, 1, exactInvoker(methodType(Object.class))); + /* + * We are left with type (MethodHandle,MethodHandle,acc) -> + * Invocable. A first bindTo, passing the ResultSetProvider- or + * Iterator-specific tree fragment, will leave us with + * (MethodHandle,acc)Invocable, and that can be bound to any + * set-returning user function handle, leaving (acc)Invocable, which + * is just what we want. Keep this as vpcCommon (only erasing its + * return type to Object as EntryPoints.invoke will expect). + */ + MethodHandle vpcCommon = + mh.asType(mh.type().changeReturnType(Object.class)); + + /* + * Build the ValuePerCall adapter handle for a function that + * returns Iterator. ValuePerCall adapters will be invoked through + * the general mechanism, and fetch their arguments from the static + * area. They'll be passed one reference argument (a "row collector" + * for the ResultSetProvider case) and two primitives: a long + * call counter (zero on the first call) and a boolean (true when + * the caller wants to stop iteration, perhaps early). An Iterator + * has no use for the row collector or the call counter, so they + * simply won't be fetched; the end-iteration boolean will be + * fetched and will cause false+null to be returned, but will not + * necessarily release any resources promptly, as Iterator has no + * close() method. + * + * These adapters change up the return-value protocol a bit: they + * will return a reference (the value for the row) *and also* a + * boolean via the first primitive slot (false if the end of rows + * has been reached, in which case the reference returned is simply + * null and is not part of the result set). If the boolean is true + * and null is returned, the null is part of the result. + * + * mh1 and mh2 both have type (Iterator)Object and side effect of + * storing to primitive slot 0. Let mh1 be the hasNext case, + * returning a value and storing true, and mh2 the no-more case, + * storing false and returning null. (They don't have a primitive- + * zeroer for either argument, as the return will clobber the first + * slot anyway, and they can only be reached if the 'close' argument + * is already zero. This is the Iterator case, so the row-collector + * reference argument is assumed already null.) + * + * Start with a few constants for parameter getters (else it is + * easy to forget the (sizeof jvalue) for the primitive getters!). + */ + final int REF0 = 0; + final int REF1 = 1; + final int PRIM0 = 0; + final int PRIM1 = 1 * s_sizeof_jvalue; + + MethodHandle mh1 = identity(Object.class); + mh1 = dropArguments(mh1, 1, Object.class); // the null from boolRet + mh1 = collectArguments(mh1, 1, + insertArguments(s_booleanReturn, 0, true)); + mt = methodType(Object.class); + mh = l.findVirtual(Iterator.class, "next", mt); + mh1 = filterArguments(mh1, 0, mh); + + MethodHandle mh2 = insertArguments(s_booleanReturn, 0, false); + mh2 = dropArguments(mh2, 0, Iterator.class); + + mt = methodType(boolean.class); + mh = l.findVirtual(Iterator.class, "hasNext", mt); + mh = guardWithTest(mh, mh1, mh2); + mh = foldArguments(mh, 0, s_countsZeroer); + + /* + * The next (in construction order; first in execution) test is of + * the 'close' argument. Tack a primitiveZeroer onto mh2 for this + * one, as it'll execute in the argument-isn't-zero case. + */ + mh2 = foldArguments(mh2, 0, + insertArguments(s_primitiveZeroer, 0, PRIM1)); + mh2 = foldArguments(mh2, 0, s_countsZeroer); + + mh = guardWithTest( + insertArguments(s_booleanGetter, 0, PRIM1), mh2, mh); + + /* + * mh now has type (Iterator)Object. Erase the Iterator to Object + * (so this and the ResultSetProvider one can have a common type), + * give it an acc argument that it will ignore, bind it into + * vpcCommon, and we'll have the Iterator VPC adapter. + */ + mh = mh.asType(mh.type().erase()); + mh = dropArguments(mh, 1, AccessControlContext.class); + s_iteratorVPC = vpcCommon.bindTo(mh); + + s_resultSetProviderVPC = null; } catch ( ReflectiveOperationException e ) { throw new ExceptionInInitializerError(e); } - s_referenceNuller = - insertArguments(arrayElementSetter(Object[].class), 2, (Object)null) - .bindTo(s_referenceParameters); - - s_primitiveZeroer = insertArguments(longSetter, 1, 0L); - - s_countsZeroer = - insertArguments(s_primitiveZeroer, 0, s_offset_paramCounts); - /* * The lid is a "nobody special" AccessControlContext: it isn't allowed * any permission that isn't granted by the Policy to everybody. @@ -1063,15 +1208,19 @@ private static Invocable init( String methodName = info.group("meth"); - return - invocable( - adaptHandle( - getMethodHandle(schemaLoader, clazz, methodName, - resolvedTypes, retTypeIsOutParameter, isMultiCall) - .asFixedArity() - ), - accessControlContextFor(clazz) + MethodHandle handle = + adaptHandle( + getMethodHandle(schemaLoader, clazz, methodName, + resolvedTypes, retTypeIsOutParameter, isMultiCall) + .asFixedArity() ); + + if ( isMultiCall && ! retTypeIsOutParameter ) + handle = s_iteratorVPC.bindTo(handle); + else + handle = dropArguments(handle, 0, AccessControlContext.class); + + return invocable(handle, accessControlContextFor(clazz)); } /** From eedf157920fb74c77c23c7b9503d2a9b950c7b24 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 10:42:57 -0400 Subject: [PATCH 0762/1087] Bring ResultSet{Provider,Handle} SRFs into scheme 'Type' still has two SRF-related "virtual methods", one to make a "row collector" (Composite uses one, nobody else does), and one to get a Datum from whatever the row producer returned (Composite ignores whatever was returned and relies on the row collector instead, everybody else just passes the returned value to coerceObject and makes no use of a row collector). --- .../postgresql/pljava/ResultSetProvider.java | 63 +++++++++++++- pljava-so/src/main/c/Function.c | 11 +++ pljava-so/src/main/c/type/Composite.c | 41 +-------- pljava-so/src/main/c/type/Type.c | 71 ++++------------ pljava-so/src/main/include/pljava/Function.h | 7 ++ pljava-so/src/main/include/pljava/type/Type.h | 21 +---- .../src/main/include/pljava/type/Type_priv.h | 5 +- .../postgresql/pljava/internal/Function.java | 85 +++++++++++++++++-- .../pljava/internal/ResultSetPicker.java | 18 ++-- 9 files changed, 193 insertions(+), 129 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java index e19d4db8..294b8133 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/ResultSetProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -14,6 +14,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLNonTransientException; /** * An implementation of this interface is returned from functions and procedures @@ -36,7 +37,7 @@ public interface ResultSetProvider * This method is called once for each row that should be returned from * a procedure that returns a set of rows. The receiver * is a {@code SingleRowWriter} - * instance that is used for capturing the data for the row. + * instance that is used to capture the data for the row. *

    * If the return type is {@code RECORD} rather than a specific complex type, * SQL requires a column definition list to follow any use of the function @@ -45,6 +46,11 @@ public interface ResultSetProvider * and types of the columns expected by the caller. (It can also be used in * the case of a specific complex type, but in that case the names and types * are probably already known.) + *

    + * This default implementation calls + * {@link #assignRowValues(ResultSet,int)}, or throws an + * {@code SQLException} with SQLState 54000 (program limit exceeded) if + * the row number exceeds {@code Integer.MAX_VALUE}. * @param receiver Receiver of values for the given row. * @param currentRow Row number, zero on the first call, incremented by one * on each subsequent call. @@ -52,6 +58,21 @@ public interface ResultSetProvider * if not (end of data). * @throws SQLException */ + default boolean assignRowValues(ResultSet receiver, long currentRow) + throws SQLException + { + if ( currentRow <= Integer.MAX_VALUE ) + return assignRowValues(receiver, (int)currentRow); + throw new SQLNonTransientException( + getClass().getCanonicalName() + + " implements only the assignRowValues method limited to" + + " Integer.MAX_VALUE rows; this result set is too large", "54000"); + } + + /** + * Older version where currentRow is limited to the range + * of {@code int}. + */ boolean assignRowValues(ResultSet receiver, int currentRow) throws SQLException; @@ -61,4 +82,42 @@ boolean assignRowValues(ResultSet receiver, int currentRow) */ void close() throws SQLException; + + /** + * Version of {@code ResultSetProvider} where the {@code assignRowValues} + * method accepting a {@code long} row count must be implemented, and the + * {@code int} version defaults to using it. + */ + interface Large extends ResultSetProvider + { + /** + * This method is called once for each row that should be returned + * from a procedure that returns a set of rows. The receiver is a + * {@code SingleRowWriter} instance that is used to capture the data + * for the row. + *

    + * If the return type is {@code RECORD} rather than a specific complex + * type, SQL requires a column definition list to follow any use of the + * function in a query. + * The {@link ResultSet#getMetaData() ResultSetMetaData} + * of {@code receiver} can be used here to learn the number, names, and + * types of the columns expected by the caller. (It can also be used in + * the case of a specific complex type, but in that case the names and + * types are probably already known.) + * @param receiver Receiver of values for the given row. + * @param currentRow Row number, zero on the first call, incremented by one + * on each subsequent call. + * @return {@code true} if a new row was provided, {@code false} + * if not (end of data). + * @throws SQLException + */ + boolean assignRowValues(ResultSet receiver, long currentRow) + throws SQLException; + + default boolean assignRowValues(ResultSet receiver, int currentRow) + throws SQLException + { + return assignRowValues(receiver, (long)currentRow); + } + } } diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 0bac98db..4f643e79 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -437,6 +437,17 @@ jboolean pljava_Function_vpcInvoke( jobject invocable, jobject rowcollect, jlong call_cntr, jboolean close, jobject *result) { + /* + * When retrieving the very first row, this call happens under the same + * Invocation as the call to the user function itself that returned this + * invocable (and may, rarely, have pushed a ParameterFrame). What does + * the reservation below imply for ParameterFrame management? + * + * It's ok, because the user function's invocation will have cleared the + * static area parameter counts; this reservation will therefore not see a + * need to push a frame. If one was pushed for the user function itself, it + * remains on top, to be popped when the Invocation is. + */ reserveParameterFrame(1, 2); JNI_setObjectArrayElement(s_referenceParameters, 0, rowcollect); s_primitiveParameters[0].j = call_cntr; diff --git a/pljava-so/src/main/c/type/Composite.c b/pljava-so/src/main/c/type/Composite.c index 80c11a12..f53b4ecb 100644 --- a/pljava-so/src/main/c/type/Composite.c +++ b/pljava-so/src/main/c/type/Composite.c @@ -110,18 +110,6 @@ static Datum _Composite_invoke(Type self, Function fn, PG_FUNCTION_ARGS) return result; } -static jobject _Composite_getSRFProducer(Type self, Function fn) -{ - jobject tmp = pljava_Function_refInvoke(fn); - if(tmp != 0 && JNI_isInstanceOf(tmp, s_ResultSetHandle_class)) - { - jobject wrapper = JNI_newObject(s_ResultSetPicker_class, s_ResultSetPicker_init, tmp); - JNI_deleteLocalRef(tmp); - tmp = wrapper; - } - return tmp; -} - static jobject _Composite_getSRFCollector(Type self, PG_FUNCTION_ARGS) { jobject tmp1; @@ -136,23 +124,8 @@ static jobject _Composite_getSRFCollector(Type self, PG_FUNCTION_ARGS) return tmp2; } -static bool _Composite_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jlong callCounter) -{ - /* Obtain next row using the RowCollector as a parameter to the - * ResultSetProvider.assignRowValues method. - */ - if ( callCounter > PG_INT32_MAX ) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("the ResultSetProvider cannot return more than " - "INT32_MAX rows"))); - return (JNI_callBooleanMethod(rowProducer, - s_ResultSetProvider_assignRowValues, - rowCollector, - (jint)callCounter) == JNI_TRUE); -} - -static Datum _Composite_nextSRF(Type self, jobject rowProducer, jobject rowCollector) +static Datum _Composite_datumFromSRF( + Type self, jobject row, jobject rowCollector) { Datum result = 0; HeapTuple tuple = _getTupleAndClear(rowCollector); @@ -161,11 +134,6 @@ static Datum _Composite_nextSRF(Type self, jobject rowProducer, jobject rowColle return result; } -static void _Composite_closeSRF(Type self, jobject rowProducer) -{ - JNI_callVoidMethod(rowProducer, s_ResultSetProvider_close); -} - /* Assume that the Datum is a HeapTupleHeader and convert it into * a SingleRowReader instance. */ @@ -266,11 +234,8 @@ void Composite_initialize(void) s_CompositeClass->getTupleDesc = _Composite_getTupleDesc; s_CompositeClass->coerceDatum = _Composite_coerceDatum; s_CompositeClass->invoke = _Composite_invoke; - s_CompositeClass->getSRFProducer = _Composite_getSRFProducer; s_CompositeClass->getSRFCollector = _Composite_getSRFCollector; - s_CompositeClass->hasNextSRF = _Composite_hasNextSRF; - s_CompositeClass->nextSRF = _Composite_nextSRF; - s_CompositeClass->closeSRF = _Composite_closeSRF; + s_CompositeClass->datumFromSRF = _Composite_datumFromSRF; s_CompositeClass->outParameter = true; Type_registerType2(InvalidOid, "java.sql.ResultSet", Composite_obtain); diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 83bb1e05..b32fe1bf 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -159,10 +159,11 @@ static void stashCallContext(CallContextData *ctxData) */ static void _closeIteration(CallContextData* ctxData) { + jobject dummy; currentInvocation->hasConnected = ctxData->hasConnected; currentInvocation->invocation = ctxData->invocation; - Type_closeSRF(ctxData->elemType, ctxData->rowProducer); + pljava_Function_vpcInvoke(ctxData->rowProducer, NULL, 0, JNI_TRUE, &dummy); JNI_deleteGlobalRef(ctxData->rowProducer); if(ctxData->rowCollector != 0) JNI_deleteGlobalRef(ctxData->rowCollector); @@ -456,7 +457,7 @@ Datum Type_invoke(Type self, Function fn, PG_FUNCTION_ARGS) Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) { - bool hasRow; + jobject row; CallContextData* ctxData; FuncCallContext* context; MemoryContext currCtx; @@ -483,10 +484,10 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) */ currCtx = MemoryContextSwitchTo(context->multi_call_memory_ctx); - /* Call the declared Java function. It returns an instance that can produce - * the rows. + /* Call the declared Java function. It returns an instance + * that can produce the rows. */ - tmp = Type_getSRFProducer(self, fn); + tmp = pljava_Function_refInvoke(fn); if(tmp == 0) { Invocation_assertDisconnect(); @@ -541,12 +542,12 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) currentInvocation->hasConnected = ctxData->hasConnected; currentInvocation->invocation = ctxData->invocation; - hasRow = Type_hasNextSRF(self, ctxData->rowProducer, ctxData->rowCollector, - (jlong)context->call_cntr); - - if(hasRow) + if(JNI_TRUE == pljava_Function_vpcInvoke( + ctxData->rowProducer, ctxData->rowCollector, (jlong)context->call_cntr, + JNI_FALSE, &row)) { - Datum result = Type_nextSRF(self, ctxData->rowProducer, ctxData->rowCollector); + Datum result = Type_datumFromSRF(self, row, ctxData->rowCollector); + JNI_deleteLocalRef(row); stashCallContext(ctxData); currentInvocation->hasConnected = false; currentInvocation->invocation = 0; @@ -765,41 +766,14 @@ static Type _Type_createArrayType(Type self, Oid arrayTypeId) return Array_fromOid(arrayTypeId, self); } -static jobject _Type_getSRFProducer(Type self, Function fn) -{ - return pljava_Function_refInvoke(fn); -} - static jobject _Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) { return 0; } -static jobject testingStash; - -static bool _Type_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jlong callCounter) -{ - return (JNI_TRUE == pljava_Function_vpcInvoke( - rowProducer, rowCollector, callCounter, JNI_FALSE, &testingStash)); -} - -static Datum _Type_nextSRF(Type self, jobject rowProducer, jobject rowCollector) -{ - /* XXX make an entry point - jobject tmp = JNI_callObjectMethod(rowProducer, s_Iterator_next); */ - Datum result = Type_coerceObject(self, testingStash); - JNI_deleteLocalRef(testingStash); - return result; -} - -static void _Type_closeSRF(Type self, jobject rowProducer) +static Datum _Type_datumFromSRF(Type self, jobject row, jobject rowCollector) { - pljava_Function_vpcInvoke(rowProducer, NULL, 0, JNI_TRUE, &testingStash); -} - -jobject Type_getSRFProducer(Type self, Function fn) -{ - return self->typeClass->getSRFProducer(self, fn); + return Type_coerceObject(self, row); } jobject Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) @@ -807,19 +781,9 @@ jobject Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) return self->typeClass->getSRFCollector(self, fcinfo); } -bool Type_hasNextSRF(Type self, jobject rowProducer, jobject rowCollector, jlong callCounter) -{ - return self->typeClass->hasNextSRF(self, rowProducer, rowCollector, callCounter); -} - -Datum Type_nextSRF(Type self, jobject rowProducer, jobject rowCollector) -{ - return self->typeClass->nextSRF(self, rowProducer, rowCollector); -} - -void Type_closeSRF(Type self, jobject rowProducer) +Datum Type_datumFromSRF(Type self, jobject row, jobject rowCollector) { - self->typeClass->closeSRF(self, rowProducer); + return self->typeClass->datumFromSRF(self, row, rowCollector); } static Type _Type_getRealType(Type self, Oid realId, jobject typeMap) @@ -1014,11 +978,8 @@ TypeClass TypeClass_alloc2(const char* typeName, Size classSize, Size instanceSi self->coerceObject = (ObjectCoercer)_PgObject_pureVirtualCalled; self->createArrayType = _Type_createArrayType; self->invoke = _Type_invoke; - self->getSRFProducer = _Type_getSRFProducer; self->getSRFCollector = _Type_getSRFCollector; - self->hasNextSRF = _Type_hasNextSRF; - self->nextSRF = _Type_nextSRF; - self->closeSRF = _Type_closeSRF; + self->datumFromSRF = _Type_datumFromSRF; self->getTupleDesc = _Type_getTupleDesc; self->getJNISignature = _Type_getJNISignature; self->dynamic = false; diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 5df70478..2a646c93 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -107,6 +107,13 @@ extern jfloat pljava_Function_floatInvoke(Function self); extern jlong pljava_Function_longInvoke(Function self); extern jdouble pljava_Function_doubleInvoke(Function self); +/* + * Call the invocable that was returned by the invocation of a set-returning + * user function that observes the SFRM_ValuePerCall protocol. Call with + * close == JNI_FALSE to retrieve the next row if any, JNI_TRUE when done (which + * may be before all rows have been retrieved). Returns JNI_TRUE/JNI_FALSE to + * indicate whether a row was retrieved, AND puts a value (or null) in *result. + */ extern jboolean pljava_Function_vpcInvoke( jobject invocable, jobject rowcollect, jlong call_cntr, jboolean close, jobject *result); diff --git a/pljava-so/src/main/include/pljava/type/Type.h b/pljava-so/src/main/include/pljava/type/Type.h index 87fc56bd..72170d98 100644 --- a/pljava-so/src/main/include/pljava/type/Type.h +++ b/pljava-so/src/main/include/pljava/type/Type.h @@ -199,12 +199,6 @@ extern Oid Type_getOid(Type self); */ extern TupleDesc Type_getTupleDesc(Type self, PG_FUNCTION_ARGS); -/* - * Obtains the Java object that acts as the SRF producer. This instance will be - * called once for each row that should be produced. - */ -extern jobject Type_getSRFProducer(Type self, Function fn); - /* * Obtains the optional Java object that will act as the value collector for * the SRF producer. The collector typically manifest itself as an OUT @@ -213,19 +207,10 @@ extern jobject Type_getSRFProducer(Type self, Function fn); extern jobject Type_getSRFCollector(Type self, PG_FUNCTION_ARGS); /* - * Called to determine if the producer will produce another row. - */ -extern bool Type_hasNextSRF(Type self, jobject producer, jobject collector, jlong counter); - -/* - * Converts the next row into a Datum of the expected type. - */ -extern Datum Type_nextSRF(Type self, jobject producer, jobject collector); - -/* - * Called at the end of an SRF iteration. + * Return a Datum of the expected type, from the row collector (if any) and/or + * the value returned by the row producer. */ -extern void Type_closeSRF(Type self, jobject producer); +extern Datum Type_datumFromSRF(Type self, jobject row, jobject rowCollector); /* * Function used when obtaining a type based on an Oid diff --git a/pljava-so/src/main/include/pljava/type/Type_priv.h b/pljava-so/src/main/include/pljava/type/Type_priv.h index b6eb9de9..1b118f3d 100644 --- a/pljava-so/src/main/include/pljava/type/Type_priv.h +++ b/pljava-so/src/main/include/pljava/type/Type_priv.h @@ -107,11 +107,8 @@ struct TypeClass_ */ Datum (*invoke)(Type self, Function fn, PG_FUNCTION_ARGS); - jobject (*getSRFProducer)(Type self, Function fn); jobject (*getSRFCollector)(Type self, PG_FUNCTION_ARGS); - bool (*hasNextSRF)(Type self, jobject producer, jobject collector, jlong counter); - Datum (*nextSRF)(Type self, jobject producer, jobject collector); - void (*closeSRF)(Type self, jobject producer); + Datum (*datumFromSRF)(Type self, jobject row, jobject collector); const char* (*getJNISignature)(Type self); /* diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 3258f213..eb76e1f0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -316,7 +316,9 @@ private static MethodHandle getMethodHandle( isMultiCall, true); // this time altForm = true try { - return lookupFor(clazz).findStatic(clazz, methodName, mt); + MethodHandle h = + lookupFor(clazz).findStatic(clazz, methodName, mt); + return filterReturnValue(h, s_wrapWithPicker); } catch ( ReflectiveOperationException e ) { @@ -570,11 +572,14 @@ else if ( rt.isPrimitive() ) private static final MethodHandle s_nonNull; /* - * Handles that fit the SFRM_ValuePerCall entry point, from a function that - * returns an Iterator or ResultSetProvider, respectively. + * Handles used to retrieve rows using SFRM_ValuePerCall protocol, from a + * function that returns an Iterator or ResultSetProvider, respectively. + * (One that returns a ResultSetHandle gets its return value wrapped with a + * ResultSetPicker and is then treated as in the ResultSetProvider case.) */ private static final MethodHandle s_iteratorVPC; private static final MethodHandle s_resultSetProviderVPC; + private static final MethodHandle s_wrapWithPicker; private static final int s_sizeof_jvalue = 8; // Function.c StaticAssertStmt @@ -871,6 +876,8 @@ mh, empty(methodType(Invocable.class, mh.asType(mh.type().changeReturnType(Object.class)); /* + * VALUE-PER-CALL Iterator DRIVER + * * Build the ValuePerCall adapter handle for a function that * returns Iterator. ValuePerCall adapters will be invoked through * the general mechanism, and fetch their arguments from the static @@ -946,7 +953,71 @@ mh, empty(methodType(Invocable.class, mh = dropArguments(mh, 1, AccessControlContext.class); s_iteratorVPC = vpcCommon.bindTo(mh); - s_resultSetProviderVPC = null; + /* + * VALUE-PER-CALL ResultSetProvider DRIVER + * + * The same drill as above, only to drive a ResultSetProvider. + * For now, this will always return a null reference, even when + * a row is retrieved; the thing it would return is just the + * row collector, which the C caller already has, and must extract + * the tuple from. If that could be done in Java, it would be + * a different story. + */ + mt = methodType(boolean.class, ResultSet.class, long.class); + mh1 = collectArguments(s_booleanReturn, 0, + l.findVirtual(ResultSetProvider.class, "assignRowValues", mt)); + mh1 = dropArguments(mh1, 0, boolean.class); + + /* + * The next (in construction order; first in execution) test is of + * the 'close' argument. If it is true, use mh2 to zero that prim + * slot, call close, and return false. + */ + mh2 = insertArguments(s_booleanReturn, 0, false); + mh2 = collectArguments(mh2, 0, + l.findVirtual(ResultSetProvider.class, "close", + methodType(void.class))); + mh2 = foldArguments(mh2, 0, + insertArguments(s_primitiveZeroer, 0, PRIM1)); + mh2 = dropArguments(mh2, 0, boolean.class); + mh2 = dropArguments(mh2, 2, ResultSet.class, long.class); + + mh = guardWithTest(identity(boolean.class), mh2, mh1); + mh = foldArguments(mh, 0, s_countsZeroer); + mh = foldArguments(mh, 0, + insertArguments(s_booleanGetter, 0, PRIM1)); + // ^^^ Test the 'close' flag, prim slot 1 (insert as arg 0) ^^^ + + mh = foldArguments(mh, 2, insertArguments(s_longGetter, 0, PRIM0)); + // ^^^ Get the row count, prim slot 0; return will clobber ^^^ + + /* + * mh now has type (ResultSetProvider,ResultSet)Object. Erase both + * argument types to Object now (so the ResultSet will match the + * refGetter here, and the result will be (Object)Object as expected + * below. + */ + mh = mh.asType(mh.type().erase()); + mh = foldArguments(mh, 1, + insertArguments(s_referenceNuller, 0, REF0)); + mh = foldArguments(mh, 1, insertArguments(s_refGetter, 0, REF0)); + // ^^^ Get and then null the row collector, ref slot 0 ^^^ + + /* + * mh now has type (Object)Object. Give it an acc argument that it + * will ignore, bind it into vpcCommon, and we'll have the + * ResultSetProvider VPC adapter. + */ + mh = dropArguments(mh, 1, AccessControlContext.class); + s_resultSetProviderVPC = vpcCommon.bindTo(mh); + + /* + * WRAPPER for ResultSetHandle to present it as ResultSetProvider + */ + mt = methodType(void.class, ResultSetHandle.class); + mh = myL.findConstructor(ResultSetPicker.class, mt); + s_wrapWithPicker = + mh.asType(mh.type().changeReturnType(ResultSetProvider.class)); } catch ( ReflectiveOperationException e ) { @@ -1215,8 +1286,10 @@ private static Invocable init( .asFixedArity() ); - if ( isMultiCall && ! retTypeIsOutParameter ) - handle = s_iteratorVPC.bindTo(handle); + if ( isMultiCall ) + handle = ( + retTypeIsOutParameter ? s_resultSetProviderVPC : s_iteratorVPC + ).bindTo(handle); else handle = dropArguments(handle, 0, AccessControlContext.class); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java b/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java index 6752e93d..f1d7ae3c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ResultSetPicker.java @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root directory of this distribution or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 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: + * Tada AB - Thomas Hallgren + * Chapman Flack */ package org.postgresql.pljava.internal; @@ -28,7 +34,7 @@ * should be declared to return {@code ResultSetProvider} and do this work * itself. */ -public class ResultSetPicker implements ResultSetProvider +public class ResultSetPicker implements ResultSetProvider.Large { private final ResultSetHandle m_resultSetHandle; private final ResultSet m_resultSet; @@ -40,7 +46,7 @@ public ResultSetPicker(ResultSetHandle resultSetHandle) m_resultSet = resultSetHandle.getResultSet(); } - public boolean assignRowValues(ResultSet receiver, int currentRow) + public boolean assignRowValues(ResultSet receiver, long currentRow) throws SQLException { if(m_resultSet == null || !m_resultSet.next()) From e399ef3ef13bef833d90e3aacf933632fa211e15 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 20 Sep 2020 13:31:01 -0400 Subject: [PATCH 0763/1087] Cover transaction and savepoint listeners These will have an AccessControlContext saved when they are registered and used when they are invoked. The EntryPoints.Invocable class is used here for convenience, though invocation is done directly, without going through any EntryPoints method. The saved AccessControlContext is going to include pljava.internal's protection domain anyway (without a lot of work to avoid it); using an EntryPoints method wouldn't help. A listener that wants to do something that is permitted to its own jar but not to PL/Java will have to accept the inconvenience of using doPrivileged. In passing, adds a Checked.BiConsumer, as it may be useful elsewhere. (The four-argument consumer needed in SubXactListener is just declared there as a private interface.) --- .../java/org/postgresql/pljava/Session.java | 34 +++++---- .../postgresql/pljava/internal/Checked.java | 41 +++++++++++ .../pljava/internal/SubXactListener.java | 73 ++++++++++++++----- .../pljava/internal/XactListener.java | 59 ++++++++++----- 4 files changed, 154 insertions(+), 53 deletions(-) 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 04c1818f..5706b8fb 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-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -12,6 +12,8 @@ */ package org.postgresql.pljava; +import java.security.AccessControlContext; // linked from javadoc + import java.sql.Connection; import java.sql.SQLException; @@ -34,17 +36,23 @@ public interface Session { /** - * Adds the specified listener to the list of listeners that will - * receive savepoint events. This method does nothing if the listener - * was already added. + * Adds the specified {@code listener} to the list of listeners that will + * receive savepoint events. An {@link AccessControlContext} saved by this + * method will be used when the listener is invoked. If the listener was + * already registered, it remains registered just once, though the + * {@code AccessControlContext} is updated and its order of invocation + * relative to other listeners may change. * @param listener The listener to be added. */ void addSavepointListener(SavepointListener listener); /** - * Adds the specified listener to the list of listeners that will - * receive transactional events. This method does nothing if the listener - * was already added. + * Adds the specified {@code listener} to the list of listeners that will + * receive transaction events. An {@link AccessControlContext} saved by this + * method will be used when the listener is invoked. If the listener was + * already registered, it remains registered just once, though the + * {@code AccessControlContext} is updated and its order of invocation + * relative to other listeners may change. * @param listener The listener to be added. */ void addTransactionListener(TransactionListener listener); @@ -162,17 +170,17 @@ void executeAsSessionUser(Connection conn, String statement) void removeAttribute(String attributeName); /** - * Removes the specified listener from the list of listeners that will - * receive savepoint events. This method does nothing unless the listener is - * found. + * Removes the specified {@code listener} from the list of listeners that + * will receive savepoint events. This method does nothing unless + * the listener is found. * @param listener The listener to be removed. */ void removeSavepointListener(SavepointListener listener); /** - * Removes the specified listener from the list of listeners that will - * receive transactional events. This method does nothing unless the listener is - * found. + * Removes the specified {@code listener} from the list of listeners that + * will receive transaction events. This method does nothing unless + * the listener is found. * @param listener The listener to be removed. */ void removeTransactionListener(TransactionListener listener); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java index 064818e6..05b2e295 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java @@ -202,6 +202,18 @@ Function composed( return t -> after.apply(first.apply(t)); } + static + BiConsumer composed( + BiConsumer first, + BiConsumer after) + { + return (t, u) -> + { + first.accept(t, u); + after.accept(t, u); + }; + } + static Consumer composed( Consumer first, @@ -840,6 +852,35 @@ ToFloatFunction use(ToFloatFunction o) * Consumers that have checked-exception-less counterparts in the Java API. */ + @FunctionalInterface + interface BiConsumer + extends Checked, E> + { + void accept(T t, U u) throws E; + + @Override + default java.util.function.BiConsumer ederWrap() + { + return (t, u) -> + { + try + { + accept(t, u); + } + catch ( Throwable thw ) + { + throw Checked.ederThrow(thw); + } + }; + } + + static BiConsumer + use(BiConsumer o) + { + return o; + } + } + @FunctionalInterface interface Consumer extends Checked, E> diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java index c5feb815..6407fa57 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -12,58 +12,91 @@ */ package org.postgresql.pljava.internal; +import org.postgresql.pljava.SavepointListener; +import org.postgresql.pljava.Session; + import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.EntryPoints.Invocable; +import static org.postgresql.pljava.internal.Privilege.doPrivileged; + +import static java.security.AccessController.getContext; +import java.sql.Savepoint; import java.sql.SQLException; + import java.util.ArrayDeque; import java.util.Deque; +import static java.util.Objects.requireNonNull; -import org.postgresql.pljava.SavepointListener; - +import static java.util.stream.Collectors.toList; /** - * Class that enables registrations using the PostgreSQL RegisterSubXactCallback - * function. + * Class that enables registrations using the PostgreSQL + * {@code RegisterSubXactCallback} function. * * @author Thomas Hallgren */ class SubXactListener { - private static final Deque s_listeners = + @FunctionalInterface + private interface Target + { + void accept(SavepointListener l, Session s, Savepoint sp, Savepoint p) + throws SQLException; + } + + /* + * A non-thread-safe Deque; will be made safe by doing all mutations on the + * PG thread (even though actually calling into PG is necessary only when + * the size changes from 0 to 1 or 1 to 0). + */ + private static final Deque> s_listeners = new ArrayDeque<>(); static void onAbort(PgSavepoint sp, PgSavepoint parent) throws SQLException { - // Take a snapshot. Handlers might unregister during event processing - for ( SavepointListener listener : - s_listeners.toArray(new SavepointListener[s_listeners.size()]) ) - listener.onAbort(Backend.getSession(), sp, parent); + invokeListeners(SavepointListener::onAbort, sp, parent); } static void onCommit(PgSavepoint sp, PgSavepoint parent) throws SQLException { - for ( SavepointListener listener : - s_listeners.toArray(new SavepointListener[s_listeners.size()]) ) - listener.onCommit(Backend.getSession(), sp, parent); + invokeListeners(SavepointListener::onCommit, sp, parent); } static void onStart(PgSavepoint sp, PgSavepoint parent) throws SQLException { - for ( SavepointListener listener : - s_listeners.toArray(new SavepointListener[s_listeners.size()]) ) - listener.onStart(Backend.getSession(), sp, parent); + invokeListeners(SavepointListener::onStart, sp, parent); + } + + private static void invokeListeners( + Target target, Savepoint sp, Savepoint parent) + throws SQLException + { + Session session = Backend.getSession(); + + // Take a snapshot. Handlers might unregister during event processing + for ( Invocable listener : + s_listeners.stream().collect(toList()) ) + { + doPrivileged(() -> + { + target.accept(listener.payload, session, sp, parent); + }, listener.acc); + } } static void addListener(SavepointListener listener) { + Invocable invocable = + new Invocable<>(requireNonNull(listener), getContext()); + doInPG(() -> { - if ( s_listeners.contains(listener) ) - return; - s_listeners.push(listener); + s_listeners.removeIf(v -> v.payload.equals(listener)); + s_listeners.push(invocable); if( 1 == s_listeners.size() ) _register(); }); @@ -73,7 +106,7 @@ static void removeListener(SavepointListener listener) { doInPG(() -> { - if ( ! s_listeners.remove(listener) ) + if ( ! s_listeners.removeIf(v -> v.payload.equals(listener)) ) return; if ( 0 == s_listeners.size() ) _unregister(); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java index 1e972c08..67863f17 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -12,18 +12,25 @@ */ package org.postgresql.pljava.internal; +import org.postgresql.pljava.TransactionListener; + import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.EntryPoints.Invocable; +import static org.postgresql.pljava.internal.Privilege.doPrivileged; + +import static java.security.AccessController.getContext; import java.sql.SQLException; + import java.util.ArrayDeque; import java.util.Deque; +import static java.util.Objects.requireNonNull; -import org.postgresql.pljava.TransactionListener; - +import static java.util.stream.Collectors.toList; /** - * Class that enables registrations using the PostgreSQL RegisterXactCallback - * function. + * Class that enables registrations using the PostgreSQL + * {@code RegisterXactCallback} function. * * @author Thomas Hallgren */ @@ -34,38 +41,50 @@ class XactListener * PG thread (even though actually calling into PG is necessary only when * the size changes from 0 to 1 or 1 to 0). */ - private static final Deque s_listeners = + private static final Deque> s_listeners = new ArrayDeque<>(); static void onAbort() throws SQLException { - // Take a snapshot. Handlers might unregister during event processing - for ( TransactionListener listener : - s_listeners.toArray(new TransactionListener[s_listeners.size()]) ) - listener.onAbort(Backend.getSession()); + invokeListeners(TransactionListener::onAbort); } static void onCommit() throws SQLException { - for ( TransactionListener listener : - s_listeners.toArray(new TransactionListener[s_listeners.size()]) ) - listener.onCommit(Backend.getSession()); + invokeListeners(TransactionListener::onCommit); } static void onPrepare() throws SQLException { - for ( TransactionListener listener : - s_listeners.toArray(new TransactionListener[s_listeners.size()]) ) - listener.onPrepare(Backend.getSession()); + invokeListeners(TransactionListener::onPrepare); + } + + private static void invokeListeners( + Checked.BiConsumer target) + throws SQLException + { + Session session = Backend.getSession(); + + // Take a snapshot. Handlers might unregister during event processing + for ( Invocable listener : + s_listeners.stream().collect(toList()) ) + { + doPrivileged(() -> + { + target.accept(listener.payload, session); + }, listener.acc); + } } static void addListener(TransactionListener listener) { + Invocable invocable = + new Invocable<>(requireNonNull(listener), getContext()); + doInPG(() -> { - if ( s_listeners.contains(listener) ) - return; - s_listeners.push(listener); + s_listeners.removeIf(v -> v.payload.equals(listener)); + s_listeners.push(invocable); if( 1 == s_listeners.size() ) _register(); }); @@ -75,7 +94,7 @@ static void removeListener(TransactionListener listener) { doInPG(() -> { - if ( ! s_listeners.remove(listener) ) + if ( ! s_listeners.removeIf(v -> v.payload.equals(listener)) ) return; if ( 0 == s_listeners.size() ) _unregister(); From b4e6dccfe7aa8c7daa69098097ef314a36706ac3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 22 Sep 2020 20:55:05 -0400 Subject: [PATCH 0764/1087] Handle newer transaction/subtransaction events It may finally be time to do away with those unhandled-case warnings the C compiler has been producing since PostgreSQL 9.3. Now that interface default methods can be used, this can be done without breaking legacy code. --- .../postgresql/pljava/SavepointListener.java | 37 +++++++++---- .../pljava/TransactionListener.java | 29 ++++++++--- pljava-so/src/main/c/SubXactListener.c | 50 +++++++++--------- pljava-so/src/main/c/XactListener.c | 52 ++++++++++++------- .../pljava/internal/SubXactListener.java | 39 +++++++------- .../pljava/internal/XactListener.java | 48 +++++++++++------ 6 files changed, 156 insertions(+), 99 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/SavepointListener.java b/pljava-api/src/main/java/org/postgresql/pljava/SavepointListener.java index 478844fa..967ff307 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/SavepointListener.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/SavepointListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -9,6 +9,7 @@ * Contributors: * Tada AB * Purdue University + * Chapman Flack */ package org.postgresql.pljava; @@ -16,10 +17,11 @@ import java.sql.Savepoint; /** - * Interface for a listener to be notified of the start and commit or abort of - * savepoints. To receive such notifications, implement this interface, with - * the three methods that will be called in those three cases, and pass an - * instance to {@link Session#addSavepointListener}. + * Interface for a listener to be notified of the start and pre-commit, commit, + * or abort of savepoints. To receive such notifications, implement this + * interface, with the methods that will be called in the cases of interest, + * and pass an instance to {@link Session#addSavepointListener}. The default + * implementations of these methods do nothing. *

    * It is possible for a listener method to be called with savepoint * null, or parent null, or both; that can happen if the application @@ -35,12 +37,25 @@ */ public interface SavepointListener { - void onAbort(Session session, Savepoint savepoint, Savepoint parent) - throws SQLException; + default void onAbort(Session session, Savepoint savepoint, Savepoint parent) + throws SQLException + { + } - void onCommit(Session session, Savepoint savepoint, Savepoint parent) - throws SQLException; + default void onCommit( + Session session, Savepoint savepoint, Savepoint parent) + throws SQLException + { + } - void onStart(Session session, Savepoint savepoint, Savepoint parent) - throws SQLException; + default void onStart(Session session, Savepoint savepoint, Savepoint parent) + throws SQLException + { + } + + default void onPreCommit( + Session session, Savepoint savepoint, Savepoint parent) + throws SQLException + { + } } diff --git a/pljava-api/src/main/java/org/postgresql/pljava/TransactionListener.java b/pljava-api/src/main/java/org/postgresql/pljava/TransactionListener.java index d8598708..65e18edc 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/TransactionListener.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/TransactionListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -9,16 +9,19 @@ * Contributors: * Tada AB * Purdue University + * Chapman Flack */ package org.postgresql.pljava; import java.sql.SQLException; /** - * Interface for a listener to be notified of prepare, and commit or abort, of - * distributed transactions. To receive such notifications, implement this - * interface, with the three methods that will be called in those three cases, - * and pass an instance to {@link Session#addTransactionListener}. + * Interface for a listener to be notified of prepare, and commit, abort, + * or other phase transitions, of distributed transactions. To receive + * such notifications, implement this interface, with the methods that + * will be called in the cases of interest, and pass an instance to + * {@link Session#addTransactionListener}. The default implementations of these + * methods do nothing. * * TransactionListener exposes a * static jclass s_SubXactListener_class; -static jmethodID s_SubXactListener_onStart; -static jmethodID s_SubXactListener_onCommit; -static jmethodID s_SubXactListener_onAbort; +static jmethodID s_SubXactListener_invokeListeners; -static void subXactCB(SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, void* arg) +static void subXactCB( + SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, + void* arg) { /* * Map the subids to PgSavepoints first - this function upcalls into Java @@ -41,20 +41,19 @@ static void subXactCB(SubXactEvent event, SubTransactionId mySubid, SubTransacti * thread from stepping in. These methods should not blindly assert * Backend.threadMayEnterPG(), as for some java_thread_pg_entry settings it * won't be true. This is the legacy behavior, so not changed for 1.5.x. + * + * The event ordinal can simply be passed to Java, as long as upstream + * hasn't changed the order; list the known ones in a switch, for a better + * chance that a clever compiler will warn if upstream has added any. */ switch(event) { case SUBXACT_EVENT_START_SUB: - JNI_callStaticVoidMethod(s_SubXactListener_class, - s_SubXactListener_onStart, sp, parent); - break; case SUBXACT_EVENT_COMMIT_SUB: - JNI_callStaticVoidMethod(s_SubXactListener_class, - s_SubXactListener_onCommit, sp, parent); - break; case SUBXACT_EVENT_ABORT_SUB: + case SUBXACT_EVENT_PRE_COMMIT_SUB: JNI_callStaticVoidMethod(s_SubXactListener_class, - s_SubXactListener_onAbort, sp, parent); + s_SubXactListener_invokeListeners, (jint)event, sp, parent); } } @@ -77,19 +76,22 @@ void SubXactListener_initialize(void) PgObject_registerNatives("org/postgresql/pljava/internal/SubXactListener", methods); - s_SubXactListener_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/SubXactListener")); - s_SubXactListener_onAbort = - PgObject_getStaticJavaMethod(s_SubXactListener_class, "onAbort", - "(Lorg/postgresql/pljava/internal/PgSavepoint;" - "Lorg/postgresql/pljava/internal/PgSavepoint;)V"); - s_SubXactListener_onCommit = - PgObject_getStaticJavaMethod(s_SubXactListener_class, "onCommit", - "(Lorg/postgresql/pljava/internal/PgSavepoint;" - "Lorg/postgresql/pljava/internal/PgSavepoint;)V"); - s_SubXactListener_onStart = - PgObject_getStaticJavaMethod(s_SubXactListener_class, "onStart", - "(Lorg/postgresql/pljava/internal/PgSavepoint;" + s_SubXactListener_class = JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/SubXactListener")); + s_SubXactListener_invokeListeners = + PgObject_getStaticJavaMethod(s_SubXactListener_class, "invokeListeners", + "(ILorg/postgresql/pljava/internal/PgSavepoint;" "Lorg/postgresql/pljava/internal/PgSavepoint;)V"); + +#define CONFIRMCONST(c) \ +StaticAssertStmt((SUBXACT_EVENT_##c) == \ +(org_postgresql_pljava_internal_SubXactListener_##c), \ + "Java/C value mismatch for " #c) + + CONFIRMCONST( START_SUB ); + CONFIRMCONST( COMMIT_SUB ); + CONFIRMCONST( ABORT_SUB ); + CONFIRMCONST( PRE_COMMIT_SUB ); } /* diff --git a/pljava-so/src/main/c/XactListener.c b/pljava-so/src/main/c/XactListener.c index a6e30dca..543c2cd4 100644 --- a/pljava-so/src/main/c/XactListener.c +++ b/pljava-so/src/main/c/XactListener.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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,27 +17,39 @@ #include "access/xact.h" static jclass s_XactListener_class; -static jmethodID s_XactListener_onAbort; -static jmethodID s_XactListener_onCommit; -static jmethodID s_XactListener_onPrepare; +static jmethodID s_XactListener_invokeListeners; static void xactCB(XactEvent event, void* arg) { + /* + * Upstream has, regrettably, not merely added events over the years, but + * changed their order, so a mapping is needed. Use a switch with the known + * cases enumerated, to improve the chance that a clever compiler will warn + * if yet more have been added, and initialize 'mapped' to a value that the + * Java code won't mistake for a real one. + */ +#define CASE(c) \ +case XACT_EVENT_##c: \ + mapped = org_postgresql_pljava_internal_XactListener_##c; \ + break + + jint mapped = -1; switch(event) { - case XACT_EVENT_ABORT: - JNI_callStaticVoidMethod(s_XactListener_class, - s_XactListener_onAbort); - break; - case XACT_EVENT_COMMIT: - JNI_callStaticVoidMethod(s_XactListener_class, - s_XactListener_onCommit); - break; - case XACT_EVENT_PREPARE: - JNI_callStaticVoidMethod(s_XactListener_class, - s_XactListener_onPrepare); - break; + CASE( COMMIT ); + CASE( ABORT ); + CASE( PREPARE ); + CASE( PRE_COMMIT ); + CASE( PRE_PREPARE ); +#if PG_VERSION_NUM >= 90500 + CASE( PARALLEL_COMMIT ); + CASE( PARALLEL_ABORT ); + CASE( PARALLEL_PRE_COMMIT ); +#endif } + + JNI_callStaticVoidMethod(s_XactListener_class, + s_XactListener_invokeListeners, mapped); } extern void XactListener_initialize(void); @@ -59,10 +71,10 @@ void XactListener_initialize(void) PgObject_registerNatives("org/postgresql/pljava/internal/XactListener", methods); - s_XactListener_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/XactListener")); - s_XactListener_onAbort = PgObject_getStaticJavaMethod(s_XactListener_class, "onAbort", "()V"); - s_XactListener_onCommit = PgObject_getStaticJavaMethod(s_XactListener_class, "onCommit", "()V"); - s_XactListener_onPrepare = PgObject_getStaticJavaMethod(s_XactListener_class, "onPrepare", "()V"); + s_XactListener_class = JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/internal/XactListener")); + s_XactListener_invokeListeners = PgObject_getStaticJavaMethod( + s_XactListener_class, "invokeListeners", "(I)V"); } /* diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java index 6407fa57..7dfa837f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java @@ -45,6 +45,24 @@ void accept(SavepointListener l, Session s, Savepoint sp, Savepoint p) throws SQLException; } + /* + * These must match the values of the PostgreSQL enum; StaticAssertStmt + * is used in the C source to produce errors (from compilers with the + * feature) if they do not. + */ + private static final int START_SUB = 0; + private static final int COMMIT_SUB = 1; + private static final int ABORT_SUB = 2; + private static final int PRE_COMMIT_SUB = 3; + + private static final Target[] s_refs = + { + SavepointListener::onStart, + SavepointListener::onCommit, + SavepointListener::onAbort, + SavepointListener::onPreCommit + }; + /* * A non-thread-safe Deque; will be made safe by doing all mutations on the * PG thread (even though actually calling into PG is necessary only when @@ -53,28 +71,11 @@ void accept(SavepointListener l, Session s, Savepoint sp, Savepoint p) private static final Deque> s_listeners = new ArrayDeque<>(); - static void onAbort(PgSavepoint sp, PgSavepoint parent) - throws SQLException - { - invokeListeners(SavepointListener::onAbort, sp, parent); - } - - static void onCommit(PgSavepoint sp, PgSavepoint parent) - throws SQLException - { - invokeListeners(SavepointListener::onCommit, sp, parent); - } - - static void onStart(PgSavepoint sp, PgSavepoint parent) - throws SQLException - { - invokeListeners(SavepointListener::onStart, sp, parent); - } - private static void invokeListeners( - Target target, Savepoint sp, Savepoint parent) + int eventIndex, PgSavepoint sp, PgSavepoint parent) throws SQLException { + Target target = s_refs[eventIndex]; Session session = Backend.getSession(); // Take a snapshot. Handlers might unregister during event processing diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java index 67863f17..f24fda5f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java @@ -24,6 +24,7 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.List; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; @@ -36,6 +37,33 @@ */ class XactListener { + /* + * These do not need to match the values of the PostgreSQL enum (which, over + * the years, has had members not merely added but reordered). The C code + * will map those to these. + */ + private static final int COMMIT = 0; + private static final int ABORT = 1; + private static final int PREPARE = 2; + private static final int PRE_COMMIT = 3; + private static final int PRE_PREPARE = 4; + private static final int PARALLEL_COMMIT = 5; + private static final int PARALLEL_ABORT = 6; + private static final int PARALLEL_PRE_COMMIT = 7; + + private static final + List> s_refs = + List.of( + TransactionListener::onCommit, + TransactionListener::onAbort, + TransactionListener::onPrepare, + TransactionListener::onPreCommit, + TransactionListener::onPrePrepare, + TransactionListener::onParallelCommit, + TransactionListener::onParallelAbort, + TransactionListener::onParallelPreCommit + ); + /* * A non-thread-safe Deque; will be made safe by doing all mutations on the * PG thread (even though actually calling into PG is necessary only when @@ -44,25 +72,11 @@ class XactListener private static final Deque> s_listeners = new ArrayDeque<>(); - static void onAbort() throws SQLException - { - invokeListeners(TransactionListener::onAbort); - } - - static void onCommit() throws SQLException - { - invokeListeners(TransactionListener::onCommit); - } - - static void onPrepare() throws SQLException - { - invokeListeners(TransactionListener::onPrepare); - } - - private static void invokeListeners( - Checked.BiConsumer target) + private static void invokeListeners(int eventIndex) throws SQLException { + Checked.BiConsumer target = + s_refs.get(eventIndex); Session session = Backend.getSession(); // Take a snapshot. Handlers might unregister during event processing From 3ca34a011726300cfdc03f0e91abc155193f308d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Oct 2020 17:37:50 -0400 Subject: [PATCH 0765/1087] Restore the trusted/untrusted language distinction The distinction that was wiped out in 18abf79 is restored here by introducing a special Principal, PLPrincipal, with its two concrete subclasses Sandboxed and Unsandboxed, one of which will be in the AccessControlContext for an executing function and can be granted permissions in the policy file. Hence, what PostgreSQL's 'trusted' and 'untrusted' mean for a PL can both be freely configured for PL/Java via the policy file. The initial grants in this policy are simply FilePermission "<>", "read,readlink,write,delete" to Unsandboxed, and nothing special to Sandboxed. That reflects a typical filesystem-centric view of what trusted/untrusted ought to mean, but of course other permissions in the extensive Java permissions model can be selectively granted as the needs of an application dictate. The use of Sandboxed/Unsandboxed in place of PostgreSQL's trusted/ untrusted follows the usage in @Function(trust=...) since c6805e5, discussed in [1], with the aim of being unambiguous both to people who are or who aren't long-time users of the PostgreSQL nomenclature. A Java Principal has both a class and a name; the PLPrincipal.Sandboxed or .Unsandboxed in an executing function's AccessControlContext will carry the name of the PL, as given to PostgreSQL's CREATE LANGUAGE and used in the function's declaration. A grant in the policy file can have the form grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed * { ... }; to define permissions for any Unsandboxed function with no regard to the name of the language, or a form like grant principal org.postgresql.pljava.PLPrincipal$Sandboxed "java" { ... }; The two CREATE LANGUAGE commands normally issued when PL/Java is installed will create a sandboxed language named java and an unsandboxed one named javau. It is also possible, with additional CREATE LANGUAGE commands, to create more named language entries that share PL/Java's handlers, and can be used to declare Java functions. CREATE TRUSTED LANGUAGE java_net HANDLER java_call_handler VALIDATOR java_validator; would create an entry that could be given extra networking permissions with a grant to PLPrincipal$Sandboxed "java_net" (and would also have any permissions granted to PLPrincipal$Sandboxed *). One gap in the scheme pertains to the readSQL and writeSQL methods of "mapped UDTs", that is, user-defined types that are not implementing new PostgreSQL base types. These methods are called by PL/Java without being declared as functions to SQL, and therefore without SQL function declarations to name a language or intended trust. Such methods will execute with no PLPrincipal in their AccessControlContext, and therefore with only the permissions the policy grants specifically to their containing jar, or to all. The input/output/read/write functions of a "base UDT" do have SQL function declarations, and will be executed with the declared language name and trust, like any ordinary function. Recognizing this fundamental difference between the UDT types did have the effect of simplifying a bunch of code around UDTs that had grown a bit hairy, especially in bbbb75c. TransactionListeners and SavepointListeners will execute with the AccessControlContext that was in force when they were registered, although their permissions may be limited by intersection with those the policy grants to PL/Java itself. If such a listener needs to exercise a privilege that is not granted to PL/Java, it will have to wrap the exercise in doPrivileged. [1] https://www.postgresql.org/message-id/56759A57.9060106%40anastigmatix.net --- .../org/postgresql/pljava/BasePrincipal.java | 87 ++++++++ .../org/postgresql/pljava/PLPrincipal.java | 89 +++++++++ .../postgresql/pljava/sqlgen/Lexicals.java | 16 ++ .../src/main/resources/pljava.policy | 22 +++ pljava-so/src/main/c/Backend.c | 7 +- pljava-so/src/main/c/Function.c | 186 +++++++++++++----- pljava-so/src/main/c/InstallHelper.c | 22 ++- pljava-so/src/main/c/type/Type.c | 32 ++- pljava-so/src/main/c/type/UDT.c | 23 ++- pljava-so/src/main/include/pljava/Function.h | 22 ++- .../src/main/include/pljava/InstallHelper.h | 8 +- .../postgresql/pljava/internal/Function.java | 125 ++++++++---- 12 files changed, 514 insertions(+), 125 deletions(-) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java diff --git a/pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java b/pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java new file mode 100644 index 00000000..fad251e6 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 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; + +import java.io.InvalidObjectException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamException; +import java.io.Serializable; + +import static java.lang.reflect.Modifier.isFinal; + +import static java.util.Objects.requireNonNull; + +import java.security.Principal; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Abstract base class for {@link Principal}s named by SQL simple identifiers. + *

    + * Subclasses are expected to be either {@code abstract} or {@code final}. + */ +abstract class BasePrincipal implements Principal, Serializable +{ + private static final long serialVersionUID = -3577164744804938351L; + + BasePrincipal(String name) + { + this(Simple.fromJava(name)); + } + + BasePrincipal(Simple name) + { + m_name = requireNonNull(name); + assert isFinal(getClass().getModifiers()) + : "instantiating a non-final BasePrincipal subclass"; + } + + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + if ( null == m_name ) + throw new InvalidObjectException( + "deserializing a BasePrincipal with null name"); + } + + private Simple m_name; + + @Override + public boolean equals(Object other) + { + if ( getClass().isInstance(other) ) + return m_name.equals(((BasePrincipal)other).m_name); + return false; + } + + @Override + public String toString() + { + Class c = getClass(); + return c.getCanonicalName() + .substring(1+c.getPackageName().length()) + ": " + getName(); + } + + @Override + public int hashCode() + { + return m_name.hashCode(); + } + + @Override + public String getName() + { + return m_name.toString(); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java b/pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java new file mode 100644 index 00000000..8e965ce3 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 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; + +import java.io.InvalidObjectException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamException; +import java.io.Serializable; + +import org.postgresql.pljava.annotation.Function.Trust; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +public abstract class PLPrincipal extends BasePrincipal +{ + private static final long serialVersionUID = 4876111394761861189L; + + PLPrincipal(String name) + { + super(name); + } + + PLPrincipal(Simple name) + { + super(name); + } + + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + Class c = getClass(); + if ( c != Sandboxed.class && c != Unsandboxed.class ) + throw new InvalidObjectException( + "deserializing unknown PLPrincipal subclass: " + + c.getName()); + } + + public abstract Trust trust(); + + public static final class Sandboxed extends PLPrincipal + { + private static final long serialVersionUID = 55704990613451177L; + + public Sandboxed(String name) + { + super(name); + } + public Sandboxed(Simple name) + { + super(name); + } + + @Override + public Trust trust() + { + return Trust.SANDBOXED; + } + } + + public static final class Unsandboxed extends PLPrincipal + { + private static final long serialVersionUID = 7487230786813048525L; + + public Unsandboxed(String name) + { + super(name); + } + public Unsandboxed(Simple name) + { + super(name); + } + + @Override + public Trust trust() + { + return Trust.UNSANDBOXED; + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 20f175c5..d024a33f 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -440,6 +440,8 @@ public static Identifier.Simple identifierFrom(Matcher m) */ public static abstract class Identifier implements Serializable { + private static final long serialVersionUID = 8963213648466350967L; + Identifier() { } // not API /** @@ -513,6 +515,8 @@ private void readObject(ObjectInputStream in) public static abstract class Unqualified> extends Identifier { + private static final long serialVersionUID = -6580227110716782079L; + Unqualified() { } // not API /** @@ -535,6 +539,8 @@ public static abstract class Unqualified> */ public static class Simple extends Unqualified { + private static final long serialVersionUID = 8571819710429273206L; + protected final String m_nonFolded; /** @@ -843,6 +849,8 @@ private void readObject(ObjectInputStream in) */ static class Foldable extends Simple { + private static final long serialVersionUID = 108336518899180185L; + private transient /*otherwise final*/ int m_hashCode; private Foldable(String nonFolded) @@ -918,6 +926,8 @@ protected static String isoFold(String s) */ static class Folding extends Foldable { + private static final long serialVersionUID = -1222773531891296743L; + private transient /*otherwise final*/ String m_pgFolded; private transient /*otherwise final*/ String m_isoFolded; @@ -998,6 +1008,8 @@ public boolean equals(Object other, Messager msgr) */ public static class Pseudo extends Simple { + private static final long serialVersionUID = 4760344682650087583L; + /** * Instance intended to represent {@code PUBLIC} when used as a * privilege grantee. @@ -1042,6 +1054,8 @@ private Object readResolve() throws ObjectStreamException */ public static class Operator extends Unqualified { + private static final long serialVersionUID = -7230613628520513783L; + private final String m_name; private Operator(String name) @@ -1162,6 +1176,8 @@ public String deparse(Simple qualifier, Charset cs) public static class Qualified> extends Identifier { + private static final long serialVersionUID = 4834510180698247396L; + private final Simple m_qualifier; private final T m_local; diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index 2ec4db97..6e8c3982 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -33,6 +33,8 @@ grant codebase "${org.postgresql.pljava.codesource}" { "createClassLoader"; permission java.util.logging.LoggingPermission "control"; + permission java.security.SecurityPermission + "createAccessControlContext"; // This gives the PL/Java implementation code permission to read // any file, which it only exercises on behalf of sqlj.install_jar() @@ -52,3 +54,23 @@ grant codebase "sqlj:examples" { // the PreJSR310 test involves setting the time zone permission java.util.PropertyPermission "user.timezone", "write"; }; + +// This grant defines the mapping onto Java of PostgreSQL's "untrusted language" +// category. When PL/Java executes a function whose SQL declaration names a +// language that was declared without the TRUSTED keyword, it will have these +// permissions (in addition to whatever others might be granted to all code, or +// to its specific jar, etc.). +// +grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed * { + permission java.io.FilePermission + "<>", "read,write,delete,readlink"; +}; + +// This grant defines the mapping onto Java of PostgreSQL's "trusted language" +// category. When PL/Java executes a function whose SQL declaration names a +// language that was declared WITH the TRUSTED keyword, it will have these +// permissions (in addition to whatever others might be granted to all code, or +// to its specific jar, etc.). +// +grant principal org.postgresql.pljava.PLPrincipal$Sandboxed * { +}; diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index e2ac8ac7..604fe843 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1783,7 +1783,7 @@ static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS) PG_TRY(); { Function function = - Function_getFunction(funcoid, forTrigger, false, true); + Function_getFunction(funcoid, trusted, forTrigger, false, true); if(forTrigger) { /* Called as a trigger procedure @@ -1841,7 +1841,7 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) * we decide we don't like this function, which would make the Oid we just * stashed for it invalid, and frustrate getting the load path later. */ - if ( ! InstallHelper_isPLJavaFunction(funcoid) ) + if ( ! InstallHelper_isPLJavaFunction(funcoid, NULL, NULL) ) elog(ERROR, "unexpected error validating PL/Java function"); @@ -1863,7 +1863,8 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) Invocation_pushInvocation(&ctx); PG_TRY(); { - Function_getFunction(funcoid, false, true, check_function_bodies); + Function_getFunction( + funcoid, trusted, false, true, check_function_bodies); Invocation_popInvocation(false); } PG_CATCH(); diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 4f643e79..15c14473 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -212,10 +212,6 @@ void Function_initialize(void) { "_storeToUDT", "(JLjava/lang/ClassLoader;Ljava/lang/Class;ZII" - "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" - "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" - "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" - "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;" ")V", Java_org_postgresql_pljava_internal_Function__1storeToUDT }, @@ -249,7 +245,7 @@ void Function_initialize(void) s_Function_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/Function")); s_Function_create = PgObject_getStaticJavaMethod(s_Function_class, "create", - "(JLjava/sql/ResultSet;Ljava/lang/String;Ljava/lang/String;ZZZ)" + "(JLjava/sql/ResultSet;Ljava/lang/String;Ljava/lang/String;ZZZZ)" "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); s_Function_getClassIfUDT = PgObject_getStaticJavaMethod(s_Function_class, "getClassIfUDT", @@ -289,17 +285,17 @@ void Function_initialize(void) "Ljava/lang/String;)Ljava/sql/SQLData;"); s_Function_udtReadHandle = PgObject_getStaticJavaMethod(s_Function_class, - "udtReadHandle", "(Ljava/lang/Class;)" + "udtReadHandle", "(Ljava/lang/Class;Ljava/lang/String;Z)" "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); s_Function_udtParseHandle = PgObject_getStaticJavaMethod(s_Function_class, - "udtParseHandle", "(Ljava/lang/Class;)" + "udtParseHandle", "(Ljava/lang/Class;Ljava/lang/String;Z)" "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); s_Function_udtWriteHandle = PgObject_getStaticJavaMethod(s_Function_class, - "udtWriteHandle", "(Ljava/lang/Class;)" + "udtWriteHandle", "(Ljava/lang/Class;Ljava/lang/String;Z)" "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); s_Function_udtToStringHandle = PgObject_getStaticJavaMethod(s_Function_class, - "udtToStringHandle", "(Ljava/lang/Class;)" + "udtToStringHandle", "(Ljava/lang/Class;Ljava/lang/String;Z)" "Lorg/postgresql/pljava/internal/EntryPoints$Invocable;"); PgObject_registerNatives2(s_Function_class, functionMethods); @@ -484,28 +480,45 @@ jobject pljava_Function_udtParseInvoke( s_EntryPoints_udtParseInvoke, parseInvocable, stringRep, typeName); } -jobject pljava_Function_udtReadHandle(jclass clazz) +static jobject obtainUDTHandle( + jmethodID which, jclass clazz, char *langName, bool trusted); + +jobject pljava_Function_udtReadHandle( + jclass clazz, char *langName, bool trusted) { - return JNI_callStaticObjectMethod(s_Function_class, - s_Function_udtReadHandle, clazz); + return obtainUDTHandle( + s_Function_udtReadHandle, clazz, langName, trusted); } -jobject pljava_Function_udtParseHandle(jclass clazz) +jobject pljava_Function_udtParseHandle( + jclass clazz, char *langName, bool trusted) { - return JNI_callStaticObjectMethod(s_Function_class, - s_Function_udtParseHandle, clazz); + return obtainUDTHandle( + s_Function_udtParseHandle, clazz, langName, trusted); } -jobject pljava_Function_udtWriteHandle(jclass clazz) +jobject pljava_Function_udtWriteHandle( + jclass clazz, char *langName, bool trusted) { - return JNI_callStaticObjectMethod(s_Function_class, - s_Function_udtWriteHandle, clazz); + return obtainUDTHandle( + s_Function_udtWriteHandle, clazz, langName, trusted); } -jobject pljava_Function_udtToStringHandle(jclass clazz) +jobject pljava_Function_udtToStringHandle( + jclass clazz, char *langName, bool trusted) { - return JNI_callStaticObjectMethod(s_Function_class, - s_Function_udtToStringHandle, clazz); + return obtainUDTHandle( + s_Function_udtToStringHandle, clazz, langName, trusted); +} + +static jobject obtainUDTHandle( + jmethodID which, jclass clazz, char *langName, bool trusted) +{ + jstring jname = String_createJavaStringFromNTS(langName); + jobject result = JNI_callStaticObjectMethod(s_Function_class, + which, clazz, jname, trusted ? JNI_TRUE : JNI_FALSE); + JNI_deleteLocalRef(jname); + return result; } static jstring getSchemaName(int namespaceOid) @@ -517,46 +530,102 @@ static jstring getSchemaName(int namespaceOid) return schemaName; } -Type Function_checkTypeUDT(Oid typeId, Form_pg_type typeStruct) +/* + * This checks specifically for a "Java-based scalar" a/k/a BaseUDT, + * and will call UDT_registerUDT accordingly if it is. + */ +Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct) { HeapTuple procTup; Datum d; Form_pg_proc procStruct; Type t = NULL; jstring schemaName; - jclass clazz; - - if ( ! InstallHelper_isPLJavaFunction(typeStruct->typinput) - || ! InstallHelper_isPLJavaFunction(typeStruct->typoutput) - || ! InstallHelper_isPLJavaFunction(typeStruct->typreceive) - || ! InstallHelper_isPLJavaFunction(typeStruct->typsend) ) - return NULL; + jclass clazz = NULL; + jclass t_clazz = NULL; - /* typinput as good as any, all four had better be in same class */ - procTup = PgObject_getValidTuple(PROCOID, typeStruct->typinput, "function"); - - procStruct = (Form_pg_proc)GETSTRUCT(procTup); - schemaName = getSchemaName(procStruct->pronamespace); + Oid procId[4] = + { + typeStruct->typinput, typeStruct->typreceive, + typeStruct->typsend, typeStruct->typoutput + }; + jobject (*getter[4])(jclass, char *, bool) = + { + pljava_Function_udtParseHandle, pljava_Function_udtReadHandle, + pljava_Function_udtWriteHandle, pljava_Function_udtToStringHandle + }; + char *langName[4] = {}; + bool trusted[4]; + jobject handle[4]; + int i; - d = heap_copy_tuple_as_datum(procTup, Type_getTupleDesc(s_pgproc_Type, 0)); + for ( i = 0; i < 4; ++ i ) + { + if ( ! InstallHelper_isPLJavaFunction( + procId[i], &langName[i], &trusted[i]) ) + break; + } - clazz = (jclass)JNI_callStaticObjectMethod(s_Function_class, - s_Function_getClassIfUDT, Type_coerceDatum(s_pgproc_Type, d), - schemaName); + if ( i < 4 ) + { + for ( ; i >= 0 ; -- i ) + if ( NULL != langName[i] ) + pfree(langName[i]); + return NULL; + } - pfree((void *)d); - JNI_deleteLocalRef(schemaName); - ReleaseSysCache(procTup); + for ( i = 0; i < 4; ++ i ) + { + procTup = PgObject_getValidTuple(PROCOID, procId[i], "function"); + procStruct = (Form_pg_proc)GETSTRUCT(procTup); + schemaName = getSchemaName(procStruct->pronamespace); + d = heap_copy_tuple_as_datum( + procTup, Type_getTupleDesc(s_pgproc_Type, 0)); + t_clazz = (jclass)JNI_callStaticObjectMethod(s_Function_class, + s_Function_getClassIfUDT, Type_coerceDatum(s_pgproc_Type, d), + schemaName); + pfree((void *)d); + JNI_deleteLocalRef(schemaName); + ReleaseSysCache(procTup); + if ( 0 == i ) + clazz = t_clazz; + else + { + if ( JNI_FALSE == JNI_isSameObject(clazz, t_clazz) ) + goto classMismatch; + JNI_deleteLocalRef(t_clazz); + } + handle[i] = (getter[i])(clazz, langName[i], trusted[i]); + } if ( NULL != clazz ) t = (Type)UDT_registerUDT(clazz, typeId, typeStruct, 0, true, - NULL, NULL, NULL, NULL); + handle[0], handle[1], handle[2], handle[3]); + /* + * UDT_registerUDT will already have called JNI_deleteLocalRef on the + * four handles. + */ + JNI_deleteLocalRef(clazz); + for ( i = 0; i < 4; ++ i ) + pfree(langName[i]); return t; + +classMismatch: + while ( i --> 0 ) + JNI_deleteLocalRef(handle[i]); + for ( i = 0; i < 4; ++ i ) + pfree(langName[i]); + JNI_deleteLocalRef(clazz); + JNI_deleteLocalRef(t_clazz); + ereport(ERROR, (errmsg( + "PL/Java UDT with oid %u declares input/output/send/recv functions " + "in more than one class", typeId))); } static Function Function_create( - Oid funcOid, bool forTrigger, bool forValidator, bool checkBody) + Oid funcOid, bool trusted, bool forTrigger, + bool forValidator, bool checkBody) { Function self; HeapTuple procTup = @@ -566,11 +635,18 @@ static Function Function_create( PgObject_getValidTuple(LANGOID, procStruct->prolang, "language"); Form_pg_language lngStruct = (Form_pg_language)GETSTRUCT(lngTup); jstring lname = String_createJavaStringFromNTS(NameStr(lngStruct->lanname)); + bool ltrust = lngStruct->lanpltrusted; jstring schemaName; Ptr2Long p2l; Datum d; jobject invocable; + if ( trusted != ltrust ) + elog(ERROR, + "function with oid %u invoked through wrong call handler " + "for %strusted language %s", funcOid, ltrust ? "" : "un", + NameStr(lngStruct->lanname)); + d = heap_copy_tuple_as_datum(procTup, Type_getTupleDesc(s_pgproc_Type, 0)); schemaName = getSchemaName(procStruct->pronamespace); @@ -586,6 +662,7 @@ static Function Function_create( JNI_callStaticObjectMethod(s_Function_class, s_Function_create, p2l.longVal, Type_coerceDatum(s_pgproc_Type, d), lname, schemaName, + trusted ? JNI_TRUE : JNI_FALSE, forTrigger ? JNI_TRUE : JNI_FALSE, forValidator ? JNI_TRUE : JNI_FALSE, checkBody ? JNI_TRUE : JNI_FALSE); @@ -654,14 +731,16 @@ static Function Function_create( * use the result. */ Function Function_getFunction( - Oid funcOid, bool forTrigger, bool forValidator, bool checkBody) + Oid funcOid, bool trusted, bool forTrigger, + bool forValidator, bool checkBody) { Function func = forValidator ? NULL : (Function)HashMap_getByOid(s_funcMap, funcOid); if ( NULL == func ) { - func = Function_create(funcOid, forTrigger, forValidator, checkBody); + func = Function_create( + funcOid, trusted, forTrigger, forValidator, checkBody); if ( NULL != func ) HashMap_putByOid(s_funcMap, funcOid, func); } @@ -1068,13 +1147,12 @@ JNIEXPORT jboolean JNICALL /* * Class: org_postgresql_pljava_internal_Function * Method: _storeToUDT - * Signature: (JLjava/lang/ClassLoader;Ljava/lang/Class;ZIILorg/postgresql/pljava/internal/EntryPoints$Invocable;Lorg/postgresql/pljava/internal/EntryPoints$Invocable;Lorg/postgresql/pljava/internal/EntryPoints$Invocable;Lorg/postgresql/pljava/internal/EntryPoints$Invocable;)V + * Signature: (JLjava/lang/ClassLoader;Ljava/lang/Class;ZII)V */ JNIEXPORT void JNICALL Java_org_postgresql_pljava_internal_Function__1storeToUDT( JNIEnv *env, jclass jFunctionClass, jlong wrappedPtr, jobject schemaLoader, - jclass clazz, jboolean readOnly, jint funcInitial, jint udtId, - jobject parseMH, jobject readMH, jobject writeMH, jobject toStringMH) + jclass clazz, jboolean readOnly, jint funcInitial, jint udtId) { Ptr2Long p2l; Function self; @@ -1106,9 +1184,15 @@ JNIEXPORT void JNICALL self->schemaLoader = JNI_newWeakGlobalRef(schemaLoader); self->clazz = JNI_newGlobalRef(clazz); - self->func.udt.udt = - UDT_registerUDT(self->clazz, udtId, pgType, 0, true, - parseMH, readMH, writeMH, toStringMH); + /* + * Only a BaseUDT has SQL-declared PL/Java I/O functions, so only + * a BaseUDT can arrive at this code. Its four I/O functions are + * most easily looked up by Function_checkTypeBaseUDT, which has to + * exist separately anyway in case the UDT is first encountered by + * the Type machinery instead of an explicit function invocation. + */ + self->func.udt.udt = (UDT) + Function_checkTypeBaseUDT((Oid)udtId, pgType); switch ( funcInitial ) { diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index efe62942..74a9735c 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -354,7 +354,7 @@ static void getExtensionLoadPath() * * If a string is returned, it has been palloc'd in the current context. */ -char *pljavaFnOidToLibPath(Oid fnOid) +char *pljavaFnOidToLibPath(Oid fnOid, char **langName, bool *trusted) { bool isnull; HeapTuple procTup; @@ -394,13 +394,15 @@ char *pljavaFnOidToLibPath(Oid fnOid) elog(ERROR, "cache lookup failed for language %u", langId); langStruct = (Form_pg_language) GETSTRUCT(langTup); handlerOid = langStruct->lanplcallfoid; - ReleaseSysCache(langTup); /* * PL/Java has certainly got a function call handler, so if this language * hasn't, PL/Java it's not. */ if ( InvalidOid == handlerOid ) + { + ReleaseSysCache(langTup); return NULL; + } /* * Da capo al coda ... handlerOid is another function to be looked up. @@ -413,7 +415,10 @@ char *pljavaFnOidToLibPath(Oid fnOid) * If the call handler's not a C function, this isn't PL/Java.... */ if ( ClanguageId != procStruct->prolang ) + { + ReleaseSysCache(langTup); return NULL; + } /* * Now that the handler is known to be a C function, it should have a @@ -423,6 +428,11 @@ char *pljavaFnOidToLibPath(Oid fnOid) SysCacheGetAttr(PROCOID, procTup, Anum_pg_proc_probin, &isnull); if ( isnull ) elog(ERROR, "null probin for C function %u", handlerOid); + if ( NULL != langName ) + *langName = pstrdup(NameStr(langStruct->lanname)); + if ( NULL != trusted ) + *trusted = langStruct->lanpltrusted; + ReleaseSysCache(langTup); probinstring = /* TextDatumGetCString(probinattr); */ DatumGetCString(DirectFunctionCall1(textout, probinattr)); /*archaic*/ ReleaseSysCache(procTup); @@ -443,13 +453,13 @@ bool InstallHelper_shouldDeferInit() return IsBackgroundWorker || IsBinaryUpgrade; } -bool InstallHelper_isPLJavaFunction(Oid fn) +bool InstallHelper_isPLJavaFunction(Oid fn, char **langName, bool *trusted) { char *itsPath; char *pljPath; bool result = false; - itsPath = pljavaFnOidToLibPath(fn); + itsPath = pljavaFnOidToLibPath(fn, langName, trusted); if ( NULL == itsPath ) return false; @@ -457,9 +467,9 @@ bool InstallHelper_isPLJavaFunction(Oid fn) { pljPath = NULL; if ( InvalidOid != pljavaTrustedOid ) - pljPath = pljavaFnOidToLibPath(pljavaTrustedOid); + pljPath = pljavaFnOidToLibPath(pljavaTrustedOid, NULL, NULL); if ( NULL == pljPath && InvalidOid != pljavaUntrustedOid ) - pljPath = pljavaFnOidToLibPath(pljavaUntrustedOid); + pljPath = pljavaFnOidToLibPath(pljavaUntrustedOid, NULL, NULL); if ( NULL == pljPath ) { elog(WARNING, "unable to determine PL/Java's load path"); diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index b32fe1bf..ef9a8849 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -675,18 +675,33 @@ Type Type_fromOid(Oid typeId, jobject typeMap) if(typeMap != 0) { jobject joid = Oid_create(typeId); - jclass typeClass = (jclass)JNI_callObjectMethod(typeMap, s_Map_get, joid); + jclass typeClass = + (jclass)JNI_callObjectMethod(typeMap, s_Map_get, joid); JNI_deleteLocalRef(joid); if(typeClass != 0) { - TupleDesc tupleDesc = lookup_rowtype_tupdesc_noerror(typeId, -1, true); + /* + * We have found a MappedUDT. It doesn't have SQL-declared I/O + * functions, so we need to look up only the read and write handles, + * and there will be no PLPrincipal to associate them with, + * indicated by passing NULL as the language name. + */ + jobject readMH = + pljava_Function_udtReadHandle(typeClass, NULL, true); + jobject writeMH = + pljava_Function_udtWriteHandle(typeClass, NULL, true); + TupleDesc tupleDesc = + lookup_rowtype_tupdesc_noerror(typeId, -1, true); bool hasTupleDesc = NULL != tupleDesc; if ( hasTupleDesc ) ReleaseTupleDesc(tupleDesc); type = (Type)UDT_registerUDT( typeClass, typeId, typeStruct, hasTupleDesc, false, - NULL, NULL, NULL, NULL); + NULL, readMH, writeMH, NULL); + /* + * UDT_registerUDT calls JNI_deleteLocalRef on readMH and writeMH. + */ JNI_deleteLocalRef(typeClass); goto finally; } @@ -694,7 +709,8 @@ Type Type_fromOid(Oid typeId, jobject typeMap) /* Composite and record types will not have a TypeObtainer registered */ - if(typeStruct->typtype == 'c' || (typeStruct->typtype == 'p' && typeId == RECORDOID)) + if(typeStruct->typtype == 'c' + || (typeStruct->typtype == 'p' && typeId == RECORDOID)) { type = Composite_obtain(typeId); goto finally; @@ -703,11 +719,15 @@ Type Type_fromOid(Oid typeId, jobject typeMap) ce = (CacheEntry)HashMap_getByOid(s_obtainerByOid, typeId); if(ce == 0) { - type = Function_checkTypeUDT(typeId, typeStruct); + /* + * Perhaps we have found a BaseUDT. If so, this check will register and + * return it. + */ + type = Function_checkTypeBaseUDT(typeId, typeStruct); if ( 0 != type ) goto finally; /* - * Default to String and standard textin/textout coersion. + * Default to String and standard textin/textout coercion. * Note: if the AS spec includes a Java signature, and the corresponding * Java type is not String, that will trigger a call to * Type_fromJavaType to see if a mapping is registered that way. If not, diff --git a/pljava-so/src/main/c/type/UDT.c b/pljava-so/src/main/c/type/UDT.c index e4a7aa25..0f824c58 100644 --- a/pljava-so/src/main/c/type/UDT.c +++ b/pljava-so/src/main/c/type/UDT.c @@ -426,7 +426,8 @@ bool UDT_isScalar(UDT udt) return ! udt->hasTupleDesc; } -/* Make this datatype available to the postgres system. +/* Make this datatype available to the postgres system. The four ...MH arguments + * are passed to JNI_deleteLocalRef after being saved as global references. */ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, bool hasTupleDesc, bool isJavaBasedScalar, jobject parseMH, jobject readMH, @@ -456,6 +457,10 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, errcode(ERRCODE_CANNOT_COERCE), errmsg("Attempt to register UDT with Oid %d failed. Oid appoints a non UDT type", typeId))); } + JNI_deleteLocalRef(parseMH); + JNI_deleteLocalRef(readMH); + JNI_deleteLocalRef(writeMH); + JNI_deleteLocalRef(toStringMH); return (UDT)existing; } @@ -526,10 +531,10 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, /* The parse method is a static method on the class with the signature * (Ljava/lang/String;Ljava/lang/String;) */ - if ( NULL == parseMH ) - parseMH = pljava_Function_udtParseHandle(clazz); - if ( NULL == toStringMH ) - toStringMH = pljava_Function_udtToStringHandle(clazz); + if ( NULL == parseMH || NULL == toStringMH ) + elog(ERROR, + "PL/Java UDT with oid %u registered without both i/o handles", + typeId); udt->parse = JNI_newGlobalRef(parseMH); udt->toString = JNI_newGlobalRef(toStringMH); JNI_deleteLocalRef(parseMH); @@ -542,10 +547,10 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, } udt->hasTupleDesc = hasTupleDesc; - if ( NULL == readMH ) - readMH = pljava_Function_udtReadHandle(clazz); - if ( NULL == writeMH ) - writeMH = pljava_Function_udtWriteHandle(clazz); + if ( NULL == readMH || NULL == writeMH ) + elog(ERROR, + "PL/Java UDT with oid %u registered without both r/w handles", + typeId); udt->readSQL = JNI_newGlobalRef(readMH); udt->writeSQL = JNI_newGlobalRef(writeMH); JNI_deleteLocalRef(readMH); diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 2a646c93..5d0e1857 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -55,9 +55,15 @@ extern void Function_clearFunctionCache(void); * meaning. */ extern Function Function_getFunction( - Oid funcOid, bool forTrigger, bool forValidator, bool checkBody); + Oid funcOid, bool trusted, bool forTrigger, + bool forValidator, bool checkBody); -extern Type Function_checkTypeUDT(Oid typeId, Form_pg_type typeStruct); +/* + * Determine whether the type represented by typeId is declared as a + * "Java-based scalar" a/k/a BaseUDT and, if so, return a freshly-registered + * UDT Type for it; otherwise return NULL. + */ +extern Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct); /* * Invoke a trigger. Wrap the TriggerData in org.postgresql.pljava.TriggerData @@ -127,10 +133,14 @@ extern jobject pljava_Function_udtReadInvoke( extern jobject pljava_Function_udtParseInvoke( jobject invocable, jstring stringRep, jstring typeName); -extern jobject pljava_Function_udtWriteHandle(jclass clazz); -extern jobject pljava_Function_udtToStringHandle(jclass clazz); -extern jobject pljava_Function_udtReadHandle(jclass clazz); -extern jobject pljava_Function_udtParseHandle(jclass clazz); +extern jobject pljava_Function_udtWriteHandle( + jclass clazz, char *langName, bool trusted); +extern jobject pljava_Function_udtToStringHandle( + jclass clazz, char *langName, bool trusted); +extern jobject pljava_Function_udtReadHandle( + jclass clazz, char *langName, bool trusted); +extern jobject pljava_Function_udtParseHandle( + jclass clazz, char *langName, bool trusted); /* * Returns the Type Map that is associated with the function diff --git a/pljava-so/src/main/include/pljava/InstallHelper.h b/pljava-so/src/main/include/pljava/InstallHelper.h index b923eebf..2edf837e 100644 --- a/pljava-so/src/main/include/pljava/InstallHelper.h +++ b/pljava-so/src/main/include/pljava/InstallHelper.h @@ -64,12 +64,16 @@ extern bool pljavaLoadingAsExtension; * isPLJavaFunction can use the stashed information to determine whether an * arbitrary function Oid is a function built on PL/Java, without relying on * assumptions about the language name, etc. + * + * It can return the language name and/or trusted flag if non-null pointers + * are supplied, as it will be looking up the language anyway. */ -extern char *pljavaFnOidToLibPath(Oid fn); +extern char *pljavaFnOidToLibPath(Oid fn, char **langName, bool *trusted); extern Oid pljavaTrustedOid, pljavaUntrustedOid; -extern bool InstallHelper_isPLJavaFunction(Oid fn); +extern bool InstallHelper_isPLJavaFunction( + Oid fn, char **langName, bool *trusted); /* * Return the name of the current database, from MyProcPort ... don't free it. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index eb76e1f0..c0b2168f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -65,10 +65,15 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.util.regex.Pattern.compile; +import javax.security.auth.Subject; +import javax.security.auth.SubjectDomainCombiner; + +import org.postgresql.pljava.PLPrincipal; import org.postgresql.pljava.ResultSetHandle; import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.sqlgen.Lexicals.Identifier; @@ -77,6 +82,7 @@ import org.postgresql.pljava.internal.EntryPoints; import org.postgresql.pljava.internal.EntryPoints.Invocable; import static org.postgresql.pljava.internal.EntryPoints.invocable; +import static org.postgresql.pljava.internal.Privilege.doPrivileged; import static org.postgresql.pljava.jdbc.TypeOid.INVALID; import static org.postgresql.pljava.jdbc.TypeOid.TRIGGEROID; import org.postgresql.pljava.management.Commands; @@ -596,6 +602,14 @@ else if ( rt.isPrimitive() ) */ private static final AccessControlContext s_lid; + /** + * An {@code AccessControlContext} representing "no other restrictions": + * it will be used to build the initial context for any {@code Invocable} + * whose target is in a PL/Java-managed jar, so that it will enjoy whatever + * permissions the policy grants to its jar directly. + */ + private static final AccessControlContext s_noLid; + /* * Static areas for passing reference and primitive parameters. A Java * method can have no more than 255 parameters, so each area gets the @@ -1038,7 +1052,11 @@ mh, empty(methodType(Invocable.class, * The ProtectionDomain constructor allows the permissions parameter * to be null, and says so in the javadocs. It seems to allow * the principals parameter to be null too, but doesn't say that, so why - * take a chance.... + * take a chance? + * + * An empty ProtectionDomain array is all it takes to make s_noLid. + * (As far as doPrivileged is concerned, a null AccessControlContext has + * the same effect, but one can't attach a DomainCombiner to that.) */ s_lid = new AccessControlContext(new ProtectionDomain[] { @@ -1047,6 +1065,8 @@ mh, empty(methodType(Invocable.class, null, ClassLoader.getSystemClassLoader(), new Principal[0]) }); + s_noLid = + new AccessControlContext(new ProtectionDomain[] {}); } /** @@ -1092,10 +1112,12 @@ private static void paramCountsAre(short counts) * The access control context of the {@code Invocable} returned here is used * at the corresponding entry point; the payload is not. */ - private static Invocable udtWriteHandle(Class clazz) + private static Invocable udtWriteHandle( + Class clazz, String language, boolean trusted) throws SQLException { - return invocable(null, accessControlContextFor(clazz)); + return invocable( + null, accessControlContextFor(clazz, language, trusted)); } /** @@ -1105,10 +1127,12 @@ private static Invocable udtWriteHandle(Class clazz) * The access control context of the {@code Invocable} returned here is used * at the corresponding entry point; the payload is not. */ - private static Invocable udtToStringHandle(Class clazz) + private static Invocable udtToStringHandle( + Class clazz, String language, boolean trusted) throws SQLException { - return invocable(null, accessControlContextFor(clazz)); + return invocable( + null, accessControlContextFor(clazz, language, trusted)); } /** @@ -1125,7 +1149,8 @@ private static Invocable udtToStringHandle(Class clazz) * assigned here will be in effect for both the constructor and the * {@code readSQL} call. */ - private static Invocable udtReadHandle(Class clazz) + private static Invocable udtReadHandle( + Class clazz, String language, boolean trusted) throws SQLException { Lookup l = lookupFor(clazz); @@ -1144,7 +1169,8 @@ private static Invocable udtReadHandle(Class clazz) " must have a no-argument public constructor", "38000", e); } - return invocable(ctor, accessControlContextFor(clazz)); + return invocable( + ctor, accessControlContextFor(clazz, language, trusted)); } /** @@ -1154,22 +1180,17 @@ private static Invocable udtReadHandle(Class clazz) * a NUL-terminated storage form, so it gets its own dedicated entry point * and does not use the static parameter area. */ - private static Invocable udtParseHandle(Class clazz) + private static Invocable udtParseHandle( + Class clazz, String language, boolean trusted) throws SQLException { Lookup l = lookupFor(clazz); + MethodHandle mh; try { - MethodHandle mh = - l.findStatic(clazz, "parse", - methodType(clazz, String.class, String.class)); - - return - invocable( - mh.asType(mh.type().changeReturnType(SQLData.class)), - accessControlContextFor(clazz) - ); + mh = l.findStatic(clazz, "parse", + methodType(clazz, String.class, String.class)); } catch ( ReflectiveOperationException e ) { @@ -1179,6 +1200,10 @@ private static Invocable udtParseHandle(Class clazz) " must have a public static parse(String,String) method", "38000", e); } + + return invocable( + mh.asType(mh.type().changeReturnType(SQLData.class)), + accessControlContextFor(clazz, language, trusted)); } /** @@ -1189,7 +1214,8 @@ private static Invocable udtParseHandle(Class clazz) */ public static Invocable create( long wrappedPtr, ResultSet procTup, String langName, String schemaName, - boolean calledAsTrigger, boolean forValidator, boolean checkBody) + boolean trusted, boolean calledAsTrigger, + boolean forValidator, boolean checkBody) throws SQLException { Matcher info = parse(procTup); @@ -1200,7 +1226,7 @@ public static Invocable create( Identifier.Simple schema = Identifier.Simple.fromCatalog(schemaName); return init(wrappedPtr, info, procTup, schema, calledAsTrigger, - forValidator); + forValidator, langName, trusted); } /** @@ -1229,7 +1255,8 @@ private static Matcher parse(ResultSet procTup) throws SQLException */ private static Invocable init( long wrappedPtr, Matcher info, ResultSet procTup, - Identifier.Simple schema, boolean calledAsTrigger, boolean forValidator) + Identifier.Simple schema, boolean calledAsTrigger, boolean forValidator, + String language, boolean trusted) throws SQLException { Map> typeMap = null; @@ -1293,7 +1320,8 @@ private static Invocable init( else handle = dropArguments(handle, 0, AccessControlContext.class); - return invocable(handle, accessControlContextFor(clazz)); + return invocable(handle, + accessControlContextFor(clazz, language, trusted)); } /** @@ -1324,17 +1352,43 @@ private static boolean isTrigger(ResultSet procTup) * PL/Java's own {@code Commands} class; they get a lid. It is reasonable to * ask them to use {@code doPrivileged} when appropriate. */ - private static AccessControlContext accessControlContextFor(Class clazz) + private static AccessControlContext accessControlContextFor( + Class clazz, String language, boolean trusted) { - if ( clazz.getClassLoader() instanceof Loader ) - return null; // policy already applies appropriate permissions + Set p = + (null == language) + ? Set.of() + : Set.of( + trusted + ? new PLPrincipal.Sandboxed(language) + : new PLPrincipal.Unsandboxed(language) + ); - return s_lid; // put a lid on permissions if calling JRE directly + AccessControlContext acc = clazz.getClassLoader() instanceof Loader + ? s_noLid // policy already applies appropriate permissions + : s_lid; // put a lid on permissions if calling JRE directly + + /* + * A cache to avoid the following machinations might be good. + */ + return doPrivileged(() -> + new AccessControlContext(acc, new SubjectDomainCombiner( + new Subject(true, p, Set.of(), Set.of())))); } /** * The initialization specific to a UDT function. */ + /* + * A MappedUDT will not have PL/Java I/O functions declared in SQL, + * and therefore will never reach this method. Ergo, this is handling a + * BaseUDT, which must have all four functions, not just the one + * happening to be looked up at this instant. Rather than looking up one + * handle here and leaving the C code to find the rest anyway, simply let + * the C code look up all four; Function.c already contains logic for doing + * that, which it has to have in case the UDT is first encountered by the + * Type machinery rather than in an explicit function call. + */ private static void setupUDT( long wrappedPtr, Matcher info, ResultSet procTup, ClassLoader schemaLoader, Class clazz, @@ -1344,34 +1398,23 @@ private static void setupUDT( String udtFunc = info.group("udtfun"); int udtInitial = Character.toLowerCase(udtFunc.charAt(0)); Oid udtId; + switch ( udtInitial ) { case 'i': case 'r': udtId = (Oid)procTup.getObject("prorettype"); break; - case 'o': case 's': + case 'o': udtId = ((Oid[])procTup.getObject("proargtypes"))[0]; break; default: throw new SQLException("internal error in PL/Java UDT parsing"); } - Invocable readMH = udtReadHandle(clazz); - Invocable writeMH = udtWriteHandle(clazz); - /* - * A Base UDT will have an 'i' (parse) and an 'o' (toString) function - * to register. We'll look them both up here when resolving the 'i' - * case, and they'll be registered at one time, saving upcalls if the - * 'i' case is first to be resolved. - */ - Invocable parseMH = 'i' == udtInitial? udtParseHandle(clazz) :null; - Invocable toStringMH = 'i' == udtInitial? udtToStringHandle(clazz):null; - doInPG(() -> _storeToUDT(wrappedPtr, schemaLoader, - clazz, readOnly, udtInitial, udtId.intValue(), - parseMH, readMH, writeMH, toStringMH)); + clazz, readOnly, udtInitial, udtId.intValue())); } /** @@ -1856,9 +1899,7 @@ private static native boolean _storeToNonUDT( private static native void _storeToUDT( long wrappedPtr, ClassLoader schemaLoader, Class clazz, - boolean readOnly, int funcInitial, int udtOid, - Invocable readMH, Invocable parseMH, - Invocable writeMH, Invocable toStringMH); + boolean readOnly, int funcInitial, int udtOid); private static native void _reconcileTypes( long wrappedPtr, String[] resolvedTypes, String[] explicitTypes, int i); From 74611c27e9ccd5c58a879fcce36c29b90a092182 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Sep 2020 21:39:15 -0400 Subject: [PATCH 0766/1087] Add pljava.policy_urls GUC. Allows setting the pljava.policy URL (and any others as desired) at chosen places in the policy.url.n sequence begun in the systemwide java.security file. While it is not yet (with these patches) possible to grant permissions based on the code signers of a jar, or the PostgreSQL role under which the code is executing, it is at least possible to have multiple policy files and use ALTER ROLE ... SET pljava.policy_urls ...; to select a specific policy based on the authenticated role at the time of connection. Of course only a superuser can set this GUC. --- pljava-so/src/main/c/Backend.c | 66 ++++++++++++++ .../postgresql/pljava/internal/Backend.java | 4 +- .../pljava/internal/InstallHelper.java | 87 ++++++++++++++++++- .../pljava/management/DDRExecutor.java | 2 +- 4 files changed, 153 insertions(+), 6 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 604fe843..b38d406e 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -116,6 +116,7 @@ static char* libjvmlocation; static char* vmoptions; static char* modulepath; static char* implementors; +static char* policy_urls; static int statementCacheSize; static bool pljavaDebug; static bool pljavaReleaseLingeringSavepoints; @@ -189,6 +190,7 @@ enum initstage IS_FORMLESS_VOID, IS_GUCS_REGISTERED, IS_CAND_JVMLOCATION, + IS_CAND_POLICYURLS, IS_PLJAVA_ENABLED, IS_CAND_JVMOPENED, IS_CREATEVM_SYM_FOUND, @@ -234,6 +236,8 @@ static void initsequencer(enum initstage is, bool tolerant); char **newval, void **extra, GucSource source); static bool check_modulepath( char **newval, void **extra, GucSource source); + static bool check_policy_urls( + char **newval, void **extra, GucSource source); static bool check_enabled( bool *newval, void **extra, GucSource source); static bool check_java_thread_pg_entry( @@ -302,6 +306,25 @@ static void initsequencer(enum initstage is, bool tolerant); return false; } + static bool check_policy_urls( + char **newval, void **extra, GucSource source) + { + if ( initstage < IS_JAVAVM_OPTLIST ) + return true; + if ( policy_urls == *newval ) + return true; + if ( policy_urls && *newval && 0 == strcmp(policy_urls, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.policy_urls\" setting"); + GUC_check_errdetail( + "Changing the setting has no effect after " + "PL/Java has started the Java virtual machine."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; + } + static bool check_enabled( bool *newval, void **extra, GucSource source) { @@ -429,6 +452,19 @@ ASSIGNSTRINGHOOK(modulepath) ASSIGNRETURN(newval); } +ASSIGNSTRINGHOOK(policy_urls) +{ + ASSIGNRETURNIFCHECK(newval); + policy_urls = (char *)newval; + if ( IS_FORMLESS_VOID < initstage && initstage < IS_JAVAVM_OPTLIST ) + { + alteredSettingsWereNeeded = true; + ASSIGNRETURNIFNXACT(newval); + initsequencer( initstage, true); + } + ASSIGNRETURN(newval); +} + ASSIGNHOOK(enabled, bool) { ASSIGNRETURNIFCHECK(true); @@ -525,6 +561,18 @@ static void initsequencer(enum initstage is, bool tolerant) initstage = IS_CAND_JVMLOCATION; case IS_CAND_JVMLOCATION: + if ( NULL == policy_urls ) + { + ereport(WARNING, ( + errmsg("Java virtual machine not yet loaded"), + errdetail("Java policy URL(s) not configured"), + errhint("SET pljava.policy_urls TO the security policy " + "files PL/Java is to use."))); + goto check_tolerant; + } + initstage = IS_CAND_POLICYURLS; + + case IS_CAND_POLICYURLS: if ( ! pljavaEnabled ) { ereport(WARNING, ( @@ -1642,6 +1690,24 @@ static void registerGUCOptions(void) assign_modulepath, NULL); /* show hook */ + STRING_GUC( + "pljava.policy_urls", + "URLs to Java security policy file(s) for PL/Java's use", + "Quote each URL and separate with commas. Any URL may begin (inside " + "the quotes) with n= where n is the index of the Java " + "policy.url.n property to set. If not specified, the first will " + "become policy.url.2 (following the JRE-installed policy) with " + "subsequent entries following in sequence. The last entry may be a " + "bare = (still quoted) to prevent use of any higher-numbered policy " + "URLs from the java.security file.", + &policy_urls, + "\"file:${org.postgresql.sysconfdir}/pljava.policy\",\"=\"", + PGC_SUSET, + PLJAVA_IMPLEMENTOR_FLAGS, + check_policy_urls, /* check hook */ + assign_policy_urls, + NULL); /* show hook */ + BOOL_GUC( "pljava.debug", "Stop the backend to attach a debugger", diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index e2ab698a..35bab8c7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -247,11 +247,11 @@ public static String getConfigOption(String key) return doInPG(() -> _getConfigOption(key)); } - public static List getListConfigOption(String key) + public static List getListConfigOption(String key) throws SQLException { final Matcher m = s_gucList.matcher(getConfigOption(key)); - ArrayList al = new ArrayList<>(); + ArrayList al = new ArrayList<>(); while ( m.find() ) { al.add(identifierFrom(m)); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index fbb9ce19..c48db2d0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -28,7 +28,8 @@ import java.sql.Savepoint; import java.sql.Statement; import java.text.ParseException; -import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static java.nio.charset.StandardCharsets.UTF_8; import static java.sql.Types.VARCHAR; @@ -36,6 +37,7 @@ import org.postgresql.pljava.jdbc.SQLUtils; import org.postgresql.pljava.management.SQLDeploymentDescriptor; import static org.postgresql.pljava.annotation.processing.DDRWriter.eQuote; +import static org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; /** * Group of methods intended to streamline the PL/Java installation/startup @@ -55,6 +57,7 @@ public static String hello( String nativeVer, String serverBuiltVer, String serverRunningVer, String user, String dbname, String clustername, String datadir, String libdir, String sharedir, String etcdir) + throws SQLException { String implVersion = InstallHelper.class.getModule().getDescriptor().rawVersion().get(); @@ -129,8 +132,7 @@ public static String hello( InstallHelper.class.getProtectionDomain().getCodeSource() .getLocation().toString()); - Security.setProperty( "policy.url.2", - "file:${org.postgresql.sysconfdir}/pljava.policy"); + setPolicyURLs(); /* * Construct the strings announcing the versions in use. @@ -174,6 +176,85 @@ public static String hello( return sb.toString(); } + /** + * Set the URLs to be read by Java's Policy implementation according to + * pljava.policy_urls. That is a {@code GUC_LIST}-formatted config variable + * where each element can be a plain URL, or a URL prefixed with {@code n=} + * to set the index of the URL to be set or replaced to n. + * If no n is specified, the index following the last one set will + * be used; if the first URL in the list has no n, it will + * be placed at index 2 (after the presumed JRE installed policy at index + * 1). + *

    + * An entry with nothing after the {@code =} causes that and subsequent URL + * positions not to be processed, in case they had been set in the + * systemwide {@code java.security} file. As there is not actually a way to + * delete a security property, the code will simply replace any + * {@code policy.url.n} entries found at that index and higher with copies + * of the URL at the immediately preceding index. + */ + private static void setPolicyURLs() + throws SQLException + { + /* This index is incremented before setting each specified policy URL. + * Initializing it to 1 means the first URL set (if it does not specify + * an index) will be at 2, following the JRE's installed policy. Any URL + * entry can begin with n= in order to set URL number n instead (and + * any that follow will be in sequence after n, unless another n= is + * used). + */ + int urlIndex = 1; + + int stopIndex = -1; + + Pattern p = Pattern.compile( "^(?:(\\d++)?+=)?+"); + + String prevURL = null; + for (Simple u : Backend.getListConfigOption( "pljava.policy_urls")) + { + if ( -1 != stopIndex ) + throw new SQLNonTransientException( + "stop (=) entry must be last in pljava.policy_urls", + "F0000"); + ++ urlIndex; + String s = u.nonFolded(); + Matcher m = p.matcher(s); + if ( m.find() ) + { + s = s.substring(m.end()); + String i = m.group(1); + if ( null != i ) + urlIndex = Integer.parseInt(i); + if ( s.isEmpty() ) + stopIndex = urlIndex; + } + if ( urlIndex < 1 ) + throw new SQLNonTransientException( + "index (n=) must be >= 1 in pljava.policy_urls", + "F0000"); + int prevIndex = urlIndex - 1; + if ( urlIndex > 1 ) + { + prevURL = Security.getProperty( "policy.url." + prevIndex); + if ( null == prevURL ) + throw new SQLNonTransientException(String.format( + "URL at %d in pljava.policy_urls follows an unset URL", + urlIndex), "F0000"); + } + if ( -1 != stopIndex ) + continue; /* should be last, but resume loop to make sure */ + Security.setProperty( "policy.url." + urlIndex, s); + } + if ( -1 == stopIndex ) + return; + + while ( null != Security.getProperty( "policy.url." + stopIndex) ) + { + Security.setProperty( "policy.url." + stopIndex, prevURL); + ++ stopIndex; + } + } + public static void groundwork( String module_pathname, String loadpath_tbl, String loadpath_tbl_quoted, boolean asExtension, boolean exNihilo) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java index 62acf1cd..52ac52c2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/DDRExecutor.java @@ -98,7 +98,7 @@ public static DDRExecutor forImplementor( Identifier name) if ( null == name ) return PLAIN; - Iterable imps = + Iterable imps = Backend.getListConfigOption( "pljava.implementors"); for ( Identifier i : imps ) From 0d9a6b088d58c0169ff335a2afdc0de5befedaa4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Sep 2020 22:37:32 -0400 Subject: [PATCH 0767/1087] Normalize some exceptions in EntryPoints Now that there is one class through which nearly all entry into Java will go, there is a convenient place to turn some exceptions that aren't SQLException into appropriate SQLExceptions. This could be an argument for also dispatching the XactListener and SubXactListener through here, even though that doesn't otherwise help them. This is surely work in progress. --- .../pljava/internal/EntryPoints.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java index cc29e21b..7df1b22a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java @@ -21,6 +21,7 @@ import java.sql.SQLData; import java.sql.SQLException; +import java.sql.SQLSyntaxErrorException; import java.sql.SQLInput; import java.sql.SQLOutput; @@ -123,6 +124,10 @@ static Invocable invocable(MethodHandle mh, AccessControlContext acc) { return mh.invokeExact(acc); } + catch ( Error | RuntimeException e ) + { + throw e; + } catch ( Throwable t ) { throw unchecked(t); @@ -238,6 +243,10 @@ private static SQLData udtReadInvoke( o.readSQL(stream, typeName); return o; } + catch ( Error | RuntimeException e ) + { + throw e; + } catch ( Throwable t ) { throw unchecked(t); @@ -271,6 +280,10 @@ private static SQLData udtParseInvoke( { return (SQLData)target.payload.invokeExact(textRep, typeName); } + catch ( Error | RuntimeException e ) + { + throw e; + } catch ( Throwable t ) { throw unchecked(t); @@ -288,14 +301,35 @@ private static T doPrivilegedAndUnwrap( PrivilegedAction action, AccessControlContext context) throws Throwable { + Throwable t; try { return doPrivileged(action, context); } + catch ( Error e ) + { + throw e; + } catch ( UncheckedException e ) { - throw e.unwrap(); + t = e.unwrap(); + } + catch ( Throwable e ) + { + t = e; } + + if ( t instanceof SQLException ) + throw t; + + if ( t instanceof SecurityException ) + /* + * Yes, SQL and JDBC lump syntax errors and access violations + * together, and this is the right exception class for 42xxx. + */ + throw new SQLSyntaxErrorException(t.getMessage(), "42501", t); + + throw new SQLException(t.getMessage(), t); } /** From 7b9c98962dc85794fafa68d8f5277d2d940c1c53 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Sep 2020 18:53:11 -0400 Subject: [PATCH 0768/1087] Give @Function annotation a language element The runtime support for assigning permissions by policy based on procedural language name presupposes that one can actually declare a function with a chosen language name, not just java or javaU. In passing, fix a bunch of places where PL/Java was spelled pljava in visible diagnostic messages from the annotation processor (and some in javadoc comments too). And add some missing javadoc paragraph breaks in @Function. --- .../pljava/annotation/Function.java | 27 ++++-- .../annotation/processing/DDRProcessor.java | 82 +++++++++++++------ 2 files changed, 79 insertions(+), 30 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java index fac1ae1a..f3f7c603 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -102,14 +102,14 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; /** * Estimated cost in units of cpu_operator_cost. - * + *

    * If left unspecified (-1) the backend's default will apply. */ int cost() default -1; /** * Estimated number of rows returned (only for functions returning set). - * + *

    * If left unspecified (-1) the backend's default will apply. */ int rows() default -1; @@ -137,7 +137,7 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; /** * What the query optimizer is allowed to assume about this function. - * + *

    * IMMUTABLE describes a pure function whose return will always be the same * for the same parameter values, with no side effects and no dependency on * anything in the environment. STABLE describes a function that has no @@ -155,6 +155,23 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; */ Trust trust() default Trust.SANDBOXED; + /** + * The name of the PostgreSQL procedural language to which this function + * should be declared, as an alternative to specifying {@link #trust trust}. + *

    + * Ordinarily, PL/Java installs two procedural languages, {@code java} and + * {@code javau}, and a function is declared in one or the other by giving + * {@code trust} the value {@code SANDBOXED} or {@code UNSANDBOXED}, + * respectively. It is possible to declare other named procedural languages + * sharing PL/Java's handler functions, and assign customized permissions + * to them in {@code pljava.policy}. A function can be declared in the + * specific language named with this element. + *

    + * It is an error to specify both {@code language} and {@code trust} in + * the same annotation. + */ + String language() default ""; + /** * Whether the function is UNSAFE to use in any parallel query plan at all * (the default), or avoids all disqualifying operations and so is SAFE to @@ -195,7 +212,7 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; * configuration_parameter FROM CURRENT. The latter will ensure that the * function executes with the same setting for configuration_parameter that * was in effect when the function was created. - * + *

    * Appeared in PostgreSQL 8.3. */ String[] settings() default {}; 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 7996615a..d49ead16 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 @@ -191,8 +191,8 @@ class DDRProcessorImpl // Options obtained from the invocation // - final String nameTrusted; - final String nameUntrusted; + final Identifier.Simple nameTrusted; + final Identifier.Simple nameUntrusted; final String output; final Identifier.Simple defaultImplementor; final boolean reproducible; @@ -255,15 +255,15 @@ class DDRProcessorImpl optv = opts.get( "ddr.name.trusted"); if ( null != optv ) - nameTrusted = optv; + nameTrusted = Identifier.Simple.fromJava(optv); else - nameTrusted = "java"; + nameTrusted = Identifier.Simple.fromJava("java"); optv = opts.get( "ddr.name.untrusted"); if ( null != optv ) - nameUntrusted = optv; + nameUntrusted = Identifier.Simple.fromJava(optv); else - nameUntrusted = "javaU"; + nameUntrusted = Identifier.Simple.fromJava("javaU"); optv = opts.get( "ddr.implementor"); if ( null != optv ) @@ -442,7 +442,7 @@ else if ( AN_SQLTYPE.equals( te) ) else { msg( Kind.WARNING, te, - "pljava annotation processor version may be older than " + + "PL/Java annotation processor version may be older than " + "this annotation:\n%s", te.toString()); willClaim = false; } @@ -793,18 +793,18 @@ void processUDT( Element e, UDTKind k) case ANNOTATION_TYPE: case ENUM: case INTERFACE: - msg( Kind.ERROR, e, "A pljava UDT must be a class"); + msg( Kind.ERROR, e, "A PL/Java UDT must be a class"); default: return; } Set mods = e.getModifiers(); if ( ! mods.contains( Modifier.PUBLIC) ) { - msg( Kind.ERROR, e, "A pljava UDT must be public"); + msg( Kind.ERROR, e, "A PL/Java UDT must be public"); } if ( mods.contains( Modifier.ABSTRACT) ) { - msg( Kind.ERROR, e, "A pljava UDT must not be abstract"); + msg( Kind.ERROR, e, "A PL/Java UDT must not be abstract"); } if ( ! ((TypeElement)e).getNestingKind().equals( NestingKind.TOP_LEVEL) ) @@ -812,13 +812,13 @@ void processUDT( Element e, UDTKind k) if ( ! mods.contains( Modifier.STATIC) ) { msg( Kind.ERROR, e, - "When nested, a pljava UDT must be static (not inner)"); + "When nested, a PL/Java UDT must be static (not inner)"); } for ( Element ee = e; null != ( ee = ee.getEnclosingElement() ); ) { if ( ! ee.getModifiers().contains( Modifier.PUBLIC) ) msg( Kind.ERROR, ee, - "A pljava UDT must not have a non-public " + + "A PL/Java UDT must not have a non-public " + "enclosing class"); if ( ((TypeElement)ee).getNestingKind().equals( NestingKind.TOP_LEVEL) ) @@ -891,7 +891,7 @@ ExecutableElement huntFor(List ees, String name, /** * Process a single element annotated with @Function. After checking that - * it has the right modifiers to be called via pljava, analyze its type + * it has the right modifiers to be called via PL/Java, analyze its type * information and annotations and register an appropriate SQL code snippet. */ void processFunction( Element e) @@ -909,7 +909,7 @@ void processFunction( Element e) Set mods = e.getModifiers(); if ( ! mods.contains( Modifier.PUBLIC) ) { - msg( Kind.ERROR, e, "A pljava function must be public"); + msg( Kind.ERROR, e, "A PL/Java function must be public"); } for ( Element ee = e; null != ( ee = ee.getEnclosingElement() ); ) @@ -918,7 +918,7 @@ void processFunction( Element e) { if ( ! ee.getModifiers().contains( Modifier.PUBLIC) ) msg( Kind.ERROR, ee, - "A pljava function must not have a non-public " + + "A PL/Java function must not have a non-public " + "enclosing class"); if ( ((TypeElement)ee).getNestingKind().equals( NestingKind.TOP_LEVEL) ) @@ -1581,6 +1581,10 @@ class FunctionImpl public String[] provides() { return _provides; } public String[] requires() { return _requires; } public Trigger[] triggers() { return _triggers; } + public String language() + { + return _languageIdent.toString(); + } ExecutableElement func; @@ -1601,6 +1605,8 @@ class FunctionImpl public String[] _requires; Trigger[] _triggers; + public Identifier.Simple _languageIdent; + boolean complexViaInOut = false; boolean setof = false; TypeMirror setofComponent = null; @@ -1618,6 +1624,19 @@ class FunctionImpl func = e; } + public void setTrust( Object o, boolean explicit, Element e) + { + if ( explicit ) + _trust = Trust.valueOf( + ((VariableElement)o).getSimpleName().toString()); + } + + public void setLanguage( Object o, boolean explicit, Element e) + { + if ( explicit ) + _languageIdent = Identifier.Simple.fromJava((String)o); + } + public void setCost( Object o, boolean explicit, Element e) { _cost = ((Integer)o).intValue(); @@ -1650,10 +1669,12 @@ public boolean characterize() if ( "".equals( _name) ) _name = func.getSimpleName().toString(); + resolveLanguage(); + Set mods = func.getModifiers(); if ( ! mods.contains( Modifier.STATIC) ) { - msg( Kind.ERROR, func, "A pljava function must be static"); + msg( Kind.ERROR, func, "A PL/Java function must be static"); } TypeMirror ret = func.getReturnType(); @@ -1756,6 +1777,20 @@ else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) return true; } + void resolveLanguage() + { + if ( null != _trust && null != _languageIdent ) + msg( Kind.ERROR, func, "A PL/Java function may specify " + + "only one of trust, language"); + if ( null == _languageIdent ) + { + if ( null == _trust || Trust.SANDBOXED == _trust ) + _languageIdent = nameTrusted; + else + _languageIdent = nameUntrusted; + } + } + /* * Factored out of characterize() so it could be called if needed by * BaseUDTFunctionImpl.characterize(), which does not need anything else @@ -1942,10 +1977,7 @@ public String[] deployStrings() sb.append( returnType); } sb.append( "\n\tLANGUAGE "); - if ( Trust.SANDBOXED.equals( trust()) ) - sb.append( nameTrusted); - else - sb.append( nameUntrusted); + sb.append( _languageIdent.toString()); sb.append( ' ').append( effects()); if ( leakproof() ) sb.append( " LEAKPROOF"); @@ -2093,7 +2125,6 @@ class BaseUDTFunctionImpl extends FunctionImpl _onNullInput = OnNullInput.CALLED; _security = Security.INVOKER; _effects = Effects.VOLATILE; - _trust = Trust.SANDBOXED; _parallel = Parallel.UNSAFE; _leakproof = false; _settings = new String[0]; @@ -2132,6 +2163,7 @@ StringBuilder appendTypeOp( StringBuilder sb) @Override public boolean characterize() { + resolveLanguage(); recordImplicitTags(); recordExplicitTags(_provides, _requires); return true; @@ -2223,7 +2255,7 @@ abstract class AbstractUDTImpl if ( ! typu.isAssignable( e.asType(), TY_SQLDATA) ) { - msg( Kind.ERROR, e, "A pljava UDT must implement %s", + msg( Kind.ERROR, e, "A PL/Java UDT must implement %s", TY_SQLDATA); } @@ -2234,7 +2266,7 @@ abstract class AbstractUDTImpl if ( null == niladicCtor ) { msg( Kind.ERROR, tclass, - "A pljava UDT must have a public no-arg constructor"); + "A PL/Java UDT must have a public no-arg constructor"); } } @@ -2474,7 +2506,7 @@ void registerFunctions() if ( null == staticParse ) { msg( Kind.ERROR, tclass, - "A pljava UDT must have a public static " + + "A PL/Java UDT must have a public static " + "parse(String,String) method that returns the UDT"); } else @@ -2743,7 +2775,7 @@ private boolean mappingsFrozen() * to avoid looking up the Classes later and getting different * mirrors. * - * This should work as long as all the sources containg pljava + * This should work as long as all the sources containg PL/Java * annotations will be found in round 1. That would only not be the case * if some other annotation processor is in use that could generate new * sources with pljava annotations in them, requiring additional rounds. From a09e1c6900daa2ce71f6c840d0d4f9de40f4d78c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Sep 2020 21:15:31 -0400 Subject: [PATCH 0769/1087] Some selective linting near recent changes --- pljava-api/src/main/java/module-info.java | 2 +- .../annotation/processing/DDRProcessor.java | 15 +++++++++------ .../org/postgresql/pljava/sqlgen/Lexicals.java | 3 ++- .../org/postgresql/pljava/internal/Function.java | 16 ++++++++-------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/pljava-api/src/main/java/module-info.java b/pljava-api/src/main/java/module-info.java index 95c01177..fbfcb8bd 100644 --- a/pljava-api/src/main/java/module-info.java +++ b/pljava-api/src/main/java/module-info.java @@ -18,7 +18,7 @@ { requires java.base; requires transitive java.sql; - requires java.compiler; + requires transitive java.compiler; exports org.postgresql.pljava; exports 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 d49ead16..63c71670 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 @@ -3972,9 +3972,9 @@ public int hashCode() static final class Named extends DBType { - private final Identifier.Qualified m_ident; + private final Identifier.Qualified m_ident; - Named(Identifier.Qualified ident) + Named(Identifier.Qualified ident) { m_ident = ident; } @@ -4234,19 +4234,22 @@ public boolean equals(Object o, Messager msgr) } } - static final class Type extends Named + static final class Type + extends Named> { - Type(Identifier.Qualified value) + Type(Identifier.Qualified value) { super(requireNonNull(value)); } } - static final class Function extends Named + static final class Function + extends Named> { private DBType[] m_signature; - Function(Identifier.Qualified value, DBType[] signature) + Function( + Identifier.Qualified value, DBType[] signature) { super(requireNonNull(value)); m_signature = signature.clone(); diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index d024a33f..f6335e13 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -1273,6 +1273,7 @@ public static Qualified nameFromJava(String s) * or null if not in a compilation context. * @return the Identifier.Qualified<Simple> */ + @SuppressWarnings("fallthrough") public static Qualified nameFromJava( String s, Messager msgr) { @@ -1483,7 +1484,7 @@ public boolean equals(Object other, Messager msgr) { if ( ! (other instanceof Qualified) ) return false; - Qualified oi = (Qualified)other; + Qualified oi = (Qualified)other; return (null == m_qualifier ? null == oi.m_qualifier diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index c0b2168f..a1de2709 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -194,7 +194,7 @@ private static MethodType buildSignature( if ( ! isMultiCall && retTypeIsOutParameter ) ++ rtIdx; - Class[] pTypes = new Class[ rtIdx ]; + Class[] pTypes = new Class[ rtIdx ]; for ( int i = 0 ; i < rtIdx ; ++ i ) pTypes[i] = loadClass(schemaLoader, jTypes[i]); @@ -1112,7 +1112,7 @@ private static void paramCountsAre(short counts) * The access control context of the {@code Invocable} returned here is used * at the corresponding entry point; the payload is not. */ - private static Invocable udtWriteHandle( + private static Invocable udtWriteHandle( Class clazz, String language, boolean trusted) throws SQLException { @@ -1127,7 +1127,7 @@ private static Invocable udtWriteHandle( * The access control context of the {@code Invocable} returned here is used * at the corresponding entry point; the payload is not. */ - private static Invocable udtToStringHandle( + private static Invocable udtToStringHandle( Class clazz, String language, boolean trusted) throws SQLException { @@ -1149,7 +1149,7 @@ private static Invocable udtToStringHandle( * assigned here will be in effect for both the constructor and the * {@code readSQL} call. */ - private static Invocable udtReadHandle( + private static Invocable udtReadHandle( Class clazz, String language, boolean trusted) throws SQLException { @@ -1180,7 +1180,7 @@ private static Invocable udtReadHandle( * a NUL-terminated storage form, so it gets its own dedicated entry point * and does not use the static parameter area. */ - private static Invocable udtParseHandle( + private static Invocable udtParseHandle( Class clazz, String language, boolean trusted) throws SQLException { @@ -1212,7 +1212,7 @@ private static Invocable udtParseHandle( * {@code Invocable} for invoking the method, or null in the * case of a UDT. */ - public static Invocable create( + public static Invocable create( long wrappedPtr, ResultSet procTup, String langName, String schemaName, boolean trusted, boolean calledAsTrigger, boolean forValidator, boolean checkBody) @@ -1253,7 +1253,7 @@ private static Matcher parse(ResultSet procTup) throws SQLException * @return an Invocable to invoke the implementing method, or * null in the case of a UDT */ - private static Invocable init( + private static Invocable init( long wrappedPtr, Matcher info, ResultSet procTup, Identifier.Simple schema, boolean calledAsTrigger, boolean forValidator, String language, boolean trusted) @@ -1836,7 +1836,7 @@ static class TypeBindings implements Type } } - Type resolve(TypeVariable v) + Type resolve(TypeVariable v) { for ( int i = 0; i < formalTypeParams.length; ++ i ) if ( formalTypeParams[i].equals(v) ) From 48e7fb5f689197b9c84ffb9b9daa30dcb6780a6c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 9 Oct 2020 20:56:20 -0400 Subject: [PATCH 0770/1087] Document --- .../src/main/resources/pljava.policy | 78 +++-- src/site/markdown/use/policy.md | 330 ++++++++++++++++++ src/site/markdown/use/use.md | 6 + src/site/markdown/use/variables.md | 35 ++ 4 files changed, 427 insertions(+), 22 deletions(-) create mode 100644 src/site/markdown/use/policy.md diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index 6e8c3982..6e8f44f0 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -1,7 +1,17 @@ +// +// Security policy for PL/Java. These grants are intended to add to those +// contained in the java.policy file of the standard Java installation. +// + + +// +// This grant is unconditional. It adds these properties to the standard Java +// list of system properties that any code may read. +// grant { // "standard" properties that can be read by anyone, by analogy to the - // ones so treated in Java itself - + // ones so treated in Java itself. + // permission java.util.PropertyPermission "org.postgresql.version", "read"; permission java.util.PropertyPermission @@ -15,19 +25,28 @@ grant { permission java.util.PropertyPermission "org.postgresql.server.encoding", "read"; - // PostgreSQL allows SELECT current_database() or SHOW cluster_name anyway - + // PostgreSQL allows SELECT current_database() or SHOW cluster_name anyway. + // permission java.util.PropertyPermission "org.postgresql.database", "read"; permission java.util.PropertyPermission "org.postgresql.cluster", "read"; - // SQL/JRT specifies this property - + // SQL/JRT specifies this property. + // permission java.util.PropertyPermission "sqlj.defaultconnection", "read"; }; + +// +// This grant is specific to the internal implementation of PL/Java itself, +// which needs these permissions for its own operations. +// +// Historically, PL/Java has been able to read any file on the server filesystem +// when a file: URL is passed to sqlj.install_jar or sqlj.replace_jar. Such a +// broad grant is not necessary, and can be narrowed below if desired. +// grant codebase "${org.postgresql.pljava.codesource}" { permission java.lang.RuntimePermission "createClassLoader"; @@ -38,39 +57,54 @@ grant codebase "${org.postgresql.pljava.codesource}" { // This gives the PL/Java implementation code permission to read // any file, which it only exercises on behalf of sqlj.install_jar() - // or sqlj.replace_jar() calls with a file: URL. + // or sqlj.replace_jar() when called with a file: URL. + // // There would be nothing wrong with restricting this permission to // a specific directory, if all jar files to be loaded will be found there, // or replacing it with a URLPermission if they will be hosted on a remote // server, etc. + // permission java.io.FilePermission "<>", "read"; }; -// These grants apply to the supplied examples, if sqlj.install_jar is given the -// exact name 'examples' as the desired jar name. (Otherwise, they will apply to + +// +// This grant applies to the supplied examples, if sqlj.install_jar is given the +// exact name 'examples' as the desired jar name. (Otherwise, it will apply to // any other jar that is installed with the name 'examples'. Beware!) +// grant codebase "sqlj:examples" { - // the PreJSR310 test involves setting the time zone + + // The PreJSR310 test involves setting the time zone. + // permission java.util.PropertyPermission "user.timezone", "write"; }; -// This grant defines the mapping onto Java of PostgreSQL's "untrusted language" -// category. When PL/Java executes a function whose SQL declaration names a -// language that was declared without the TRUSTED keyword, it will have these -// permissions (in addition to whatever others might be granted to all code, or -// to its specific jar, etc.). + // -grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed * { - permission java.io.FilePermission - "<>", "read,write,delete,readlink"; +// This grant defines the mapping onto Java of PostgreSQL's "trusted language" +// category. When PL/Java executes a function whose SQL declaration names +// a language that was declared WITH the TRUSTED keyword, it will have these +// permissions, if any (in addition to whatever others might be granted to all +// code, or to its specific jar, etc.). +// +grant principal org.postgresql.pljava.PLPrincipal$Sandboxed * { }; -// This grant defines the mapping onto Java of PostgreSQL's "trusted language" -// category. When PL/Java executes a function whose SQL declaration names a -// language that was declared WITH the TRUSTED keyword, it will have these + +// +// This grant defines the mapping onto Java of PostgreSQL's "untrusted language" +// category. When PL/Java executes a function whose SQL declaration names +// a language that was declared WITHOUT the TRUSTED keyword, it will have these // permissions (in addition to whatever others might be granted to all code, or // to its specific jar, etc.). // -grant principal org.postgresql.pljava.PLPrincipal$Sandboxed * { +grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed * { + + // Java does not circumvent operating system access controls; this grant + // will still be limited to what the OS allows a PostgreSQL backend process + // to do. + permission java.io.FilePermission + "<>", "read,readlink,write,delete"; }; diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md new file mode 100644 index 00000000..01055619 --- /dev/null +++ b/src/site/markdown/use/policy.md @@ -0,0 +1,330 @@ +# Configuring permissions in PL/Java + +## `TRUSTED` (and untrusted) procedural languages + +PostgreSQL allows a procedural language to be installed with or without +the designation `TRUSTED`. For a language designated `TRUSTED`, functions +can be created in that language by any user (PostgreSQL role) with `USAGE` +permission on that language, as configured with the SQL commands +`GRANT USAGE ON LANGUAGE ...` and `REVOKE USAGE ON LANGUAGE ...`. For a +language that is _not_ designated `TRUSTED`, only a database superuser +may create functions that use it, no matter who has been granted `USAGE` +on it. + +In either case, once any function has been created, that function may +be executed by any user/role granted `EXECUTE` permission on the function +itself; a language's `USAGE` privilege (plus superuser status, if the language +is not `TRUSTED`) is only needed to create a function that uses the language. + +Because PL functions execute in the database server, a general-purpose +programming language with no restrictions on access to its containing process +or the server file system may be used for actions or access that PostgreSQL +would normally not permit. A superuser can implement such a function using +a non-`TRUSTED` PL, and design the function to enforce its own limits and +be safe for use by whatever roles will be granted `EXECUTE` permission on it. + +A `TRUSTED` PL is expected to enforce appropriate restrictions so that +non-superusers can be allowed to use it to create functions on their own, +while still subject to PostgreSQL's normal protections. + +Both kinds have their uses, and many of the available PLs, including PL/Java, +install two similarly-named 'languages' to permit both. Although either can be +renamed, a normal installation of PL/Java will create the language `java` with +the `TRUSTED` property, and `javaU` without it. + +*Note: like any SQL identifier, these language names are case-insensitive +when not quoted, and are stored in lowercase in PostgreSQL. The spelling with +capital `U` for untrusted is a common convention.* + +### `TRUSTED`/untrusted versus sandboxed/unsandboxed + +In various places in PL/Java's API, and in the sections below, the words +'sandboxed' or 'unsandboxed' are used in place of the PostgreSQL `TRUSTED` or +untrusted, respectively. That choice reflects a little trick of language some +readers may notice when new to PostgreSQL: it is about equally easy to read +'trusted'/'untrusted' in two opposite ways. (Is this language trusted because of +how tightly I restrict it? Or do I restrict it less tightly because I trust it? +Is it like a teenager with the car keys?) Old hands at PostgreSQL know which +reading is correct, but because some users of PL/Java may be old hands at Java +and newcomers to PostgreSQL, it seems safer for PL/Java to use terms that +should give the right idea to readers in both groups. + +## Permissions available in sandboxed/unsandboxed PL/Java + +Most PLs that offer both variants, including PL/Java before 1.6, hardcode +the differences between what a function in each language is allowed to do. +The sandboxed language would apply a fixed set of limitations, such as +forbidding access to the server's file system, and those limits were +not adjustable. + +A needed function that would only access one, specific, known-safe file, or +perhaps would need no file access but have to make a network connection to one +known server, might _almost_ be written under those predetermined restrictions, +but that wouldn't count. It would simply have to be created for the unsandboxed +language instead, and written defensively against a much wider range of possible +misuses or mistakes. + +Beginning with 1.6, PL/Java takes a more configurable approach. Using the Java +[policy file syntax][pfsyn], any of the permissions known to the JDK can +be granted to chosen Java code. The default policy file installed with PL/Java +includes these lines: + +``` +grant principal org.postgresql.pljava.PLPrincipal$Sandboxed * { +}; + +grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed * { + permission java.io.FilePermission + "<>", "read,write,delete,readlink"; +}; +``` + +A few observations fall out. Whatever the names may suggest, neither alternative +is truly "unsandboxed". Both are subject to the same Java policy, but can +be granted different permissions within it. + +As distributed, the only difference between the two is access to the filesystem. +The "sandboxed" case grants no additional permissions at all, and the +"unsandboxed" case adds read, readlink, write, and delete permission for any +file (still subject to the operating system permissions in effect for the +PostgreSQL server process, which will be enforced independently of Java). + +The permissions granted for either case are freely configurable. Granting +the more lenient or dangerous permissions to the "unsandboxed" language is +conventional, and reflected in the way PostgreSQL is more restrictive about +what roles can create functions in that language. + +The [permissions known to the JDK][jdkperms] are plentiful and fine-grained. +New permissions can also be defined and required in custom code, and selectively +granted in the policy like any other permission. + +The `PLPrincipal` indicating sandboxed/unsandboxed is only one of the conditions +that can be referred to in a policy to control the permissions granted. Others +are described below. + +## Sources of Java policy + +Java's standard `Policy` implementation will read from a sequence of policy +files specified as URLs. The first is normally part of the Java installation, +supplying permission grants necessary for trouble-free operation of the JVM +itself, and a second will be read, if present, from a user's home directory. + +PL/Java, by default, uses the first Java-supplied URL, for the policy file +installed with Java, followed by the file `pljava.policy` in the directory +reported by `pg_config --sysconfdir`. A default version of that file is +installed with PL/Java. + +The `pljava.policy` file, by default, is used _instead of_ any `.java.policy` +file in the OS user's home directory that Java would normally load. There +probably is no such file in the `postgres` user's home directory, and if +for any reason there is one, it probably is not tailored to PL/Java. + +The [configuration variable][confvar] `pljava.policy_urls` can be +used to name different, or additional, policy files. + +Permission grants are cumulative in Java's standard `Policy` implementation: +there is no policy syntax to _deny_ a permission if it is conveyed by some other +applicable grant in any of the files on the `policy_urls` list. If an +application must restrict a permission that is granted unconditionally +in the Java-supplied policy file, for example, the typical approach would be +to copy that file, remove the grant of that permission, and alter +`pljava.policy_urls` to read the modified file in place of the original. + +## Conditional and unconditional permission grants + +A `grant` in a policy can be unconditional, for example: + +``` +grant { + permission java.util.PropertyPermission + "sqlj.defaultconnection", "read"; +}; +``` + +That grant (which is included in the default `pljava.policy`) allows any Java +code to read that property. + +Conditional grants to `PLPrincipal$Sandboxed` and `PLPrincipal$Unsandboxed` were +shown above. + +It is also possible to condition a grant on the codebase (represented as +a URL) of the code being executed. If the `SQLJ.INSTALL_JAR` function is used +to install PL/Java's examples jar under the name `examples`, this grant will +allow the JSR-310 test example to work: + +``` +grant codebase "sqlj:examples" { + permission java.util.PropertyPermission "user.timezone", "write"; +}; +``` + +The `sqlj` URL scheme is (trivially, and otherwise nonfunctionally) defined +within PL/Java to allow forming a codebase URL from the name of an installed +jar. + +### Grant conditions currently unsupported + +A reader familiar with Java security policy may consider granting permissions +based on the signer identity of a cryptographically signed jar, or on a +`Principal` representing the PostgreSQL role executing the current function. +In this version of PL/Java, such grants are not yet supported. + +While it is not yet possible to grant permissions based on a principal +representing the PostgreSQL session user or role, it is possible for +a superuser, with `ALTER ROLE ... SET`, to set user-specific values of +`pljava.policy_urls` that will load different, or additional, policy files. +While that will only reflect the connected user at the start of the session +and not any role changes during the session, it may be enough for some uses. + +### `PLPrincipal` with a language name + +The grants for sandboxed/unsandboxed shown above have a `*` wildcard after +the principal class name. It is possible to replace the wildcard with the name +of the language (as used in SQL with `CREATE LANGUAGE` and `CREATE FUNCTION`) +in which a function is declared. + +A basic installation of PL/Java creates just two named languages, `java` and +`javaU`, declared as `TRUSTED`/sandboxed and untrusted/unsandboxed, +respectively. In such an installation, these grants would be effectively +equivalent to those shown earlier: + +``` +grant principal org.postgresql.pljava.PLPrincipal$Sandboxed "java" { +}; + +grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed "javaU" { + + // Java does not circumvent operating system access controls; + // this grant will still be limited to what the OS allows a + // PostgreSQL backend process to do. + permission java.io.FilePermission + "<>", "read,readlink,write,delete"; +}; +``` + +However, it is possible to use `CREATE LANGUAGE` to create any number of +named languages that share PL/Java's handler entries and can be used to +declare PL/Java functions. For example, suppose `CREATE TRUSTED LANGUAGE` is +used to create another language entry with the name `java_tzset` and this +grant is included in the policy: + +``` +grant principal org.postgresql.pljava.PLPrincipal$Sandboxed "java_tzset" { + permission java.util.PropertyPermission "user.timezone", "write"; +}; +``` + +If the JSR-310 test example in PL/Java's examples jar is declared with +`LANGUAGE java_tzset` rather than `LANGUAGE java`, it will be able to set +the time zone and succeed. + +When grants to specific named languages and grants with the wildcard are +present, code will have all of the permissions granted to the specific +language by name, in addition to all permissions that appear in grants to the +language class (`PLPrincipal$Sandboxed` or `PLPrincipal$Unsandboxed`, whichever +applies) with a wildcard name. + +A grant is silently ignored unless the class and the name both match. If the +`java_tzset` language were declared as above but a grant entry used the right +name but the `PLPrincipal$Unsandboxed` class by mistake, that grant would be +silently ignored. + +### Grants to a codebase compared with grants to a principal + +Whenever a Java operation requires a permission check, it could be on a call +stack several levels deep, perhaps involving code from more than one codebase +(or, more generally, "protection domain"). The Java rule is that the needed +permission must be in effect, one way or another, for every protection domain +on the call stack at the point where the permission is needed. In other words, +the available permissions are the _intersection_, over all domains on the stack, +of the permissions in effect for each domain. The rationale is that the proposed +action must not only be something the currently executing method is allowed to +do; there is a calling method causing this method to do it, so it must also be +something the caller is allowed to do, and so on up the stack. (For one crucial +exception to this rule, see [handling privileges][dopriv].) + +Permissions granted to a `Principal` are not so tightly bound to what specific +code is executing; the same code may execute at different times on behalf of +more than one principal. A principal often represents a user or role for whom +the code is executing, though role principals are not implemented in this +PL/Java release. The sandboxed/unsandboxed function distinction is represented +as a kind of `Principal` because it, too, is a property of the thread of +execution, from its entry at the SQL-declared function entry point and through +any number of protection domains the thread may traverse. Any permissions +granted by principal may be thought of as combined with any codebase-specific +permissions in every domain present on the stack. + +### Entry points other than SQL-declared functions + +Not every entry into PL/Java is through an SQL-declared function with an +associated language name or sandboxed/unsandboxed property. For those that are +not, permission decisions are based on an "access control context" +(essentially, the in-effect `Principal`s and initial protection domains) +constructed as described here. + +#### Set-returning functions + +While a set-returning function _is_ declared as an SQL function, the +initial call is followed by repeated calls to the returned iterator or +ResultSet provider or handle, and a final call to close the provider or handle. +The access control context constructed for the initial call is saved, and reused +while iterating and closing. + +#### Savepoint and transaction listeners + +Java code may register listeners for callbacks at lifecycle stages of savepoints +or transactions. Each callback will execute in the access control context of +the code that registered it, except that PL/Java's own domain will also be +represented on the stack. Because effective permissions are an intersection +over all domains on the stack, if any permission has been granted to the +callback's codebase that is not also granted to PL/Java's own code, the +callback code will be unable to exercise that permission except within +a [`doPrivileged`][dopriv] block. + +#### Mapped UDT `readSQL`/`writeSQL` methods + +When a Java user-defined type is defined without fully integrating it into +PostgreSQL's type system as a `BaseUDT`, its `readSQL` and `writeSQL` methods +will not have corresponding SQL function declarations, but will be called +directly as PL/Java converts values between PostgreSQL and Java form. Those +calls will be made without any `PLPrincipal`, sandboxed or unsandboxed, so +they will execute with only the permissions granted to their codebase or +unconditionally. + +The conversion functions for a `BaseUDT` do have SQL function declarations, and +will execute in a context constructed based on the declaration in the usual way. + +### SQL-declared functions not in PL/Java-managed jars + +It is possible to issue an SQL `CREATE FUNCTION` naming a method from a codebase +that is not a PL/Java-managed `sqlj:` jar, such as a jar on the filesystem +module path, or a method of the Java runtime itself. For example, many how-to +articles can be found on the web that demonstrate a successful PL/Java +installation by declaring an SQL function that directly calls +`java.lang.System.getProperty`. + +Such declarations are allowed, but will execute as if called from a protection +domain with no permissions other than those the policy grants unconditionally. +If the target method is in a Java runtime class that Java's bootstrap loader +loads, it will also not be able to exercise permissions granted by principal +(because a bootstrap class has no protection domain with which the per-principal +permissions could be combined). + +_Note: many of the how-to articles that can be found on the +web happen to demonstrate their `System.getProperty`-calling example functions +on some property that isn't readable under Java's default policy. +Those examples should be changed to use a property that is normally readable, +such as `java.version` or `org.postgresql.pljava.version`._ + +## Forward compatibility + +The current implementation makes use of the Java classes +`Subject` and `SubjectDomainCombiner` in the `javax.security.auth` package. +That should be regarded as an implementation detail; it may change in a future +release, so relying on it is not recommended. + + +[pfsyn]: https://docs.oracle.com/en/java/javase/14/security/permissions-jdk1.html#GUID-7942E6F8-8AAB-4404-9FE9-E08DD6FFCFFA +[jdkperms]: https://docs.oracle.com/en/java/javase/14/security/permissions-jdk1.html#GUID-1E8E213A-D7F2-49F1-A2F0-EFB3397A8C95 +[confvar]: variables.html +[dopriv]: https://docs.oracle.com/en/java/javase/14/security/java-se-platform-security-architecture.html#GUID-E8898CB5-65BB-4D1A-A574-8F7112FC353F diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index 67622f95..5ed1cec5 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -36,6 +36,12 @@ PL/Java's own. ## Special topics +### Configuring permissions + +The permissions in effect for PL/Java functions can be tailored, independently +for functions declared to the `TRUSTED` or untrusted language, as described +[here](policy.html). + ### Choices when mapping data types #### Date and time types diff --git a/src/site/markdown/use/variables.md b/src/site/markdown/use/variables.md index 19db9eba..ba3bf046 100644 --- a/src/site/markdown/use/variables.md +++ b/src/site/markdown/use/variables.md @@ -107,6 +107,40 @@ These PostgreSQL configuration variables can influence PL/Java's operation: For more on PL/Java's "module path" and "class path", see [PL/Java and the Java Platform Module System](jpms.html). +`pljava.policy_urls` +: A list of URLs to Java security [policy files](policy.html) determining + the permissions available to PL/Java functions. Each URL should be + enclosed in double quotes; any double quote that is literally part of + the URL may be represented as two double quotes (in SQL style) or as + `%22` in the URL convention. Between double-quoted URLs, a comma is the + list delimiter. + + The Java installation's `java.security` file usually defines two policy + file locations: + + 0. A systemwide policy from the Java vendor, sufficient for the Java runtime + itself to function as expected + 0. A per-user location, where a policy file, if found, can add to the policy + from the systemwide file. + + The list in `pljava.policy_urls` will modify the list from the Java + installation, by default after the first entry, keeping the Java-supplied + systemwide policy but replacing the customary per-user file (there + probably isn't one in the home of the `postgres` user, and if there is + it is probably not tailored for PL/Java). + + Any entry in this list can start with _n_`=` (inside the quotes) for a + positive integer _n_, to specify which entry of Java's policy location list + it will replace (entry 1 is the systemwide policy, 2 the customary user + location). URLs not prefixed with _n_`=` will follow consecutively. If the + first entry is not so prefixed, `2=` is assumed. + + A final entry of `=` (in the required double quotes) will prevent + use of any remaining entries in the Java site-configured list. + + This setting defaults to + `"file:${org.postgresql.sysconfdir}/pljava.policy","="` + `pljava.release_lingering_savepoints` : How the return from a PL/Java function will treat any savepoints created within it that have not been explicitly either released (the savepoint @@ -149,3 +183,4 @@ These PostgreSQL configuration variables can influence PL/Java's operation: [jow]: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html [jou]: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html [vmop]: ../install/vmoptions.html + From bae4602d0a96f3fa42ac5a288526cfa53138399f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Oct 2020 17:49:49 -0400 Subject: [PATCH 0771/1087] Build lids with principals The fact that a SubjectDomainCombiner does not combine its Subject into the domains of the /inherited/ AccessControlContext leads to behavior that would strain the principle of least astonishment: an SQL declaration of, say, a Java API function would execute as if it lacked the permissions granted to the Principal that should clearly, by inspection of the SQL declaration, be supplied. One way to tell when you've violated the POLA is when the effort to write a documentation paragraph that explains how the thing /does/ behave is more painful than just making it behave the way you'd expect. The lidWithPrincipals method could perhaps be more efficient, but the case of SQL declarations for functions in non-PL/Java-managed jars isn't expected to be common. --- .../postgresql/pljava/internal/Function.java | 70 ++++++++++++------- src/site/markdown/use/policy.md | 7 +- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index a1de2709..0251318b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1039,34 +1039,52 @@ mh, empty(methodType(Invocable.class, } /* - * The lid is a "nobody special" AccessControlContext: it isn't allowed - * any permission that isn't granted by the Policy to everybody. - * - * A null CodeSource is too strict; if your code source is null, you are - * somebody special in a bad way: no dynamic permissions for you! At - * least according to the default policy provider. - * - * So, to achieve mere "nobody special"-ness requires a real CodeSource - * with null URL and null code signers. - * - * The ProtectionDomain constructor allows the permissions parameter - * to be null, and says so in the javadocs. It seems to allow - * the principals parameter to be null too, but doesn't say that, so why - * take a chance? - * * An empty ProtectionDomain array is all it takes to make s_noLid. * (As far as doPrivileged is concerned, a null AccessControlContext has * the same effect, but one can't attach a DomainCombiner to that.) + * + * A lid is a bit more work, but there's a method for that. */ - s_lid = - new AccessControlContext(new ProtectionDomain[] { - new ProtectionDomain( - new CodeSource(null, (CodeSigner[])null), - null, ClassLoader.getSystemClassLoader(), - new Principal[0]) - }); - s_noLid = - new AccessControlContext(new ProtectionDomain[] {}); + s_noLid = new AccessControlContext(new ProtectionDomain[] {}); + s_lid = lidWithPrincipals(new Principal[0]); + } + + /** + * Construct a 'lid' {@code AccessControlContext}, optionally with + * associated {@code Principal}s. + *

    + * A 'lid' is a "nobody special" {@code AccessControlContext}: it isn't + * allowed any permission that isn't granted by the Policy to everybody, + * unless it also has a nonempty array of principals. With an empty array, + * there need be only one such lid, so it can be kept in a static. + *

    + * This method also allows creating a lid with associated principals, + * because a {@code SubjectDomainCombiner} does not combine its subject into + * the domains of its inherited {@code AccessControlContext}, and + * that strains the principle of least astonishment if the code is being + * invoked through an SQL declaration that one expects would have a + * {@code PLPrincipal} associated. + *

    + * A null CodeSource is too strict; if your code source is null, you are + * somebody special in a bad way: no dynamic permissions for you! At + * least according to the default policy provider. + *

    + * So, to achieve mere "nobody special"-ness requires a real CodeSource + * with null URL and null code signers. + *

    + * The ProtectionDomain constructor allows the permissions parameter + * to be null, and says so in the javadocs. It seems to allow + * the principals parameter to be null too, but doesn't say that, so an + * array will always be expected here. + */ + private static AccessControlContext lidWithPrincipals(Principal[] ps) + { + return new AccessControlContext(new ProtectionDomain[] { + new ProtectionDomain( + new CodeSource(null, (CodeSigner[])null), + null, ClassLoader.getSystemClassLoader(), + Objects.requireNonNull(ps)) + }); } /** @@ -1366,7 +1384,9 @@ private static AccessControlContext accessControlContextFor( AccessControlContext acc = clazz.getClassLoader() instanceof Loader ? s_noLid // policy already applies appropriate permissions - : s_lid; // put a lid on permissions if calling JRE directly + : p.isEmpty() // put a lid on permissions if calling JRE directly + ? s_lid + : lidWithPrincipals(p.toArray(new Principal[1])); /* * A cache to avoid the following machinations might be good. diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index 01055619..f343d969 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -304,11 +304,8 @@ installation by declaring an SQL function that directly calls `java.lang.System.getProperty`. Such declarations are allowed, but will execute as if called from a protection -domain with no permissions other than those the policy grants unconditionally. -If the target method is in a Java runtime class that Java's bootstrap loader -loads, it will also not be able to exercise permissions granted by principal -(because a bootstrap class has no protection domain with which the per-principal -permissions could be combined). +domain with the same `Principal`s, if any, that PL/Java would normally supply, +and no other permissions but those the policy grants unconditionally. _Note: many of the how-to articles that can be found on the web happen to demonstrate their `System.getProperty`-calling example functions From 677f433ca6d9316fd41f8a452aeaac8c0e108cd9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Oct 2020 18:30:41 -0400 Subject: [PATCH 0772/1087] Make validator more aggressive at class loading In Hotspot, the validator showed the welcome behavior of rejecting a function declaration if a dependent jar was missing; it seems Hotspot would do enough resolution at the time of method-handle lookup to detect such an issue. Not so in OpenJ9, it turns out, where lazy resolution has been brought to such a pinnacle of refinement that you can declare functions in the S9 example class, load it, and even get their method handles, all without the Saxon jar being present and without a peep from the validator. (Then of course it fails when you try to call the functions, which was what you wanted a validator to prevent in the first place.) So tweak Function.loadClass to force initialization of the loaded class if called with forValidator true, in an effort to front-load the detection of about as many possible problems as can be. As a bonus, this improves validation error reporting in Hotspot too. Previously you could get a puzzling "unable to find method" for a method that's obviously there, with the real problem, not finding some class it refers to, buried in a couple layers of getCause. Now it is reported more clearly at the time of loading the class. --- .../postgresql/pljava/internal/Function.java | 39 +++++++++++-------- src/site/markdown/examples/examples.md.vm | 9 ++--- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 0251318b..31cc1a3b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -159,7 +159,7 @@ public static Class getClassIfUDT( Identifier.Simple schema = Identifier.Simple.fromCatalog(schemaName); return - loadClass(Loader.getSchemaLoader(schema), className) + loadClass(Loader.getSchemaLoader(schema), className, false) .asSubclass(SQLData.class); } @@ -170,7 +170,7 @@ public static Class getClassIfUDT( * The return type is the last element of {@code jTypes}. */ private static MethodType buildSignature( - ClassLoader schemaLoader, String[] jTypes, + ClassLoader schemaLoader, String[] jTypes, boolean forValidator, boolean retTypeIsOutParameter, boolean isMultiCall, boolean altForm) throws SQLException { @@ -197,10 +197,10 @@ private static MethodType buildSignature( Class[] pTypes = new Class[ rtIdx ]; for ( int i = 0 ; i < rtIdx ; ++ i ) - pTypes[i] = loadClass(schemaLoader, jTypes[i]); + pTypes[i] = loadClass(schemaLoader, jTypes[i], forValidator); Class returnType = - getReturnSignature(schemaLoader, retJType, + getReturnSignature(schemaLoader, retJType, forValidator, retTypeIsOutParameter, isMultiCall, altForm); return methodType(returnType, pTypes); @@ -220,7 +220,7 @@ private static MethodType buildSignature( * {@code ResultSetProvider} depending on {@code altForm}. */ private static Class getReturnSignature( - ClassLoader schemaLoader, String retJType, + ClassLoader schemaLoader, String retJType, boolean forValidator, boolean isComposite, boolean isMultiCall, boolean altForm) throws SQLException { @@ -228,7 +228,7 @@ private static Class getReturnSignature( { if ( isMultiCall ) return Iterator.class; - return loadClass(schemaLoader, retJType); + return loadClass(schemaLoader, retJType, forValidator); } /* The composite case */ @@ -273,12 +273,13 @@ private static Lookup lookupFor(Class clazz) */ private static MethodHandle getMethodHandle( ClassLoader schemaLoader, Class clazz, String methodName, + boolean forValidator, String[] jTypes, boolean retTypeIsOutParameter, boolean isMultiCall) throws SQLException { MethodType mt = - buildSignature(schemaLoader, jTypes, retTypeIsOutParameter, - isMultiCall, false); // first try altForm = false + buildSignature(schemaLoader, jTypes, forValidator, + retTypeIsOutParameter, isMultiCall, false); // try altForm false ReflectiveOperationException ex1 = null; try @@ -292,7 +293,8 @@ private static MethodHandle getMethodHandle( MethodType origMT = mt; Class altType = null; - Class realRetType = loadClass(schemaLoader, jTypes[jTypes.length-1]); + Class realRetType = + loadClass(schemaLoader, jTypes[jTypes.length-1], forValidator); /* COPIED COMMENT: * One valid reason for not finding the method is when @@ -318,8 +320,8 @@ private static MethodHandle getMethodHandle( if ( null != altType ) { jTypes[jTypes.length - 1] = altType.getCanonicalName(); - mt = buildSignature(schemaLoader, jTypes, retTypeIsOutParameter, - isMultiCall, true); // this time altForm = true + mt = buildSignature(schemaLoader, jTypes, forValidator, + retTypeIsOutParameter, isMultiCall, true); // retry altForm true try { MethodHandle h = @@ -1290,7 +1292,7 @@ private static Invocable init( boolean readOnly = ((byte)'v' != procTup.getByte("provolatile")); ClassLoader schemaLoader = Loader.getSchemaLoader(schema); - Class clazz = loadClass(schemaLoader, className); + Class clazz = loadClass(schemaLoader, className, forValidator); if ( isUDT ) { @@ -1326,7 +1328,7 @@ private static Invocable init( MethodHandle handle = adaptHandle( - getMethodHandle(schemaLoader, clazz, methodName, + getMethodHandle(schemaLoader, clazz, methodName, forValidator, resolvedTypes, retTypeIsOutParameter, isMultiCall) .asFixedArity() ); @@ -1592,9 +1594,12 @@ private static void parseParameters( * in explicit signatures in the AS string. Just a bit of gymnastics to * turn that form of name into the right class, including for primitives, * void, and arrays. + * + * @param forValidator if true, force initialization of the loaded class, in + * an effort to bring forward as many possible errors as can be. */ private static Class loadClass( - ClassLoader schemaLoader, String className) + ClassLoader schemaLoader, String className, boolean forValidator) throws SQLException { Matcher m = typeNameInAS.matcher(className); @@ -1616,12 +1621,12 @@ private static Class loadClass( default: try { - c = schemaLoader.loadClass(className); + c = Class.forName(className, forValidator, schemaLoader); } - catch ( ClassNotFoundException e ) + catch ( ClassNotFoundException | LinkageError e ) { throw new SQLNonTransientException( - "No such class: " + className, "46103", e); + "Resolving class " + className + ": " + e, "46103", e); } } diff --git a/src/site/markdown/examples/examples.md.vm b/src/site/markdown/examples/examples.md.vm index 933ab546..84ded104 100644 --- a/src/site/markdown/examples/examples.md.vm +++ b/src/site/markdown/examples/examples.md.vm @@ -126,7 +126,7 @@ The Saxon example [documentation is here](../examples/saxon.html). [Saxon-HE]: http://www.saxonica.com/html/products/products.html -$h2 Unable to find class or method (message when installing examples) +$h2 Exception resolving class or method (message when installing examples) As described above, there are some optionally-built examples included in the source. For example, there are XML Query examples that are not built @@ -135,10 +135,9 @@ by default because they depend on the Saxon jar. If your examples jar was built with the optional examples enabled, then PL/Java will normally validate that all of the functions it creates can be used. That validation will fail if a needed dependency, such as the Saxon jar, is -not already installed and on the classpath. The error message can be puzzling, -as it won't say the Saxon jar is missing, it will say it can't find a method -that is clearly present in the examples jar. The explanation is that Java does -not resolve the method, because a dependency is missing. +not already installed and on the classpath. The error message will not directly +say the Saxon jar is missing, but will say it failed to find a class (whose name +will suggest it should be part of Saxon). There are two ways to proceed: From eaae7178a3a935f8df970ffcffdba7af19cc6774 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Oct 2020 22:51:08 -0400 Subject: [PATCH 0773/1087] Preserve AccessControlContext of varlena verifier A thread from a thread-pool executor might have been created with a different one. And a bit of overlooked java7ification. --- .../pljava/internal/VarlenaWrapper.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index e040a105..89594488 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -27,6 +27,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import static java.util.concurrent.Executors.privilegedCallable; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; @@ -938,18 +939,11 @@ protected void verify(InputStream is) throws Exception @Override public final Void call() throws Exception { - InputStream is = null; - try + try ( InputStream is = new MarkableSequenceInputStream(m_queue) ) { - is = new MarkableSequenceInputStream(m_queue); verify(is); + return null; } - finally - { - if ( null != is ) - is.close(); - } - return null; } /** @@ -1037,7 +1031,9 @@ public Verifier schedule() { if ( 1 == m_latch.getCount() ) { - m_future = LazyExecutorService.INSTANCE.submit(this); + m_future = + LazyExecutorService.INSTANCE + .submit(privilegedCallable(this)); m_latch.countDown(); } } From 7c081328da9b6292bcb550539e37c8d5827cc07a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Oct 2020 10:51:07 -0400 Subject: [PATCH 0774/1087] More overlooked java7ification in Commands.java --- .../pljava/management/Commands.java | 171 +++++++----------- 1 file changed, 68 insertions(+), 103 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 5e50e2fd..b78727d9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -24,6 +24,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharacterCodingException; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLData; @@ -54,7 +55,7 @@ import org.postgresql.pljava.internal.Checked; import org.postgresql.pljava.internal.Oid; import static org.postgresql.pljava.internal.Privilege.doPrivileged; -import org.postgresql.pljava.jdbc.SQLUtils; +import static org.postgresql.pljava.jdbc.SQLUtils.getDefaultConnection; import org.postgresql.pljava.sqlj.Loader; import org.postgresql.pljava.annotation.Function; @@ -388,31 +389,33 @@ static void addClassImages(int jarId, String urlString) static void addClassImages(int jarId, InputStream urlStream, long sz) throws SQLException { - PreparedStatement stmt = null; - PreparedStatement descIdFetchStmt = null; - PreparedStatement descIdStoreStmt = null; - ResultSet rs = null; - - try + try ( + Connection conn = getDefaultConnection(); + PreparedStatement stmt = conn.prepareStatement( + "INSERT INTO sqlj.jar_entry(entryName, jarId, entryImage) " + + "VALUES (?, ?, ?)"); + PreparedStatement descIdFetchStmt = conn.prepareStatement( + "SELECT entryId FROM sqlj.jar_entry " + + "WHERE jarId OPERATOR(pg_catalog.=) ?" + + " AND entryName OPERATOR(pg_catalog.=) ?"); + PreparedStatement descIdStoreStmt = conn.prepareStatement( + "INSERT INTO sqlj.jar_descriptor (jarId, entryId, ordinal)" + + " VALUES ( ?, ?, ? )"); + ) { byte[] buf = new byte[1024]; ByteArrayOutputStream img = new ByteArrayOutputStream(); - stmt = SQLUtils - .getDefaultConnection() - .prepareStatement( - "INSERT INTO sqlj.jar_entry(entryName, jarId, entryImage) VALUES(?, ?, ?)"); BufferedInputStream bis = new BufferedInputStream( urlStream); String manifest = rawManifest( bis, sz); JarInputStream jis = new JarInputStream(bis); if(manifest != null) { - PreparedStatement us = SQLUtils - .getDefaultConnection() + try ( PreparedStatement us = conn .prepareStatement( "UPDATE sqlj.jar_repository SET jarManifest = ? " + "WHERE jarId OPERATOR(pg_catalog.=) ?"); - try + ) { us.setString(1, manifest); us.setInt(2, jarId); @@ -420,10 +423,6 @@ static void addClassImages(int jarId, InputStream urlStream, long sz) throw new SQLException( "Jar repository update did not update 1 row"); } - finally - { - SQLUtils.close(us); - } } for(;;) @@ -452,31 +451,24 @@ static void addClassImages(int jarId, InputStream urlStream, long sz) } Matcher ddr = ddrSection.matcher( null != manifest ? manifest : ""); - Matcher cnt = mfCont.matcher( ""); + Matcher continuations = mfCont.matcher( ""); for ( int ordinal = 0; ddr.find(); ++ ordinal ) { - String entryName = cnt.reset( ddr.group( 1)).replaceAll( ""); - if ( descIdFetchStmt == null ) - descIdFetchStmt = SQLUtils.getDefaultConnection() - .prepareStatement( - "SELECT entryId FROM sqlj.jar_entry " + - "WHERE jarId OPERATOR(pg_catalog.=) ?" + - " AND entryName OPERATOR(pg_catalog.=) ?"); + String entryName = + continuations.reset( ddr.group( 1)).replaceAll( ""); descIdFetchStmt.setInt(1, jarId); descIdFetchStmt.setString(2, entryName); - rs = descIdFetchStmt.executeQuery(); - if(!rs.next()) - throw new SQLException( - "Failed to refetch row in sqlj.jar_entry"); - int deployImageId = rs.getInt(1); + int deployImageId; + try ( ResultSet rs = descIdFetchStmt.executeQuery() ) + { + if ( ! rs.next() ) + throw new SQLException( + "Failed to refetch row in sqlj.jar_entry"); + + deployImageId = rs.getInt(1); + } - if ( descIdStoreStmt == null ) - descIdStoreStmt = SQLUtils.getDefaultConnection() - .prepareStatement( - "INSERT INTO sqlj.jar_descriptor" - + " (jarId, entryId, ordinal) VALUES" - + " ( ?, ?, ? )"); descIdStoreStmt.setInt(1, jarId); descIdStoreStmt.setInt(2, deployImageId); descIdStoreStmt.setInt(3, ordinal); @@ -491,13 +483,6 @@ static void addClassImages(int jarId, InputStream urlStream, long sz) throw new SQLException("I/O exception reading jar file: " + e.getMessage(), "58030", e); } - finally - { - SQLUtils.close(rs); - SQLUtils.close(descIdStoreStmt); - SQLUtils.close(descIdFetchStmt); - SQLUtils.close(stmt); - } } private final static Pattern ddrSection = Pattern.compile( @@ -577,7 +562,7 @@ private static String rawManifest( BufferedInputStream bis, long markLimit) public static void addTypeMapping(String sqlTypeName, String javaClassName) throws SQLException { - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "INSERT INTO sqlj.typemap_entry(javaName, sqlName)" + " VALUES(?,?)")) @@ -615,7 +600,7 @@ public static void addTypeMapping(String sqlTypeName, String javaClassName) requires="sqlj.tables") public static void dropTypeMapping(String sqlTypeName) throws SQLException { - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "DELETE FROM sqlj.typemap_entry " + "WHERE sqlName OPERATOR(pg_catalog.=) ?")) @@ -647,7 +632,7 @@ public static String getClassPath(String schemaName) throws SQLException public static String getClassPath(Identifier.Simple schema) throws SQLException { - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "SELECT r.jarName" + " FROM" + @@ -747,7 +732,7 @@ public static void removeJar(String jarName, boolean undeploy) AclId[] ownerRet = new AclId[1]; int jarId = getJarId(jarName, ownerRet); if(jarId < 0) - throw new SQLException("No Jar named '" + jarName + throw new SQLException("No jar named '" + jarName + "' is known to the system", "4600B"); @@ -759,22 +744,17 @@ public static void removeJar(String jarName, boolean undeploy) if(undeploy) deployRemove(jarId, jarName); - PreparedStatement stmt = SQLUtils - .getDefaultConnection() + try ( PreparedStatement stmt = getDefaultConnection() .prepareStatement( "DELETE FROM sqlj.jar_repository " + "WHERE jarId OPERATOR(pg_catalog.=) ?"); - try + ) { stmt.setInt(1, jarId); if(stmt.executeUpdate() != 1) throw new SQLException( "Jar repository update did not update 1 row"); } - finally - { - SQLUtils.close(stmt); - } Loader.clearSchemaLoaders(); } @@ -870,7 +850,7 @@ public static void setClassPath(Identifier.Simple schema, String path) // valid jar // entries = new ArrayList<>(); - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "SELECT jarId FROM sqlj.jar_repository " + "WHERE jarName OPERATOR(pg_catalog.=) ?")) @@ -901,7 +881,7 @@ public static void setClassPath(Identifier.Simple schema, String path) // Delete the old classpath // - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "DELETE FROM sqlj.classpath_entry " + "WHERE schemaName OPERATOR(pg_catalog.=) ?")) @@ -915,7 +895,7 @@ public static void setClassPath(Identifier.Simple schema, String path) // Insert the new path. // ; - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "INSERT INTO sqlj.classpath_entry("+ " schemaName, ordinal, jarId) VALUES(?, ?, ?)")) @@ -1017,7 +997,7 @@ private static void deployInstall(int jarId, String jarName) withJarInPath(jarName, false, () -> { for ( SQLDeploymentDescriptor dd : depDesc ) - dd.install(SQLUtils.getDefaultConnection()); + dd.install(getDefaultConnection()); }); } @@ -1029,36 +1009,37 @@ private static void deployRemove(int jarId, String jarName) withJarInPath(jarName, true, () -> { for ( int i = depDesc.length ; i --> 0 ; ) - depDesc[i].remove(SQLUtils.getDefaultConnection()); + depDesc[i].remove(getDefaultConnection()); }); } private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) throws SQLException { - ResultSet rs = null; - PreparedStatement stmt = SQLUtils.getDefaultConnection() + try ( PreparedStatement stmt = getDefaultConnection() .prepareStatement( "SELECT e.entryImage" + " FROM sqlj.jar_descriptor d INNER JOIN sqlj.jar_entry e" + " ON d.entryId OPERATOR(pg_catalog.=) e.entryId" + " WHERE d.jarId OPERATOR(pg_catalog.=) ?" + " ORDER BY d.ordinal"); - try + ) { stmt.setInt(1, jarId); - rs = stmt.executeQuery(); - ArrayList sdds = new ArrayList<>(); - while(rs.next()) + try ( ResultSet rs = stmt.executeQuery() ) { - ByteBuffer bytes = ByteBuffer.wrap(rs.getBytes(1)); - // According to the SQLJ standard, this entry must be - // UTF8 encoded. - // - sdds.add( new SQLDeploymentDescriptor( - UTF_8.newDecoder().decode(bytes).toString())); + ArrayList sdds = new ArrayList<>(); + while ( rs.next() ) + { + ByteBuffer bytes = ByteBuffer.wrap(rs.getBytes(1)); + // According to the SQLJ standard, this entry must be + // UTF8 encoded. + // + sdds.add( new SQLDeploymentDescriptor( + UTF_8.newDecoder().decode(bytes).toString())); + } + return sdds.toArray( new SQLDeploymentDescriptor[sdds.size()]); } - return sdds.toArray( new SQLDeploymentDescriptor[sdds.size()]); } catch(CharacterCodingException e) { @@ -1071,11 +1052,6 @@ private static SQLDeploymentDescriptor[] getDeploymentDescriptors(int jarId) "%1$s at %2$s", e.getMessage(), e.getErrorOffset()), "42601", e); } - finally - { - SQLUtils.close(rs); - SQLUtils.close(stmt); - } } /* @@ -1091,7 +1067,7 @@ private static String getFullSqlNameOwned(String sqlTypeName) AclId invoker = AclId.getOuterUser(); - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "SELECT n.nspname, t.typname," + " pg_catalog.pg_has_role(?, t.typowner, 'USAGE')" @@ -1148,7 +1124,7 @@ private static int getJarId(PreparedStatement stmt, String jarName, private static int getJarId(String jarName, AclId[] ownerRet) throws SQLException { - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "SELECT jarId, jarOwner FROM sqlj.jar_repository"+ " WHERE jarName OPERATOR(pg_catalog.=) ?")) @@ -1167,7 +1143,7 @@ private static int getJarId(String jarName, AclId[] ownerRet) */ private static Oid getSchemaId(Identifier.Simple schema) throws SQLException { - try(PreparedStatement stmt = SQLUtils.getDefaultConnection() + try(PreparedStatement stmt = getDefaultConnection() .prepareStatement( "SELECT oid FROM pg_catalog.pg_namespace " + "WHERE nspname OPERATOR(pg_catalog.=) ?")) @@ -1197,11 +1173,10 @@ private static void installJar(String urlString, String jarName, + "' already exists", "46002"); - PreparedStatement stmt = SQLUtils - .getDefaultConnection() - .prepareStatement( - "INSERT INTO sqlj.jar_repository(jarName, jarOrigin, jarOwner) VALUES(?, ?, ?)"); - try + try ( PreparedStatement stmt = getDefaultConnection().prepareStatement( + "INSERT INTO sqlj.jar_repository(jarName, jarOrigin, jarOwner)" + + " VALUES(?, ?, ?)"); + ) { stmt.setString(1, jarName); stmt.setString(2, urlString); @@ -1210,10 +1185,6 @@ private static void installJar(String urlString, String jarName, throw new SQLException( "Jar repository insert did not insert 1 row"); } - finally - { - SQLUtils.close(stmt); - } AclId[] ownerRet = new AclId[1]; int jarId = getJarId(jarName, ownerRet); @@ -1260,13 +1231,12 @@ private static void replaceJar(String urlString, String jarName, if(redeploy) deployRemove(jarId, jarName); - PreparedStatement stmt = SQLUtils - .getDefaultConnection() + try ( PreparedStatement stmt = getDefaultConnection() .prepareStatement( "UPDATE sqlj.jar_repository " + "SET jarOrigin = ?, jarOwner = ?, jarManifest = NULL " + "WHERE jarId OPERATOR(pg_catalog.=) ?"); - try + ) { stmt.setString(1, urlString); stmt.setString(2, user.getName()); @@ -1275,22 +1245,15 @@ private static void replaceJar(String urlString, String jarName, throw new SQLException( "Jar repository update did not update 1 row"); } - finally - { - SQLUtils.close(stmt); - } - stmt = SQLUtils.getDefaultConnection().prepareStatement( + try ( PreparedStatement stmt = getDefaultConnection().prepareStatement( "DELETE FROM sqlj.jar_entry WHERE jarId OPERATOR(pg_catalog.=) ?"); - try + ) { stmt.setInt(1, jarId); stmt.executeUpdate(); } - finally - { - SQLUtils.close(stmt); - } + if(image == null) addClassImages(jarId, urlString); else @@ -1298,7 +1261,9 @@ private static void replaceJar(String urlString, String jarName, InputStream imageStream = new ByteArrayInputStream(image); addClassImages(jarId, imageStream, image.length); } + Loader.clearSchemaLoaders(); + if(!redeploy) return; From 5565a3c9c4b8d6dd0b0f7fff4090d4e8120dc10a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Oct 2020 13:16:47 -0400 Subject: [PATCH 0775/1087] Add sqlj.alias_java_language in Commands.java It simplifies getting the details right when issuing CREATE LANGUAGE to make an additional 'alias' for PL/Java that can be referred to in pljava.policy for language-specific grant clauses. It will create a sandboxed or unsandboxed language, revoke public USAGE permission on it, and add a comment. It does not depend on existing PL/Java LANGUAGE entries or handler functions to have their usual names, as long as it succeeds in looking up what language(s) and functions refer to the entry points in the loaded shared object. Had to create an additional HTML anchor by hand in the Commands.java javadoc for policy.md to link to, because Maven's markdown processor corrupts URLs with fragment ids of the form generated by Javadoc for method signatures. Did not find any documentation about getting Maven's markdown processor not to do that. --- pljava-so/src/main/c/Backend.c | 40 +++++ .../postgresql/pljava/internal/Backend.java | 29 +++- .../pljava/management/Commands.java | 161 ++++++++++++++++++ src/site/markdown/use/policy.md | 4 + 4 files changed, 227 insertions(+), 7 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index b38d406e..296b836d 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1002,6 +1002,11 @@ static void initPLJavaClasses(void) "()Z", Java_org_postgresql_pljava_internal_Backend__1isCreatingExtension }, + { + "_myLibraryPath", + "()Ljava/lang/String;", + Java_org_postgresql_pljava_internal_Backend__1myLibraryPath + }, { 0, 0, 0 } }; @@ -2067,6 +2072,41 @@ Java_org_postgresql_pljava_internal_Backend__1isCreatingExtension(JNIEnv *env, j return inExtension ? JNI_TRUE : JNI_FALSE; } +/* + * Class: org_postgresql_pljava_internal_Backend + * Method: _myLibraryPath + * Signature: ()Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL +Java_org_postgresql_pljava_internal_Backend__1myLibraryPath(JNIEnv *env, jclass cls) +{ + jstring result = NULL; + + BEGIN_NATIVE + + if ( NULL == pljavaLoadPath ) + { + Oid funcoid = pljavaTrustedOid; + + if ( InvalidOid == funcoid ) + funcoid = pljavaUntrustedOid; + if ( InvalidOid == funcoid ) + return NULL; + + /* + * Result not needed, but pljavaLoadPath is set as a side effect. + */ + InstallHelper_isPLJavaFunction(funcoid, NULL, NULL); + } + + if ( NULL != pljavaLoadPath ) + result = String_createJavaStringFromNTS(pljavaLoadPath); + + END_NATIVE + + return result; +} + /* * Class: org_postgresql_pljava_internal_Backend_EarlyNatives * Method: _forbidOtherThreads diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index 35bab8c7..b855780d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -296,26 +296,41 @@ public static boolean isCreatingExtension() return doInPG(Backend::_isCreatingExtension); } + /** + * Returns the path of PL/Java's shared library. + * @throws SQLException if for some reason it can't be determined. + */ + public static String myLibraryPath() throws SQLException + { + String result = doInPG(Backend::_myLibraryPath); + + if ( null != result ) + return result; + + throw new SQLException("Unable to retrieve PL/Java's library path"); + } + /** * Returns true if the backend is awaiting a return from a * call into the JVM. This method will only return false * when called from a thread other then the main thread and the main * thread has returned from the call into the JVM. */ - public native static boolean isCallingJava(); + public static native boolean isCallingJava(); /** * Returns the value of the GUC custom variable * pljava.release_lingering_savepoints. */ - public native static boolean isReleaseLingeringSavepoints(); + public static native boolean isReleaseLingeringSavepoints(); - private native static String _getConfigOption(String key); + private static native String _getConfigOption(String key); - private native static int _getStatementCacheSize(); - private native static void _log(int logLevel, String str); - private native static void _clearFunctionCache(); - private native static boolean _isCreatingExtension(); + private static native int _getStatementCacheSize(); + private static native void _log(int logLevel, String str); + private static native void _clearFunctionCache(); + private static native boolean _isCreatingExtension(); + private static native String _myLibraryPath(); private static class EarlyNatives { diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index b78727d9..d6fec76f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -48,7 +48,11 @@ import org.postgresql.pljava.Session; import org.postgresql.pljava.SessionManager; +import static org.postgresql.pljava.annotation.processing.DDRWriter.eQuote; + import org.postgresql.pljava.sqlgen.Lexicals.Identifier; +import static + org.postgresql.pljava.sqlgen.Lexicals.Identifier.Qualified.nameFromCatalog; import org.postgresql.pljava.internal.AclId; import org.postgresql.pljava.internal.Backend; @@ -60,6 +64,8 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLType; +import static org.postgresql.pljava.annotation.Function.OnNullInput.CALLED; import static org.postgresql.pljava.annotation.Function.Security.DEFINER; /** @@ -249,6 +255,41 @@ * to the current setting of the search_path. *

  • *
    Items the roundTrip function can return
    Column nameColumn typeWhat is returned
    + *

    alias_java_language

    + * The {@link #aliasJavaLanguage alias_java_language command} issues + * a PostgreSQL {@code CREATE LANGUAGE} command to define a named "language" + * that is an alias for PL/Java. The name can appear in the + * Java security policy to grant + * specific permissions to functions created in this "language". + *

    Usage

    + *
    + * {@code SELECT sqlj.alias_java_language(, sandboxed => );} + *
    + *

    Parameters

    + *
    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Parameters for sqlj.alias_java_language
    aliasThe name desired for the language alias. Language names are not + * schema-qualified.
    sandboxedWhether to create a sandboxed "{@code TRUSTED}" language, in which + * functions can be created by any role granted {@code USAGE} permission (true), + * or an unsandboxed one in which only superusers may create functions (false). + *
    orReplaceOptional parameter, default false. + * See {@link #aliasJavaLanguage the method documentation} for details.
    commentOptional parameter. If empty string (the default), a comment is supplied. + * See {@link #aliasJavaLanguage the method documentation} for details.
    * * @author Thomas Hallgren * @author Chapman Flack @@ -962,6 +1003,126 @@ private static void withJarInPath(String jarName, boolean schemaMayVanish, } } + /** + * Creates a named PostgreSQL {@code LANGUAGE} that refers to PL/Java; + * its name may be referred to in the Java security policy to grant selected + * permissions to functions created in this "language". + *

    + * More on configuring Java permissions specific to this alias can be found + * in the policy documentation. + *

    + * PostgreSQL normally grants {@code USAGE} to {@code PUBLIC} if a sandboxed + * language is created. This routine does not, so that {@code USAGE} on the + * new alias can then be {@code GRANT}ed to specific roles or to + * {@code PUBLIC} as desired. + * @param alias Name for this "language". + * @param sandboxed Whether this alias should be a sandboxed/"TRUSTED" + * language that USAGE can be granted on, or an unsandboxed one that only + * superusers can create functions in. Must be specified. + * @param orReplace Whether to succeed even if a language by the same name + * already exists; if so, the sandboxed bit, handler entry points, and + * comment may all be changed. Default is false. + * @param comment A comment to associate with the alias "language". If an + * empty string (the default), a default comment will be constructed. Pass + * null explicitly to avoid setting any comment (or changing any existing + * comment, in the orReplace case). + */ + @Function( + schema="sqlj", name="alias_java_language", onNullInput=CALLED, + requires="sqlj.tables" + ) + public static void aliasJavaLanguage( + String alias, + Boolean sandboxed, + @SQLType(defaultValue="false") Boolean orReplace, + @SQLType(defaultValue="") String comment) + throws SQLException + { + if ( null == alias ) + throw new SQLDataException( + "parameter \"alias\" may not be null", "22004"); + if ( null == sandboxed ) + throw new SQLDataException( + "parameter \"sandboxed\" may not be null", "22004"); + if ( null == orReplace ) + throw new SQLDataException( + "parameter \"orReplace\" may not be null", "22004"); + + if ( "".equals(comment) ) + comment = "PL/Java language alias that may be assigned " + + "distinct permissions in the security policy. Routines may " + + "be created in this \"language\" by " + ( sandboxed + ? "any role with USAGE permission." : "superusers only." ); + + Identifier.Simple aliasIdent = Identifier.Simple.fromJava(alias); + + String libraryPath = Backend.myLibraryPath(); + + try ( + Connection conn = getDefaultConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT DISTINCT" + + " cn.nspname, cf.proname, vn.nspname, vf.proname" + + " FROM" + + " (VALUES (?,?)) AS params(sandboxed, libpath)," + + " pg_catalog.pg_language AS lan" + + " JOIN pg_catalog.pg_proc AS cf" + + " ON lan.lanplcallfoid OPERATOR(pg_catalog.=) cf.oid" + + " JOIN pg_catalog.pg_namespace AS cn" + + " ON cf.pronamespace OPERATOR(pg_catalog.=) cn.oid" + + " JOIN pg_catalog.pg_proc AS vf" + + " ON lan.lanvalidator OPERATOR(pg_catalog.=) vf.oid" + + " JOIN pg_catalog.pg_namespace AS vn" + + " ON vf.pronamespace OPERATOR(pg_catalog.=) vn.oid" + + " WHERE" + + " lanispl AND lanpltrusted OPERATOR(pg_catalog.=) sandboxed" + + " AND cf.probin OPERATOR(pg_catalog.=) libpath" + + " AND vf.probin OPERATOR(pg_catalog.=) libpath"); + ) + { + Identifier.Qualified callHandler; + Identifier.Qualified valHandler; + + ps.setBoolean(1, sandboxed); + ps.setString(2, libraryPath); + try ( ResultSet rs = ps.executeQuery() ) + { + if ( ! rs.next() ) + throw new SQLException( + "Failed to find handlers for " + + (sandboxed ? "" : "un") + "sandboxed PL/Java"); + + callHandler = nameFromCatalog(rs.getString(1), rs.getString(2)); + valHandler = nameFromCatalog(rs.getString(3), rs.getString(4)); + + if ( rs.next() ) + throw new SQLException( + "Failed to find handlers uniquely for " + + (sandboxed ? "" : "un") + "sandboxed PL/Java"); + } + + try ( Statement s = conn.createStatement() ) + { + s.execute( + "CREATE " + + ( orReplace ? "OR REPLACE " : "" ) + + ( sandboxed ? "TRUSTED " : "" ) + "LANGUAGE " + + aliasIdent + + " HANDLER " + callHandler + + " VALIDATOR " + valHandler); + if ( sandboxed ) // GRANT/REVOKE not even allowed on unTRUSTED + s.execute( + "REVOKE USAGE ON LANGUAGE " + aliasIdent + + " FROM PUBLIC"); + if ( null == comment ) + return; + s.execute( + "COMMENT ON LANGUAGE " + aliasIdent + " IS " + + eQuote(comment)); + } + } + } + /** * Throws an exception if the given name cannot be used as the name of a * jar. diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index f343d969..dd37b7ef 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -218,6 +218,9 @@ If the JSR-310 test example in PL/Java's examples jar is declared with `LANGUAGE java_tzset` rather than `LANGUAGE java`, it will be able to set the time zone and succeed. +The [`SQLJ.ALIAS_JAVA_LANGUAGE`][sqljajl] function can be used to create such +aliases conveniently. + When grants to specific named languages and grants with the wildcard are present, code will have all of the permissions granted to the specific language by name, in addition to all permissions that appear in grants to the @@ -325,3 +328,4 @@ release, so relying on it is not recommended. [jdkperms]: https://docs.oracle.com/en/java/javase/14/security/permissions-jdk1.html#GUID-1E8E213A-D7F2-49F1-A2F0-EFB3397A8C95 [confvar]: variables.html [dopriv]: https://docs.oracle.com/en/java/javase/14/security/java-se-platform-security-architecture.html#GUID-E8898CB5-65BB-4D1A-A574-8F7112FC353F +[sqljajl]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#alias_java_language From 132700e539760aeb36e5e2e02cc43546505a6687 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Oct 2020 16:23:14 -0400 Subject: [PATCH 0776/1087] Have examples make a java_tzset alias Change the supplied policy to grant "user.timezone" "write" only to functions declared in language java_tzset, rather than to everything in the examples jar. Get the examples jar deployment to create such a language alias and declare the issue199() function in it, so that test succeeds, without granting the permission more broadly. In passing, add unconditional read for a system property that is read deep in the innards of Java 9 and 10, but that they forgot to give themselves permission to read. Missed that in testing on more recent Javas. --- .../pljava/example/annotation/PreJSR310.java | 24 +++++++++++--- .../src/main/resources/pljava.policy | 33 +++++++++++-------- src/site/markdown/use/policy.md | 4 +-- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java index 9f3913a0..b19f5ce4 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java @@ -24,6 +24,7 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLActions; /** * Some tests of pre-JSR 310 date/time/timestamp conversions. @@ -33,9 +34,17 @@ * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example. */ -@SQLAction(implementor="postgresql_ge_90300", // needs LATERAL - requires="issue199", install={ - "SELECT javatest.issue199()" +@SQLActions({ + @SQLAction(provides="language java_tzset", install={ + "SELECT sqlj.alias_java_language('java_tzset', true)" + }, remove={ + "DROP LANGUAGE java_tzset" + }), + + @SQLAction(implementor="postgresql_ge_90300", // needs LATERAL + requires="issue199", install={ + "SELECT javatest.issue199()" + }) }) public class PreJSR310 { @@ -49,8 +58,15 @@ public class PreJSR310 * are converted correctly in the Europe/Prague timezone. The actual issue * was by no means limited to that timezone, but this test reproducibly * detects it. + *

    + * This function is defined in the 'alias' language {@code java_tzset}, for + * which there is an entry in the default {@code pljava.policy} granting + * permission to adjust the time zone, which is temporarily done here. */ - @Function(schema="javatest", provides="issue199") + @Function( + schema="javatest", language="java_tzset", + requires="language java_tzset", provides="issue199" + ) public static void issue199() throws SQLException { TimeZone oldZone = TimeZone.getDefault(); diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index 6e8f44f0..d2723126 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -36,6 +36,12 @@ grant { // permission java.util.PropertyPermission "sqlj.defaultconnection", "read"; + + // This property is read in the innards of Java 9 and 10, but they forgot + // to add a permission for it. Not needed for Java 11 and later. + // + permission java.util.PropertyPermission + "jdk.lang.ref.disableClearBeforeEnqueue", "read"; }; @@ -69,19 +75,6 @@ grant codebase "${org.postgresql.pljava.codesource}" { }; -// -// This grant applies to the supplied examples, if sqlj.install_jar is given the -// exact name 'examples' as the desired jar name. (Otherwise, it will apply to -// any other jar that is installed with the name 'examples'. Beware!) -// -grant codebase "sqlj:examples" { - - // The PreJSR310 test involves setting the time zone. - // - permission java.util.PropertyPermission "user.timezone", "write"; -}; - - // // This grant defines the mapping onto Java of PostgreSQL's "trusted language" // category. When PL/Java executes a function whose SQL declaration names @@ -108,3 +101,17 @@ grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed * { permission java.io.FilePermission "<>", "read,readlink,write,delete"; }; + + +// +// This grant applies to a specific PL/Java sandboxed language named java_tzset +// (if such a language exists) and grants functions created in that language +// permission to adjust the time zone. There is an example method in the +// org.postgresql.pljava.example.annotation.PreJSR310 class, which needs to +// temporarily adjust the time zone for a test. That example also uses +// sqlj.alias_java_language to create the java_tzset "language" when deployed, +// and DROP LANGUAGE to remove it when undeployed. +// +grant principal org.postgresql.pljava.PLPrincipal$Sandboxed "java_tzset" { + permission java.util.PropertyPermission "user.timezone", "write"; +}; diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index dd37b7ef..7c1c85cf 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -215,8 +215,8 @@ grant principal org.postgresql.pljava.PLPrincipal$Sandboxed "java_tzset" { ``` If the JSR-310 test example in PL/Java's examples jar is declared with -`LANGUAGE java_tzset` rather than `LANGUAGE java`, it will be able to set -the time zone and succeed. +`LANGUAGE java_tzset` rather than `LANGUAGE java` (as, in fact, it is), +it will be able to set the time zone and succeed. The [`SQLJ.ALIAS_JAVA_LANGUAGE`][sqljajl] function can be used to create such aliases conveniently. From ed3e7761a3a70588f8e388dfe2e7fb9ffaf5a61b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Oct 2020 16:37:04 -0400 Subject: [PATCH 0777/1087] Prune pre-PG8.4 conditionals from Backend.c The exact cutoff version for PL/Java 1.6 might not be set in stone yet, but it will certainly not be pre-8.4. This allows adding GUC_SUPERUSER_ONLY (which appeared in 8.4) to some things without extra fuss. They were already only settable by superusers, but the PostgreSQL practice seems to be to avoid showing settings also, if they might include filesystem paths, etc. --- pljava-so/src/main/c/Backend.c | 58 +++------------------------------- 1 file changed, 5 insertions(+), 53 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 296b836d..89d84125 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -122,11 +122,7 @@ static bool pljavaDebug; static bool pljavaReleaseLingeringSavepoints; static bool pljavaEnabled; -#if PG_VERSION_NUM >= 80400 static int java_thread_pg_entry; -#else -static char* java_thread_pg_entry; -#endif static int s_javaLogLevel; @@ -387,21 +383,9 @@ static void initsequencer(enum initstage is, bool tolerant); #define ASSIGNSTRINGHOOK(name) ASSIGNHOOK(name, const char *) #endif -#if PG_VERSION_NUM >= 80400 #define ASSIGNENUMHOOK(name) ASSIGNHOOK(name,int) #define ENUMBOOTVAL(entry) ((entry).val) #define ENUMHOOKRET true -#else -#define ASSIGNENUMHOOK(name) ASSIGNSTRINGHOOK(name) -#define ENUMBOOTVAL(entry) ((char *)((entry).name)) -#define ENUMHOOKRET newval -struct config_enum_entry -{ - const char *name; - int val; - bool hidden; -}; -#endif static const struct config_enum_entry java_thread_pg_entry_options[] = { {"allow", 0, false}, /* numeric value is bit-coded: */ @@ -480,21 +464,7 @@ ASSIGNHOOK(enabled, bool) ASSIGNENUMHOOK(java_thread_pg_entry) { -#if PG_VERSION_NUM >= 80400 int val = newval; -#else - int val = -1; - struct config_enum_entry const *e; - for ( e = java_thread_pg_entry_options; NULL != e->name; ++ e ) - { - if ( 0 == strcmp(e->name, newval) ) - { - val = e->val; - } - } - if ( -1 == val ) - ASSIGNRETURN(NULL); -#endif ASSIGNRETURNIFCHECK(ENUMHOOKRET); pljava_JNI_setThreadPolicy( !!(val&1) /*error*/, !(val&2) /*monitorops*/); ASSIGNRETURN(ENUMHOOKRET); @@ -891,11 +861,7 @@ static void reLogWithChangedLevel(int level) FreeErrorData(edata); #else if (!errstart(level, edata->filename, edata->lineno, - edata->funcname -#if PG_VERSION_NUM >= 80400 - , NULL -#endif - )) + edata->funcname, NULL)) { FreeErrorData(edata); return; @@ -906,10 +872,8 @@ static void reLogWithChangedLevel(int level) errmsg("%s", edata->message); if (edata->detail) errdetail("%s", edata->detail); -#if PG_VERSION_NUM >= 80400 if (edata->detail_log) errdetail_log("%s", edata->detail_log); -#endif if (edata->hint) errhint("%s", edata->hint); if (edata->context) @@ -1589,21 +1553,9 @@ static jint initializeJavaVM(JVMOptList *optList) return jstat; } -#if PG_VERSION_NUM >= 80400 #define GUCBOOTVAL(v) (v), #define GUCBOOTASSIGN(a, v) #define GUCFLAGS(f) (f), -#else -#define GUCBOOTVAL(v) -#define GUCBOOTASSIGN(a, v) \ - StaticAssertStmt(NULL != (valueAddr), "NULL valueAddr for GUC"); \ - *(a) = (v); -#define GUCFLAGS(f) -#define DefineCustomEnumVariable(name, short_desc, long_desc, valueAddr, \ - options, context, assign_hook, show_hook) \ - DefineCustomStringVariable((name), (short_desc), (long_desc), (valueAddr), \ - (context), (assign_hook), (show_hook)) -#endif #if PG_VERSION_NUM >= 90100 #define GUCCHECK(h) (h), @@ -1666,7 +1618,7 @@ static void registerGUCOptions(void) &libjvmlocation, PLJAVA_LIBJVMDEFAULT, PGC_SUSET, - 0, /* flags */ + GUC_SUPERUSER_ONLY, /* flags */ check_libjvm_location, assign_libjvm_location, NULL); /* show hook */ @@ -1678,7 +1630,7 @@ static void registerGUCOptions(void) &vmoptions, NULL, /* boot value */ PGC_SUSET, - 0, /* flags */ + GUC_SUPERUSER_ONLY, /* flags */ check_vmoptions, assign_vmoptions, NULL); /* show hook */ @@ -1690,7 +1642,7 @@ static void registerGUCOptions(void) &modulepath, InstallHelper_defaultModulePath(pathbuf,s_path_var_sep),/* boot value */ PGC_SUSET, - 0, /* flags */ + GUC_SUPERUSER_ONLY, /* flags */ check_modulepath, assign_modulepath, NULL); /* show hook */ @@ -1708,7 +1660,7 @@ static void registerGUCOptions(void) &policy_urls, "\"file:${org.postgresql.sysconfdir}/pljava.policy\",\"=\"", PGC_SUSET, - PLJAVA_IMPLEMENTOR_FLAGS, + PLJAVA_IMPLEMENTOR_FLAGS | GUC_SUPERUSER_ONLY, check_policy_urls, /* check hook */ assign_policy_urls, NULL); /* show hook */ From d07daa772e9da319ac5ca01cbca298878b43631c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 17 Oct 2020 18:09:29 -0400 Subject: [PATCH 0778/1087] Add javadoc in new classes Tedious and perhaps premature to add so much javadoc to Checked, but not doing it leaves the javadocs looking very gappy. --- .../org/postgresql/pljava/PLPrincipal.java | 62 ++ .../postgresql/pljava/internal/Checked.java | 841 ++++++++++++++++++ 2 files changed, 903 insertions(+) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java b/pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java index 8e965ce3..cd003cae 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/PLPrincipal.java @@ -20,6 +20,15 @@ import org.postgresql.pljava.annotation.Function.Trust; import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +/** + * Java {@code Principal} representing a PostgreSQL {@code PROCEDURAL LANGUAGE}, + * which has a name (a simple identifier, not schema-qualified) and is either + * {@code Sandboxed} (declared with SQL {@code CREATE TRUSTED LANGUAGE} or + * {@code Unsandboxed}. + *

    + * Only the subclasses, {@code Sandboxed} or {@code Unsandboxed} can be + * instantiated, or granted permissions in policy. + */ public abstract class PLPrincipal extends BasePrincipal { private static final long serialVersionUID = 4876111394761861189L; @@ -45,21 +54,50 @@ private void readObject(ObjectInputStream in) + c.getName()); } + /** + * Returns either {@link Trust#SANDBOXED SANDBOXED} or + * {@link Trust#UNSANDBOXED UNSANDBOXED} + * according to PostgreSQL's catalog entry for the language. + */ public abstract Trust trust(); + /** + * Java {@code Principal} representing a PostgreSQL + * {@code PROCEDURAL LANGUAGE} that was declared with the {@code TRUSTED} + * keyword and can be used to declare new functions by any role that has + * been granted {@code USAGE} permission on it. + *

    + * A Java security policy can grant permissions to this {@code Principal} + * by class and wildcard name, or by class and the specific name given in + * SQL to the language. + */ public static final class Sandboxed extends PLPrincipal { private static final long serialVersionUID = 55704990613451177L; + /** + * Construct an instance given its name in {@code String} form. + *

    + * The name will be parsed as described for + * {@link Simple#fromJava Identifier.Simple.fromJava}. + */ public Sandboxed(String name) { super(name); } + + /** + * Construct an instance given its name already as an + * {@code Identifier.Simple}. + */ public Sandboxed(Simple name) { super(name); } + /** + * Returns {@code SANDBOXED}. + */ @Override public Trust trust() { @@ -67,19 +105,43 @@ public Trust trust() } } + /** + * Java {@code Principal} representing a PostgreSQL + * {@code PROCEDURAL LANGUAGE} that was declared without the + * {@code TRUSTED} keyword, and can be used to declare new functions only + * by a PostgreSQL superuser. + *

    + * A Java security policy can grant permissions to this {@code Principal} + * by class and wildcard name, or by class and the specific name given in + * SQL to the language. + */ public static final class Unsandboxed extends PLPrincipal { private static final long serialVersionUID = 7487230786813048525L; + /** + * Construct an instance given its name in {@code String} form. + *

    + * The name will be parsed as described for + * {@link Simple#fromJava Identifier.Simple.fromJava}. + */ public Unsandboxed(String name) { super(name); } + + /** + * Construct an instance given its name already as an + * {@code Identifier.Simple}. + */ public Unsandboxed(Simple name) { super(name); } + /** + * Returns {@code UNSANDBOXED}. + */ @Override public Trust trust() { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java index 05b2e295..3707c43c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Checked.java @@ -89,17 +89,57 @@ * parameter, and some {@link #closing(AutoCloseable) closing} methods (inspired * by Python, for use with resources that do not already implement * {@code AutoCloseable}), are also provided. + * + * @param The type this functional interface can be wrapped as by + * ederWrap(), which may be a corresponding Java functional interface that does + * not allow checked exceptions. + * @param The checked exception type (or least upper bound of the checked + * exception types) that the body of this functional interface may throw. */ public interface Checked { + /** + * Throw an exception, unsafely cast to a different exception type, chiefly + * as used by Lukas Eder to fly a checked exception through a section of + * code that does not accept it. + *

    + * In PL/Java, this method is not intended to be called directly, but used + * transparently within the {@link #in in(...)} construct, which re-exposes + * the checked exception type {@code EX} to the compiler. + * @param The throwable type the argument t should be presented as. + * @param t A throwable. + * @throws E The argument t, represented to the compiler as of type E. + */ @SuppressWarnings("unchecked") static E ederThrow(Throwable t) throws E { throw (E) t; } + /** + * Wraps this {@code Checked} functional interfaces as its + * corresponding Java functional interface {@code WT}, which possibly + * does not allow checked exceptions. + *

    + * Checked exceptions of type {@code EX} may still, in reality, be thrown. + * This method is not intended to be called directly; it is used + * transparently by {@link #in in(...)}, which passes the wrapper type into + * code that requires it, but re-exposes the original checked exception type + * to the compiler. + */ WT ederWrap(); + /** + * Passes this functional interface, wrapped as its wrapper type {@code WT}, + * into a code body that requires that wrapper type, while remembering for + * the compiler the checked exception type that may, in fact, be thrown. + * + * @param Any exception type that the body, c, detectably can throw. + * @param c A Consumer to which this instance, wrapped as its corresponding + * functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body c. + */ default void in(Consumer c) throws EX, RX @@ -107,6 +147,16 @@ void in(Consumer c) c.accept(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns a non-primitive type. + * + * @param Return type of the body f. + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToDoubleFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default RT inReturning(Function f) throws EX, RX @@ -114,6 +164,15 @@ RT inReturning(Function f) return f.apply(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns a {@code double}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToDoubleFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default double inDoubleReturning(ToDoubleFunction f) throws EX, RX @@ -121,6 +180,15 @@ RT inReturning(Function f) return f.apply(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns an {@code int}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToIntFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default int inIntReturning(ToIntFunction f) throws EX, RX @@ -128,6 +196,15 @@ int inIntReturning(ToIntFunction f) return f.apply(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns a {@code long}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToLongFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default long inLongReturning(ToLongFunction f) throws EX, RX @@ -135,6 +212,15 @@ long inLongReturning(ToLongFunction f) return f.apply(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns a {@code boolean}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A Predicate to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default boolean inBooleanReturning(Predicate f) throws EX, RX @@ -142,6 +228,20 @@ boolean inBooleanReturning(Predicate f) return f.test(ederWrap()); } + + /** + * Like {@link #in in(...)} but where the body returns a {@code byte}. + *

    + * This method is provided for consistency of notation, even though it is + * not strictly needed because Java has no checked-exception-less + * counterpart of {@code ToByteFunction}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToByteFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default byte inByteReturning(ToByteFunction f) throws EX, RX @@ -149,6 +249,19 @@ byte inByteReturning(ToByteFunction f) return f.apply(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns a {@code short}. + *

    + * This method is provided for consistency of notation, even though it is + * not strictly needed because Java has no checked-exception-less + * counterpart of {@code ToShortFunction}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToShortFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default short inShortReturning(ToShortFunction f) throws EX, RX @@ -156,6 +269,19 @@ short inShortReturning(ToShortFunction f) return f.apply(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns a {@code char}. + *

    + * This method is provided for consistency of notation, even though it is + * not strictly needed because Java has no checked-exception-less + * counterpart of {@code ToCharFunction}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToCharFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default char inCharReturning(ToCharFunction f) throws EX, RX @@ -163,6 +289,19 @@ char inCharReturning(ToCharFunction f) return f.apply(ederWrap()); } + /** + * Like {@link #in in(...)} but where the body returns a {@code float}. + *

    + * This method is provided for consistency of notation, even though it is + * not strictly needed because Java has no checked-exception-less + * counterpart of {@code ToFloatFunction}. + * + * @param Any exception type that the body, f, detectably can throw. + * @param f A ToFloatFunction to which this instance, wrapped as its + * corresponding functional interface WT, will be passed. + * @throws EX whatever can be thrown by the body of this instance + * @throws RX whatever can be thrown by the body f. + */ default float inFloatReturning(ToFloatFunction f) throws EX, RX @@ -174,6 +313,17 @@ float inFloatReturning(ToFloatFunction f) * Short-circuiting predicate combinators. */ + /** + * Returns a {@code Predicate} that is the short-circuiting {@code AND} of + * two others. + * @param Greatest-lower-bound parameter type acceptable to first and + * after, and the parameter type of the resulting predicate. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * predicate. + * @param first The predicate to be tested first. + * @param after The predicate to be tested next. + */ static Predicate and( Predicate first, @@ -182,6 +332,17 @@ Predicate and( return t -> first.test(t) && after.test(t); } + /** + * Returns a {@code Predicate} that is the short-circuiting {@code OR} of + * two others. + * @param Greatest-lower-bound parameter type acceptable to first and + * after, and the parameter type of the resulting predicate. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * predicate. + * @param first The predicate to be tested first. + * @param after The predicate to be tested next. + */ static Predicate or( Predicate first, @@ -194,6 +355,21 @@ Predicate or( * composed() methods. */ + /** + * Returns a {@code Function} that is the composition of + * two others. + * @param Parameter type of the resulting function, and acceptable as + * parameter of first. + * @param Type subsuming the return type of first, and acceptable as + * parameter of after. + * @param Return type of the composed function, subsuming the return + * type of after. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * function. + * @param first The first function to be applied. + * @param after The function applied to the result of first. + */ static Function composed( Function first, @@ -202,6 +378,19 @@ Function composed( return t -> after.apply(first.apply(t)); } + /** + * Returns a {@code BiConsumer} that is the composition of + * two others. + * @param First parameter type of the resulting BiConsumer, acceptable + * as first parameter to both first and after. + * @param Second parameter type of the resulting BiConsumer, acceptable + * as second parameter to both first and after. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * biconsumer. + * @param first The first consumer to be applied. + * @param after The consumer next applied to the same inputs. + */ static BiConsumer composed( BiConsumer first, @@ -214,6 +403,17 @@ BiConsumer composed( }; } + /** + * Returns a {@code Consumer} that is the composition of + * two others. + * @param Parameter type of the resulting Consumer, acceptable + * as parameter to both first and after. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * consumer. + * @param first The first consumer to be applied. + * @param after The consumer next applied to the same input. + */ static Consumer composed( Consumer first, @@ -226,6 +426,15 @@ Consumer composed( }; } + /** + * Returns a {@code DoubleConsumer} that is the composition of + * two others. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * consumer. + * @param first The first consumer to be applied. + * @param after The consumer next applied to the same input. + */ static DoubleConsumer composed( DoubleConsumer first, @@ -238,6 +447,15 @@ DoubleConsumer composed( }; } + /** + * Returns an {@code IntConsumer} that is the composition of + * two others. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * consumer. + * @param first The first consumer to be applied. + * @param after The consumer next applied to the same input. + */ static IntConsumer composed( IntConsumer first, @@ -250,6 +468,15 @@ IntConsumer composed( }; } + /** + * Returns a {@code LongConsumer} that is the composition of + * two others. + * @param Least upper bound of the exception types thrown by first and + * after, representing the exception types thrown by the resulting + * consumer. + * @param first The first consumer to be applied. + * @param after The consumer next applied to the same input. + */ static LongConsumer composed( LongConsumer first, @@ -306,6 +533,8 @@ interface AutoCloseable * ... * } * + * @param Least upper bound of exceptions that can be thrown by o + * @param o Lambda or method reference to serve as the close operation. */ static AutoCloseable closing(AutoCloseable o) @@ -318,6 +547,10 @@ AutoCloseable closing(AutoCloseable o) * that can supply the payload and implements {@code AutoCloseable} using * the lambda; useful in a {@code try}-with-resources when the payload * itself does not implement {@code AutoCloseable}. + * @param Type of the payload. + * @param Least upper bound of exceptions that may be thrown at close. + * @param payload Any object. + * @param closer Lambda or method reference to serve as the close operation. */ static Closing closing(T payload, AutoCloseable closer) @@ -340,6 +573,13 @@ Closing closing(T payload, AutoCloseable closer) * stream's {@code close} method is not declared to throw any. When used as * intended in a {@code try}-with-resources, any such surprise is bounded * by the scope of that statement. + * @param Type of the stream elements + * @param Type of the stream + * @param Least upper bound of exceptions that can be thrown by closer, + * and the declared throwable type of the close method of the returned + * Closing instance. + * @param stream Stream to have closer added as an action on close. + * @param closer Runnable to be executed when the returned stream is closed. */ static , E extends Exception> Closing closing(S stream, Runnable closer) @@ -413,10 +653,18 @@ default WT ederWrap() * Runnable. */ + /** + * Like {@link java.lang.Runnable} but with a body that can throw checked + * exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface Runnable extends Checked { + /** + * Execute the body of this {@code Runnable}. + */ void run() throws E; @Override @@ -435,6 +683,15 @@ default java.lang.Runnable ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static Runnable use(Runnable o) { return o; @@ -445,10 +702,19 @@ static Runnable use(Runnable o) * Suppliers that have checked-exception-less counterparts in the Java API. */ + /** + * Like {@link java.util.function.Supplier} but with a body that can throw + * checked exceptions. + * @param Type the supplier will supply. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface Supplier extends Checked, E> { + /** + * Get the supplied value. + */ T get() throws E; @Override @@ -467,16 +733,33 @@ default java.util.function.Supplier ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static Supplier use(Supplier o) { return o; } } + /** + * Like {@link java.util.function.BooleanSupplier} but with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface BooleanSupplier extends Checked { + /** + * Get the supplied value. + */ boolean getAsBoolean() throws E; @Override @@ -495,6 +778,15 @@ default java.util.function.BooleanSupplier ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static BooleanSupplier use(BooleanSupplier o) { @@ -502,10 +794,18 @@ BooleanSupplier use(BooleanSupplier o) } } + /** + * Like {@link java.util.function.DoubleSupplier} but with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface DoubleSupplier extends Checked { + /** + * Get the supplied value. + */ double getAsDouble() throws E; @Override @@ -524,16 +824,33 @@ default java.util.function.DoubleSupplier ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static DoubleSupplier use(DoubleSupplier o) { return o; } } + /** + * Like {@link java.util.function.IntSupplier} but with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface IntSupplier extends Checked { + /** + * Get the supplied value. + */ int getAsInt() throws E; @Override @@ -552,16 +869,33 @@ default java.util.function.IntSupplier ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static IntSupplier use(IntSupplier o) { return o; } } + /** + * Like {@link java.util.function.LongSupplier} but with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface LongSupplier extends Checked { + /** + * Get the supplied value. + */ long getAsLong() throws E; @Override @@ -580,6 +914,15 @@ default java.util.function.LongSupplier ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static LongSupplier use(LongSupplier o) { return o; @@ -590,48 +933,116 @@ static LongSupplier use(LongSupplier o) * Suppliers without checked-exception-less Java API counterparts. */ + /** + * A supplier of byte-valued results, with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ByteSupplier extends Closing.Trivial, E> { + /** + * Get the supplied value. + */ byte getAsByte() throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ByteSupplier use(ByteSupplier o) { return o; } } + /** + * A supplier of short-valued results, with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ShortSupplier extends Closing.Trivial, E> { + /** + * Get the supplied value. + */ short getAsShort() throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ShortSupplier use(ShortSupplier o) { return o; } } + /** + * A supplier of char-valued results, with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface CharSupplier extends Closing.Trivial, E> { + /** + * Get the supplied value. + */ char getAsChar() throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static CharSupplier use(CharSupplier o) { return o; } } + /** + * A supplier of float-valued results, with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface FloatSupplier extends Closing.Trivial, E> { + /** + * Get the supplied value. + */ float getAsFloat() throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static FloatSupplier use(FloatSupplier o) { return o; @@ -642,10 +1053,20 @@ static FloatSupplier use(FloatSupplier o) * Functions that have checked-exception-less counterparts in the Java API. */ + /** + * Like {@link java.util.function.Function} but with a body that can throw + * checked exceptions. + * @param Type of the function's parameter. + * @param Type of the function's result. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface Function extends Checked, E> { + /** + * Applies this function to the given argument. + */ R apply(T t) throws E; @Override @@ -664,6 +1085,15 @@ default java.util.function.Function ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static Function use(Function o) { @@ -671,10 +1101,19 @@ Function use(Function o) } } + /** + * Like {@link java.util.function.ToDoubleFunction} but with a body that can + * throw checked exceptions. + * @param Type of the function's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ToDoubleFunction extends Checked, E> { + /** + * Applies this function to the given argument. + */ double apply(T t) throws E; @Override @@ -693,6 +1132,15 @@ default java.util.function.ToDoubleFunction ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ToDoubleFunction use(ToDoubleFunction o) { @@ -700,10 +1148,19 @@ ToDoubleFunction use(ToDoubleFunction o) } } + /** + * Like {@link java.util.function.ToIntFunction} but with a body that can + * throw checked exceptions. + * @param Type of the function's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ToIntFunction extends Checked, E> { + /** + * Applies this function to the given argument. + */ int apply(T t) throws E; @Override @@ -722,6 +1179,15 @@ default java.util.function.ToIntFunction ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ToIntFunction use(ToIntFunction o) { @@ -729,10 +1195,19 @@ ToIntFunction use(ToIntFunction o) } } + /** + * Like {@link java.util.function.ToLongFunction} but with a body that can + * throw checked exceptions. + * @param Type of the function's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ToLongFunction extends Checked, E> { + /** + * Applies this function to the given argument. + */ long apply(T t) throws E; @Override @@ -751,6 +1226,15 @@ default java.util.function.ToLongFunction ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ToLongFunction use(ToLongFunction o) { @@ -758,12 +1242,21 @@ ToLongFunction use(ToLongFunction o) } } + /** + * Like {@link java.util.function.Predicate} but with a body that can + * throw checked exceptions. + * @param Type of the predicate's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface Predicate extends Checked, E> { boolean test(T t) throws E; + /** + * Evaluates this predicate on the given argument. + */ default Predicate negate() { return t -> ! test(t); @@ -785,6 +1278,15 @@ default java.util.function.Predicate ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static Predicate use(Predicate o) { @@ -796,12 +1298,30 @@ Predicate use(Predicate o) * Functions without checked-exception-less Java API counterparts. */ + /** + * Represents a function that produces a byte-valued result and can + * throw checked exceptions. + * @param Type of the function's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ToByteFunction extends Closing.Trivial, E> { + /** + * Applies this function to the given argument. + */ byte apply(T t) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ToByteFunction use(ToByteFunction o) { @@ -809,12 +1329,30 @@ ToByteFunction use(ToByteFunction o) } } + /** + * Represents a function that produces a short-valued result and can + * throw checked exceptions. + * @param Type of the function's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ToShortFunction extends Closing.Trivial, E> { + /** + * Applies this function to the given argument. + */ short apply(T t) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ToShortFunction use(ToShortFunction o) { @@ -822,12 +1360,30 @@ ToShortFunction use(ToShortFunction o) } } + /** + * Represents a function that produces a char-valued result and can + * throw checked exceptions. + * @param Type of the function's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ToCharFunction extends Closing.Trivial, E> { + /** + * Applies this function to the given argument. + */ char apply(T t) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ToCharFunction use(ToCharFunction o) { @@ -835,12 +1391,30 @@ ToCharFunction use(ToCharFunction o) } } + /** + * Represents a function that produces a float-valued result and can + * throw checked exceptions. + * @param Type of the function's parameter. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ToFloatFunction extends Closing.Trivial, E> { + /** + * Applies this function to the given argument. + */ float apply(T t) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ToFloatFunction use(ToFloatFunction o) { @@ -852,10 +1426,20 @@ ToFloatFunction use(ToFloatFunction o) * Consumers that have checked-exception-less counterparts in the Java API. */ + /** + * Like {@link java.util.function.BiConsumer} but with a body that can + * throw checked exceptions. + * @param Type of the first argument to the operation. + * @param Type of the second argument to the operation. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface BiConsumer extends Checked, E> { + /** + * Performs this operation on the given arguments. + */ void accept(T t, U u) throws E; @Override @@ -874,6 +1458,15 @@ default java.util.function.BiConsumer ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static BiConsumer use(BiConsumer o) { @@ -881,10 +1474,19 @@ default java.util.function.BiConsumer ederWrap() } } + /** + * Like {@link java.util.function.Consumer} but with a body that can + * throw checked exceptions. + * @param Type of the input to the operation. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface Consumer extends Checked, E> { + /** + * Performs this operation on the given argument. + */ void accept(T t) throws E; @Override @@ -903,16 +1505,33 @@ default java.util.function.Consumer ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static Consumer use(Consumer o) { return o; } } + /** + * Like {@link java.util.function.DoubleConsumer} but with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface DoubleConsumer extends Checked { + /** + * Performs this operation on the given argument. + */ void accept(double value) throws E; @Override @@ -931,16 +1550,33 @@ default java.util.function.DoubleConsumer ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static DoubleConsumer use(DoubleConsumer o) { return o; } } + /** + * Like {@link java.util.function.IntConsumer} but with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface IntConsumer extends Checked { + /** + * Performs this operation on the given argument. + */ void accept(int value) throws E; @Override @@ -959,16 +1595,33 @@ default java.util.function.IntConsumer ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static IntConsumer use(IntConsumer o) { return o; } } + /** + * Like {@link java.util.function.LongConsumer} but with a body that can + * throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface LongConsumer extends Checked { + /** + * Performs this operation on the given argument. + */ void accept(long value) throws E; @Override @@ -987,6 +1640,15 @@ default java.util.function.LongConsumer ederWrap() }; } + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static LongConsumer use(LongConsumer o) { return o; @@ -997,12 +1659,29 @@ static LongConsumer use(LongConsumer o) * Consumers without checked-exception-less counterparts in the Java API. */ + /** + * Represents an operation that accepts a single boolean-valued argument + * and can throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface BooleanConsumer extends Closing.Trivial, E> { + /** + * Performs this operation on the given argument. + */ void accept(boolean value) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static BooleanConsumer use(BooleanConsumer o) { @@ -1010,48 +1689,116 @@ BooleanConsumer use(BooleanConsumer o) } } + /** + * Represents an operation that accepts a single byte-valued argument + * and can throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ByteConsumer extends Closing.Trivial, E> { + /** + * Performs this operation on the given argument. + */ void accept(byte value) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ByteConsumer use(ByteConsumer o) { return o; } } + /** + * Represents an operation that accepts a single short-valued argument + * and can throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface ShortConsumer extends Closing.Trivial, E> { + /** + * Performs this operation on the given argument. + */ void accept(short value) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static ShortConsumer use(ShortConsumer o) { return o; } } + /** + * Represents an operation that accepts a single char-valued argument + * and can throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface CharConsumer extends Closing.Trivial, E> { + /** + * Performs this operation on the given argument. + */ void accept(char value) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static CharConsumer use(CharConsumer o) { return o; } } + /** + * Represents an operation that accepts a single float-valued argument + * and can throw checked exceptions. + * @param Exception type that can be thrown by the body. + */ @FunctionalInterface interface FloatConsumer extends Closing.Trivial, E> { + /** + * Performs this operation on the given argument. + */ void accept(float value) throws E; + /** + * Shapes a lambda or method reference into an instance of this + * functional interface. + *

    + * This is simply an identity function that can take the place of a + * more unwieldy cast. + * @param Least upper bound of exception types o can throw. + * @param o The implementing lambda or method reference. + */ static FloatConsumer use(FloatConsumer o) { return o; @@ -1068,8 +1815,25 @@ static FloatConsumer use(FloatConsumer o) * value-based classes, and they will behave. */ + /** + * Head of a family of {@link java.util.Optional Optional}-like types + * covering the Java primitives that the {@code java.util.Optional...} + * classes do not cover, and whose methods that expect functional interfaces + * will accept the checked-exception versions declared here. + *

    + * Each {@code Optional}Foo class here should be treated as if + * it were a Java "value-based" class; that they might have this class as + * an ancestor, or any superclass/subclass relationships at all, may change + * and should not be relied on. It may be convenient to + * {@code import static} the {@code ofNullable} methods of this class, + * however, which even cover the {@code java.util}-supplied primitive + * optionals. + */ abstract class OptionalBase { + /** + * If a value is present, returns true, otherwise false. + */ public boolean isPresent() { return false; @@ -1098,48 +1862,80 @@ public String toString() return getClass().getSimpleName() + ".empty"; } + /** + * Return an {@code OptionalDouble} representing the argument, empty + * if the argument is null. + */ public static OptionalDouble ofNullable(Double value) { return null == value ? OptionalDouble.empty() : OptionalDouble.of(value); } + /** + * Return an {@code OptionalInt} representing the argument, empty + * if the argument is null. + */ public static OptionalInt ofNullable(Integer value) { return null == value ? OptionalInt.empty() : OptionalInt.of(value); } + /** + * Return an {@code OptionalLong} representing the argument, empty + * if the argument is null. + */ public static OptionalLong ofNullable(Long value) { return null == value ? OptionalLong.empty() : OptionalLong.of(value); } + /** + * Return an {@code OptionalBoolean} representing the argument, empty + * if the argument is null. + */ public static OptionalBoolean ofNullable(Boolean value) { return null == value ? OptionalBoolean.EMPTY : OptionalBoolean.of(value); } + /** + * Return an {@code OptionalByte} representing the argument, empty + * if the argument is null. + */ public static OptionalByte ofNullable(Byte value) { return null == value ? OptionalByte.EMPTY : OptionalByte.of(value); } + /** + * Return an {@code OptionalShort} representing the argument, empty + * if the argument is null. + */ public static OptionalShort ofNullable(Short value) { return null == value ? OptionalShort.EMPTY : OptionalShort.of(value); } + /** + * Return an {@code OptionalChar} representing the argument, empty + * if the argument is null. + */ public static OptionalChar ofNullable(Character value) { return null == value ? OptionalChar.EMPTY : OptionalChar.of(value); } + /** + * Return an {@code OptionalFloat} representing the argument, empty + * if the argument is null. + */ public static OptionalFloat ofNullable(Float value) { return null == value ? @@ -1147,10 +1943,27 @@ public static OptionalFloat ofNullable(Float value) } } + /** + * A container object which may or may not contain a {@code boolean} value. + */ class OptionalBoolean extends OptionalBase { + /** + * An empty {@code OptionalBoolean}, for convenience; not to be used in + * identity-sensitive operations. + */ public static final OptionalBoolean EMPTY = new OptionalBoolean(); + + /** + * An {@code OptionalBoolean} containing {@code false}, for convenience; + * not to be used in identity-sensitive operations. + */ public static final OptionalBoolean FALSE = new False(); + + /** + * An {@code OptionalBoolean} containing {@code true}, for convenience; + * not to be used in identity-sensitive operations. + */ public static final OptionalBoolean TRUE = new True(); private OptionalBoolean() @@ -1299,8 +2112,15 @@ public boolean getAsBoolean() } } + /** + * A container object which may or may not contain a {@code byte} value. + */ class OptionalByte extends OptionalBase { + /** + * An empty {@code OptionalByte}, for convenience; not to be used in + * identity-sensitive operations. + */ public static final OptionalByte EMPTY = new OptionalByte(); private OptionalByte() @@ -1430,8 +2250,15 @@ public byte orElseThrow( } } + /** + * A container object which may or may not contain a {@code short} value. + */ class OptionalShort extends OptionalBase { + /** + * An empty {@code OptionalShort}, for convenience; not to be used in + * identity-sensitive operations. + */ public static final OptionalShort EMPTY = new OptionalShort(); private OptionalShort() @@ -1562,8 +2389,15 @@ public short orElseThrow( } } + /** + * A container object which may or may not contain a {@code char} value. + */ class OptionalChar extends OptionalBase { + /** + * An empty {@code OptionalChar}, for convenience; not to be used in + * identity-sensitive operations. + */ public static final OptionalChar EMPTY = new OptionalChar(); private OptionalChar() @@ -1693,8 +2527,15 @@ public char orElseThrow( } } + /** + * A container object which may or may not contain a {@code float} value. + */ class OptionalFloat extends OptionalBase { + /** + * An empty {@code OptionalFloat}, for convenience; not to be used in + * identity-sensitive operations. + */ public static final OptionalFloat EMPTY = new OptionalFloat(); private OptionalFloat() From 5701799d750f2565f3c434fb47831dfaa72f4aa2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 17:02:00 -0400 Subject: [PATCH 0779/1087] So MSVC doesn't believe in empty initializers Yay CI! --- pljava-so/src/main/c/Function.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 15c14473..b9ae2f6c 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -554,7 +554,7 @@ Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct) pljava_Function_udtParseHandle, pljava_Function_udtReadHandle, pljava_Function_udtWriteHandle, pljava_Function_udtToStringHandle }; - char *langName[4] = {}; + char *langName[4] = { NULL, NULL, NULL, NULL }; bool trusted[4]; jobject handle[4]; int i; From b747a4cf2ae9aceefa45cc0ba0debe805528000f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 13 Feb 2019 01:00:57 -0500 Subject: [PATCH 0780/1087] Add an SQL_ASCII encoding. This is a principled Java take on the PostgreSQL definition of SQL_ASCII as an encoding where the seven-bit ASCII values are themselves and the remaining eight-bit values are who-knows-what. It isn't appropriate to just copy byte values with no conversion, as that would amount to saying we know the values correspond to LATIN-1, which would be lying. Java strings are by definition Unicode, so it's not ok to go stuffing code points in that do not mean what Unicode defines those code points to mean. What this decoder does is decode the seven-bit ASCII values as themselves, and decode each eight-bit value into a pair of Unicode noncharacters, one from the range u+fdd8 to u+fddf, followed by one from u+fde0 to u+fdef, where the first one's four low bits are the four high bits of the original value, and the second has the low four. The encoder transparently reverses that. Those noncharacter code points are permanently defined in Unicode to have no glyphs, no correspondence to specific characters, and no interesting properties. Implementing this charset allows PL/Java code to work usefully in a database with SQL_ASCII encoding, when the expectation is that whatever the code needs to recognize, act on, or edit will be in ASCII, and any non-ASCII content can be passed along uninterpreted and unchanged. --- .../src/main/resources/pljava.policy | 2 + pljava/src/main/java/module-info.java | 3 + .../postgresql/pljava/internal/SQL_ASCII.java | 214 ++++++++++++++++++ pljava/src/test/java/CharsetTest.java | 59 +++++ pom.xml | 5 + 5 files changed, 283 insertions(+) create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java create mode 100644 pljava/src/test/java/CharsetTest.java diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index d2723126..7513219e 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -54,6 +54,8 @@ grant { // broad grant is not necessary, and can be narrowed below if desired. // grant codebase "${org.postgresql.pljava.codesource}" { + permission java.lang.RuntimePermission + "charsetProvider"; permission java.lang.RuntimePermission "createClassLoader"; permission java.util.logging.LoggingPermission diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 18385c21..610f5861 100644 --- a/pljava/src/main/java/module-info.java +++ b/pljava/src/main/java/module-info.java @@ -26,6 +26,9 @@ provides java.net.spi.URLStreamHandlerProvider with org.postgresql.pljava.sqlj.Handler; + provides java.nio.charset.spi.CharsetProvider + with org.postgresql.pljava.internal.SQL_ASCII.Provider; + provides java.sql.Driver with org.postgresql.pljava.jdbc.SPIDriver; provides org.postgresql.pljava.Session diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java b/pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java new file mode 100644 index 00000000..1e3ef747 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2020 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.internal; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.spi.CharsetProvider; +import static java.nio.charset.StandardCharsets.US_ASCII; + +import static java.util.Collections.singletonList; +import java.util.Iterator; +import java.util.List; + +/** + * An {@code SQL_ASCII}, a/k/a {@code X-PGSQL_ASCII}, "character set". + *

    + * This is a principled Java take on the PostgreSQL definition of + * SQL_ASCII as an encoding where the seven-bit ASCII values are + * themselves and the remaining eight-bit values are who-knows-what. + * It isn't appropriate to just copy byte values with no conversion, + * as that would amount to saying we know the values correspond to + * LATIN-1, which would be lying. Java strings are by definition Unicode, + * so it's not ok to go stuffing code points in that do not mean what + * Unicode defines those code points to mean. + *

    + * What this decoder does is decode the seven-bit ASCII values as + * themselves, and decode each eight-bit value into a pair of Unicode + * noncharacters, one from the range u+fdd8 to u+fddf, followed by one + * from u+fde0 to u+fdef, where the first one's four low bits are the + * four high bits of the original value, and the second has the low four. + * The encoder transparently reverses that. + *

    + * Those noncharacter code points are permanently defined in Unicode + * to have no glyphs, no correspondence to specific characters, and + * no interesting properties. Implementing this charset allows PL/Java + * code to work usefully in a database with SQL_ASCII encoding, when the + * expectation is that whatever the code needs to recognize, act on, or + * edit will be in ASCII, and any non-ASCII content can be passed along + * uninterpreted and unchanged. + */ +class SQL_ASCII extends Charset +{ + static class Holder + { + static final List s_list = + singletonList((Charset)new SQL_ASCII()); + } + + + public static class Provider extends CharsetProvider + { + static final String s_canonName = "X-PGSQL_ASCII"; + static final String[] s_aliases = { "SQL_ASCII" }; + + @Override + public Charset charsetForName(String charsetName) + { + if ( s_canonName.equalsIgnoreCase(charsetName) ) + return Holder.s_list.get(0); + for ( String s : s_aliases ) + if ( s.equalsIgnoreCase(charsetName) ) + return Holder.s_list.get(0); + return null; + } + + @Override + public Iterator charsets() + { + return Holder.s_list.iterator(); + } + } + + + private SQL_ASCII() + { + super(Provider.s_canonName, Provider.s_aliases); + } + + @Override + public boolean contains(Charset cs) + { + return this.equals(cs) || US_ASCII.equals(cs); + } + + @Override + public CharsetDecoder newDecoder() + { + return new Decoder(); + } + + @Override + public CharsetEncoder newEncoder() + { + return new Encoder(); + } + + + static class Decoder extends CharsetDecoder + { + Decoder() + { + super(Holder.s_list.get(0), 1.002f, 2.0f); + } + + @Override + protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) + { + int ipos = in.position(); + int opos = out.position(); + int ilim = in.limit(); + int olim = out.limit(); + + for ( ; ipos < ilim ; ++ ipos ) + { + char b = (char)(0xff & in.get(ipos)); + + if ( b < 128 ) + { + if ( opos == olim ) + { + in.position(ipos); + out.position(opos); + return CoderResult.OVERFLOW; + } + out.put(opos++, b); + } + else + { + if ( opos + 1 >= olim ) + { + in.position(ipos); + out.position(opos); + return CoderResult.OVERFLOW; + } + out.put(opos++, (char)(0xFDD0 | (b >> 4))); + out.put(opos++, (char)(0xFDE0 | (b & 0xf))); + } + } + in.position(ipos); + out.position(opos); + return CoderResult.UNDERFLOW; + } + } + + static class Encoder extends CharsetEncoder + { + Encoder() + { + super(Holder.s_list.get(0), 0.998f, 1.0f); + } + + @Override + protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) + { + int ipos = in.position(); + int opos = out.position(); + int ilim = in.limit(); + int olim = out.limit(); + + for ( ; ipos < ilim ; ++ ipos ) + { + if ( opos == olim ) + { + in.position(ipos); + out.position(opos); + return CoderResult.OVERFLOW; + } + + char c = in.get(ipos); + + if ( '\uFDD8' <= c && c < '\uFDE0' ) + { + if ( ipos + 1 == ilim ) + break; + + char d = in.get(ipos + 1); + + if ( '\uFDE0' > d || d > '\uFDEF' ) + { + in.position(ipos); + out.position(opos); + return CoderResult.malformedForLength(2); + } + c = (char)(( (c & 0xf) << 4 ) | (d & 0xf)); + ++ ipos; + } + else if ( c >= 128 ) + { + in.position(ipos); + out.position(opos); + return CoderResult.unmappableForLength(1); + } + out.put(opos++, (byte)c); + } + in.position(ipos); + out.position(opos); + return CoderResult.UNDERFLOW; + } + } +} diff --git a/pljava/src/test/java/CharsetTest.java b/pljava/src/test/java/CharsetTest.java new file mode 100644 index 00000000..5049605d --- /dev/null +++ b/pljava/src/test/java/CharsetTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 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.internal; + +import junit.framework.TestCase; + +import static org.junit.Assert.*; +import org.junit.Ignore; +import static org.hamcrest.CoreMatchers.*; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +import java.nio.charset.Charset; +import static java.nio.charset.StandardCharsets.US_ASCII; + +import static java.util.regex.Pattern.matches; + +public class CharsetTest extends TestCase +{ + public CharsetTest(String name) { super(name); } + + public void testSQL_ASCII() throws Exception + { + Charset sqa = Charset.forName("SQL_ASCII"); + assertNotNull(sqa); + + assertTrue(sqa.contains(sqa)); + assertTrue(sqa.contains(US_ASCII)); + + ByteBuffer bb = ByteBuffer.allocate(256); + + while ( bb.hasRemaining() ) + bb.put((byte)bb.position()); + + bb.flip(); + + CharBuffer cb = sqa.decode(bb); + + assertTrue(matches("[\\u0000-\\u007f]{128}+" + + "(?:[\\ufdd8-\\ufddf][\\ufde0-\\ufdef]){128}+", cb)); + + cb.rewind(); + + ByteBuffer bb2 = sqa.encode(cb); + bb.rewind(); + + assertTrue(bb2.equals(bb)); + } +} diff --git a/pom.xml b/pom.xml index cc8b1d68..2da8943d 100644 --- a/pom.xml +++ b/pom.xml @@ -197,6 +197,11 @@ maven-jar-plugin 3.0.2 + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + org.apache.maven.plugins maven-site-plugin From 46715abda6b93cb09f007a4d69fcd985d468e76b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 20 Jan 2020 20:37:33 -0500 Subject: [PATCH 0781/1087] Make SQL_ASCII another special case in String.c. String.c now treats both UTF-8 and SQL_ASCII server encodings as special cases, where the Java codec of the same name will be used directly on the native bytes, while all other encodings are handled as two-step conversions using the PostgreSQL native conversion between the server encoding and UTF-8, and the Java UTF-8 codec. Future project: simplify all this by assuming (as SQLXML does) that there is a Java charset corresponding to the server encoding, found either by name match or by manual configuration (-Dorg.postgresql.server.encoding in pljava.vmoptions), so that all supported server encodings are handled the same way. The current addition of SQL_ASCII as a Java charset is a step down that path. Perhaps there could also be a CharsetProvider that "provides" any missing codecs by wrapping the native ones. --- pljava-so/src/main/c/type/String.c | 59 ++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index e095485e..95495a39 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB - Thomas Hallgren + * Chapman Flack */ #include "pljava/type/String_priv.h" #include "pljava/HashMap.h" @@ -377,15 +381,15 @@ void String_initialize(void) static void String_initialize_codec() { + /* + * Wondering why this function doesn't bother deleting its many local refs? + * The call is wrapped in pushLocalFrame/popLocalFrame in the caller. + */ jmethodID string_intern = PgObject_getJavaMethod(s_String_class, "intern", "()Ljava/lang/String;"); jstring empty = JNI_newStringUTF( ""); - jclass scharset_class = - PgObject_getJavaClass("java/nio/charset/StandardCharsets"); - jfieldID scharset_UTF_8 = PgObject_getStaticJavaField(scharset_class, - "UTF_8", "Ljava/nio/charset/Charset;"); - jobject u8cs = JNI_getStaticObjectField(scharset_class, scharset_UTF_8); - jclass charset_class = JNI_getObjectClass(u8cs); + jclass charset_class = + PgObject_getJavaClass("java/nio/charset/Charset"); jmethodID charset_newDecoder = PgObject_getJavaMethod(charset_class, "newDecoder", "()Ljava/nio/charset/CharsetDecoder;"); jmethodID charset_newEncoder = PgObject_getJavaMethod(charset_class, @@ -402,11 +406,38 @@ static void String_initialize_codec() jfieldID underflow = PgObject_getStaticJavaField(result_class, "UNDERFLOW", "Ljava/nio/charset/CoderResult;"); jclass buffer_class = PgObject_getJavaClass("java/nio/Buffer"); + jobject servercs; + + s_server_encoding = GetDatabaseEncoding(); + + if ( PG_SQL_ASCII == s_server_encoding ) + { + jmethodID forname = + PgObject_getStaticJavaMethod(charset_class, + "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;"); + jstring sql_ascii = JNI_newStringUTF("X-PGSQL_ASCII"); + + s_two_step_conversion = false; + + servercs = JNI_callStaticObjectMethodLocked(charset_class, + forname, sql_ascii); + } + else + { + jclass scharset_class = + PgObject_getJavaClass("java/nio/charset/StandardCharsets"); + jfieldID scharset_UTF_8 = PgObject_getStaticJavaField(scharset_class, + "UTF_8", "Ljava/nio/charset/Charset;"); + + s_two_step_conversion = PG_UTF8 != s_server_encoding; + + servercs = JNI_getStaticObjectField(scharset_class, scharset_UTF_8); + } s_CharsetDecoder_instance = - JNI_newGlobalRef(JNI_callObjectMethod(u8cs, charset_newDecoder)); + JNI_newGlobalRef(JNI_callObjectMethod(servercs, charset_newDecoder)); s_CharsetEncoder_instance = - JNI_newGlobalRef(JNI_callObjectMethod(u8cs, charset_newEncoder)); + JNI_newGlobalRef(JNI_callObjectMethod(servercs, charset_newEncoder)); s_CharsetDecoder_decode = PgObject_getJavaMethod(decoder_class, "decode", "(Ljava/nio/ByteBuffer;)Ljava/nio/CharBuffer;"); s_CharsetEncoder_encode = PgObject_getJavaMethod(encoder_class, "encode", @@ -432,7 +463,5 @@ static void String_initialize_codec() s_the_empty_string = JNI_newGlobalRef( JNI_callObjectMethod(empty, string_intern)); - s_server_encoding = GetDatabaseEncoding(); - s_two_step_conversion = PG_UTF8 != s_server_encoding; uninitialized = false; } From d081a8ca2becd743c3e10a26330deb754e1894d3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 1 Feb 2020 00:01:55 -0500 Subject: [PATCH 0782/1087] Update the character-set encodings doc --- src/site/markdown/use/charsets.md | 37 ++++++++++++++++++++++-------- src/site/markdown/use/variables.md | 13 ++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/site/markdown/use/charsets.md b/src/site/markdown/use/charsets.md index f0f41cb5..d22a86eb 100644 --- a/src/site/markdown/use/charsets.md +++ b/src/site/markdown/use/charsets.md @@ -39,11 +39,26 @@ The encoding `SQL_ASCII`, as described [in the PostgreSQL documentation][mbc], ### Using PL/Java with server encoding `SQL_ASCII` -When the server encoding is `SQL_ASCII`, the only safe way for PL/Java to treat -it is as strict ASCII, throwing exceptions if asked to convert any string -involving characters outside of ASCII. - -If PL/Java must be used with `SQL_ASCII` as the server encoding, the cases are +Java strings are Unicode by definition; PL/Java must not create strings where +some of the characters have their Unicode meanings while others mean something +else. PL/Java does supply a `Charset` with encoder and decoder for `SQL_ASCII`, +which behaves as follows: + +* Encoded bytes in the ASCII range map to the corresponding Unicode characters. +* Other encoded bytes are stuffed, two `char`s for each byte, into a range of + codepoints Unicode defines as permanently unassigned. For those codepoints, + `Character.getType` returns `UNASSIGNED`, `Character.getName` returns null, + `Character.UnicodeScript.of` returns `UNKNOWN`, and they will not match + patterns for letters, digits, punctuation, or generally anything else + interesting (other than `\p{Cn}`, the exact test for noncharacters). + +The mapping is transparently reversed when such a Java string is returned +to PostgreSQL. With this convention, Java code can work usefully with +`SQL_ASCII` encoded data, matching and manipulating the ASCII parts, while +treating the non-defined subsequences as opaque and returning them to PostgreSQL +unchanged. + +If PL/Java is used with `SQL_ASCII` as the server encoding, the cases are (by increasing complexity): 0. The database contains no non-ASCII data (or none that will be touched @@ -52,15 +67,19 @@ If PL/Java must be used with `SQL_ASCII` as the server encoding, the cases are 0. The database contains non-ASCII data all known to be in one standard encoding. It would be simplest for the database to be recreated with this encoding selected, but that may be impractical for various reasons. - In that case, this can be handled in the same way as the next case. + In that case, this can be handled in the same way as the next case, or + PL/Java can be 'lied to' about the server encoding by including a + `-Dorg.postgresql.server.encoding=...` in `pljava.vmoptions` that names + the known correct encoding instead. 0. The database contains non-ASCII data in _more than one_ encoding, with the application somehow knowing which encoding is used where. That is completely possible because `SQL_ASCII` does not guarantee or validate anything (which means it can also happen over time without being intended). - The rigorous approach in this case is to write Java code expecting and - returning `bytea` and some indication of the encoding to be used, and - perform explicit conversions. + Java code can find regions of strings that match the pattern + `(?:[\ufdd8-\ufddf][\ufde0-\ufdef])++` and pass those regions back through + the `SQL_ASCII` encoder, and then the decoder for whatever other encoding + it determines should apply. ## Using PL/Java with standard (not `SQL_ASCII`) encodings other than `UTF8` diff --git a/src/site/markdown/use/variables.md b/src/site/markdown/use/variables.md index ba3bf046..867b847b 100644 --- a/src/site/markdown/use/variables.md +++ b/src/site/markdown/use/variables.md @@ -24,14 +24,11 @@ These PostgreSQL configuration variables can influence PL/Java's operation: `server_encoding` : Another non-PL/Java variable, this affects all text/character strings exchanged between PostgreSQL and Java. `UTF8` as the database and server - encoding is _strongly_ recommended. If a different encoding is used, it + encoding is strongly recommended. If a different encoding is used, it should be any of the available _fully defined_ character encodings. In - particular, the PostgreSQL pseudo-encoding `SQL_ASCII` (which means - "characters within ASCII are ASCII, others are no-one-knows-what") will - _not_ work well with PL/Java, raising exceptions whenever strings contain - non-ASCII characters. (PL/Java can still be used in such a database, but - the application code needs to know what it's doing and use the right - conversion functions where needed.) + particular, the PostgreSQL pseudo-encoding `SQL_ASCII` does not fully + define what any values outside ASCII represent; it is usable, but + [subject to limitations][sqlascii]. `pljava.debug` : A boolean variable that, if set `on`, stops the process on first entry to @@ -183,4 +180,4 @@ These PostgreSQL configuration variables can influence PL/Java's operation: [jow]: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html [jou]: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html [vmop]: ../install/vmoptions.html - +[sqlascii]: charsets.html#Using_PLJava_with_server_encoding_SQL_ASCII From ea82ef1294ce5ca1f784dcda92252ca3cacbd1ff Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 14:27:34 -0400 Subject: [PATCH 0783/1087] Migrate a few examples to ResultSetProvider.Large Not necessarily the ones most likely to need 64-bit row counts (at the moment, I think even Saxon has an internal limitation to 32-bit sequence lengths), but these are trivial to update, and at least serve to illustrate use of the interface. --- .../postgresql/pljava/example/UsingProperties.java | 4 ++-- .../pljava/example/annotation/UsingProperties.java | 7 ++++--- .../org/postgresql/pljava/example/saxon/S9.java | 13 +++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingProperties.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingProperties.java index 6cf815d6..4d16286e 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingProperties.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/UsingProperties.java @@ -31,7 +31,7 @@ * of PL/Java's {@code ObjectPool} facility. * @author Thomas Hallgren */ -public class UsingProperties implements ResultSetProvider, PooledObject { +public class UsingProperties implements ResultSetProvider.Large, PooledObject { private static Logger s_logger = Logger.getAnonymousLogger(); public static ResultSetProvider getProperties() throws SQLException { @@ -71,7 +71,7 @@ public void activate() { } @Override - public boolean assignRowValues(ResultSet receiver, int currentRow) + public boolean assignRowValues(ResultSet receiver, long currentRow) throws SQLException { if (m_propertyIterator == null || !m_propertyIterator.hasMoreElements()) { s_logger.fine("no more rows, returning false"); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java index ddd9f497..bcd3f2b9 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -9,6 +9,7 @@ * Contributors: * Tada AB * Purdue University + * Chapman Flack */ package org.postgresql.pljava.example.annotation; @@ -32,7 +33,7 @@ * interface. * @author Thomas Hallgren */ -public class UsingProperties implements ResultSetProvider +public class UsingProperties implements ResultSetProvider.Large { private static Logger s_logger = Logger.getAnonymousLogger(); private final Iterator m_propertyIterator; @@ -56,7 +57,7 @@ public UsingProperties() } } - public boolean assignRowValues(ResultSet receiver, int currentRow) + public boolean assignRowValues(ResultSet receiver, long currentRow) throws SQLException { if(!m_propertyIterator.hasNext()) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java index 8b03cff9..8f530193 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java @@ -230,7 +230,7 @@ * 'let $e := PREMIER_NAME * return if ( empty($e) )then $DPREMIER else $e' * ]) AS ( - * id int, ordinality int, "COUNTRY_NAME" text, country_id text, + * id int, ordinality int8, "COUNTRY_NAME" text, country_id text, * size_sq_km float, size_other text, premier_name text * ); * @@ -288,7 +288,7 @@ * XQuery regular-expression methods provided here. * @author Chapman Flack */ -public class S9 implements ResultSetProvider +public class S9 implements ResultSetProvider.Large { private S9( XdmSequenceIterator xsi, @@ -803,8 +803,9 @@ private static SQLXML returnContent( * columns in the column definition list that follows the SQL call to this * function. This array must not be null. It is allowed for one element (and * no more than one) to be null, marking the corresponding column to be - * "FOR ORDINALITY" (the column must be of integer, or, ahem, "exact numeric - * with scale zero", type). + * "FOR ORDINALITY" (the column must be of "exact numeric with scale zero" + * type; PostgreSQL supports 64-bit row counters, so {@code int8} is + * recommended). * @param passing A row value whose columns will be supplied to the query * as parameters, just as described for * {@link #xq_ret_content xq_ret_content()}. If a context item is supplied, @@ -1188,7 +1189,7 @@ public void close() * be changed to match a future clarification of the spec. */ @Override - public boolean assignRowValues(ResultSet receive, int currentRow) + public boolean assignRowValues(ResultSet receive, long currentRow) throws SQLException { if ( 0 == currentRow ) @@ -1308,7 +1309,7 @@ public boolean assignRowValues(ResultSet receive, int currentRow) if ( null == xqe ) { - receive.updateInt( i, currentRow); + receive.updateLong( i, currentRow); continue; } From e783685f1210ddf8eef11bc1a28e857eaee326ab Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 14:42:46 -0400 Subject: [PATCH 0784/1087] Add to release notes ... with slight conforming amendments to versions.md and charsets.md. --- src/site/markdown/build/versions.md | 3 ++ src/site/markdown/releasenotes.md.vm | 80 +++++++++++++++++++++++++++- src/site/markdown/use/charsets.md | 2 + src/site/markdown/use/policy.md | 15 ++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index 52ab63f6..ccf79f7b 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -38,6 +38,9 @@ PL/Java has been successfully used with [Oracle Java][orj] and with [either the Hotspot or the OpenJ9 JVM][hsj9]. It can also be built and used with [GraalVM][]. +If building with GraalVM, please add `-Dpolyglot.js.nashorn-compat=true` on +the `mvn` command line. + [jvml]: ../use/variables.html [cds]: ../install/vmoptions.html#Class_data_sharing [orj]: https://www.oracle.com/technetwork/java/javase/downloads/index.html diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 24e08d3b..da1da349 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -39,9 +39,48 @@ When used with GraalVM as the runtime VM, PL/Java functions can use Graal's GraalVM. In this release, it is not yet possible to directly declare a function in a language other than Java. +If building with GraalVM, please add `-Dpolyglot.js.nashorn-compat=true` on +the `mvn` command line. + $h3 Changes -$h4 Validation at `CREATE FUNCTION` time +$h4 New configurable permissions may require configuration + +Prior to 1.6.0, PL/Java hard-coded the permissions that were available to +functions declared in the 'trusted' language `java` or the 'untrusted' language +`javaU`. With 1.6.0, the exact permissions available for both cases can be +configured in the `pljava.policy` file (found in the directory reported by +`pg_config --sysconfdir`) as described in the +[new policy documentation][policy]. + +Java's policy language can conditionally grant permissions but not deny them +if another clause grants them. Therefore, the default policy must be somewhat +restrictive, so a desired policy can be built from it with grant clauses. + +In the 1.6.0 default policy, 'trusted' (`java`) code has minimal permissions, +suitable for general computation and interacting with the database, and +'untrusted' (`javaU`) code has only the additional permission to access the +file system. Existing user functions that worked in PL/Java 1.5.x and performed +other actions, such as making network connections, will need the appropriate +permissions (such as `java.net.URLPermission` or `java.net.SocketPermission`) +granted via the policy file. + +The policy can grant permissions more selectively than just to `java` +or `javaU`. The [new documentation][policy] covers the details, and also how +to log, for troubleshooting purposes, the permissions being requested. + +Whatever the reason, all down the years, a favorite "is PL/Java working?" check +found online has been to read a Java system property with `System.getProperty`. +Not all of those examples pick properties that can be read under the default +policy. So, even some familiar habits like that may need revision, at least to +use a property like `java.version` that is readable by default. + +The former hard-coded permissions were by turns too lax or too strict, depending +on what was needed, and interfered in some cases with the operation of the Java +runtime itself, breaking (at least) its XSLT implementation and the profiling +functions of `visualvm`. This release fixes those issues. + +$h4 Validation at `CREATE FUNCTION` time may force changes to deployment procedures PL/Java can now detect problems with a function declaration, including missing dependencies, at the time of `CREATE FUNCTION`, rather than allowing the @@ -55,6 +94,20 @@ path first; in the past, the order did not matter. For details, see and the description of `check_function_bodies` in the [configuration variable reference](use/variables.html). +$h4 Java 9 module system; `pljava.classpath` -> `pljava.module_path` + +Because PL/Java itself is now modular code conforming to the module system +introduced with Java 9, one configuration variable has changed: +`pljava.classpath` is now `pljava.module_path`. + +As before, its default value will be correct when PL/Java is installed to +the usual locations. It should be rare for any installation to have needed +to think about the old one, or to need to think about the new one. For a +rare installation that does, the details are [in the documentation][jpms]. + +In this release, user code is not treated as modular; the `SQLJ.INSTALL_JAR` +routine still treats its jars as unnamed-module code on a class path, as before. + $h4 Improvements to the annotation-driven SQL generator $h5 Infers additional implicit ordering dependencies @@ -72,7 +125,18 @@ $h5 Generates variadic function declarations PL/Java 1.6 can declare functions that can be called from SQL with varying numbers of arguments. [Example code][variadic] is provided. -$h4 Build system and continuous integration +$h4 Better support for PostgreSQL's `SQL_ASCII` encoding + +PostgreSQL's legacy `SQL_ASCII` encoding is difficult to use in Java because +128 of its code points have no defined mapping to Unicode, which Java uses. +The page on [database character set encodings][charsets] has a section +suggesting workable approaches if PL/Java is used in a database with that +encoding. A new addition among those options is a Java `Charset` supporting +the encoding names `X-PGSQL_ASCII` or `SQL_ASCII`, which maps the ASCII +characters as expected, and reversibly encodes the others using Unicode +permanently-undefined codepoints. + +$h4 Build system, continuous integration, quality assurance * The `nar-maven-plugin` formerly used in the build has been replaced with a newly-developed Maven plugin tailored to PostgreSQL extension building. @@ -94,6 +158,10 @@ $h4 Build system and continuous integration platform-specific code to a minimum, and may be useful for other purposes. Some [documentation](develop/node.html) is included. +* Having fixed the permission issues that were breaking the profiling functions + of `visualvm`, it will be easier to incorporate profiling into future + development. + $h3 Enhancement requests addressed * [Add regression testing](${ghbug}11) @@ -104,6 +172,11 @@ $h3 Bugs fixed * [`-Dpljava.libjvmlocation` breaks Windows build](${ghbug}190) * [XML Schema regression-test failure in de_DE locale](${ghbug}312) +$h3 Updated PostgreSQL APIs tracked + +* 64-bit `FuncCallContext.call_cntr` (`ResultSetProvider`/`ResultSetHandle` + can now return more than `INT_MAX` rows) + $h3 Credits There is a PL/Java 1.6.0 thanks in part to @@ -116,10 +189,13 @@ and the many contributors to earlier versions. The work of Kartik Ohri in summer 2020 on the build system renovation and continuous integration was supported by Google Summer of Code. +[policy]: use/policy.html [linkage]: examples/examples.html#Exception_resolving_class_or_method_.28message_when_installing_examples.29 [udtd32f84e]: https://github.com/jcflack/pljava-udt-type-extension/commit/d32f84e [udt0066a1e]: https://github.com/jcflack/pljava-udt-type-extension/commit/0066a1e [variadic]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/Variadic.html#method.detail +[charsets]: use/charsets.html +[jpms]: use/jpms.html $h2 Earlier releases diff --git a/src/site/markdown/use/charsets.md b/src/site/markdown/use/charsets.md index d22a86eb..1b873e0a 100644 --- a/src/site/markdown/use/charsets.md +++ b/src/site/markdown/use/charsets.md @@ -101,6 +101,8 @@ except `UTF8`: > The Unicode escape syntax works only when the server encoding is UTF8. > When other server encodings are used, only code points in the ASCII range > (up to \007F) can be specified. + + PostgreSQL 13 eliminates this limitation. * The [`ascii` and `chr` functions][acfns] behave two different ways, depending on whether the server encoding is *a single-byte encoding*, or *any multi-byte encoding other than `UTF8`*. diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index 7c1c85cf..02cdf8b0 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -316,6 +316,20 @@ on some property that isn't readable under Java's default policy. Those examples should be changed to use a property that is normally readable, such as `java.version` or `org.postgresql.pljava.version`._ +## Troubleshooting + +When in doubt what permissions are needed to get some existing PL/Java code +working again, it may be helpful to add `-Djava.security.debug=access` in +the setting of `pljava.vmoptions`, and observe the messages on the PostgreSQL +backend's standard error (which should be included in the log file, +if `logging_collector` is `on`). It is not necessary to change the +`pljava.vmoptions` setting cluster-wide, such as in `postgresql.conf`; it can +be set in a single session for troubleshooting purposes. + +Other options for `java.security.debug` can be found in +[Troubleshooting Security][tssec]. Some can be used to filter the logging down +to requests for specific permissions or from a specific codebase. + ## Forward compatibility The current implementation makes use of the Java classes @@ -329,3 +343,4 @@ release, so relying on it is not recommended. [confvar]: variables.html [dopriv]: https://docs.oracle.com/en/java/javase/14/security/java-se-platform-security-architecture.html#GUID-E8898CB5-65BB-4D1A-A574-8F7112FC353F [sqljajl]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#alias_java_language +[tssec]: https://docs.oracle.com/en/java/javase/14/security/troubleshooting-security.html From 3492dbb336d241c0cdf49e6f61e920bcf0ea07a9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 11:54:54 -0400 Subject: [PATCH 0785/1087] Fix a few broken links out of javadoc Later javadoc versions add a directory level for module, changing the ../ count. Could be fixed by using RELDOTS and the recently-added RelativizingFileManager ... maybe later, but it isn't already used in these subprojects and seemed more work than just fixing a few links. --- .../src/main/java/org/postgresql/pljava/Session.java | 8 ++++---- .../java/org/postgresql/pljava/annotation/Function.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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 5706b8fb..7cd2e5fa 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Session.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Session.java @@ -118,8 +118,8 @@ public interface Session * unconditionally, which is incorrect for any PostgreSQL version newer * than 8.0, because it was unaware of {@code SET ROLE} introduced in * 8.1. Any actual use case for a method that ignores roles and reports - * only the session ID should be - * reported as an issue. + * only the session ID should be reported as an issue. */ @Deprecated(since="1.5.0", forRemoval=true) String getSessionUserName(); @@ -147,8 +147,8 @@ void executeAsOuterUser(Connection conn, String statement) * which is incorrect for any PostgreSQL version newer than 8.0, because * it was unaware of {@code SET ROLE} introduced in 8.1. Any actual use * case for a method that ignores roles and uses only the session ID - * should be reported as an - * issue. + * should be reported as an issue. */ @Deprecated(since="1.5.0", forRemoval=true) void executeAsSessionUser(Connection conn, String statement) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java index f3f7c603..e37e224e 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java @@ -184,7 +184,7 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; *

    * For much more on the practicalities of parallel query and PL/Java, * please see the users' guide. +'../../../../../../../use/parallel.html'>the users' guide. *

    * Appeared in PostgreSQL 9.6. */ From 63bea945a3073d5dc0287f18fe14d3f85f21a362 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 18:45:31 -0400 Subject: [PATCH 0786/1087] Poke migration-management versions for 1.6.0 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index c48db2d0..f89d0015 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -668,6 +668,8 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_6_0 = REL_1_5_0; + static final SchemaVariant REL_1_5_6 = REL_1_5_0; static final SchemaVariant REL_1_5_5 = REL_1_5_0; static final SchemaVariant REL_1_5_4 = REL_1_5_0; From 06f64e810d3d70d1419914e8935a4928de97d111 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 19:49:07 -0400 Subject: [PATCH 0787/1087] Add control file in preparation for next release Now that 1.6.0 is released, the next release should include an extension SQL file allowing upgrade from 1.6.0. Advance version to 1.6-SNAPSHOT. This is a change from past practice, which always updated to 1.x.-SNAPSHOT, which always created a change in every POM that would have to be resolved out when merging up to master. This way will require the same resolution tedium once, but not for future 1.6.x releases. 1.6-SNAPSHOT will just always mean a snapshot on the way to whatever will be released next in the 1.6 line. --- pljava-ant/pom.xml | 2 +- pljava-api/pom.xml | 2 +- pljava-examples/pom.xml | 2 +- pljava-packaging/build.xml | 4 ++++ pljava-packaging/pom.xml | 2 +- pljava-pgxs/pom.xml | 2 +- pljava-so/pom.xml | 2 +- pljava/pom.xml | 2 +- pom.xml | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index 01ddaecb..0f25aeb3 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pljava-ant PL/Java Ant tasks diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index f0da547a..b655cb6a 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pljava-api PL/Java API diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 9d6f7b46..320788c2 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pljava-examples PL/Java examples diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index ea612df8..bd048c37 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -255,6 +255,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pljava-packaging PL/Java packaging diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 7b929397..79660f0d 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -5,7 +5,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pljava-pgxs diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index ae33321f..e3555e78 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pljava-so PL/Java backend native code diff --git a/pljava/pom.xml b/pljava/pom.xml index bf9ab611..51eb86f5 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pljava PL/Java backend Java code diff --git a/pom.xml b/pom.xml index 2da8943d..f32deee8 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.postgresql pljava.app - 1.6.0-SNAPSHOT + 1.6-SNAPSHOT pom PostgreSQL PL/Java https://tada.github.io/pljava/ From d0467c33173e0a7a282e0116b2e2fedb27de34b6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Oct 2020 19:56:34 -0400 Subject: [PATCH 0788/1087] Branch 'master' is now dev for a future 2.0 It is not recommended for production. Released versions at the moment are from REL1_6_STABLE (relatively modern, Java 9+, PostgreSQL 9.5+) or from REL1_5_STABLE (if support for Java < 9 or PostgreSQL < 9.5 is needed). --- pljava-ant/pom.xml | 2 +- pljava-api/pom.xml | 2 +- pljava-examples/pom.xml | 2 +- pljava-packaging/pom.xml | 2 +- pljava-pgxs/pom.xml | 2 +- pljava-so/pom.xml | 2 +- pljava/pom.xml | 2 +- pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index 01ddaecb..15e6c675 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pljava-ant PL/Java Ant tasks diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index f0da547a..314d7523 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pljava-api PL/Java API diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 9d6f7b46..973cf4bf 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pljava-examples PL/Java examples diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 82ea1e5b..a77b8763 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pljava-packaging PL/Java packaging diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 7b929397..19896dbd 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -5,7 +5,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pljava-pgxs diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index ae33321f..dbd52812 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pljava-so PL/Java backend native code diff --git a/pljava/pom.xml b/pljava/pom.xml index bf9ab611..fb9b23f1 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pljava PL/Java backend Java code diff --git a/pom.xml b/pom.xml index 2da8943d..7ebb4c29 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.postgresql pljava.app - 1.6.0-SNAPSHOT + 2-SNAPSHOT pom PostgreSQL PL/Java https://tada.github.io/pljava/ From b597a203420b2c34bab57390276e013e3d882ed8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 19 Oct 2020 20:20:03 -0400 Subject: [PATCH 0789/1087] Add a Cast annotation --- .../postgresql/pljava/annotation/Cast.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/annotation/Cast.java diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Cast.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Cast.java new file mode 100644 index 00000000..140993f8 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Cast.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a PostgreSQL {@code CAST}. + *

    + * May annotate a Java method (which should also carry a + * {@link Function @Function} annotation, making it a PostgreSQL function), + * or a class or interface (just to have a place to put it when not directly + * associated with a method). + *

    + * If not applied to a method, must supply {@code path=} specifying + * {@code BINARY} or {@code INOUT}. + *

    + * The source and target types must be specified with {@code from} and + * {@code to}, unless the annotation appears on a method, in which case these + * default to the first parameter and return types of the function, + * respectively. + *

    + * The cast will, by default, have to be applied explicitly, unless + * {@code application=ASSIGNMENT} or {@code application=IMPLICIT} is given. + * + * @author Chapman Flack + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +@Repeatable(Cast.Container.class) +@Retention(RetentionPolicy.CLASS) +public @interface Cast +{ + /** + * When this cast can be applied: only in explicit form, when used in + * assignment context, or implicitly whenever needed. + */ + enum Application { EXPLICIT, ASSIGNMENT, IMPLICIT }; + + /** + * A known conversion path when a dedicated function is not supplied: + * {@code BINARY} for two types that are known to have the same internal + * representation, or {@code INOUT} to invoke the first type's text-output + * function followed by the second type's text-input function. + */ + enum Path { BINARY, INOUT }; + + /** + * The source type to be cast. Will default to the first parameter type of + * the associated function, if known. + *

    + * PostgreSQL will allow this type and the function's first parameter type + * to differ, if there is an existing binary cast between them. That cannot + * be checked at compile time, so a cast with a different type given here + * might successfully compile but fail to deploy in PostgreSQL. + */ + String from() default ""; + + /** + * The target type to cast to. Will default to the return type of + * the associated function, if known. + *

    + * PostgreSQL will allow this type and the function's return type + * to differ, if there is an existing binary cast between them. That cannot + * be checked at compile time, so a cast with a different type given here + * might successfully compile but fail to deploy in PostgreSQL. + */ + String to() default ""; + + /** + * A stock conversion path when a dedicated function is not supplied: + * {@code BINARY} for two types that are known to have the same internal + * representation, or {@code INOUT} to invoke the first type's text-output + * function followed by the second type's text-input function. + *

    + * To declare an {@code INOUT} cast, {@code with=INOUT} must appear + * explicitly; the default value is treated as unspecified. + */ + Path path() default Path.INOUT; + + /** + * When this cast can be applied: only in explicit form, when used in + * assignment context, or implicitly whenever needed. + */ + Application application() default Application.EXPLICIT; + + /** + * One or more arbitrary labels that will be considered 'provided' by the + * object carrying this annotation. The deployment descriptor will be + * 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. + */ + String[] provides() default {}; + + /** + * One or more arbitrary labels that will be considered 'required' by the + * object carrying this annotation. The deployment descriptor will be + * generated in such an order that other objects that 'provide' labels + * 'required' by this come earlier in the output for install actions, and + * later for remove actions. + */ + String[] requires() default {}; + + /** + * The {@code } to be used around SQL code generated + * for this cast. Defaults to {@code PostgreSQL}. Set explicitly to + * {@code ""} to emit code not wrapped in an {@code }. + */ + String implementor() default ""; + + /** + * A comment to be associated with the cast. If left to default, and the + * annotated Java construct has a doc comment, its first sentence will be + * used. If an empty string is explicitly given, no comment will be set. + */ + String comment() default ""; + + /** + * @hidden container type allowing Cast to be repeatable. + */ + @Documented + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.CLASS) + @interface Container + { + Cast[] value(); + } +} From a870347fd40c1b01b9b7b6d01ecc758de579e7fd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 21 Oct 2020 21:31:50 -0400 Subject: [PATCH 0790/1087] Generalize SQLActionsImpl to Container Now that there will be more than one repeatable annotation type, and there isn't really anything else about the containing annotation that matters. In passing, squelch a deprecation warning in LexicalsTest (a junit method was deprecated, in favor of one from hamcrest). --- .../pljava/annotation/SQLAction.java | 4 +- .../pljava/annotation/SQLActions.java | 8 +++- .../annotation/processing/DDRProcessor.java | 37 +++++++++++++++---- pljava-api/src/test/java/LexicalsTest.java | 1 + 4 files changed, 39 insertions(+), 11 deletions(-) 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 edbf63fe..face7771 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-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -35,6 +36,7 @@ */ @Documented @Target({ElementType.PACKAGE,ElementType.TYPE}) +@Repeatable(SQLActions.class) @Retention(RetentionPolicy.CLASS) public @interface SQLAction { diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLActions.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLActions.java index bc618b4a..753e1df2 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLActions.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLActions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -21,7 +21,11 @@ /** * Container for multiple {@link SQLAction} annotations (in case it is * convenient to hang more than one on a given program element). - * + *

    + * This container annotation is documented for historical reasons (it existed + * in PL/Java versions targeting earlier Java versions than 8). In new code, it + * would be more natural to simply hang more than one {@code SQLAction} + * annotation directly on a program element. * @author Thomas Hallgren - pre-Java6 version * @author Chapman Flack (Purdue Mathematics) - updated to Java6, * added SQLActions 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 63c71670..a9c6e8ca 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 @@ -763,7 +763,8 @@ void processSQLActions( Element e) { if ( am.getAnnotationType().asElement().equals( AN_SQLACTIONS) ) { - SQLActionsImpl sas = new SQLActionsImpl(); + Container sas = + new Container<>(SQLActionImpl.class); populateAnnotationImpl( sas, e, am); for ( SQLAction sa : sas.value() ) putSnippet( sa, (Snippet)sa); @@ -1265,22 +1266,42 @@ public void setName( Object o, boolean explicit, Element e) } } - class SQLActionsImpl extends AbstractAnnotationImpl implements SQLActions + class Container + extends AbstractAnnotationImpl { - public SQLAction[] value() { return _value; } + public T[] value() { return _value; } - SQLAction[] _value; + T[] _value; + final Class _clazz; + + Container(Class clazz) + { + _clazz = clazz; + } public void setValue( Object o, boolean explicit, Element e) { AnnotationMirror[] ams = avToArray( o, AnnotationMirror.class); - _value = new SQLAction [ ams.length ]; + + @SuppressWarnings("unchecked") + T[] t = (T[])Array.newInstance( _clazz, ams.length); + _value = t; + int i = 0; for ( AnnotationMirror am : ams ) { - SQLActionImpl a = new SQLActionImpl(); - populateAnnotationImpl( a, e, am); - _value [ i++ ] = a; + try + { + T a = _clazz.getDeclaredConstructor(DDRProcessorImpl.class) + .newInstance(DDRProcessorImpl.this); + populateAnnotationImpl( a, e, am); + _value [ i++ ] = a; + } + catch ( ReflectiveOperationException re ) + { + throw new RuntimeException( + "Incorrect implementation of annotation processor", re); + } } } } diff --git a/pljava-api/src/test/java/LexicalsTest.java b/pljava-api/src/test/java/LexicalsTest.java index ec58a2a4..e12ca967 100644 --- a/pljava-api/src/test/java/LexicalsTest.java +++ b/pljava-api/src/test/java/LexicalsTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; import static org.postgresql.pljava.sqlgen.Lexicals.ISO_AND_PG_IDENTIFIER_CAPTURING; From 19396c85e2c01b382a46438bd6b7f3e4f5174a5c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 22 Oct 2020 18:46:50 -0400 Subject: [PATCH 0791/1087] Generalize processSQLAction to processRepeatable Introduce a Repeatable subclass of AbstractAnnotationImpl that will carry references to its source Element and AnnotationMirror so they are available a subclass's characterize method, if any. --- .../annotation/processing/DDRProcessor.java | 100 +++++++++++------- 1 file changed, 61 insertions(+), 39 deletions(-) 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 a9c6e8ca..06597166 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 @@ -419,7 +419,6 @@ boolean process( Set tes, RoundEnvironment re) { boolean functionPresent = false; boolean sqlActionPresent = false; - boolean sqlActionsPresent = false; boolean baseUDTPresent = false; boolean mappedUDTPresent = false; @@ -429,10 +428,8 @@ boolean process( Set tes, RoundEnvironment re) { if ( AN_FUNCTION.equals( te) ) functionPresent = true; - else if ( AN_SQLACTION.equals( te) ) + else if ( AN_SQLACTION.equals( te) || AN_SQLACTIONS.equals( te) ) sqlActionPresent = true; - else if ( AN_SQLACTIONS.equals( te) ) - sqlActionsPresent = true; else if ( AN_BASEUDT.equals( te) ) baseUDTPresent = true; else if ( AN_MAPPEDUDT.equals( te) ) @@ -461,14 +458,12 @@ else if ( AN_SQLTYPE.equals( te) ) processFunction( e); if ( sqlActionPresent ) - for ( Element e : re.getElementsAnnotatedWith( AN_SQLACTION) ) - processSQLAction( e); - - if ( sqlActionsPresent ) - for ( Element e : re.getElementsAnnotatedWith( AN_SQLACTIONS) ) - processSQLActions( e); + for ( Element e + : re.getElementsAnnotatedWithAny( AN_SQLACTION, AN_SQLACTIONS) ) + processRepeatable( + e, AN_SQLACTION, AN_SQLACTIONS, SQLActionImpl.class); - tmpr.workAroundJava7Breakage(); // perhaps it will be fixed in Java 9? + tmpr.workAroundJava7Breakage(); // perhaps to be fixed in Java 9? nope. if ( ! re.processingOver() ) defensiveEarlyCharacterize(); @@ -739,35 +734,44 @@ Snippet[] order( } /** - * Process a single element annotated with @SQLAction. - */ - void processSQLAction( Element e) - { - SQLActionImpl sa = - getSnippet( e, SQLActionImpl.class, SQLActionImpl::new); - for ( AnnotationMirror am : elmu.getAllAnnotationMirrors( e) ) - { - if ( am.getAnnotationType().asElement().equals( AN_SQLACTION) ) - populateAnnotationImpl( sa, e, am); - } - } - - /** - * Process a single element annotated with @SQLActions (which simply takes - * an array of @SQLAction as a way to associate more than one SQLAction with - * a single program element).. + * Process an element carrying a repeatable annotation, the container + * of that repeatable annotation, or both. + *

    + * Snippets corresponding to repeatable annotations are not entered in the + * {@code snippets} map keyed by the target element, as that might not be + * unique. Each snippet is entered with a key made from its class and + * itself. They are not expected to be looked up, only processed when all of + * the map entries are enumerated. */ - void processSQLActions( Element e) + void processRepeatable( + Element e, TypeElement annot, TypeElement container, Class clazz) { for ( AnnotationMirror am : elmu.getAllAnnotationMirrors( e) ) { - if ( am.getAnnotationType().asElement().equals( AN_SQLACTIONS) ) + Element asElement = am.getAnnotationType().asElement(); + if ( asElement.equals( annot) ) + { + T snip; + try + { + snip = clazz.getDeclaredConstructor( DDRProcessorImpl.class, + Element.class, AnnotationMirror.class) + .newInstance( DDRProcessorImpl.this, e, am); + } + catch ( ReflectiveOperationException re ) + { + throw new RuntimeException( + "Incorrect implementation of annotation processor", re); + } + populateAnnotationImpl( snip, e, am); + putSnippet( snip, (Snippet)snip); + } + else if ( asElement.equals( container) ) { - Container sas = - new Container<>(SQLActionImpl.class); - populateAnnotationImpl( sas, e, am); - for ( SQLAction sa : sas.value() ) - putSnippet( sa, (Snippet)sa); + Container c = new Container<>(clazz); + populateAnnotationImpl( c, e, am); + for ( T snip : c.value() ) + putSnippet( snip, (Snippet)snip); } } } @@ -1099,6 +1103,18 @@ public Set requireTags() } } + class Repeatable extends AbstractAnnotationImpl + { + final Element m_targetElement; + final AnnotationMirror m_origin; + + Repeatable(Element e, AnnotationMirror am) + { + m_targetElement = e; + m_origin = am; + } + } + /** * Populate an AbstractAnnotationImpl-derived Annotation implementation * from the element-value pairs in an AnnotationMirror. For each element @@ -1266,7 +1282,7 @@ public void setName( Object o, boolean explicit, Element e) } } - class Container + class Container extends AbstractAnnotationImpl { public T[] value() { return _value; } @@ -1292,8 +1308,9 @@ public void setValue( Object o, boolean explicit, Element e) { try { - T a = _clazz.getDeclaredConstructor(DDRProcessorImpl.class) - .newInstance(DDRProcessorImpl.this); + T a = _clazz.getDeclaredConstructor(DDRProcessorImpl.class, + Element.class, AnnotationMirror.class) + .newInstance(DDRProcessorImpl.this, e, am); populateAnnotationImpl( a, e, am); _value [ i++ ] = a; } @@ -1307,9 +1324,14 @@ public void setValue( Object o, boolean explicit, Element e) } class SQLActionImpl - extends AbstractAnnotationImpl + extends Repeatable implements SQLAction, Snippet { + SQLActionImpl(Element e, AnnotationMirror am) + { + super(e, am); + } + public String[] install() { return _install; } public String[] remove() { return _remove; } public String[] provides() { return _provides; } From 6feac3ac4b284277bb4fd296c45cc19370cc7222 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 24 Oct 2020 12:28:58 -0400 Subject: [PATCH 0792/1087] Support @Cast in DDRProcessor In passing, give FunctionImpl's appendNameAndParams an extra boolean parameter to suppress parameter names, so it can be used generating CREATE CAST, where function parameter names aren't wanted. --- .../annotation/processing/DDRProcessor.java | 275 ++++++++++++++++-- 1 file changed, 257 insertions(+), 18 deletions(-) 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 06597166..07805d51 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 @@ -49,6 +49,7 @@ import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -104,6 +105,7 @@ import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.TriggerData; +import org.postgresql.pljava.annotation.Cast; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLActions; @@ -214,15 +216,19 @@ class DDRProcessorImpl // Our own annotations // final TypeElement AN_FUNCTION; - final TypeElement AN_SQLACTION; - final TypeElement AN_SQLACTIONS; final TypeElement AN_SQLTYPE; final TypeElement AN_TRIGGER; final TypeElement AN_BASEUDT; final TypeElement AN_MAPPEDUDT; + final TypeElement AN_SQLACTION; + final TypeElement AN_SQLACTIONS; + final TypeElement AN_CAST; + final TypeElement AN_CASTS; // Certain familiar DBTypes (capitalized as this file historically has) // + final DBType DT_BOOLEAN = new DBType.Reserved("boolean"); + final DBType DT_INTEGER = new DBType.Reserved("integer"); final DBType DT_RECORD = new DBType.Named( Identifier.Qualified.nameFromJava("pg_catalog.RECORD")); final DBType DT_TRIGGER = new DBType.Named( @@ -234,8 +240,7 @@ class DDRProcessorImpl // final DBType[] SIG_TYPMODIN = { DBType.fromSQLTypeAnnotation("pg_catalog.cstring[]") }; - final DBType[] SIG_TYPMODOUT = - { DBType.fromSQLTypeAnnotation("integer") }; + final DBType[] SIG_TYPMODOUT = { DT_INTEGER }; final DBType[] SIG_ANALYZE = { DBType.fromSQLTypeAnnotation("pg_catalog.internal") }; @@ -309,12 +314,18 @@ class DDRProcessorImpl TY_VOID = typu.getNoType( TypeKind.VOID); AN_FUNCTION = elmu.getTypeElement( Function.class.getName()); - AN_SQLACTION = elmu.getTypeElement( SQLAction.class.getName()); - AN_SQLACTIONS = elmu.getTypeElement( SQLActions.class.getName()); AN_SQLTYPE = elmu.getTypeElement( SQLType.class.getName()); AN_TRIGGER = elmu.getTypeElement( Trigger.class.getName()); AN_BASEUDT = elmu.getTypeElement( BaseUDT.class.getName()); AN_MAPPEDUDT = elmu.getTypeElement( MappedUDT.class.getName()); + + // Repeatable annotations and their containers. + // + AN_SQLACTION = elmu.getTypeElement( SQLAction.class.getName()); + AN_SQLACTIONS = elmu.getTypeElement( SQLActions.class.getName()); + AN_CAST = elmu.getTypeElement( Cast.class.getName()); + AN_CASTS = elmu.getTypeElement( + Cast.Container.class.getCanonicalName()); } void msg( Kind kind, String fmt, Object... args) @@ -377,7 +388,13 @@ public int hashCode() * one round), keyed by the object for which each snippet has been * generated. */ - Map snippets = new HashMap<>(); + /* + * This is a LinkedHashMap so that the order of handling annotation types + * in process() below will be preserved in calling their characterize() + * methods at end-of-round, and so, for example, characterize() on a Cast + * can use values set by characterize() on an associated Function. + */ + Map snippets = new LinkedHashMap<>(); S getSnippet(Object o, Class c, Supplier ctor) { @@ -421,6 +438,7 @@ boolean process( Set tes, RoundEnvironment re) boolean sqlActionPresent = false; boolean baseUDTPresent = false; boolean mappedUDTPresent = false; + boolean castPresent = false; boolean willClaim = true; @@ -428,14 +446,16 @@ boolean process( Set tes, RoundEnvironment re) { if ( AN_FUNCTION.equals( te) ) functionPresent = true; - else if ( AN_SQLACTION.equals( te) || AN_SQLACTIONS.equals( te) ) - sqlActionPresent = true; else if ( AN_BASEUDT.equals( te) ) baseUDTPresent = true; else if ( AN_MAPPEDUDT.equals( te) ) mappedUDTPresent = true; else if ( AN_SQLTYPE.equals( te) ) ; // these are handled within FunctionImpl + else if ( AN_SQLACTION.equals( te) || AN_SQLACTIONS.equals( te) ) + sqlActionPresent = true; + else if ( AN_CAST.equals( te) || AN_CASTS.equals( te) ) + castPresent = true; else { msg( Kind.WARNING, te, @@ -463,6 +483,12 @@ else if ( AN_SQLTYPE.equals( te) ) processRepeatable( e, AN_SQLACTION, AN_SQLACTIONS, SQLActionImpl.class); + if ( castPresent ) + for ( Element e + : re.getElementsAnnotatedWithAny( AN_CAST, AN_CASTS) ) + processRepeatable( + e, AN_CAST, AN_CASTS, CastImpl.class); + tmpr.workAroundJava7Breakage(); // perhaps to be fixed in Java 9? nope. if ( ! re.processingOver() ) @@ -1571,7 +1597,7 @@ public String[] deployStrings() if ( ! "".equals( _when) ) sb.append( "\n\tWHEN ").append( _when); sb.append( "\n\tEXECUTE PROCEDURE "); - func.appendNameAndParams( sb, false); + func.appendNameAndParams( sb, true, false); sb.setLength( sb.length() - 1); // drop closing ) s = _arguments.length; for ( String a : _arguments ) @@ -1959,15 +1985,15 @@ public void subsume() * * @param dflts Whether to include the defaults, if any. */ - void appendNameAndParams( StringBuilder sb, boolean dflts) + void appendNameAndParams(StringBuilder sb, boolean names, boolean dflts) { sb.append(qnameFrom(name(), schema())).append( '('); - appendParams( sb, dflts); + appendParams( sb, names, dflts); // TriggerImpl relies on ) being the very last character sb.append( ')'); } - void appendParams( StringBuilder sb, boolean dflts) + void appendParams( StringBuilder sb, boolean names, boolean dflts) { int count = parameterTypes.length; for ( ParameterInfo i @@ -1984,7 +2010,10 @@ void appendParams( StringBuilder sb, boolean dflts) if ( _variadic && 0 == count ) sb.append("VARIADIC "); - sb.append(name).append(' ').append(i.dt.toString(dflts)); + if ( names ) + sb.append(name).append(' '); + + sb.append(i.dt.toString(dflts)); if ( 0 < count ) sb.append(','); @@ -2009,7 +2038,7 @@ public String[] deployStrings() ArrayList al = new ArrayList<>(); StringBuilder sb = new StringBuilder(); sb.append( "CREATE OR REPLACE FUNCTION "); - appendNameAndParams( sb, true); + appendNameAndParams( sb, true, true); sb.append( "\n\tRETURNS "); if ( trigger ) sb.append( DT_TRIGGER.toString()); @@ -2047,7 +2076,7 @@ public String[] deployStrings() { sb.setLength( 0); sb.append( "COMMENT ON FUNCTION "); - appendNameAndParams( sb, false); + appendNameAndParams( sb, true, false); sb.append( "\nIS "); sb.append( DDRWriter.eQuote( comm)); al.add( sb.toString()); @@ -2072,7 +2101,7 @@ public String[] undeployStrings() StringBuilder sb = new StringBuilder(); sb.append( "DROP FUNCTION "); - appendNameAndParams( sb, false); + appendNameAndParams( sb, true, false); rslt [ rslt.length - 1 ] = sb.toString(); return rslt; } @@ -2181,7 +2210,7 @@ class BaseUDTFunctionImpl extends FunctionImpl BaseUDTFunctionID id; @Override - void appendParams( StringBuilder sb, boolean dflts) + void appendParams( StringBuilder sb, boolean names, boolean dflts) { sb.append( Arrays.stream(id.getParam( ui)) @@ -2743,6 +2772,216 @@ public Vertex breakCycle(Vertex v, boolean deploy) } } + class CastImpl + extends Repeatable + implements Cast, Snippet, Commentable + { + CastImpl(Element e, AnnotationMirror am) + { + super(e, am); + } + + public String from() { return _from; } + public String to() { return _to; } + public Cast.Path path() { return _path; } + public Cast.Application application() { return _application; } + public String[] provides() { return _provides; } + public String[] requires() { return _requires; } + + public String _from; + public String _to; + public Cast.Path _path; + public Cast.Application _application; + public String[] _provides; + public String[] _requires; + + FunctionImpl func; + DBType fromType; + DBType toType; + + public void setPath( Object o, boolean explicit, Element e) + { + if ( explicit ) + _path = Path.valueOf( + ((VariableElement)o).getSimpleName().toString()); + } + + public boolean characterize() + { + boolean ok = true; + + if ( ElementKind.METHOD.equals(m_targetElement.getKind()) ) + { + func = getSnippet(m_targetElement, FunctionImpl.class, + () -> (FunctionImpl)null); + if ( null == func ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "A method annotated with @Cast must also have @Function" + ); + ok = false; + } + } + + if ( null == func && "".equals(_from) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Cast not annotating a method must specify from=" + ); + ok = false; + } + + if ( null == func && "".equals(_to) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Cast not annotating a method must specify to=" + ); + ok = false; + } + + if ( null == func && null == _path ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Cast not annotating a method, and without path=, " + + "is not yet supported" + ); + ok = false; + } + + if ( ok ) + { + fromType = ("".equals(_from)) + ? func.parameterTypes[0] + : DBType.fromSQLTypeAnnotation(_from); + + toType = ("".equals(_to)) + ? func.returnType + : DBType.fromSQLTypeAnnotation(_to); + } + + if ( null != _path ) + { + if ( ok && Path.BINARY == _path && fromType.equals(toType) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "A cast with from and to types the same can only " + + "apply a type modifier; path=BINARY will have " + + "no effect"); + ok = false; + } + } + else if ( null != func ) + { + int nparams = func.parameterTypes.length; + + if ( ok && 2 > nparams && fromType.equals(toType) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "A cast with from and to types the same can only " + + "apply a type modifier, therefore must have at least " + + "two parameters"); + ok = false; + } + + if ( 1 > nparams || nparams > 3 ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "A cast function must have 1, 2, or 3 parameters"); + ok = false; + } + + if (1 < nparams && ! DT_INTEGER.equals(func.parameterTypes[1])) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Parameter 2 of a cast function must have integer type" + ); + ok = false; + } + + if (3 == nparams && ! DT_BOOLEAN.equals(func.parameterTypes[2])) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Parameter 3 of a cast function must have boolean type" + ); + ok = false; + } + } + + if ( ! ok ) + return false; + + recordImplicitTags(); + recordExplicitTags(_provides, _requires); + return true; + } + + void recordImplicitTags() + { + Set requires = requireTags(); + + DependTag dt = fromType.dependTag(); + if ( null != dt ) + requires.add(dt); + + dt = toType.dependTag(); + if ( null != dt ) + requires.add(dt); + + if ( null == _path ) + { + dt = func.provideTags().stream() + .filter(DependTag.Function.class::isInstance) + .findAny().get(); + requires.add(dt); + } + } + + public String[] deployStrings() + { + List al = new ArrayList<>(); + + StringBuilder sb = new StringBuilder(); + + sb.append("CREATE CAST (") + .append(fromType).append(" AS ").append(toType).append(")\n\t"); + + if ( Path.BINARY == _path ) + sb.append("WITHOUT FUNCTION"); + else if ( Path.INOUT == _path ) + sb.append("WITH INOUT"); + else + { + sb.append("WITH FUNCTION "); + func.appendNameAndParams(sb, false, false); + } + + switch ( _application ) + { + case ASSIGNMENT: sb.append("\n\tAS ASSIGNMENT"); break; + case EXPLICIT: break; + case IMPLICIT: sb.append("\n\tAS IMPLICIT"); + } + + al.add(sb.toString()); + + if ( null != comment() ) + al.add( + "COMMENT ON CAST (" + + fromType + " AS " + toType + ") IS " + + DDRWriter.eQuote(comment())); + + return al.toArray( new String [ al.size() ]); + } + + public String[] undeployStrings() + { + return new String[] + { + "DROP CAST (" + fromType + " AS " + toType + ")" + }; + } + } + /** * Provides the default mappings from Java types to SQL types. */ From ef3796febcd43180fd22bf14a5f854070769a48b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 24 Oct 2020 12:46:24 -0400 Subject: [PATCH 0793/1087] Adapt an example to use @Cast In passing, remove its postgresql_ge_80300 conditionals. The exact oldest PostgreSQL version supportable by PL/Java 1.6 might still be slightly negotiable, but definitely won't be that far back. --- .../pljava/example/annotation/IntWithMod.java | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java index c2821561..a31928a6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java @@ -21,6 +21,7 @@ import java.sql.SQLOutput; import java.sql.Statement; +import org.postgresql.pljava.annotation.Cast; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; @@ -51,32 +52,13 @@ *

    * Of course this example more or less duplicates what you could do in two lines * with CREATE DOMAIN. But it is enough to illustrate the process. - *

    - * Certainly, it would be less tedious with some more annotation support and - * autogeneration of the ordering dependencies that are now added by hand here. - *

    - * Most of this must be suppressed (using conditional implementor tags) if the - * PostgreSQL instance is older than 8.3, because it won't have the cstring[] - * type, so the typeModifierInput function can't be declared, and so neither - * can the type, or functions that accept or return it. See the - * {@link ConditionalDDR} example for where the implementor tag is set up. */ -@SQLAction(requires={"IntWithMod type", "IntWithMod modApply"}, - implementor="postgresql_ge_80300", - remove="DROP CAST (javatest.IntWithMod AS javatest.IntWithMod)", +@SQLAction(requires="IntWithMod modCast", install={ - "CREATE CAST (javatest.IntWithMod AS javatest.IntWithMod)" + - " WITH FUNCTION javatest.intwithmod_typmodapply(" + - " javatest.IntWithMod, integer, boolean)", - - "COMMENT ON CAST (javatest.IntWithMod AS javatest.IntWithMod) IS '" + - "Cast that applies/verifies the type modifier on an IntWithMod.'", - "SELECT CAST('42' AS javatest.IntWithMod(even))" } ) @BaseUDT(schema="javatest", provides="IntWithMod type", - implementor="postgresql_ge_80300", typeModifierInput="javatest.intwithmod_typmodin", typeModifierOutput="javatest.intwithmod_typmodout", like="pg_catalog.int4") @@ -146,7 +128,6 @@ public void writeSQL(SQLOutput stream) throws SQLException { * "even" or "odd". The modifier value is 0 for even or 1 for odd. */ @Function(schema="javatest", name="intwithmod_typmodin", - implementor="postgresql_ge_80300", effects=IMMUTABLE, onNullInput=RETURNS_NULL) public static int modIn(@SQLType("pg_catalog.cstring[]") String[] toks) throws SQLException { @@ -180,9 +161,10 @@ public static String modOut(int mod) throws SQLException { * Function backing the type-modifier application cast for IntWithMod type. */ @Function(schema="javatest", name="intwithmod_typmodapply", - implementor="postgresql_ge_80300", - provides="IntWithMod modApply", effects=IMMUTABLE, onNullInput=RETURNS_NULL) + @Cast(comment= + "Cast that applies/verifies the type modifier on an IntWithMod.", + provides="IntWithMod modCast") public static IntWithMod modApply(IntWithMod iwm, int mod, boolean explicit) throws SQLException { From d56f7153c7b92d1a2d339ce6a74b4a1bf326f24a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 24 Oct 2020 13:09:34 -0400 Subject: [PATCH 0794/1087] Have CI also test undeploy actions Routine tests should include sqlj.remove_jar with undeploy=true, as those actions also come from the SQL generator. --- .travis.yml | 16 ++++++++++++++++ appveyor.yml | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/.travis.yml b/.travis.yml index ab996476..c6403a36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -250,6 +250,22 @@ script: | // state 9: must be end of input (o,p,q) -> null == o ); + + /* + * 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 + ); } } catch ( Throwable t ) { diff --git a/appveyor.yml b/appveyor.yml index 8fab1166..ca13103d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -188,6 +188,22 @@ test_script: // state 9: must be end of input (o,p,q) -> null == o ); + + /* + * 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 + ); } } catch ( Throwable t ) { From a0efb99ad1d4237251b610b41c06df640d323f7d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 24 Oct 2020 14:36:36 -0400 Subject: [PATCH 0795/1087] Examples no longer need SQLActions The SQLAction annotation itself can now simply be repeated. --- .../example/annotation/ConditionalDDR.java | 22 +++++++++---------- .../example/annotation/Enumeration.java | 6 ++--- .../pljava/example/annotation/JDBC42_21.java | 2 -- .../pljava/example/annotation/PassXML.java | 12 +++++----- .../pljava/example/annotation/PreJSR310.java | 6 ++--- .../annotation/RecordParameterDefaults.java | 4 +--- .../pljava/example/annotation/SPIActions.java | 4 +--- .../pljava/example/annotation/Triggers.java | 11 +++++----- .../annotation/UnicodeRoundTripTest.java | 8 +++---- .../example/annotation/XMLRenderedTypes.java | 4 +--- 10 files changed, 30 insertions(+), 49 deletions(-) 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 ac26d8a9..5cd9351a 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- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2020 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 @@ -68,7 +68,6 @@ * several statements setting PostgreSQL-version-based implementor tags that * are relied on by various other examples in this directory. */ -@SQLActions({ @SQLAction(provides={"LifeIsGood","LifeIsNotGood"}, install= "SELECT CASE 42 WHEN 42 THEN " + " set_config('pljava.implementors', 'LifeIsGood,' || " + @@ -77,15 +76,15 @@ " set_config('pljava.implementors', 'LifeIsNotGood,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(implementor="LifeIsGood", install= "SELECT javatest.logmessage('INFO', 'Looking good!')" - ), + ) @SQLAction(implementor="LifeIsNotGood", install= "SELECT javatest.logmessage('WARNING', 'This should not be executed')" - ), + ) @SQLAction(provides="postgresql_ge_80300", install= "SELECT CASE WHEN" + @@ -93,7 +92,7 @@ " THEN set_config('pljava.implementors', 'postgresql_ge_80300,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(provides="postgresql_ge_80400", install= "SELECT CASE WHEN" + @@ -101,7 +100,7 @@ " THEN set_config('pljava.implementors', 'postgresql_ge_80400,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(provides="postgresql_ge_90000", install= "SELECT CASE WHEN" + @@ -109,7 +108,7 @@ " THEN set_config('pljava.implementors', 'postgresql_ge_90000,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(provides="postgresql_ge_90100", install= "SELECT CASE WHEN" + @@ -117,7 +116,7 @@ " THEN set_config('pljava.implementors', 'postgresql_ge_90100,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(provides="postgresql_ge_90300", install= "SELECT CASE WHEN" + @@ -125,7 +124,7 @@ " THEN set_config('pljava.implementors', 'postgresql_ge_90300,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(provides="postgresql_ge_100000", install= "SELECT CASE WHEN" + @@ -133,6 +132,5 @@ " THEN set_config('pljava.implementors', 'postgresql_ge_100000,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), -}) + ) public class ConditionalDDR { } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java index 29805053..4666140d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -27,11 +27,10 @@ * version, set up in the {@link ConditionalDDR} example. PostgreSQL before 8.3 * did not have enum types. */ -@SQLActions({ @SQLAction(provides="mood type", implementor="postgresql_ge_80300", install="CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", remove="DROP TYPE mood" - ), + ) @SQLAction(implementor="postgresql_ge_80300", requires={"textToMood", "moodToText", "textsToMoods", "moodsToTexts"}, install={ @@ -41,7 +40,6 @@ "SELECT moodsToTexts(array['happy','happy','sad','ok']::mood[])" } ) -}) public class Enumeration { @Function(requires="mood type", provides="textToMood", type="mood", 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 76872617..c2e34bf6 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 @@ -27,7 +27,6 @@ * Relies on PostgreSQL-version-specific implementor tags set up in the * {@link ConditionalDDR} example. */ -@SQLActions({ @SQLAction( implementor="postgresql_ge_90300",requires="TypeRoundTripper.roundTrip", install={ @@ -123,7 +122,6 @@ " javatest.roundtrip(p, 'java.time.OffsetTime', true)" + " AS r(roundtripped timetz)" }) -}) public class JDBC42_21 { /** 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 e5614933..66f13b1b 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 @@ -105,13 +105,12 @@ * Everything mentioning the type XML here needs a conditional implementor tag * in case of being loaded into a PostgreSQL instance built without that type. */ -@SQLActions({ @SQLAction(provides="postgresql_xml", install= "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + " THEN set_config('pljava.implementors', 'postgresql_xml,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(implementor="postgresql_ge_80400", provides="postgresql_xml_ge84", @@ -120,7 +119,7 @@ " THEN set_config('pljava.implementors', 'postgresql_xml_ge84,' || " + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(implementor="postgresql_xml_ge84", requires="echoXMLParameter", install= @@ -145,7 +144,7 @@ " END " + "FROM" + " r" - ), + ) @SQLAction(implementor="postgresql_xml_ge84", requires="proxiedXMLEcho", install= @@ -169,7 +168,7 @@ " END " + "FROM" + " r" - ), + ) @SQLAction(implementor="postgresql_xml_ge84", requires="lowLevelXMLEcho", install={ @@ -211,7 +210,7 @@ "FROM" + " r" } - ), + ) @SQLAction(implementor="postgresql_xml", requires={"prepareXMLTransform", "transformXML"}, @@ -250,7 +249,6 @@ " END" } ) -}) @MappedUDT(schema="javatest", name="onexml", structure="c1 xml", implementor="postgresql_xml", comment="A composite type mapped by the PassXML example class") diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java index b19f5ce4..9572eaeb 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018- Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2020 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 @@ -34,18 +34,16 @@ * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example. */ -@SQLActions({ @SQLAction(provides="language java_tzset", install={ "SELECT sqlj.alias_java_language('java_tzset', true)" }, remove={ "DROP LANGUAGE java_tzset" - }), + }) @SQLAction(implementor="postgresql_ge_90300", // needs LATERAL requires="issue199", install={ "SELECT javatest.issue199()" }) -}) public class PreJSR310 { private static final String TZPRAGUE = "Europe/Prague"; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index d4f240ce..52b86e65 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018- Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2020 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 @@ -33,7 +33,6 @@ * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example. */ -@SQLActions({ @SQLAction( provides = "paramtypeinfo type", // created in Triggers.java install = { @@ -45,7 +44,6 @@ "DROP TYPE javatest.paramtypeinfo" } ) -}) public class RecordParameterDefaults implements ResultSetProvider { /** 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 f2606c91..0982619b 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 @@ -38,7 +38,6 @@ * * @author Thomas Hallgren */ -@SQLActions({ @SQLAction(provides = "employees tables", install = { "CREATE TABLE javatest.employees1" + " (" + @@ -59,9 +58,8 @@ "DROP TABLE javatest.employees2", "DROP TABLE javatest.employees1" } - ), + ) @SQLAction(requires = "issue228", install = "SELECT javatest.issue228()") -}) public class SPIActions { private static final String SP_CHECKSTATE = "sp.checkState"; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index f950a602..a5119f2f 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -9,6 +9,7 @@ * Contributors: * Tada AB * Purdue University + * Chapman Flack */ package org.postgresql.pljava.example.annotation; @@ -41,7 +42,6 @@ * version, set up in the {@link ConditionalDDR} example. Constraint triggers * appear in PG 9.1, transition tables in PG 10. */ -@SQLActions({ @SQLAction( provides = "foobar tables", install = { @@ -52,16 +52,16 @@ "DROP TABLE javatest.foobar_2", "DROP TABLE javatest.foobar_1" } - ), + ) @SQLAction( requires = "constraint triggers", install = "INSERT INTO javatest.foobar_2(value) VALUES (45)" - ), + ) @SQLAction( requires = "foobar triggers", provides = "foobar2_42", install = "INSERT INTO javatest.foobar_2(value) VALUES (42)" - ), + ) @SQLAction( requires = { "transition triggers", "foobar2_42" }, install = "UPDATE javatest.foobar_2 SET value = 43 WHERE value = 42" @@ -75,7 +75,6 @@ * A proper test for it will have to wait for a proper testing harness * invoking tests from outside PL/Java itself. */ -}) public class Triggers { /** diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 568d6401..1195bb18 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2020 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 @@ -39,7 +39,6 @@ * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example, and also sets its own. */ -@SQLActions({ @SQLAction(provides="postgresql_unicodetest", implementor="postgresql_ge_90000", install= "SELECT CASE" + @@ -47,7 +46,7 @@ " THEN set_config('pljava.implementors', 'postgresql_unicodetest,' ||" + " current_setting('pljava.implementors'), true) " + "END" - ), + ) @SQLAction(requires="unicodetest fn", implementor="postgresql_unicodetest", install= @@ -88,7 +87,7 @@ " 'all Unicode codepoint ranges roundtripped successfully.') " + " end " + " from test_summary" - ), + ) @SQLAction( install= "CREATE TYPE unicodetestrow AS " + @@ -96,7 +95,6 @@ remove="DROP TYPE unicodetestrow", provides="unicodetestrow type" ) -}) public class UnicodeRoundTripTest { /** * This function takes a string and an array of ints constructed in PG, diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java index ca7f501d..a8794ad8 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2020 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 @@ -30,7 +30,6 @@ * in case of being loaded into a PostgreSQL instance built without that type. * The {@code pg_node_tree} type appears in 9.1. */ -@SQLActions({ @SQLAction(implementor="postgresql_ge_90100", provides="postgresql_xml_ge91", install= @@ -39,7 +38,6 @@ " current_setting('pljava.implementors'), true) " + "END" ) -}) public class XMLRenderedTypes { @Function(schema="javatest", implementor="postgresql_xml_ge91") From ee0d156b463d65096329c7765c8e883b5cb24316 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 24 Oct 2020 14:50:56 -0400 Subject: [PATCH 0796/1087] Whitespace only Drop the extra indentation left behind now that @SQLAction needn't be nested in @SQLActions. --- .../example/annotation/ConditionalDDR.java | 114 +++---- .../example/annotation/Enumeration.java | 26 +- .../pljava/example/annotation/JDBC42_21.java | 180 +++++------ .../pljava/example/annotation/PassXML.java | 284 +++++++++--------- .../pljava/example/annotation/PreJSR310.java | 18 +- .../annotation/RecordParameterDefaults.java | 22 +- .../pljava/example/annotation/SPIActions.java | 44 +-- .../pljava/example/annotation/Triggers.java | 66 ++-- .../annotation/UnicodeRoundTripTest.java | 38 +-- .../example/annotation/XMLRenderedTypes.java | 16 +- 10 files changed, 404 insertions(+), 404 deletions(-) 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 5cd9351a..c26d6db6 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 @@ -68,69 +68,69 @@ * several statements setting PostgreSQL-version-based implementor tags that * are relied on by various other examples in this directory. */ - @SQLAction(provides={"LifeIsGood","LifeIsNotGood"}, install= - "SELECT CASE 42 WHEN 42 THEN " + - " set_config('pljava.implementors', 'LifeIsGood,' || " + - " current_setting('pljava.implementors'), true) " + - "ELSE " + - " set_config('pljava.implementors', 'LifeIsNotGood,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(provides={"LifeIsGood","LifeIsNotGood"}, install= + "SELECT CASE 42 WHEN 42 THEN " + + " set_config('pljava.implementors', 'LifeIsGood,' || " + + " current_setting('pljava.implementors'), true) " + + "ELSE " + + " set_config('pljava.implementors', 'LifeIsNotGood,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) - @SQLAction(implementor="LifeIsGood", install= - "SELECT javatest.logmessage('INFO', 'Looking good!')" - ) +@SQLAction(implementor="LifeIsGood", install= + "SELECT javatest.logmessage('INFO', 'Looking good!')" +) - @SQLAction(implementor="LifeIsNotGood", install= - "SELECT javatest.logmessage('WARNING', 'This should not be executed')" - ) +@SQLAction(implementor="LifeIsNotGood", install= + "SELECT javatest.logmessage('WARNING', 'This should not be executed')" +) - @SQLAction(provides="postgresql_ge_80300", install= - "SELECT CASE WHEN" + - " 80300 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_80300,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(provides="postgresql_ge_80300", install= + "SELECT CASE WHEN" + + " 80300 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_80300,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) - @SQLAction(provides="postgresql_ge_80400", install= - "SELECT CASE WHEN" + - " 80400 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_80400,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(provides="postgresql_ge_80400", install= + "SELECT CASE WHEN" + + " 80400 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_80400,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) - @SQLAction(provides="postgresql_ge_90000", install= - "SELECT CASE WHEN" + - " 90000 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90000,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(provides="postgresql_ge_90000", install= + "SELECT CASE WHEN" + + " 90000 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_90000,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) - @SQLAction(provides="postgresql_ge_90100", install= - "SELECT CASE WHEN" + - " 90100 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90100,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(provides="postgresql_ge_90100", install= + "SELECT CASE WHEN" + + " 90100 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_90100,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) - @SQLAction(provides="postgresql_ge_90300", install= - "SELECT CASE WHEN" + - " 90300 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90300,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(provides="postgresql_ge_90300", install= + "SELECT CASE WHEN" + + " 90300 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_90300,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) - @SQLAction(provides="postgresql_ge_100000", install= - "SELECT CASE WHEN" + - " 100000 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_100000,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(provides="postgresql_ge_100000", install= + "SELECT CASE WHEN" + + " 100000 <= CAST(current_setting('server_version_num') AS integer)" + + " THEN set_config('pljava.implementors', 'postgresql_ge_100000,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) public class ConditionalDDR { } diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java index 4666140d..0a999fe6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java @@ -27,19 +27,19 @@ * version, set up in the {@link ConditionalDDR} example. PostgreSQL before 8.3 * did not have enum types. */ - @SQLAction(provides="mood type", implementor="postgresql_ge_80300", - install="CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", - remove="DROP TYPE mood" - ) - @SQLAction(implementor="postgresql_ge_80300", - requires={"textToMood", "moodToText", "textsToMoods", "moodsToTexts"}, - install={ - "SELECT textToMood('happy')", - "SELECT moodToText('happy'::mood)", - "SELECT textsToMoods(array['happy','happy','sad','ok'])", - "SELECT moodsToTexts(array['happy','happy','sad','ok']::mood[])" - } - ) +@SQLAction(provides="mood type", implementor="postgresql_ge_80300", + install="CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", + remove="DROP TYPE mood" +) +@SQLAction(implementor="postgresql_ge_80300", + requires={"textToMood", "moodToText", "textsToMoods", "moodsToTexts"}, + install={ + "SELECT textToMood('happy')", + "SELECT moodToText('happy'::mood)", + "SELECT textsToMoods(array['happy','happy','sad','ok'])", + "SELECT moodsToTexts(array['happy','happy','sad','ok']::mood[])" + } +) public class Enumeration { @Function(requires="mood type", provides="textToMood", type="mood", 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 c2e34bf6..1cb3f7fd 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 @@ -27,101 +27,101 @@ * Relies on PostgreSQL-version-specific implementor tags set up in the * {@link ConditionalDDR} example. */ - @SQLAction( - implementor="postgresql_ge_90300",requires="TypeRoundTripper.roundTrip", - install={ - " SELECT" + - " CASE WHEN every(orig = roundtripped)" + - " THEN javatest.logmessage('INFO', 'java.time.LocalDate passes')" + - " ELSE javatest.logmessage('WARNING', 'java.time.LocalDate fails')" + - " END" + - " FROM" + - " (VALUES" + - " (date '2017-08-21')," + - " (date '1970-03-07')," + - " (date '1919-05-29')" + - " ) AS p(orig)," + - " javatest.roundtrip(p, 'java.time.LocalDate')" + - " AS r(roundtripped date)", +@SQLAction( + implementor="postgresql_ge_90300",requires="TypeRoundTripper.roundTrip", + install={ + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.LocalDate passes')" + + " ELSE javatest.logmessage('WARNING', 'java.time.LocalDate fails')" + + " END" + + " FROM" + + " (VALUES" + + " (date '2017-08-21')," + + " (date '1970-03-07')," + + " (date '1919-05-29')" + + " ) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.LocalDate')" + + " AS r(roundtripped date)", - " SELECT" + - " CASE WHEN every(orig = roundtripped)" + - " THEN javatest.logmessage('INFO', 'java.time.LocalTime passes')" + - " ELSE javatest.logmessage('WARNING', 'java.time.LocalTime fails')" + - " END" + - " FROM" + - " (SELECT current_time::time) AS p(orig)," + - " javatest.roundtrip(p, 'java.time.LocalTime')" + - " AS r(roundtripped time)", + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.LocalTime passes')" + + " ELSE javatest.logmessage('WARNING', 'java.time.LocalTime fails')" + + " END" + + " FROM" + + " (SELECT current_time::time) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.LocalTime')" + + " AS r(roundtripped time)", - " SELECT" + - " CASE WHEN every(orig = roundtripped)" + - " THEN javatest.logmessage('INFO', 'java.time.OffsetTime passes')" + - " ELSE javatest.logmessage('WARNING', 'java.time.OffsetTime fails')" + - " END" + - " FROM" + - " (SELECT current_time::timetz) AS p(orig)," + - " javatest.roundtrip(p, 'java.time.OffsetTime')" + - " AS r(roundtripped timetz)", + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.OffsetTime passes')" + + " ELSE javatest.logmessage('WARNING', 'java.time.OffsetTime fails')" + + " END" + + " FROM" + + " (SELECT current_time::timetz) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.OffsetTime')" + + " AS r(roundtripped timetz)", - " SELECT" + - " CASE WHEN every(orig = roundtripped)" + - " THEN javatest.logmessage('INFO', 'java.time.LocalDateTime passes')" + - " ELSE javatest.logmessage('WARNING','java.time.LocalDateTime fails')"+ - " END" + - " FROM" + - " (SELECT 'on' = current_setting('integer_datetimes')) AS ck(idt)," + - " LATERAL (" + - " SELECT" + - " value" + - " FROM" + - " (VALUES" + - " (true, timestamp '2017-08-21 18:25:29.900005')," + - " (true, timestamp '1970-03-07 17:37:49.300009')," + - " (true, timestamp '1919-05-29 13:08:33.600001')," + - " (idt, timestamp 'infinity')," + - " (idt, timestamp '-infinity')" + - " ) AS vs(cond, value)" + - " WHERE cond" + - " ) AS p(orig)," + - " javatest.roundtrip(p, 'java.time.LocalDateTime')" + - " AS r(roundtripped timestamp)", + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.LocalDateTime passes')" + + " ELSE javatest.logmessage('WARNING','java.time.LocalDateTime fails')"+ + " END" + + " FROM" + + " (SELECT 'on' = current_setting('integer_datetimes')) AS ck(idt)," + + " LATERAL (" + + " SELECT" + + " value" + + " FROM" + + " (VALUES" + + " (true, timestamp '2017-08-21 18:25:29.900005')," + + " (true, timestamp '1970-03-07 17:37:49.300009')," + + " (true, timestamp '1919-05-29 13:08:33.600001')," + + " (idt, timestamp 'infinity')," + + " (idt, timestamp '-infinity')" + + " ) AS vs(cond, value)" + + " WHERE cond" + + " ) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.LocalDateTime')" + + " AS r(roundtripped timestamp)", - " SELECT" + - " CASE WHEN every(orig = roundtripped)" + - " THEN javatest.logmessage('INFO', 'java.time.OffsetDateTime passes')"+ - " ELSE javatest.logmessage(" + - " 'WARNING','java.time.OffsetDateTime fails')"+ - " END" + - " FROM" + - " (SELECT 'on' = current_setting('integer_datetimes')) AS ck(idt)," + - " LATERAL (" + - " SELECT" + - " value" + - " FROM" + - " (VALUES" + - " (true, timestamptz '2017-08-21 18:25:29.900005Z')," + - " (true, timestamptz '1970-03-07 17:37:49.300009Z')," + - " (true, timestamptz '1919-05-29 13:08:33.600001Z')," + - " (idt, timestamptz 'infinity')," + - " (idt, timestamptz '-infinity')" + - " ) AS vs(cond, value)" + - " WHERE cond" + - " ) AS p(orig)," + - " javatest.roundtrip(p, 'java.time.OffsetDateTime')" + - " AS r(roundtripped timestamptz)", + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'java.time.OffsetDateTime passes')"+ + " ELSE javatest.logmessage(" + + " 'WARNING','java.time.OffsetDateTime fails')"+ + " END" + + " FROM" + + " (SELECT 'on' = current_setting('integer_datetimes')) AS ck(idt)," + + " LATERAL (" + + " SELECT" + + " value" + + " FROM" + + " (VALUES" + + " (true, timestamptz '2017-08-21 18:25:29.900005Z')," + + " (true, timestamptz '1970-03-07 17:37:49.300009Z')," + + " (true, timestamptz '1919-05-29 13:08:33.600001Z')," + + " (idt, timestamptz 'infinity')," + + " (idt, timestamptz '-infinity')" + + " ) AS vs(cond, value)" + + " WHERE cond" + + " ) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.OffsetDateTime')" + + " AS r(roundtripped timestamptz)", - " SELECT" + - " CASE WHEN every(orig = roundtripped)" + - " THEN javatest.logmessage('INFO', 'OffsetTime as stmt param passes')"+ - " ELSE javatest.logmessage(" + - " 'WARNING','java.time.OffsetTime as stmt param fails')"+ - " END" + - " FROM" + - " (SELECT current_time::timetz) AS p(orig)," + - " javatest.roundtrip(p, 'java.time.OffsetTime', true)" + - " AS r(roundtripped timetz)" - }) + " SELECT" + + " CASE WHEN every(orig = roundtripped)" + + " THEN javatest.logmessage('INFO', 'OffsetTime as stmt param passes')"+ + " ELSE javatest.logmessage(" + + " 'WARNING','java.time.OffsetTime as stmt param fails')"+ + " END" + + " FROM" + + " (SELECT current_time::timetz) AS p(orig)," + + " javatest.roundtrip(p, 'java.time.OffsetTime', true)" + + " AS r(roundtripped timetz)" +}) public class JDBC42_21 { /** 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 66f13b1b..81f8695f 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 @@ -105,150 +105,150 @@ * Everything mentioning the type XML here needs a conditional implementor tag * in case of being loaded into a PostgreSQL instance built without that type. */ - @SQLAction(provides="postgresql_xml", install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) - - @SQLAction(implementor="postgresql_ge_80400", - provides="postgresql_xml_ge84", - install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml_ge84,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) - - @SQLAction(implementor="postgresql_xml_ge84", requires="echoXMLParameter", - install= - "WITH" + - " s(how) AS (SELECT generate_series(1, 7))," + - " t(x) AS (" + - " SELECT table_to_xml('pg_catalog.pg_operator', true, false, '')" + - " )," + - " r(howin, howout, isdoc) AS (" + - " SELECT" + - " i.how, o.how," + - " javatest.echoxmlparameter(x, i.how, o.how) IS DOCUMENT" + - " FROM" + - " t, s AS i, s AS o" + - " WHERE" + - " NOT (i.how = 6 and o.how = 7)" + // 6->7 unreliable in some JREs - " ) " + - "SELECT" + - " CASE WHEN every(isdoc)" + - " THEN javatest.logmessage('INFO', 'SQLXML echos succeeded')" + - " ELSE javatest.logmessage('WARNING', 'SQLXML echos had problems')" + - " END " + - "FROM" + - " r" - ) - - @SQLAction(implementor="postgresql_xml_ge84", requires="proxiedXMLEcho", - install= - "WITH" + - " s(how) AS (SELECT unnest('{1,2,4,5,6,7}'::int[]))," + - " t(x) AS (" + - " SELECT table_to_xml('pg_catalog.pg_operator', true, false, '')" + - " )," + - " r(how, isdoc) AS (" + - " SELECT" + - " how," + - " javatest.proxiedxmlecho(x, how) IS DOCUMENT" + - " FROM" + - " t, s" + - " )" + - "SELECT" + - " CASE WHEN every(isdoc)" + - " THEN javatest.logmessage('INFO', 'proxied SQLXML echos succeeded')" + - " ELSE javatest.logmessage('WARNING'," + - " 'proxied SQLXML echos had problems')" + - " END " + - "FROM" + - " r" - ) - - @SQLAction(implementor="postgresql_xml_ge84", requires="lowLevelXMLEcho", - install={ +@SQLAction(provides="postgresql_xml", install= + "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + + " THEN set_config('pljava.implementors', 'postgresql_xml,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) + +@SQLAction(implementor="postgresql_ge_80400", + provides="postgresql_xml_ge84", + install= + "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + + " THEN set_config('pljava.implementors', 'postgresql_xml_ge84,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) + +@SQLAction(implementor="postgresql_xml_ge84", requires="echoXMLParameter", + install= + "WITH" + + " s(how) AS (SELECT generate_series(1, 7))," + + " t(x) AS (" + + " SELECT table_to_xml('pg_catalog.pg_operator', true, false, '')" + + " )," + + " r(howin, howout, isdoc) AS (" + + " SELECT" + + " i.how, o.how," + + " javatest.echoxmlparameter(x, i.how, o.how) IS DOCUMENT" + + " FROM" + + " t, s AS i, s AS o" + + " WHERE" + + " NOT (i.how = 6 and o.how = 7)" + // 6->7 unreliable in some JREs + " ) " + + "SELECT" + + " CASE WHEN every(isdoc)" + + " THEN javatest.logmessage('INFO', 'SQLXML echos succeeded')" + + " ELSE javatest.logmessage('WARNING', 'SQLXML echos had problems')" + + " END " + + "FROM" + + " r" +) + +@SQLAction(implementor="postgresql_xml_ge84", requires="proxiedXMLEcho", + install= + "WITH" + + " s(how) AS (SELECT unnest('{1,2,4,5,6,7}'::int[]))," + + " t(x) AS (" + + " SELECT table_to_xml('pg_catalog.pg_operator', true, false, '')" + + " )," + + " r(how, isdoc) AS (" + + " SELECT" + + " how," + + " javatest.proxiedxmlecho(x, how) IS DOCUMENT" + + " FROM" + + " t, s" + + " )" + + "SELECT" + + " CASE WHEN every(isdoc)" + + " THEN javatest.logmessage('INFO', 'proxied SQLXML echos succeeded')" + + " ELSE javatest.logmessage('WARNING'," + + " 'proxied SQLXML echos had problems')" + + " END " + + "FROM" + + " r" +) + +@SQLAction(implementor="postgresql_xml_ge84", requires="lowLevelXMLEcho", + install={ + "SELECT" + + " preparexmlschema('schematest', $$" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "" + + "$$, 'http://www.w3.org/2001/XMLSchema', 5)", + + "WITH" + + " s(how) AS (SELECT unnest('{4,5,7}'::int[]))," + + " r(isdoc) AS (" + + " SELECT" + + " javatest.lowlevelxmlecho(" + + " query_to_xml(" + + " 'SELECT ''hi'' AS textcol, 1 AS intcol', true, true, 'urn:testme'"+ + " ), how, params) IS DOCUMENT" + + " FROM" + + " s," + + " (SELECT 'schematest' AS schema) AS params" + + " )" + + "SELECT" + + " CASE WHEN every(isdoc)" + + " THEN javatest.logmessage('INFO', 'XML Schema tests succeeded')" + + " ELSE javatest.logmessage('WARNING'," + + " 'XML Schema tests had problems')" + + " END " + + "FROM" + + " r" + } +) + +@SQLAction(implementor="postgresql_xml", + requires={"prepareXMLTransform", "transformXML"}, + install={ "SELECT" + - " preparexmlschema('schematest', $$" + - "" + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - "" + - "$$, 'http://www.w3.org/2001/XMLSchema', 5)", - - "WITH" + - " s(how) AS (SELECT unnest('{4,5,7}'::int[]))," + - " r(isdoc) AS (" + - " SELECT" + - " javatest.lowlevelxmlecho(" + - " query_to_xml(" + - " 'SELECT ''hi'' AS textcol, 1 AS intcol', true, true, 'urn:testme'"+ - " ), how, params) IS DOCUMENT" + - " FROM" + - " s," + - " (SELECT 'schematest' AS schema) AS params" + - " )" + + " javatest.prepareXMLTransform('distinctElementNames'," + + "'" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "', 5, true)", + "SELECT" + - " CASE WHEN every(isdoc)" + - " THEN javatest.logmessage('INFO', 'XML Schema tests succeeded')" + - " ELSE javatest.logmessage('WARNING'," + - " 'XML Schema tests had problems')" + - " END " + - "FROM" + - " r" - } - ) - - @SQLAction(implementor="postgresql_xml", - requires={"prepareXMLTransform", "transformXML"}, - install={ - "SELECT" + - " javatest.prepareXMLTransform('distinctElementNames'," + - "'" + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - " " + - "', 5, true)", - - "SELECT" + - " CASE WHEN" + - " javatest.transformXML('distinctElementNames'," + - " '', 5, 5)::text" + - " =" + - " 'abcde'"+ - " THEN javatest.logmessage('INFO', 'XSLT 1.0 test succeeded')" + - " ELSE javatest.logmessage('WARNING', 'XSLT 1.0 test failed')" + - " END" - } - ) + " CASE WHEN" + + " javatest.transformXML('distinctElementNames'," + + " '', 5, 5)::text" + + " =" + + " 'abcde'"+ + " THEN javatest.logmessage('INFO', 'XSLT 1.0 test succeeded')" + + " ELSE javatest.logmessage('WARNING', 'XSLT 1.0 test failed')" + + " END" + } +) @MappedUDT(schema="javatest", name="onexml", structure="c1 xml", implementor="postgresql_xml", comment="A composite type mapped by the PassXML example class") diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java index 9572eaeb..4332860f 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java @@ -34,16 +34,16 @@ * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example. */ - @SQLAction(provides="language java_tzset", install={ - "SELECT sqlj.alias_java_language('java_tzset', true)" - }, remove={ - "DROP LANGUAGE java_tzset" - }) +@SQLAction(provides="language java_tzset", install={ + "SELECT sqlj.alias_java_language('java_tzset', true)" +}, remove={ + "DROP LANGUAGE java_tzset" +}) - @SQLAction(implementor="postgresql_ge_90300", // needs LATERAL - requires="issue199", install={ - "SELECT javatest.issue199()" - }) +@SQLAction(implementor="postgresql_ge_90300", // needs LATERAL + requires="issue199", install={ + "SELECT javatest.issue199()" +}) public class PreJSR310 { private static final String TZPRAGUE = "Europe/Prague"; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index 52b86e65..d6bab39f 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -33,17 +33,17 @@ * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example. */ - @SQLAction( - provides = "paramtypeinfo type", // created in Triggers.java - install = { - "CREATE TYPE javatest.paramtypeinfo AS (" + - " name text, pgtypename text, javaclass text, tostring text" + - ")" - }, - remove = { - "DROP TYPE javatest.paramtypeinfo" - } - ) +@SQLAction( + provides = "paramtypeinfo type", // created in Triggers.java + install = { + "CREATE TYPE javatest.paramtypeinfo AS (" + + " name text, pgtypename text, javaclass text, tostring text" + + ")" + }, + remove = { + "DROP TYPE javatest.paramtypeinfo" + } +) public class RecordParameterDefaults implements ResultSetProvider { /** 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 0982619b..66125e11 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 @@ -38,28 +38,28 @@ * * @author Thomas Hallgren */ - @SQLAction(provides = "employees tables", install = { - "CREATE TABLE javatest.employees1" + - " (" + - " id int PRIMARY KEY," + - " name varchar(200)," + - " salary int" + - " )", - - "CREATE TABLE javatest.employees2" + - " (" + - " id int PRIMARY KEY," + - " name varchar(200)," + - " salary int," + - " transferDay date," + - " transferTime time" + - " )" - }, remove = { - "DROP TABLE javatest.employees2", - "DROP TABLE javatest.employees1" - } - ) - @SQLAction(requires = "issue228", install = "SELECT javatest.issue228()") +@SQLAction(provides = "employees tables", install = { + "CREATE TABLE javatest.employees1" + + " (" + + " id int PRIMARY KEY," + + " name varchar(200)," + + " salary int" + + " )", + + "CREATE TABLE javatest.employees2" + + " (" + + " id int PRIMARY KEY," + + " name varchar(200)," + + " salary int," + + " transferDay date," + + " transferTime time" + + " )" + }, remove = { + "DROP TABLE javatest.employees2", + "DROP TABLE javatest.employees1" +} +) +@SQLAction(requires = "issue228", install = "SELECT javatest.issue228()") public class SPIActions { private static final String SP_CHECKSTATE = "sp.checkState"; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index a5119f2f..6ec38888 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -42,39 +42,39 @@ * version, set up in the {@link ConditionalDDR} example. Constraint triggers * appear in PG 9.1, transition tables in PG 10. */ - @SQLAction( - provides = "foobar tables", - install = { - "CREATE TABLE javatest.foobar_1 ( username text, stuff text )", - "CREATE TABLE javatest.foobar_2 ( username text, value numeric )" - }, - remove = { - "DROP TABLE javatest.foobar_2", - "DROP TABLE javatest.foobar_1" - } - ) - @SQLAction( - requires = "constraint triggers", - install = "INSERT INTO javatest.foobar_2(value) VALUES (45)" - ) - @SQLAction( - requires = "foobar triggers", - provides = "foobar2_42", - install = "INSERT INTO javatest.foobar_2(value) VALUES (42)" - ) - @SQLAction( - requires = { "transition triggers", "foobar2_42" }, - install = "UPDATE javatest.foobar_2 SET value = 43 WHERE value = 42" - ) - /* - * Note for another day: this would seem an excellent place to add a - * regression test for github issue #134 (make sure invocations of a - * trigger do not fail with SPI_ERROR_UNCONNECTED). However, any test - * here that runs from the deployment descriptor will be running when - * SPI is already connected, so a regression would not be caught. - * A proper test for it will have to wait for a proper testing harness - * invoking tests from outside PL/Java itself. - */ +@SQLAction( + provides = "foobar tables", + install = { + "CREATE TABLE javatest.foobar_1 ( username text, stuff text )", + "CREATE TABLE javatest.foobar_2 ( username text, value numeric )" + }, + remove = { + "DROP TABLE javatest.foobar_2", + "DROP TABLE javatest.foobar_1" + } +) +@SQLAction( + requires = "constraint triggers", + install = "INSERT INTO javatest.foobar_2(value) VALUES (45)" +) +@SQLAction( + requires = "foobar triggers", + provides = "foobar2_42", + install = "INSERT INTO javatest.foobar_2(value) VALUES (42)" +) +@SQLAction( + requires = { "transition triggers", "foobar2_42" }, + install = "UPDATE javatest.foobar_2 SET value = 43 WHERE value = 42" +) +/* + * Note for another day: this would seem an excellent place to add a + * regression test for github issue #134 (make sure invocations of a + * trigger do not fail with SPI_ERROR_UNCONNECTED). However, any test + * here that runs from the deployment descriptor will be running when + * SPI is already connected, so a regression would not be caught. + * A proper test for it will have to wait for a proper testing harness + * invoking tests from outside PL/Java itself. + */ public class Triggers { /** diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 1195bb18..3117610f 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -39,17 +39,17 @@ * This example relies on {@code implementor} tags reflecting the PostgreSQL * version, set up in the {@link ConditionalDDR} example, and also sets its own. */ - @SQLAction(provides="postgresql_unicodetest", - implementor="postgresql_ge_90000", install= - "SELECT CASE" + - " WHEN 'UTF8' = current_setting('server_encoding')" + - " THEN set_config('pljava.implementors', 'postgresql_unicodetest,' ||" + - " current_setting('pljava.implementors'), true) " + - "END" - ) - @SQLAction(requires="unicodetest fn", - implementor="postgresql_unicodetest", - install= +@SQLAction(provides="postgresql_unicodetest", + implementor="postgresql_ge_90000", install= + "SELECT CASE" + + " WHEN 'UTF8' = current_setting('server_encoding')" + + " THEN set_config('pljava.implementors', 'postgresql_unicodetest,' ||" + + " current_setting('pljava.implementors'), true) " + + "END" +) +@SQLAction(requires="unicodetest fn", +implementor="postgresql_unicodetest", +install= " with " + " usable_codepoints ( cp ) as ( " + " select generate_series(1,x'd7ff'::int) " + @@ -87,14 +87,14 @@ " 'all Unicode codepoint ranges roundtripped successfully.') " + " end " + " from test_summary" - ) - @SQLAction( - install= - "CREATE TYPE unicodetestrow AS " + - "(matched boolean, cparray integer[], s text)", - remove="DROP TYPE unicodetestrow", - provides="unicodetestrow type" - ) +) +@SQLAction( + install= + "CREATE TYPE unicodetestrow AS " + + "(matched boolean, cparray integer[], s text)", + remove="DROP TYPE unicodetestrow", + provides="unicodetestrow type" +) public class UnicodeRoundTripTest { /** * This function takes a string and an array of ints constructed in PG, diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java index a8794ad8..7410fd41 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java @@ -30,14 +30,14 @@ * in case of being loaded into a PostgreSQL instance built without that type. * The {@code pg_node_tree} type appears in 9.1. */ - @SQLAction(implementor="postgresql_ge_90100", - provides="postgresql_xml_ge91", - install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml_ge91,' || " + - " current_setting('pljava.implementors'), true) " + - "END" - ) +@SQLAction(implementor="postgresql_ge_90100", + provides="postgresql_xml_ge91", + install= + "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + + " THEN set_config('pljava.implementors', 'postgresql_xml_ge91,' || " + + " current_setting('pljava.implementors'), true) " + + "END" +) public class XMLRenderedTypes { @Function(schema="javatest", implementor="postgresql_xml_ge91") From b574e6dd8168f096d481dfbfdfd18e036e3fe92b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 24 Oct 2020 20:33:40 -0400 Subject: [PATCH 0797/1087] Remove obsolete imports in examples Should have been removed in a0efb99. --- .../org/postgresql/pljava/example/annotation/ConditionalDDR.java | 1 - .../org/postgresql/pljava/example/annotation/Enumeration.java | 1 - .../java/org/postgresql/pljava/example/annotation/JDBC42_21.java | 1 - .../java/org/postgresql/pljava/example/annotation/PassXML.java | 1 - .../java/org/postgresql/pljava/example/annotation/PreJSR310.java | 1 - .../pljava/example/annotation/RecordParameterDefaults.java | 1 - .../org/postgresql/pljava/example/annotation/SPIActions.java | 1 - .../java/org/postgresql/pljava/example/annotation/Triggers.java | 1 - .../pljava/example/annotation/UnicodeRoundTripTest.java | 1 - .../postgresql/pljava/example/annotation/XMLRenderedTypes.java | 1 - 10 files changed, 10 deletions(-) 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 c26d6db6..0140f437 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 @@ -12,7 +12,6 @@ package org.postgresql.pljava.example.annotation; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; /** * Test of a very simple form of conditional execution in the deployment diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java index 0a999fe6..2fcee7df 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java @@ -15,7 +15,6 @@ import java.util.Arrays; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.SQLType; import org.postgresql.pljava.annotation.Function; 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 1cb3f7fd..212cb13b 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 @@ -13,7 +13,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.example.annotation.ConditionalDDR; // for javadoc 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 81f8695f..bd22169e 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 @@ -67,7 +67,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.example.LoggerTest.logMessage; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java index 4332860f..c0409c0d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java @@ -24,7 +24,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; /** * Some tests of pre-JSR 310 date/time/timestamp conversions. diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index d6bab39f..09d3dbbe 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -20,7 +20,6 @@ import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.SQLType; /** 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 66125e11..dca34e5c 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 @@ -31,7 +31,6 @@ import org.postgresql.pljava.annotation.Function; import static org.postgresql.pljava.annotation.Function.Effects.*; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; /** * Some methods used for testing the SPI JDBC driver. diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index 6ec38888..804ef9d8 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -23,7 +23,6 @@ import org.postgresql.pljava.TriggerData; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.Trigger; import static org.postgresql.pljava.annotation.Trigger.Called.*; import static org.postgresql.pljava.annotation.Trigger.Constraint.*; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 3117610f..4f2c0ec4 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -15,7 +15,6 @@ import java.sql.SQLException; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.Function; /** diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java index 7410fd41..871e9644 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java @@ -17,7 +17,6 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.SQLType; import static org.postgresql.pljava.example.LoggerTest.logMessage; From bb88d822a5ba8000eb86c72236ad0d782a07a5e4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 26 Oct 2020 18:27:53 -0400 Subject: [PATCH 0798/1087] Allow PL/Java's class loader to make magic URLs PL/Java's class loader has historically handled resource lookups by returning a URL that has a custom URLStreamHandler wired in. Permission to do that wasn't included in the pljava.policy shipped with 1.6.0. Fix that. Addresses #322. Still does not get closer to fixing #266, but at least avoids making it worse. --- pljava-packaging/src/main/resources/pljava.policy | 2 ++ pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index 7513219e..5fbdd171 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -58,6 +58,8 @@ grant codebase "${org.postgresql.pljava.codesource}" { "charsetProvider"; permission java.lang.RuntimePermission "createClassLoader"; + permission java.net.NetPermission + "specifyStreamHandler"; permission java.util.logging.LoggingPermission "control"; permission java.security.SecurityPermission diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 032ce8f7..7dbf84b2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -342,12 +342,12 @@ private static URL entryURL(int entryId) { try { - return new URL( + return doPrivileged(() -> new URL( "dbf", "localhost", -1, "/" + entryId, - EntryStreamHandler.getInstance()); + EntryStreamHandler.getInstance())); } catch(MalformedURLException e) { From 9b60f132b24c55f63dc0a2d7482702897b8453df Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 26 Oct 2020 19:20:23 -0400 Subject: [PATCH 0799/1087] Add a test of getResourceAsStream --- .../example/annotation/UsingProperties.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java index bcd3f2b9..34be0c65 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java @@ -26,6 +26,7 @@ import java.util.logging.Logger; import org.postgresql.pljava.ResultSetProvider; +import org.postgresql.pljava.annotation.SQLAction; /** * An example that retrieves a {@code Properties} resource, and returns @@ -33,6 +34,20 @@ * interface. * @author Thomas Hallgren */ +@SQLAction(requires = "propertyExampleAnno", install = { + "WITH" + + " expected AS (VALUES" + + " ('adjective' ::varchar(200), 'avaricious' ::varchar(200))," + + " ('noun', 'platypus')" + + " )" + + "SELECT" + + " CASE WHEN every(prop IN (SELECT expected FROM expected))" + + " THEN javatest.logmessage('INFO', 'get resource passes')" + + " ELSE javatest.logmessage('WARNING', 'get resource fails')" + + " END" + + " FROM" + + " propertyexampleanno() AS prop" +}) public class UsingProperties implements ResultSetProvider.Large { private static Logger s_logger = Logger.getAnonymousLogger(); @@ -42,7 +57,9 @@ public UsingProperties() throws IOException { Properties v = new Properties(); - InputStream propStream = this.getClass().getResourceAsStream("example.properties"); + InputStream propStream = + this.getClass().getResourceAsStream("example.properties"); + if(propStream == null) { s_logger.fine("example.properties was null"); @@ -76,7 +93,7 @@ public boolean assignRowValues(ResultSet receiver, long currentRow) * Return the contents of the {@code example.properties} resource, * one (key,value) row per entry. */ - @Function( type = "javatest._properties") + @Function(type = "javatest._properties", provides = "propertyExampleAnno") public static ResultSetProvider propertyExampleAnno() throws SQLException { From 919cc7665c6446635b0db3487503cb50f5d79d89 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 26 Oct 2020 20:13:21 -0400 Subject: [PATCH 0800/1087] Add test of ResourceBundle.getBundle This reads the same property file as the earlier test, but using a different mechanism. --- .../example/annotation/UsingProperties.java | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java index 34be0c65..27a7a5ea 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Properties; +import java.util.ResourceBundle; import java.util.logging.Logger; import org.postgresql.pljava.ResultSetProvider; @@ -34,7 +35,7 @@ * interface. * @author Thomas Hallgren */ -@SQLAction(requires = "propertyExampleAnno", install = { +@SQLAction(requires = {"propertyExampleAnno", "propertyExampleRB"}, install = { "WITH" + " expected AS (VALUES" + " ('adjective' ::varchar(200), 'avaricious' ::varchar(200))," + @@ -46,7 +47,20 @@ " ELSE javatest.logmessage('WARNING', 'get resource fails')" + " END" + " FROM" + - " propertyexampleanno() AS prop" + " propertyExampleAnno() AS prop", + + "WITH" + + " expected AS (VALUES" + + " ('adjective' ::varchar(200), 'avaricious' ::varchar(200))," + + " ('noun', 'platypus')" + + " )" + + "SELECT" + + " CASE WHEN every(prop IN (SELECT expected FROM expected))" + + " THEN javatest.logmessage('INFO', 'get ResourceBundle passes')" + + " ELSE javatest.logmessage('WARNING', 'get ResourceBundle fails')" + + " END" + + " FROM" + + " propertyExampleRB() AS prop" }) public class UsingProperties implements ResultSetProvider.Large { @@ -74,6 +88,33 @@ public UsingProperties() } } + /** + * This constructor (distinguished by signature) reads the same property + * file, but using the {@code ResourceBundle} machinery instead of + * {@code Properties}. + */ + private UsingProperties(Void usingResourceBundle) + { + ResourceBundle b = + ResourceBundle.getBundle(getClass().getPackageName() + ".example"); + + Iterator keys = b.getKeys().asIterator(); + + m_propertyIterator = new Iterator>() + { + public boolean hasNext() + { + return keys.hasNext(); + } + + public Map.Entry next() + { + String k = keys.next(); + return Map.entry(k, b.getString(k)); + } + }; + } + public boolean assignRowValues(ResultSet receiver, long currentRow) throws SQLException { @@ -107,6 +148,17 @@ public static ResultSetProvider propertyExampleAnno() } } + /** + * Return the contents of the {@code example.properties} resource, + * one (key,value) row per entry, using {@code ResourceBundle} to load it. + */ + @Function(type = "javatest._properties", provides = "propertyExampleRB") + public static ResultSetProvider propertyExampleRB() + throws SQLException + { + return new UsingProperties(null); + } + public void close() { } From 4f4beec4630a5e79a3811334d2d95aaa63e10bf3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 28 Oct 2020 23:55:36 -0400 Subject: [PATCH 0801/1087] Add an out= element to the Function annotation Between creating a named composite type and declaring a function to return that (it is then easy to use the function and work with its result, but sometimes it's a nuisance to have to create a type just for that purpose) and simply declaring RECORD as the return type (avoids one nuisance, but then every use of the function in SQL must be followed by AS (column definition list)), a happy medium is to declare a function to SQL with OUT parameters. The function can be called (without a column definition list) and produce a result with usefully named and typed columns. On discovering that PL/Java's runtime needs no changes to handle functions declared that way, it seems quite useful to add annotation support for producing that style of function declaration. --- .../postgresql/pljava/annotation/Function.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java index e37e224e..b8ec4895 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java @@ -78,9 +78,27 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; * {@link org.postgresql.pljava.ResultSetProvider ResultSetProvider}, * or can be used to specify the return type of any function if the * compiler hasn't inferred it correctly. + *

    + * Only one of {@code type} or {@code out} may appear. */ String type() default ""; + /** + * The result column names and types of a composite-returning function. + *

    + * This is for a function defining its own one-off composite type + * (declared with {@code OUT} parameters). If the function returns some + * composite type known to the catalog, simply use {@code type} and the name + * of that type. + *

    + * Each element is a name and a type specification, separated by whitespace. + * An element that begins with whitespace declares an output column with no + * name, only a type. The name is an ordinary SQL identifier; if it would + * be quoted in SQL, naturally each double-quote must be represented as + * {@code \"} in Java. + */ + String[] out() default {}; + /** * The name of the function. This is optional. The default is * to use the name of the annotated method. From f9cebce2a9c58696f3c8cda9eb33823f9733a106 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 28 Oct 2020 23:57:31 -0400 Subject: [PATCH 0802/1087] Teach SQL generator out= in Function annotation --- .../annotation/processing/DDRProcessor.java | 109 +++++++++++++++--- 1 file changed, 95 insertions(+), 14 deletions(-) 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 07805d51..776490f2 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 @@ -55,6 +55,7 @@ import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import static java.util.Objects.hash; import static java.util.Objects.requireNonNull; import java.util.PriorityQueue; @@ -65,6 +66,7 @@ import java.util.stream.Stream; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1597,7 +1599,7 @@ public String[] deployStrings() if ( ! "".equals( _when) ) sb.append( "\n\tWHEN ").append( _when); sb.append( "\n\tEXECUTE PROCEDURE "); - func.appendNameAndParams( sb, true, false); + func.appendNameAndParams( sb, true, false, false); sb.setLength( sb.length() - 1); // drop closing ) s = _arguments.length; for ( String a : _arguments ) @@ -1635,6 +1637,7 @@ class FunctionImpl implements Function, Snippet, Commentable { public String type() { return _type; } + public String[] out() { return _out; } public String name() { return _name; } public String schema() { return _schema; } public boolean variadic() { return _variadic; } @@ -1658,6 +1661,7 @@ public String language() ExecutableElement func; public String _type; + public String[] _out; public String _name; public String _schema; public boolean _variadic; @@ -1685,6 +1689,7 @@ public String language() DBType returnType; DBType[] parameterTypes; + List> outParameters; boolean subsumed = false; @@ -1693,6 +1698,18 @@ public String language() func = e; } + public void setType( Object o, boolean explicit, Element e) + { + if ( explicit ) + _type = (String)o; + } + + public void setOut( Object o, boolean explicit, Element e) + { + if ( explicit ) + _out = avToArray( o, String.class); + } + public void setTrust( Object o, boolean explicit, Element e) { if ( explicit ) @@ -1759,7 +1776,7 @@ public boolean characterize() List typeArgs; int arity = ptms.size(); - if ( ! "".equals( type()) + if ( ( null != _type || null != _out ) && ret.getKind().equals( TypeKind.BOOLEAN) ) { complexViaInOut = true; @@ -1931,8 +1948,12 @@ Stream parameterInfo() */ void resolveParameterAndReturnTypes() { - if ( ! "".equals( type()) ) - returnType = DBType.fromSQLTypeAnnotation( type()); + if ( null != _type && null != _out ) + msg( Kind.ERROR, func, "A PL/Java function may specify " + + "only one of type, out"); + + if ( null != _type ) + returnType = DBType.fromSQLTypeAnnotation( _type); else if ( null != setofComponent ) returnType = tmpr.getSQLType( setofComponent, func); else if ( setof ) @@ -1943,6 +1964,14 @@ else if ( setof ) parameterTypes = parameterInfo() .map(i -> tmpr.getSQLType(i.tm, i.ve, i.st, true, true)) .toArray(DBType[]::new); + + if ( null != _out ) + { + returnType = DT_RECORD; + outParameters = Arrays.stream(_out) + .map(DBType::fromNameAndType) + .collect(toList()); + } } /** @@ -1970,6 +1999,12 @@ void recordImplicitTags() if ( null != t ) requires.add(t); } + + if ( null != outParameters ) + outParameters.stream() + .map(m -> m.getValue().dependTag()) + .filter(Objects::nonNull) + .forEach(requires::add); } @Override @@ -1985,16 +2020,20 @@ public void subsume() * * @param dflts Whether to include the defaults, if any. */ - void appendNameAndParams(StringBuilder sb, boolean names, boolean dflts) + void appendNameAndParams( + StringBuilder sb, boolean names, boolean outs, boolean dflts) { sb.append(qnameFrom(name(), schema())).append( '('); - appendParams( sb, names, dflts); + appendParams( sb, names, outs, dflts); // TriggerImpl relies on ) being the very last character sb.append( ')'); } - void appendParams( StringBuilder sb, boolean names, boolean dflts) + void appendParams( + StringBuilder sb, boolean names, boolean outs, boolean dflts) { + int lengthOnEntry = sb.length(); + int count = parameterTypes.length; for ( ParameterInfo i : (Iterable)parameterInfo()::iterator ) @@ -2015,9 +2054,21 @@ void appendParams( StringBuilder sb, boolean names, boolean dflts) sb.append(i.dt.toString(dflts)); - if ( 0 < count ) - sb.append(','); + sb.append(','); } + + if ( outs && null != outParameters ) + { + outParameters.forEach(e -> { + sb.append("\n\tOUT "); + if ( null != e.getKey() ) + sb.append(e.getKey()).append(' '); + sb.append(e.getValue().toString(false)).append(','); + }); + } + + if ( lengthOnEntry < sb.length() ) + sb.setLength(sb.length() - 1); // that last pesky comma } void appendAS( StringBuilder sb) @@ -2038,7 +2089,7 @@ public String[] deployStrings() ArrayList al = new ArrayList<>(); StringBuilder sb = new StringBuilder(); sb.append( "CREATE OR REPLACE FUNCTION "); - appendNameAndParams( sb, true, true); + appendNameAndParams( sb, true, true, true); sb.append( "\n\tRETURNS "); if ( trigger ) sb.append( DT_TRIGGER.toString()); @@ -2076,7 +2127,7 @@ public String[] deployStrings() { sb.setLength( 0); sb.append( "COMMENT ON FUNCTION "); - appendNameAndParams( sb, true, false); + appendNameAndParams( sb, true, false, false); sb.append( "\nIS "); sb.append( DDRWriter.eQuote( comm)); al.add( sb.toString()); @@ -2101,7 +2152,7 @@ public String[] undeployStrings() StringBuilder sb = new StringBuilder(); sb.append( "DROP FUNCTION "); - appendNameAndParams( sb, true, false); + appendNameAndParams( sb, true, false, false); rslt [ rslt.length - 1 ] = sb.toString(); return rslt; } @@ -2210,7 +2261,8 @@ class BaseUDTFunctionImpl extends FunctionImpl BaseUDTFunctionID id; @Override - void appendParams( StringBuilder sb, boolean names, boolean dflts) + void appendParams( + StringBuilder sb, boolean names, boolean outs, boolean dflts) { sb.append( Arrays.stream(id.getParam( ui)) @@ -2248,6 +2300,13 @@ public void setType( Object o, boolean explicit, Element e) "The type of a UDT function may not be changed"); } + public void setOut( Object o, boolean explicit, Element e) + { + if ( explicit ) + msg( Kind.ERROR, e, + "The type of a UDT function may not be changed"); + } + public void setVariadic( Object o, boolean explicit, Element e) { if ( explicit ) @@ -2952,7 +3011,7 @@ else if ( Path.INOUT == _path ) else { sb.append("WITH FUNCTION "); - func.appendNameAndParams(sb, false, false); + func.appendNameAndParams(sb, false, false, false); } switch ( _application ) @@ -3966,6 +4025,28 @@ public final boolean equals(Object o, Messager msgr) ")" ); + /** + * Parse a string, representing an optional parameter/column name followed + * by a type, into an {@code Identifier.Simple}, possibly null, and a + * {@code DBType}. + *

    + * Whitespace (or, strictly, separator; comments would be accepted) must + * separate the name from the type, if the name is not quoted. To omit a + * name and supply only the type, the string must begin with whitespace + * (ahem, separator). + */ + static Map.Entry fromNameAndType(String nandt) + { + Identifier.Simple name = null; + Matcher m = ISO_AND_PG_IDENTIFIER_CAPTURING.matcher(nandt); + if ( m.lookingAt() ) + { + nandt = nandt.substring(m.end()); + name = identifierFrom(m); + } + return Map.entry(name, fromSQLTypeAnnotation(nandt)); + } + /** * Make a {@code DBType} from whatever might appear in an {@code SQLType} * annotation. From 07efa02fd50ae2dfe3a48f07cf8f0ba4f7ae804f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 29 Oct 2020 18:27:59 -0400 Subject: [PATCH 0803/1087] Add example functions declaring composite returns In passing, tighten a regression test recently added for an unrelated issue. --- .../example/annotation/ReturnComposite.java | 108 ++++++++++++++++++ .../example/annotation/UsingProperties.java | 6 +- 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java new file mode 100644 index 00000000..9a5be5ed --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2020 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.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; + +import org.postgresql.pljava.ResultSetProvider; +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; + +/** + * Demonstrates {@code @Function(out={...})} for a function that returns a + * non-predeclared composite type. + */ +@SQLAction(requires = { "helloOutParams", "helloTable" }, install = { + "SELECT" + + " CASE WHEN want IS NOT DISTINCT FROM helloOutParams()" + + " THEN javatest.logmessage('INFO', 'composite return passes')" + + " ELSE javatest.logmessage('WARNING', 'composite return fails')" + + " END" + + " FROM" + + " (SELECT 'Hello' ::text, 'world' ::text) AS want", + + "WITH" + + " expected AS (VALUES" + + " ('Hello' ::text, 'twelve' ::text)," + + " ('Hello', 'thirteen')," + + " ('Hello', 'love')" + + " )" + + "SELECT" + + " CASE WHEN every(want IS NOT DISTINCT FROM got)" + + " THEN javatest.logmessage('INFO', 'set of composite return passes')" + + " ELSE javatest.logmessage('WARNING', 'set of composite return fails')" + + " END" + + " FROM" + + " (SELECT row_number() OVER (), * FROM expected) AS want" + + " LEFT JOIN (SELECT row_number() OVER (), * FROM hellotable()) AS got" + + " USING (row_number)" +}) +public class ReturnComposite implements ResultSetProvider.Large +{ + /** + * Returns a two-column composite result that does not have to be + * a predeclared composite type, or require the calling SQL query to + * follow the function call with a result column definition list, as is + * needed for a bare {@code RECORD} return type. + */ + @Function( + schema = "javatest", out = { "greeting text", "addressee text" }, + provides = "helloOutParams" + ) + public static boolean helloOutParams(ResultSet out) throws SQLException + { + out.updateString(1, "Hello"); + out.updateString(2, "world"); + return true; + } + + /** + * Returns a two-column table result that does not have to be + * a predeclared composite type, or require the calling SQL query to + * follow the function call with a result column definition list, as is + * needed for a bare {@code RECORD} return type. + */ + @Function( + schema = "javatest", out = { "greeting text", "addressee text" }, + provides = "helloTable" + ) + public static ResultSetProvider helloTable() + throws SQLException + { + return new ReturnComposite(); + } + + Iterator addressees = + List.of("twelve", "thirteen", "love").iterator(); + + @Override + public boolean assignRowValues(ResultSet out, long currentRow) + throws SQLException + { + if ( ! addressees.hasNext() ) + return false; + + out.updateString(1, "Hello"); + out.updateString(2, addressees.next()); + return true; + } + + @Override + public void close() + { + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java index 27a7a5ea..417853db 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UsingProperties.java @@ -42,7 +42,8 @@ " ('noun', 'platypus')" + " )" + "SELECT" + - " CASE WHEN every(prop IN (SELECT expected FROM expected))" + + " CASE WHEN" + + " 2 = count(prop) AND every(prop IN (SELECT expected FROM expected))" + " THEN javatest.logmessage('INFO', 'get resource passes')" + " ELSE javatest.logmessage('WARNING', 'get resource fails')" + " END" + @@ -55,7 +56,8 @@ " ('noun', 'platypus')" + " )" + "SELECT" + - " CASE WHEN every(prop IN (SELECT expected FROM expected))" + + " CASE WHEN" + + " 2 = count(prop) AND every(prop IN (SELECT expected FROM expected))" + " THEN javatest.logmessage('INFO', 'get ResourceBundle passes')" + " ELSE javatest.logmessage('WARNING', 'get ResourceBundle fails')" + " END" + From 06a7a76c16ad629592426150d4a6c3f8cdb62c09 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 30 Oct 2020 12:03:51 -0400 Subject: [PATCH 0804/1087] Fix nameAndType for no-name case A Reserved type could be saved with initial separator chars that should have been stripped, and while nothing inherent about a Map.Entry precludes a null key or value, the Java 9 added Map.entry() static method won't allow it. --- .../pljava/annotation/processing/DDRProcessor.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 776490f2..4c54f978 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 @@ -4044,7 +4044,9 @@ static Map.Entry fromNameAndType(String nandt) nandt = nandt.substring(m.end()); name = identifierFrom(m); } - return Map.entry(name, fromSQLTypeAnnotation(nandt)); + return + new AbstractMap.SimpleImmutableEntry<>( + name, fromSQLTypeAnnotation(nandt)); } /** @@ -4085,6 +4087,7 @@ static DBType fromSQLTypeAnnotation(String value) Matcher m = SEPARATOR.matcher(value); separator(m, false); + int postSeparator = m.regionStart(); if ( m.usePattern(ISO_AND_PG_IDENTIFIER_CAPTURING).lookingAt() ) { @@ -4184,7 +4187,8 @@ else if ( null != qname.qualifier() ) DBType result; if ( reserved ) - result = new DBType.Reserved(value.substring(0, m.regionEnd())); + result = new DBType.Reserved( + value.substring(postSeparator, m.regionEnd())); else { result = new DBType.Named(qname); @@ -4639,6 +4643,12 @@ public boolean equals(Object o, Messager msgr) } return true; } + + @Override + public String toString() + { + return super.toString() + Arrays.toString(m_signature); + } } } From fe5dd3b8903ef1b4e18a02ae413760b68820105c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 25 Oct 2020 18:50:27 -0400 Subject: [PATCH 0805/1087] Add an Aggregate annotation This can represent most of what a PostgreSQL CREATE AGGREGATE command can say. Omitted for the moment are SERIALFUNC and DESERIALFUNC (there isn't much PL/Java functions can do with datatype internal), and SORTOP (not enough awareness of operators yet). There is still @SQLAction for any rare need to get a CREATE AGGREGATE into a deployment descriptor if this annotation can't describe it. Since the operations of a PostgreSQL aggregate correspond nicely to those of a Java java.util.stream.Collector (accumulator, combiner, finisher) I've elected to use accumulate, combine, finish (plus remove) as the annotation elements, rather than the PostgreSQLisms SFUNC/MSFUNC, FINALFUNC, MINVFUNC, etc., ideally to spark a flash of recognition in Java users. An SQL generator is just the thing to produce the rather JCL-esque PostgreSQL forms from annotations that are easier to read. For now, the only allowed target is a type, with no more significance than that a bunch of these annotations could be conveniently hung at the top of some source file for a class. Because an aggregate can refer to more than one other function for its supporting operations, it did not seem quite natural to assign a meaning to hanging the annotation on a method. On the other hand, the only required function is the "accumulate" of the main Plan, and the simplest aggregates can be declared with nothing more than name, args, stateType, and accumulate; perhaps it would be convenient some day to allow a super-shorthand where the annotation goes on a method that could serve as accumulate and as much as possible is inferred. Because of the close mapping between a PostgreSQL aggregate and a Java Collector, it is also intriguing to imagine placing this aggregate on some class that implements Collector and trying to generate all the glue to make a PostgreSQL aggregate of it. Intriguing, but highly speculative, and certainly not on any roadmap at the moment. --- .../pljava/annotation/Aggregate.java | 382 ++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java new file mode 100644 index 00000000..2e377804 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a PostgreSQL aggregate. + *

    + * An aggregate function in PostgreSQL is defined by using + * {@code CREATE AGGREGATE} to specify its name and argument types, along with + * at least one "plan" for evaluating it, where the plan specifies at least: + * a data type to use for the accumulating state, and a function (here called + * "accumulate") called for each row to update the state. If the plan includes + * a function "finish", its return type is the return type of the aggregate; + * with no "finish" function, the state type is also the aggregate's return + * type. + *

    + * Optionally, a plan can include a "combine" function, which is passed two + * instances of the state type and combines them, to allow aggregate evaluation + * to be parallelized. The names "accumulate", "combine", and "finish" are not + * exactly as used in the PostgreSQL command (those are unpronounceable + * abbreviations), but follow the usage in {@code java.util.stream.Collector}, + * which should make them natural to Java programmers. PL/Java will generate the + * SQL with the unpronounceable names. + *

    + * If an aggregate function might be used in a window with a moving frame start, + * it can be declared with a second plan ({@code movingPlan}) that includes a + * "remove" function that may be called, passing values that were earlier + * accumulated into the state, to remove them again as the frame start advances + * past them. (Java's {@code Collector} has no equivalent of a "remove" + * function.) A "remove" function may only be specified (and must be specified) + * in a plan given as {@code movingPlan}. + *

    + * Any function referred to in a plan is specified by its name, optionally + * schema-qualified. Its argument types are not specified; they are implied by + * those declared for the aggregate itself. An "accumulate" function gets one + * argument of the state type, followed by all those given as {@code arguments}. + * The same is true of a "remove" function. A "combine" function is passed + * two arguments of the state type. + *

    + * A "finish" function has a first argument of the state type. If the aggregate + * is declared with any {@code directArguments}, those follow the state type. + * (Declaring {@code directArguments} makes the aggregate an "ordered-set + * aggregate", which could additionally have {@code hypothetical=true} to make + * it a "hypothetical-set aggregate", for which the PostgreSQL documentation + * covers the details.) If {@code polymorphic=true}, the "finish" function's + * argument list will end with {@code arguments.length} additional arguments; + * they will all be passed as {@code NULL} when the finisher is called, but will + * have the right run-time types, which may be necessary to resolve the + * finisher's return type, if polymorphic types are involved. + *

    + * If any of the functions or types mentioned in this declaration are also being + * generated into the same deployment descriptor, the {@code CREATE AGGREGATE} + * generated from this annotation will follow them. Other ordering dependencies, + * if necessary, can be explicitly arranged with {@code provides} and + * {@code requires}. + *

    + * While this annotation can generate {@code CREATE AGGREGATE} deployment + * commands with most of the features available in PostgreSQL (currently not + * covered are {@code SERIALFUNC}, {@code DESERIALFUNC}, and {@code SORTOP}), + * at present there are limits to which aggregate features can be implemented + * purely in PL/Java. In particular, PL/Java functions currently have no access + * to the PostgreSQL data structures needed for an ordered-set or + * hypothetical-set aggregate. Such an aggregate could be implemented by writing + * some of its support functions in another procedural language; this annotation + * could still be used to automatically generate the declaration. + * @author Chapman Flack + */ +@Documented +@Target(ElementType.TYPE) +@Repeatable(Aggregate.Container.class) +@Retention(RetentionPolicy.CLASS) +public @interface Aggregate +{ + /** + * Declares the effect of the {@code finish} function in a {@code Plan}. + *

    + * If {@code READ_ONLY}, PostgreSQL can continue updating the same state + * with additional rows, and call the finisher again for updated results. + *

    + * If {@code SHAREABLE}, the state cannot be further updated after + * a finisher call, but finishers for other aggregates that use the same + * state representation (and are also {@code SHAREABLE}) can be called to + * produce the results for those aggregates. An example could be the several + * linear-regression-related aggregates, all of which can work from a state + * that contains the count of values, sum of values, and sum of squares. + *

    + * If {@code READ_WRITE}, no further use can be made of the state after + * the finisher has run. + */ + enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; + + /** + * Specifies one "plan" for evaluating the aggregate; one must always be + * specified (as {@code plan}), and a second may be specified (as + * {@code movingPlan}). + *

    + * A plan must specify a data type {@code stateType} to hold the + * accumulating state, optionally an estimate of its expected size in bytes, + * and optionally its initial contents. The plan also specifies up to four + * functions {@code accumulate}, {@code combine}, {@code finish}, and + * {@code remove}. Only {@code accumulate} is always required; + * {@code remove} is required in a {@code movingPlan}, and otherwise not + * allowed. + *

    + * Each of the four functions may be specified as a single string "name", + * which will be leniently parsed as an optionally schema-qualified name, + * or as two strings {@code {"schema","local"}} with the schema made + * explicit. The two-string form with {@code ""} as the schema represents + * an explicitly non-schema-qualified name. + */ + @Target({}) + @Retention(RetentionPolicy.CLASS) + @interface Plan + { + /** + * The data type to be used to hold the accumulating state. + *

    + * This will be the first argument type for all of the support functions + * (both argument types for {@code combine}) and also, if there is no + * {@code finish} function, the result type of the aggregate. + */ + String stateType(); + + /** + * An optional estimate of the size in bytes that the state may grow + * to occupy. + */ + int stateSize() default 0; + + /** + * An optional initial value for the state (which will otherwise be + * initially null). + *

    + * Must be a string the state type's text-input conversion would accept. + *

    + * Omitting the initial value only works if the {@code accumulate} + * function is {@code onNullInput=CALLED}, or if the aggregate's first + * argument type is the same as the state type. + */ + String initialState() default ""; + + /** + * Name of the function that will be called for each row being + * aggregated. + *

    + * The function named here must have an argument list that starts with + * one argument of the state type, followed by all of this aggregate's + * {@code arguments}. It does not receive the {@code directArguments}, + * if any. + */ + String[] accumulate(); + + /** + * Name of an optional function to combine two instances of the state + * type. + *

    + * The function named here should be one that has two arguments, both + * of the state type, and returns the state type. + */ + String[] combine() default {}; + + /** + * Name of an optional function to produce the aggregate's result from + * the final value of the state; without this function, the aggregate's + * result type is the state type, and the result is simply the final + * value of the state. + *

    + * When this function is specified, its result type determines the + * result type of the aggregate. Its argument list signature is a single + * argument of the state type, followed by all the + * {@code directArguments} if any, followed (only if {@code polymorphic} + * is true) by {@code arguments.length} additional arguments for which + * nulls will be passed at runtime but with their resolved runtime + * types. + */ + String[] finish() default {}; + + /** + * Name of an optional function that can reverse the effect on the state + * of a row previously passed to {@code accumulate}. + *

    + * The function named here should have the same argument list signature + * as the {@code accumulate} function. + *

    + * Required in a {@code movingPlan}; not allowed otherwise. + */ + String[] remove() default {}; + + /** + * Whether the argument list for {@code finish} should be extended with + * slots corresponding to the aggregated {@code arguments}, all nulls at + * runtime but with their resolved runtime types. + */ + boolean polymorphic() default false; + + /** + * The effect of the {@code finish} function in this {@code Plan}. + *

    + * If {@code READ_ONLY}, PostgreSQL can continue updating the same + * state with additional rows, and call the finisher again for updated + * results. + *

    + * If {@code SHAREABLE}, the state cannot be further updated after a + * finisher call, but finishers for other aggregates that use the same + * state representation (and are also {@code SHAREABLE}) can be called + * to produce the results for those aggregates. An example could be the + * several linear-regression-related aggregates, all of which can work + * from a state that contains the count of values, sum of values, and + * sum of squares. + *

    + * If {@code READ_WRITE}, no further use can be made of the state after + * the finisher has run. + *

    + * Leaving this to default is not exactly equivalent to specifying the + * default value shown here. If left to default, it will be left + * unspecified in the generated {@code CREATE AGGREGATE}, and PostgreSQL + * will apply its default, which is {@code READ_ONLY} in the case of an + * ordinary aggregate, but {@code READ_WRITE} for an ordered-set or + * hypothetical-set aggregate. + */ + FinishEffect finishEffect() default FinishEffect.READ_ONLY; + } + + /** + * Name for this aggregate. + *

    + * May be specified in explicit {@code {"schema","localname"}} form, or as + * a single string that will be leniently parsed as an optionally + * schema-qualified name. In the explicit form, {@code ""} as the schema + * will make the name explicitly unqualified (in case the local name might + * contain a dot and be misread as a qualified name). + */ + String[] name(); + + /** + * Names and types of the arguments to be aggregated: the ones passed to the + * {@code accumulate} function for each aggregated row. + *

    + * Each element is a name and a type specification, separated by whitespace. + * An element that begins with whitespace declares a parameter with no + * name, only a type. The name is an ordinary SQL identifier; if it would + * be quoted in SQL, naturally each double-quote must be represented as + * {@code \"} in Java. + */ + String[] arguments(); + + /** + * Names and types of the "direct arguments" to an ordered-set or + * hypothetical-set aggregate (specifying this element is what makes an + * ordered-set aggregate, which will be a hypothetical-set aggregate if + * {@code hypothetical=true} is also supplied). + *

    + * Specified as for {@code arguments}. The direct arguments are not passed + * to the {@code accumulate} function for each aggregated row; they are only + * passed to the {@code finish} function when producing the result. + */ + String[] directArguments() default {}; + + /** + * Specify {@code true} in an ordered-set aggregate (one with + * {@code directArguments} specified) to make it a hypothetical-set + * aggregate. + *

    + * When {@code true}, the {@code directArguments} list must be at least as + * long as {@code arguments}, and its last {@code arguments.length} types + * must match {@code arguments} one-to-one. When the {@code finish} function + * is called, those last direct arguments will carry the caller-supplied + * values for the "hypothetical" row. + */ + boolean hypothetical() default false; + + /** + * Whether the aggregate has a variadic last argument. + *

    + * Specify as a single boolean, {@code variadic=true}, to declare an + * ordinary aggregate variadic. The last type of its declared + * {@code arguments} must then be either an array type, or + * {@code pg_catalog."any"} + *

    + * The form {@code variadic={boolean,boolean}} is for an ordered-set + * aggregate, which has both a list of {@code directArguments} (the first + * boolean) and its aggregated {@code arguments} (the second). For an + * ordered-set aggregate, {@code "any"} is the only allowed type for a + * variadic argument. + *

    + * When also {@code hypothetical} is true, the requirement that the + * {@code directArguments} have a tail matching the {@code arguments} + * implies that the two lists must both or neither be variadic. + */ + boolean[] variadic() default {}; + + /** + * The {@link Plan Plan} normally to be used for evaluating this aggregate, + * except possibly in a moving-window context if {@code movingPlan} is also + * supplied. + *

    + * Required. This plan may not name a {@code remove} function; only + * a {@code movingPlan} can do that. + */ + Plan plan(); + + + /** + * An optional {@link Plan Plan} that may be more efficient for evaluating + * this aggregate in a moving-window context. + *

    + * Though declared as an array, only one plan is allowed here. It must + * name a {@code remove} function. + */ + Plan[] movingPlan() default {}; + + /** + * Parallel-safety declaration for this aggregate; PostgreSQL's planner + * will consult this only, not the declarations on the individual supporting + * functions. + *

    + * See {@link Function#parallel() Function.parallel} for the implications. + * In PL/Java, any setting other than {@code UNSAFE} should be considered + * experimental. + */ + Function.Parallel parallel() default Function.Parallel.UNSAFE; + + // not yet here: serialfunc, deserialfunc, sortop + + /** + * One or more arbitrary labels that will be considered 'provided' by the + * object carrying this annotation. The deployment descriptor will be + * 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. + */ + String[] provides() default {}; + + /** + * One or more arbitrary labels that will be considered 'required' by the + * object carrying this annotation. The deployment descriptor will be + * generated in such an order that other objects that 'provide' labels + * 'required' by this come earlier in the output for install actions, and + * later for remove actions. + */ + String[] requires() default {}; + + /** + * The {@code } to be used around SQL code generated + * for this aggregate. Defaults to {@code PostgreSQL}. Set explicitly to + * {@code ""} to emit code not wrapped in an {@code }. + */ + String implementor() default ""; + + /** + * A comment to be associated with the aggregate. The default is to create + * no comment. + */ + String comment() default ""; + + /** + * @hidden container type allowing Cast to be repeatable. + */ + @Documented + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.CLASS) + @interface Container + { + Aggregate[] value(); + } +} From c6c6821dd5aec39429241adfc7aa325f6af46302 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 30 Oct 2020 12:00:38 -0400 Subject: [PATCH 0806/1087] Generate SQL for the Aggregate annotation --- .../annotation/processing/DDRProcessor.java | 680 ++++++++++++++++++ 1 file changed, 680 insertions(+) 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 4c54f978..b948fa63 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 @@ -52,6 +52,7 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; @@ -63,6 +64,7 @@ import java.util.Set; import java.util.function.Supplier; +import static java.util.function.UnaryOperator.identity; import java.util.stream.Stream; import static java.util.stream.Collectors.joining; @@ -107,6 +109,7 @@ import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.TriggerData; +import org.postgresql.pljava.annotation.Aggregate; import org.postgresql.pljava.annotation.Cast; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; @@ -226,6 +229,8 @@ class DDRProcessorImpl final TypeElement AN_SQLACTIONS; final TypeElement AN_CAST; final TypeElement AN_CASTS; + final TypeElement AN_AGGREGATE; + final TypeElement AN_AGGREGATES; // Certain familiar DBTypes (capitalized as this file historically has) // @@ -237,6 +242,8 @@ class DDRProcessorImpl Identifier.Qualified.nameFromJava("pg_catalog.trigger")); final DBType DT_VOID = new DBType.Named( Identifier.Qualified.nameFromJava("pg_catalog.void")); + final DBType DT_ANY = new DBType.Named( + Identifier.Qualified.nameFromJava("pg_catalog.\"any\"")); // Function signatures for certain known functions // @@ -328,6 +335,9 @@ class DDRProcessorImpl AN_CAST = elmu.getTypeElement( Cast.class.getName()); AN_CASTS = elmu.getTypeElement( Cast.Container.class.getCanonicalName()); + AN_AGGREGATE = elmu.getTypeElement( Aggregate.class.getName()); + AN_AGGREGATES = elmu.getTypeElement( + Aggregate.Container.class.getCanonicalName()); } void msg( Kind kind, String fmt, Object... args) @@ -441,6 +451,7 @@ boolean process( Set tes, RoundEnvironment re) boolean baseUDTPresent = false; boolean mappedUDTPresent = false; boolean castPresent = false; + boolean aggregatePresent = false; boolean willClaim = true; @@ -458,6 +469,8 @@ else if ( AN_SQLACTION.equals( te) || AN_SQLACTIONS.equals( te) ) sqlActionPresent = true; else if ( AN_CAST.equals( te) || AN_CASTS.equals( te) ) castPresent = true; + else if ( AN_AGGREGATE.equals( te) || AN_AGGREGATES.equals( te) ) + aggregatePresent = true; else { msg( Kind.WARNING, te, @@ -491,6 +504,12 @@ else if ( AN_CAST.equals( te) || AN_CASTS.equals( te) ) processRepeatable( e, AN_CAST, AN_CASTS, CastImpl.class); + if ( aggregatePresent ) + for ( Element e + : re.getElementsAnnotatedWithAny( AN_AGGREGATE, AN_AGGREGATES) ) + processRepeatable( + e, AN_AGGREGATE, AN_AGGREGATES, AggregateImpl.class); + tmpr.workAroundJava7Breakage(); // perhaps to be fixed in Java 9? nope. if ( ! re.processingOver() ) @@ -3041,6 +3060,637 @@ public String[] undeployStrings() } } + class AggregateImpl + extends Repeatable + implements Aggregate, Snippet, Commentable + { + AggregateImpl(Element e, AnnotationMirror am) + { + super(e, am); + } + + public String[] name() { return qstrings(qname); } + public String[] arguments() { return argsOut(aggregateArgs); } + public String[] directArguments() { return argsOut(directArgs); } + public boolean hypothetical() { return _hypothetical; } + public boolean[] variadic() { return _variadic; } + public Plan plan() { return _plan; } + public Plan[] movingPlan() { return _movingPlan; } + public Function.Parallel parallel() { return _parallel; } + public String[] provides() { return _provides; } + public String[] requires() { return _requires; } + + public boolean _hypothetical; + public boolean[] _variadic = {false, false}; + public Plan _plan; + public Plan[] _movingPlan; + public Function.Parallel _parallel; + public String[] _provides; + public String[] _requires; + + Identifier.Qualified qname; + List> aggregateArgs; + List> directArgs; + static final int DIRECT_ARGS = 0; // index into _variadic[] + static final int AGG_ARGS = 1; // likewise + boolean directVariadicExplicit; + + private List> + argsIn(String[] names) + { + return Arrays.stream(names) + .map(DBType::fromNameAndType) + .collect(toList()); + } + + private String[] + argsOut(List> names) + { + return names.stream() + .map(e -> e.getKey() + " " + e.getValue()) + .toArray(String[]::new); + } + + @Override + public String derivedComment( Element e) + { + /* + * For the time being, this annotation targets a TYPE, just as a + * place to hang it, and there's no particular reason to believe a + * doc comment on the type has anything to do with this aggregate. + */ + return null; + } + + public void setName( Object o, boolean explicit, Element e) + { + qname = qnameFrom(avToArray( o, String.class)); + } + + public void setArguments( Object o, boolean explicit, Element e) + { + aggregateArgs = argsIn( avToArray( o, String.class)); + } + + public void setDirectArguments( Object o, boolean explicit, Element e) + { + if ( explicit ) + directArgs = argsIn( avToArray( o, String.class)); + } + + public void setVariadic( Object o, boolean explicit, Element e) + { + if ( ! explicit ) + return; + + Boolean[] a = avToArray( o, Boolean.class); + + if ( 1 > a.length || a.length > 2 ) + throw new IllegalArgumentException( + "supply only boolean or {boolean,boolean} for variadic"); + + if ( ! Arrays.asList(a).contains(true) ) + throw new IllegalArgumentException( + "supply variadic= only if aggregated arguments, direct " + + "arguments, or both, are variadic"); + + _variadic[AGG_ARGS] = a[a.length - 1]; + if ( 2 == a.length ) + { + directVariadicExplicit = true; + _variadic[DIRECT_ARGS] = a[0]; + } + } + + public void setPlan( Object o, boolean explicit, Element e) + { + Plan p = new Plan(); + populateAnnotationImpl( p, e, (AnnotationMirror)o); + _plan = p; + } + + public void setMovingPlan( Object o, boolean explicit, Element e) + { + if ( ! explicit ) + return; + + AnnotationMirror[] ams = avToArray( o, AnnotationMirror.class); + + if ( 1 != ams.length ) + throw new IllegalArgumentException( + "movingPlan must be given exactly one @Plan"); + + _movingPlan = new Plan[1]; + + Plan p = new Moving(); + populateAnnotationImpl( p, e, ams[0]); + _movingPlan [ 0 ] = p; + } + + public boolean characterize() + { + boolean ok = true; + boolean orderedSet = null != directArgs; + boolean moving = null != _movingPlan; + + // Must have a name, a plan with an accumulate function, and an + // array of arguments (length 0 for (*)). No check needed here; the + // annotation declares those without defaults. + + // Could check argument count agains FUNC_MAX_ARGS, but that would + // hardcode an assumed value for PostgreSQL's FUNC_MAX_ARGS. + + // Check that, if a stateType is polymorphic, there are compatible + // polymorphic arg types? Not today. + + // If a plan has no initialState, then either the accumulate + // function must NOT be RETURNS NULL ON NULL INPUT, or the first + // aggregated argument type must be the same as the state type. + // The type check is easy, but the returnsNull check on the + // accumulate function would require looking up the function (and + // still we wouldn't know, if it's not seen in this compilation). + // For another day. + + // Allow hypothetical only for ordered-set aggregate. + if ( _hypothetical && ! orderedSet ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "hypothetical=true is only allowed for an ordered-set " + + "aggregate (one with directArguments specified, " + + "even if only {})"); + ok = false; + } + + // Allow two-element variadic= only for ordered-set aggregate. + if ( directVariadicExplicit && ! orderedSet ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Two values for variadic= are only allowed for an " + + "ordered-set aggregate (one with directArguments " + + "specified, even if only {})"); + ok = false; + } + + // Require a movingPlan to have a remove function. + if ( moving && null == _movingPlan[0].remove ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "a movingPlan must include a remove function"); + ok = false; + } + + // Checks if the aggregated argument list is declared variadic. + // The last element must be an array type or "any"; an ordered-set + // aggregate allows only one argument and it must be "any". + if ( _variadic[AGG_ARGS] ) + { + if ( 1 > aggregateArgs.size() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "To declare the aggregated argument list variadic, " + + "there must be at least one argument."); + ok = false; + } + else + { + DBType t = + aggregateArgs.get(aggregateArgs.size() - 1).getValue(); + boolean isAny = // allow omission of pg_catalog namespace + DT_ANY.equals(t) || "\"any\"".equals(t.toString()); + if ( orderedSet && (! isAny || 1 != aggregateArgs.size()) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "If variadic, an ordered-set aggregate's " + + "aggregated argument list must be only one " + + "argument and of type \"any\"."); + ok = false; + } + else if ( ! isAny && ! t.isArray() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "If variadic, the last aggregated argument must " + + "be an array type (or \"any\")."); + ok = false; + } + } + } + + // Checks specific to ordered-set aggregates. + if ( orderedSet ) + { + if ( 0 == aggregateArgs.size() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "An ordered-set aggregate needs at least one " + + "aggregated argument"); + ok = false; + } + + // Checks specific to hypothetical-set aggregates. + // The aggregated argument types must match the trailing direct + // arguments, and the two variadic declarations must match. + if ( _hypothetical ) + { + if ( _variadic[DIRECT_ARGS] != _variadic[AGG_ARGS] ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "For a hypothetical-set aggregate, neither or " + + "both the direct and aggregated argument lists " + + "must be declared variadic."); + ok = false; + } + if ( directArgs.size() < aggregateArgs.size() + || + ! directArgs.subList( + directArgs.size() - aggregateArgs.size(), + directArgs.size()) + .equals(aggregateArgs) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "The last direct arguments of a hypothetical-set " + + "aggregate must match the types of the " + + "aggregated arguments"); + ok = false; + } + } + } + + // It is allowed to omit a finisher function, but some things + // make no sense without one. + if ( orderedSet && null == _plan.finish && 0 < directArgs.size() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Direct arguments serve no purpose without a finisher"); + ok = false; + } + + if ( null == _plan.finish && _plan._polymorphic ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "The polymorphic flag is meaningless with no finisher"); + ok = false; + } + + // The same finisher checks for a movingPlan, if present. + if ( moving ) + { + if ( orderedSet + && null == _movingPlan[0].finish + && directArgs.size() > 0 ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Direct arguments serve no purpose without a finisher"); + ok = false; + } + + if ( null == _movingPlan[0].finish + && _movingPlan[0]._polymorphic ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "The polymorphic flag is meaningless with no finisher"); + ok = false; + } + } + + if ( ! ok ) + return false; + + Set requires = requireTags(); + + DBType[] accumulatorSig = + Stream.of( + Stream.of(_plan.stateType), + aggregateArgs.stream().map(Map.Entry::getValue)) + .flatMap(identity()).toArray(DBType[]::new); + + DBType[] combinerSig = { _plan.stateType, _plan.stateType }; + + DBType[] finisherSig = + Stream.of( + Stream.of(_plan.stateType), + orderedSet + ? directArgs.stream().map(Map.Entry::getValue) + : Stream.of(), + _plan._polymorphic + ? aggregateArgs.stream().map(Map.Entry::getValue) + : Stream.of() + ) + .flatMap(identity()) + .toArray(DBType[]::new); + + requires.add( + new DependTag.Function(_plan.accumulate, accumulatorSig)); + + if ( null != _plan.combine ) + requires.add( + new DependTag.Function(_plan.combine, combinerSig)); + + if ( null != _plan.finish ) + requires.add( + new DependTag.Function(_plan.finish, finisherSig)); + + if ( moving ) + { + accumulatorSig = + Stream.of( + Stream.of(_movingPlan[0].stateType), + aggregateArgs.stream().map(Map.Entry::getValue)) + .flatMap(identity()).toArray(DBType[]::new); + + combinerSig = new DBType[] + { _movingPlan[0].stateType, _movingPlan[0].stateType }; + + finisherSig = + Stream.of( + Stream.of(_movingPlan[0].stateType), + orderedSet ? + directArgs.stream().map(Map.Entry::getValue) + : Stream.of(), + _movingPlan[0]._polymorphic + ? aggregateArgs.stream().map(Map.Entry::getValue) + : Stream.of() + ) + .flatMap(identity()) + .toArray(DBType[]::new); + + requires.add(new DependTag.Function( + _movingPlan[0].accumulate, accumulatorSig)); + + requires.add(new DependTag.Function( + _movingPlan[0].remove, accumulatorSig)); + + if ( null != _movingPlan[0].combine ) + requires.add(new DependTag.Function( + _movingPlan[0].combine, combinerSig)); + + if ( null != _movingPlan[0].finish ) + requires.add(new DependTag.Function( + _movingPlan[0].finish, finisherSig)); + } + + /* + * That establishes dependency on the various support functions, + * which should, transitively, depend on all of the types. But it is + * possible we do not have a whole-program view (perhaps some + * support functions are implemented in other languages, and there + * are @SQLActions setting them up?). Therefore also, redundantly as + * it may be, declare dependency on the types. + */ + + Stream.of( + aggregateArgs.stream().map(Map.Entry::getValue), + orderedSet + ? directArgs.stream().map(Map.Entry::getValue) + : Stream.of(), + Stream.of(_plan.stateType), + moving + ? Stream.of(_movingPlan[0].stateType) + : Stream.of() + ) + .flatMap(identity()) + .map(DBType::dependTag) + .filter(Objects::nonNull) + .forEach(requires::add); + + recordExplicitTags(_provides, _requires); + return true; + } + + public String[] deployStrings() + { + List al = new ArrayList<>(); + + StringBuilder sb = new StringBuilder("CREATE AGGREGATE "); + appendNameAndArguments(sb); + sb.append(" ("); + + String[] planStrings = _plan.deployStrings(); + int n = planStrings.length; + for ( String s : planStrings ) + { + sb.append("\n\t").append(s); + if ( 0 < -- n ) + sb.append(','); + } + + if ( null != _movingPlan ) + { + planStrings = _movingPlan[0].deployStrings(); + for ( String s : planStrings ) + sb.append(",\n\tM").append(s); + } + + if ( Function.Parallel.UNSAFE != _parallel ) + sb.append(",\n\tPARALLEL = ").append(_parallel); + + if ( _hypothetical ) + sb.append(",\n\tHYPOTHETICAL"); + + sb.append(')'); + + al.add(sb.toString()); + + if ( null != comment() ) + { + sb = new StringBuilder("COMMENT ON AGGREGATE "); + appendNameAndArguments(sb); + sb.append(" IS ").append(DDRWriter.eQuote(comment())); + al.add(sb.toString()); + } + + return al.toArray( new String [ al.size() ]); + } + + public String[] undeployStrings() + { + StringBuilder sb = new StringBuilder("DROP AGGREGATE "); + appendNameAndArguments(sb); + return new String[] { sb.toString() }; + } + + private void appendNameAndArguments(StringBuilder sb) + { + ListIterator> iter; + Map.Entry entry; + + sb.append(qname).append('('); + if ( null != directArgs ) + { + iter = directArgs.listIterator(); + while ( iter.hasNext() ) + { + entry = iter.next(); + sb.append("\n\t"); + if ( _variadic[DIRECT_ARGS] && ! iter.hasNext() ) + sb.append("VARIADIC "); + if ( null != entry.getKey() ) + sb.append(entry.getKey()).append(' '); + sb.append(entry.getValue()); + if ( iter.hasNext() ) + sb.append(','); + else + sb.append("\n\t"); + } + sb.append("ORDER BY"); + } + else if ( 0 == aggregateArgs.size() ) + sb.append('*'); + + iter = aggregateArgs.listIterator(); + while ( iter.hasNext() ) + { + entry = iter.next(); + sb.append("\n\t"); + if ( _variadic[AGG_ARGS] && ! iter.hasNext() ) + sb.append("VARIADIC "); + if ( null != entry.getKey() ) + sb.append(entry.getKey()).append(' '); + sb.append(entry.getValue()); + if ( iter.hasNext() ) + sb.append(','); + } + sb.append(')'); + } + + class Plan extends AbstractAnnotationImpl implements Aggregate.Plan + { + public String stateType() { return stateType.toString(); } + public int stateSize() { return _stateSize; } + public String initialState() { return _initialState; } + public String[] accumulate() { return qstrings(accumulate); } + public String[] combine() { return qstrings(combine); } + public String[] finish() { return qstrings(finish); } + public String[] remove() { return qstrings(remove); } + public boolean polymorphic() { return _polymorphic; } + public FinishEffect finishEffect() { return _finishEffect; } + + public int _stateSize; + public String _initialState; + public boolean _polymorphic; + public FinishEffect _finishEffect; + + DBType stateType; + Identifier.Qualified accumulate; + Identifier.Qualified combine; + Identifier.Qualified finish; + Identifier.Qualified remove; + + public void setStateType(Object o, boolean explicit, Element e) + { + stateType = DBType.fromSQLTypeAnnotation((String)o); + } + + public void setStateSize(Object o, boolean explicit, Element e) + { + _stateSize = (Integer)o; + if ( explicit && 0 >= _stateSize ) + throw new IllegalArgumentException( + "An explicit stateSize must be positive"); + } + + public void setInitialState(Object o, boolean explicit, Element e) + { + if ( explicit ) + _initialState = (String)o; + } + + public void setAccumulate(Object o, boolean explicit, Element e) + { + if ( explicit ) + accumulate = qnameFrom(avToArray( o, String.class)); + } + + public void setCombine(Object o, boolean explicit, Element e) + { + if ( explicit ) + combine = qnameFrom(avToArray( o, String.class)); + } + + public void setFinish(Object o, boolean explicit, Element e) + { + if ( explicit ) + finish = qnameFrom(avToArray( o, String.class)); + } + + public void setRemove(Object o, boolean explicit, Element e) + { + if ( explicit ) + throw new IllegalArgumentException( + "Only a movingPlan may have a remove function"); + } + + public void setFinishEffect( Object o, boolean explicit, Element e) + { + if ( explicit ) + _finishEffect = FinishEffect.valueOf( + ((VariableElement)o).getSimpleName().toString()); + } + + public boolean characterize() + { + return false; + } + + /** + * Returns one string per plan element (not per SQL statement). + *

    + * This method has to be here anyway because the class extends + * {@code AbstractAnnotationImpl}, but it will never be processed as + * an actual SQL snippet. This will be called by the containing + * {@code AggregateImpl} and return the individual plan elements + * that it will build into its own deploy strings. + *

    + * When this class represents a moving plan, the caller will prefix + * each of these strings with {@code M}. + */ + public String[] deployStrings() + { + List al = new ArrayList<>(); + + al.add("STYPE = " + stateType); + + if ( 0 != _stateSize ) + al.add("SSPACE = " + _stateSize); + + if ( null != _initialState ) + al.add("INITCOND = " + DDRWriter.eQuote(_initialState)); + + al.add("SFUNC = " + accumulate); + + if ( null != remove ) + al.add("INVFUNC = " + remove); + + if ( null != finish ) + al.add("FINALFUNC = " + finish); + + if ( _polymorphic ) + al.add("FINALFUNC_EXTRA"); + + if ( null != _finishEffect ) + al.add("FINALFUNC_MODIFY = " + _finishEffect); + + if ( null != combine ) + al.add("COMBINEFUNC = " + combine); + + return al.toArray( new String [ al.size() ]); + } + + public String[] undeployStrings() + { + return null; + } + } + + class Moving extends Plan + { + public void setRemove(Object o, boolean explicit, Element e) + { + if ( explicit ) + remove = qnameFrom(avToArray( o, String.class)); + } + } + } + /** * Provides the default mappings from Java types to SQL types. */ @@ -3515,6 +4165,36 @@ Identifier.Qualified qnameFrom(String name) { return Identifier.Qualified.nameFromJava(name, msgr); } + + /** + * Return an {@code Identifier.Qualified} from an array of Java strings + * representing schema and local name separately if of length two, or as by + * {@link #qnameFrom(String)} if of length one; invalid if of any other + * length. + *

    + * The first of two elements may be explicitly {@code ""} to produce a + * qualified name with null qualifier. + */ + Identifier.Qualified qnameFrom(String[] names) + { + switch ( names.length ) + { + case 2: return qnameFrom(names[1], names[0]); + case 1: return qnameFrom(names[0]); + default: + throw new IllegalArgumentException( + "Only a one- or two-element String array is accepted"); + } + } + + String[] qstrings(Identifier.Qualified qname) + { + if ( null == qname ) + return null; + Identifier.Simple q = qname.qualifier(); + String local = qname.local().toString(); + return new String[] { null == q ? null : q.toString(), local }; + } } /** From 4021f3a18a6e06ad4f87cec80a685008da4e28f8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 30 Oct 2020 00:21:33 -0400 Subject: [PATCH 0807/1087] Add some example aggregates --- .../pljava/example/annotation/Aggregates.java | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java new file mode 100644 index 00000000..76e16b9c --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2020 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 static java.lang.Math.fma; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.postgresql.pljava.annotation.Aggregate; +import org.postgresql.pljava.annotation.Function; +import static + org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL; +import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; +import org.postgresql.pljava.annotation.SQLAction; + +/** + * A class demonstrating several aggregate functions. + *

    + * They are (some of) the same two-variable statistical aggregates already + * offered in core PostgreSQL, just because they make clear examples. For + * numerical reasons, they might not produce results identical to PG's built-in + * ones. These closely follow the "schoolbook" formulas in the HP-11C calculator + * owner's handbook, while the ones built into PostgreSQL use a more clever + * algorithm instead to reduce rounding error in the finishers. + *

    + * All these aggregates can be computed by different finishers that share a + * state that accumulates the count of rows, sum of x, sum of xx, sum of y, sum + * of yy, and sum of xy. That is easy with finishers that don't need to modify + * the state, so the default {@code FinishEffect=READ_ONLY} is appropriate. + *

    + * Everything here takes the y parameter first, then x, like the SQL ones. + */ +@SQLAction(requires = { "avgx", "avgy", "slope", "intercept" }, install = { + "WITH" + + " data (y, x) AS (VALUES" + + " (1.761 ::float8, 5.552::float8)," + + " (1.775, 5.963)," + + " (1.792, 6.135)," + + " (1.884, 6.313)," + + " (1.946, 6.713)" + + " )," + + " expected (avgx, avgy, slope, intercept) AS (" + + " SELECT 6.1352, 1.8316, 0.1718, 0.7773" + + " )," + + " got AS (" + + " SELECT" + + " round( avgx(y,x)::numeric, 4) AS avgx," + + " round( avgy(y,x)::numeric, 4) AS avgy," + + " round( slope(y,x)::numeric, 4) AS slope," + + " round(intercept(y,x)::numeric, 4) AS intercept" + + " FROM" + + " data" + + " )" + + "SELECT" + + " CASE WHEN expected IS NOT DISTINCT FROM got" + + " THEN javatest.logmessage('INFO', 'aggregate examples ok')" + + " ELSE javatest.logmessage('WARNING', 'aggregate examples ng')" + + " END" + + " FROM" + + " expected, got" +}) +@Aggregate(provides = "avgx", + name = "avgx", + arguments = { "y double precision", "x double precision" }, + plan = @Aggregate.Plan( + stateType = "double precision[]", + stateSize = 82, + initialState = "{0,0,0,0,0,0}", + accumulate = { "javatest", "accumulateXY" }, + finish = { "javatest", "finishAvgX" } + ) +) +@Aggregate(provides = "avgy", + name = "avgy", + arguments = { "y double precision", "x double precision" }, + plan = @Aggregate.Plan( + stateType = "double precision[]", + stateSize = 82, + initialState = "{0,0,0,0,0,0}", + accumulate = { "javatest", "accumulateXY" }, + finish = { "javatest", "finishAvgY" } + ) +) +@Aggregate(provides = "slope", + name = "slope", + arguments = { "y double precision", "x double precision" }, + plan = @Aggregate.Plan( + stateType = "double precision[]", + stateSize = 82, + initialState = "{0,0,0,0,0,0}", + accumulate = { "javatest", "accumulateXY" }, + finish = { "javatest", "finishSlope" } + ) +) +@Aggregate(provides = "intercept", + name = "intercept", + arguments = { "y double precision", "x double precision" }, + plan = @Aggregate.Plan( + stateType = "double precision[]", + stateSize = 82, + initialState = "{0,0,0,0,0,0}", + accumulate = { "javatest", "accumulateXY" }, + finish = { "javatest", "finishIntercept" } + ) +) +@Aggregate( + name = "regression", + arguments = { "y double precision", "x double precision" }, + plan = @Aggregate.Plan( + stateType = "double precision[]", + stateSize = 82, + initialState = "{0,0,0,0,0,0}", + accumulate = { "javatest", "accumulateXY" }, + finish = { "javatest", "finishRegr" } + ), + /* + * There is no special reason for this aggregate and not the others to have + * a movingPlan; one example is enough, that's all. + */ + movingPlan = @Aggregate.Plan( + stateType = "double precision[]", + stateSize = 82, + initialState = "{0,0,0,0,0,0}", + accumulate = { "javatest", "accumulateXY" }, + remove = { "javatest", "removeXY" }, + finish = { "javatest", "finishRegr" } + ) +) +public class Aggregates +{ + private static final int N = 0; + private static final int SX = 1; + private static final int SXX = 2; + private static final int SY = 3; + private static final int SYY = 4; + private static final int SXY = 5; + + /** + * A common accumulator for two-variable statistical aggregates that + * depend on n, Sx, Sxx, Sy, Syy, and Sxy. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static double[] accumulateXY(double[] state, double y, double x) + { + state[N ] += 1.; + state[SX ] += x; + state[SXX] = fma(x, x, state[2]); + state[SY ] += y; + state[SYY] = fma(y, y, state[4]); + state[SXY] = fma(x, y, state[5]); + return state; + } + + /** + * 'Removes' from the state a row previously accumulated, for possible use + * in a window with a moving frame start. + *

    + * This can be a numerically poor idea for exactly the reasons covered in + * the PostgreSQL docs involving loss of significance in long sums, but it + * does demonstrate the idea. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static double[] removeXY(double[] state, double y, double x) + { + state[N ] -= 1.; + state[SX ] -= x; + state[SXX] = fma(x, -x, state[2]); + state[SY ] -= y; + state[SYY] = fma(y, -y, state[4]); + state[SXY] = fma(x, -y, state[5]); + return state; + } + + /** + * Finisher that returns the count of non-null rows accumulated. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static long finishCount(double[] state) + { + return (long)state[N]; + } + + /** + * Finisher that returns the mean of the accumulated x values. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static Double finishAvgX(double[] state) + { + if ( 0. == state[N] ) + return null; + return state[SX] / state[N]; + } + + /** + * Finisher that returns the mean of the accumulated y values. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static Double finishAvgY(double[] state) + { + if ( 0. == state[N] ) + return null; + return state[SY] / state[N]; + } + + /** + * Finisher that returns the slope of a regression line. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static Double finishSlope(double[] state) + { + if ( 2. > state[N] ) + return null; + + double numer = fma(state[SX], -state[SY], state[N] * state[SXY]); + double denom = fma(state[SX], -state[SX], state[N] * state[SXX]); + return 0. == denom ? null : numer / denom; + } + + /** + * Finisher that returns the intercept of a regression line. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static Double finishIntercept(double[] state) + { + if ( 2 > state[N] ) + return null; + + double numer = fma(state[SY], state[SXX], -state[SX] * state[SXY]); + double denom = fma(state[SX], -state[SX], state[N] * state[SXX]); + return 0. == denom ? null : numer / denom; + } + + /** + * A finisher that returns the slope and intercept together. + *

    + * An aggregate can be built over this finisher and will return a record + * result, but at present (PG 13) access to that record by field doesn't + * work, as its tuple descriptor gets lost along the way. Unclear so far + * whether it might be feasible to fix that. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL, + out = { "slope double precision", "intercept double precision" } + ) + public static boolean finishRegr(double[] state, ResultSet out) + throws SQLException + { + out.updateObject(1, finishSlope(state)); + out.updateObject(2, finishIntercept(state)); + return true; + } +} From 70217a9b008a237fbfdf8f96e4423c673975d5d0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 31 Oct 2020 12:33:56 -0400 Subject: [PATCH 0808/1087] Simplify slightly and improve examples The only difference between the function signatures from plan to movingPlan is a possibly different stateType; no need to re-create the whole signature when it is a mutable array. (Yes, a clone is taken in DependTag.Function().) Examples had a mistake in the stateSize value and did not explain how to get it. --- .../annotation/processing/DDRProcessor.java | 26 ++----------- .../pljava/example/annotation/Aggregates.java | 37 ++++++++++++++++--- 2 files changed, 35 insertions(+), 28 deletions(-) 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 b948fa63..80c64683 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 @@ -3197,7 +3197,7 @@ public boolean characterize() // array of arguments (length 0 for (*)). No check needed here; the // annotation declares those without defaults. - // Could check argument count agains FUNC_MAX_ARGS, but that would + // Could check argument count against FUNC_MAX_ARGS, but that would // hardcode an assumed value for PostgreSQL's FUNC_MAX_ARGS. // Check that, if a stateType is polymorphic, there are compatible @@ -3391,27 +3391,9 @@ else if ( ! isAny && ! t.isArray() ) if ( moving ) { - accumulatorSig = - Stream.of( - Stream.of(_movingPlan[0].stateType), - aggregateArgs.stream().map(Map.Entry::getValue)) - .flatMap(identity()).toArray(DBType[]::new); - - combinerSig = new DBType[] - { _movingPlan[0].stateType, _movingPlan[0].stateType }; - - finisherSig = - Stream.of( - Stream.of(_movingPlan[0].stateType), - orderedSet ? - directArgs.stream().map(Map.Entry::getValue) - : Stream.of(), - _movingPlan[0]._polymorphic - ? aggregateArgs.stream().map(Map.Entry::getValue) - : Stream.of() - ) - .flatMap(identity()) - .toArray(DBType[]::new); + accumulatorSig[0] = _movingPlan[0].stateType; + Arrays.fill(combinerSig, _movingPlan[0].stateType); + finisherSig[0] = _movingPlan[0].stateType; requires.add(new DependTag.Function( _movingPlan[0].accumulate, accumulatorSig)); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java index 76e16b9c..83964133 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java @@ -74,7 +74,32 @@ arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", - stateSize = 82, + /* + * State size is merely a hint to PostgreSQL's planner and can + * be omitted. Perhaps it is worth hinting, as the state type + * "double precision[]" does not tell PostgreSQL how large the array + * might be. Anyway, this is an example and should show how to do it. + * For this aggregate, the state never grows; the size of the initial + * value is the size forever. + * + * To get a quick sense of the size, one can assign the initial state + * as the default for a table column, then consult the pg_node_tree for + * the attribute default entry: + * + * CREATE TEMPORARY TABLE + * foo (bar DOUBLE PRECISION[] DEFAULT '{0,0,0,0,0,0}'); + * + * SELECT + * xpath('/CONST/member[@name="constvalue"]/@length', + * javatest.pgNodeTreeAsXML(adbin) ) + * FROM pg_attrdef + * WHERE adrelid = 'foo'::regclass; + * + * In this case the 72 that comes back represents 48 bytes for six + * float8s, plus 24 for varlena and array overhead, with no null bitmap + * because no element is null. + */ + stateSize = 72, initialState = "{0,0,0,0,0,0}", accumulate = { "javatest", "accumulateXY" }, finish = { "javatest", "finishAvgX" } @@ -85,7 +110,7 @@ arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", - stateSize = 82, + stateSize = 72, initialState = "{0,0,0,0,0,0}", accumulate = { "javatest", "accumulateXY" }, finish = { "javatest", "finishAvgY" } @@ -96,7 +121,7 @@ arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", - stateSize = 82, + stateSize = 72, initialState = "{0,0,0,0,0,0}", accumulate = { "javatest", "accumulateXY" }, finish = { "javatest", "finishSlope" } @@ -107,7 +132,7 @@ arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", - stateSize = 82, + stateSize = 72, initialState = "{0,0,0,0,0,0}", accumulate = { "javatest", "accumulateXY" }, finish = { "javatest", "finishIntercept" } @@ -118,7 +143,7 @@ arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", - stateSize = 82, + stateSize = 72, initialState = "{0,0,0,0,0,0}", accumulate = { "javatest", "accumulateXY" }, finish = { "javatest", "finishRegr" } @@ -129,7 +154,7 @@ */ movingPlan = @Aggregate.Plan( stateType = "double precision[]", - stateSize = 82, + stateSize = 72, initialState = "{0,0,0,0,0,0}", accumulate = { "javatest", "accumulateXY" }, remove = { "javatest", "removeXY" }, From fb27817325949d786f5ae9a460ac55a4df4038e1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 6 Nov 2020 20:50:44 -0500 Subject: [PATCH 0809/1087] Allow Aggregate annotation placed on a method It is worthwhile after all to allow the annotation to go on a method, either the accumulate method (from which the arguments, state type, and a default name can all be inferred) or the finish method (useful in cases where several finishers share an accumulate method and state). A concise, expressive notation results. Add a couple new examples demonstrating that, and fix the names of the others, which had the schema qualification left off. --- .../pljava/annotation/Aggregate.java | 41 +++- .../annotation/processing/DDRProcessor.java | 231 ++++++++++++++++-- .../pljava/example/annotation/Aggregates.java | 50 +++- 3 files changed, 285 insertions(+), 37 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java index 2e377804..5003ae9c 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java @@ -82,7 +82,7 @@ * @author Chapman Flack */ @Documented -@Target(ElementType.TYPE) +@Target({ElementType.TYPE,ElementType.METHOD}) @Repeatable(Aggregate.Container.class) @Retention(RetentionPolicy.CLASS) public @interface Aggregate @@ -135,7 +135,7 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * (both argument types for {@code combine}) and also, if there is no * {@code finish} function, the result type of the aggregate. */ - String stateType(); + String stateType() default ""; /** * An optional estimate of the size in bytes that the state may grow @@ -164,7 +164,7 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * {@code arguments}. It does not receive the {@code directArguments}, * if any. */ - String[] accumulate(); + String[] accumulate() default {}; /** * Name of an optional function to combine two instances of the state @@ -245,8 +245,19 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * schema-qualified name. In the explicit form, {@code ""} as the schema * will make the name explicitly unqualified (in case the local name might * contain a dot and be misread as a qualified name). + *

    + * When this annotation is not placed on a method, there is no default, and + * a name must be supplied. When the annotation is on a method (which can be + * either the {@code accumulate} or the {@code finish} function for the + * aggregate), the default name will be the same as the SQL name given for + * the function. That is typically possible because the parameter signature + * for the aggregate function will not be the same as either the + * {@code accumulate} or the {@code finish} function. The exception is if + * the annotation is on the {@code finish} function and the aggregate has + * exactly one parameter of the same type as the state; in that case another + * name must be given here. */ - String[] name(); + String[] name() default {}; /** * Names and types of the arguments to be aggregated: the ones passed to the @@ -257,8 +268,14 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * name, only a type. The name is an ordinary SQL identifier; if it would * be quoted in SQL, naturally each double-quote must be represented as * {@code \"} in Java. + *

    + * When this annotation does not appear on a method, there is no default, + * and arguments must be declared here. If the annotation appears on a + * method supplying the {@code accumulate} function, this element can be + * omitted, and the arguments will be those of the function (excepting the + * first one, which corresponds to the state). */ - String[] arguments(); + String[] arguments() default {}; /** * Names and types of the "direct arguments" to an ordered-set or @@ -310,10 +327,13 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * except possibly in a moving-window context if {@code movingPlan} is also * supplied. *

    - * Required. This plan may not name a {@code remove} function; only - * a {@code movingPlan} can do that. + * Though declared as an array, only one plan is allowed here. It may not + * name a {@code remove} function; only a {@code movingPlan} can do that. + * This plan can be omitted only if the {@code @Aggregate} annotation + * appears on a Java method intended as the {@code accumulate} function and + * the rest of the plan is all to be inferred or defaulted. */ - Plan plan(); + Plan[] plan() default {}; /** @@ -364,8 +384,9 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; String implementor() default ""; /** - * A comment to be associated with the aggregate. The default is to create - * no comment. + * A comment to be associated with the aggregate. The default is no comment + * if the annotation does not appear on a method, or the first sentence of + * the method's Javadoc comment, if any, if it does. */ String comment() default ""; 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 80c64683..ec0c99a8 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 @@ -2059,9 +2059,7 @@ void appendParams( { -- count; - String name = null == i.st ? null : i.st.name(); - if ( null == name ) - name = i.ve.getSimpleName().toString(); + String name = i.name(); sb.append("\n\t"); @@ -3074,7 +3072,7 @@ class AggregateImpl public String[] directArguments() { return argsOut(directArgs); } public boolean hypothetical() { return _hypothetical; } public boolean[] variadic() { return _variadic; } - public Plan plan() { return _plan; } + public Plan[] plan() { return new Plan[]{_plan}; } public Plan[] movingPlan() { return _movingPlan; } public Function.Parallel parallel() { return _parallel; } public String[] provides() { return _provides; } @@ -3088,6 +3086,7 @@ class AggregateImpl public String[] _provides; public String[] _requires; + FunctionImpl func; Identifier.Qualified qname; List> aggregateArgs; List> directArgs; @@ -3115,21 +3114,26 @@ class AggregateImpl public String derivedComment( Element e) { /* - * For the time being, this annotation targets a TYPE, just as a - * place to hang it, and there's no particular reason to believe a - * doc comment on the type has anything to do with this aggregate. + * When this annotation targets a TYPE, just as a + * place to hang it, there's no particular reason to believe a + * doc comment on the type is a good choice for this aggregate. + * When the annotation is on a method, the chances are better. */ + if ( ElementKind.METHOD.equals(e.getKind()) ) + return super.derivedComment(e); return null; } public void setName( Object o, boolean explicit, Element e) { - qname = qnameFrom(avToArray( o, String.class)); + if ( explicit ) + qname = qnameFrom(avToArray( o, String.class)); } public void setArguments( Object o, boolean explicit, Element e) { - aggregateArgs = argsIn( avToArray( o, String.class)); + if ( explicit ) + aggregateArgs = argsIn( avToArray( o, String.class)); } public void setDirectArguments( Object o, boolean explicit, Element e) @@ -3164,9 +3168,10 @@ public void setVariadic( Object o, boolean explicit, Element e) public void setPlan( Object o, boolean explicit, Element e) { - Plan p = new Plan(); - populateAnnotationImpl( p, e, (AnnotationMirror)o); - _plan = p; + _plan = new Plan(); // always a plan, even if members uninitialized + + if ( explicit ) + _plan = planFrom( _plan, o, e, "plan"); } public void setMovingPlan( Object o, boolean explicit, Element e) @@ -3174,17 +3179,20 @@ public void setMovingPlan( Object o, boolean explicit, Element e) if ( ! explicit ) return; + _movingPlan = new Plan[1]; + _movingPlan [ 0 ] = planFrom( new Moving(), o, e, "movingPlan"); + } + + Plan planFrom( Plan p, Object o, Element e, String which) + { AnnotationMirror[] ams = avToArray( o, AnnotationMirror.class); if ( 1 != ams.length ) throw new IllegalArgumentException( - "movingPlan must be given exactly one @Plan"); + which + " must be given exactly one @Plan"); - _movingPlan = new Plan[1]; - - Plan p = new Moving(); populateAnnotationImpl( p, e, ams[0]); - _movingPlan [ 0 ] = p; + return p; } public boolean characterize() @@ -3192,10 +3200,162 @@ public boolean characterize() boolean ok = true; boolean orderedSet = null != directArgs; boolean moving = null != _movingPlan; + boolean checkAccumulatorSig = false; + boolean checkFinisherSig = false; + + if ( ElementKind.METHOD.equals(m_targetElement.getKind()) ) + { + func = getSnippet(m_targetElement, FunctionImpl.class, + () -> (FunctionImpl)null); + if ( null == func ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "A method annotated with @Aggregate must " + + "also have @Function" + ); + ok = false; + } + } + + if ( null != func ) + { + Identifier.Qualified funcName = + qnameFrom(func.name(), func.schema()); + boolean inferAccumulator = + null == _plan.accumulate || null == aggregateArgs; + boolean inferFinisher = + null == _plan.finish && ! inferAccumulator; + + if ( null == qname ) + { + + if ( inferFinisher && 1 == aggregateArgs.size() + && 1 == func.parameterTypes.length + && func.parameterTypes[0] == + aggregateArgs.get(0).getValue() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Default name %s for this aggregate would " + + "collide with finish function; use name= to " + + "specify a name", funcName + ); + ok = false; + } + else + qname = funcName; + } + + if ( 1 > func.parameterTypes.length ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Function with no arguments cannot be @Aggregate " + + "accumulate or finish function" + ); + ok = false; + } + else if ( null == _plan.stateType ) + { + _plan.stateType = func.parameterTypes[0]; + if (null != _movingPlan + && null == _movingPlan[0].stateType) + _movingPlan[0].stateType = func.parameterTypes[0]; + } + + if ( inferAccumulator || inferFinisher ) + { + if ( ok ) + { + if ( inferAccumulator ) + { + if ( null == aggregateArgs ) + { + aggregateArgs = + func.parameterInfo() + .skip(1) // skip the state argument + .map(pi -> + (Map.Entry) + new AbstractMap.SimpleImmutableEntry( + Identifier.Simple.fromJava( + pi.name() + ), + pi.dt + ) + ) + .collect(toList()); + } + _plan.accumulate = funcName; + if ( null != _movingPlan + && null == _movingPlan[0].accumulate ) + _movingPlan[0].accumulate = funcName; + } + else // inferFinisher + { + _plan.finish = funcName; + if ( null != _movingPlan + && null == _movingPlan[0].finish ) + _movingPlan[0].finish = funcName; + } + } + } + else if ( funcName.equals(_plan.accumulate) ) + checkAccumulatorSig = true; + else if ( funcName.equals(_plan.finish) ) + checkFinisherSig = true; + else + { + msg(Kind.WARNING, m_targetElement, m_origin, + "@Aggregate annotation on a method not recognized " + + "as either the accumulate or the finish function " + + "for the aggregate"); + } + + // If the method is the accumulator and is RETURNS_NULL, ensure + // there is either an initialState or a first aggregate arg that + // matches the stateType. + if ( ok && ( inferAccumulator || checkAccumulatorSig ) ) + { + if ( Function.OnNullInput.RETURNS_NULL == func.onNullInput() + && ( 0 == aggregateArgs.size() + || ! _plan.stateType.equals( + aggregateArgs.get(0).getValue()) ) + && null == _plan._initialState ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Aggregate without initialState= must have " + + "either a first argument matching the stateType " + + "or an accumulate method with onNullInput=CALLED."); + ok = false; + } + } + } + + if ( null == qname ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Aggregate missing name="); + ok = false; + } - // Must have a name, a plan with an accumulate function, and an - // array of arguments (length 0 for (*)). No check needed here; the - // annotation declares those without defaults. + if ( null == aggregateArgs ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Aggregate missing arguments="); + ok = false; + } + + if ( null == _plan.stateType ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Aggregate missing stateType="); + ok = false; + } + + if ( null == _plan.accumulate ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Aggregate plan missing accumulate="); + ok = false; + } // Could check argument count against FUNC_MAX_ARGS, but that would // hardcode an assumed value for PostgreSQL's FUNC_MAX_ARGS. @@ -3378,6 +3538,26 @@ else if ( ! isAny && ! t.isArray() ) .flatMap(identity()) .toArray(DBType[]::new); + if ( checkAccumulatorSig + && ! Arrays.equals(accumulatorSig, func.parameterTypes) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Aggregate annotation on a method that matches the name " + + "but not argument types expected for the aggregate's " + + "accumulate function"); + ok = false; + } + + if ( checkFinisherSig + && ! Arrays.equals(finisherSig, func.parameterTypes) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Aggregate annotation on a method that matches the name " + + "but not argument types expected for the aggregate's " + + "finish function"); + ok = false; + } + requires.add( new DependTag.Function(_plan.accumulate, accumulatorSig)); @@ -3559,7 +3739,8 @@ class Plan extends AbstractAnnotationImpl implements Aggregate.Plan public void setStateType(Object o, boolean explicit, Element e) { - stateType = DBType.fromSQLTypeAnnotation((String)o); + if ( explicit ) + stateType = DBType.fromSQLTypeAnnotation((String)o); } public void setStateSize(Object o, boolean explicit, Element e) @@ -5325,6 +5506,14 @@ class ParameterInfo final SQLType st; final DBType dt; + String name() + { + String name = null == st ? null : st.name(); + if ( null == name ) + name = ve.getSimpleName().toString(); + return name; + } + ParameterInfo(TypeMirror m, VariableElement e, SQLType t, DBType d) { tm = m; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java index 83964133..500d18bd 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Aggregates.java @@ -70,7 +70,7 @@ " expected, got" }) @Aggregate(provides = "avgx", - name = "avgx", + name = { "javatest", "avgx" }, arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", @@ -106,7 +106,7 @@ ) ) @Aggregate(provides = "avgy", - name = "avgy", + name = { "javatest", "avgy" }, arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", @@ -117,7 +117,7 @@ ) ) @Aggregate(provides = "slope", - name = "slope", + name = { "javatest", "slope" }, arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", @@ -128,7 +128,7 @@ ) ) @Aggregate(provides = "intercept", - name = "intercept", + name = { "javatest", "intercept" }, arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", @@ -139,7 +139,7 @@ ) ) @Aggregate( - name = "regression", + name = "javatest.regression", arguments = { "y double precision", "x double precision" }, plan = @Aggregate.Plan( stateType = "double precision[]", @@ -163,6 +163,8 @@ ) public class Aggregates { + private Aggregates() { } // do not instantiate + private static final int N = 0; private static final int SX = 1; private static final int SXX = 2; @@ -212,11 +214,31 @@ public static double[] removeXY(double[] state, double y, double x) /** * Finisher that returns the count of non-null rows accumulated. + *

    + * As an alternative to collecting all {@code @Aggregate} annotations up at + * the top of the class and specifying everything explicitly, an + * {@code @Aggregate} annotation can be placed on a method, either + * the accumulator or the finisher, in which case less needs to be + * specified. The state type can always be determined from the annotated + * method (whether it is the accumulator or the finisher), and its SQL name + * will be the default name for the aggregate also. When the method is the + * accumulator, the aggregate's arguments are also determined. + *

    + * This being a finisher method, the {@code @Aggregate} annotation placed + * here does need to specify the arguments, initial state, and accumulator. */ + @Aggregate( + arguments = { "y double precision", "x double precision" }, + plan = @Aggregate.Plan( + stateSize = 72, + initialState = "{0,0,0,0,0,0}", + accumulate = { "javatest", "accumulateXY" } + ) + ) @Function( schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL ) - public static long finishCount(double[] state) + public static long count(double[] state) { return (long)state[N]; } @@ -298,4 +320,20 @@ public static boolean finishRegr(double[] state, ResultSet out) out.updateObject(2, finishIntercept(state)); return true; } + + /** + * An example aggregate that sums its input. + *

    + * The simplest kind of aggregate, having only an accumulate function, + * default initial state, and no finisher (the state value is the return) + * can be declared very concisely by annotating the accumulate method. + */ + @Aggregate + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static double sum(double state, double x) + { + return state + x; + } } From fc96933d523018dac1894c52271d4ae1171bb6e6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 31 Oct 2020 18:13:46 -0400 Subject: [PATCH 0810/1087] Add an Operator annotation --- .../pljava/annotation/Operator.java | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java new file mode 100644 index 00000000..b33422a6 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares a PostgreSQL {@code OPERATOR}. + *

    + * May annotate a Java method (which should also carry a + * {@link Function @Function} annotation, making it a PostgreSQL function), + * or a class or interface (just to have a place to put it when not directly + * annotating a method). + * + * @author Chapman Flack + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +@Repeatable(Operator.Container.class) +@Retention(RetentionPolicy.CLASS) +public @interface Operator +{ + /** + * Name for this operator. + *

    + * May be specified in explicit {@code {"schema","operatorname"}} form, or + * as a single string that will be leniently parsed as an optionally + * schema-qualified name. In the explicit form, {@code ""} as the schema + * will make the name explicitly unqualified. + */ + String[] name(); + + /** + * The type of the operator's left operand, if any. + * Will default to the first parameter type of an associated two-parameter + * function, or none for an associated one-parameter function. + */ + String left() default ""; + + /** + * The type of the operator's right operand, if any. + * Will default to the second parameter type of an associated two-parameter + * function, or the parameter type for an associated one-parameter function. + */ + String right() default ""; + + /** + * Name of the function backing the operator; may be omitted if this + * annotation appears on a method. + *

    + * The function named here must take one parameter of the matching type if + * only one of {@code left} or {@code right} is specified, or the + * {@code left} and {@code right} types in that order if both are present. + */ + String[] function() default {}; + + /** + * Name of an operator that is the commutator of this one. + *

    + * Specified in the same ways as {@code name}. + */ + String[] commutator() default {}; + + /** + * Name of an operator that is the negator of this one. + *

    + * Specified in the same ways as {@code name}. + */ + String[] negator() default {}; + + /** + * Whether this operator can be used in computing a hash join. + *

    + * Only sensible for a boolean-valued binary operator, which must have a + * commutator in the same hash index operator family, with the underlying + * functions marked {@link Function.Effects#IMMUTABLE} or + * {@link Function.Effects#STABLE}. + */ + boolean hashes() default false; + + /** + * Whether this operator can be used in computing a merge join. + *

    + * Only sensible for a boolean-valued binary operator, which must have a + * commutator also appearing as an equality member in the same btree index + * operator family, with the underlying functions marked + * {@link Function.Effects#IMMUTABLE} or {@link Function.Effects#STABLE}. + */ + boolean merges() default false; + + /** + * Name of a function that can estimate the selectivity of this operator + * when used in a {@code WHERE} clause. + *

    + * Specified in the same ways as {@code function}. + *

    + * A custom estimator is a complex undertaking (and, at present, requires + * a language other than Java), but several predefined ones can be found in + * {@link SelectivityEstimators}. + */ + String[] restrict() default {}; + + /** + * Name of a function that can estimate the selectivity of this operator + * when used in a join. + *

    + * Specified in the same ways as {@code function}. + *

    + * A custom estimator is a complex undertaking (and, at present, requires + * a language other than Java), but several predefined ones can be found in + * {@link SelectivityEstimators}. + */ + String[] join() default {}; + + /** + * One or more arbitrary labels that will be considered 'provided' by the + * object carrying this annotation. The deployment descriptor will be + * 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. + */ + String[] provides() default {}; + + /** + * One or more arbitrary labels that will be considered 'required' by the + * object carrying this annotation. The deployment descriptor will be + * generated in such an order that other objects that 'provide' labels + * 'required' by this come earlier in the output for install actions, and + * later for remove actions. + */ + String[] requires() default {}; + + /** + * The {@code } to be used around SQL code generated + * for this operator. Defaults to {@code PostgreSQL}. Set explicitly to + * {@code ""} to emit code not wrapped in an {@code }. + */ + String implementor() default ""; + + /** + * A comment to be associated with the operator. If left to default, and the + * annotated Java construct has a doc comment, its first sentence will be + * used. If an empty string is explicitly given, no comment will be set. + */ + String comment() default ""; + + /** + * Names of several functions predefined in PostgreSQL for estimating the + * selectivity of operators in restriction clauses or joins. + */ + interface SelectivityEstimators + { + /** + * A restriction-selectivity estimator suitable for an operator + * with rather high selectivity typical of an operator like {@code =}. + */ + String EQSEL = "pg_catalog.eqsel"; + + /** + * A restriction-selectivity estimator suitable for an operator + * somewhat less strict than a typical {@code =} operator. + */ + String MATCHINGSEL = "pg_catalog.matchingsel"; + + /** + * A restriction-selectivity estimator suitable for an operator + * with rather low selectivity typical of an operator like {@code <>}. + */ + String NEQSEL = "pg_catalog.neqsel"; + + /** + * A restriction-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code <}. + */ + String SCALARLTSEL = "pg_catalog.scalarltsel"; + + /** + * A restriction-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code <=}. + */ + String SCALARLESEL = "pg_catalog.scalarlesel"; + + /** + * A restriction-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code >}. + */ + String SCALARGTSEL = "pg_catalog.scalargtsel"; + + /** + * A restriction-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code >=}. + */ + String SCALARGESEL = "pg_catalog.scalargesel"; + + /** + * A join-selectivity estimator suitable for an operator + * with rather high selectivity typical of an operator like {@code =}. + */ + String EQJOINSEL = "pg_catalog.eqjoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * somewhat less strict than a typical {@code =} operator. + */ + String MATCHINGJOINSEL = "pg_catalog.matchingjoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * with rather low selectivity typical of an operator like {@code <>}. + */ + String NEQJOINSEL = "pg_catalog.neqjoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code <}. + */ + String SCALARLTJOINSEL = "pg_catalog.scalarltjoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code <=}. + */ + String SCALARLEJOINSEL = "pg_catalog.scalarlejoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code >}. + */ + String SCALARGTJOINSEL = "pg_catalog.scalargtjoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * with selectivity typical of an operator like {@code >=}. + */ + String SCALARGEJOINSEL = "pg_catalog.scalargejoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * doing 2-D area-based comparisons. + */ + String AREAJOINSEL = "pg_catalog.areajoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * doing 2-D position-based comparisons. + */ + String POSITIONJOINSEL = "pg_catalog.positionjoinsel"; + + /** + * A join-selectivity estimator suitable for an operator + * doing 2-D containment-based comparisons. + */ + String CONTJOINSEL = "pg_catalog.contjoinsel"; + } + + /** + * @hidden container type allowing Operator to be repeatable. + */ + @Documented + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.CLASS) + @interface Container + { + Operator[] value(); + } +} From 8781ffaf220306e7a40e3d1fde3e34495e042a62 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 3 Nov 2020 21:01:08 -0500 Subject: [PATCH 0811/1087] Teach SQL generator to produce CREATE OPERATOR --- .../annotation/processing/DDRProcessor.java | 496 ++++++++++++++++++ 1 file changed, 496 insertions(+) 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 ec0c99a8..85437542 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 @@ -112,6 +112,7 @@ import org.postgresql.pljava.annotation.Aggregate; import org.postgresql.pljava.annotation.Cast; import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.Operator; import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLActions; import org.postgresql.pljava.annotation.SQLType; @@ -231,6 +232,8 @@ class DDRProcessorImpl final TypeElement AN_CASTS; final TypeElement AN_AGGREGATE; final TypeElement AN_AGGREGATES; + final TypeElement AN_OPERATOR; + final TypeElement AN_OPERATORS; // Certain familiar DBTypes (capitalized as this file historically has) // @@ -338,6 +341,9 @@ class DDRProcessorImpl AN_AGGREGATE = elmu.getTypeElement( Aggregate.class.getName()); AN_AGGREGATES = elmu.getTypeElement( Aggregate.Container.class.getCanonicalName()); + AN_OPERATOR = elmu.getTypeElement( Operator.class.getName()); + AN_OPERATORS = elmu.getTypeElement( + Operator.Container.class.getCanonicalName()); } void msg( Kind kind, String fmt, Object... args) @@ -452,6 +458,7 @@ boolean process( Set tes, RoundEnvironment re) boolean mappedUDTPresent = false; boolean castPresent = false; boolean aggregatePresent = false; + boolean operatorPresent = false; boolean willClaim = true; @@ -471,6 +478,8 @@ else if ( AN_CAST.equals( te) || AN_CASTS.equals( te) ) castPresent = true; else if ( AN_AGGREGATE.equals( te) || AN_AGGREGATES.equals( te) ) aggregatePresent = true; + else if ( AN_OPERATOR.equals( te) || AN_OPERATORS.equals( te) ) + operatorPresent = true; else { msg( Kind.WARNING, te, @@ -504,6 +513,12 @@ else if ( AN_AGGREGATE.equals( te) || AN_AGGREGATES.equals( te) ) processRepeatable( e, AN_CAST, AN_CASTS, CastImpl.class); + if ( operatorPresent ) + for ( Element e + : re.getElementsAnnotatedWithAny( AN_OPERATOR, AN_OPERATORS) ) + processRepeatable( + e, AN_OPERATOR, AN_OPERATORS, OperatorImpl.class); + if ( aggregatePresent ) for ( Element e : re.getElementsAnnotatedWithAny( AN_AGGREGATE, AN_AGGREGATES) ) @@ -3058,6 +3073,424 @@ public String[] undeployStrings() } } + class OperatorImpl + extends Repeatable + implements Operator, Snippet, Commentable + { + OperatorImpl(Element e, AnnotationMirror am) + { + super(e, am); + } + + public String[] name() { return qstrings(qname); } + public String left() { return operand(0); } + public String right() { return operand(1); } + public String[] function() { return qstrings(funcName); } + public String[] commutator() { return qstrings(commutator); } + public String[] negator() { return qstrings(negator); } + public boolean hashes() { return _hashes; } + public boolean merges() { return _merges; } + public String[] restrict() { return qstrings(restrict); } + public String[] join() { return qstrings(join); } + public String[] provides() { return _provides; } + public String[] requires() { return _requires; } + + public String[] _provides; + public String[] _requires; + public boolean _hashes; + public boolean _merges; + + Identifier.Qualified qname; + DBType[] operands = { null, null }; + FunctionImpl func; + Identifier.Qualified funcName; + Identifier.Qualified commutator; + Identifier.Qualified negator; + Identifier.Qualified restrict; + Identifier.Qualified join; + + private String operand(int i) + { + return null == operands[i] ? null : operands[i].toString(); + } + + public void setName( Object o, boolean explicit, Element e) + { + qname = operatorNameFrom(avToArray( o, String.class)); + } + + public void setLeft( Object o, boolean explicit, Element e) + { + if ( explicit ) + operands[0] = DBType.fromSQLTypeAnnotation((String)o); + } + + public void setRight( Object o, boolean explicit, Element e) + { + if ( explicit ) + operands[1] = DBType.fromSQLTypeAnnotation((String)o); + } + + public void setFunction( Object o, boolean explicit, Element e) + { + if ( explicit ) + funcName = qnameFrom(avToArray( o, String.class)); + } + + public void setCommutator( Object o, boolean explicit, Element e) + { + if ( explicit ) + commutator = operatorNameFrom(avToArray( o, String.class)); + } + + public void setNegator( Object o, boolean explicit, Element e) + { + if ( explicit ) + negator = operatorNameFrom(avToArray( o, String.class)); + } + + public void setRestrict( + Object o, boolean explicit, Element e) + { + if ( explicit ) + restrict = qnameFrom(avToArray( o, String.class)); + } + + public void setJoin( + Object o, boolean explicit, Element e) + { + if ( explicit ) + join = qnameFrom(avToArray( o, String.class)); + } + + public boolean characterize() + { + boolean ok = true; + + if ( ElementKind.METHOD.equals(m_targetElement.getKind()) ) + { + func = getSnippet(m_targetElement, FunctionImpl.class, + () -> (FunctionImpl)null); + } + + if ( null == func && null == funcName ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator not annotating a method must specify function=" + ); + ok = false; + } + + if ( null == func ) + { + if ( null == operands[0] && null == operands[1] ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator not annotating a method must specify " + + "left= or right= or both" + ); + ok = false; + } + } + else + { + Identifier.Qualified fn = + qnameFrom(func.name(), func.schema()); + + if ( null == funcName ) + funcName = fn; + else if ( ! funcName.equals(fn) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator annotates a method but function= gives a " + + "different name" + ); + ok = false; + } + + long explicit = + Arrays.stream(operands).filter(Objects::nonNull).count(); + + if ( 0 == explicit ) + { + int nparams = func.parameterTypes.length; + if ( 1 > nparams || nparams > 2 ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "method annotated with @Operator must take one " + + "or two parameters" + ); + ok = false; + } + if ( 1 == nparams ) + operands[1] = func.parameterTypes[0]; + else + System.arraycopy(func.parameterTypes,0, operands,0,2); + } + else if ( explicit != func.parameterTypes.length ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator annotates a method but specifies " + + "a different number of operands" + ); + ok = false; + } + else if ( 2 == explicit + && ! Arrays.equals(operands, func.parameterTypes) + || 1 == explicit + && ! Arrays.asList(operands) + .contains(func.parameterTypes[0]) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator annotates a method but specifies " + + "different operand types" + ); + ok = false; + } + } + + /* + * At this point, ok ==> there is a non-null funcName + */ + + if ( ! ok ) + return false; + + long arity = + Arrays.stream(operands).filter(Objects::nonNull).count(); + + if ( 1 == arity && null == operands[1] ) + { + msg(Kind.WARNING, m_targetElement, m_origin, + "Right unary (postfix) operators are deprecated and will " + + "be removed in PostgreSQL version 14." + ); + } + + if ( null != commutator ) + { + if ( 2 != arity ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "unary @Operator cannot have a commutator" + ); + ok = false; + } + } + + boolean knownNotBoolean = + null != func && ! DT_BOOLEAN.equals(func.returnType); + + if ( null != negator ) + { + if ( knownNotBoolean ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "negator= only belongs on a boolean @Operator" + ); + ok = false; + } + else if ( negator.equals(qname) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator can never be its own negator" + ); + ok = false; + } + } + + boolean knownNotBinaryBoolean = 2 != arity || knownNotBoolean; + boolean knownVolatile = + null != func && Function.Effects.VOLATILE == func.effects(); + boolean operandTypesDiffer = + 2 == arity && ! operands[0].equals(operands[1]); + boolean selfCommutates = + null != commutator && commutator.equals(qname); + + ok &= Stream.of( + _hashes ? "hashes" : null, + _merges ? "merges" : null) + .filter(Objects::nonNull) + .map(s -> + { + boolean inner_ok = true; + if ( knownNotBinaryBoolean ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "%s= only belongs on a boolean " + + "binary @Operator", s + ); + inner_ok = false; + } + if ( null == commutator ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "%s= requires that the @Operator " + + "have a commutator", s + ); + inner_ok = false; + } + else if ( ! (operandTypesDiffer || selfCommutates) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "%s= requires the @Operator to be its own" + + "commutator as its operand types are the same", s + ); + inner_ok = false; + } + if ( knownVolatile ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "%s= requires an underlying function " + + "declared IMMUTABLE or STABLE", s + ); + inner_ok = false; + } + return inner_ok; + }) + .allMatch(t -> t); + + if ( null != restrict && knownNotBinaryBoolean ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "restrict= only belongs on a boolean binary @Operator" + ); + ok = false; + } + + if ( null != join && knownNotBinaryBoolean ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "join= only belongs on a boolean binary @Operator" + ); + ok = false; + } + + if ( ! ok ) + return false; + + recordImplicitTags(); + recordExplicitTags(_provides, _requires); + return true; + } + + void recordImplicitTags() + { + Set provides = provideTags(); + Set requires = requireTags(); + + provides.add(new DependTag.Operator(qname, operands)); + + /* + * Commutator and negator often involve cycles. PostgreSQL already + * has its own means of breaking them, so it is not necessary here + * even to declare dependencies based on them. + * + * There is also, for now, no point in declaring dependencies on + * selectivity estimators; they can't be written in Java, so they + * won't be products of this compilation. + * + * So, just require the operand types and the function. + */ + + Arrays.stream(operands) + .filter(Objects::nonNull) + .map(DBType::dependTag) + .filter(Objects::nonNull) + .forEach(requires::add); + + if ( null != func ) + { + func.provideTags().stream() + .filter(DependTag.Function.class::isInstance) + .forEach(requires::add); + } + else + { + requires.add(new DependTag.Function(funcName, + Arrays.stream(operands) + .filter(Objects::nonNull) + .toArray(DBType[]::new))); + } + } + + /** + * Just to keep things interesting, a schema-qualified operator name is + * wrapped in OPERATOR(...) pretty much everywhere, except as the guest + * of honor in a CREATE OPERATOR or DROP OPERATOR, where the unwrapped + * form is needed. + */ + private String qnameUnwrapped() + { + String local = qname.local().toString(); + Identifier.Simple qualifier = qname.qualifier(); + return null == qualifier ? local : qualifier + "." + local; + } + + /** + * An operator is identified this way in a COMMENT or DROP. + */ + private String commentDropForm() + { + return qnameUnwrapped() + " (" + + (null == operands[0] ? "NONE" : operands[0]) + ", " + + (null == operands[1] ? "NONE" : operands[1]) + ")"; + } + + public String[] deployStrings() + { + List al = new ArrayList<>(); + + StringBuilder sb = new StringBuilder(); + + sb.append("CREATE OPERATOR ").append(qnameUnwrapped()); + sb.append(" (\n\tPROCEDURE = ").append(funcName); + + if ( null != operands[0] ) + sb.append(",\n\tLEFTARG = ").append(operands[0]); + + if ( null != operands[1] ) + sb.append(",\n\tRIGHTARG = ").append(operands[1]); + + if ( null != commutator ) + sb.append(",\n\tCOMMUTATOR = ").append(commutator); + + if ( null != negator ) + sb.append(",\n\tNEGATOR = ").append(negator); + + if ( null != restrict ) + sb.append(",\n\tRESTRICT = ").append(restrict); + + if ( null != join ) + sb.append(",\n\tJOIN = ").append(join); + + if ( _hashes ) + sb.append(",\n\tHASHES"); + + if ( _merges ) + sb.append(",\n\tMERGES"); + + sb.append(')'); + + al.add(sb.toString()); + + if ( null != comment() ) + al.add( + "COMMENT ON OPERATOR " + commentDropForm() + " IS " + + DDRWriter.eQuote(comment())); + + return al.toArray( new String [ al.size() ]); + } + + public String[] undeployStrings() + { + return new String[] + { + "DROP OPERATOR " + commentDropForm() + }; + } + } + class AggregateImpl extends Repeatable implements Aggregate, Snippet, Commentable @@ -4350,6 +4783,27 @@ Identifier.Qualified qnameFrom(String[] names) } } + /** + * Like {@link #qnameFrom(String[])} but for an operator name. + */ + Identifier.Qualified operatorNameFrom(String[] names) + { + switch ( names.length ) + { + case 2: + Identifier.Simple qualifier = null; + if ( ! names[0].isEmpty() ) + qualifier = Identifier.Simple.fromJava(names[0], msgr); + return Identifier.Operator.from(names[1], msgr) + .withQualifier(qualifier); + case 1: + return Identifier.Qualified.operatorFromJava(names[0], msgr); + default: + throw new IllegalArgumentException( + "Only a one- or two-element String array is accepted"); + } + } + String[] qstrings(Identifier.Qualified qname) { if ( null == qname ) @@ -5493,6 +5947,48 @@ public String toString() return super.toString() + Arrays.toString(m_signature); } } + + static final class Operator + extends Named> + { + private DBType[] m_signature; + + Operator( + Identifier.Qualified value, DBType[] signature) + { + super(requireNonNull(value)); + assert 2 == signature.length : "invalid Operator signature length"; + m_signature = signature.clone(); + } + + @Override + public boolean equals(Object o, Messager msgr) + { + if ( ! super.equals(o, msgr) ) + return false; + Operator op = (Operator)o; + if ( m_signature.length != op.m_signature.length ) + return false; + for ( int i = 0; i < m_signature.length; ++ i ) + { + if ( null == m_signature[i] || null == op.m_signature[i] ) + { + if ( m_signature[i] != op.m_signature[i] ) + return false; + continue; + } + if ( ! m_signature[i].equals(op.m_signature[i], msgr) ) + return false; + } + return true; + } + + @Override + public String toString() + { + return super.toString() + Arrays.toString(m_signature); + } + } } /** From ab63d0b6dad71ee57c38c001f356cc1c901e24eb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 31 Oct 2020 21:39:52 -0400 Subject: [PATCH 0812/1087] Add Operator annotation in an example --- .../pljava/example/annotation/ComplexScalar.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 9e643cab..e2e74d19 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -22,6 +22,7 @@ import java.util.logging.Logger; import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.Operator; import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.BaseUDT; @@ -107,6 +108,19 @@ public static ComplexScalar parse(String input, String typeName) public ComplexScalar() { } + /** + * Add two instances of {@code ComplexScalar}. + */ + @Operator(name = {"javatest","+"}, commutator = "javatest.+") + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static ComplexScalar add(ComplexScalar a, ComplexScalar b) + { + return new ComplexScalar( + a.m_x + b.m_x, a.m_y + b.m_y, a.m_typeName); + } + public ComplexScalar(double x, double y, String typeName) { m_x = x; m_y = y; From 1b198701f005d171ad0bf203cb26ec4b711efe6e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 3 Nov 2020 21:24:07 -0500 Subject: [PATCH 0813/1087] Allow a shorthand commutator = SELF ... tedious and mistake-prone to respell the operator's schema and name for this rather common case. --- .../pljava/annotation/Operator.java | 11 +++++++++- .../annotation/processing/DDRProcessor.java | 20 ++++++++++++++++++- .../example/annotation/ComplexScalar.java | 3 ++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java index b33422a6..838d9831 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java @@ -34,6 +34,13 @@ @Retention(RetentionPolicy.CLASS) public @interface Operator { + /** + * Distinguished value usable for {@link #commutator commutator=} to + * indicate that an operator is its own commutator without having to + * repeat its schema and name. + */ + String SELF = "self"; + /** * Name for this operator. *

    @@ -71,7 +78,9 @@ /** * Name of an operator that is the commutator of this one. *

    - * Specified in the same ways as {@code name}. + * Specified in the same ways as {@code name}. The value + * {@link #SELF SELF} can be used to avoid repeating the schema and name + * for the common case of an operator that is its own commutator. */ String[] commutator() default {}; 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 85437542..e7510a07 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 @@ -3108,6 +3108,7 @@ class OperatorImpl Identifier.Qualified negator; Identifier.Qualified restrict; Identifier.Qualified join; + boolean selfCommutator; private String operand(int i) { @@ -3140,7 +3141,13 @@ public void setFunction( Object o, boolean explicit, Element e) public void setCommutator( Object o, boolean explicit, Element e) { if ( explicit ) - commutator = operatorNameFrom(avToArray( o, String.class)); + { + String[] ss = avToArray( o, String.class); + if ( 1 == ss.length && SELF.equals(ss[0]) ) + selfCommutator = true; + else + commutator = operatorNameFrom(ss); + } } public void setNegator( Object o, boolean explicit, Element e) @@ -3167,6 +3174,9 @@ public boolean characterize() { boolean ok = true; + if ( selfCommutator ) + commutator = qname; + if ( ElementKind.METHOD.equals(m_targetElement.getKind()) ) { func = getSnippet(m_targetElement, FunctionImpl.class, @@ -3276,6 +3286,14 @@ else if ( 2 == explicit ); ok = false; } + else if ( selfCommutator && ! operands[0].equals(operands[1]) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator with different left and right operand " + + "types cannot be its own commutator" + ); + ok = false; + } } boolean knownNotBoolean = diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index e2e74d19..77106be5 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -23,6 +23,7 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.Operator; +import static org.postgresql.pljava.annotation.Operator.SELF; import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.BaseUDT; @@ -111,7 +112,7 @@ public ComplexScalar() { /** * Add two instances of {@code ComplexScalar}. */ - @Operator(name = {"javatest","+"}, commutator = "javatest.+") + @Operator(name = {"javatest","+"}, commutator = SELF) @Function( schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL ) From 8bb0ce428433e48c641d5cf25fc30b98ed351ab9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 3 Nov 2020 21:09:36 -0500 Subject: [PATCH 0814/1087] Generate synthetic-function declarations It is often necessary to supply several very similar operators (and the functions behind them), for example to complete a btree operator class. The needed operators are simply related to each other by parameter commutation and/or return-value negation, as already declared with commutator and negator in the Operator annotation. Give the Operator annotation a new element synthetic= that takes a function name. Say there is already @Function(name = "lt") @Operator(name = "<") public static void boolean less(int a, int b) { return a < b; } Allow related operators to be declared in the same place, like this: @Function(name = "lt") @Operator(name = "<", commutator = ">", negator = ">=") @Operator(name = ">", synthetic = "gt", negator = "<=") @Operator(name = ">=", synthetic = "ge", commutator = "<=") @Operator(name = "<=", synthetic = "le") public static void boolean less(int a, int b) { return a < b; } The order of the Operator annotations doesn't matter, only that they include enough commutator/negator information to form a graph (rooted at the 'real' operator) that reaches all the synthetic ones. Back edges are added automatically (so > does not need to specify its commutator, >= does not need to specify its negator, and <= needn't mention either). The deployment descriptor will include CREATE FUNCTION commands for lt, gt, ge, and le, all pointing to the same Java less() method, with the three synthetic ones including a prefix in the AS string indicating to commute, negate, or both. The CREATE OPERATOR commands then use the corresponding named functions in the ordinary way. The PL/Java runtime does not yet know what to do with the extra prefix in a function's AS string. --- .../pljava/annotation/Operator.java | 11 + .../annotation/processing/DDRProcessor.java | 749 ++++++++++++++++-- .../example/annotation/ComplexScalar.java | 50 ++ 3 files changed, 729 insertions(+), 81 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java index 838d9831..f786442b 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java @@ -75,6 +75,17 @@ */ String[] function() default {}; + /** + * Name of a function to be synthesized by PL/Java based on the method this + * annotation appears on and this operator's {@code commutator} or + * {@code negator} relationship to another operator declared on the same + * method. + *

    + * Only allowed in an annotation on a Java method, and where + * {@code function} is not specified. + */ + String[] synthetic() default {}; + /** * Name of an operator that is the commutator of this one. *

    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 e7510a07..6bbacce2 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 @@ -45,6 +45,7 @@ import java.util.Collections; import static java.util.Collections.unmodifiableSet; import java.util.Comparator; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; @@ -63,12 +64,16 @@ import java.util.Queue; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Supplier; import static java.util.function.UnaryOperator.identity; import java.util.stream.Stream; +import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -505,25 +510,26 @@ else if ( AN_OPERATOR.equals( te) || AN_OPERATORS.equals( te) ) for ( Element e : re.getElementsAnnotatedWithAny( AN_SQLACTION, AN_SQLACTIONS) ) processRepeatable( - e, AN_SQLACTION, AN_SQLACTIONS, SQLActionImpl.class); + e, AN_SQLACTION, AN_SQLACTIONS, SQLActionImpl.class, null); if ( castPresent ) for ( Element e : re.getElementsAnnotatedWithAny( AN_CAST, AN_CASTS) ) processRepeatable( - e, AN_CAST, AN_CASTS, CastImpl.class); + e, AN_CAST, AN_CASTS, CastImpl.class, null); if ( operatorPresent ) for ( Element e : re.getElementsAnnotatedWithAny( AN_OPERATOR, AN_OPERATORS) ) processRepeatable( - e, AN_OPERATOR, AN_OPERATORS, OperatorImpl.class); + e, AN_OPERATOR, AN_OPERATORS, OperatorImpl.class, + this::operatorPreSynthesize); if ( aggregatePresent ) for ( Element e : re.getElementsAnnotatedWithAny( AN_AGGREGATE, AN_AGGREGATES) ) processRepeatable( - e, AN_AGGREGATE, AN_AGGREGATES, AggregateImpl.class); + e, AN_AGGREGATE, AN_AGGREGATES, AggregateImpl.class, null); tmpr.workAroundJava7Breakage(); // perhaps to be fixed in Java 9? nope. @@ -549,13 +555,15 @@ void defensiveEarlyCharacterize() { for ( Snippet snip : snippets.values() ) { - if ( ! snip.characterize() ) - continue; - VertexPair v = new VertexPair<>( snip); - snippetVPairs.add( v); - for ( DependTag s : snip.provideTags() ) - if ( null != provider.put( s, v) ) - msg( Kind.ERROR, "tag %s has more than one provider", s); + Set ready = snip.characterize(); + for ( Snippet readySnip : ready ) + { + VertexPair v = new VertexPair<>( readySnip); + snippetVPairs.add( v); + for ( DependTag s : readySnip.provideTags() ) + if ( null != provider.put( s, v) ) + msg(Kind.ERROR, "tag %s has more than one provider", s); + } } snippets.clear(); } @@ -794,20 +802,37 @@ Snippet[] order( } return snips.toArray(new Snippet[snips.size()]); } + + void putRepeatableSnippet(Element e, T snip) + { + if ( null != snip ) + putSnippet( snip, (Snippet)snip); + } /** * Process an element carrying a repeatable annotation, the container * of that repeatable annotation, or both. *

    - * Snippets corresponding to repeatable annotations are not entered in the + * Snippets corresponding to repeatable annotations might not be entered in the * {@code snippets} map keyed by the target element, as that might not be - * unique. Each snippet is entered with a key made from its class and - * itself. They are not expected to be looked up, only processed when all of - * the map entries are enumerated. + * unique. Each populated snippet is passed to putter along with + * the element it annotates, and putter determines what to do with + * it. If putter is null, the default enters the snippet with a key + * made from its class and itself, as typical repeatable snippets are are + * not expected to be looked up, only processed when all of the map entries + * are enumerated. + *

    + * After all snippets of the desired class have been processed for a given + * element, a final call to putter is made passing the element and + * null for the snippet. */ void processRepeatable( - Element e, TypeElement annot, TypeElement container, Class clazz) + Element e, TypeElement annot, TypeElement container, Class clazz, + BiConsumer putter) { + if ( null == putter ) + putter = this::putRepeatableSnippet; + for ( AnnotationMirror am : elmu.getAllAnnotationMirrors( e) ) { Element asElement = am.getAnnotationType().asElement(); @@ -826,16 +851,18 @@ void processRepeatable( "Incorrect implementation of annotation processor", re); } populateAnnotationImpl( snip, e, am); - putSnippet( snip, (Snippet)snip); + putter.accept( e, snip); } else if ( asElement.equals( container) ) { Container c = new Container<>(clazz); populateAnnotationImpl( c, e, am); for ( T snip : c.value() ) - putSnippet( snip, (Snippet)snip); + putter.accept( e, snip); } } + + putter.accept( e, null); } static enum UDTKind { BASE, MAPPED } @@ -1080,6 +1107,7 @@ public Identifier.Simple implementorName() Identifier.Simple _implementor = defaultImplementor; String _comment; + boolean commentDerived; public void setImplementor( Object o, boolean explicit, Element e) { @@ -1106,7 +1134,18 @@ public void setComment( Object o, boolean explicit, Element e) _comment = null; } else + { _comment = ((Commentable)this).derivedComment( e); + commentDerived = true; + } + } + + protected void replaceCommentIfDerived( String comment) + { + if ( ! commentDerived ) + return; + commentDerived = false; + _comment = comment; } public String derivedComment( Element e) @@ -1407,10 +1446,10 @@ class SQLActionImpl public String[] _provides; public String[] _requires; - public boolean characterize() + public Set characterize() { recordExplicitTags(_provides, _requires); - return true; + return Set.of(this); } } @@ -1480,7 +1519,7 @@ public void setConstraint( Object o, boolean explicit, Element e) origin = am; } - public boolean characterize() + public Set characterize() { if ( Scope.ROW.equals( _scope) ) { @@ -1562,7 +1601,7 @@ else if ( Called.INSTEAD_OF.equals( _called) ) if ( "".equals( _name) ) _name = TriggerNamer.synthesizeName( this); - return false; + return Set.of(); } public String[] deployStrings() @@ -1784,7 +1823,7 @@ public void setTriggers( Object o, boolean explicit, Element e) } } - public boolean characterize() + public Set characterize() { if ( "".equals( _name) ) _name = func.getSimpleName().toString(); @@ -1802,7 +1841,7 @@ public boolean characterize() { msg( Kind.ERROR, func, "Unable to resolve return type of function"); - return false; + return Set.of(); } ExecutableType et = (ExecutableType)func.asType(); @@ -1822,7 +1861,7 @@ public boolean characterize() msg( Kind.ERROR, func.getParameters().get( arity - 1), "Last parameter of complex-type-returning function " + "must be ResultSet"); - return false; + return Set.of(); } } else if ( null != (typeArgs = specialization( ret, TY_ITERATOR)) ) @@ -1832,14 +1871,14 @@ else if ( null != (typeArgs = specialization( ret, TY_ITERATOR)) ) { msg( Kind.ERROR, func, "Need one type argument for Iterator return type"); - return false; + return Set.of(); } setofComponent = typeArgs.get( 0); if ( null == setofComponent ) { msg( Kind.ERROR, func, "Failed to find setof component type"); - return false; + return Set.of(); } } else if ( typu.isAssignable( ret, TY_RESULTSETPROVIDER) @@ -1894,7 +1933,7 @@ else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) for ( Trigger t : triggers() ) ((TriggerImpl)t).characterize(); - return true; + return Set.of(this); } void resolveLanguage() @@ -2057,28 +2096,46 @@ public void subsume() void appendNameAndParams( StringBuilder sb, boolean names, boolean outs, boolean dflts) { - sb.append(qnameFrom(name(), schema())).append( '('); - appendParams( sb, names, outs, dflts); + appendNameAndParams(sb, names, outs, dflts, + qnameFrom(name(), schema()), parameterInfo().collect(toList())); + } + + /** + * Internal version taking name and parameter stream as extra arguments + * so they can be overridded from {@link Transformed}. + */ + void appendNameAndParams( + StringBuilder sb, boolean names, boolean outs, boolean dflts, + Identifier.Qualified qname, + Iterable params) + { + sb.append(qname).append( '('); + appendParams( sb, names, outs, dflts, params); // TriggerImpl relies on ) being the very last character sb.append( ')'); } + /** + * Takes the parameter stream as an extra argument + * so it can be overridded from {@link Transformed}. + */ void appendParams( - StringBuilder sb, boolean names, boolean outs, boolean dflts) + StringBuilder sb, boolean names, boolean outs, boolean dflts, + Iterable params) { int lengthOnEntry = sb.length(); - int count = parameterTypes.length; - for ( ParameterInfo i - : (Iterable)parameterInfo()::iterator ) + Iterator iter = params.iterator(); + ParameterInfo i; + while ( iter.hasNext() ) { - -- count; + i = iter.next(); String name = i.name(); sb.append("\n\t"); - if ( _variadic && 0 == count ) + if ( _variadic && ! iter.hasNext() ) sb.append("VARIADIC "); if ( names ) @@ -2103,8 +2160,9 @@ void appendParams( sb.setLength(sb.length() - 1); // that last pesky comma } - void appendAS( StringBuilder sb) + String makeAS() { + StringBuilder sb = new StringBuilder(); if ( ! ( complexViaInOut || setof || trigger ) ) sb.append( typu.erasure( func.getReturnType())).append( '='); Element e = func.getEnclosingElement(); @@ -2114,14 +2172,29 @@ void appendAS( StringBuilder sb) "than a class"); sb.append( e.toString()).append( '.'); sb.append( trigger ? func.getSimpleName() : func.toString()); + return sb.toString(); } public String[] deployStrings() + { + return deployStrings( + qnameFrom(name(), schema()), parameterInfo().collect(toList()), + makeAS(), comment()); + } + + /** + * Internal version taking the function name, parameter stream, + * AS string, and comment (if any) as extra arguments so they can be + * overridden from {@link Transformed}. + */ + String[] deployStrings( + Identifier.Qualified qname, + Iterable params, String as, String comment) { ArrayList al = new ArrayList<>(); StringBuilder sb = new StringBuilder(); sb.append( "CREATE OR REPLACE FUNCTION "); - appendNameAndParams( sb, true, true, true); + appendNameAndParams( sb, true, true, true, qname, params); sb.append( "\n\tRETURNS "); if ( trigger ) sb.append( DT_TRIGGER.toString()); @@ -2149,19 +2222,16 @@ public String[] deployStrings() sb.append( "\tROWS ").append( rows()).append( '\n'); for ( String s : settings() ) sb.append( "\tSET ").append( s).append( '\n'); - sb.append( "\tAS '"); - appendAS( sb); - sb.append( '\''); + sb.append( "\tAS ").append( DDRWriter.eQuote( as)); al.add( sb.toString()); - String comm = comment(); - if ( null != comm ) + if ( null != comment ) { sb.setLength( 0); sb.append( "COMMENT ON FUNCTION "); - appendNameAndParams( sb, true, false, false); + appendNameAndParams( sb, true, false, false, qname, params); sb.append( "\nIS "); - sb.append( DDRWriter.eQuote( comm)); + sb.append( DDRWriter.eQuote( comment)); al.add( sb.toString()); } @@ -2172,6 +2242,14 @@ public String[] deployStrings() } public String[] undeployStrings() + { + return undeployStrings( + qnameFrom(name(), schema()), parameterInfo().collect(toList())); + } + + String[] undeployStrings( + Identifier.Qualified qname, + Iterable params) { if ( subsumed ) return new String[0]; @@ -2184,7 +2262,7 @@ public String[] undeployStrings() StringBuilder sb = new StringBuilder(); sb.append( "DROP FUNCTION "); - appendNameAndParams( sb, true, false, false); + appendNameAndParams( sb, true, false, false, qname, params); rslt [ rslt.length - 1 ] = sb.toString(); return rslt; } @@ -2221,6 +2299,85 @@ List specialization( */ return Collections.emptyList(); } + + class Transformed implements Snippet + { + final Identifier.Qualified m_qname; + final boolean m_commute; + final boolean m_negate; + final String m_comment; + + Transformed( + Identifier.Qualified qname, + boolean commute, boolean negate, String comment) + { + assert commute || negate : "no transformation to apply"; + m_qname = requireNonNull(qname); + m_commute = commute; + m_negate = negate; + m_comment = comment; + } + + List parameterInfo() + { + List params = + FunctionImpl.this.parameterInfo().collect(toList()); + if ( ! m_commute ) + return params; + assert 2 == params.size() : "commute with arity != 2"; + Collections.reverse(params); + return params; + } + + @Override + public Set characterize() + { + return Set.of(); + } + + @Override + public Identifier.Simple implementorName() + { + return FunctionImpl.this.implementorName(); + } + + @Override + public Set requireTags() + { + return FunctionImpl.this.requireTags(); + } + + @Override + public Set provideTags() + { + DBType[] sig = + parameterInfo().stream() + .map(p -> p.dt) + .toArray(DBType[]::new); + return Set.of(new DependTag.Function(m_qname, sig)); + } + + @Override + public String[] deployStrings() + { + String as = Stream.of( + m_commute ? "commute" : (String)null, + m_negate ? "negate" : (String)null) + .filter(Objects::nonNull) + .collect(joining(",", "[", "]")) + + FunctionImpl.this.makeAS(); + + return FunctionImpl.this.deployStrings( + m_qname, parameterInfo(), as, m_comment); + } + + @Override + public String[] undeployStrings() + { + return FunctionImpl.this.undeployStrings( + m_qname, parameterInfo()); + } + } } static enum BaseUDTFunctionID @@ -2292,9 +2449,28 @@ class BaseUDTFunctionImpl extends FunctionImpl TypeElement te; BaseUDTFunctionID id; + @Override + public String[] deployStrings() + { + return deployStrings( + qnameFrom(name(), schema()), + null, // parameter iterable unused in appendParams below + "UDT[" + te + "] " + id.name(), + comment()); + } + + @Override + public String[] undeployStrings() + { + return undeployStrings( + qnameFrom(name(), schema()), + null); // parameter iterable unused in appendParams below + } + @Override void appendParams( - StringBuilder sb, boolean names, boolean outs, boolean dflts) + StringBuilder sb, boolean names, boolean outs, boolean dflts, + Iterable params) { sb.append( Arrays.stream(id.getParam( ui)) @@ -2303,13 +2479,6 @@ void appendParams( ); } - @Override - void appendAS( StringBuilder sb) - { - sb.append( "UDT[").append( te.toString()).append( "] "); - sb.append( id.name()); - } - StringBuilder appendTypeOp( StringBuilder sb) { sb.append( id.name()).append( " = "); @@ -2317,12 +2486,12 @@ StringBuilder appendTypeOp( StringBuilder sb) } @Override - public boolean characterize() + public Set characterize() { resolveLanguage(); recordImplicitTags(); recordExplicitTags(_provides, _requires); - return true; + return Set.of(this); } public void setType( Object o, boolean explicit, Element e) @@ -2478,7 +2647,7 @@ public void registerMapping() setQname(); } - public boolean characterize() + public Set characterize() { if ( null != structure() ) { @@ -2487,7 +2656,7 @@ public boolean characterize() provideTags().add(t); } recordExplicitTags(_provides, _requires); - return true; + return Set.of(this); } public String[] deployStrings() @@ -2559,9 +2728,9 @@ public Set requireTags() } @Override - public boolean characterize() + public Set characterize() { - return false; + return Set.of(); } } @@ -2693,7 +2862,7 @@ void registerFunctions() send); } - public boolean characterize() + public Set characterize() { if ( "".equals( typeModifierInput()) && ! "".equals( typeModifierOutput()) ) @@ -2726,7 +2895,7 @@ public boolean characterize() recordImplicitTags(); recordExplicitTags(_provides, _requires); - return true; + return Set.of(this); } void recordImplicitTags() @@ -2897,7 +3066,7 @@ public void setPath( Object o, boolean explicit, Element e) ((VariableElement)o).getSimpleName().toString()); } - public boolean characterize() + public Set characterize() { boolean ok = true; @@ -2999,11 +3168,11 @@ else if ( null != func ) } if ( ! ok ) - return false; + return Set.of(); recordImplicitTags(); recordExplicitTags(_provides, _requires); - return true; + return Set.of(this); } void recordImplicitTags() @@ -3073,6 +3242,187 @@ public String[] undeployStrings() } } + /* + * Called by processRepeatable for each @Operator processed. + * This happens before characterize, but after populating, so the + * operator's name and commutator/negator/synthetic elements can be + * inspected. All operators annotating a given element e are processed + * consecutively, and followed by a call with the same e and null snip. + * + * This will accumulate the snippets onto two lists, for non-synthetic and + * synthetic ones and, on the final call, process the lists to find possible + * paths from non-synthetic to synthetic ones via commutation and/or + * negation. The possible paths will be recorded on each synthetic operator. + * They will have to be confirmed during characterize after things like + * operand types and arity have been resolved. + */ + void operatorPreSynthesize( Element e, OperatorImpl snip) + { + if ( ! ElementKind.METHOD.equals(e.getKind()) ) + { + if ( null != snip ) + putSnippet( snip, (Snippet)snip); + return; + } + + if ( null != snip ) + { + (null == snip.synthetic ? m_nonSynthetic : m_synthetic).add(snip); + return; + } + + /* + * Initially: + * processed: is empty + * ready: contains all non-synthetic snippets + * pending: contains all synthetic snippets + * Step: + * A snippet s is removed from ready and added to processed. + * If s.commutator or s.negator matches a synthetic snippet in pending + * or ready, a corresponding path is recorded on that snippet. If it is + * the first path recorded on that snippet (which must have been found + * on pending), that snippet is moved to ready. + */ + + List processed = + new ArrayList<>(m_nonSynthetic.size() + m_synthetic.size()); + Queue ready = new LinkedList<>(m_nonSynthetic); + LinkedList pending = new LinkedList<>(m_synthetic); + m_nonSynthetic.clear(); + m_synthetic.clear(); + + while ( null != (snip = ready.poll()) ) + { + processed.add(snip); + if ( null != snip.commutator ) + { + for ( OperatorImpl other : ready ) + maybeAddPath(snip, other, + OperatorPath.Transform.COMMUTATION); + ListIterator it = pending.listIterator(); + while ( it.hasNext() ) + { + OperatorImpl other = it.next(); + if ( maybeAddPath(snip, other, + OperatorPath.Transform.COMMUTATION) ) + { + it.remove(); + ready.add(other); + } + } + } + if ( null != snip.negator ) + { + for ( OperatorImpl other : ready ) + maybeAddPath(snip, other, + OperatorPath.Transform.NEGATION); + ListIterator it = pending.listIterator(); + while ( it.hasNext() ) + { + OperatorImpl other = it.next(); + if ( maybeAddPath(snip, other, + OperatorPath.Transform.NEGATION) ) + { + it.remove(); + ready.add(other); + } + } + } + } + + if ( ! pending.isEmpty() ) + msg(Kind.ERROR, e, "Cannot synthesize operator(s) (%s)", + pending.stream() + .map(o -> o.qname.toString()) + .collect(joining(" "))); + + for ( OperatorImpl s : processed ) + putSnippet( s, (Snippet)s); + } + + boolean maybeAddPath( + OperatorImpl from, OperatorImpl to, OperatorPath.Transform how) + { + if ( null == to.synthetic ) + return false; // don't add paths to a non-synthetic operator + + switch ( how ) + { + case COMMUTATION: + if ( ! from.commutator.equals(to.qname) ) + return false; // this is not the operator you're looking for + if ( null != to.commutator && ! to.commutator.equals(from.qname) ) + return false; // you're not the one it's looking for + break; + case NEGATION: + if ( ! from.negator.equals(to.qname) ) + return false; // move along + if ( null != to.negator && ! to.negator.equals(from.qname) ) + return false; // move along + break; + } + + if ( null == to.paths ) + to.paths = new ArrayList<>(); + + if ( null == from.synthetic ) + to.paths.add(new OperatorPath(from, from, null, EnumSet.of(how))); + else + { + for ( OperatorPath path : from.paths ) + { + to.paths.add(new OperatorPath( + path.base, from, path.fromBase, EnumSet.of(how))); + } + } + + return true; + } + + List m_nonSynthetic = new ArrayList<>(); + List m_synthetic = new ArrayList<>(); + + static class OperatorPath + { + OperatorImpl base; + OperatorImpl proximate; + EnumSet fromBase; + EnumSet fromProximate; + + enum Transform { NEGATION, COMMUTATION } + + OperatorPath( + OperatorImpl base, OperatorImpl proximate, + EnumSet baseToProximate, + EnumSet proximateToNew) + { + this.base = base; + this.proximate = proximate; + fromProximate = proximateToNew.clone(); + + if ( base == proximate ) + fromBase = proximateToNew; + else + { + fromBase = baseToProximate.clone(); + fromBase.removeAll(proximateToNew); + proximateToNew = proximateToNew.clone(); + proximateToNew.removeAll(fromBase); + fromBase.addAll(proximateToNew); + } + } + + public String toString() + { + return + base.commentDropForm() + " " + fromBase + + (base == proximate + ? "" + : " (... " + proximate.commentDropForm() + + " " + fromProximate); + } + } + class OperatorImpl extends Repeatable implements Operator, Snippet, Commentable @@ -3086,6 +3436,7 @@ class OperatorImpl public String left() { return operand(0); } public String right() { return operand(1); } public String[] function() { return qstrings(funcName); } + public String[] synthetic() { return qstrings(synthetic); } public String[] commutator() { return qstrings(commutator); } public String[] negator() { return qstrings(negator); } public boolean hashes() { return _hashes; } @@ -3108,7 +3459,9 @@ class OperatorImpl Identifier.Qualified negator; Identifier.Qualified restrict; Identifier.Qualified join; + Identifier.Qualified synthetic; boolean selfCommutator; + List paths; private String operand(int i) { @@ -3138,6 +3491,12 @@ public void setFunction( Object o, boolean explicit, Element e) funcName = qnameFrom(avToArray( o, String.class)); } + public void setSynthetic( Object o, boolean explicit, Element e) + { + if ( explicit ) + synthetic = qnameFrom(avToArray( o, String.class)); + } + public void setCommutator( Object o, boolean explicit, Element e) { if ( explicit ) @@ -3170,9 +3529,10 @@ public void setJoin( join = qnameFrom(avToArray( o, String.class)); } - public boolean characterize() + public Set characterize() { boolean ok = true; + Snippet syntheticFunction = null; if ( selfCommutator ) commutator = qname; @@ -3183,6 +3543,19 @@ public boolean characterize() () -> (FunctionImpl)null); } + if ( null != synthetic ) + { + if ( null != funcName ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator may not specify both function= and " + + "synthetic=" + ); + ok = false; + } + funcName = synthetic; + } + if ( null == func && null == funcName ) { msg(Kind.ERROR, m_targetElement, m_origin, @@ -3209,7 +3582,7 @@ public boolean characterize() if ( null == funcName ) funcName = fn; - else if ( ! funcName.equals(fn) ) + else if ( ! funcName.equals(fn) && null == synthetic ) { msg(Kind.ERROR, m_targetElement, m_origin, "@Operator annotates a method but function= gives a " + @@ -3221,6 +3594,15 @@ else if ( ! funcName.equals(fn) ) long explicit = Arrays.stream(operands).filter(Objects::nonNull).count(); + if ( 0 != explicit && null != synthetic ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator with synthetic= must not specify " + + "operand types" + ); + ok = false; + } + if ( 0 == explicit ) { int nparams = func.parameterTypes.length; @@ -3264,7 +3646,7 @@ else if ( 2 == explicit */ if ( ! ok ) - return false; + return Set.of(); long arity = Arrays.stream(operands).filter(Objects::nonNull).count(); @@ -3385,11 +3767,213 @@ else if ( ! (operandTypesDiffer || selfCommutates) ) } if ( ! ok ) - return false; + return Set.of(); + + if ( null != synthetic ) + { + if ( null == func ) + { + /* + * It could be possible to relax this requirement if there + * is a need, but this way is easier. + */ + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator annotation must appear " + + "on the method to be used as the base"); + ok = false; + } + + if ( paths.isEmpty() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator %s has no derivation path " + + "involving negation or commutation from another " + + "operator", qnameUnwrapped()); + /* + * If no paths at all, return empty from here; no point in + * further checks. + */ + return Set.of(); + } + + /* + * Check for conditions where deriving by commutation wouldn't + * make sense. Any of these three conditions will trigger the + * test of available paths. The conditions are rechecked but the + * third one is changed, so either of the first two will always + * preclude commutation, but ! operandTypesDiffer only does if + * the synthetic function's name will be the same as the base's. + * (If the types were different, PostgreSQL overloading would + * allow the functions to share a name, but that's not possible + * if the types are the same.) In those cases, any commutation + * paths are filtered out; if no path remains, that's an error. + */ + if ( 2 != arity || selfCommutator || ! operandTypesDiffer ) + { + List filtered = + paths.stream() + .filter( + p -> ! p.fromBase.contains( + OperatorPath.Transform.COMMUTATION)) + .collect(toList()); + if ( 2 != arity || selfCommutator || + synthetic.equals(qnameFrom(func.name(), func.schema()))) + { + if ( filtered.isEmpty() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator %s cannot be another " + + "operator's commutator, but found only " + + "path(s) involving commutation: %s", + qnameUnwrapped(), paths.toString()); + ok = false; + } + else + paths = filtered; + } + } + + ok &= paths.stream().collect( + groupingBy(p -> p.base, + mapping(p -> p.fromBase, toSet()))) + .entrySet().stream() + .filter(e -> 1 < e.getValue().size()) + .map(e -> + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator %s found paths with " + + "different transforms %s from same base %s", + qnameUnwrapped(), + e.getValue(), e.getKey().qnameUnwrapped()); + return false; + }) + .allMatch(t -> t); + + ok &= paths.stream().collect( + groupingBy(p -> p.proximate, + mapping(p -> p.fromProximate, toSet()))) + .entrySet().stream() + .filter(e -> 1 < e.getValue().size()) + .map(e -> + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator %s found paths with " + + "different transforms %s from %s", + qnameUnwrapped(), + e.getValue(), e.getKey().qnameUnwrapped()); + return false; + }) + .allMatch(t -> t); + + Set> + commutatorCandidates = + paths.stream() + .filter( + p -> p.fromProximate.contains( + OperatorPath.Transform.COMMUTATION)) + .map(p -> p.proximate.qname) + .collect(toSet()); + if ( null == commutator && 0 < commutatorCandidates.size() ) + { + if ( 1 == commutatorCandidates.size() ) + commutator = commutatorCandidates.iterator().next(); + else + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator %s has muliple commutator " + + "candidates %s", + qnameUnwrapped(), commutatorCandidates); + ok = false; + } + } + + Set> + negatorCandidates = + paths.stream() + .filter( + p -> p.fromProximate.contains( + OperatorPath.Transform.NEGATION)) + .map(p -> p.proximate.qname) + .collect(toSet()); + if ( null == negator && 0 < negatorCandidates.size() ) + { + if ( 1 == negatorCandidates.size() ) + negator = negatorCandidates.iterator().next(); + else + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator %s has muliple negator " + + "candidates %s", + qnameUnwrapped(), negatorCandidates); + ok = false; + } + } + + /* + * Filter paths to only those based on an operator that is built + * over this method. (That's currently guaranteed by the way + * operatorPreSynthesize generates paths, but may as well check + * here to ensure sanity during future maintenance.) + */ + + paths = paths.stream() + .filter(p -> p.base.func == func).collect(toList()); + + if ( 0 == paths.size() ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Synthetic operator %s has no derivation path " + + "from an operator that is based on this method", + qnameUnwrapped()); + ok = false; + } + + if ( ! ok ) + return Set.of(); + + /* + * Select a base. Could there be more than one? As the checks + * for transform inconsistencies above found none, we will + * assume any should be ok, and choose one semi-arbitrarily. + */ + + OperatorPath selected = + paths.stream() + .sorted( + Comparator.comparingInt( + p -> p.fromBase.size()) + .thenComparingInt( + p -> p.fromBase.stream() + .mapToInt(Enum::ordinal) + .max().getAsInt()) + .thenComparing(p -> p.base.qnameUnwrapped())) + .findFirst().get(); + + replaceCommentIfDerived("Operator " + qnameUnwrapped() + + " automatically derived by " + + selected.fromBase + " from " + + selected.base.qnameUnwrapped()); + + boolean commute = selected.fromBase + .contains(OperatorPath.Transform.COMMUTATION); + boolean negate = selected.fromBase + .contains(OperatorPath.Transform.NEGATION); + + if ( operandTypesDiffer && commute ) + { + DBType t = operands[0]; + operands[0] = operands[1]; + operands[1] = t; + } + + syntheticFunction = + func.new Transformed(synthetic, commute, negate, comment()); + } recordImplicitTags(); recordExplicitTags(_provides, _requires); - return true; + return null == syntheticFunction + ? Set.of(this) : Set.of(syntheticFunction, this); } void recordImplicitTags() @@ -3417,7 +4001,7 @@ void recordImplicitTags() .filter(Objects::nonNull) .forEach(requires::add); - if ( null != func ) + if ( null != func && null == synthetic ) { func.provideTags().stream() .filter(DependTag.Function.class::isInstance) @@ -3491,7 +4075,6 @@ public String[] deployStrings() sb.append(')'); al.add(sb.toString()); - if ( null != comment() ) al.add( "COMMENT ON OPERATOR " + commentDropForm() + " IS " + @@ -3646,7 +4229,7 @@ Plan planFrom( Plan p, Object o, Element e, String which) return p; } - public boolean characterize() + public Set characterize() { boolean ok = true; boolean orderedSet = null != directArgs; @@ -3964,7 +4547,7 @@ else if ( ! isAny && ! t.isArray() ) } if ( ! ok ) - return false; + return Set.of(); Set requires = requireTags(); @@ -4066,7 +4649,7 @@ else if ( ! isAny && ! t.isArray() ) .forEach(requires::add); recordExplicitTags(_provides, _requires); - return true; + return Set.of(this); } public String[] deployStrings() @@ -4240,9 +4823,9 @@ public void setFinishEffect( Object o, boolean explicit, Element e) ((VariableElement)o).getSimpleName().toString()); } - public boolean characterize() + public Set characterize() { - return false; + return Set.of(); } /** @@ -4897,10 +5480,14 @@ default DependTag implementorTag() * undeployStrings() can be called. May also check for and report semantic * errors that are not easily checked earlier while populating the * element/value pairs. - * @return true if this Snippet is standalone and should be scheduled and - * emitted based on provides/requires; false if something else will emit it. + * @return A set of snippets that are now prepared and should be added to + * the graph to be scheduled and emitted according to provides/requires. + * Typically Set.of(this) if all went well, or Set.of() in case of an error + * or when the snippet will be emitted by something else. In some cases a + * characterize method can return additional snippets that are ready to be + * scheduled. */ - public boolean characterize(); + public Set characterize(); /** * If it is possible to break an ordering cycle at this snippet, return a @@ -5172,7 +5759,7 @@ class ImpProvider implements Snippet @Override public String[] undeployStrings() { return s.deployStrings(); } @Override public Set provideTags() { return s.provideTags(); } @Override public Set requireTags() { return s.requireTags(); } - @Override public boolean characterize() { return s.characterize(); } + @Override public Set characterize() { return s.characterize(); } } /** diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 77106be5..89413881 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -15,10 +15,14 @@ import java.io.IOException; import java.io.StreamTokenizer; import java.io.StringReader; + +import static java.lang.Math.hypot; + import java.sql.SQLData; import java.sql.SQLException; import java.sql.SQLInput; import java.sql.SQLOutput; + import java.util.logging.Logger; import org.postgresql.pljava.annotation.Function; @@ -122,6 +126,52 @@ public static ComplexScalar add(ComplexScalar a, ComplexScalar b) a.m_x + b.m_x, a.m_y + b.m_y, a.m_typeName); } + /** + * True if the left argument is smaller than the right in magnitude + * (Euclidean distance from the origin). + */ + @Operator( + name = "javatest.<", + commutator = "javatest.>", negator = "javatest.>=" + ) + @Operator( + name = "javatest.<=", synthetic = "javatest.magnitudeLE" + ) + @Operator( + name = "javatest.>=", synthetic = "javatest.magnitudeGE", + commutator = "javatest.<=" + ) + @Operator( + name = "javatest.>", synthetic = "javatest.magnitudeGT", + negator = "javatest.<=" + ) + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static boolean magnitudeLT(ComplexScalar a, ComplexScalar b) + { + return hypot(a.m_x, a.m_y) < hypot(b.m_x, b.m_y); + } + + /** + * True if the left argument and the right are componentwise equal. + */ + @Operator( + name = "javatest.=", + commutator = SELF, negator = "javatest.<>" + ) + @Operator( + name = "javatest.<>", synthetic = "javatest.magnitudeNE", + commutator = SELF + ) + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static boolean componentsEQ(ComplexScalar a, ComplexScalar b) + { + return a.m_x == b.m_x && a.m_y == b.m_y; + } + public ComplexScalar(double x, double y, String typeName) { m_x = x; m_y = y; From c9f3dabfa6f87f68a75ce603b33388837cbfa285 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 3 Nov 2020 12:22:10 -0500 Subject: [PATCH 0815/1087] Teach the runtime about commute/negate info in AS When creating the MethodHandle tree used for invoking a method, simply build in a swap of the first two arguments, negation of the boolean return value, or both. --- .../postgresql/pljava/internal/Function.java | 95 ++++++++++++++++--- 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 31cc1a3b..3a0274a8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -17,9 +17,11 @@ import static java.lang.invoke.MethodHandles.arrayElementGetter; import static java.lang.invoke.MethodHandles.arrayElementSetter; import static java.lang.invoke.MethodHandles.collectArguments; +import static java.lang.invoke.MethodHandles.constant; import static java.lang.invoke.MethodHandles.dropArguments; import static java.lang.invoke.MethodHandles.empty; import static java.lang.invoke.MethodHandles.exactInvoker; +import static java.lang.invoke.MethodHandles.explicitCastArguments; import static java.lang.invoke.MethodHandles.filterArguments; import static java.lang.invoke.MethodHandles.filterReturnValue; import static java.lang.invoke.MethodHandles.foldArguments; @@ -171,6 +173,7 @@ public static Class getClassIfUDT( */ private static MethodType buildSignature( ClassLoader schemaLoader, String[] jTypes, boolean forValidator, + boolean commute, boolean retTypeIsOutParameter, boolean isMultiCall, boolean altForm) throws SQLException { @@ -199,6 +202,13 @@ private static MethodType buildSignature( for ( int i = 0 ; i < rtIdx ; ++ i ) pTypes[i] = loadClass(schemaLoader, jTypes[i], forValidator); + if ( commute ) + { + Class t = pTypes[0]; + pTypes[0] = pTypes[1]; + pTypes[1] = t; + } + Class returnType = getReturnSignature(schemaLoader, retJType, forValidator, retTypeIsOutParameter, isMultiCall, altForm); @@ -273,12 +283,12 @@ private static Lookup lookupFor(Class clazz) */ private static MethodHandle getMethodHandle( ClassLoader schemaLoader, Class clazz, String methodName, - boolean forValidator, + boolean forValidator, boolean commute, String[] jTypes, boolean retTypeIsOutParameter, boolean isMultiCall) throws SQLException { MethodType mt = - buildSignature(schemaLoader, jTypes, forValidator, + buildSignature(schemaLoader, jTypes, forValidator, commute, retTypeIsOutParameter, isMultiCall, false); // try altForm false ReflectiveOperationException ex1 = null; @@ -320,7 +330,7 @@ private static MethodHandle getMethodHandle( if ( null != altType ) { jTypes[jTypes.length - 1] = altType.getCanonicalName(); - mt = buildSignature(schemaLoader, jTypes, forValidator, + mt = buildSignature(schemaLoader, jTypes, forValidator, commute, retTypeIsOutParameter, isMultiCall, true); // retry altForm true try { @@ -578,6 +588,8 @@ else if ( rt.isPrimitive() ) private static final MethodHandle s_paramCountsAre; private static final MethodHandle s_countsZeroer; private static final MethodHandle s_nonNull; + private static final MethodHandle s_not; + private static final MethodHandle s_boxedNot; /* * Handles used to retrieve rows using SFRM_ValuePerCall protocol, from a @@ -840,6 +852,18 @@ private static void pop() s_nonNull = l.findStatic(Objects.class, "nonNull", methodType(boolean.class, Object.class)); + s_not = guardWithTest(identity(boolean.class), + dropArguments(constant(boolean.class, false), 0, boolean.class), + dropArguments(constant(boolean.class, true), 0, boolean.class)); + + s_boxedNot = + guardWithTest( + explicitCastArguments(s_nonNull, + methodType(boolean.class, Boolean.class)), + explicitCastArguments(s_not, + methodType(Boolean.class, Boolean.class)), + identity(Boolean.class)); + /* * Build a bit of MethodHandle tree for invoking a set-returning * user function that will implement the ValuePerCall protocol. @@ -1304,6 +1328,8 @@ private static Invocable init( String[] resolvedTypes; boolean isMultiCall = false; boolean retTypeIsOutParameter = false; + boolean commute = (null != info.group("com")); + boolean negate = (null != info.group("neg")); if ( forValidator ) calledAsTrigger = isTrigger(procTup); @@ -1319,7 +1345,7 @@ private static Invocable init( boolean[] multi = new boolean[] { isMultiCall }; boolean[] rtiop = new boolean[] { retTypeIsOutParameter }; resolvedTypes = setupFunctionParams(wrappedPtr, info, procTup, - schemaLoader, clazz, readOnly, typeMap, multi, rtiop); + schemaLoader, clazz, readOnly, typeMap, multi, rtiop, commute); isMultiCall = multi [ 0 ]; retTypeIsOutParameter = rtiop [ 0 ]; } @@ -1327,11 +1353,39 @@ private static Invocable init( String methodName = info.group("meth"); MethodHandle handle = - adaptHandle( - getMethodHandle(schemaLoader, clazz, methodName, forValidator, - resolvedTypes, retTypeIsOutParameter, isMultiCall) - .asFixedArity() - ); + getMethodHandle(schemaLoader, clazz, methodName, forValidator, + commute, resolvedTypes, retTypeIsOutParameter, isMultiCall) + .asFixedArity(); + MethodType mt = handle.type(); + + if ( commute ) + { + Class[] types = mt.parameterArray(); + mt = mt + .changeParameterType(0, types[1]) + .changeParameterType(1, types[0]); + handle = retTypeIsOutParameter + ? permuteArguments(handle, mt, 1, 0, 2) + : permuteArguments(handle, mt, 1, 0); + } + + if ( negate ) + { + MethodHandle inverter = null; + Class rt = mt.returnType(); + if ( boolean.class == rt ) + inverter = s_not; + else if ( Boolean.class == rt ) + inverter = s_boxedNot; + + if ( null == inverter || retTypeIsOutParameter ) + throw new SQLSyntaxErrorException( + "wrong return type for transformation [negate]", "42P13"); + + handle = filterReturnValue(handle, inverter); + } + + handle = adaptHandle(handle); if ( isMultiCall ) handle = ( @@ -1470,7 +1524,7 @@ private static String[] setupFunctionParams( long wrappedPtr, Matcher info, ResultSet procTup, ClassLoader schemaLoader, Class clazz, boolean readOnly, Map> typeMap, - boolean[] multi, boolean[] returnTypeIsOP) + boolean[] multi, boolean[] returnTypeIsOP, boolean commute) throws SQLException { int numParams = procTup.getInt("pronargs"); @@ -1500,7 +1554,7 @@ private static String[] setupFunctionParams( * resolvedTypes that the mapping from SQL types suggested above. */ parseParameters( wrappedPtr, resolvedTypes, explicitSignature, - isMultiCall, returnTypeIsOutputParameter); + isMultiCall, returnTypeIsOutputParameter, commute); } /* As in the original C setupFunctionParams, if an explicit Java return @@ -1546,7 +1600,8 @@ private static String[] setupFunctionParams( */ private static void parseParameters( long wrappedPtr, String[] resolvedTypes, String explicitSignature, - boolean isMultiCall, boolean returnTypeIsOutputParameter) + boolean isMultiCall, boolean returnTypeIsOutputParameter, + boolean commute) throws SQLException { boolean lastIsOut = ( ! isMultiCall ) && returnTypeIsOutputParameter; @@ -1560,6 +1615,17 @@ private static void parseParameters( "AS (Java): expected %1$d parameter types, found %2$d", expect, explicitTypes.length), "42601"); + if ( commute ) + { + if ( explicitTypes.length != (lastIsOut ? 3 : 2) ) + throw new SQLSyntaxErrorException( + "wrong number of parameters for transformation [commute]", + "42P13"); + String t = explicitTypes[0]; + explicitTypes[0] = explicitTypes[1]; + explicitTypes[1] = t; + } + doInPG(() -> { for ( int i = 0 ; i < resolvedTypes.length - 1 ; ++ i ) @@ -1714,6 +1780,11 @@ private static String getAS(ResultSet procTup) throws SQLException /* or the non-UDT form (which can't begin, insensitively, with UDT) */ "|(?!(?i:udt\\[))" + + /* allow a prefix like [commute] or [negate] or [commute,negate] */ + "(?:\\[(?:" + + "(?:(?:(?commute)|(?negate))(?:(?=\\])|,(?!\\])))" + + ")++\\])?+" + + /* and the long-standing method spec syntax */ "(?:(?%2$s)=)?+(?%1$s)\\.(?%3$s)" + "(?:\\((?(?:(?:%2$s,)*+%2$s)?+)\\))?+", javaTypeName, From 7e94aa171e35804ebb9a4834083e0a2504118e1e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 4 Nov 2020 18:46:13 -0500 Subject: [PATCH 0816/1087] Allow another shorthand notation, TWIN Where commutator=SELF strictly means that an operator, with both operand types the same, is its own commutator, use commutator=TWIN on an operator whose operand types differ, to indicate that the same-named operator with the types reversed is the commutator. PostgreSQL itself may not even need to know that (as the names are the same, it would just pick whichever one matched the order of the operands in a given expression), but PL/Java's SQL generator will use that information, if the other Operator is marked synthetic, to know how to synthesize it. TWIN is also a valid value for the synthetic= attribute, and means the SQL function generated to back the operator should have the same name as the one it is based on, which is sensible if the function is a commutative one like multiply or add. --- .../pljava/annotation/Operator.java | 41 ++++++- .../annotation/processing/DDRProcessor.java | 111 ++++++++++++++---- .../example/annotation/ComplexScalar.java | 14 +++ 3 files changed, 140 insertions(+), 26 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java index f786442b..cf2b4771 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java @@ -38,8 +38,32 @@ * Distinguished value usable for {@link #commutator commutator=} to * indicate that an operator is its own commutator without having to * repeat its schema and name. + *

    + * This value strictly declares that the operator is its own commutator, and + * therefore is only allowed for an operator with two operands of the same + * type. If the types are different, a commutator with the same + * name would in fact be a different operator with the operand + * types exchanged; use {@link #TWIN TWIN} for that. + */ + String SELF = ".self."; + + /** + * Distinguished value usable for {@link #commutator commutator=} to + * indicate that an operator's commutator is another operator with the same + * schema and name, without having to repeat them. + *

    + * This value strictly declares that the commutator is a different + * operator, and therefore is only allowed for an operator with two + * operands of different types. As commutators, this operator and its twin + * will have those operand types in opposite orders, so PostgreSQL + * overloading will allow them to have the same name. + *

    + * This value may also be used with {@link #synthetic synthetic=} to give + * the synthesized function the same schema and name as the one it is based + * on; this also is possible only for a function synthesized by commutation + * where the commuted parameter types differ. */ - String SELF = "self"; + String TWIN = ".twin."; /** * Name for this operator. @@ -83,6 +107,14 @@ *

    * Only allowed in an annotation on a Java method, and where * {@code function} is not specified. + *

    + * The special value {@link #TWIN TWIN} is allowed, to avoid repeating the + * schema and name when the desired name for the synthesized function is the + * same as the one it is derived from (which is only possible if the + * derivation involves commuting the arguments and their types are + * different, so the two functions can be distinguished by overloading). A + * typical case would be the twin of a cross-type function like {@code add} + * that is commutative, so using the same name makes sense. */ String[] synthetic() default {}; @@ -91,7 +123,12 @@ *

    * Specified in the same ways as {@code name}. The value * {@link #SELF SELF} can be used to avoid repeating the schema and name - * for the common case of an operator that is its own commutator. + * for the common case of an operator that is its own commutator. The value + * {@link #TWIN TWIN} can likewise declare that the commutator is the + * different operator with the same name and schema but the operand types + * (which must be different) reversed. A typical case would be the twin of a + * cross-type operator like {@code +} that is commutative, so using the same + * name makes sense. */ String[] commutator() default {}; 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 6bbacce2..b7c1a492 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 @@ -3267,7 +3267,10 @@ void operatorPreSynthesize( Element e, OperatorImpl snip) if ( null != snip ) { - (null == snip.synthetic ? m_nonSynthetic : m_synthetic).add(snip); + if ( snip.selfCommutator || snip.twinCommutator ) + snip.commutator = snip.qname; + + (snip.isSynthetic ? m_synthetic : m_nonSynthetic).add(snip); return; } @@ -3343,7 +3346,7 @@ void operatorPreSynthesize( Element e, OperatorImpl snip) boolean maybeAddPath( OperatorImpl from, OperatorImpl to, OperatorPath.Transform how) { - if ( null == to.synthetic ) + if ( ! to.isSynthetic ) return false; // don't add paths to a non-synthetic operator switch ( how ) @@ -3460,7 +3463,9 @@ class OperatorImpl Identifier.Qualified restrict; Identifier.Qualified join; Identifier.Qualified synthetic; + boolean isSynthetic; boolean selfCommutator; + boolean twinCommutator; List paths; private String operand(int i) @@ -3493,20 +3498,46 @@ public void setFunction( Object o, boolean explicit, Element e) public void setSynthetic( Object o, boolean explicit, Element e) { - if ( explicit ) - synthetic = qnameFrom(avToArray( o, String.class)); + if ( ! explicit ) + return; + + /* + * Use isSynthetic to indicate that synthetic= has been used at all. + * Set synthetic to the supplied qname only if it is a qname, and + * not the distinguished value TWIN. + * + * Most of the processing below only needs to look at isSynthetic. + * The TWIN case, recognized by isSynthetic && null == synthetic, + * will be handled late in the game by copying the base function's + * qname. + */ + + isSynthetic = true; + String[] ss = avToArray( o, String.class); + if ( 1 != ss.length || ! TWIN.equals(ss[0]) ) + synthetic = qnameFrom(ss); } public void setCommutator( Object o, boolean explicit, Element e) { - if ( explicit ) + if ( ! explicit ) + return; + + String[] ss = avToArray( o, String.class); + if ( 1 == ss.length ) { - String[] ss = avToArray( o, String.class); - if ( 1 == ss.length && SELF.equals(ss[0]) ) + if ( SELF.equals(ss[0]) ) + { selfCommutator = true; - else - commutator = operatorNameFrom(ss); + return; + } + if ( TWIN.equals(ss[0]) ) + { + twinCommutator = true; + return; + } } + commutator = operatorNameFrom(ss); } public void setNegator( Object o, boolean explicit, Element e) @@ -3534,16 +3565,13 @@ public Set characterize() boolean ok = true; Snippet syntheticFunction = null; - if ( selfCommutator ) - commutator = qname; - if ( ElementKind.METHOD.equals(m_targetElement.getKind()) ) { func = getSnippet(m_targetElement, FunctionImpl.class, () -> (FunctionImpl)null); } - if ( null != synthetic ) + if ( isSynthetic ) { if ( null != funcName ) { @@ -3553,10 +3581,10 @@ public Set characterize() ); ok = false; } - funcName = synthetic; + funcName = synthetic; // can be null (the TWIN case) } - if ( null == func && null == funcName ) + if ( null == func && null == funcName && ! isSynthetic ) { msg(Kind.ERROR, m_targetElement, m_origin, "@Operator not annotating a method must specify function=" @@ -3582,7 +3610,7 @@ public Set characterize() if ( null == funcName ) funcName = fn; - else if ( ! funcName.equals(fn) && null == synthetic ) + else if ( ! funcName.equals(fn) && ! isSynthetic ) { msg(Kind.ERROR, m_targetElement, m_origin, "@Operator annotates a method but function= gives a " + @@ -3594,7 +3622,7 @@ else if ( ! funcName.equals(fn) && null == synthetic ) long explicit = Arrays.stream(operands).filter(Objects::nonNull).count(); - if ( 0 != explicit && null != synthetic ) + if ( 0 != explicit && ! isSynthetic ) { msg(Kind.ERROR, m_targetElement, m_origin, "@Operator with synthetic= must not specify " + @@ -3642,7 +3670,12 @@ else if ( 2 == explicit } /* - * At this point, ok ==> there is a non-null funcName + * At this point, ok ==> there is a non-null funcName ... UNLESS + * isSynthetic is true, synthetic=TWIN was given, and we are not + * annotating a method (that last condition is currently not + * supported, so we could in fact rely on having a funcName here, + * but that condition may be worth supporting in the future, so + * better to keep the exception in mind). */ if ( ! ok ) @@ -3672,7 +3705,15 @@ else if ( selfCommutator && ! operands[0].equals(operands[1]) ) { msg(Kind.ERROR, m_targetElement, m_origin, "@Operator with different left and right operand " + - "types cannot be its own commutator" + "types cannot have commutator=SELF" + ); + ok = false; + } + else if ( twinCommutator && operands[0].equals(operands[1]) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "@Operator with matching left and right operand " + + "types cannot have commutator=TWIN" ); ok = false; } @@ -3769,7 +3810,7 @@ else if ( ! (operandTypesDiffer || selfCommutates) ) if ( ! ok ) return Set.of(); - if ( null != synthetic ) + if ( isSynthetic ) { if ( null == func ) { @@ -3816,7 +3857,8 @@ else if ( ! (operandTypesDiffer || selfCommutates) ) p -> ! p.fromBase.contains( OperatorPath.Transform.COMMUTATION)) .collect(toList()); - if ( 2 != arity || selfCommutator || + if ( 2 != arity || selfCommutator + || null == synthetic || synthetic.equals(qnameFrom(func.name(), func.schema()))) { if ( filtered.isEmpty() ) @@ -3914,17 +3956,28 @@ else if ( ! (operandTypesDiffer || selfCommutates) ) * over this method. (That's currently guaranteed by the way * operatorPreSynthesize generates paths, but may as well check * here to ensure sanity during future maintenance.) + * + * For synthetic=TWIN (represented here by null==synthetic), + * also filter out paths that don't involve commutation (without + * it, the synthetic function would collide with the base one). */ + boolean nonCommutedOK = null != synthetic; + paths = paths.stream() - .filter(p -> p.base.func == func).collect(toList()); + .filter( + p -> p.base.func == func + && (nonCommutedOK || p.fromBase.contains( + OperatorPath.Transform.COMMUTATION)) + ).collect(toList()); if ( 0 == paths.size() ) { msg(Kind.ERROR, m_targetElement, m_origin, "Synthetic operator %s has no derivation path " + - "from an operator that is based on this method", - qnameUnwrapped()); + "from an operator that is based on this method%s", + qnameUnwrapped(), + nonCommutedOK ? "" : " and involves commutation"); ok = false; } @@ -3949,6 +4002,16 @@ else if ( ! (operandTypesDiffer || selfCommutates) ) .thenComparing(p -> p.base.qnameUnwrapped())) .findFirst().get(); + /* + * At last, the possibly null funcName (synthetic=TWIN case) + * can be fixed up. + */ + if ( null == synthetic ) + { + FunctionImpl f = selected.base.func; + funcName = synthetic = qnameFrom(f.name(), f.schema()); + } + replaceCommentIfDerived("Operator " + qnameUnwrapped() + " automatically derived by " + selected.fromBase + " from " diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 89413881..27025b1d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -28,6 +28,7 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.Operator; import static org.postgresql.pljava.annotation.Operator.SELF; +import static org.postgresql.pljava.annotation.Operator.TWIN; import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.BaseUDT; @@ -126,6 +127,19 @@ public static ComplexScalar add(ComplexScalar a, ComplexScalar b) a.m_x + b.m_x, a.m_y + b.m_y, a.m_typeName); } + /** + * Add a {@code ComplexScalar} and a real (supplied as a {@code double}). + */ + @Operator(name = {"javatest","+"}, commutator = TWIN) + @Operator(name = {"javatest","+"}, synthetic = TWIN) + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static ComplexScalar add(ComplexScalar a, double b) + { + return new ComplexScalar(a.m_x + b, a.m_y, a.m_typeName); + } + /** * True if the left argument is smaller than the right in magnitude * (Euclidean distance from the origin). From 258a820350a53567357f11657f1b0ba933d42c96 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 4 Nov 2020 19:18:08 -0500 Subject: [PATCH 0817/1087] Add deployment-time tests The SQLAction performing the tests must be emitted after all of the operators it refers to. Until now, DDRProcessor has strictly enforced an unnecessary rule that a given dependency tag must have no more than one providing snippet. Perhaps that has helped someone detect once or twice when code was copy/pasted without renaming a tag, but it was also quite inconvenient for a case like this: it would require making up n different tags for the n operators to 'provide', and the SQLAction would then have to 'require' all n of them. So, away with the unnecessary rule. An explicit dependency tag can be 'provided' by more than one snippet, all of which will have to be emitted before whatever 'requires' it. The correct ordering of the test block here can thus be declared using only one tag. An implicit tag being 'provided' by more than one snippet will still be reported as an error; that shouldn't happen. --- .../annotation/processing/DDRProcessor.java | 67 ++++++++++++------- .../example/annotation/ComplexScalar.java | 39 ++++++++--- 2 files changed, 74 insertions(+), 32 deletions(-) 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 b7c1a492..31e09716 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 @@ -445,10 +445,14 @@ void putSnippet( Object o, Snippet s) /** * Map from each arbitrary provides/requires label to the snippet - * that 'provides' it. Has to be out here as an instance field for the - * same reason {@code snippetVPairs} does. + * that 'provides' it (snippets, in some cases). Has to be out here as an + * instance field for the same reason {@code snippetVPairs} does. + *

    + * Originally limited each tag to have only one provider; that is still + * enforced for implicitly-generated tags, but relaxed for explicit ones + * supplied in annotations, hence the list. */ - Map> provider = new HashMap<>(); + Map>> provider = new HashMap<>(); /** * Find the elements in each round that carry any of the annotations of @@ -560,9 +564,18 @@ void defensiveEarlyCharacterize() { VertexPair v = new VertexPair<>( readySnip); snippetVPairs.add( v); - for ( DependTag s : readySnip.provideTags() ) - if ( null != provider.put( s, v) ) - msg(Kind.ERROR, "tag %s has more than one provider", s); + for ( DependTag t : readySnip.provideTags() ) + { + List> ps = + provider.computeIfAbsent(t, k -> new ArrayList<>()); + /* + * Explicit tags are allowed more than one provider. + */ + if ( t instanceof DependTag.Explicit || ps.isEmpty() ) + ps.add(v); + else + msg(Kind.ERROR, "tag %s has more than one provider", t); + } } } snippets.clear(); @@ -581,7 +594,7 @@ void generateDescriptor() for ( VertexPair v : snippetVPairs ) { - VertexPair p; + List> ps; /* * First handle the implicit requires(implementor()). This is unlike @@ -594,23 +607,26 @@ void generateDescriptor() DependTag imp = v.payload().implementorTag(); if ( null != imp ) { - p = provider.get( imp); - if ( null != p ) + ps = provider.get( imp); + if ( null != ps ) { fwdConsumers.add( imp); revConsumers.add( imp); - p.fwd.precede( v.fwd); - p.rev.precede( v.rev); - - /* - * A snippet providing an implementor tag probably has no - * undeployStrings, because its deployStrings should be used - * on both occasions; if so, replace it with a proxy that - * returns deployStrings for undeployStrings. - */ - if ( 0 == p.rev.payload.undeployStrings().length ) - p.rev.payload = new ImpProvider( p.rev.payload); + ps.forEach(p -> + { + p.fwd.precede( v.fwd); + p.rev.precede( v.rev); + + /* + * A snippet providing an implementor tag probably has + * no undeployStrings, because its deployStrings should + * be used on both occasions; if so, replace it with a + * proxy that returns deployStrings for undeployStrings. + */ + if ( 0 == p.rev.payload.undeployStrings().length ) + p.rev.payload = new ImpProvider( p.rev.payload); + }); } else if ( ! defaultImplementor.equals( impName, msgr) ) { @@ -629,13 +645,16 @@ else if ( ! defaultImplementor.equals( impName, msgr) ) } for ( DependTag s : v.payload().requireTags() ) { - p = provider.get( s); - if ( null != p ) + ps = provider.get( s); + if ( null != ps ) { fwdConsumers.add( s); revConsumers.add( s); - p.fwd.precede( v.fwd); - v.rev.precede( p.rev); // these relationships do reverse + ps.forEach(p -> + { + p.fwd.precede( v.fwd); + v.rev.precede( p.rev); // these relationships do reverse + }); } else if ( s instanceof DependTag.Explicit ) { diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 27025b1d..7dfc87b9 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -38,11 +38,31 @@ /** * Complex (re and im parts are doubles) implemented in Java as a scalar UDT. + *

    + * The {@code SQLAction} here demonstrates a {@code requires} tag + * ("complex relationals"} that has multiple providers, something not allowed + * prior to PL/Java 1.6.1. It is more succinct to require one tag and have each + * of the relational operators 'provide' it than to have to define and require + * several different tags to accomplish the same thing. */ -@SQLAction(requires = "complex assertHasValues", +@SQLAction(requires = { "complex assertHasValues", "complex relationals" }, install = { "SELECT javatest.assertHasValues(" + - " CAST('(1,2)' AS javatest.complex), 1, 2)" + " CAST('(1,2)' AS javatest.complex), 1, 2)", + + "SELECT javatest.assertHasValues(" + + " 2.0 + CAST('(1,2)' AS javatest.complex) + 3.0, 6, 2)", + + "SELECT" + + " CASE WHEN" + + " '(1,2)'::javatest.complex < '(2,2)'::javatest.complex" + + " AND" + + " '(2,2)'::javatest.complex > '(1,2)'::javatest.complex" + + " AND" + + " '(1,2)'::javatest.complex <= '(2,2)'::javatest.complex" + + " THEN javatest.logmessage('INFO', 'ComplexScalar operators ok')" + + " ELSE javatest.logmessage('WARNING', 'ComplexScalar operators ng')" + + " END" } ) @BaseUDT(schema="javatest", name="complex", @@ -146,18 +166,20 @@ public static ComplexScalar add(ComplexScalar a, double b) */ @Operator( name = "javatest.<", - commutator = "javatest.>", negator = "javatest.>=" + commutator = "javatest.>", negator = "javatest.>=", + provides = "complex relationals" ) @Operator( - name = "javatest.<=", synthetic = "javatest.magnitudeLE" + name = "javatest.<=", synthetic = "javatest.magnitudeLE", + provides = "complex relationals" ) @Operator( name = "javatest.>=", synthetic = "javatest.magnitudeGE", - commutator = "javatest.<=" + commutator = "javatest.<=", provides = "complex relationals" ) @Operator( name = "javatest.>", synthetic = "javatest.magnitudeGT", - negator = "javatest.<=" + negator = "javatest.<=", provides = "complex relationals" ) @Function( schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL @@ -172,11 +194,12 @@ public static boolean magnitudeLT(ComplexScalar a, ComplexScalar b) */ @Operator( name = "javatest.=", - commutator = SELF, negator = "javatest.<>" + commutator = SELF, negator = "javatest.<>", + provides = "complex relationals" ) @Operator( name = "javatest.<>", synthetic = "javatest.magnitudeNE", - commutator = SELF + commutator = SELF, provides = "complex relationals" ) @Function( schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL From db3393fcef03c4a8ac76996954be444ddb5568e0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 11 Nov 2020 22:04:29 -0500 Subject: [PATCH 0818/1087] Add missing bits of Aggregate annotation This isn't the PR for aggregate generation, but that one left a few bits unimplemented (waiting for this PR to add operator support so sortOperator's dependency can be declared). So complete the Aggregate annotation support for sortOperator, serialize, and deserialize. There is still no PL/Java support for manipulating a stateType of internal, and hence nothing very useful to do with serialize and deserialize, but that's no pressing reason to leave their SQL generation unimplemented. An Aggregate annotation could be used to create an aggregate that uses some functions in some other PL. Add a trivial aggregate in ComplexScalar that uses sortOperator. Check for some more errors at compile time. --- .../pljava/annotation/Aggregate.java | 48 ++++- .../annotation/processing/DDRProcessor.java | 168 +++++++++++++++++- .../example/annotation/ComplexScalar.java | 67 +++++++ 3 files changed, 269 insertions(+), 14 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java index 5003ae9c..d92f02d8 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java @@ -71,8 +71,7 @@ * {@code requires}. *

    * While this annotation can generate {@code CREATE AGGREGATE} deployment - * commands with most of the features available in PostgreSQL (currently not - * covered are {@code SERIALFUNC}, {@code DESERIALFUNC}, and {@code SORTOP}), + * commands with the features available in PostgreSQL, * at present there are limits to which aggregate features can be implemented * purely in PL/Java. In particular, PL/Java functions currently have no access * to the PostgreSQL data structures needed for an ordered-set or @@ -132,8 +131,9 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * The data type to be used to hold the accumulating state. *

    * This will be the first argument type for all of the support functions - * (both argument types for {@code combine}) and also, if there is no - * {@code finish} function, the result type of the aggregate. + * except {@code deserialize} (both argument types for {@code combine}) + * and also, if there is no {@code finish} function, the result type + * of the aggregate. */ String stateType() default ""; @@ -235,6 +235,25 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * hypothetical-set aggregate. */ FinishEffect finishEffect() default FinishEffect.READ_ONLY; + + /** + * Name of a serializing function ({@code internal} to {@code bytea}), + * usable only if a {@link #combine() combine} function is specified and + * the aggregate's state type is {@code internal}. + *

    + * Not allowed in a {@code movingPlan}. Not allowed without + * {@code deserialize}. + */ + String[] serialize() default {}; + + /** + * Name of a deserializing function (({@code bytea}, {@code internal}) + * to {@code internal}), usable only if a {@code serialize} function is + * also specified. + *

    + * Not allowed in a {@code movingPlan}. + */ + String[] deserialize() default {}; } /** @@ -342,6 +361,9 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; *

    * Though declared as an array, only one plan is allowed here. It must * name a {@code remove} function. + *

    + * A {@code movingPlan} may not have {@code serialize}/{@code deserialize} + * functions; only {@code plan} can have those. */ Plan[] movingPlan() default {}; @@ -356,7 +378,23 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; */ Function.Parallel parallel() default Function.Parallel.UNSAFE; - // not yet here: serialfunc, deserialfunc, sortop + /** + * Name of an operator (declared as either the less-than or greater-than + * strategy member of a {@code btree} operator class) such that the result + * of this aggregate is the same as the first result from {@code ORDER BY} + * over the aggregated values, using this operator. + *

    + * May be specified in explicit {@code {"schema","localname"}} form, or as + * a single string that will be leniently parsed as an optionally + * schema-qualified name. In the explicit form, {@code ""} as the schema + * will make the name explicitly unqualified. The operator will be assumed + * to have two operands of the same type as the argument to the aggregate + * (which must have exactly one aggregated argument, and no direct + * arguments). The operator's membership in a {@code btree} operator class + * is not (currently) checked at compile time, but if it does not hold at + * run time, the optimization will not be used. + */ + String[] sortOperator() default {}; /** * One or more arbitrary labels that will be considered 'provided' by the 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 31e09716..d8ef2404 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 @@ -252,14 +252,17 @@ class DDRProcessorImpl Identifier.Qualified.nameFromJava("pg_catalog.void")); final DBType DT_ANY = new DBType.Named( Identifier.Qualified.nameFromJava("pg_catalog.\"any\"")); + final DBType DT_BYTEA = new DBType.Named( + Identifier.Qualified.nameFromJava("pg_catalog.bytea")); + final DBType DT_INTERNAL = new DBType.Named( + Identifier.Qualified.nameFromJava("pg_catalog.internal")); // Function signatures for certain known functions // final DBType[] SIG_TYPMODIN = { DBType.fromSQLTypeAnnotation("pg_catalog.cstring[]") }; final DBType[] SIG_TYPMODOUT = { DT_INTEGER }; - final DBType[] SIG_ANALYZE = - { DBType.fromSQLTypeAnnotation("pg_catalog.internal") }; + final DBType[] SIG_ANALYZE = { DT_INTERNAL }; DDRProcessorImpl( ProcessingEnvironment processingEnv) { @@ -4191,6 +4194,7 @@ class AggregateImpl public Plan[] plan() { return new Plan[]{_plan}; } public Plan[] movingPlan() { return _movingPlan; } public Function.Parallel parallel() { return _parallel; } + public String[] sortOperator() { return qstrings(sortop); } public String[] provides() { return _provides; } public String[] requires() { return _requires; } @@ -4206,6 +4210,7 @@ class AggregateImpl Identifier.Qualified qname; List> aggregateArgs; List> directArgs; + Identifier.Qualified sortop; static final int DIRECT_ARGS = 0; // index into _variadic[] static final int AGG_ARGS = 1; // likewise boolean directVariadicExplicit; @@ -4258,6 +4263,12 @@ public void setDirectArguments( Object o, boolean explicit, Element e) directArgs = argsIn( avToArray( o, String.class)); } + public void setSortOperator( Object o, boolean explicit, Element e) + { + if ( explicit ) + sortop = operatorNameFrom(avToArray( o, String.class)); + } + public void setVariadic( Object o, boolean explicit, Element e) { if ( ! explicit ) @@ -4318,6 +4329,7 @@ public Set characterize() boolean moving = null != _movingPlan; boolean checkAccumulatorSig = false; boolean checkFinisherSig = false; + boolean unary = false; if ( ElementKind.METHOD.equals(m_targetElement.getKind()) ) { @@ -4341,6 +4353,7 @@ public Set characterize() null == _plan.accumulate || null == aggregateArgs; boolean inferFinisher = null == _plan.finish && ! inferAccumulator; + boolean stateTypeExplicit = false; if ( null == qname ) { @@ -4376,6 +4389,8 @@ else if ( null == _plan.stateType ) && null == _movingPlan[0].stateType) _movingPlan[0].stateType = func.parameterTypes[0]; } + else + stateTypeExplicit = true; if ( inferAccumulator || inferFinisher ) { @@ -4399,6 +4414,8 @@ else if ( null == _plan.stateType ) ) .collect(toList()); } + else + checkAccumulatorSig = true; _plan.accumulate = funcName; if ( null != _movingPlan && null == _movingPlan[0].accumulate ) @@ -4412,6 +4429,16 @@ else if ( null == _plan.stateType ) _movingPlan[0].finish = funcName; } } + + if ( stateTypeExplicit + && ! _plan.stateType.equals(func.parameterTypes[0]) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "First function argument does not match " + + "stateType specified with @Aggregate" + ); + ok = false; + } } else if ( funcName.equals(_plan.accumulate) ) checkAccumulatorSig = true; @@ -4458,6 +4485,8 @@ else if ( funcName.equals(_plan.finish) ) "@Aggregate missing arguments="); ok = false; } + else + unary = 1 == aggregateArgs.size(); if ( null == _plan.stateType ) { @@ -4628,6 +4657,54 @@ else if ( ! isAny && ! t.isArray() ) } } + // Checks involving sortOperator + if ( null != sortop ) + { + if ( orderedSet ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "The sortOperator optimization is not available for " + + "an ordered-set aggregate (one with directArguments)"); + ok = false; + } + + if ( ! unary || _variadic[AGG_ARGS] ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "The sortOperator optimization is only available for " + + "a one-argument (and non-variadic) aggregate"); + ok = false; + } + } + + // Checks involving serialize / deserialize + if ( null != _plan.serialize || null != _plan.deserialize ) + { + if ( null == _plan.combine ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "An aggregate plan without combine= may not have " + + "serialize= or deserialize="); + ok = false; + } + + if ( null == _plan.serialize || null == _plan.deserialize ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "An aggregate plan must have both " + + "serialize= and deserialize= or neither"); + ok = false; + } + + if ( ! DT_INTERNAL.equals(_plan.stateType) ) + { + msg(Kind.ERROR, m_targetElement, m_origin, + "Only an aggregate plan with stateType " + + "pg_catalog.internal may have serialize=/deserialize="); + ok = false; + } + } + if ( ! ok ) return Set.of(); @@ -4678,9 +4755,22 @@ else if ( ! isAny && ! t.isArray() ) new DependTag.Function(_plan.accumulate, accumulatorSig)); if ( null != _plan.combine ) + { + DBType[] serialSig = { DT_INTERNAL }; + DBType[] deserialSig = { DT_BYTEA, DT_INTERNAL }; + requires.add( new DependTag.Function(_plan.combine, combinerSig)); + if ( null != _plan.serialize ) + { + requires.add( + new DependTag.Function(_plan.serialize, serialSig)); + requires.add( + new DependTag.Function(_plan.deserialize, deserialSig)); + } + } + if ( null != _plan.finish ) requires.add( new DependTag.Function(_plan.finish, finisherSig)); @@ -4706,6 +4796,13 @@ else if ( ! isAny && ! t.isArray() ) _movingPlan[0].finish, finisherSig)); } + if ( null != sortop ) + { + DBType arg = aggregateArgs.get(0).getValue(); + DBType[] opSig = { arg, arg }; + requires.add(new DependTag.Operator(sortop, opSig)); + } + /* * That establishes dependency on the various support functions, * which should, transitively, depend on all of the types. But it is @@ -4758,6 +4855,9 @@ public String[] deployStrings() sb.append(",\n\tM").append(s); } + if ( null != sortop ) + sb.append(",\n\tSORTOP = ").append(sortop); + if ( Function.Parallel.UNSAFE != _parallel ) sb.append(",\n\tPARALLEL = ").append(_parallel); @@ -4839,6 +4939,8 @@ class Plan extends AbstractAnnotationImpl implements Aggregate.Plan public String[] combine() { return qstrings(combine); } public String[] finish() { return qstrings(finish); } public String[] remove() { return qstrings(remove); } + public String[] serialize() { return qstrings(serialize); } + public String[] deserialize() { return qstrings(deserialize); } public boolean polymorphic() { return _polymorphic; } public FinishEffect finishEffect() { return _finishEffect; } @@ -4852,6 +4954,8 @@ class Plan extends AbstractAnnotationImpl implements Aggregate.Plan Identifier.Qualified combine; Identifier.Qualified finish; Identifier.Qualified remove; + Identifier.Qualified serialize; + Identifier.Qualified deserialize; public void setStateType(Object o, boolean explicit, Element e) { @@ -4898,6 +5002,18 @@ public void setRemove(Object o, boolean explicit, Element e) "Only a movingPlan may have a remove function"); } + public void setSerialize(Object o, boolean explicit, Element e) + { + if ( explicit ) + serialize = qnameFrom(avToArray( o, String.class)); + } + + public void setDeserialize(Object o, boolean explicit, Element e) + { + if ( explicit ) + deserialize = qnameFrom(avToArray( o, String.class)); + } + public void setFinishEffect( Object o, boolean explicit, Element e) { if ( explicit ) @@ -4951,6 +5067,12 @@ public String[] deployStrings() if ( null != combine ) al.add("COMBINEFUNC = " + combine); + if ( null != serialize ) + al.add("SERIALFUNC = " + serialize); + + if ( null != deserialize ) + al.add("DESERIALFUNC = " + deserialize); + return al.toArray( new String [ al.size() ]); } @@ -4967,6 +5089,22 @@ public void setRemove(Object o, boolean explicit, Element e) if ( explicit ) remove = qnameFrom(avToArray( o, String.class)); } + + public void setSerialize(Object o, boolean explicit, Element e) + { + if ( explicit ) + throw new IllegalArgumentException( + "Only a (non-moving) plan may have a " + + "serialize function"); + } + + public void setDeserialize(Object o, boolean explicit, Element e) + { + if ( explicit ) + throw new IllegalArgumentException( + "Only a (non-moving) plan may have a " + + "deserialize function"); + } } } @@ -4984,8 +5122,8 @@ class TypeMapper // Primitives (these need not, indeed cannot, be schema-qualified) // - this.addMap(boolean.class, "boolean"); - this.addMap(Boolean.class, "boolean"); + this.addMap(boolean.class, DT_BOOLEAN); + this.addMap(Boolean.class, DT_BOOLEAN); this.addMap(byte.class, "smallint"); this.addMap(Byte.class, "smallint"); this.addMap(char.class, "smallint"); @@ -4994,8 +5132,8 @@ class TypeMapper this.addMap(Double.class, "double precision"); this.addMap(float.class, "real"); this.addMap(Float.class, "real"); - this.addMap(int.class, "integer"); - this.addMap(Integer.class, "integer"); + this.addMap(int.class, DT_INTEGER); + this.addMap(Integer.class, DT_INTEGER); this.addMap(long.class, "bigint"); this.addMap(Long.class, "bigint"); this.addMap(short.class, "smallint"); @@ -5012,10 +5150,10 @@ class TypeMapper this.addMap(java.sql.SQLXML.class, "pg_catalog", "xml"); this.addMap(BigInteger.class, "pg_catalog", "numeric"); this.addMap(BigDecimal.class, "pg_catalog", "numeric"); - this.addMap(ResultSet.class, "pg_catalog", "record"); - this.addMap(Object.class, "pg_catalog", "\"any\""); + this.addMap(ResultSet.class, DT_RECORD); + this.addMap(Object.class, DT_ANY); - this.addMap(byte[].class, "pg_catalog", "bytea"); + this.addMap(byte[].class, DT_BYTEA); this.addMap(LocalDate.class, "pg_catalog", "date"); this.addMap(LocalTime.class, "pg_catalog", "time"); @@ -5174,6 +5312,18 @@ void addMap(Class k, String schema, String local) new DBType.Named(qnameFrom(local, schema))); } + /** + * Add a custom mapping from a Java class to an SQL type + * already in the form of a {@code DBType}. + * + * @param k Class representing the Java type + * @param DBType representing the SQL type to be used + */ + void addMap(Class k, DBType type) + { + addMap( typeMirrorFromClass( k), type); + } + /** * Add a custom mapping from a Java class to an SQL type, if a class * with the given name exists. diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 7dfc87b9..54b92cb1 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -25,6 +25,7 @@ import java.util.logging.Logger; +import org.postgresql.pljava.annotation.Aggregate; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.Operator; import static org.postgresql.pljava.annotation.Operator.SELF; @@ -44,9 +45,30 @@ * prior to PL/Java 1.6.1. It is more succinct to require one tag and have each * of the relational operators 'provide' it than to have to define and require * several different tags to accomplish the same thing. + *

    + * The operator class created here is not actively used for anything (the + * examples will not break if it is removed), but the {@code minMagnitude} + * example aggregate does specify a {@code sortOperator}, which PostgreSQL will + * not exploit in query optimization without finding it as a member of + * a {@code btree} operator class. + *

    + * Note that {@code CREATE OPERATOR CLASS} implicitly creates an operator family + * as well (unless one is explicitly specified), so the correct {@code remove} + * action to clean everything up is {@code DROP OPERATOR FAMILY} (which takes + * care of dropping the class). */ @SQLAction(requires = { "complex assertHasValues", "complex relationals" }, install = { + "CREATE OPERATOR CLASS javatest.complex_ops" + + " DEFAULT FOR TYPE javatest.complex USING btree" + + " AS" + + " OPERATOR 1 javatest.< ," + + " OPERATOR 2 javatest.<= ," + + " OPERATOR 3 javatest.= ," + + " OPERATOR 4 javatest.>= ," + + " OPERATOR 5 javatest.> ," + + " FUNCTION 1 javatest.cmpMagnitude(javatest.complex,javatest.complex)", + "SELECT javatest.assertHasValues(" + " CAST('(1,2)' AS javatest.complex), 1, 2)", @@ -63,6 +85,10 @@ " THEN javatest.logmessage('INFO', 'ComplexScalar operators ok')" + " ELSE javatest.logmessage('WARNING', 'ComplexScalar operators ng')" + " END" + }, + + remove = { + "DROP OPERATOR FAMILY javatest.complex_ops USING btree" } ) @BaseUDT(schema="javatest", name="complex", @@ -209,6 +235,47 @@ public static boolean componentsEQ(ComplexScalar a, ComplexScalar b) return a.m_x == b.m_x && a.m_y == b.m_y; } + /** + * As an ordinary function, returns the lesser in magnitude of two + * arguments; as a simple aggregate, returns the least in magnitude over its + * aggregated arguments. + *

    + * As an aggregate, this is a simple example where this method serves as the + * {@code accumulate} function, the state (a here) has the same + * type as the argument (here b), there is no {@code finish} + * function, and the final value of the state is the result. + *

    + * An optimization is available in case there is an index on the aggregated + * values based on the {@code <} operator above; in that case, the first + * value found in a scan of that index is the aggregate result. That is + * indicated here by naming the {@code <} operator as {@code sortOperator}. + */ + @Aggregate(sortOperator = "javatest.<") + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static ComplexScalar minMagnitude(ComplexScalar a, ComplexScalar b) + { + return magnitudeLT(a, b) ? a : b; + } + + /** + * An integer-returning comparison function by complex magnitude, usable to + * complete an example {@code btree} operator class. + */ + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL, + provides = "complex relationals" + ) + public static int cmpMagnitude(ComplexScalar a, ComplexScalar b) + { + if ( magnitudeLT(a, b) ) + return -1; + if ( magnitudeLT(b, a) ) + return 1; + return 0; + } + public ComplexScalar(double x, double y, String typeName) { m_x = x; m_y = y; From dbf410fab0e918065945730b4c4d62e511573081 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Oct 2020 20:03:13 -0400 Subject: [PATCH 0819/1087] Mistrust localized punctuation (issue #312) The previous approach, requiring punctuation around the element name, reflected an ill-thought-out belief that it's important to avoid giving a pass on error reporting to an element name that happens to contain the name of our wrapping element. Anybody who wanted to sneak an element past the error reporting would not be prevented from using exactly the name of our wrapping element to do it. So extra work to avoid the containing-name case achieves nothing. Better to just count the exceptions. If we are using a wrapping element, it will be first. No special treatment after that one (or at all, if we are not wrapping). Backpatched from 927e0d4. Addresses issue #312. --- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 81 +++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 4b3f5871..f182dbbe 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -1455,6 +1455,11 @@ static class Verifier extends VarlenaWrapper.Verifier.Base { private XMLReader m_xr; + /** + * Constructor called only from {@code adopt()} when an untouched + * {@code Readable} is being bounced back to PostgreSQL with a type Oid + * different from its original. + */ Verifier() throws SQLException { try @@ -1472,6 +1477,11 @@ static class Verifier extends VarlenaWrapper.Verifier.Base } } + /** + * Constructor called with an already-constructed {@code XMLReader}. + *

    + * Adjustments may have been made to the {@code XMLReader}. + */ Verifier(XMLReader xr) { m_xr = xr; @@ -1483,6 +1493,16 @@ protected void verify(InputStream is) throws Exception boolean[] wrapped = { false }; is = correctedDeclStream( is, false, implServerCharset(), wrapped); + + /* + * The supplied XMLReader is never set up to do unwrapping, which is + * ok; it never needs to. But it will have had its error handler set + * on that assumption, which must be changed here if wrapping is in + * effect, just in case schema validation has been requested. + */ + if ( wrapped[0] ) + m_xr.setErrorHandler(SAXDOMErrorHandler.instance(true)); + /* * What does an XMLReader do if no handlers have been set for * content events? Parses everything and discards the events. @@ -3923,6 +3943,11 @@ static class Parsing extends XMLCopier { super(tgt); InputSource is = src.getInputSource(); + /* + * No correctedDeclStream, no check for unwrapping: if some + * random {@code SQLXML} implementation is passing a stream + * to parse, it had better make sense to a vanilla parser. + */ m_source = new AdjustingSAXSource(is, false); } @@ -4240,13 +4265,32 @@ public T loadExternalDTD(boolean v) */ static class SAXDOMErrorHandler implements ErrorHandler { - static final SAXDOMErrorHandler INSTANCE = new SAXDOMErrorHandler(); + private static final SAXDOMErrorHandler s_nonWrappedInstance = + new SAXDOMErrorHandler(false); + /* + * Issue #312: localized error messages from the schema validator + * don't always use the same punctuation around the offending + * element name! Simplest to look for the element name (it's distinctive + * enough) and not for any punctuation--and check that only when + * wrapping is being applied, and then only once (the wrapping element, + * of course, will be first), and so avoid suppressing a later error by + * mistake should a document somehow happen to contain an element with + * the same name used here. + */ static final Pattern s_wrapelement = Pattern.compile( - "^cvc-elt\\.1(?:\\.a)?+:.*'pljava-content-wrap'"); + "^cvc-elt\\.1(?:\\.a)?+:.*pljava-content-wrap"); final Logger m_logger = Logger.getLogger("org.postgresql.pljava.jdbc"); + private int m_wrapCount; + + static SAXDOMErrorHandler instance(boolean wrapped) + { + return + wrapped ? new SAXDOMErrorHandler(true) : s_nonWrappedInstance; + } - private SAXDOMErrorHandler() + private SAXDOMErrorHandler(boolean wrap) { + m_wrapCount = wrap ? 1 : 0; } @Override @@ -4259,10 +4303,21 @@ public void error(SAXParseException exception) throws SAXException * then more lenient if the 'visible' top-level element isn't found, * and simply validates the elements that are declared in the schema * wherever it happens to find them. + * + * The check is only applied when the input has been wrapped, and + * then only once (after all, the wrapping element will be the first + * to be seen). The "only once" part may be futile inasmuch as the + * validator switches to the lenient mode described above and may + * not even report subsequent mismatched elements. But the check + * still needs to be conditional (we do know whether we applied a + * wrapper or not), so the condition may as well be the right one. */ + if ( 0 == m_wrapCount ) + throw exception; Matcher m = s_wrapelement.matcher(exception.getMessage()); if ( ! m.lookingAt() ) throw exception; + -- m_wrapCount; } @Override @@ -4584,6 +4639,14 @@ static class AdjustingStreamResult m_serverCS = serverCS; try { + /* + * When used as a verifier, an AdjustingSAXSource can be created + * with wrapping=false unconditionally, as it won't be using the + * result for anything and has no need to unwrap it. At verify + * time, the presence of wrapping still gets checked, if only to + * set up the ErrorHandler correctly in case schema validation + * has been requested. + */ m_verifierSource = new AdjustingSAXSource(null, false); } catch ( SAXException e ) @@ -4865,7 +4928,15 @@ private XMLReader theReader() if ( m_wrapped ) m_xr = new SAXUnwrapFilter(m_xr); - m_xr.setErrorHandler(SAXDOMErrorHandler.INSTANCE); + /* + * If this AdjustingSAXSource has been created for use as a + * verifier, it was passed false for m_wrapped unconditionally, + * which is mostly harmless, but may mean this is the wrong + * error handler, if schema validation has been requested. + * That's ok; the verifier checks for wrapping and will set the + * right error handler if need be. + */ + m_xr.setErrorHandler(SAXDOMErrorHandler.instance(m_wrapped)); if ( ! m_hasCalledDefaults ) defaults(); @@ -5268,7 +5339,7 @@ public DOMSource get() throws SQLException try { DocumentBuilder db = m_dbf.newDocumentBuilder(); - db.setErrorHandler(SAXDOMErrorHandler.INSTANCE); + db.setErrorHandler(SAXDOMErrorHandler.instance(m_wrapped)); if ( null != m_resolver ) db.setEntityResolver(m_resolver); DOMSource ds = new DOMSource(db.parse(m_is)); From e0cdb54444d479100889ad9c9f73383e15c8864d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 11 Nov 2020 23:34:41 -0500 Subject: [PATCH 0820/1087] Change some log levels in ComplexScalar Let routine conversions via readSQL/writeSQL/parse/toString be logged at FINE level rather than INFO, so as not to spam the CI logs. --- .../pljava/example/annotation/ComplexScalar.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 54b92cb1..b6bfda21 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -139,7 +139,7 @@ public static ComplexScalar parse(String input, String typeName) && tz.nextToken() == StreamTokenizer.TT_NUMBER) { double y = tz.nval; if (tz.nextToken() == ')') { - s_logger.info(typeName + " from string"); + s_logger.fine(typeName + " from string"); return new ComplexScalar(x, y, typeName); } } @@ -290,7 +290,7 @@ public String getSQLTypeName() { @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL) @Override public void readSQL(SQLInput stream, String typeName) throws SQLException { - s_logger.info(typeName + " from SQLInput"); + s_logger.fine(typeName + " from SQLInput"); m_x = stream.readDouble(); m_y = stream.readDouble(); m_typeName = typeName; @@ -299,7 +299,7 @@ public void readSQL(SQLInput stream, String typeName) throws SQLException { @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL) @Override public String toString() { - s_logger.info(m_typeName + " toString"); + s_logger.fine(m_typeName + " toString"); StringBuffer sb = new StringBuffer(); sb.append('('); sb.append(m_x); @@ -312,7 +312,7 @@ public String toString() { @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL) @Override public void writeSQL(SQLOutput stream) throws SQLException { - s_logger.info(m_typeName + " to SQLOutput"); + s_logger.fine(m_typeName + " to SQLOutput"); stream.writeDouble(m_x); stream.writeDouble(m_y); } From 14c85d229b43fe4274526da562275ab72dd0218d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 12 Nov 2020 21:29:07 -0500 Subject: [PATCH 0821/1087] Fix a copy/pasto and other minor doc fixes The copy/pasto (magnitudeNE rather than componentsNE) had no functional effect, but was a bit sloppy. Some of the annotations (including some of the oldest original ones) were missing @Documented, and therefore not showing up in javadocs. (Aggregate and BaseUDT are used in ComplexScalar, edited in this PR, so those changes have an excuse to be included here. MappedUDT and Trigger aren't, but it's the same change.) I am tempted by the idea of removing @Documented from @SQLAction ... its values are usually longwinded, and mess up the formatting of javadocs, and might just be better looked up in the source when somebody wants to know. But not making that change just now. --- .../java/org/postgresql/pljava/annotation/Aggregate.java | 1 + .../main/java/org/postgresql/pljava/annotation/BaseUDT.java | 5 +++-- .../java/org/postgresql/pljava/annotation/MappedUDT.java | 5 +++-- .../main/java/org/postgresql/pljava/annotation/Trigger.java | 6 ++++-- .../postgresql/pljava/example/annotation/ComplexScalar.java | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java index d92f02d8..7479c17b 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Aggregate.java @@ -123,6 +123,7 @@ enum FinishEffect { READ_ONLY, SHAREABLE, READ_WRITE }; * explicit. The two-string form with {@code ""} as the schema represents * an explicitly non-schema-qualified name. */ + @Documented @Target({}) @Retention(RetentionPolicy.CLASS) @interface Plan diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java index 7f65883a..87fb5550 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2020 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,7 @@ */ package org.postgresql.pljava.annotation; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -64,7 +65,7 @@ * be used in their {@code @Function} annotations and this annotation, to make * the order come out right. */ -@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) @Documented public @interface BaseUDT { /** diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/MappedUDT.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/MappedUDT.java index 69563f84..fcd168a5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/MappedUDT.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/MappedUDT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2020 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,7 @@ */ package org.postgresql.pljava.annotation; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -42,7 +43,7 @@ * of the class being annotated, and found in {@link #schema schema} if * specified, or by following the search path) to the annotated class. */ -@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) @Documented public @interface MappedUDT { /** diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java index ab037ca6..5c629389 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Trigger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2020 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 @@ -9,9 +9,11 @@ * Contributors: * Tada AB * Purdue University + * Chapman Flack */ package org.postgresql.pljava.annotation; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -40,7 +42,7 @@ * the complete transition tables on each invocation. * @author Thomas Hallgren */ -@Target({}) @Retention(RetentionPolicy.CLASS) +@Target({}) @Retention(RetentionPolicy.CLASS) @Documented public @interface Trigger { /** diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index b6bfda21..d664e014 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -224,7 +224,7 @@ public static boolean magnitudeLT(ComplexScalar a, ComplexScalar b) provides = "complex relationals" ) @Operator( - name = "javatest.<>", synthetic = "javatest.magnitudeNE", + name = "javatest.<>", synthetic = "javatest.componentsNE", commutator = SELF, provides = "complex relationals" ) @Function( From 3500ea6f0710304a5a0e6291cb71656f007b3816 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 14 Nov 2020 19:15:38 -0500 Subject: [PATCH 0822/1087] Fix some broken doc links Some result from modularizing code for Java 9, which adds one new directory level in apidocs URLs, and some required adding new HTML anchors by hand in the Javadocs where they are linked into from Markdown docs, because Maven's Markdown processor insists on corrupting URLs of the form Javadoc generates for method details, as noted earlier in 5565a3c. This is irritating and hardly scalable, but for now there aren't that many places, so the anchors are added. It's possible Maven's Markdown processor doesn't get 100% of the blame; it looks as if Javadoc 15 produces different fragment IDs for method signatures than whichever previous version did that was used when these links were made. In passing, noticed that the noun was missing from several of the descriptions in Commands.java: "The replace_jar will replace ..." and so on. That raises the question what noun to use. Command? Function? 9075-13 calls them 'procedures', which isn't entirely satisfying (some of them return stuff), but it's the standard, so 'procedures' it is. --- .../pljava/annotation/Function.java | 3 +- .../postgresql/pljava/example/saxon/S9.java | 18 ++++--- .../pljava/management/Commands.java | 52 ++++++++++--------- src/site/markdown/develop/coercion.md | 12 ++--- src/site/markdown/examples/saxon.md | 12 ++--- src/site/markdown/use/hello.md.vm | 2 +- src/site/markdown/use/parallel.md | 2 +- src/site/markdown/use/sqlxml.md | 2 +- 8 files changed, 56 insertions(+), 47 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java index b8ec4895..9e9ad07d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java @@ -191,7 +191,8 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; String language() default ""; /** - * Whether the function is UNSAFE to use in any parallel query plan at all + * Whether the function is UNSAFE to use in any + * parallel query plan at all * (the default), or avoids all disqualifying operations and so is SAFE to * execute anywhere in a parallel plan, or, by avoiding some such * operations, may appear in parallel plans but RESTRICTED to execute only diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java index 8f530193..fc3fb521 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/saxon/S9.java @@ -915,7 +915,8 @@ public void close() } /** - * Produce and return one row of the {@code XMLTABLE} result table per call. + * Produce and return one row of + * the {@code XMLTABLE} result table per call. *

    * The row expression has already been compiled and its evaluation begun, * producing a sequence iterator. The column XQuery expressions have all @@ -2941,7 +2942,8 @@ public void onGroupEnd(int groupNumber) } /** - * Function form of the ISO SQL {@code }. + * Function form of the ISO SQL + * {@code }. *

    * Rewrite the standard form *

    @@ -2995,7 +2997,8 @@ public static boolean like_regex(
     	}
     
     	/**
    -	 * Syntax-sugar-free form of the ISO SQL {@code OCCURRENCES_REGEX} function:
    +	 * Syntax-sugar-free form of the ISO SQL
    +	 * {@code OCCURRENCES_REGEX} function:
     	 * how many times does a pattern occur in a string?
     	 *

    * Rewrite the standard form @@ -3056,7 +3059,8 @@ public static int occurrences_regex( } /** - * Syntax-sugar-free form of the ISO SQL {@code POSITION_REGEX} function: + * Syntax-sugar-free form of the ISO SQL + * {@code POSITION_REGEX} function: * where does a pattern, or part of it, occur in a string? *

    * Rewrite the standard forms @@ -3143,7 +3147,8 @@ public static int position_regex( } /** - * Syntax-sugar-free form of the ISO SQL {@code SUBSTRING_REGEX} function: + * Syntax-sugar-free form of the ISO SQL + * {@code SUBSTRING_REGEX} function: * return a substring specified by a pattern match in a string. *

    * Rewrite the standard form @@ -3229,7 +3234,8 @@ public static String substring_regex( } /** - * Syntax-sugar-free form of the ISO SQL {@code TRANSLATE_REGEX} function: + * Syntax-sugar-free form of the ISO SQL + * {@code TRANSLATE_REGEX} function: * return a string constructed from the input string by replacing one * specified occurrence, or all occurrences, of a matching pattern. *

    diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index d6fec76f..c7b65971 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -69,13 +69,14 @@ import static org.postgresql.pljava.annotation.Function.Security.DEFINER; /** - * This methods of this class are implementations of SQLJ commands. - *

    SQLJ functions

    + * This methods of this class are implementations of SQLJ procedures (and some + * related ones beyond what ISO 9075-13 specifies). + *

    SQLJ procedures

    *

    install_jar

    - * The install_jar command loads a jar file from a location appointed by an URL - * or a binary image that constitutes the contents of a jar file into the SQLJ - * jar repository. It is an error if a jar with the given name already exists in - * the repository. + * The install_jar procedure loads a jar file from a location appointed by an + * URL or a binary image that constitutes the contents of a jar file into the + * SQLJ jar repository. It is an error if a jar with the given name already + * exists in the repository. *

    Usage 1

    *
    SELECT sqlj.install_jar(<jar_url>, <jar_name>, <deploy>); *
    @@ -121,8 +122,9 @@ * * *

    replace_jar

    - * The replace_jar will replace a loaded jar with another jar. Use this command - * to update already loaded files. It's an error if the jar is not found. + * The replace_jar procedure will replace a loaded jar with another jar. Use + * this command to update already loaded files. It's an error if the jar is not + * found. *

    Usage 1

    *
    SELECT sqlj.replace_jar(<jar_url>, <jar_name>, <redeploy>); *
    @@ -168,9 +170,9 @@ * * *

    remove_jar

    - * The remove_jar will drop the jar from the jar repository. Any classpath that - * references this jar will be updated accordingly. It's an error if the jar is - * not found. + * The remove_jar procedure will drop the jar from the jar repository. Any + * classpath that references this jar will be updated accordingly. It's an error + * if no such jar is installed. *

    Usage

    *
    SELECT sqlj.remove_jar(<jar_name>, <undeploy>); *
    @@ -188,9 +190,9 @@ * * *

    get_classpath

    - * The get_classpath will return the classpath that has been defined for the - * given schema or NULL if the schema has no classpath. It's an error if the - * given schema does not exist. + * The get_classpath procedure will return the classpath that has been defined + * for the given schema or NULL if the schema has no classpath. It's an error if + * the given schema does not exist. *

    Usage

    *
    SELECT sqlj.get_classpath(<schema>); *
    @@ -201,11 +203,11 @@ * The name of the schema * * - *

    set_classpath

    - * The set_classpath will define a classpath for the given schema. A classpath - * consists of a colon separated list of jar names. It's an error if the given - * schema does not exist or if one or more jar names references non existent - * jars. + *

    set_classpath

    + * The set_classpath procedure will define a classpath for the given schema. A + * classpath consists of a colon separated list of jar names. It's an error if + * the given schema does not exist or if one or more jar names references + * non-existent jars. *

    Usage

    *
    SELECT sqlj.set_classpath(<schema>, <classpath>); *
    @@ -220,9 +222,9 @@ * The colon separated list of jar names * * - *

    add_type_mapping

    - * The add_type_mapping defines the mapping between an SQL type and a Java - * class. + *

    add_type_mapping

    + * The add_type_mapping procedure defines the mapping between an SQL type and a + * Java class. *

    Usage

    *
    SELECT sqlj.add_type_mapping(<sqlTypeName>, <className>); *
    @@ -241,8 +243,8 @@ * * *

    drop_type_mapping

    - * The drop_type_mapping removes the mapping between an SQL type and a Java - * class. + * The drop_type_mapping procedure removes the mapping between an SQL type and a + * Java class. *

    Usage

    *
    SELECT sqlj.drop_type_mapping(<sqlTypeName>); *
    @@ -256,7 +258,7 @@ * * *

    alias_java_language

    - * The {@link #aliasJavaLanguage alias_java_language command} issues + * The {@link #aliasJavaLanguage alias_java_language procedure} issues * a PostgreSQL {@code CREATE LANGUAGE} command to define a named "language" * that is an alias for PL/Java. The name can appear in the * Java security policy to grant diff --git a/src/site/markdown/develop/coercion.md b/src/site/markdown/develop/coercion.md index 295d4f46..edd86bf3 100644 --- a/src/site/markdown/develop/coercion.md +++ b/src/site/markdown/develop/coercion.md @@ -20,13 +20,13 @@ begin. The standard also provides an `SQLJ.ALTER_JAVA_PATH` function that gives complete control, based on the jar where a search begins, of which other jars should be searched for dependencies. -By contrast, PL/Java (through and including 1.5) *does not* include the +By contrast, PL/Java (through and including 1.6) *does not* include the jar name in `AS` clauses, and provides an [`SQLJ.SET_CLASSPATH`][scp] function that can set a distinct class path for any schema in the database. The schema `public` can also have a class path, which becomes the fallback for any search that is not resolved on another schema's class path. -[scp]: ../pljava/apidocs/index.html?org/postgresql/pljava/management/Commands.html#setClassPath(java.lang.String,%20java.lang.String) +[scp]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#set_classpath The class named in an SQL function declaration's `AS` clause is looked up on the *class path for the schema in which the function is declared*, with @@ -41,8 +41,8 @@ in PL/Java with the [@BaseUDT annotation][baseudt]), which is completely integrated into PostgreSQL's type system and is usable from in or out of Java just like any other PostgreSQL type. -[basetype]: http://www.postgresql.org/docs/current/static/sql-createtype.html#AEN80283 -[baseudt]: ../pljava-api/apidocs/index.html?org/postgresql/pljava/annotation/BaseUDT.html +[basetype]: http://www.postgresql.org/docs/9.5/static/sql-createtype.html#AEN81321 +[baseudt]: ../pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/BaseUDT.html For the other flavors of user-defined type (described below), [`SQLJ.ADD_TYPE_MAPPING`][atm] (a PL/Java function, not in the standard) must @@ -50,8 +50,8 @@ be called to record the connection between the new type's SQL name and the Java class that implements it. The [@MappedUDT annotation][mappedudt] generates a call to this function along with any other SQL commands declaring the type. -[atm]: ../pljava/apidocs/index.html?org/postgresql/pljava/management/Commands.html#addTypeMapping(java.lang.String,%20java.lang.String) -[mappedudt]: ../pljava-api/apidocs/index.html?org/postgresql/pljava/annotation/MappedUDT.html +[atm]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#add_type_mapping +[mappedudt]: ../pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/MappedUDT.html What it records is simply the SQL type name as a string, and the Java class name as a string, and these mappings apply database-wide. But internally, diff --git a/src/site/markdown/examples/saxon.md b/src/site/markdown/examples/saxon.md index 83ac7afc..213ef919 100644 --- a/src/site/markdown/examples/saxon.md +++ b/src/site/markdown/examples/saxon.md @@ -499,16 +499,16 @@ the encumbrance. [j9cds]: ../install/oj9vmopt.html#How_to_set_up_class_sharing_in_OpenJ9 [Saxon-HE]: http://www.saxonica.com/html/products/products.html [ptwp]: https://github.com/tada/pljava/wiki/Performance-tuning -[assignrowvalues]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#assignRowValues-java.sql.ResultSet-int- +[assignrowvalues]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#assignRowValues [xqre]: https://www.w3.org/TR/xpath-functions-31/#regex-syntax [xsre]: https://www.w3.org/TR/xmlschema-2/#regexs [xqflags]: https://www.w3.org/TR/xpath-functions-31/#flags [uts18rl16]: http://www.unicode.org/reports/tr18/#RL1.6 -[lrx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#like_regex-java.lang.String-java.lang.String-java.lang.String-boolean- -[orx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#occurrences_regex-java.lang.String-java.lang.String-java.lang.String-int-boolean-boolean- -[prx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#position_regex-java.lang.String-java.lang.String-java.lang.String-int-boolean-boolean-int-int-boolean- -[srx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#substring_regex-java.lang.String-java.lang.String-java.lang.String-int-boolean-int-int-boolean- -[trx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#translate_regex-java.lang.String-java.lang.String-java.lang.String-java.lang.String-int-boolean-int-boolean- +[lrx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#like_regex +[orx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#occurrences_regex +[prx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#position_regex +[srx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#substring_regex +[trx]: ../pljava-examples/apidocs/org/postgresql/pljava/example/saxon/S9.html#translate_regex [saxmatrix]: https://www.saxonica.com/html/products/feature-matrix-9-9.html [xqexpr]: https://www.w3.org/TR/xquery-31/#id-expressions [xqmainmod]: https://www.w3.org/TR/xquery-31/#dt-main-module diff --git a/src/site/markdown/use/hello.md.vm b/src/site/markdown/use/hello.md.vm index cc606f2f..e37ae7b0 100644 --- a/src/site/markdown/use/hello.md.vm +++ b/src/site/markdown/use/hello.md.vm @@ -294,7 +294,7 @@ The [@Function annotation][funcanno] declares that the `hello` function should be available from SQL, so a `pljava.ddr` file will be added to the jar, containing the SQL commands to make that happen. -[funcanno]: ../pljava-api/apidocs/index.html?org/postgresql/pljava/annotation/Function.html +[funcanno]: ../pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/Function.html One more try with `mvn clean package` and there it is: diff --git a/src/site/markdown/use/parallel.md b/src/site/markdown/use/parallel.md index 083228bb..861a621b 100644 --- a/src/site/markdown/use/parallel.md +++ b/src/site/markdown/use/parallel.md @@ -26,7 +26,7 @@ of the query may execute in parallel, but the part that calls the `RESTRICTED` function will be executed only in the lead process. A function labeled `SAFE` may be executed in every process participating in the query. -[paranno]: ../pljava-api/apidocs/index.html?org/postgresql/pljava/annotation/Function.html#parallel() +[paranno]: ../pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/Function.html#parallel ### Parallel setup cost diff --git a/src/site/markdown/use/sqlxml.md b/src/site/markdown/use/sqlxml.md index acf925f4..c766cfd6 100644 --- a/src/site/markdown/use/sqlxml.md +++ b/src/site/markdown/use/sqlxml.md @@ -468,7 +468,7 @@ Java's extensive support for XML. [OWASP]: https://www.owasp.org/index.php/About_The_Open_Web_Application_Security_Project [cheat]: https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java -[adjx]: ../pljava-api/apidocs/index.html?org/postgresql/pljava/Adjusting.XML.html +[adjx]: ../pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/Adjusting.XML.html [jaxps]: https://docs.oracle.com/en/java/javase/13/security/java-api-xml-processing-jaxp-security-guide.html [catapi]: https://docs.oracle.com/javase/9/core/xml-catalog-api1.htm#JSCOR-GUID-51446739-F878-4B70-A36F-47FBBE12A26A From 05a0604125bf55c6fb6d9463c7bf76d6817c0e47 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 15 Nov 2020 21:08:31 -0500 Subject: [PATCH 0823/1087] Better workaround for javac 10 and 11 issue Revert the smelly workaround added in c763cee for javac 10 and 11 misbehaving when passed a --release naming an earlier release, and add a better workaround in DDRProcessor itself. Now it is possible to build for --release 9 whichever toolchain is used for the build, and that was the original intent and so is much more satisfying. It turned out the same issue (unsurprisingly, in retrospect) would also bite people building user code, and to have no better answer for them than "fudge your target release if building with 10 or 11" would be hard to stomach. Addresses #328. --- .../annotation/processing/DDRProcessor.java | 87 +++++++++++++------ pom.xml | 36 -------- 2 files changed, 59 insertions(+), 64 deletions(-) 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 d8ef2404..f3f5ce2a 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 @@ -89,6 +89,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.ModuleElement; import javax.lang.model.element.NestingKind; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; @@ -311,27 +312,17 @@ class DDRProcessorImpl snippetTiebreaker = reproducible ? new SnippetTiebreaker() : null; - TY_ITERATOR = typu.getDeclaredType( - elmu.getTypeElement( java.util.Iterator.class.getName())); - TY_OBJECT = typu.getDeclaredType( - elmu.getTypeElement( Object.class.getName())); - TY_RESULTSET = typu.getDeclaredType( - elmu.getTypeElement( java.sql.ResultSet.class.getName())); - TY_RESULTSETPROVIDER = typu.getDeclaredType( - elmu.getTypeElement( ResultSetProvider.class.getName())); - TY_RESULTSETHANDLE = typu.getDeclaredType( - elmu.getTypeElement( ResultSetHandle.class.getName())); - TY_SQLDATA = typu.getDeclaredType( - elmu.getTypeElement( SQLData.class.getName())); - TY_SQLINPUT = typu.getDeclaredType( - elmu.getTypeElement( SQLInput.class.getName())); - TY_SQLOUTPUT = typu.getDeclaredType( - elmu.getTypeElement( SQLOutput.class.getName())); - TY_STRING = typu.getDeclaredType( - elmu.getTypeElement( String.class.getName())); - TY_TRIGGERDATA = typu.getDeclaredType( - elmu.getTypeElement( TriggerData.class.getName())); - TY_VOID = typu.getNoType( TypeKind.VOID); + TY_ITERATOR = declaredTypeForClass(java.util.Iterator.class); + TY_OBJECT = declaredTypeForClass(Object.class); + TY_RESULTSET = declaredTypeForClass(java.sql.ResultSet.class); + TY_RESULTSETPROVIDER = declaredTypeForClass(ResultSetProvider.class); + TY_RESULTSETHANDLE = declaredTypeForClass(ResultSetHandle.class); + TY_SQLDATA = declaredTypeForClass(SQLData.class); + TY_SQLINPUT = declaredTypeForClass(SQLInput.class); + TY_SQLOUTPUT = declaredTypeForClass(SQLOutput.class); + TY_STRING = declaredTypeForClass(String.class); + TY_TRIGGERDATA = declaredTypeForClass(TriggerData.class); + TY_VOID = typu.getNoType(TypeKind.VOID); AN_FUNCTION = elmu.getTypeElement( Function.class.getName()); AN_SQLTYPE = elmu.getTypeElement( SQLType.class.getName()); @@ -376,6 +367,52 @@ void msg( Kind kind, Element e, AnnotationMirror a, AnnotationValue v, msgr.printMessage( kind, String.format( fmt, args), e, a, v); } + /** + * Map a {@code Class} to a {@code TypeElement} and from there to a + * {@code DeclaredType}. + *

    + * This needs to work around some weird breakage in javac 10 and 11 when + * given a {@code --release} option naming an earlier release, as described + * in commit c763cee. The version of of {@code getTypeElement} with a module + * parameter is needed then, because the other version will go bonkers and + * think it found the class in every module that transitively requires + * its actual module and then return null because the result wasn't + * unique. That got fixed in Java 12, but because 11 is the LTS release and + * there won't be another for a while yet, it is better to work around the + * issue here. + *

    + * If not supporting Java 10 or 11, this could be simplified to + * {@code typu.getDeclaredType(elmu.getTypeElement(className))}. + */ + private DeclaredType declaredTypeForClass(Class clazz) + { + String className = clazz.getName(); + String moduleName = clazz.getModule().getName(); + + TypeElement e; + + if ( null == moduleName ) + e = elmu.getTypeElement(className); + else + { + ModuleElement m = elmu.getModuleElement(moduleName); + if ( null == m ) + e = elmu.getTypeElement(className); + else + e = elmu.getTypeElement(m, className); + } + + if ( null == e ) + throw new AssertionError("Boo!"); + + DeclaredType t = typu.getDeclaredType(e); + + if ( null == t ) + throw new AssertionError("No TypeElement for " + e + "?"); + + return t; + } + /** * Key usable in a mapping from (Object, Snippet-subtype) to Snippet. * Because there's no telling in which order a Map implementation will @@ -5274,13 +5311,7 @@ private TypeMirror typeMirrorFromClass( Class k) return null; } - TypeElement te = elmu.getTypeElement( cname); - if ( null == te ) - { - msg( Kind.WARNING, "Found no TypeElement for %s", cname); - return null; // hope it wasn't one we'll need! - } - return te.asType(); + return declaredTypeForClass(k); } /** diff --git a/pom.xml b/pom.xml index f32deee8..9a7e0f40 100644 --- a/pom.xml +++ b/pom.xml @@ -69,42 +69,6 @@ - - release10 - - [10,11) - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 10 - - - - - - - - release11 - - [11,12) - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 11 - - - - - - nashorngone From 056e4b986e8216029faa7f3ee7cddd21696bc79e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 00:05:33 -0500 Subject: [PATCH 0824/1087] Improve two shouldn't-happen messages Those weren't quite what you'd call polished. --- .../pljava/annotation/processing/DDRProcessor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 f3f5ce2a..2a0b155a 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 @@ -402,13 +402,13 @@ private DeclaredType declaredTypeForClass(Class clazz) e = elmu.getTypeElement(m, className); } - if ( null == e ) - throw new AssertionError("Boo!"); + requireNonNull(e, + () -> "unexpected failure to resolve TypeElement " + className); DeclaredType t = typu.getDeclaredType(e); - if ( null == t ) - throw new AssertionError("No TypeElement for " + e + "?"); + requireNonNull(t, + () -> "unexpected failure to resolve DeclaredType " + e); return t; } From 7ea30ee576aacf7a9259409f0d5bf15f49786a4f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 17:15:36 -0500 Subject: [PATCH 0825/1087] Remove reference to workaround in versions.md --- src/site/markdown/build/versions.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index ccf79f7b..d5d36fb9 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -17,16 +17,7 @@ feature introduced in that release. In the PL/Java 1.6.x series, the build can be done with Java 9 or newer. Once built, PL/Java is able to use another Java 9 or later JVM at run time, simply by setting -[the `pljava.libjvm_location` variable][jvml] to the desired version's library -(but see the exceptions described next). - -### Exceptions to build-version / runtime-version compatibility - -Because of compiler bugs in Java 10 and 11, builds done with those versions -will not run on earlier Java releases. A build on 10 requires 10 or later at -run time; a build on 11 requires 11 or later at run time. To ensure that the -built extension can use any Java 9 or later at run time, it must be built on -Java 9, or on Java 12 or later. +[the `pljava.libjvm_location` variable][jvml] to the desired version's library. PL/Java can run application code written for a later Java version than PL/Java itself was built with, as long as that later JRE version is used at run time. From 873c6985a53a7b87552eb1907c8dd0376e22f953 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 17:43:20 -0500 Subject: [PATCH 0826/1087] Split off pre-1.6 release notes in REL1_6_STABLE --- src/site/markdown/releasenotes-pre1_6.md.vm | 1593 +++++++++++++++++++ src/site/markdown/releasenotes.md.vm | 1573 +----------------- 2 files changed, 1594 insertions(+), 1572 deletions(-) create mode 100644 src/site/markdown/releasenotes-pre1_6.md.vm diff --git a/src/site/markdown/releasenotes-pre1_6.md.vm b/src/site/markdown/releasenotes-pre1_6.md.vm new file mode 100644 index 00000000..a72f2001 --- /dev/null +++ b/src/site/markdown/releasenotes-pre1_6.md.vm @@ -0,0 +1,1593 @@ +# Release notes, releases prior to PL/Java 1.6 + +#set($h2 = '##') +#set($h3 = '###') +#set($h4 = '####') +#set($h5 = '#####') +#set($gborgbug = 'http://web.archive.org/web/20061208113236/http://gborg.postgresql.org/project/pljava/bugs/bugupdate.php?') +#set($pgfbug = 'https://web.archive.org/web/*/http://pgfoundry.org/tracker/?func=detail&atid=334&group_id=1000038&aid=') +#set($pgffeat = 'https://web.archive.org/web/*/http://pgfoundry.org/tracker/?func=detail&atid=337&group_id=1000038&aid=') +#set($ghbug = 'https://github.com/tada/pljava/issues/') +#set($ghpull = 'https://github.com/tada/pljava/pull/') + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + +$h2 PL/Java 1.5.6 + +This release adds support for PostgreSQL 13. + +It includes improvements to the JDBC 4.0 `java.sql.SQLXML` API that first became +available in 1.5.1, an update of the ISO SQL/XML examples based on the Saxon +product to Saxon 10 (which now includes support for XML Query higher-order +functions in the freely-licensed Saxon-HE), some improvements to internals, +and a number of bug fixes. + +$h3 Version compatibility + +PL/Java 1.5.6 can be built against recent PostgreSQL versions including 13, +and older ones back to 8.2, using Java SE 8 or later. The source code avoids +features newer than Java 6, so building with Java 7 or 6 should also be +possible, but is no longer routinely tested. The Java version used at runtime +does not have to be the same version used for building. PL/Java itself can run +on any Java version 6 or later if built with Java 11 or earlier; it can run +on Java 7 or later if built with Java 12 or later. PL/Java functions can be +written for, and use features of, whatever Java version will be loaded at run +time. See [version compatibility][versions] for more detail. + +PL/Java 1.5.6 cannot be built with Java 15 or later, as the Nashorn JavaScript +engine used in the build process no longer ships with Java 15. It can be built +with [GraalVM][], if `-Dpolyglot.js.nashorn-compat` is added to the `mvn` +command line. It will run on Java 15 if built with an earlier JDK or with Graal. + +When used with GraalVM as the runtime VM, PL/Java functions can use Graal's +"polyglot" capabilities to execute code in any other language available on +GraalVM. In this release, it is not yet possible to directly declare a function +in a language other than Java. + +$h3 Changes + +$h4 Improvements to the `java.sql.SQLXML` type + +Additions to the `Adjusting.XML` API support +[limiting resource usage][xmlreslim] in XML processing, controlling +[resolution][xmlresolv] of external documents and resources, +[validation against a schema][xmlschema], and integration of an +[XML catalog][xmlcatalog] to locally satisfy requests for external documents. + +Corrections and new documentation of [whitespace handling][xmlws] in XML values +of `CONTENT` form, and implementation [limitations][xmlimpl]. + +$h4 Improvements to the Saxon-based ISO SQL/XML example functions + +Updated the dependency for these optional examples to Saxon 10. Probably the +most significant of the [Saxon 10 changes][saxon10], for PostgreSQL's purposes, +will be that the XQuery [higher-order function feature][xqhof] is now included +in the freely-licensed Saxon-HE, so that it is now possible without cost to +integrate a modern XQuery 3.1 implementation that is lacking only the +[schema-aware feature][xqsaf] and the [typed data feature][xqtdf] (for those, +the paid Saxon-EE product is needed), and the [static typing feature][xqstf] +(which is not in any Saxon edition). + +To compensate for delivering the higher-order function support in -HE, +Saxonica moved certain optimizations to -EE. This seems a justifiable trade, as +it is better for development purposes to have the more complete implementation +of the language, leaving better optimization to be bought if and when needed. + +Thanks to a tip from Saxon's developer, the returning of results to SQL is now +done in a way that may incur less copying in some cases. + +$h4 Internals + +* Many sources of warnings reported by the Java VM's `-Xcheck:jni` option have + been tracked down, making it practical to use `-Xcheck:jni` in testing. +* Reduced pressure on the garbage collector in management of references to + PostgreSQL native state. + +$h3 Enhancement requests addressed + +* Work around PostgreSQL [API breakage in EnterpriseDB 11](${ghbug}260) + +$h3 Bugs fixed + +* [Support of arrays in composite types](${ghbug}300) +* [Order-dependent behavior caching array types](${ghbug}310) +* [Date conversion errors possible with PostgreSQL 10 on Windows/MSVC](${ghbug}297) +* [Build issue with Windows/MinGW-w64](${ghbug}282) +* ["xmltable" with XML output column or parameter](${ghbug}280) +* [Google Summer of Code catches 15-year-old PL/Java bug](${ghbug}274) +* [Several bugs in SQLXML handling](${ghbug}272) +* Work around an exception from `Reference.clear` on OpenJ9 JVM +* Bugs in SQL generator when supplying a function parameter name, or the + `category`, `delimiter`, or `storage` attribute of a user-defined type. + +$h3 Updated PostgreSQL APIs tracked + +* Removal of `CREATE EXTENSION ... FROM unpackaged` +* `numvals` in `SPITupleTable` +* `detoast.h` +* `detoast_external_attr` + +$h3 Credits + +There is a PL/Java 1.5.6 thanks in part to +Christoph Berg, +Chapman Flack, +Kartik Ohri, +original creator Thomas Hallgren, +and the many contributors to earlier versions. + +The work of Kartik Ohri in summer 2020 was supported by Google Summer of Code. + +[xmlreslim]: use/sqlxml.html#Additional_adjustments_in_recent_Java_versions +[xmlresolv]: use/sqlxml.html#Supplying_a_SAX_or_DOM_EntityResolver_or_Schema +[xmlschema]: use/sqlxml.html#Validation_against_a_schema +[xmlcatalog]: use/sqlxml.html#Using_XML_Catalogs_when_running_on_Java_9_or_later +[xmlws]: use/sqlxml.html#Effect_on_parsing_of_whitespace +[xmlimpl]: use/sqlxml.html#Known_limitations +[saxon10]: https://www.saxonica.com/html/documentation/changes/v10/installation.html +[xqhof]: https://www.w3.org/TR/xquery-31/#id-higher-order-function-feature +[xqsaf]: https://www.w3.org/TR/xquery-31/#id-schema-aware-feature +[xqtdf]: https://www.w3.org/TR/xquery-31/#id-typed-data-feature +[xqstf]: https://www.w3.org/TR/xquery-31/#id-static-typing-feature + +$h2 PL/Java 1.5.5 (4 November 2019) + +This bug-fix release fixes runtime issues reported in 32-bit `i386` builds, some +of which would not affect a more common 64-bit architecture, but some of which +could under the wrong circumstances, so this release should be used in +preference to 1.5.4 or 1.5.3 on any architecture. + +It is featurewise identical to 1.5.4, so those release notes, below, should be +consulted for the details of user-visible changes. + +Thanks to Christoph Berg for the `i386` testing that exposed these issues. + +$h3 Bugs fixed + +* [32bit i386 segfault](${ghbug}246) + +$h2 PL/Java 1.5.4 (29 October 2019) + +This minor release fixes a build issue reported with Java 11, and adds +support for building with Java 13. Issues with building the javadocs in later +Java versions are resolved. A work-in-progress feature that can +[apply the SQLXML API to other tree-structured data types](use/xmlview.html) +is introduced. + +Documentation updates include coverage of +[changes to Application Class Data Sharing](install/appcds.html) in recent +Hotspot versions, and ahead-of-time compilation using +[jaotc](install/vmoptions.html#a-XX:AOTLibrary). + +Otherwise, the release notes for 1.5.3, below, should be +consulted for the details of recent user-visible changes. + +$h3 Bugs fixed + +* [Build failure with Java 11 and --release](${ghbug}235) +* [Build with Java 13](${ghbug}236) +* [Javadoc build fails in Java 11+](${ghbug}239) +* [Javadoc build fails in Java 13](${ghbug}241) + +$h2 PL/Java 1.5.3 (4 October 2019) + +This release adds support for PostgreSQL 12, and removes the former +requirement to build with a Java release earlier than 9. + +It includes a rework of of threading and resource management, improvements to +the JDBC 4.0 `java.sql.SQLXML` API that first became available in 1.5.1, and +a substantially usable example providing the functionality of ISO SQL +`XMLEXISTS`, `XMLQUERY`, `XMLTABLE`, `XMLCAST`, `LIKE_REGEX`, +`OCCURRENCES_REGEX`, `POSITION_REGEX`, `SUBSTRING_REGEX`, and `TRANSLATE_REGEX`. +Some bugs are fixed. + +$h3 Version compatibility + +PL/Java 1.5.3 can be built against recent PostgreSQL versions including 12, +and older ones back to 8.2, using Java SE 8 or later. The source code avoids +features newer than Java 6, so building with Java 7 or 6 should also be +possible, but is no longer routinely tested. The Java version used at runtime +does not have to be the same version used for building. PL/Java itself can run +on any Java version 6 or later if built with Java 11 or earlier; it can run +on Java 7 or later if built with Java 12. PL/Java functions can be written for, +and use features of, whatever Java version will be loaded at run time. See +[version compatibility][versions] for more detail. + +When used with [GraalVM][] as the runtime VM, PL/Java functions can use its +"polyglot" capabilities to execute code in any other language available on +GraalVM. In this release, it is not yet possible to directly declare a function +in a language other than Java. + +$h3 Changes + +$h4 Threading/synchronization, finalizers, and new configuration variable + +Java is multithreaded while PostgreSQL is not, requiring ways to prevent +Java threads from entering PostgreSQL at the wrong times, while cleaning up +native resources in PostgreSQL when PL/Java references are released, and +_vice versa_. + +PL/Java has historically used an assortment of approaches including Java +object finalizers, which have long been deprecated informally, and formally +since Java 9. Finalizers enter PostgreSQL from a thread of their own, and the +synchronization approach used in PL/Java 1.5.2 and earlier has been associated +with occasional hangs at backend exit when using an OpenJ9 JVM at runtime. + +A redesigned approach using a new `DualState` class was introduced in 1.5.1, +at first only used in implementing the `java.sql.SQLXML` type, a newly-added +feature. In 1.5.3, other approaches used in the rest of PL/Java's code base are +migrated to use `DualState` also, and all uses of the deprecated Java object +finalizers have been retired. With the new techniques, the former occasional +OpenJ9 hangs have not been observed. + +This represents the most invasive change to PL/Java's thread synchronization +in many years, so it may be worthwhile to reserve extra time for +testing applications. + +A new [configuration variable](use/variables.html), +`pljava.java_thread_pg_entry`, allows adjusting the thread policy. The default +setting, `allow`, preserves PL/Java's former behavior, allowing Java threads +entry into PostgreSQL one at a time, only when any thread already in PG code +has entered or returned to Java. + +With object finalizers no longer used, PL/Java itself does not need the `allow` +mode, but there may be application code that does. Application code can be +tested by setting the `error` mode, which will raise an error for any attempted +entry to PG from a thread other than the original thread that launched PL/Java. +If an application runs in `error` mode with no errors, it can also be run in +`block` mode, which may be more efficient, as it eliminates many locking +operations that happen in `allow` or `error` mode. However, if `block` mode +is used with an application that has not been fully tested in `error` mode +first, and the application does attempt to enter PostgreSQL from a Java thread +other than the initial one, the result can be blocked threads or a deadlocked +backend that has to be killed. + +A JMX management client like `JConsole` or `jvisualvm` can identify threads that +are blocked, if needed. The new `DualState` class also offers some statistics +that can be viewed in `JConsole`, or `jvisualvm` with the `VisualVM-MBeans` +plugin. + +$h4 Improvements to the `java.sql.SQLXML` type + +Support for this JDBC 4.0 type was added in PL/Java 1.5.1. Release 1.5.3 +includes the following improvements: + +* A new ["Adjusting" API](use/sqlxml.html#Extended_API_to_configure_XML_parsers) + exposes configuration settings for Java XML parsers that may be created + internally during operations on `SQLXML` instances. That allows the default + settings to restrict certain XML parser features as advocated by the + [Open Web Application Security Project][OWASP] when XML content may be + coming from untrusted sources, with a simple API for relaxing those + restrictions when appropriate for XML content from a known source. +* It is now possible for a PL/Java function to return, pass into a + `PreparedStatement`, etc., an `SQLXML` instance that PL/Java did not create. + For example, a PL/Java function could use another database's JDBC driver to + obtain a `SQLXML` value from that database, and use that as its own return + value. Transparently, the content is copied to a PL/Java `SQLXML` instance. + The copy can also be done explicitly, allowing the "Adjusting" API to be + used if the default XML parser restrictions should be relaxed. +* Behavior when the server encoding is not UTF-8, or when it is not an + IANA-registered encoding (even if Java has a codec for it), has been + improved. + +$h4 Improvements to the Saxon-based ISO SQL/XML example functions + +Since PL/Java 1.5.1, the supplied examples have included a not-built-by-default +[example supplying ISO SQL/XML features missing from core PostgreSQL][exsaxon]. +It is not built by default because it raises the minimum Java version to 8, and +brings in the Saxon-HE XML-processing library. + +In 1.5.3, the example now provides versions of the ISO SQL `XMLEXISTS`, +`XMLQUERY`, `XMLTABLE`, and `XMLCAST` functions based on the W3C XQuery +language as ISO SQL specifies (while PostgreSQL has an "XMLTABLE" function +since release 10 and "XMLEXISTS" since 9.1, they have +[numerous limitations][appD31] inherited from a library that does not support +XQuery, and additional peculiarities prior to PostgreSQL 12), and the ISO SQL +`LIKE_REGEX`, `OCCURRENCES_REGEX`, `POSITION_REGEX`, `SUBSTRING_REGEX`, and +`TRANSLATE_REGEX` functions that apply XQuery regular expressions. It also +includes the `XMLTEXT` function, which is rather trivial, but also missing from +core PostgreSQL, and supplied here for completeness. + +As plain user-defined functions without special treatment in PostgreSQL's SQL +parser, these functions cannot be used with the exact syntax specified in +ISO SQL, but require simple rewriting into equivalent forms that are valid +ordinary PostgreSQL function calls. The rewritten forms are intended to be easy +to read and correspond closely to the ISO syntax. + +While still presented as examples and not a full implementation, these functions +are now intended to be substantially usable (subject to minor +[documented limits][exsaxon]), and testing and reports of shortcomings are +welcome. + +$h4 ResultSet holdability again + +A `ResultSet` obtained from a query done in PL/Java would return the value +`CLOSE_CURSORS_AT_COMMIT` to a caller of its `getHoldability` method, but in +reality would become unusable as soon as the PL/Java function creating it +returned to PostgreSQL. That was fixed in PL/Java 1.5.1 for a `ResultSet` +obtained from a `Statement`, but not for one obtained from a +`PreparedStatement`. It now correctly remains usable to the end of the +transaction in either case. + +$h4 Savepoint behavior at rollback + +Per JDBC, a `Savepoint` still exists after being used in a rollback, and can be +used again; the rollback only invalidates any `Savepoint` that had been created +after the one being rolled back. That should be familiar behavior, as it is the +same as PostgreSQL's own SQL `SAVEPOINT` behavior. It is also correct in pgJDBC, +which has test coverage to confirm it. PL/Java has been doing it wrong. + +In 1.5.3 it now has the JDBC-specified behavior. For compatibility with existing +application code, the meaning of the `pljava.release_lingering_savepoints` +[configuration variable](use/variables.html) has been adjusted. The setting +tells PL/Java what to do if a `Savepoint` still exists, neither released nor +rolled back, at the time a function exits. If `on`, the savepoint is released +(committed); if `off`, the savepoint is rolled back. A warning is issued in +either case. + +In an existing function that used savepoints and assumed that a rolled-back +savepoint would be no longer live, it will now be normal for such a savepoint +to reach the function exit still alive. To recognize this case, PL/Java tracks +whether any savepoint has been rolled back at least once. At function exit, any +savepoint that has been neither released nor ever rolled back is disposed of +according to the `release_lingering_savepoints` setting and with a warning, +as before, but any savepoint that has already been rolled back at least once +is simply released, regardless of the variable setting, and without producing +a warning. + +$h4 Control of function parameter names in generated SQL + +When generating the `CREATE FUNCTION` command in a deployment descriptor +according to an annotated Java function, PL/Java ordinarily gives the function +parameters names that match their Java names, unquoted. Because PostgreSQL +allows named notation when calling a function, the parameter names in its +declaration become part of its signature that cannot later be changed without +dropping and re-creating the function. + +In some cases, explicit control of the SQL parameter names may be wanted, +independently of the Java names: to align with an external standard, perhaps, +or when either the SQL or the Java name would collide with a reserved word. +For that purpose, the (already slightly overloaded) `@SQLType` annotation now +has a `name` attribute that can specify the SQL name of the annotated parameter. + +$h4 Documentation + +The user guide and guide for packagers contained incorrect instructions for +using Maven to build a single subproject of PL/Java (such as `pljava-api` or +`pljava-examples`) instead of the full project. Those have been corrected. + +$h3 Enhancement requests addressed + +* [Allow building with Java releases newer than 8](${ghbug}212) + +$h3 Bugs fixed + +* [ResultSet holdability still wrong when using PreparedStatement](${ghbug}209) +* [Can't return (or set/update PreparedStatement/ResultSet) non-PL/Java SQLXML object](${ghbug}225) +* [JDBC Savepoint behavior](${ghbug}228) +* Writing `SQLXML` via StAX when server encoding is not UTF-8 +* StAX rejecting server encoding if not an IANA-registered encoding +* Error handling when PL/Java startup fails + (may have been [issue 211](${ghbug}211)) +* SPI connection management for certain set-returning functions + +$h3 Updated PostgreSQL APIs tracked + +* Retirement of `dynloader.h` +* Retirement of magical Oids +* Retirement of `nabstime` +* Retirement of `pg_attrdef.adsrc` +* Extensible `TupleTableSlot`s +* `FunctionCallInfoBaseData` + +$h3 Credits + +There is a PL/Java 1.5.3 thanks in part to +Christoph Berg, +Chapman Flack, +`ppKrauss`, +original creator Thomas Hallgren, +and the many contributors to earlier versions. + +[GraalVM]: https://www.graalvm.org/ +[OWASP]: https://www.owasp.org/index.php/About_The_Open_Web_Application_Security_Project +[appD31]: https://www.postgresql.org/docs/12/xml-limits-conformance.html + +$h2 PL/Java 1.5.2 (5 November 2018) + +A pure bug-fix release, correcting a regression in 1.5.1 that was not caught +in pre-release testing, and could leave +[conversions between PostgreSQL `date` and `java.sql.Date`](${ghbug}199) off +by one day in certain timezones and times of the year. + +1.5.1 added support for the newer `java.time` classes from JSR 310 / JDBC 4.2, +which are [recommended as superior alternatives](use/datetime.html) to the +older conversions involving `java.sql.Date` and related classes. The new +versions are superior in part because they do not have hidden timezone +dependencies. + +However, the change to the historical `java.sql.Date` conversion behavior was +inadvertent, and is fixed in this release. + +$h3 Open issues with date/time/timestamp conversions + +During preparation of this release, other issues of longer standing were also +uncovered in the legacy conversions between PG `date`, `time`, and +`timestamp` classes and the `java.sql` types. They are detailed in +[issue #200](${ghbug}200). Because they are not regressions but long-established +behavior, they are left untouched in this release, and will be fixed in +a future release. + +The Java 8 `java.time` conversions are free of these issues as well. + +$h2 PL/Java 1.5.1 (17 October 2018) + +This release adds support for PostgreSQL 9.6, 10, and 11, +and plays more nicely with `pg_upgrade`. If a PostgreSQL installation +is to be upgraded using `pg_upgrade`, and is running a version of +PL/Java before 1.5.1, the PL/Java version should first be upgraded +in the running PostgreSQL version, and then the PostgreSQL `pg_upgrade` +can be done. + +The documentation is expanded on the topic of shared-memory precompiled +class cache features, which can substantially improve JVM startup time +and memory footprint, and are now available across Oracle Java, OpenJDK +with Hotspot, and OpenJDK with OpenJ9. When running on OpenJ9, PL/Java +cooperates with the JVM to include even the application's classes +(those loaded with `install_jar`) in the shared cache, something not +yet possible with Hotspot. While the advanced sharing feature in Oracle +Java is still subject to a commercial licensing encumbrance, the equivalent +(or superior, with OpenJ9) features in OpenJDK are not encumbered. + +Significant new functionality includes new datatype mapping support: +SQL `date`, `time`, and `timestamp` values can be mapped to the new +Java classes of the `java.time` package in Java 8 and later (JSR 310 / +JDBC 4.2), which are much more faithful representations of the values +in SQL. Values of `xml` type can be manipulated efficiently using the +JDBC 4.0 `SQLXML` API, supporting several different APIs for XML +processing in Java. + +For Java code that does not use the new date/time classes in the +`java.time` package, some minor conversion inaccuracies (less than +two seconds) in the older mapping to `java.sql.Timestamp` have been +corrected. + +Queries from PL/Java code now produce `ResultSet`s that are usable to the +end of the containing transaction, as they had already been claiming to be. + +With PostgreSQL 9.6 support comes the ability to declare functions +`PARALLEL { UNSAFE | RESTRICTED | SAFE }`, and with PG 10 support, +transition tables are available to triggers. + +$h3 Security + +$h4 Schema-qualification + +PL/Java now more consistently schema-qualifies objects in queries and DDL +it generates internally, as a measure of defense-in-depth in case the database +it is installed in has not been [protected][prot1058] from [CVE-2018-1058][]. + +_No schema-qualification work has been done on the example code._ If the +examples jar will be installed, it should be in a database that +[the recommended steps have been taken to secure][prot1058]. + +$h4 Some large-object code removed + +1.5.1 removes the code at issue in [CVE-2016-0768][], which pertained to +PostgreSQL large objects, but had never been documented or exposed as API. + +This is not expected to break any existing code at all, based on further +review showing the code in question had also been simply broken, since 2006, +with no reported issues in that time. That discovery would support an argument +for downgrading the severity of the reported vulnerability, but with no need +to keep that code around, it is more satisfying to remove it entirely. + +Developers wishing to manipulate large objects in PL/Java are able to do so +using the SPI JDBC interface and the large-object SQL functions already +available in every PostgreSQL version PL/Java currently supports. + +[CVE-2018-1058]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1058 +[prot1058]: https://wiki.postgresql.org/wiki/A_Guide_to_CVE-2018-1058:_Protect_Your_Search_Path#Next_Steps:_How_Can_I_Protect_My_Databases.3F + +$h3 Version compatibility + +PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11, +and older ones back to 8.2, using Java SE 8, 7, or 6. It can _run_ using newer +Java versions including Java 11. PL/Java functions can be written for, and use +features of, the Java version loaded at run time. See +[version compatibility][versions] for more detail. + +OpenJDK is supported, and can be downloaded in versions using the Hotspot or the +OpenJ9 JVM. Features of modern Java VMs can drastically reduce memory footprint +and startup time, in particular class-data sharing. Several choices of Java +runtime now offer such features: Oracle Java has a simple class data sharing +feature for the JVM itself, freely usable in all supported versions, and an +"application class data sharing" feature in Java 8 and later that can also share +the internal classes of PL/Java, but is a commercial feature requiring a +license from Oracle. As of Java 10, the same application class sharing feature +is present in OpenJDK/Hotspot, where it is freely usable without an additional +license. OpenJDK/OpenJ9 includes a different, and very sophisticated, class +sharing feature, freely usable from Java 8 onward. More on these features +can be found [in the installation docs][vmopts]. + +$h3 Changes + +$h4 Typing of parameters in prepared statements + +PL/Java currently does not determine the necessary types of `PreparedStatement` +parameters from the results of PostgreSQL's own type analysis of the query +(as a network client would, when using PostgreSQL's "extended query" protocol). +PostgreSQL added the means to do so in SPI only in PostgreSQL 9.0, and a future +PL/Java major release should use it. However, this release does make two small +changes to the current behavior. + +Without the query analysis results from PostgreSQL, PL/Java tries to type the +prepared-statement parameters based on the types of values supplied by the +application Java code. It now has two additional ways to do so: + +* If Java code supplies a Java user-defined type (UDT)---that is, an object + implementing the `SQLData` interface---PL/Java will now call the `SQLData` + method `getSQLTypeName` on that object and use the result to pin down + the PostgreSQL type of the parameter. Existing code should already provide + this method, but could, in the past, have returned a bogus result without + detection, as PL/Java did not use it. + +* Java code can use the three-argument form of `setNull` to specify the exact + PostgreSQL type for a parameter, and then another method can be used to + supply a non-null value for it. If the following non-null value has + a default mapping to a different PostgreSQL type, in most cases it will + overwrite the type supplied with `setNull` and re-plan the query. That was + PL/Java's existing behavior, and was not changed for this minor release. + However, the new types introduced in this release---the `java.time` types + and `SQLXML`---behave in the way that should become universal in a future + major release: the already-supplied PostgreSQL type will be respected, and + PL/Java will try to find a usable coercion to it. + +$h4 Inaccuracies converting TIMESTAMP and TIMESTAMPTZ + +When converting between PostgreSQL values of `timestamp` or `timestamptz` type +and the pre-Java 8 original JDBC type `java.sql.Timestamp`, there were cases +where values earlier than 1 January 2000 would produce exceptions rather than +converting successfully. Those have been fixed. + +Also, converting in the other direction, from `java.sql.Timestamp` to a +PostgreSQL timestamp, an error of up to 1.998 seconds (averaging 0.999) +could be introduced. + +That error has been corrected. If an application has stored Java `Timestamp`s +and corresponding SQL `timestamp`s generated in the past and requires them +to match, it could be affected by this change. + +$h4 New date/time/timestamp API in Java 8 `java.time` package + +The old, and still default, mappings in JDBC from the SQL `date`, `time`, and +`timestamp` types to `java.sql.Date`, `java.sql.Time`, and `java.sql.Timestamp`, +were never well suited to represent the PostgreSQL data types. The `Time` and +`Timestamp` classes were used to map both the with-timezone and without-timezone +variants of the corresponding SQL types and, clearly, could not represent both +equally well. These Java classes all contain timezone dependencies, requiring +the conversion to involve timezone calculations even when converting non-zoned +SQL types, and making the conversion results for non-zoned types implicitly +depend on the current PostgreSQL session timezone setting. + +Applications are strongly encouraged to adopt Java 8 as a minimum language +version and use the new-in-Java-8 types in the `java.time` package, which +eliminate those problems and map the SQL types much more faithfully. +For PL/Java function parameters and returns, the class in the method declaration +can simply be changed. For retrieving date/time/timestamp values from a +`ResultSet` or `SQLInput` object, use the variants of `getObject` / `readObject` +that take a `Class` parameter. The class to use is: + +| PostgreSQL type | `java.time` class | +|--:|:--| +|`date`|`LocalDate`| +|`time without time zone`|`LocalTime`| +|`time with time zone`|`OffsetTime`| +|`timestamp without time zone`|`LocalDateTime`| +|`timestamp with time zone`|`OffsetDateTime`| +[Correspondence of PostgreSQL date/time types and Java 8 `java.time` classes] + +Details on these mappings are [added to the documentation](use/datetime.html). + +$h4 Newly supported `java.sql.SQLXML` type + +PL/Java has not, until now, supported the JDBC 4.0 `SQLXML` type. PL/Java +functions have been able to work with PostgreSQL XML values by mapping them +as Java `String`, but that conversion could introduce character encoding issues +outside the control of the XML APIs, and also has memory implications if an +application stores, or generates in queries, large XML values. Even if the +processing to be done in the application could be structured to run in constant +bounded memory while streaming through the XML, a conversion to `String` +requires the whole, uncompressed, character-serialized value to be brought into +the Java heap at once, and any heap-size tuning has to account for that +worst-case size. The `java.sql.SQLXML` API solves those problems by allowing +XML manipulation with any of several Java XML APIs with the data remaining in +PostgreSQL native memory, never brought fully into the Java heap unless that is +what the application does. Heap sizing can be based on the just the +application's processing needs. + +The `SQLXML` type can take the place of `String` in PL/Java function parameters +and returns simply by changing their declarations from `String` to `SQLXML`. +When retrieving XML values from `ResultSet` or `SQLInput` objects, the legacy +`getObject / readObject` methods will continue to return `String` for existing +application compatibility, so the specific `getSQLXML / readSQLXML` methods, or +the forms of `getObject / readObject` with a `Class` parameter and passing +`SQLXML.class`, must be used. A [documentation page](use/sqlxml.html) has been +added, and the [PassXML example][exxml] illustrates use of the API. + +A [not-built-by-default new example][exsaxon] (because it depends on Java 8 and +the Saxon-HE XML-processing library) provides a partial implementation of true +`XMLQUERY` and `XMLTABLE` functions for PostgreSQL, using the standard-specified +XML Query language rather than the XPath 1.0 of the native PostgreSQL functions. + +[exxml]: pljava-examples/apidocs/index.html?org/postgresql/pljava/example/annotation/PassXML.html +[exsaxon]: examples/saxon.html + +$h4 New Java property exposes the PostgreSQL server character-set encoding + +A Java system property, `org.postgresql.server.encoding`, is set to the +canonical name of a supported Java `Charset` that corresponds to PostgreSQL's +`server_encoding` setting, if one can be found. If the server encoding's name +is not recognized as any known Java `Charset`, this property will be unset, and +some functionality, such as the `SQLXML` API, may be limited. If a Java +`Charset` does exist (or is made available through a `CharsetProvider`) that +does match the PostgreSQL server encoding, but is not automatically selected +because of a naming mismatch, the `org.postgresql.server.encoding` property can +be set (with a `-D` in `pljava.vmoptions`) to select it by name. + +$h4 ResultSet holdability + +A `ResultSet` obtained from a query done in PL/Java would return the value +`CLOSE_CURSORS_AT_COMMIT` to a caller of its `getHoldability` method, but in +reality would become unusable as soon as the PL/Java function creating it +returned to PostgreSQL. It now remains usable to the end of the transaction, +as claimed. + +$h4 PostgreSQL 9.6 and parallel query + +A function in PL/Java can now be [annotated][apianno] +`parallel={UNSAFE | RESTRICTED | SAFE}`, with `UNSAFE` the default. +A new [user guide section][ugparqry] explains the possibilities and +tradeoffs. (Good cases for marking a PL/Java function `SAFE` may be +rare, as pushing such a function into multiple background processes +will require them all to start JVMs. But if a practical application +arises, PostgreSQL's `parallel_setup_cost` can be tuned to help the +planner make good plans.) + +Although `RESTRICTED` and `SAFE` Java functions work in simple tests, +there has been no exhaustive audit of the code to ensure that PL/Java's +internal workings never violate the behavior constraints on such functions. +The support should be considered experimental, and could be a fruitful +area for beta testing. + +[ugparqry]: use/parallel.html + +$h4 Tuple counts widened to 64 bits with PostgreSQL 9.6 + +To accommodate the possibility of more than two billion tuples in a single +operation, the SPI implementation of the JDBC `Statement` interface now +provides the JDK 8-specified `executeLargeBatch` and `getLargeUpdateCount` +methods defined to return `long` counts. The original `executeBatch` and +`getUpdateCount` methods remain but, obviously, cannot return counts that +exceed `INT_MAX`. In case the count is too large, `getUpdateCount` will throw +an `ArithmeticException`; `executeBatch` will store `SUCCESS_NO_INFO` for +any statement in the batch that affected too many tuples to report. + +For now, a `ResultSetProvider` cannot be used to return more than `INT_MAX` +tuples, but will check that condition and throw an error to ensure predictable +behavior. + +$h4 `pg_upgrade` + +PL/Java should be upgraded to 1.5.1 in a database cluster, before that +cluster is binary-upgraded to a newer PostgreSQL version using `pg_upgrade`. +A new [Upgrading][upgrading] installation-guide section centralizes information +on both upgrading PL/Java in a database, and upgrading a database with PL/Java +in it. + +[upgrading]: install/upgrade.html + +$h4 Suppressing row operations from triggers + +In PostgreSQL, a `BEFORE ROW` trigger is able to allow the proposed row +operation, allow it with modified values, or silently suppress the operation +for that row. Way back in PL/Java 1.1.0, the way to produce the 'suppress' +outcome was for the trigger method to throw an exception. Since PL/Java 1.2.0, +however, an exception thrown in a trigger method is used to signal an error +to PostgreSQL, and there has not been a way to suppress the row operation. + +The `TriggerData` interface now has a [`suppress`][tgsuppress] method that +the trigger can invoke to suppress the operation for the row. + +[tgsuppress]: pljava-api/apidocs/index.html?org/postgresql/pljava/TriggerData.html#suppress() + +$h4 Constraint triggers + +New attributes in the `@Trigger` annotation allow the SQL generator to +create constraint triggers (a type of trigger that can be created with SQL +since PostgreSQL 9.1). Such triggers will be delivered by the PL/Java runtime +(to indicate that a constraint would be violated, a constraint trigger +method should throw an informative exception). However, the trigger method +will have access, through the `TriggerData` interface, only to the properties +common to ordinary triggers; methods on that interface to retrieve properties +specific to constraint triggers have not been added for this release. + +$h4 PostgreSQL 10 and trigger transition tables + +A trigger [annotation][apianno] can now specify `tableOld="`_name1_`"` or +`tableNew="`_name2_`"`, or both, and the PL/Java function servicing the +trigger can do SPI JDBC queries and see the transition table(s) under the +given name(s). The [triggers example code][extrig] has been extended with +a demonstration. + +[extrig]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java + +$h4 Logging from Java + +The way the Java logging system has historically been plumbed to PostgreSQL's, +as described in [issue 125](${ghbug}125), can be perplexing both because it +is unaffected by later changes to the PostgreSQL settings after PL/Java is +loaded in the session, and because it has honored only `log_min_messages` +and ignored `client_min_messages`. The second part is easy to fix, so in +1.5.1 the threshold where Java discards messages on the fast path is +determined by the finer of `log_min_messages` and `client_min_messages`. + +$h4 Conveniences for downstream package maintainers + +The `mvn` command to build PL/Java will now accept an option to provide +a useful default for `pljava.libjvm_location`, when building a package for +a particular software environment where the likely path to Java is known. + +The `mvn` command will also accept an option to specify, by the path to +the `pg_config` executable, the PostgreSQL version to build against, in +case multiple versions exist on the build host. This was already possible +by manipulating `PATH` ahead of running `mvn`, but the option makes it more +explicit. + +A new [packaging section][packaging] in the build guide documents those +and a number of considerations for making a PL/Java package. + +[packaging]: build/package.html + +$h3 Enhancement requests addressed + +$h4 In 1.5.1-BETA3 + +* [Add a ddr.reproducible option to SQL generator](${ghbug}186) + +$h4 In 1.5.1-BETA2 + +* [java 8 date/time api](${ghbug}137) +* [Annotations don't support CREATE CONSTRAINT TRIGGER](${ghbug}138) +* [Let annotations give defaults to row-type parameters](${ghpull}153) +* [Improve DDR generator on the dont-repeat-yourself dimension for UDT type mapping](${ghpull}159) +* [Support the JDBC 4.0 SQLXML type](${ghpull}171) + +$h3 Bugs fixed + +$h4 In 1.5.1-BETA3 + +* [self-install jar ClassCastException (...ConsString to String), some java 6/7 runtimes](${ghbug}179) +* [i386 libjvm_location gets mangled as .../jre/lib/1/server/libjvm.so](${ghbug}176) +* [java.lang.ClassNotFoundException installing examples jar](${ghbug}178) +* [Preprocessor errors building on Windows with MSVC](${ghbug}182) +* [Saxon example does not build since Saxon 9.9 released](${ghbug}185) +* [Segfault in VarlenaWrapper.Input on 32-bit](${ghbug}177) +* [Windows: self-install jar silently fails to replace existing files](${ghbug}189) +* [ERROR: java.sql.SQLException: _some Java class name_](${ghbug}192) +* [SetOfRecordTest with timestamp column influenced by environment ](${ghbug}195) + +$h4 In 1.5.1-BETA2 + +* [PostgreSQL 10: SPI_modifytuple failed with SPI_ERROR_UNCONNECTED](${ghbug}134) +* [SPIConnection prepareStatement doesn't recognize all parameters](${ghbug}136) +* [Ordinary (non-constraint) trigger has no way to suppress operation](${ghbug}142) +* [ResultSetHandle and column definition lists](${ghbug}146) +* [PreparedStatement doesn't get parameter types from PostgreSQL](${ghbug}149) + _(partial improvements)_ +* [internal JDBC: inaccuracies converting TIMESTAMP and TIMESTAMPTZ](${ghbug}155) +* [Missing type mapping for Java return `byte[]`](${ghbug}157) +* [The REMOVE section of DDR is in wrong order for conditionals](${ghbug}163) +* [Loading PL/Java reinitializes timeouts in PostgreSQL >= 9.3](${ghbug}166) +* [JDBC ResultSet.CLOSE_CURSORS_AT_COMMIT reported, but usable life shorter](${ghbug}168) + +$h4 In 1.5.1-BETA1 + +* [Add support for PostgreSQL 9.6](${ghbug}108) +* [Clarify documentation of ResultSetProvider](${ghbug}115) +* [`pg_upgrade` (upgrade failure from 9.5 to 9.6)](${ghbug}117) +* [Java logging should honor `client_min_messages` too](${ghbug}125) + +$h3 Updated PostgreSQL APIs tracked + +* `heap_form_tuple` +* 64-bit `SPI_processed` +* 64-bit `Portal->portalPos` +* 64-bit `FuncCallContext.call_cntr` +* 64-bit `SPITupleTable.alloced` and `.free` +* `IsBackgroundWorker` +* `IsBinaryUpgrade` +* `SPI_register_trigger_data` +* `SPI` without `SPI_push`/`SPI_pop` +* `AllocSetContextCreate` +* `DefineCustom...Variable` (no `GUC_LIST_QUOTE` in extensions) + +$h3 Credits + +There is a PL/Java 1.5.1 thanks in part to +Christoph Berg, +Thom Brown, +Luca Ferrari, +Chapman Flack, +Petr Michalek, +Steve Millington, +Kenneth Olson, +Fabian Zeindl, +original creator Thomas Hallgren, +and the many contributors to earlier versions. + +$h2 PL/Java 1.5.0 (29 March 2016) + +This, the first PL/Java numbered release since 1.4.3 in 2011, combines +compatibility with the latest PostgreSQL and Java versions with modernized +build and installation procedures, automatic generation of SQL deployment +code from Java annotations, and many significant fixes. + +$h3 Security + +Several security issues are addressed in this release. Sites already +using PL/Java are encouraged to update to 1.5.0. For several of the +issues below, practical measures are described to mitigate risk until +an update can be completed. + +[CVE-2016-0766][], a privilege escalation requiring an authenticated +PostgreSQL connection, is closed by installing PL/Java 1.5.0 (including +prereleases) or by updating PostgreSQL itself to at least 9.5.1, 9.4.6, +9.3.11, 9.2.15, 9.1.20. Vulnerable systems are only those running both +an older PL/Java and an older PostgreSQL. + +[CVE-2016-0767][], in which an authenticated PostgreSQL user with USAGE +permission on the `public` schema may alter the `public` schema classpath, +is closed by release 1.5.0 (including prereleases). If updating to 1.5.0 +must be delayed, risk can be mitigated by revoking public `EXECUTE` permission +on `sqlj.set_classpath` and granting it selectively to responsible users or +roles. + +This release brings a policy change to a more secure-by-default posture, +where the ability to create functions in `LANGUAGE java` is no longer +automatically granted to `public`, but can be selectively granted to roles +that will have that responsibility. The change reduces exposure to a known +issue present in 1.5.0 and earlier versions, that will be closed in a future +release ([CVE-2016-0768][], see **large objects, access control** below). + +The new policy will be applied in a new installation; permissions will not +be changed in an upgrade, but any site can move to this policy, even before +updating to 1.5.0, with `REVOKE USAGE ON LANGUAGE java FROM public;` followed by +explicit `GRANT` commands for the users/roles expected to create Java +functions. + +[CVE-2016-2192][], in which an authenticated user can alter type mappings +without owning the types involved. Exploitability is limited by other +permissions, but if type mapping is a feature being used at a site, one +can interfere with proper operation of code that relies on it. A mitigation +is simply to `REVOKE EXECUTE ... FROM PUBLIC` on the `sqlj.add_type_mapping` +and `sqlj.drop_type_mapping` functions, and grant the privilege only to +selected users or roles. As of 1.5.0, these functions require the invoker +to be superuser or own the type being mapped. + +[CVE-2016-0766]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0766 +[CVE-2016-0767]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0767 +[CVE-2016-0768]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0768 +[CVE-2016-2192]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-2192 + +$h3 Version compatibility + +PL/Java 1.5.0 can be built against recent PostgreSQL versions including 9.5, +using Java SE 8, 7, or 6. See [version compatibility][versions] for more +detail. OpenJDK is well supported. Support for GCJ has been dropped; features +of modern Java VMs that are useful to minimize footprint and startup time, +such as class-data sharing, are now more deeply covered +[in the installation docs][vmopts]. + +[versions]: build/versions.html +[vmopts]: install/vmoptions.html + +$h3 Build procedures + +Since 2013, PL/Java has been hosted [on GitHub][ghpljava] and built +using [Apache Maven][mvn]. See the new [build instructions][bld] for details. + +Reported build issues for specific platforms have been resolved, +with new platform-specific build documentation +for [OS X][osxbld], [Solaris][solbld], [Ubuntu][ububld], +[Windows MSVC][msvcbld], and [Windows MinGW-w64][mgwbld]. + +The build produces a redistributable installation archive usable with +the version of PostgreSQL built against and the same operating system, +architecture, and linker. The type of archive is `jar` on all platforms, as +all PL/Java installations will have Java available. + +[ghpljava]: https://github.com/tada/pljava +[mvn]: http://maven.apache.org/ +[bld]: build/build.html +[msvcbld]: build/buildmsvc.html +[mgwbld]: build/mingw64.html +[osxbld]: build/macosx.html +[solbld]: build/solaris.html +[ububld]: build/ubuntu.html + +$h3 Installation procedures + +The jar produced by the build is executable and will self-extract, +consulting `pg_config` on the destination system to find the correct +default locations for the extracted files. Any location can be overridden. +(Enhancement requests [6][gh6], [9][gh9]) + +PL/Java now uses a PostgreSQL configuration variable, `pljava.libjvm_location`, +to find the Java runtime to use, eliminating the past need for highly +platform-specific tricks like link-time options or runtime-loader configuration +just so that PL/Java could find Java. PostgreSQL configuration variables are +now the only form of configuration needed for PL/Java, and the `libjvm_location` +should be the only setting needed if file locations have not been overridden. + +In PostgreSQL 9.1 and later, PL/Java can be installed with +`CREATE EXTENSION pljava`. Regardless of PostgreSQL version, installation +has been simplified. Former procedures involving `Deployer` or `install.sql` +are no longer required. Details are in the [new installation instructions][ins]. + +$h4 Schema migration + +The tables used internally by PL/Java have changed. If PL/Java 1.5.0 is +loaded in a database with an existing `sqlj` schema populated by an earlier +PL/Java version (1.3.0 or later), the structure will be updated without data +loss (enhancement request [12][gh12]). *Remember that PL/Java runs independently +in each database session where it is in use. Older PL/Java versions active in +other sessions can be disrupted by the schema change.* + +A trial installation of PL/Java 1.5.0 can be done in a transaction, and +rolled back if desired, leaving the schema as it was. Any concurrent sessions +with active older PL/Java versions will not be disrupted by the altered schema +as long as the transaction remains open, *but they may block for the duration, +so such a test transaction should be kept short*. + +[ins]: install/install.html + +$h3 Changes + +$h4 Behavior of `readSQL` and `writeSQL` for base and mirror user-defined types + +In the course of fixing [issue #98][gh98], the actual behavior of +`readSQL` and `writeSQL` with base or mirror types, which had not +previously been documented, [now is](develop/coercion.html), along with +other details of PL/Java's type coercion rules found only in the code. +Because machine byte order was responsible for issue #98, it now (a) is +selectable, and (b) has different, appropriate, defaults for mirror UDTs +(which need to match PostgreSQL's order) and for base UDTs (which must +stay big-endian because of how binary `COPY` is specified). +A [new documentation section](use/byteorder.html) explains in detail. + +$h4 `USAGE` to `PUBLIC` no longer default for `java` language + +Of the two languages installed by PL/Java, functions that declare +`LANGUAGE javau` can be created only by superusers, while those that +declare `LANGUAGE java` can be created by any user or role granted the +`USAGE` privilege on the language. + +In the past, the language `java` has been created with PostgreSQL's +default permission granting `USAGE` to `PUBLIC`, but PL/Java 1.5.0 +leaves the permission to be explicitly granted to those users or roles +expected to create Java functions, in keeping with least-privilege +principles. See **large objects, access control** under **known issues** +for background. + +$h4 SQL generated by Java annotations + +Java code developed for use by PL/Java can carry in-code annotations, +used by the Java compiler to generate the SQL commands to declare the +new functions, types, triggers, etc. in PostgreSQL (enhancement request +[1011112][], though different in implementation). This eliminates the need +to have Java code and the corresponding SQL commands developed in parallel, +and the class of errors possible when both are not updated together. It +also allows compile-time checks that the Java methods or classes being +annotated are suitable (correct access modifiers, signatures, etc.) +for their declared SQL purposes, rather than discovering +such issues only upon loading the code into PostgreSQL and trying to use it. + +The Java compiler writes the generated SQL into a "deployment descriptor" +file (`pljava.ddr` by default), as specified by the SQL/JRT standard. The +file can be included in a `jar` archive with the compiled code, and the +commands will be executed by PL/Java when the `install_jar` function is +used to load the jar. + +SQL generation is covered in the [updated user documentation][user], +and illustrated in the [Hello, World example][hello] and +[several other supplied examples][exanno]. Reference information +is [in the API documentation][apianno]. It is currently usable to declare +functions, triggers, and user-defined types, both base and composite. + +[user]: use/use.html +[hello]: use/hello.html +[exanno]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation +[apianno]: pljava-api/apidocs/index.html?org/postgresql/pljava/annotation/package-summary.html#package_description + +The history of this feature in PL/Java is long, with the first related commits +appearing in 2005, six years in advance of an enhancement request for it. +It became generally usable in 2013 when building with +Java SE 6 or later, using the annotation processing framework Java introduced +in that release. 1.5.0 is the first PL/Java numbered release to feature it. + +$h5 Annotation keyword changes + +If you have been using the SQL generation feature in prerelease `git` builds of +2013 or later, be aware that some annotation keywords have changed in finalizing +the 1.5.0 release. Java code that was compiled using the earlier keywords will +continue to work, but will have to be updated before it can be recompiled. + +* For functions: `effects=(VOLATILE,STABLE,IMMUTABLE)` was formerly `type=` +* For functions: `type=` (_an explicit SQL return type for the function_) + was formerly `complexType=` +* For functions: `trust=(SANDBOXED,UNSANDBOXED)` was formerly + `(RESTRICTED,UNRESTRICTED)` +* For triggers: `called=(BEFORE,AFTER,INSTEAD_OF)` was formerly `when=` + and conflicted with the `WHEN` clause introduced for triggers + in PostgreSQL 9.0. + +$h4 A jar may have more than one deployment descriptor + +PL/Java formerly allowed only one entry in a jar to be a deployment +descriptor (that is, a file of SQL commands to be executed upon loading +or unloading the jar). The SQL/JRT standard allows multiple entries to +be deployment descriptors, executed in the order they are mentioned +_in the jar manifest_, or the reverse of that order when the jar is +being unloaded. PL/Java now conforms to the standard. + +The behavior is useful during transition to annotation-driven deployment +descriptor generation for a project that already has a manually-maintained +deployment descriptor. PL/Java's own `pljava-examples` project is an +illustration, in the midst of such a transition itself. + +Note the significance placed by SQL/JRT on the order of entries in a jar +manifest, whose order is normally _not_ significant according to the Jar File +Specification. Care can be needed when manipulating manifests with automated +tools that may not preserve order. + +$h4 Conditional execution within deployment descriptors + +Deployment descriptors have a primitive conditional-execution provision +defined in the SQL/JRT standard: commands wrapped in a +`BEGIN IMPLEMENTOR ` _identifier_ construct will only be executed if the +_identifier_ is recognized by the SQL/JRT implementation in use. The design +makes possible jars that can be installed on different database systems that +provide SQL/JRT features, with database-specific commands wrapped in +`BEGIN IMPLEMENTOR` blocks with an _identifier_ specific to the system. +By default, PL/Java recognizes the _identifier_ `postgresql` (matched without +regard to case). + +PL/Java extends the standard by allowing the PostgreSQL configuration +variable `pljava.implementors` to contain a list of identifiers that will +be recognized. SQL code in a deployment descriptor can conditionally add +or remove identifiers in this list to influence which subsequent implementor +blocks will be executed, giving a still-primitive but more general control +structure. + +In sufficiently recent PostgreSQL versions, the same effect could be +achieved using `DO` statements and PL/pgSQL control structures, but this +facility in PL/Java does not require either to be available. + +$h4 Interaction with `SET ROLE` corrected + +PL/Java formerly was aware of the user ID associated with the running +session, but not any role ID that user may have acquired with `SET ROLE`. +The result would commonly be failed permission checks made by PL/Java when +the session user did not have the needed permission, but had `SET ROLE` to +a role that did. Likewise, within `install_jar`, PL/Java would execute +deployment descriptor commands as the original session user rather than +as the user's current role, with permission failures a likely result. + +Correcting this issue has changed the PL/Java API, but without a bump +of major version because the prior API, while deprecated, is still available. + +* [`getOuterUserName`][goun] and [`executeAsOuterUser`][eaou] are new, and + correctly refer to the session user or current role, when active. +* [`getSessionUserName`][gsun] and [`executeAsSessionUser`][easu] are still + present but deprecated, and _their semantics are changed_. They are now + deprecated aliases for the corresponding new methods, which honor the + set role. Use cases that genuinely need to refer only to the _session_ user + and ignore the role should be rare, and should be discussed on the mailing + list or opened as issues. + +#set($sessapi = 'pljava-api/apidocs/index.html?org/postgresql/pljava/Session.html#') + +[goun]: ${sessapi}getOuterUserName() +[eaou]: ${sessapi}executeAsOuterUser(java.sql.Connection,java.lang.String) +[gsun]: ${sessapi}getSessionUserName() +[easu]: ${sessapi}executeAsSessionUser(java.sql.Connection,java.lang.String) + +$h4 Unicode transparency + +Since the resolution of [bug 21][gh21], PL/Java contains a regression test +to ensure that character strings passed and returned between PostgreSQL and +Java will round-trip without alteration for the full range of Unicode +characters, _when the database encoding is set to `UTF8`_. + +More considerations apply when the database encoding is anything other +than `UTF8`, and especially when it is `SQL_ASCII`. Please see +[character encoding support][charsets] for more. + +[charsets]: use/charsets.html + +$h3 Enhancement requests addressed + +* [Use Annotations instead of DDL Manifest][1011112] +* [Installation of pljava on postgresql servers][gh9] +* [Find an alternative way to install the pljava.so in `/usr/lib`][gh6] +* [Provide database migration][gh12] +* [Support types with type modifiers][1011140] (partial: see [example][typmex]) +* [Build process: accommodate Solaris 10][gh102] + +[1011112]: ${pgffeat}1011112 +[1011140]: ${pgffeat}1011140 +[gh9]: ${ghbug}9 +[gh6]: ${ghbug}6 +[gh12]: ${ghbug}12 +[gh102]: ${ghbug}102 + +[typmex]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java + +$h3 Bugs fixed + +$h4 Since 1.5.0-BETA3 + +* [Build process: accept variation in PostgreSQL version string][gh101] +* [Build process: accommodate PostgreSQL built with private libraries][gh103] +* Clarified message when `CREATE EXTENSION` fails because new session needed +* Reduced stack usage in SQL generator + (small-memory build no longer needs `-Xss`) + +$h4 In 1.5.0-BETA3 + +* [Bogus mirror-UDT values on little-endian hardware][gh98] +* [Base UDT not registered if first access isn't in/out/send/recv][gh99] +* `TupleDesc` leak warnings with composite UDTs +* also added regression test from [1010962][] report + +$h4 In 1.5.0-BETA2 + +* [Generate SQL for trigger function with no parameters][gh92] +* [openssl/ssl.h needed on osx el-capitan (latest 10.11.3)/postgres 9.5][gh94] + (documented) +* [Source location missing for some annotation errors][gh95] +* [OS X El Capitan "Java 6" dialog when loading ... Java 8][gh96] +* pljava-api jar missing from installation jar + +$h4 In 1.5.0-BETA1 + +* [SPIPreparedStatement.setObject() fails with Types.BIT][1011119] +* [SSLSocketFactory throws IOException on Linux][1011095] +* [PL/Java fails to compile with -Werror=format-security][1011181] +* [PL/Java does not build on POWER 7][1011197] +* [The built in functions do not use the correct error codes][1011206] +* [TupleDesc reference leak][1010962] +* [String conversion to enum fails][gh4] +* [segfault if SETOF RECORD-returning function used without AS at callsite][gh7] +* [pl/java PG9.3 Issue][gh17] +* [No-arg functions unusable: "To many parameters - expected 0"][gh8] +* [Exceptions in static initializers are masked][gh54] +* [UDT in varlena form breaks if length > 32767][gh52] +* [PL/Java kills unicode?][gh21] +* [Type.c expects pre-8.3 find_coercion_pathway behavior][gh65] +* [Support PostgreSQL 9.5][gh48] +* [pl/java getting a build on MacOSX - PostgreSQL 9.3.2][gh22] +* [build pljava on windows for PostgreSQL 9.2][gh23] +* [Error while installing PL/Java with Postgresql 9.3.4 64 bit on Windows 7 64 bit System][gh28] +* [pljava does not compile on mac osx ver 10.11.1 and postgres 9.4][gh63] +* [pljava does not compile on centos 6.5 and postgres 9.4][gh64] +* [Error installing pljava with Windows 7 64 Bit and Postgres 9.4][gh71] +## JNI_getIntArrayRegion instead of JNI_getShortArrayRegion +## Eclipse IDE artifacts +## Site +## Warnings +## Javadoc + +[1011119]: ${pgfbug}1011119 +[1011095]: ${pgfbug}1011095 +[1011181]: ${pgfbug}1011181 +[1011197]: ${pgfbug}1011197 +[1011206]: ${pgfbug}1011206 +[1010962]: ${pgfbug}1010962 +[gh4]: ${ghbug}4 +[gh7]: ${ghbug}7 +[gh8]: ${ghbug}8 +[gh17]: ${ghbug}17 +[gh54]: ${ghbug}54 +[gh52]: ${ghbug}52 +[gh21]: ${ghbug}21 +[gh65]: ${ghbug}65 +[gh48]: ${ghbug}48 +[gh22]: ${ghbug}22 +[gh23]: ${ghbug}23 +[gh28]: ${ghbug}28 +[gh63]: ${ghbug}63 +[gh64]: ${ghbug}64 +[gh71]: ${ghbug}71 +[gh92]: ${ghbug}92 +[gh94]: ${ghbug}94 +[gh95]: ${ghbug}95 +[gh96]: ${ghbug}96 +[gh98]: ${ghbug}98 +[gh99]: ${ghbug}99 +[gh101]: ${ghbug}101 +[gh103]: ${ghbug}103 + +$h3 Updated PostgreSQL APIs tracked + +Several APIs within PostgreSQL itself have been added or changed; +PL/Java now uses the current versions of these where appropriate: + +* `find_coercion_pathway` +* `set_stack_base` +* `GetOuterUserId` +* `GetUserNameFromId` +* `GetUserIdAndSecContext` +* `pg_attribute_*` +* Large objects: truncate, and 64-bit offsets + +$h3 Known issues and areas for future work + +$h4 Developments in PostgreSQL not yet covered + +Large objects, access control +: PL/Java does not yet expose PostgreSQL large objects with a documented, + stable API, and the support it does contain was developed against pre-9.0 + PostgreSQL versions, where no access control applied to large objects and + any object could be accessed by any database user. PL/Java's behavior is + proper for PostgreSQL before 9.0, but improper on 9.0+ where it would be + expected to honor access controls on large objects ([CVE-2016-0768][]). + This will be corrected in a future release. For this and earlier releases, + the recommendation is to selectively grant `USAGE` on the `java` language to + specific users or roles responsible for creating Java functions; see + "default `USAGE` permssion" under Changes. + +`INSTEAD OF` triggers, triggers on `TRUNCATE` +: These are supported by annotations and the SQL generator, and the runtime + will deliver them to the specified method, but the `TriggerData` interface + has no new methods to recognize these cases (that is, no added + methods analogous to `isFiredAfter`, `isFiredByDelete`). For a method + expressly coded to be a `TRUNCATE` trigger or an `INSTEAD OF` trigger, + that is not a problem, but care should be taken when coding a trigger + method to handle more than one type of trigger, or creating triggers of + these new types that call a method developed pre-PL/Java-1.5.0. Such a + method could be called with a `TriggerData` argument whose existing + `isFired...` methods all return `false`, likely to put the method on an + unexpected code path. + + A later PL/Java version should introduce trigger interfaces that better + support such evolution of PostgreSQL in a type-safe way. + +Constraint triggers +: Constraint trigger syntax is not supported by annotations and the SQL + generator. If declared (using hand-written SQL), they will be delivered + by the runtime, but without any constraint-trigger-specific information + available to the called method. + +Event triggers +: Event triggers are not yet supported by annotations or the SQL generator, + and will not be delivered by the PL/Java runtime. + +Range types +: No predefined mappings for range types are provided. + +`PRE_PREPARE`, `PRE_COMMIT`, `PARALLEL_ABORT`, `PARALLEL_PRE_COMMIT`, and `PARALLEL_COMMIT` transaction callbacks, `PRE_COMMIT` subtransaction callbacks +: Listeners for these events cannot be registered and the events will not + be delivered. + +$h4 Imperfect integration with PostgreSQL dependency tracking + +In a dump/restore, manual intervention can be needed if the users/roles +recorded as owners of jars are missing or have been renamed. A current +[thread on `pgsql-hackers`][ownhack] should yield a better solution for +a future release. + +[ownhack]: http://www.postgresql.org/message-id/56783412.6090005@anastigmatix.net + +$h4 Quirk if deployment descriptor loads classes from same jar + +The `install_jar` function installs a jar, optionally reading deployment +descriptors from the jar and executing the install actions they contain. +It is possible for those actions to load classes from the jar just installed. +(This would be unlikely if the install actions are limited to typical setup, +function/operator/datatype creation, but likely, if the install actions also +include basic function tests, or if the setup requirements are more +interesting.) + +If, for any class in the jar, the first attempt to load that class is made +while resolving a function declared `STABLE` or `IMMUTABLE`, a +`ClassNotFoundException` results. The cause is PostgreSQL's normal treatment of +a `STABLE` or `IMMUTABLE` function, which relies on a snapshot from the start of +the `install_jar` query, when the jar was not yet installed. A workaround is to +ensure that the install actions cause each needed class to be loaded, such as +by calling a `VOLATILE` function it supplies, before calling one that is +`STABLE` or `IMMUTABLE`. (One could even write install actions to declare a +needed function `VOLATILE` before the first call and then redeclare it.) + +This issue should be resolved as part of a broader rework of class loading +in a future PL/Java release. + +$h4 Partial implementation of JDBC 4 and later + +The changes to make PL/Java build under Java SE 6 and later, with version 4.0 +and later of JDBC, involved providing the specified methods so +compilation would succeed, with real implementations for some, but for others +only stub versions that throw `SQLFeatureNotSupportedException` if used. +Regrettably, there is nothing in the documentation indicating which methods +have real implementations and which do not; to create such a list would require +an audit of that code. If a method throws the exception when you call it, it's +one of the unimplemented ones. + +Individual methods may be fleshed out with implementations as use cases arise +that demand them, but for a long-term roadmap, it seems more promising to +reduce the overhead of maintaining another JDBC implementation by sharing +code with `pgjdbc`, as has been [discussed on pljava-dev][jdbcinherit]. + +[jdbcinherit]: http://lists.pgfoundry.org/pipermail/pljava-dev/2015/002370.html + +$h4 Exception handling and logging + +PL/Java does interconvert between PostgreSQL and Java exceptions, but with +some loss of fidelity in the two directions. PL/Java code has some access +to most fields of a PostgreSQL error data structure, but only through +internal PL/Java API that is not expected to remain usable, and code written +for PL/Java has never quite had first-class standing in its ability to +_generate_ exceptions as information-rich as those from PostgreSQL itself. + +PL/Java in some cases generates the _categorized `SQLException`s_ introduced +with JDBC 4.0, and in other cases does not. + +This area may see considerable change in a future release. +[Thoughts on logging][tol] is a preview of some of the considerations. + +[tol]: https://github.com/tada/pljava/wiki/Thoughts-on-logging + +$h4 Types with type modifiers and `COPY` + +Although it is possible to create a PL/Java user-defined type that accepts +a type modifier (see the [example][typmex]), such a type will not yet be +handled by SQL `COPY` or any other operation that requires the `input` or +`receive` function to handle the modifier. This is left for a future release. + +$h3 Credits + +PL/Java 1.5.0 owes its being to original creator Thomas Hallgren and +many contributors: + +Daniel Blanch Bataller, +Peter Brewer, +Frank Broda, +Chapman Flack, +Marty Frasier, +Bear Giles, +Christian Hammers, +Hal Hildebrand, +Robert M. Lefkowitz, +Eugenie V. Lyzenko, +Dos Moonen, +Asif Naeem, +Kenneth Olson, +Johann Oskarsson, +Thomas G. Peters, +Srivatsan Ramanujam, +Igal Sapir, +Jeff Shaw, +Rakesh Vidyadharan, +`grunjol`, +`mc-soi`. + +Periods in PL/Java's development have been sponsored by EnterpriseDB. + +In the 1.5.0 release cycle, multiple iterations of testing effort +have been generously contributed by Kilobe Systems and by Pegasystems, Inc. + +## From this point on, the entries were reconstructed from old notes at the +## same time as the 1.5.0 notes were drafted, and they use a finer level of +## heading. So restore the 'real' values of the heading variables from here +## to the end of the file. +#set($h2 = '##') +#set($h3 = '###') +#set($h4 = '####') +#set($h5 = '#####') + +$h3 PL/Java 1.4.3 (15 September 2011) + +Notable changes in this release: + +* Works with PostgreSQL 9.1 +* Correctly links against IBM Java. +* Reads microseconds correctly in timestamps. + +Bugs fixed: + +* [Be clear about not building with JDK 1.6][1010660] +* [Does not link with IBM VM][1010970] +* [SPIConnection.getMetaData() is incorrectly documented][1010971] +* [PL/Java 1.4.2 Does not build with x86_64-w64-mingw32][1011025] +* [PL/Java does not build with PostgreSQL 9.1][1011091] + +Feature Requests: + +* [Allow pg_config to be set externally to the Makefile][1011092] +* [Add option to have pljava.so built with the runtime path of libjvm.so][1010955] + +[1010660]: ${pgfbug}1010660 +[1010970]: ${pgfbug}1010970 +[1010971]: ${pgfbug}1010971 +[1011025]: ${pgfbug}1011025 +[1011091]: ${pgfbug}1011091 + +[1011092]: ${pgffeat}1011092 +[1010955]: ${pgffeat}1010955 + +$h3 PL/Java 1.4.2 (11 December 2010) + +Bugfixes: + +* [Function returning complex objects with POD arrays cause a segfault][1010956] +* [Segfault when assigning an array to ResultSet column][1010953] +* [Embedded array support in returned complex objects][1010482] + +[1010956]: ${pgfbug}1010956 +[1010953]: ${pgfbug}1010953 +[1010482]: ${pgfbug}1010482 + +$h3 PL/Java 1.4.1 (9 December 2010) + +Note: Does not compile with Java 6. Use JDK 1.5 or 1.4. + +Compiles with PostgreSQL 8.4 and 9.0. + +Connection.getCatalog() has been implemented. + +Bugfixes: + +* [Compiling error with postgresql 8.4.1][1010759] +* [org.postgresql.pljava.internal.Portal leak][1010712] +* [build java code with debugging if server has debugging enabled][1010189] +* [Connection.getCatalog() returns null][1010653] +* [VM crash in TransactionListener][1010462] +* [Link against wrong library when compiling amd64 code on Solaris][1010954] + +[1010759]: ${pgfbug}1010759 +[1010712]: ${pgfbug}1010712 +[1010189]: ${pgfbug}1010189 +[1010653]: ${pgfbug}1010653 +[1010462]: ${pgfbug}1010462 +[1010954]: ${pgfbug}1010954 + +Other commits: + +For a multi-threaded pljava function we need to adjust stack_base_ptr +before calling into the backend to avoid stack depth limit exceeded +errors. Previously this was done only on query execution, but we need +to do it on iteration of the ResultSet as well. + +When creating a variable length data type, the code was directly +assigning the varlena header length rather than going through an +access macro. The header format changed for the 8.3 release and this +manual coding was not noticed and changed accordingly. Use +SET_VARSIZE to do this correctly. + +Handle passed by value data types by reading and writing directly to +the Datum rather than dereferencing it. + +If the call to a type output function is the first pljava call in a +session, we get a crash. The first pljava call results in a SPI +connection being established and torn down. The type output function +was allocating the result in the SPI memory context which gets +destroyed prior to returning the data to the caller. Allocate the +result in the correct context to survive function exit. + +Clean up a warning about byteasend and bytearecv not having a +prototype when building against 9.0 as those declarations are now in a +new header file. + + +$h3 PL/Java 1.4.0 (1 February 2008) + +Warning! The recent postgresql security releases changed the API of a function +that PL/Java uses. The source can be built against either version, but the +binaries will only run against the version they were built against. The PL/Java +binaries for 1.4.0 have all been built against the latest server releases (which +you should be using anyway). If you are using an older you will have to build +from source. The binary releases support: 8.3 - All versions. 8.2 - 8.2.6 and +up. 8.1 - 8.1.11 and up. 8.0 - 8.0.15 and up. + +$h3 PL/Java 1.3.0 (18 June 2006) + +This release is about type mapping and the creation of new types in PL/Java. An +extensive effort has gone into making the PL/Java type system extremely +flexible. Not only can you map arbitrary SQL data types to java classes. You can +also create new scalar types completely in Java. Read about the Changes in +version 1.3. + +$h4 Changes + +* A much improved type mapping system that will allow you to: + + * [Map any SQL type to a Java class][maptype] + * [Create a Scalar UDT in Java][scalarudt] + * [Map array and pseudo types][deftypemap] + +[maptype]: https://github.com/tada/pljava/wiki/Mapping-an-sql-type-to-a-java-class +[scalarudt]: https://github.com/tada/pljava/wiki/Creating-a-scalar-udt-in-java +[deftypemap]: https://github.com/tada/pljava/wiki/Default-type-mapping + +* Get the OID for a given relation ([feature request 1319][1319]) +* Jar manifest included in the SQLJ Jar repository + ([feature request 1525][1525]) + +$h4 Fixed bugs + +* [Reconnect needed for jar manipulation to take effect][1531] +* [Backends hang with test suite][1504] +* [Keeps crashing while making a call to a function][1560] +* [Memory Leak in Statement.executeUpdate][1556] +* [jarowner incorrect after dump and reload][1506] +* [Missing JAR manifest][1525] +* [TZ adjustments for Date are incorrect][1547] +* [Functions returning sets leaks memory][1542] +* [drop lib prefix][1423] +* ["oid" column is not available in trigger's NEW/OLD ResultSet][1317] +* [fails to run with GCJ, too][1480] +* [Compile failure with 8.1.4][1558] +* [fails to build with GCJ][1479] +* [Record returning function cannot be called with different structures within one session][1440] +* [Cannot map function with complex return type to method that uses non primitive arguments][1551] +* [Get OID for given relation][1319] + +[1531]: ${gborgbug}1531 +[1504]: ${gborgbug}1504 +[1560]: ${gborgbug}1560 +[1556]: ${gborgbug}1556 +[1506]: ${gborgbug}1506 +[1525]: ${gborgbug}1525 +[1547]: ${gborgbug}1547 +[1542]: ${gborgbug}1542 +[1423]: ${gborgbug}1423 +[1317]: ${gborgbug}1317 +[1480]: ${gborgbug}1480 +[1558]: ${gborgbug}1558 +[1479]: ${gborgbug}1479 +[1440]: ${gborgbug}1440 +[1551]: ${gborgbug}1551 +[1319]: ${gborgbug}1319 + +$h3 PL/Java 1.2.0 (20 Nov 2005) + +The PL/Java 1.2.0 release is primarily targeted at the new PostgreSQL 8.1 but +full support for 8.0.x is maintained. New features include support IN/OUT +parameters, improved meta-data handling, and better memory management. + +$h3 PL/Java 1.1.0 (14 Apr 2005) + +PL/Java 1.1.0 includes a lot of new features such as `DatabaseMetaData`, +`ResultSetMetaData`, language handlers for both trusted and untrusted language, +additional semantics for functions returning `SETOF`, and simple ObjectPooling. + +$h3 PL/Java 1.0.1 (07 Feb 2005) + +This release resolves a couple of important security issues. The most important +one is perhaps that PL/Java now is a trusted language. See [Security][] for more +info. Filip Hrbek, now member of the PL/Java project, contributed what was +needed to make this happen. + +[Security]: https://github.com/tada/pljava/wiki/Security + +$h3 PL/Java 1.0.0 (23 Jan 2005) + +Today, after a long period of fine tuning, PL/Java 1.0.0 was finally released. diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index da1da349..fe035389 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -208,1575 +208,4 @@ $h2 Earlier releases #set($h4 = '#####') #set($h5 = '######') -$h2 PL/Java 1.5.6 - -This release adds support for PostgreSQL 13. - -It includes improvements to the JDBC 4.0 `java.sql.SQLXML` API that first became -available in 1.5.1, an update of the ISO SQL/XML examples based on the Saxon -product to Saxon 10 (which now includes support for XML Query higher-order -functions in the freely-licensed Saxon-HE), some improvements to internals, -and a number of bug fixes. - -$h3 Version compatibility - -PL/Java 1.5.6 can be built against recent PostgreSQL versions including 13, -and older ones back to 8.2, using Java SE 8 or later. The source code avoids -features newer than Java 6, so building with Java 7 or 6 should also be -possible, but is no longer routinely tested. The Java version used at runtime -does not have to be the same version used for building. PL/Java itself can run -on any Java version 6 or later if built with Java 11 or earlier; it can run -on Java 7 or later if built with Java 12 or later. PL/Java functions can be -written for, and use features of, whatever Java version will be loaded at run -time. See [version compatibility][versions] for more detail. - -PL/Java 1.5.6 cannot be built with Java 15 or later, as the Nashorn JavaScript -engine used in the build process no longer ships with Java 15. It can be built -with [GraalVM][], if `-Dpolyglot.js.nashorn-compat` is added to the `mvn` -command line. It will run on Java 15 if built with an earlier JDK or with Graal. - -When used with GraalVM as the runtime VM, PL/Java functions can use Graal's -"polyglot" capabilities to execute code in any other language available on -GraalVM. In this release, it is not yet possible to directly declare a function -in a language other than Java. - -$h3 Changes - -$h4 Improvements to the `java.sql.SQLXML` type - -Additions to the `Adjusting.XML` API support -[limiting resource usage][xmlreslim] in XML processing, controlling -[resolution][xmlresolv] of external documents and resources, -[validation against a schema][xmlschema], and integration of an -[XML catalog][xmlcatalog] to locally satisfy requests for external documents. - -Corrections and new documentation of [whitespace handling][xmlws] in XML values -of `CONTENT` form, and implementation [limitations][xmlimpl]. - -$h4 Improvements to the Saxon-based ISO SQL/XML example functions - -Updated the dependency for these optional examples to Saxon 10. Probably the -most significant of the [Saxon 10 changes][saxon10], for PostgreSQL's purposes, -will be that the XQuery [higher-order function feature][xqhof] is now included -in the freely-licensed Saxon-HE, so that it is now possible without cost to -integrate a modern XQuery 3.1 implementation that is lacking only the -[schema-aware feature][xqsaf] and the [typed data feature][xqtdf] (for those, -the paid Saxon-EE product is needed), and the [static typing feature][xqstf] -(which is not in any Saxon edition). - -To compensate for delivering the higher-order function support in -HE, -Saxonica moved certain optimizations to -EE. This seems a justifiable trade, as -it is better for development purposes to have the more complete implementation -of the language, leaving better optimization to be bought if and when needed. - -Thanks to a tip from Saxon's developer, the returning of results to SQL is now -done in a way that may incur less copying in some cases. - -$h4 Internals - -* Many sources of warnings reported by the Java VM's `-Xcheck:jni` option have - been tracked down, making it practical to use `-Xcheck:jni` in testing. -* Reduced pressure on the garbage collector in management of references to - PostgreSQL native state. - -$h3 Enhancement requests addressed - -* Work around PostgreSQL [API breakage in EnterpriseDB 11](${ghbug}260) - -$h3 Bugs fixed - -* [Support of arrays in composite types](${ghbug}300) -* [Order-dependent behavior caching array types](${ghbug}310) -* [Date conversion errors possible with PostgreSQL 10 on Windows/MSVC](${ghbug}297) -* [Build issue with Windows/MinGW-w64](${ghbug}282) -* ["xmltable" with XML output column or parameter](${ghbug}280) -* [Google Summer of Code catches 15-year-old PL/Java bug](${ghbug}274) -* [Several bugs in SQLXML handling](${ghbug}272) -* Work around an exception from `Reference.clear` on OpenJ9 JVM -* Bugs in SQL generator when supplying a function parameter name, or the - `category`, `delimiter`, or `storage` attribute of a user-defined type. - -$h3 Updated PostgreSQL APIs tracked - -* Removal of `CREATE EXTENSION ... FROM unpackaged` -* `numvals` in `SPITupleTable` -* `detoast.h` -* `detoast_external_attr` - -$h3 Credits - -There is a PL/Java 1.5.6 thanks in part to -Christoph Berg, -Chapman Flack, -Kartik Ohri, -original creator Thomas Hallgren, -and the many contributors to earlier versions. - -The work of Kartik Ohri in summer 2020 was supported by Google Summer of Code. - -[xmlreslim]: use/sqlxml.html#Additional_adjustments_in_recent_Java_versions -[xmlresolv]: use/sqlxml.html#Supplying_a_SAX_or_DOM_EntityResolver_or_Schema -[xmlschema]: use/sqlxml.html#Validation_against_a_schema -[xmlcatalog]: use/sqlxml.html#Using_XML_Catalogs_when_running_on_Java_9_or_later -[xmlws]: use/sqlxml.html#Effect_on_parsing_of_whitespace -[xmlimpl]: use/sqlxml.html#Known_limitations -[saxon10]: https://www.saxonica.com/html/documentation/changes/v10/installation.html -[xqhof]: https://www.w3.org/TR/xquery-31/#id-higher-order-function-feature -[xqsaf]: https://www.w3.org/TR/xquery-31/#id-schema-aware-feature -[xqtdf]: https://www.w3.org/TR/xquery-31/#id-typed-data-feature -[xqstf]: https://www.w3.org/TR/xquery-31/#id-static-typing-feature - -$h2 PL/Java 1.5.5 (4 November 2019) - -This bug-fix release fixes runtime issues reported in 32-bit `i386` builds, some -of which would not affect a more common 64-bit architecture, but some of which -could under the wrong circumstances, so this release should be used in -preference to 1.5.4 or 1.5.3 on any architecture. - -It is featurewise identical to 1.5.4, so those release notes, below, should be -consulted for the details of user-visible changes. - -Thanks to Christoph Berg for the `i386` testing that exposed these issues. - -$h3 Bugs fixed - -* [32bit i386 segfault](${ghbug}246) - -$h2 PL/Java 1.5.4 (29 October 2019) - -This minor release fixes a build issue reported with Java 11, and adds -support for building with Java 13. Issues with building the javadocs in later -Java versions are resolved. A work-in-progress feature that can -[apply the SQLXML API to other tree-structured data types](use/xmlview.html) -is introduced. - -Documentation updates include coverage of -[changes to Application Class Data Sharing](install/appcds.html) in recent -Hotspot versions, and ahead-of-time compilation using -[jaotc](install/vmoptions.html#a-XX:AOTLibrary). - -Otherwise, the release notes for 1.5.3, below, should be -consulted for the details of recent user-visible changes. - -$h3 Bugs fixed - -* [Build failure with Java 11 and --release](${ghbug}235) -* [Build with Java 13](${ghbug}236) -* [Javadoc build fails in Java 11+](${ghbug}239) -* [Javadoc build fails in Java 13](${ghbug}241) - -$h2 PL/Java 1.5.3 (4 October 2019) - -This release adds support for PostgreSQL 12, and removes the former -requirement to build with a Java release earlier than 9. - -It includes a rework of of threading and resource management, improvements to -the JDBC 4.0 `java.sql.SQLXML` API that first became available in 1.5.1, and -a substantially usable example providing the functionality of ISO SQL -`XMLEXISTS`, `XMLQUERY`, `XMLTABLE`, `XMLCAST`, `LIKE_REGEX`, -`OCCURRENCES_REGEX`, `POSITION_REGEX`, `SUBSTRING_REGEX`, and `TRANSLATE_REGEX`. -Some bugs are fixed. - -$h3 Version compatibility - -PL/Java 1.5.3 can be built against recent PostgreSQL versions including 12, -and older ones back to 8.2, using Java SE 8 or later. The source code avoids -features newer than Java 6, so building with Java 7 or 6 should also be -possible, but is no longer routinely tested. The Java version used at runtime -does not have to be the same version used for building. PL/Java itself can run -on any Java version 6 or later if built with Java 11 or earlier; it can run -on Java 7 or later if built with Java 12. PL/Java functions can be written for, -and use features of, whatever Java version will be loaded at run time. See -[version compatibility][versions] for more detail. - -When used with [GraalVM][] as the runtime VM, PL/Java functions can use its -"polyglot" capabilities to execute code in any other language available on -GraalVM. In this release, it is not yet possible to directly declare a function -in a language other than Java. - -$h3 Changes - -$h4 Threading/synchronization, finalizers, and new configuration variable - -Java is multithreaded while PostgreSQL is not, requiring ways to prevent -Java threads from entering PostgreSQL at the wrong times, while cleaning up -native resources in PostgreSQL when PL/Java references are released, and -_vice versa_. - -PL/Java has historically used an assortment of approaches including Java -object finalizers, which have long been deprecated informally, and formally -since Java 9. Finalizers enter PostgreSQL from a thread of their own, and the -synchronization approach used in PL/Java 1.5.2 and earlier has been associated -with occasional hangs at backend exit when using an OpenJ9 JVM at runtime. - -A redesigned approach using a new `DualState` class was introduced in 1.5.1, -at first only used in implementing the `java.sql.SQLXML` type, a newly-added -feature. In 1.5.3, other approaches used in the rest of PL/Java's code base are -migrated to use `DualState` also, and all uses of the deprecated Java object -finalizers have been retired. With the new techniques, the former occasional -OpenJ9 hangs have not been observed. - -This represents the most invasive change to PL/Java's thread synchronization -in many years, so it may be worthwhile to reserve extra time for -testing applications. - -A new [configuration variable](use/variables.html), -`pljava.java_thread_pg_entry`, allows adjusting the thread policy. The default -setting, `allow`, preserves PL/Java's former behavior, allowing Java threads -entry into PostgreSQL one at a time, only when any thread already in PG code -has entered or returned to Java. - -With object finalizers no longer used, PL/Java itself does not need the `allow` -mode, but there may be application code that does. Application code can be -tested by setting the `error` mode, which will raise an error for any attempted -entry to PG from a thread other than the original thread that launched PL/Java. -If an application runs in `error` mode with no errors, it can also be run in -`block` mode, which may be more efficient, as it eliminates many locking -operations that happen in `allow` or `error` mode. However, if `block` mode -is used with an application that has not been fully tested in `error` mode -first, and the application does attempt to enter PostgreSQL from a Java thread -other than the initial one, the result can be blocked threads or a deadlocked -backend that has to be killed. - -A JMX management client like `JConsole` or `jvisualvm` can identify threads that -are blocked, if needed. The new `DualState` class also offers some statistics -that can be viewed in `JConsole`, or `jvisualvm` with the `VisualVM-MBeans` -plugin. - -$h4 Improvements to the `java.sql.SQLXML` type - -Support for this JDBC 4.0 type was added in PL/Java 1.5.1. Release 1.5.3 -includes the following improvements: - -* A new ["Adjusting" API](use/sqlxml.html#Extended_API_to_configure_XML_parsers) - exposes configuration settings for Java XML parsers that may be created - internally during operations on `SQLXML` instances. That allows the default - settings to restrict certain XML parser features as advocated by the - [Open Web Application Security Project][OWASP] when XML content may be - coming from untrusted sources, with a simple API for relaxing those - restrictions when appropriate for XML content from a known source. -* It is now possible for a PL/Java function to return, pass into a - `PreparedStatement`, etc., an `SQLXML` instance that PL/Java did not create. - For example, a PL/Java function could use another database's JDBC driver to - obtain a `SQLXML` value from that database, and use that as its own return - value. Transparently, the content is copied to a PL/Java `SQLXML` instance. - The copy can also be done explicitly, allowing the "Adjusting" API to be - used if the default XML parser restrictions should be relaxed. -* Behavior when the server encoding is not UTF-8, or when it is not an - IANA-registered encoding (even if Java has a codec for it), has been - improved. - -$h4 Improvements to the Saxon-based ISO SQL/XML example functions - -Since PL/Java 1.5.1, the supplied examples have included a not-built-by-default -[example supplying ISO SQL/XML features missing from core PostgreSQL][exsaxon]. -It is not built by default because it raises the minimum Java version to 8, and -brings in the Saxon-HE XML-processing library. - -In 1.5.3, the example now provides versions of the ISO SQL `XMLEXISTS`, -`XMLQUERY`, `XMLTABLE`, and `XMLCAST` functions based on the W3C XQuery -language as ISO SQL specifies (while PostgreSQL has an "XMLTABLE" function -since release 10 and "XMLEXISTS" since 9.1, they have -[numerous limitations][appD31] inherited from a library that does not support -XQuery, and additional peculiarities prior to PostgreSQL 12), and the ISO SQL -`LIKE_REGEX`, `OCCURRENCES_REGEX`, `POSITION_REGEX`, `SUBSTRING_REGEX`, and -`TRANSLATE_REGEX` functions that apply XQuery regular expressions. It also -includes the `XMLTEXT` function, which is rather trivial, but also missing from -core PostgreSQL, and supplied here for completeness. - -As plain user-defined functions without special treatment in PostgreSQL's SQL -parser, these functions cannot be used with the exact syntax specified in -ISO SQL, but require simple rewriting into equivalent forms that are valid -ordinary PostgreSQL function calls. The rewritten forms are intended to be easy -to read and correspond closely to the ISO syntax. - -While still presented as examples and not a full implementation, these functions -are now intended to be substantially usable (subject to minor -[documented limits][exsaxon]), and testing and reports of shortcomings are -welcome. - -$h4 ResultSet holdability again - -A `ResultSet` obtained from a query done in PL/Java would return the value -`CLOSE_CURSORS_AT_COMMIT` to a caller of its `getHoldability` method, but in -reality would become unusable as soon as the PL/Java function creating it -returned to PostgreSQL. That was fixed in PL/Java 1.5.1 for a `ResultSet` -obtained from a `Statement`, but not for one obtained from a -`PreparedStatement`. It now correctly remains usable to the end of the -transaction in either case. - -$h4 Savepoint behavior at rollback - -Per JDBC, a `Savepoint` still exists after being used in a rollback, and can be -used again; the rollback only invalidates any `Savepoint` that had been created -after the one being rolled back. That should be familiar behavior, as it is the -same as PostgreSQL's own SQL `SAVEPOINT` behavior. It is also correct in pgJDBC, -which has test coverage to confirm it. PL/Java has been doing it wrong. - -In 1.5.3 it now has the JDBC-specified behavior. For compatibility with existing -application code, the meaning of the `pljava.release_lingering_savepoints` -[configuration variable](use/variables.html) has been adjusted. The setting -tells PL/Java what to do if a `Savepoint` still exists, neither released nor -rolled back, at the time a function exits. If `on`, the savepoint is released -(committed); if `off`, the savepoint is rolled back. A warning is issued in -either case. - -In an existing function that used savepoints and assumed that a rolled-back -savepoint would be no longer live, it will now be normal for such a savepoint -to reach the function exit still alive. To recognize this case, PL/Java tracks -whether any savepoint has been rolled back at least once. At function exit, any -savepoint that has been neither released nor ever rolled back is disposed of -according to the `release_lingering_savepoints` setting and with a warning, -as before, but any savepoint that has already been rolled back at least once -is simply released, regardless of the variable setting, and without producing -a warning. - -$h4 Control of function parameter names in generated SQL - -When generating the `CREATE FUNCTION` command in a deployment descriptor -according to an annotated Java function, PL/Java ordinarily gives the function -parameters names that match their Java names, unquoted. Because PostgreSQL -allows named notation when calling a function, the parameter names in its -declaration become part of its signature that cannot later be changed without -dropping and re-creating the function. - -In some cases, explicit control of the SQL parameter names may be wanted, -independently of the Java names: to align with an external standard, perhaps, -or when either the SQL or the Java name would collide with a reserved word. -For that purpose, the (already slightly overloaded) `@SQLType` annotation now -has a `name` attribute that can specify the SQL name of the annotated parameter. - -$h4 Documentation - -The user guide and guide for packagers contained incorrect instructions for -using Maven to build a single subproject of PL/Java (such as `pljava-api` or -`pljava-examples`) instead of the full project. Those have been corrected. - -$h3 Enhancement requests addressed - -* [Allow building with Java releases newer than 8](${ghbug}212) - -$h3 Bugs fixed - -* [ResultSet holdability still wrong when using PreparedStatement](${ghbug}209) -* [Can't return (or set/update PreparedStatement/ResultSet) non-PL/Java SQLXML object](${ghbug}225) -* [JDBC Savepoint behavior](${ghbug}228) -* Writing `SQLXML` via StAX when server encoding is not UTF-8 -* StAX rejecting server encoding if not an IANA-registered encoding -* Error handling when PL/Java startup fails - (may have been [issue 211](${ghbug}211)) -* SPI connection management for certain set-returning functions - -$h3 Updated PostgreSQL APIs tracked - -* Retirement of `dynloader.h` -* Retirement of magical Oids -* Retirement of `nabstime` -* Retirement of `pg_attrdef.adsrc` -* Extensible `TupleTableSlot`s -* `FunctionCallInfoBaseData` - -$h3 Credits - -There is a PL/Java 1.5.3 thanks in part to -Christoph Berg, -Chapman Flack, -`ppKrauss`, -original creator Thomas Hallgren, -and the many contributors to earlier versions. - -[GraalVM]: https://www.graalvm.org/ -[OWASP]: https://www.owasp.org/index.php/About_The_Open_Web_Application_Security_Project -[appD31]: https://www.postgresql.org/docs/12/xml-limits-conformance.html - -$h2 PL/Java 1.5.2 (5 November 2018) - -A pure bug-fix release, correcting a regression in 1.5.1 that was not caught -in pre-release testing, and could leave -[conversions between PostgreSQL `date` and `java.sql.Date`](${ghbug}199) off -by one day in certain timezones and times of the year. - -1.5.1 added support for the newer `java.time` classes from JSR 310 / JDBC 4.2, -which are [recommended as superior alternatives](use/datetime.html) to the -older conversions involving `java.sql.Date` and related classes. The new -versions are superior in part because they do not have hidden timezone -dependencies. - -However, the change to the historical `java.sql.Date` conversion behavior was -inadvertent, and is fixed in this release. - -$h3 Open issues with date/time/timestamp conversions - -During preparation of this release, other issues of longer standing were also -uncovered in the legacy conversions between PG `date`, `time`, and -`timestamp` classes and the `java.sql` types. They are detailed in -[issue #200](${ghbug}200). Because they are not regressions but long-established -behavior, they are left untouched in this release, and will be fixed in -a future release. - -The Java 8 `java.time` conversions are free of these issues as well. - -$h2 PL/Java 1.5.1 (17 October 2018) - -This release adds support for PostgreSQL 9.6, 10, and 11, -and plays more nicely with `pg_upgrade`. If a PostgreSQL installation -is to be upgraded using `pg_upgrade`, and is running a version of -PL/Java before 1.5.1, the PL/Java version should first be upgraded -in the running PostgreSQL version, and then the PostgreSQL `pg_upgrade` -can be done. - -The documentation is expanded on the topic of shared-memory precompiled -class cache features, which can substantially improve JVM startup time -and memory footprint, and are now available across Oracle Java, OpenJDK -with Hotspot, and OpenJDK with OpenJ9. When running on OpenJ9, PL/Java -cooperates with the JVM to include even the application's classes -(those loaded with `install_jar`) in the shared cache, something not -yet possible with Hotspot. While the advanced sharing feature in Oracle -Java is still subject to a commercial licensing encumbrance, the equivalent -(or superior, with OpenJ9) features in OpenJDK are not encumbered. - -Significant new functionality includes new datatype mapping support: -SQL `date`, `time`, and `timestamp` values can be mapped to the new -Java classes of the `java.time` package in Java 8 and later (JSR 310 / -JDBC 4.2), which are much more faithful representations of the values -in SQL. Values of `xml` type can be manipulated efficiently using the -JDBC 4.0 `SQLXML` API, supporting several different APIs for XML -processing in Java. - -For Java code that does not use the new date/time classes in the -`java.time` package, some minor conversion inaccuracies (less than -two seconds) in the older mapping to `java.sql.Timestamp` have been -corrected. - -Queries from PL/Java code now produce `ResultSet`s that are usable to the -end of the containing transaction, as they had already been claiming to be. - -With PostgreSQL 9.6 support comes the ability to declare functions -`PARALLEL { UNSAFE | RESTRICTED | SAFE }`, and with PG 10 support, -transition tables are available to triggers. - -$h3 Security - -$h4 Schema-qualification - -PL/Java now more consistently schema-qualifies objects in queries and DDL -it generates internally, as a measure of defense-in-depth in case the database -it is installed in has not been [protected][prot1058] from [CVE-2018-1058][]. - -_No schema-qualification work has been done on the example code._ If the -examples jar will be installed, it should be in a database that -[the recommended steps have been taken to secure][prot1058]. - -$h4 Some large-object code removed - -1.5.1 removes the code at issue in [CVE-2016-0768][], which pertained to -PostgreSQL large objects, but had never been documented or exposed as API. - -This is not expected to break any existing code at all, based on further -review showing the code in question had also been simply broken, since 2006, -with no reported issues in that time. That discovery would support an argument -for downgrading the severity of the reported vulnerability, but with no need -to keep that code around, it is more satisfying to remove it entirely. - -Developers wishing to manipulate large objects in PL/Java are able to do so -using the SPI JDBC interface and the large-object SQL functions already -available in every PostgreSQL version PL/Java currently supports. - -[CVE-2018-1058]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1058 -[prot1058]: https://wiki.postgresql.org/wiki/A_Guide_to_CVE-2018-1058:_Protect_Your_Search_Path#Next_Steps:_How_Can_I_Protect_My_Databases.3F - -$h3 Version compatibility - -PL/Java 1.5.1 can be built against recent PostgreSQL versions including 11, -and older ones back to 8.2, using Java SE 8, 7, or 6. It can _run_ using newer -Java versions including Java 11. PL/Java functions can be written for, and use -features of, the Java version loaded at run time. See -[version compatibility][versions] for more detail. - -OpenJDK is supported, and can be downloaded in versions using the Hotspot or the -OpenJ9 JVM. Features of modern Java VMs can drastically reduce memory footprint -and startup time, in particular class-data sharing. Several choices of Java -runtime now offer such features: Oracle Java has a simple class data sharing -feature for the JVM itself, freely usable in all supported versions, and an -"application class data sharing" feature in Java 8 and later that can also share -the internal classes of PL/Java, but is a commercial feature requiring a -license from Oracle. As of Java 10, the same application class sharing feature -is present in OpenJDK/Hotspot, where it is freely usable without an additional -license. OpenJDK/OpenJ9 includes a different, and very sophisticated, class -sharing feature, freely usable from Java 8 onward. More on these features -can be found [in the installation docs][vmopts]. - -$h3 Changes - -$h4 Typing of parameters in prepared statements - -PL/Java currently does not determine the necessary types of `PreparedStatement` -parameters from the results of PostgreSQL's own type analysis of the query -(as a network client would, when using PostgreSQL's "extended query" protocol). -PostgreSQL added the means to do so in SPI only in PostgreSQL 9.0, and a future -PL/Java major release should use it. However, this release does make two small -changes to the current behavior. - -Without the query analysis results from PostgreSQL, PL/Java tries to type the -prepared-statement parameters based on the types of values supplied by the -application Java code. It now has two additional ways to do so: - -* If Java code supplies a Java user-defined type (UDT)---that is, an object - implementing the `SQLData` interface---PL/Java will now call the `SQLData` - method `getSQLTypeName` on that object and use the result to pin down - the PostgreSQL type of the parameter. Existing code should already provide - this method, but could, in the past, have returned a bogus result without - detection, as PL/Java did not use it. - -* Java code can use the three-argument form of `setNull` to specify the exact - PostgreSQL type for a parameter, and then another method can be used to - supply a non-null value for it. If the following non-null value has - a default mapping to a different PostgreSQL type, in most cases it will - overwrite the type supplied with `setNull` and re-plan the query. That was - PL/Java's existing behavior, and was not changed for this minor release. - However, the new types introduced in this release---the `java.time` types - and `SQLXML`---behave in the way that should become universal in a future - major release: the already-supplied PostgreSQL type will be respected, and - PL/Java will try to find a usable coercion to it. - -$h4 Inaccuracies converting TIMESTAMP and TIMESTAMPTZ - -When converting between PostgreSQL values of `timestamp` or `timestamptz` type -and the pre-Java 8 original JDBC type `java.sql.Timestamp`, there were cases -where values earlier than 1 January 2000 would produce exceptions rather than -converting successfully. Those have been fixed. - -Also, converting in the other direction, from `java.sql.Timestamp` to a -PostgreSQL timestamp, an error of up to 1.998 seconds (averaging 0.999) -could be introduced. - -That error has been corrected. If an application has stored Java `Timestamp`s -and corresponding SQL `timestamp`s generated in the past and requires them -to match, it could be affected by this change. - -$h4 New date/time/timestamp API in Java 8 `java.time` package - -The old, and still default, mappings in JDBC from the SQL `date`, `time`, and -`timestamp` types to `java.sql.Date`, `java.sql.Time`, and `java.sql.Timestamp`, -were never well suited to represent the PostgreSQL data types. The `Time` and -`Timestamp` classes were used to map both the with-timezone and without-timezone -variants of the corresponding SQL types and, clearly, could not represent both -equally well. These Java classes all contain timezone dependencies, requiring -the conversion to involve timezone calculations even when converting non-zoned -SQL types, and making the conversion results for non-zoned types implicitly -depend on the current PostgreSQL session timezone setting. - -Applications are strongly encouraged to adopt Java 8 as a minimum language -version and use the new-in-Java-8 types in the `java.time` package, which -eliminate those problems and map the SQL types much more faithfully. -For PL/Java function parameters and returns, the class in the method declaration -can simply be changed. For retrieving date/time/timestamp values from a -`ResultSet` or `SQLInput` object, use the variants of `getObject` / `readObject` -that take a `Class` parameter. The class to use is: - -| PostgreSQL type | `java.time` class | -|--:|:--| -|`date`|`LocalDate`| -|`time without time zone`|`LocalTime`| -|`time with time zone`|`OffsetTime`| -|`timestamp without time zone`|`LocalDateTime`| -|`timestamp with time zone`|`OffsetDateTime`| -[Correspondence of PostgreSQL date/time types and Java 8 `java.time` classes] - -Details on these mappings are [added to the documentation](use/datetime.html). - -$h4 Newly supported `java.sql.SQLXML` type - -PL/Java has not, until now, supported the JDBC 4.0 `SQLXML` type. PL/Java -functions have been able to work with PostgreSQL XML values by mapping them -as Java `String`, but that conversion could introduce character encoding issues -outside the control of the XML APIs, and also has memory implications if an -application stores, or generates in queries, large XML values. Even if the -processing to be done in the application could be structured to run in constant -bounded memory while streaming through the XML, a conversion to `String` -requires the whole, uncompressed, character-serialized value to be brought into -the Java heap at once, and any heap-size tuning has to account for that -worst-case size. The `java.sql.SQLXML` API solves those problems by allowing -XML manipulation with any of several Java XML APIs with the data remaining in -PostgreSQL native memory, never brought fully into the Java heap unless that is -what the application does. Heap sizing can be based on the just the -application's processing needs. - -The `SQLXML` type can take the place of `String` in PL/Java function parameters -and returns simply by changing their declarations from `String` to `SQLXML`. -When retrieving XML values from `ResultSet` or `SQLInput` objects, the legacy -`getObject / readObject` methods will continue to return `String` for existing -application compatibility, so the specific `getSQLXML / readSQLXML` methods, or -the forms of `getObject / readObject` with a `Class` parameter and passing -`SQLXML.class`, must be used. A [documentation page](use/sqlxml.html) has been -added, and the [PassXML example][exxml] illustrates use of the API. - -A [not-built-by-default new example][exsaxon] (because it depends on Java 8 and -the Saxon-HE XML-processing library) provides a partial implementation of true -`XMLQUERY` and `XMLTABLE` functions for PostgreSQL, using the standard-specified -XML Query language rather than the XPath 1.0 of the native PostgreSQL functions. - -[exxml]: pljava-examples/apidocs/index.html?org/postgresql/pljava/example/annotation/PassXML.html -[exsaxon]: examples/saxon.html - -$h4 New Java property exposes the PostgreSQL server character-set encoding - -A Java system property, `org.postgresql.server.encoding`, is set to the -canonical name of a supported Java `Charset` that corresponds to PostgreSQL's -`server_encoding` setting, if one can be found. If the server encoding's name -is not recognized as any known Java `Charset`, this property will be unset, and -some functionality, such as the `SQLXML` API, may be limited. If a Java -`Charset` does exist (or is made available through a `CharsetProvider`) that -does match the PostgreSQL server encoding, but is not automatically selected -because of a naming mismatch, the `org.postgresql.server.encoding` property can -be set (with a `-D` in `pljava.vmoptions`) to select it by name. - -$h4 ResultSet holdability - -A `ResultSet` obtained from a query done in PL/Java would return the value -`CLOSE_CURSORS_AT_COMMIT` to a caller of its `getHoldability` method, but in -reality would become unusable as soon as the PL/Java function creating it -returned to PostgreSQL. It now remains usable to the end of the transaction, -as claimed. - -$h4 PostgreSQL 9.6 and parallel query - -A function in PL/Java can now be [annotated][apianno] -`parallel={UNSAFE | RESTRICTED | SAFE}`, with `UNSAFE` the default. -A new [user guide section][ugparqry] explains the possibilities and -tradeoffs. (Good cases for marking a PL/Java function `SAFE` may be -rare, as pushing such a function into multiple background processes -will require them all to start JVMs. But if a practical application -arises, PostgreSQL's `parallel_setup_cost` can be tuned to help the -planner make good plans.) - -Although `RESTRICTED` and `SAFE` Java functions work in simple tests, -there has been no exhaustive audit of the code to ensure that PL/Java's -internal workings never violate the behavior constraints on such functions. -The support should be considered experimental, and could be a fruitful -area for beta testing. - -[ugparqry]: use/parallel.html - -$h4 Tuple counts widened to 64 bits with PostgreSQL 9.6 - -To accommodate the possibility of more than two billion tuples in a single -operation, the SPI implementation of the JDBC `Statement` interface now -provides the JDK 8-specified `executeLargeBatch` and `getLargeUpdateCount` -methods defined to return `long` counts. The original `executeBatch` and -`getUpdateCount` methods remain but, obviously, cannot return counts that -exceed `INT_MAX`. In case the count is too large, `getUpdateCount` will throw -an `ArithmeticException`; `executeBatch` will store `SUCCESS_NO_INFO` for -any statement in the batch that affected too many tuples to report. - -For now, a `ResultSetProvider` cannot be used to return more than `INT_MAX` -tuples, but will check that condition and throw an error to ensure predictable -behavior. - -$h4 `pg_upgrade` - -PL/Java should be upgraded to 1.5.1 in a database cluster, before that -cluster is binary-upgraded to a newer PostgreSQL version using `pg_upgrade`. -A new [Upgrading][upgrading] installation-guide section centralizes information -on both upgrading PL/Java in a database, and upgrading a database with PL/Java -in it. - -[upgrading]: install/upgrade.html - -$h4 Suppressing row operations from triggers - -In PostgreSQL, a `BEFORE ROW` trigger is able to allow the proposed row -operation, allow it with modified values, or silently suppress the operation -for that row. Way back in PL/Java 1.1.0, the way to produce the 'suppress' -outcome was for the trigger method to throw an exception. Since PL/Java 1.2.0, -however, an exception thrown in a trigger method is used to signal an error -to PostgreSQL, and there has not been a way to suppress the row operation. - -The `TriggerData` interface now has a [`suppress`][tgsuppress] method that -the trigger can invoke to suppress the operation for the row. - -[tgsuppress]: pljava-api/apidocs/index.html?org/postgresql/pljava/TriggerData.html#suppress() - -$h4 Constraint triggers - -New attributes in the `@Trigger` annotation allow the SQL generator to -create constraint triggers (a type of trigger that can be created with SQL -since PostgreSQL 9.1). Such triggers will be delivered by the PL/Java runtime -(to indicate that a constraint would be violated, a constraint trigger -method should throw an informative exception). However, the trigger method -will have access, through the `TriggerData` interface, only to the properties -common to ordinary triggers; methods on that interface to retrieve properties -specific to constraint triggers have not been added for this release. - -$h4 PostgreSQL 10 and trigger transition tables - -A trigger [annotation][apianno] can now specify `tableOld="`_name1_`"` or -`tableNew="`_name2_`"`, or both, and the PL/Java function servicing the -trigger can do SPI JDBC queries and see the transition table(s) under the -given name(s). The [triggers example code][extrig] has been extended with -a demonstration. - -[extrig]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java - -$h4 Logging from Java - -The way the Java logging system has historically been plumbed to PostgreSQL's, -as described in [issue 125](${ghbug}125), can be perplexing both because it -is unaffected by later changes to the PostgreSQL settings after PL/Java is -loaded in the session, and because it has honored only `log_min_messages` -and ignored `client_min_messages`. The second part is easy to fix, so in -1.5.1 the threshold where Java discards messages on the fast path is -determined by the finer of `log_min_messages` and `client_min_messages`. - -$h4 Conveniences for downstream package maintainers - -The `mvn` command to build PL/Java will now accept an option to provide -a useful default for `pljava.libjvm_location`, when building a package for -a particular software environment where the likely path to Java is known. - -The `mvn` command will also accept an option to specify, by the path to -the `pg_config` executable, the PostgreSQL version to build against, in -case multiple versions exist on the build host. This was already possible -by manipulating `PATH` ahead of running `mvn`, but the option makes it more -explicit. - -A new [packaging section][packaging] in the build guide documents those -and a number of considerations for making a PL/Java package. - -[packaging]: build/package.html - -$h3 Enhancement requests addressed - -$h4 In 1.5.1-BETA3 - -* [Add a ddr.reproducible option to SQL generator](${ghbug}186) - -$h4 In 1.5.1-BETA2 - -* [java 8 date/time api](${ghbug}137) -* [Annotations don't support CREATE CONSTRAINT TRIGGER](${ghbug}138) -* [Let annotations give defaults to row-type parameters](${ghpull}153) -* [Improve DDR generator on the dont-repeat-yourself dimension for UDT type mapping](${ghpull}159) -* [Support the JDBC 4.0 SQLXML type](${ghpull}171) - -$h3 Bugs fixed - -$h4 In 1.5.1-BETA3 - -* [self-install jar ClassCastException (...ConsString to String), some java 6/7 runtimes](${ghbug}179) -* [i386 libjvm_location gets mangled as .../jre/lib/1/server/libjvm.so](${ghbug}176) -* [java.lang.ClassNotFoundException installing examples jar](${ghbug}178) -* [Preprocessor errors building on Windows with MSVC](${ghbug}182) -* [Saxon example does not build since Saxon 9.9 released](${ghbug}185) -* [Segfault in VarlenaWrapper.Input on 32-bit](${ghbug}177) -* [Windows: self-install jar silently fails to replace existing files](${ghbug}189) -* [ERROR: java.sql.SQLException: _some Java class name_](${ghbug}192) -* [SetOfRecordTest with timestamp column influenced by environment ](${ghbug}195) - -$h4 In 1.5.1-BETA2 - -* [PostgreSQL 10: SPI_modifytuple failed with SPI_ERROR_UNCONNECTED](${ghbug}134) -* [SPIConnection prepareStatement doesn't recognize all parameters](${ghbug}136) -* [Ordinary (non-constraint) trigger has no way to suppress operation](${ghbug}142) -* [ResultSetHandle and column definition lists](${ghbug}146) -* [PreparedStatement doesn't get parameter types from PostgreSQL](${ghbug}149) - _(partial improvements)_ -* [internal JDBC: inaccuracies converting TIMESTAMP and TIMESTAMPTZ](${ghbug}155) -* [Missing type mapping for Java return `byte[]`](${ghbug}157) -* [The REMOVE section of DDR is in wrong order for conditionals](${ghbug}163) -* [Loading PL/Java reinitializes timeouts in PostgreSQL >= 9.3](${ghbug}166) -* [JDBC ResultSet.CLOSE_CURSORS_AT_COMMIT reported, but usable life shorter](${ghbug}168) - -$h4 In 1.5.1-BETA1 - -* [Add support for PostgreSQL 9.6](${ghbug}108) -* [Clarify documentation of ResultSetProvider](${ghbug}115) -* [`pg_upgrade` (upgrade failure from 9.5 to 9.6)](${ghbug}117) -* [Java logging should honor `client_min_messages` too](${ghbug}125) - -$h3 Updated PostgreSQL APIs tracked - -* `heap_form_tuple` -* 64-bit `SPI_processed` -* 64-bit `Portal->portalPos` -* 64-bit `FuncCallContext.call_cntr` -* 64-bit `SPITupleTable.alloced` and `.free` -* `IsBackgroundWorker` -* `IsBinaryUpgrade` -* `SPI_register_trigger_data` -* `SPI` without `SPI_push`/`SPI_pop` -* `AllocSetContextCreate` -* `DefineCustom...Variable` (no `GUC_LIST_QUOTE` in extensions) - -$h3 Credits - -There is a PL/Java 1.5.1 thanks in part to -Christoph Berg, -Thom Brown, -Luca Ferrari, -Chapman Flack, -Petr Michalek, -Steve Millington, -Kenneth Olson, -Fabian Zeindl, -original creator Thomas Hallgren, -and the many contributors to earlier versions. - -$h2 PL/Java 1.5.0 (29 March 2016) - -This, the first PL/Java numbered release since 1.4.3 in 2011, combines -compatibility with the latest PostgreSQL and Java versions with modernized -build and installation procedures, automatic generation of SQL deployment -code from Java annotations, and many significant fixes. - -$h3 Security - -Several security issues are addressed in this release. Sites already -using PL/Java are encouraged to update to 1.5.0. For several of the -issues below, practical measures are described to mitigate risk until -an update can be completed. - -[CVE-2016-0766][], a privilege escalation requiring an authenticated -PostgreSQL connection, is closed by installing PL/Java 1.5.0 (including -prereleases) or by updating PostgreSQL itself to at least 9.5.1, 9.4.6, -9.3.11, 9.2.15, 9.1.20. Vulnerable systems are only those running both -an older PL/Java and an older PostgreSQL. - -[CVE-2016-0767][], in which an authenticated PostgreSQL user with USAGE -permission on the `public` schema may alter the `public` schema classpath, -is closed by release 1.5.0 (including prereleases). If updating to 1.5.0 -must be delayed, risk can be mitigated by revoking public `EXECUTE` permission -on `sqlj.set_classpath` and granting it selectively to responsible users or -roles. - -This release brings a policy change to a more secure-by-default posture, -where the ability to create functions in `LANGUAGE java` is no longer -automatically granted to `public`, but can be selectively granted to roles -that will have that responsibility. The change reduces exposure to a known -issue present in 1.5.0 and earlier versions, that will be closed in a future -release ([CVE-2016-0768][], see **large objects, access control** below). - -The new policy will be applied in a new installation; permissions will not -be changed in an upgrade, but any site can move to this policy, even before -updating to 1.5.0, with `REVOKE USAGE ON LANGUAGE java FROM public;` followed by -explicit `GRANT` commands for the users/roles expected to create Java -functions. - -[CVE-2016-2192][], in which an authenticated user can alter type mappings -without owning the types involved. Exploitability is limited by other -permissions, but if type mapping is a feature being used at a site, one -can interfere with proper operation of code that relies on it. A mitigation -is simply to `REVOKE EXECUTE ... FROM PUBLIC` on the `sqlj.add_type_mapping` -and `sqlj.drop_type_mapping` functions, and grant the privilege only to -selected users or roles. As of 1.5.0, these functions require the invoker -to be superuser or own the type being mapped. - -[CVE-2016-0766]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0766 -[CVE-2016-0767]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0767 -[CVE-2016-0768]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0768 -[CVE-2016-2192]: http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-2192 - -$h3 Version compatibility - -PL/Java 1.5.0 can be built against recent PostgreSQL versions including 9.5, -using Java SE 8, 7, or 6. See [version compatibility][versions] for more -detail. OpenJDK is well supported. Support for GCJ has been dropped; features -of modern Java VMs that are useful to minimize footprint and startup time, -such as class-data sharing, are now more deeply covered -[in the installation docs][vmopts]. - -[versions]: build/versions.html -[vmopts]: install/vmoptions.html - -$h3 Build procedures - -Since 2013, PL/Java has been hosted [on GitHub][ghpljava] and built -using [Apache Maven][mvn]. See the new [build instructions][bld] for details. - -Reported build issues for specific platforms have been resolved, -with new platform-specific build documentation -for [OS X][osxbld], [Solaris][solbld], [Ubuntu][ububld], -[Windows MSVC][msvcbld], and [Windows MinGW-w64][mgwbld]. - -The build produces a redistributable installation archive usable with -the version of PostgreSQL built against and the same operating system, -architecture, and linker. The type of archive is `jar` on all platforms, as -all PL/Java installations will have Java available. - -[ghpljava]: https://github.com/tada/pljava -[mvn]: http://maven.apache.org/ -[bld]: build/build.html -[msvcbld]: build/buildmsvc.html -[mgwbld]: build/mingw64.html -[osxbld]: build/macosx.html -[solbld]: build/solaris.html -[ububld]: build/ubuntu.html - -$h3 Installation procedures - -The jar produced by the build is executable and will self-extract, -consulting `pg_config` on the destination system to find the correct -default locations for the extracted files. Any location can be overridden. -(Enhancement requests [6][gh6], [9][gh9]) - -PL/Java now uses a PostgreSQL configuration variable, `pljava.libjvm_location`, -to find the Java runtime to use, eliminating the past need for highly -platform-specific tricks like link-time options or runtime-loader configuration -just so that PL/Java could find Java. PostgreSQL configuration variables are -now the only form of configuration needed for PL/Java, and the `libjvm_location` -should be the only setting needed if file locations have not been overridden. - -In PostgreSQL 9.1 and later, PL/Java can be installed with -`CREATE EXTENSION pljava`. Regardless of PostgreSQL version, installation -has been simplified. Former procedures involving `Deployer` or `install.sql` -are no longer required. Details are in the [new installation instructions][ins]. - -$h4 Schema migration - -The tables used internally by PL/Java have changed. If PL/Java 1.5.0 is -loaded in a database with an existing `sqlj` schema populated by an earlier -PL/Java version (1.3.0 or later), the structure will be updated without data -loss (enhancement request [12][gh12]). *Remember that PL/Java runs independently -in each database session where it is in use. Older PL/Java versions active in -other sessions can be disrupted by the schema change.* - -A trial installation of PL/Java 1.5.0 can be done in a transaction, and -rolled back if desired, leaving the schema as it was. Any concurrent sessions -with active older PL/Java versions will not be disrupted by the altered schema -as long as the transaction remains open, *but they may block for the duration, -so such a test transaction should be kept short*. - -[ins]: install/install.html - -$h3 Changes - -$h4 Behavior of `readSQL` and `writeSQL` for base and mirror user-defined types - -In the course of fixing [issue #98][gh98], the actual behavior of -`readSQL` and `writeSQL` with base or mirror types, which had not -previously been documented, [now is](develop/coercion.html), along with -other details of PL/Java's type coercion rules found only in the code. -Because machine byte order was responsible for issue #98, it now (a) is -selectable, and (b) has different, appropriate, defaults for mirror UDTs -(which need to match PostgreSQL's order) and for base UDTs (which must -stay big-endian because of how binary `COPY` is specified). -A [new documentation section](use/byteorder.html) explains in detail. - -$h4 `USAGE` to `PUBLIC` no longer default for `java` language - -Of the two languages installed by PL/Java, functions that declare -`LANGUAGE javau` can be created only by superusers, while those that -declare `LANGUAGE java` can be created by any user or role granted the -`USAGE` privilege on the language. - -In the past, the language `java` has been created with PostgreSQL's -default permission granting `USAGE` to `PUBLIC`, but PL/Java 1.5.0 -leaves the permission to be explicitly granted to those users or roles -expected to create Java functions, in keeping with least-privilege -principles. See **large objects, access control** under **known issues** -for background. - -$h4 SQL generated by Java annotations - -Java code developed for use by PL/Java can carry in-code annotations, -used by the Java compiler to generate the SQL commands to declare the -new functions, types, triggers, etc. in PostgreSQL (enhancement request -[1011112][], though different in implementation). This eliminates the need -to have Java code and the corresponding SQL commands developed in parallel, -and the class of errors possible when both are not updated together. It -also allows compile-time checks that the Java methods or classes being -annotated are suitable (correct access modifiers, signatures, etc.) -for their declared SQL purposes, rather than discovering -such issues only upon loading the code into PostgreSQL and trying to use it. - -The Java compiler writes the generated SQL into a "deployment descriptor" -file (`pljava.ddr` by default), as specified by the SQL/JRT standard. The -file can be included in a `jar` archive with the compiled code, and the -commands will be executed by PL/Java when the `install_jar` function is -used to load the jar. - -SQL generation is covered in the [updated user documentation][user], -and illustrated in the [Hello, World example][hello] and -[several other supplied examples][exanno]. Reference information -is [in the API documentation][apianno]. It is currently usable to declare -functions, triggers, and user-defined types, both base and composite. - -[user]: use/use.html -[hello]: use/hello.html -[exanno]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation -[apianno]: pljava-api/apidocs/index.html?org/postgresql/pljava/annotation/package-summary.html#package_description - -The history of this feature in PL/Java is long, with the first related commits -appearing in 2005, six years in advance of an enhancement request for it. -It became generally usable in 2013 when building with -Java SE 6 or later, using the annotation processing framework Java introduced -in that release. 1.5.0 is the first PL/Java numbered release to feature it. - -$h5 Annotation keyword changes - -If you have been using the SQL generation feature in prerelease `git` builds of -2013 or later, be aware that some annotation keywords have changed in finalizing -the 1.5.0 release. Java code that was compiled using the earlier keywords will -continue to work, but will have to be updated before it can be recompiled. - -* For functions: `effects=(VOLATILE,STABLE,IMMUTABLE)` was formerly `type=` -* For functions: `type=` (_an explicit SQL return type for the function_) - was formerly `complexType=` -* For functions: `trust=(SANDBOXED,UNSANDBOXED)` was formerly - `(RESTRICTED,UNRESTRICTED)` -* For triggers: `called=(BEFORE,AFTER,INSTEAD_OF)` was formerly `when=` - and conflicted with the `WHEN` clause introduced for triggers - in PostgreSQL 9.0. - -$h4 A jar may have more than one deployment descriptor - -PL/Java formerly allowed only one entry in a jar to be a deployment -descriptor (that is, a file of SQL commands to be executed upon loading -or unloading the jar). The SQL/JRT standard allows multiple entries to -be deployment descriptors, executed in the order they are mentioned -_in the jar manifest_, or the reverse of that order when the jar is -being unloaded. PL/Java now conforms to the standard. - -The behavior is useful during transition to annotation-driven deployment -descriptor generation for a project that already has a manually-maintained -deployment descriptor. PL/Java's own `pljava-examples` project is an -illustration, in the midst of such a transition itself. - -Note the significance placed by SQL/JRT on the order of entries in a jar -manifest, whose order is normally _not_ significant according to the Jar File -Specification. Care can be needed when manipulating manifests with automated -tools that may not preserve order. - -$h4 Conditional execution within deployment descriptors - -Deployment descriptors have a primitive conditional-execution provision -defined in the SQL/JRT standard: commands wrapped in a -`BEGIN IMPLEMENTOR ` _identifier_ construct will only be executed if the -_identifier_ is recognized by the SQL/JRT implementation in use. The design -makes possible jars that can be installed on different database systems that -provide SQL/JRT features, with database-specific commands wrapped in -`BEGIN IMPLEMENTOR` blocks with an _identifier_ specific to the system. -By default, PL/Java recognizes the _identifier_ `postgresql` (matched without -regard to case). - -PL/Java extends the standard by allowing the PostgreSQL configuration -variable `pljava.implementors` to contain a list of identifiers that will -be recognized. SQL code in a deployment descriptor can conditionally add -or remove identifiers in this list to influence which subsequent implementor -blocks will be executed, giving a still-primitive but more general control -structure. - -In sufficiently recent PostgreSQL versions, the same effect could be -achieved using `DO` statements and PL/pgSQL control structures, but this -facility in PL/Java does not require either to be available. - -$h4 Interaction with `SET ROLE` corrected - -PL/Java formerly was aware of the user ID associated with the running -session, but not any role ID that user may have acquired with `SET ROLE`. -The result would commonly be failed permission checks made by PL/Java when -the session user did not have the needed permission, but had `SET ROLE` to -a role that did. Likewise, within `install_jar`, PL/Java would execute -deployment descriptor commands as the original session user rather than -as the user's current role, with permission failures a likely result. - -Correcting this issue has changed the PL/Java API, but without a bump -of major version because the prior API, while deprecated, is still available. - -* [`getOuterUserName`][goun] and [`executeAsOuterUser`][eaou] are new, and - correctly refer to the session user or current role, when active. -* [`getSessionUserName`][gsun] and [`executeAsSessionUser`][easu] are still - present but deprecated, and _their semantics are changed_. They are now - deprecated aliases for the corresponding new methods, which honor the - set role. Use cases that genuinely need to refer only to the _session_ user - and ignore the role should be rare, and should be discussed on the mailing - list or opened as issues. - -#set($sessapi = 'pljava-api/apidocs/index.html?org/postgresql/pljava/Session.html#') - -[goun]: ${sessapi}getOuterUserName() -[eaou]: ${sessapi}executeAsOuterUser(java.sql.Connection,java.lang.String) -[gsun]: ${sessapi}getSessionUserName() -[easu]: ${sessapi}executeAsSessionUser(java.sql.Connection,java.lang.String) - -$h4 Unicode transparency - -Since the resolution of [bug 21][gh21], PL/Java contains a regression test -to ensure that character strings passed and returned between PostgreSQL and -Java will round-trip without alteration for the full range of Unicode -characters, _when the database encoding is set to `UTF8`_. - -More considerations apply when the database encoding is anything other -than `UTF8`, and especially when it is `SQL_ASCII`. Please see -[character encoding support][charsets] for more. - -[charsets]: use/charsets.html - -$h3 Enhancement requests addressed - -* [Use Annotations instead of DDL Manifest][1011112] -* [Installation of pljava on postgresql servers][gh9] -* [Find an alternative way to install the pljava.so in `/usr/lib`][gh6] -* [Provide database migration][gh12] -* [Support types with type modifiers][1011140] (partial: see [example][typmex]) -* [Build process: accommodate Solaris 10][gh102] - -[1011112]: ${pgffeat}1011112 -[1011140]: ${pgffeat}1011140 -[gh9]: ${ghbug}9 -[gh6]: ${ghbug}6 -[gh12]: ${ghbug}12 -[gh102]: ${ghbug}102 - -[typmex]: $project.scm.url/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/IntWithMod.java - -$h3 Bugs fixed - -$h4 Since 1.5.0-BETA3 - -* [Build process: accept variation in PostgreSQL version string][gh101] -* [Build process: accommodate PostgreSQL built with private libraries][gh103] -* Clarified message when `CREATE EXTENSION` fails because new session needed -* Reduced stack usage in SQL generator - (small-memory build no longer needs `-Xss`) - -$h4 In 1.5.0-BETA3 - -* [Bogus mirror-UDT values on little-endian hardware][gh98] -* [Base UDT not registered if first access isn't in/out/send/recv][gh99] -* `TupleDesc` leak warnings with composite UDTs -* also added regression test from [1010962][] report - -$h4 In 1.5.0-BETA2 - -* [Generate SQL for trigger function with no parameters][gh92] -* [openssl/ssl.h needed on osx el-capitan (latest 10.11.3)/postgres 9.5][gh94] - (documented) -* [Source location missing for some annotation errors][gh95] -* [OS X El Capitan "Java 6" dialog when loading ... Java 8][gh96] -* pljava-api jar missing from installation jar - -$h4 In 1.5.0-BETA1 - -* [SPIPreparedStatement.setObject() fails with Types.BIT][1011119] -* [SSLSocketFactory throws IOException on Linux][1011095] -* [PL/Java fails to compile with -Werror=format-security][1011181] -* [PL/Java does not build on POWER 7][1011197] -* [The built in functions do not use the correct error codes][1011206] -* [TupleDesc reference leak][1010962] -* [String conversion to enum fails][gh4] -* [segfault if SETOF RECORD-returning function used without AS at callsite][gh7] -* [pl/java PG9.3 Issue][gh17] -* [No-arg functions unusable: "To many parameters - expected 0"][gh8] -* [Exceptions in static initializers are masked][gh54] -* [UDT in varlena form breaks if length > 32767][gh52] -* [PL/Java kills unicode?][gh21] -* [Type.c expects pre-8.3 find_coercion_pathway behavior][gh65] -* [Support PostgreSQL 9.5][gh48] -* [pl/java getting a build on MacOSX - PostgreSQL 9.3.2][gh22] -* [build pljava on windows for PostgreSQL 9.2][gh23] -* [Error while installing PL/Java with Postgresql 9.3.4 64 bit on Windows 7 64 bit System][gh28] -* [pljava does not compile on mac osx ver 10.11.1 and postgres 9.4][gh63] -* [pljava does not compile on centos 6.5 and postgres 9.4][gh64] -* [Error installing pljava with Windows 7 64 Bit and Postgres 9.4][gh71] -## JNI_getIntArrayRegion instead of JNI_getShortArrayRegion -## Eclipse IDE artifacts -## Site -## Warnings -## Javadoc - -[1011119]: ${pgfbug}1011119 -[1011095]: ${pgfbug}1011095 -[1011181]: ${pgfbug}1011181 -[1011197]: ${pgfbug}1011197 -[1011206]: ${pgfbug}1011206 -[1010962]: ${pgfbug}1010962 -[gh4]: ${ghbug}4 -[gh7]: ${ghbug}7 -[gh8]: ${ghbug}8 -[gh17]: ${ghbug}17 -[gh54]: ${ghbug}54 -[gh52]: ${ghbug}52 -[gh21]: ${ghbug}21 -[gh65]: ${ghbug}65 -[gh48]: ${ghbug}48 -[gh22]: ${ghbug}22 -[gh23]: ${ghbug}23 -[gh28]: ${ghbug}28 -[gh63]: ${ghbug}63 -[gh64]: ${ghbug}64 -[gh71]: ${ghbug}71 -[gh92]: ${ghbug}92 -[gh94]: ${ghbug}94 -[gh95]: ${ghbug}95 -[gh96]: ${ghbug}96 -[gh98]: ${ghbug}98 -[gh99]: ${ghbug}99 -[gh101]: ${ghbug}101 -[gh103]: ${ghbug}103 - -$h3 Updated PostgreSQL APIs tracked - -Several APIs within PostgreSQL itself have been added or changed; -PL/Java now uses the current versions of these where appropriate: - -* `find_coercion_pathway` -* `set_stack_base` -* `GetOuterUserId` -* `GetUserNameFromId` -* `GetUserIdAndSecContext` -* `pg_attribute_*` -* Large objects: truncate, and 64-bit offsets - -$h3 Known issues and areas for future work - -$h4 Developments in PostgreSQL not yet covered - -Large objects, access control -: PL/Java does not yet expose PostgreSQL large objects with a documented, - stable API, and the support it does contain was developed against pre-9.0 - PostgreSQL versions, where no access control applied to large objects and - any object could be accessed by any database user. PL/Java's behavior is - proper for PostgreSQL before 9.0, but improper on 9.0+ where it would be - expected to honor access controls on large objects ([CVE-2016-0768][]). - This will be corrected in a future release. For this and earlier releases, - the recommendation is to selectively grant `USAGE` on the `java` language to - specific users or roles responsible for creating Java functions; see - "default `USAGE` permssion" under Changes. - -`INSTEAD OF` triggers, triggers on `TRUNCATE` -: These are supported by annotations and the SQL generator, and the runtime - will deliver them to the specified method, but the `TriggerData` interface - has no new methods to recognize these cases (that is, no added - methods analogous to `isFiredAfter`, `isFiredByDelete`). For a method - expressly coded to be a `TRUNCATE` trigger or an `INSTEAD OF` trigger, - that is not a problem, but care should be taken when coding a trigger - method to handle more than one type of trigger, or creating triggers of - these new types that call a method developed pre-PL/Java-1.5.0. Such a - method could be called with a `TriggerData` argument whose existing - `isFired...` methods all return `false`, likely to put the method on an - unexpected code path. - - A later PL/Java version should introduce trigger interfaces that better - support such evolution of PostgreSQL in a type-safe way. - -Constraint triggers -: Constraint trigger syntax is not supported by annotations and the SQL - generator. If declared (using hand-written SQL), they will be delivered - by the runtime, but without any constraint-trigger-specific information - available to the called method. - -Event triggers -: Event triggers are not yet supported by annotations or the SQL generator, - and will not be delivered by the PL/Java runtime. - -Range types -: No predefined mappings for range types are provided. - -`PRE_PREPARE`, `PRE_COMMIT`, `PARALLEL_ABORT`, `PARALLEL_PRE_COMMIT`, and `PARALLEL_COMMIT` transaction callbacks, `PRE_COMMIT` subtransaction callbacks -: Listeners for these events cannot be registered and the events will not - be delivered. - -$h4 Imperfect integration with PostgreSQL dependency tracking - -In a dump/restore, manual intervention can be needed if the users/roles -recorded as owners of jars are missing or have been renamed. A current -[thread on `pgsql-hackers`][ownhack] should yield a better solution for -a future release. - -[ownhack]: http://www.postgresql.org/message-id/56783412.6090005@anastigmatix.net - -$h4 Quirk if deployment descriptor loads classes from same jar - -The `install_jar` function installs a jar, optionally reading deployment -descriptors from the jar and executing the install actions they contain. -It is possible for those actions to load classes from the jar just installed. -(This would be unlikely if the install actions are limited to typical setup, -function/operator/datatype creation, but likely, if the install actions also -include basic function tests, or if the setup requirements are more -interesting.) - -If, for any class in the jar, the first attempt to load that class is made -while resolving a function declared `STABLE` or `IMMUTABLE`, a -`ClassNotFoundException` results. The cause is PostgreSQL's normal treatment of -a `STABLE` or `IMMUTABLE` function, which relies on a snapshot from the start of -the `install_jar` query, when the jar was not yet installed. A workaround is to -ensure that the install actions cause each needed class to be loaded, such as -by calling a `VOLATILE` function it supplies, before calling one that is -`STABLE` or `IMMUTABLE`. (One could even write install actions to declare a -needed function `VOLATILE` before the first call and then redeclare it.) - -This issue should be resolved as part of a broader rework of class loading -in a future PL/Java release. - -$h4 Partial implementation of JDBC 4 and later - -The changes to make PL/Java build under Java SE 6 and later, with version 4.0 -and later of JDBC, involved providing the specified methods so -compilation would succeed, with real implementations for some, but for others -only stub versions that throw `SQLFeatureNotSupportedException` if used. -Regrettably, there is nothing in the documentation indicating which methods -have real implementations and which do not; to create such a list would require -an audit of that code. If a method throws the exception when you call it, it's -one of the unimplemented ones. - -Individual methods may be fleshed out with implementations as use cases arise -that demand them, but for a long-term roadmap, it seems more promising to -reduce the overhead of maintaining another JDBC implementation by sharing -code with `pgjdbc`, as has been [discussed on pljava-dev][jdbcinherit]. - -[jdbcinherit]: http://lists.pgfoundry.org/pipermail/pljava-dev/2015/002370.html - -$h4 Exception handling and logging - -PL/Java does interconvert between PostgreSQL and Java exceptions, but with -some loss of fidelity in the two directions. PL/Java code has some access -to most fields of a PostgreSQL error data structure, but only through -internal PL/Java API that is not expected to remain usable, and code written -for PL/Java has never quite had first-class standing in its ability to -_generate_ exceptions as information-rich as those from PostgreSQL itself. - -PL/Java in some cases generates the _categorized `SQLException`s_ introduced -with JDBC 4.0, and in other cases does not. - -This area may see considerable change in a future release. -[Thoughts on logging][tol] is a preview of some of the considerations. - -[tol]: https://github.com/tada/pljava/wiki/Thoughts-on-logging - -$h4 Types with type modifiers and `COPY` - -Although it is possible to create a PL/Java user-defined type that accepts -a type modifier (see the [example][typmex]), such a type will not yet be -handled by SQL `COPY` or any other operation that requires the `input` or -`receive` function to handle the modifier. This is left for a future release. - -$h3 Credits - -PL/Java 1.5.0 owes its being to original creator Thomas Hallgren and -many contributors: - -Daniel Blanch Bataller, -Peter Brewer, -Frank Broda, -Chapman Flack, -Marty Frasier, -Bear Giles, -Christian Hammers, -Hal Hildebrand, -Robert M. Lefkowitz, -Eugenie V. Lyzenko, -Dos Moonen, -Asif Naeem, -Kenneth Olson, -Johann Oskarsson, -Thomas G. Peters, -Srivatsan Ramanujam, -Igal Sapir, -Jeff Shaw, -Rakesh Vidyadharan, -`grunjol`, -`mc-soi`. - -Periods in PL/Java's development have been sponsored by EnterpriseDB. - -In the 1.5.0 release cycle, multiple iterations of testing effort -have been generously contributed by Kilobe Systems and by Pegasystems, Inc. - -## From this point on, the entries were reconstructed from old notes at the -## same time as the 1.5.0 notes were drafted, and they use a finer level of -## heading. So restore the 'real' values of the heading variables from here -## to the end of the file. -#set($h2 = '##') -#set($h3 = '###') -#set($h4 = '####') -#set($h5 = '#####') - -$h3 PL/Java 1.4.3 (15 September 2011) - -Notable changes in this release: - -* Works with PostgreSQL 9.1 -* Correctly links against IBM Java. -* Reads microseconds correctly in timestamps. - -Bugs fixed: - -* [Be clear about not building with JDK 1.6][1010660] -* [Does not link with IBM VM][1010970] -* [SPIConnection.getMetaData() is incorrectly documented][1010971] -* [PL/Java 1.4.2 Does not build with x86_64-w64-mingw32][1011025] -* [PL/Java does not build with PostgreSQL 9.1][1011091] - -Feature Requests: - -* [Allow pg_config to be set externally to the Makefile][1011092] -* [Add option to have pljava.so built with the runtime path of libjvm.so][1010955] - -[1010660]: ${pgfbug}1010660 -[1010970]: ${pgfbug}1010970 -[1010971]: ${pgfbug}1010971 -[1011025]: ${pgfbug}1011025 -[1011091]: ${pgfbug}1011091 - -[1011092]: ${pgffeat}1011092 -[1010955]: ${pgffeat}1010955 - -$h3 PL/Java 1.4.2 (11 December 2010) - -Bugfixes: - -* [Function returning complex objects with POD arrays cause a segfault][1010956] -* [Segfault when assigning an array to ResultSet column][1010953] -* [Embedded array support in returned complex objects][1010482] - -[1010956]: ${pgfbug}1010956 -[1010953]: ${pgfbug}1010953 -[1010482]: ${pgfbug}1010482 - -$h3 PL/Java 1.4.1 (9 December 2010) - -Note: Does not compile with Java 6. Use JDK 1.5 or 1.4. - -Compiles with PostgreSQL 8.4 and 9.0. - -Connection.getCatalog() has been implemented. - -Bugfixes: - -* [Compiling error with postgresql 8.4.1][1010759] -* [org.postgresql.pljava.internal.Portal leak][1010712] -* [build java code with debugging if server has debugging enabled][1010189] -* [Connection.getCatalog() returns null][1010653] -* [VM crash in TransactionListener][1010462] -* [Link against wrong library when compiling amd64 code on Solaris][1010954] - -[1010759]: ${pgfbug}1010759 -[1010712]: ${pgfbug}1010712 -[1010189]: ${pgfbug}1010189 -[1010653]: ${pgfbug}1010653 -[1010462]: ${pgfbug}1010462 -[1010954]: ${pgfbug}1010954 - -Other commits: - -For a multi-threaded pljava function we need to adjust stack_base_ptr -before calling into the backend to avoid stack depth limit exceeded -errors. Previously this was done only on query execution, but we need -to do it on iteration of the ResultSet as well. - -When creating a variable length data type, the code was directly -assigning the varlena header length rather than going through an -access macro. The header format changed for the 8.3 release and this -manual coding was not noticed and changed accordingly. Use -SET_VARSIZE to do this correctly. - -Handle passed by value data types by reading and writing directly to -the Datum rather than dereferencing it. - -If the call to a type output function is the first pljava call in a -session, we get a crash. The first pljava call results in a SPI -connection being established and torn down. The type output function -was allocating the result in the SPI memory context which gets -destroyed prior to returning the data to the caller. Allocate the -result in the correct context to survive function exit. - -Clean up a warning about byteasend and bytearecv not having a -prototype when building against 9.0 as those declarations are now in a -new header file. - - -$h3 PL/Java 1.4.0 (1 February 2008) - -Warning! The recent postgresql security releases changed the API of a function -that PL/Java uses. The source can be built against either version, but the -binaries will only run against the version they were built against. The PL/Java -binaries for 1.4.0 have all been built against the latest server releases (which -you should be using anyway). If you are using an older you will have to build -from source. The binary releases support: 8.3 - All versions. 8.2 - 8.2.6 and -up. 8.1 - 8.1.11 and up. 8.0 - 8.0.15 and up. - -$h3 PL/Java 1.3.0 (18 June 2006) - -This release is about type mapping and the creation of new types in PL/Java. An -extensive effort has gone into making the PL/Java type system extremely -flexible. Not only can you map arbitrary SQL data types to java classes. You can -also create new scalar types completely in Java. Read about the Changes in -version 1.3. - -$h4 Changes - -* A much improved type mapping system that will allow you to: - - * [Map any SQL type to a Java class][maptype] - * [Create a Scalar UDT in Java][scalarudt] - * [Map array and pseudo types][deftypemap] - -[maptype]: https://github.com/tada/pljava/wiki/Mapping-an-sql-type-to-a-java-class -[scalarudt]: https://github.com/tada/pljava/wiki/Creating-a-scalar-udt-in-java -[deftypemap]: https://github.com/tada/pljava/wiki/Default-type-mapping - -* Get the OID for a given relation ([feature request 1319][1319]) -* Jar manifest included in the SQLJ Jar repository - ([feature request 1525][1525]) - -$h4 Fixed bugs - -* [Reconnect needed for jar manipulation to take effect][1531] -* [Backends hang with test suite][1504] -* [Keeps crashing while making a call to a function][1560] -* [Memory Leak in Statement.executeUpdate][1556] -* [jarowner incorrect after dump and reload][1506] -* [Missing JAR manifest][1525] -* [TZ adjustments for Date are incorrect][1547] -* [Functions returning sets leaks memory][1542] -* [drop lib prefix][1423] -* ["oid" column is not available in trigger's NEW/OLD ResultSet][1317] -* [fails to run with GCJ, too][1480] -* [Compile failure with 8.1.4][1558] -* [fails to build with GCJ][1479] -* [Record returning function cannot be called with different structures within one session][1440] -* [Cannot map function with complex return type to method that uses non primitive arguments][1551] -* [Get OID for given relation][1319] - -[1531]: ${gborgbug}1531 -[1504]: ${gborgbug}1504 -[1560]: ${gborgbug}1560 -[1556]: ${gborgbug}1556 -[1506]: ${gborgbug}1506 -[1525]: ${gborgbug}1525 -[1547]: ${gborgbug}1547 -[1542]: ${gborgbug}1542 -[1423]: ${gborgbug}1423 -[1317]: ${gborgbug}1317 -[1480]: ${gborgbug}1480 -[1558]: ${gborgbug}1558 -[1479]: ${gborgbug}1479 -[1440]: ${gborgbug}1440 -[1551]: ${gborgbug}1551 -[1319]: ${gborgbug}1319 - -$h3 PL/Java 1.2.0 (20 Nov 2005) - -The PL/Java 1.2.0 release is primarily targeted at the new PostgreSQL 8.1 but -full support for 8.0.x is maintained. New features include support IN/OUT -parameters, improved meta-data handling, and better memory management. - -$h3 PL/Java 1.1.0 (14 Apr 2005) - -PL/Java 1.1.0 includes a lot of new features such as `DatabaseMetaData`, -`ResultSetMetaData`, language handlers for both trusted and untrusted language, -additional semantics for functions returning `SETOF`, and simple ObjectPooling. - -$h3 PL/Java 1.0.1 (07 Feb 2005) - -This release resolves a couple of important security issues. The most important -one is perhaps that PL/Java now is a trusted language. See [Security][] for more -info. Filip Hrbek, now member of the PL/Java project, contributed what was -needed to make this happen. - -[Security]: https://github.com/tada/pljava/wiki/Security - -$h3 PL/Java 1.0.0 (23 Jan 2005) - -Today, after a long period of fine tuning, PL/Java 1.0.0 was finally released. +$h2 [Releases prior to PL/Java 1.6.0](releasenotes-pre1_6.html) From 3aabd42a0efd80f0fc8d90b48dcf3b7478421f61 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 19:29:46 -0500 Subject: [PATCH 0827/1087] Update release notes. --- src/site/markdown/releasenotes.md.vm | 38 +++++++++++++++++++--------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index b6c205a6..a2639413 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,7 +10,32 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') -$h2 PL/Java 1.5.6 +$h2 PL/Java 1.5.7 + +1.5.7 is a bug-fix release, with a single issue backpatched from the 1.6 +branch, correcting a problem in XML Schema validation in some non-`en_US` +locales. + +$h3 Bugs fixed + +* [XML Schema regression-test failure in de_DE locale](${ghbug}312) + +$h3 Credits + +Thanks to Christoph Berg for the report. + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + +$h2 PL/Java 1.5.6 (4 October 2020) This release adds support for PostgreSQL 13. @@ -128,17 +153,6 @@ The work of Kartik Ohri in summer 2020 was supported by Google Summer of Code. [xqtdf]: https://www.w3.org/TR/xquery-31/#id-typed-data-feature [xqstf]: https://www.w3.org/TR/xquery-31/#id-static-typing-feature -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 PL/Java 1.5.5 (4 November 2019) This bug-fix release fixes runtime issues reported in 32-bit `i386` builds, some From 6d8f8fd8c94b29dbcce1adc8a4cbf2d3abf7015c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 19:30:46 -0500 Subject: [PATCH 0828/1087] Poke migration-management versions for 1.5.7 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 0d6e5414..f52baf7d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -495,6 +495,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_5_7 = REL_1_5_0; static final SchemaVariant REL_1_5_6 = REL_1_5_0; static final SchemaVariant REL_1_5_5 = REL_1_5_0; static final SchemaVariant REL_1_5_4 = REL_1_5_0; From 5ff0d7296e0bd4c65f0c0648d7b9dbf17ca36512 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 19:35:42 -0500 Subject: [PATCH 0829/1087] Add control file in preparation for next release Now that 1.5.7 is released, the next release should include an extension SQL file allowing upgrade from 1.5.7. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 55cb8c27..cb83152b 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Mon, 16 Nov 2020 19:49:47 -0500 Subject: [PATCH 0830/1087] Update release notes --- src/site/markdown/releasenotes.md.vm | 95 ++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index fe035389..a2a76f77 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,7 +10,89 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') -$h2 PL/Java 1.6.0 +$h2 PL/Java 1.6.1 + +This is the first minor update in the PL/Java 1.6 series, with two bugs fixed. +It also adds functionality in the SQL generator, allowing automated +declaration of new PostgreSQL aggregates, casts, and operators, and functions +with `OUT` parameters. + +$h3 Changes + +$h4 Limitations when built with Java 10 or 11 removed + +PL/Java can now be built with any Java 9 or later (latest tested is 15 at +time of writing), and the built artifact can use any Java 9 or later at +run time (as selected by the `pljava.libjvm_location` configuration variable). + +That was previously true when built with Java 9 or with Java 12 or later, but +not when built with 10 (would not run on 9) or with 11 (would not run on 9 +or 10). Those limits have been removed. + +$h4 Functions with `OUT` parameters + +PL/Java has long been able to declare a function that returns a composite +type (or a set of such), by returning a named composite PostgreSQL type, or +by being declared to return `RECORD`. + +The former approach requires separately declaring a new composite type to +PostgreSQL so it can be named as the function return. The `RECORD` approach +does not require pre-declaring a type, but requires every caller of the +function to supply a column definition list at the call site. + +Declaring the function [with `OUT` parameters][outprm] offers a middle ground, +where the function has a fixed composite return type with known member +names and types, callers do not need to supply a column definition list, +and no separate declaration of the type is needed. + +There is no change to how such a function is coded at the Java source level; +the new annotation element only changes the SQL generated to declare the +function to PostgreSQL. [Examples][outprmeg] are provided. + +$h4 Generation of aggregate, cast, and operator declarations + +The SQL generator now recognizes [`@Aggregate`][agganno], [`@Cast`][castanno], +and [`@Operator`][opranno] annotations, generating the corresponding SQL +deploy/undeploy scripts. Some examples (for [aggregates][aggeg], +[casts][casteg], and [operators][opreg]) are provided. The reduction +in boilerplate needed for a realistically-complete example can be seen +in [this comparison][bg160161] of Bear Giles's `pljava-udt-type-extension` +example; the two branches compared here are (1) using only the annotations +supported in PL/Java 1.6.0 and (2) using also the new support in 1.6.1. + +$h3 Bugs fixed + +* [1.6.0: opening a ResourceBundle (or a resource) fails](${ghbug}322) +* [Better workaround needed for javac 10 and 11 --release bug](${ghbug}328) + +[outprm]: pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/Function.html#annotation.type.element.detail +[outprmeg]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/ReturnComposite.html#method.detail +[agganno]: pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/Aggregate.html +[castanno]: pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/Cast.html +[opranno]: pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/Operator.html +[aggeg]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/Aggregates.html +[casteg]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/IntWithMod.html +[opreg]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/ComplexScalar.html +[bg160161]: https://github.com/beargiles/pljava-udt-type-extension/compare/98f1a6e...jcflack:3e56056 + +$h3 Credits + +Thanks to Bear Giles for the `pljava-udt-type-extension` example, which not only +illustrates the SQL generation improvements in this release, but also exposed +both of the bugs fixed here. + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + +$h2 PL/Java 1.6.0 (18 October 2020) This is the first release of a significantly refactored PL/Java 1.6 branch with a number of new features and changes. It requires Java 9 or later at @@ -197,15 +279,4 @@ continuous integration was supported by Google Summer of Code. [charsets]: use/charsets.html [jpms]: use/jpms.html -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 [Releases prior to PL/Java 1.6.0](releasenotes-pre1_6.html) From 0000d87625bad9c689564580e6e6d3034065eaa7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 19:50:27 -0500 Subject: [PATCH 0831/1087] Poke migration-management versions for 1.6.1 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index cc42ca59..9e23c30b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -668,6 +668,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_6_1 = REL_1_5_0; static final SchemaVariant REL_1_6_0 = REL_1_5_0; static final SchemaVariant REL_1_5_7 = REL_1_5_0; From c4459751377d2080bea37bac1d72eee1e99c0ad9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 16 Nov 2020 20:04:12 -0500 Subject: [PATCH 0832/1087] Add control file in preparation for next release Now that 1.6.1 is released, the next release should include an extension SQL file allowing upgrade from 1.6.1. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 300842c2..cc4fed95 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -255,6 +255,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Tue, 17 Nov 2020 21:40:09 -0500 Subject: [PATCH 0833/1087] Evolve transformXML example with adjust & indent Although XSLT 1.0 is very much like punishment when a modern version is available with Saxon, it does have the advantage of being included in Java rather than a large separate download. Now that it's usable, the example functions may as well be polished a bit (say, by making the uninteresting-but-for-testing 'how' parameters optional, and accepting an 'adjust' parameter) and made suitable for day-to-day use. One useful feature of the XSLT 1.0 transformer is the ability to indent XML: great for readability, not available in core PostgreSQL, and easy to do here. It doesn't require any particular transformation defined; the default identity transform from the no-argument TransformerFactory.newTransformer is enough. So make it possible to pass null for transformName for a plain identity transform, and add optional indent and indentWidth direct arguments to make it simple. This can also serve as an example to clarify just how one gets xalan to indent, as the details are subtle enough to have needed hashing out on Stack Overflow [1]. [1] https://stackoverflow.com/a/60610218/4062350 --- .../pljava/example/annotation/PassXML.java | 128 ++++++++++++++++-- 1 file changed, 118 insertions(+), 10 deletions(-) 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 bd22169e..54baef87 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 @@ -404,48 +404,81 @@ public static SQLXML castTextXML(@SQLType("text") SQLXML sx) */ @Function(schema="javatest", implementor="postgresql_xml", provides="prepareXMLTransform") - public static void prepareXMLTransform(String name, SQLXML source, int how, - @SQLType(defaultValue="false") boolean enableExtensionFunctions) + public static void prepareXMLTransform(String name, SQLXML source, + @SQLType(defaultValue="0") int how, + @SQLType(defaultValue="false") boolean enableExtensionFunctions, + @SQLType(defaultValue={}) ResultSet adjust) throws SQLException { TransformerFactory tf = TransformerFactory.newInstance(); String exf = "http://www.oracle.com/xml/jaxp/properties/enableExtensionFunctions"; + Source src = sxToSource(source, how, adjust); try { tf.setFeature(exf, enableExtensionFunctions); - s_tpls.put(name, tf.newTemplates(sxToSource(source, how))); + s_tpls.put(name, tf.newTemplates(src)); } catch ( TransformerException te ) { - throw new SQLException("XML transformation failed", te); + throw new SQLException( + "Preparing XML transformation: " + te.getMessage(), te); } } /** * Transform some XML according to a named transform prepared with * {@code prepareXMLTransform}. + *

    + * Pass null for {@code transformName} to get a plain identity transform + * (not such an interesting thing to do, unless you also specify indenting). */ @Function(schema="javatest", implementor="postgresql_xml", provides="transformXML") public static SQLXML transformXML( - String transformName, SQLXML source, int howin, int howout) + String transformName, SQLXML source, + @SQLType(defaultValue="0") int howin, + @SQLType(defaultValue="0") int howout, + @SQLType(defaultValue={}) ResultSet adjust, + @SQLType(defaultValue="false") boolean indent, + @SQLType(defaultValue="4") int indentWidth) throws SQLException { - Templates tpl = s_tpls.get(transformName); - Source src = sxToSource(source, howin); + Templates tpl = null == transformName? null: s_tpls.get(transformName); + Source src = sxToSource(source, howin, adjust); + + if ( indent && 0 == howout ) + howout = 4; // transformer only indents if writing a StreamResult + Connection c = DriverManager.getConnection("jdbc:default:connection"); SQLXML result = c.createSQLXML(); - Result rlt = sxToResult(result, howout); + Result rlt = sxToResult(result, howout, adjust); try { - Transformer t = tpl.newTransformer(); + Transformer t = + null == tpl ? s_tf.newTransformer() : tpl.newTransformer(); + /* + * For the non-SAX/StAX/DOM flavors of output, you're responsible + * for setting the Transformer to use the server encoding. + */ + if ( rlt instanceof StreamResult ) + t.setOutputProperty(ENCODING, + System.getProperty("org.postgresql.server.encoding")); + else if ( indent ) + logMessage("WARNING", + "indent requested, but howout specifies a non-stream " + + "Result type; no indenting will happen"); + + t.setOutputProperty("indent", indent ? "yes" : "no"); + t.setOutputProperty( + "{http://xml.apache.org/xalan}indent-amount", "" + indentWidth); + t.transform(src, rlt); } catch ( TransformerException te ) { - throw new SQLException("XML transformation failed", te); + throw new SQLException("Transforming XML: " + te.getMessage(), te); } return ensureClosed(rlt, result, howout); @@ -1041,6 +1074,17 @@ public static void unclosedSQLXML(int howmany, int how) throws SQLException } } + + /** + * Return some instance of {@code Source} for reading an {@code SQLXML} + * object, depending on the parameter {@code how}. + *

    + * Note that this method always returns a {@code Source}, even for cases + * 1 and 2 (obtaining readable streams directly from the {@code SQLXML} + * object; this method wraps them in {@code Source}), and case 3 + * ({@code getString}; this method creates a {@code StringReader} and + * returns it wrapped in a {@code Source}. + */ private static Source sxToSource(SQLXML sx, int how) throws SQLException { switch ( how ) @@ -1087,6 +1131,70 @@ private static Result sxToResult(SQLXML sx, int how) throws SQLException } } + /** + * Return some instance of {@code Source} for reading an {@code SQLXML} + * object, depending on the parameter {@code how}, applying any adjustments + * in {@code adjust}. + *

    + * Allows {@code how} to be zero, meaning to let the implementation choose + * what kind of {@code Source} to present. Otherwise identical to the other + * {@code sxToSource}. + */ + private static Source sxToSource(SQLXML sx, int how, ResultSet adjust) + throws SQLException + { + Source s; + switch ( how ) + { + case 0: s = sx.getSource(Adjusting.XML.Source.class); break; + case 1: + case 2: + case 3: + case 4: + return sxToSource(sx, how); // no adjustments on a StreamSource + case 5: s = sx.getSource(Adjusting.XML.SAXSource.class); break; + case 6: s = sx.getSource(Adjusting.XML.StAXSource.class); break; + case 7: s = sx.getSource(Adjusting.XML.DOMSource.class); break; + default: throw new SQLDataException("how should be 0-7", "22003"); + } + + if ( s instanceof Adjusting.XML.Source ) + return applyAdjustments(adjust, (Adjusting.XML.Source)s).get(); + return s; + } + + /** + * Return some instance of {@code Result} for writing an {@code SQLXML} + * object, depending on the parameter {@code how} applying any adjustments + * in {@code adjust}. + *

    + * Allows {@code how} to be zero, meaning to let the implementation choose + * what kind of {@code Result} to present. Otherwise identical to the other + * {@code sxToResult}. + */ + private static Result sxToResult(SQLXML sx, int how, ResultSet adjust) + throws SQLException + { + Result r; + switch ( how ) + { + case 1: // you might wish you could adjust a raw BinaryStream + case 2: // or CharacterStream + case 3: // or String, but you can't. Ask for a StreamResult. + case 5: // SAXResult needs no adjustment + case 6: // StAXResult needs no adjustment + case 7: // DOMResult needs no adjustment + return sxToResult(sx, how); + case 4: r = sx.setResult(Adjusting.XML.StreamResult.class); break; + case 0: r = sx.setResult(Adjusting.XML.Result.class); break; + default: throw new SQLDataException("how should be 0-7", "22003"); + } + + if ( r instanceof Adjusting.XML.Result ) + return applyAdjustments(adjust, (Adjusting.XML.Result)r).get(); + return r; + } + /** * Ensure the closing of whatever method was used to add content to * an {@code SQLXML} object. From e3d98e9b22768020a02d8887f719e8e6ade76b45 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 19 Nov 2020 21:41:30 -0500 Subject: [PATCH 0834/1087] Add example of XSLT 1.0 calling a Java method The ability in xalan to call out to Java methods can be extremely useful in the otherwise very limited XSLT 1.0 dialect. But it sorely needs an example, being fiddly enough to get right the first time that newcomers might otherwise flee in frustration. --- .../pljava/example/annotation/PassXML.java | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) 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 54baef87..34884c1e 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 @@ -214,6 +214,9 @@ @SQLAction(implementor="postgresql_xml", requires={"prepareXMLTransform", "transformXML"}, install={ + "REVOKE EXECUTE ON FUNCTION javatest.prepareXMLTransformWithJava" + + " FROM PUBLIC", + "SELECT" + " javatest.prepareXMLTransform('distinctElementNames'," + "'" + "', 5, true)", + "SELECT" + + " javatest.prepareXMLTransformWithJava('getPLJavaVersion'," + + "'" + + " " + + " " + + " " + + "', enableExtensionFunctions => true)", + "SELECT" + " CASE WHEN" + " javatest.transformXML('distinctElementNames'," + @@ -245,7 +263,17 @@ " 'abcde'"+ " THEN javatest.logmessage('INFO', 'XSLT 1.0 test succeeded')" + " ELSE javatest.logmessage('WARNING', 'XSLT 1.0 test failed')" + - " END" + " END", + + "SELECT" + + " CASE WHEN" + + " javatest.transformXML('getPLJavaVersion', '')::text" + + " OPERATOR(pg_catalog.=) extversion" + + " THEN javatest.logmessage('INFO', 'XSLT 1.0 with Java succeeded')" + + " ELSE javatest.logmessage('WARNING', 'XSLT 1.0 with Java failed')" + + " END" + + " FROM pg_catalog.pg_extension" + + " WHERE extname = 'pljava'" } ) @MappedUDT(schema="javatest", name="onexml", structure="c1 xml", @@ -409,14 +437,68 @@ public static void prepareXMLTransform(String name, SQLXML source, @SQLType(defaultValue="false") boolean enableExtensionFunctions, @SQLType(defaultValue={}) ResultSet adjust) throws SQLException + { + prepareXMLTransform( + name, source, how, enableExtensionFunctions, adjust, false); + } + + /** + * Precompile an XSL transform {@code source} and save it (for the + * current session) as {@code name}, where the transform may call Java + * methods. + *

    + * Otherwise identical to {@code prepareXMLTransform}, this version sets the + * {@code TransformerFactory}'s {@code extensionClassLoader} (to the same + * loader that loads this class), so the transform will be able to use + * xalan's Java call syntax to call any public Java methods that would be + * accessible to this class. (That can make a big difference in usefulness + * for the otherwise rather limited XSLT 1.0.) + *

    + * This example function will be installed with {@code EXECUTE} permission + * revoked from {@code PUBLIC}, as it essentially confers the ability to + * create arbitrary new Java functions, so should only be granted to roles + * you would be willing to grant {@code USAGE ON LANGUAGE java}. + *

    + * Because this function only prepares the transform, and + * {@link #transformXML transformXML} applies it, there is some division of + * labor in determining what limits apply to its behavior. The use of this + * method instead of {@code prepareXMLTransform} determines whether the + * transform is allowed to see external Java methods at all; it will be + * the policy permissions granted to {@code transformXML} that control what + * those methods can do when the transform is applied. For now, that method + * is defined in the trusted/sandboxed {@code java} language, so this + * function could reasonably be granted to any role with {@code USAGE} on + * {@code java}. If, by contrast, {@code transformXML} were declared in the + * 'untrusted' {@code javaU}, it would be prudent to allow only superusers + * access to this function, just as only they can {@code CREATE FUNCTION} in + * an untrusted language. + */ + @Function(schema="javatest", implementor="postgresql_xml", + provides="prepareXMLTransform") + public static void prepareXMLTransformWithJava(String name, SQLXML source, + @SQLType(defaultValue="0") int how, + @SQLType(defaultValue="false") boolean enableExtensionFunctions, + @SQLType(defaultValue={}) ResultSet adjust) + throws SQLException + { + prepareXMLTransform( + name, source, how, enableExtensionFunctions, adjust, true); + } + + private static void prepareXMLTransform(String name, SQLXML source, int how, + boolean enableExtensionFunctions, ResultSet adjust, boolean withJava) + throws SQLException { TransformerFactory tf = TransformerFactory.newInstance(); String exf = "http://www.oracle.com/xml/jaxp/properties/enableExtensionFunctions"; + String ecl = "jdk.xml.transform.extensionClassLoader"; Source src = sxToSource(source, how, adjust); try { tf.setFeature(exf, enableExtensionFunctions); + if ( withJava ) + tf.setAttribute(ecl, PassXML.class.getClassLoader()); s_tpls.put(name, tf.newTemplates(src)); } catch ( TransformerException te ) From 5e6955a8c594534c15fea9359bf55360fd7a12b3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 19 Nov 2020 22:05:35 -0500 Subject: [PATCH 0835/1087] Fix copy-pasto in Operator annotation checking and add a regression test. Addresses #330. --- .../pljava/annotation/processing/DDRProcessor.java | 2 +- .../pljava/example/annotation/ComplexScalar.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) 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 2a0b155a..a087ab71 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 @@ -3681,7 +3681,7 @@ else if ( ! funcName.equals(fn) && ! isSynthetic ) long explicit = Arrays.stream(operands).filter(Objects::nonNull).count(); - if ( 0 != explicit && ! isSynthetic ) + if ( 0 != explicit && isSynthetic ) { msg(Kind.ERROR, m_targetElement, m_origin, "@Operator with synthetic= must not specify " + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index d664e014..064bbea6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -98,9 +98,16 @@ public class ComplexScalar implements SQLData { /** * Return the same 'complex' passed in, logging its contents at level INFO. + *

    + * Also create an unnecessary {@code <<} operator for this, with an equally + * unnecessary explicit operand type, simply as a regression test + * of issue #330. * @param cpl any instance of this UDT * @return the same instance passed in */ + @Operator( + name = "javatest.<<", right = "javatest.complex" + ) @Function( schema="javatest", name="logcomplex", effects=IMMUTABLE, onNullInput=RETURNS_NULL) From faf98835e05e394a2c8b261fe32f0dc7ab251ca4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 20 Nov 2020 18:16:10 -0500 Subject: [PATCH 0836/1087] Function args in REVOKE aren't optional pre-PG10 It's tedious to duplicate them in an @SQLAction GRANT or REVOKE. Maybe that indicates it would be useful to add grant/revoke support in annotations someday. --- .../java/org/postgresql/pljava/example/annotation/PassXML.java | 2 ++ 1 file changed, 2 insertions(+) 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 34884c1e..89ef40e6 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 @@ -215,6 +215,8 @@ requires={"prepareXMLTransform", "transformXML"}, install={ "REVOKE EXECUTE ON FUNCTION javatest.prepareXMLTransformWithJava" + + " (pg_catalog.varchar, pg_catalog.xml, integer, boolean," + + " pg_catalog.RECORD)" + " FROM PUBLIC", "SELECT" + From 5791fe9e250d0baf6242916162a056de195f653c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 20 Nov 2020 19:29:18 -0500 Subject: [PATCH 0837/1087] Don't fail reading policy_urls as non-superuser Have to access the value directly for internal purposes, as GetConfigOption will honor the GUC_SUPERUSER_ONLY on it. Add tests as non-superuser to the Travis and AppVeyor configs. Not having any was an oversight. In passing, try adding PG 13 and Java 15 to the Travis and AppVeyor builds. I have not otherwise checked whether those are available yet in those services. This should be a way to find out. --- .travis.yml | 40 +++++++++++++++++++++++++ appveyor.yml | 53 ++++++++++++++++++++++++++++++---- pljava-so/src/main/c/Backend.c | 9 ++++-- 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index c6403a36..e362f691 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,10 @@ arch: - ppc64le dist: bionic env: + - POSTGRESQL_VERSION: 13 + JAVA_VERSION: 15 + JVM_IMPL: hotspot + MVN_VERSION: 3.5.2 - POSTGRESQL_VERSION: 12 JAVA_VERSION: 14 JVM_IMPL: hotspot @@ -153,6 +157,7 @@ script: | import static java.nio.file.Paths.get import java.sql.Connection + 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 @@ -267,6 +272,41 @@ script: | (o,p,q) -> null == o ); } + + /* + * Get another new connection and make sure the extension can be loaded + * in a non-superuser session. + */ + try ( Connection c = n1.connect() ) + { + succeeding &= stateMachine( + "become non-superuser", + null, + + q(c, + "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(c, "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 + ); + } } catch ( Throwable t ) { succeeding = false; diff --git a/appveyor.yml b/appveyor.yml index ca13103d..ce74f549 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,17 +10,23 @@ environment: JDK: 10 PG: 12 - SYS: MINGW - JDK: 14 - PG: 12 - - SYS: MINGW - JDK: 13 + JDK: 11 PG: 12 - SYS: MINGW JDK: 12 PG: 12 - SYS: MINGW - JDK: 11 + JDK: 13 PG: 12 + - SYS: MINGW + JDK: 14 + PG: 13 + - SYS: MINGW + JDK: 15 + PG: 13 + - SYS: MSVC + JDK: 15 + PG: 13 - SYS: MSVC JDK: 14 PG: 12 @@ -84,6 +90,7 @@ test_script: import static java.nio.file.Paths.get import java.sql.Connection + 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 @@ -205,6 +212,42 @@ test_script: (o,p,q) -> null == o ); } + + /* + * Get another new connection and make sure the extension can be loaded + * in a non-superuser session. + */ + try ( Connection c = n1.connect() ) + { + succeeding &= stateMachine( + "become non-superuser", + null, + + q(c, + "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(c, + "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 + ); + } } catch ( Throwable t ) { succeeding = false; diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 89d84125..4d3b5258 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -208,6 +208,7 @@ static bool seenVisualVMName; static bool seenModuleMain; static char const visualVMprefix[] = "-Dvisualvm.display.name="; static char const moduleMainPrefix[] = "-Djdk.module.main="; +static char const policyUrlsGUC[] = "pljava.policy_urls"; /* * In a background worker, _PG_init may be called very early, before much of @@ -1648,7 +1649,7 @@ static void registerGUCOptions(void) NULL); /* show hook */ STRING_GUC( - "pljava.policy_urls", + policyUrlsGUC, "URLs to Java security policy file(s) for PL/Java's use", "Quote each URL and separate with commas. Any URL may begin (inside " "the quotes) with n= where n is the index of the Java " @@ -1923,7 +1924,11 @@ JNICALL Java_org_postgresql_pljava_internal_Backend__1getConfigOption(JNIEnv* en { PG_TRY(); { - const char *value = PG_GETCONFIGOPTION(key); + const char *value; + if ( 0 == strcmp(policyUrlsGUC, key) ) + value = policy_urls; + else + value = PG_GETCONFIGOPTION(key); pfree(key); if(value != 0) result = String_createJavaStringFromNTS(value); From 165fe6f6f654805c158b59406b5d5ce73cfe74f2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 20 Nov 2020 20:08:35 -0500 Subject: [PATCH 0838/1087] Fix a lint warning --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a087ab71..a2f9fcd4 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 @@ -4442,7 +4442,7 @@ else if ( null == _plan.stateType ) .skip(1) // skip the state argument .map(pi -> (Map.Entry) - new AbstractMap.SimpleImmutableEntry( + new AbstractMap.SimpleImmutableEntry<>( Identifier.Simple.fromJava( pi.name() ), From e1b1941b4a8a4d39b7768bbc529be0d313c4e5fd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 20 Nov 2020 21:14:57 -0500 Subject: [PATCH 0839/1087] Relent on testing MSVC PG 13 in AppVeyor It appears that the time is not yet ripe. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index ce74f549..22ed7b83 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,7 @@ environment: PG: 13 - SYS: MSVC JDK: 15 - PG: 13 + PG: 12 - SYS: MSVC JDK: 14 PG: 12 From 41d9861316ade0328d0ba2d75e825a2153fe9cc7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 21 Nov 2020 10:23:37 -0500 Subject: [PATCH 0840/1087] Implement a TrialPolicy The system property org.postgresql.pljava.policy.trial, if supplied, is a URI naming another policy file that can serve as a second chance for accesses denied by the real policy laid out by pljava.policy_urls. If the denied access is allowed by this policy, it is allowed, but logged, so needed permissions can be identified while running code. If denied by this policy, the denial is final. A good bit of honest work in this policy goes to abbreviating the log output in a useful way. The approach is to keep one stack frame above and one below each crossing of a module or protection-domain boundary, with ... replacing intermediate frames within the same module/domain, and the code source/principals of the failing domain shown at the appropriate position in the trace. --- .../src/main/resources/pljava.policy | 2 + .../pljava/internal/InstallHelper.java | 22 ++ .../pljava/internal/TrialPolicy.java | 235 ++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/TrialPolicy.java diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index 5fbdd171..7924dc98 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -58,6 +58,8 @@ grant codebase "${org.postgresql.pljava.codesource}" { "charsetProvider"; permission java.lang.RuntimePermission "createClassLoader"; + permission java.lang.RuntimePermission + "getProtectionDomain"; permission java.net.NetPermission "specifyStreamHandler"; permission java.util.logging.LoggingPermission diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 9e23c30b..ce9666d8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -17,6 +17,8 @@ import java.net.URL; import java.net.MalformedURLException; import java.nio.charset.Charset; +import java.security.NoSuchAlgorithmException; +import java.security.Policy; import java.security.Security; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -161,6 +163,8 @@ public static String hello( e); } + setTrialPolicyIfSpecified(); + System.setSecurityManager( new SecurityManager()); StringBuilder sb = new StringBuilder(); @@ -255,6 +259,24 @@ private static void setPolicyURLs() } } + private static void setTrialPolicyIfSpecified() throws SQLException + { + String trialURI = System.getProperty( + "org.postgresql.pljava.policy.trial"); + + if ( null == trialURI ) + return; + + try + { + Policy.setPolicy( new TrialPolicy( trialURI)); + } + catch ( NoSuchAlgorithmException e ) + { + throw new SQLException(e.getMessage(), e); + } + } + public static void groundwork( String module_pathname, String loadpath_tbl, String loadpath_tbl_quoted, boolean asExtension, boolean exNihilo) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TrialPolicy.java b/pljava/src/main/java/org/postgresql/pljava/internal/TrialPolicy.java new file mode 100644 index 00000000..f5e7470f --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TrialPolicy.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2020 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.internal; + +import java.net.URI; + +import java.security.CodeSource; +import java.security.NoSuchAlgorithmException; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Policy; +import java.security.ProtectionDomain; +import java.security.URIParameter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static org.postgresql.pljava.elog.ELogHandler.LOG_LOG; +import static org.postgresql.pljava.internal.Privilege.doPrivileged; + +/** + * An implementation of {@link Policy} intended for temporary use while + * identifying needed permission grants for existing code. + *

    + * This policy is meant to operate as a fallback in conjunction with the normal + * PL/Java policy specified with the {@code pljava.policy_urls} configuration + * setting. This policy is activated by specifying an additional policy file + * URL with {@code -Dorg.postgresql.pljava.policy.trial=}url in the + * {@code pljava.vmoptions} setting. + *

    + * Permission checks that are allowed by the normal policy in + * {@code pljava.policy_urls} are allowed with no further checking. Permissions + * denied by that policy are checked in this one. If denied in this policy, that + * is the end of the matter. A permission check that is denied by the normal + * policy but allowed by this one is allowed, with a message to the server log. + *

    + * The log message begins with {@code POLICY DENIES/TRIAL POLICY ALLOWS:} + * and the requested permission, followed by an abbreviated stack trace. + * To minimize log volume, the stack trace includes a frame above and below + * each crossing of a module or protection domain boundary; a single {@code ...} + * replaces intermediate frames within the same module and domain. + * At the position in the trace of the protection domain that failed the policy + * check, a line is inserted with the domain's code source and principals, + * such as {@code >> sqlj:examples [PLPrincipal.Sandboxed: java] <<}. This + * abbreviated trace should be well suited to the purpose of determining where + * any additional permission grants ought to be made. + *

    + * Because each check that is logged is then allowed, it can be possible to see + * multiple log entries for the same permission check, one for each domain in + * the call stack that is not granted the permission in the normal policy. + *

    About false positives

    + * It is not uncommon to have software that checks in normal operation for + * certain permissions, catches exceptions, and proceeds to function normally. + * Use of this policy, if it is configured to grant the permissions being + * checked, will produce log entries for those 'hidden' checks and may create + * the appearance that permissions need to be granted when, in fact, the + * software would show no functional impairment without them. It is difficult + * to distinguish such false positives from other log entries for permissions + * that do need to be granted for the software to properly function. + *

    + * One approach would be to try to determine, from the log entries, which + * functions of the software led to the permission checks that were logged, and + * specifically test those functions in a database session that has been set up + * with a different policy file that does not grant those permissions. If the + * software then functions without incident, it may be concluded that those + * log entries were false positives. + */ +public class TrialPolicy extends Policy +{ + private static final String TYPE = "JavaPolicy"; + private static final RuntimePermission GET_PROTECTION_DOMAIN = + new RuntimePermission("getProtectionDomain"); + private final Policy realPolicy; + private final Policy limitPolicy; + private final StackWalker walker = + StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + + TrialPolicy(String limitURI) throws NoSuchAlgorithmException + { + URIParameter lim = new URIParameter(URI.create(limitURI)); + realPolicy = Policy.getInstance(TYPE, null); + limitPolicy = Policy.getInstance(TYPE, lim); + } + + @Override + public PermissionCollection getPermissions(CodeSource codesource) + { + return realPolicy.getPermissions(codesource); + } + + @Override + public PermissionCollection getPermissions(ProtectionDomain domain) + { + return realPolicy.getPermissions(domain); + } + + @Override + public boolean implies(ProtectionDomain domain, Permission permission) + { + if ( realPolicy.implies(domain, permission) ) + return true; + + if ( ! limitPolicy.implies(domain, permission) ) + return false; + + /* + * Construct a (with any luck, useful) abbreviated stack trace, using + * the first frame encountered at each change of protection domain while + * walking up the stack, saving the index of the first entry for the + * domain being checked. + */ + List stack = new ArrayList<>(); + int matchingDomainIndex = doPrivileged(() -> walker.walk(s -> + { + ProtectionDomain lastDomain = null; + StackWalker.StackFrame lastFrame = null; + Module lastModule = null; + Module thisModule = getClass().getModule(); + int matchIndex = -1; + int walkIndex = 0; + int newDomainIndex = 0; // walkIndex of first frame in a new domain + for ( StackWalker.StackFrame f : + (Iterable)s.skip(5)::iterator ) + { + ++ walkIndex; + Class frameClass = f.getDeclaringClass(); + Module frameModule = frameClass.getModule(); + ProtectionDomain frameDomain = frameClass.getProtectionDomain(); + if ( ! equals(lastDomain, frameDomain) + || null != lastModule && ! lastModule.equals(frameModule) ) + { + if ( null != lastFrame && walkIndex > 1 + newDomainIndex ) + { + if ( walkIndex > 2 + newDomainIndex ) + stack.add(null); // will be rendered as ... + stack.add(lastFrame.toStackTraceElement()); + } + if ( -1 == matchIndex && equals(domain, frameDomain) ) + matchIndex = stack.size(); + stack.add(f.toStackTraceElement()); + lastModule = frameModule; + lastDomain = frameDomain; + newDomainIndex = walkIndex; + } + + /* + * Exit the walk early, skip boring EntryPoints. + */ + if ( frameModule.equals(thisModule) + && "org.postgresql.pljava.internal.EntryPoints" + .equals(frameClass.getName()) ) + { + if ( newDomainIndex == walkIndex ) + stack.remove(stack.size() - 1); + -- walkIndex; + break; + } + + lastFrame = f; + } + + if ( null != lastFrame && walkIndex > 1 + newDomainIndex ) + stack.add(lastFrame.toStackTraceElement()); + + if ( -1 == matchIndex ) + matchIndex = stack.size(); + return matchIndex; + }), null, GET_PROTECTION_DOMAIN); + + /* + * Construct a string representation of the trace. + */ + StringBuilder sb = new StringBuilder(); + Iterator it = stack.iterator(); + int i = 0; + for ( ;; ) + { + if ( matchingDomainIndex == i ++ ) + sb.append(">> ") + .append(domain.getCodeSource().getLocation()) + .append(' ') + .append(Arrays.toString(domain.getPrincipals())) + .append(" <<\n"); + if ( ! it.hasNext() ) + break; + StackTraceElement e = it.next(); + sb.append(null == e ? "..." : e.toString()); + if ( it.hasNext() || matchingDomainIndex == i ) + sb.append('\n'); + } + + Backend.log(LOG_LOG, + "POLICY DENIES/TRIAL POLICY ALLOWS: " + permission + '\n' + sb); + + return true; + } + + @Override + public void refresh() + { + realPolicy.refresh(); + limitPolicy.refresh(); + } + + /* + * Compare two protection domains, only by their code source for now. + * It appears that StackWalker doesn't invoke domain combiners, so the + * frames seen in the walk won't match the principals of the argument + * to implies(). + */ + private boolean equals(ProtectionDomain a, ProtectionDomain b) + { + if ( null == a || null == b) + return a == b; + + CodeSource csa = a.getCodeSource(); + CodeSource csb = b.getCodeSource(); + + if ( null == csa || null == csb ) + return csa == csb; + + return csa.equals(csb); + } +} From 206a963ab056bac3506e3dc0257e1eb9f85094bd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 22 Nov 2020 16:48:02 -0500 Subject: [PATCH 0841/1087] Add TrialPolicy$Permission Java has an AllPermission, which can be granted in a TrialPolicy in order to identify, log, and allow any and all requested permissions that are not granted in the normal policy. Some may not be comfortable proceeding that way, and because Java does not have negative permissions, there is no way to say "grant AllPermission except for these that I would like to exclude". Therefore, org.postgresql.pljava.policy.TrialPolicy$Permission is provided here, as pretty much exactly "AllPermission except for a couple dozen unsurprising exclusions." This permission can be granted in a TrialPolicy (and, in the unlikely event that any of the permissions it excludes also have to be granted there, they can be granted explicitly, in the usual way). A package in a named module that implements any new Permission must be exported to java.base; this is moved to the new package org.postgresql.pljava.policy and so exported. --- pljava/src/main/java/module-info.java | 2 + .../pljava/internal/InstallHelper.java | 1 + .../{internal => policy}/TrialPolicy.java | 200 +++++++++++++++++- .../pljava/policy/package-info.java | 22 ++ 4 files changed, 221 insertions(+), 4 deletions(-) rename pljava/src/main/java/org/postgresql/pljava/{internal => policy}/TrialPolicy.java (55%) create mode 100644 pljava/src/main/java/org/postgresql/pljava/policy/package-info.java diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 610f5861..68923bbe 100644 --- a/pljava/src/main/java/module-info.java +++ b/pljava/src/main/java/module-info.java @@ -23,6 +23,8 @@ exports org.postgresql.pljava.elog to java.logging; + exports org.postgresql.pljava.policy to java.base; // has custom Permission + provides java.net.spi.URLStreamHandlerProvider with org.postgresql.pljava.sqlj.Handler; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index ce9666d8..42ed012b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -38,6 +38,7 @@ import org.postgresql.pljava.jdbc.SQLUtils; import org.postgresql.pljava.management.SQLDeploymentDescriptor; +import org.postgresql.pljava.policy.TrialPolicy; import static org.postgresql.pljava.annotation.processing.DDRWriter.eQuote; import static org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TrialPolicy.java b/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java similarity index 55% rename from pljava/src/main/java/org/postgresql/pljava/internal/TrialPolicy.java rename to pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java index f5e7470f..4f81ef50 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TrialPolicy.java +++ b/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java @@ -9,7 +9,9 @@ * Contributors: * Chapman Flack */ -package org.postgresql.pljava.internal; +package org.postgresql.pljava.policy; + +import java.lang.reflect.ReflectPermission; import java.net.URI; @@ -19,14 +21,19 @@ import java.security.PermissionCollection; import java.security.Policy; import java.security.ProtectionDomain; +import java.security.SecurityPermission; import java.security.URIParameter; import java.util.ArrayList; import java.util.Arrays; +import static java.util.Collections.emptyEnumeration; +import static java.util.Collections.enumeration; +import java.util.Enumeration; import java.util.Iterator; import java.util.List; import static org.postgresql.pljava.elog.ELogHandler.LOG_LOG; +import static org.postgresql.pljava.internal.Backend.log; import static org.postgresql.pljava.internal.Privilege.doPrivileged; /** @@ -86,7 +93,7 @@ public class TrialPolicy extends Policy private final StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); - TrialPolicy(String limitURI) throws NoSuchAlgorithmException + public TrialPolicy(String limitURI) throws NoSuchAlgorithmException { URIParameter lim = new URIParameter(URI.create(limitURI)); realPolicy = Policy.getInstance(TYPE, null); @@ -106,13 +113,28 @@ public PermissionCollection getPermissions(ProtectionDomain domain) } @Override - public boolean implies(ProtectionDomain domain, Permission permission) + public boolean implies( + ProtectionDomain domain, java.security.Permission permission) { if ( realPolicy.implies(domain, permission) ) return true; if ( ! limitPolicy.implies(domain, permission) ) + { + /* + * The TrialPolicy.Permission below is an unusual one: like Java's + * own AllPermission, its implies() can be true for permissions of + * other classes than its own. Java's AllPermission is handled + * magically, and this one must be also, because deep down, the + * built-in Policy implementation keeps its PermissionCollections + * segregated by permission class. It would not notice on its own + * that 'permission' might be implied by a permission that is held + * but is of some other class. + */ + if ( ! limitPolicy.implies(domain, Permission.INSTANCE) + || ! Permission.INSTANCE.implies(permission) ) return false; + } /* * Construct a (with any luck, useful) abbreviated stack trace, using @@ -200,7 +222,7 @@ public boolean implies(ProtectionDomain domain, Permission permission) sb.append('\n'); } - Backend.log(LOG_LOG, + log(LOG_LOG, "POLICY DENIES/TRIAL POLICY ALLOWS: " + permission + '\n' + sb); return true; @@ -232,4 +254,174 @@ private boolean equals(ProtectionDomain a, ProtectionDomain b) return csa.equals(csb); } + + /** + * A permission like {@code java.security.AllPermission}, but without + * any {@code FilePermission} (the real policy's sandboxed/unsandboxed + * grants should handle those), nor a couple dozen varieties of + * {@code RuntimePermission}, {@code SecurityPermission}, and + * {@code ReflectPermission} that would typically not be granted without + * clear intent. + *

    + * This permission can be granted in a {@code TrialPolicy} while identifying + * any straggling permissions needed by some existing code, without quite + * the excitement of granting {@code AllPermission}. Any of the permissions + * excluded from this one can also be granted in the {@code TrialPolicy}, + * of course, if there is reason to believe the code might need them. + *

    + * The proper spelling in a policy file is + * {@code org.postgresql.pljava.policy.TrialPolicy$Permission}. + *

    + * This permission will probably only work right in a {@code TrialPolicy}. + * Any permission whose {@code implies} method can return true for + * permissions of other classes than its own may be ineffective in a stock + * Java policy, where permission collections are kept segregated by the + * class of the permission to be checked. Java's {@code AllPermission} gets + * special-case treatment in the stock implementation, and this permission + * likewise has to be treated specially in {@code TrialPolicy}. The only + * kind of custom permission that can genuinely drop in and work is one + * whose {@code implies} method only imposes semantics on the names/actions + * of different instances of that permission class. + *

    + * A permission that does not live on the boot classpath is initially read + * from a policy file as an instance of {@code UnresolvedPermission}, and + * only gets resolved when a permission check is made, checking for an + * instance of its actual class. That is another complication when + * implementing a permission that may imply permissions of other classes. + *

    + * A permission implemented in a different named module must be in a package + * that is exported to {@code java.base}. + */ + public static final class Permission extends java.security.Permission + { + private static final long serialVersionUID = 6401893677037633706L; + + /** + * An instance of this permission (not a singleton, merely one among + * possible others). + */ + static final Permission INSTANCE = new Permission(); + + public Permission() + { + super(""); + } + + public Permission(String name, String actions) + { + super(""); + } + + @Override + public boolean equals(Object other) + { + return other instanceof Permission; + } + + @Override + public int hashCode() + { + return 131113; + } + + @Override + public String getActions() + { + return null; + } + + @Override + public PermissionCollection newPermissionCollection() + { + return new Collection(); + } + + @Override + public boolean implies(java.security.Permission p) + { + if ( p instanceof Permission ) + return true; + + if ( p instanceof java.io.FilePermission ) + return false; + + if ( Holder.EXCLUDERHS.stream().anyMatch(r -> p.implies(r)) ) + return false; + + if ( Holder.EXCLUDELHS.stream().anyMatch(l -> l.implies(p)) ) + return false; + + return true; + } + + static class Collection extends PermissionCollection + { + private static final long serialVersionUID = 917249873714843122L; + + Permission the_permission = null; + + @Override + public void add(java.security.Permission p) + { + if ( isReadOnly() ) + throw new SecurityException( + "attempt to add a Permission to a readonly " + + "PermissionCollection"); + + if ( ! (p instanceof Permission) ) + throw new IllegalArgumentException( + "invalid in homogeneous PermissionCollection: " + p); + + if ( null == the_permission ) + the_permission = (Permission) p; + } + + @Override + public boolean implies(java.security.Permission p) + { + if ( null == the_permission ) + return false; + return the_permission.implies(p); + } + + @Override + public Enumeration elements() + { + if ( null == the_permission ) + return emptyEnumeration(); + return enumeration(List.of(the_permission)); + } + } + + static class Holder + { + static final List EXCLUDERHS = List.of( + new RuntimePermission("createClassLoader"), + new RuntimePermission("getClassLoader"), + new RuntimePermission("setContextClassLoader"), + new RuntimePermission("enableContextClassLoaderOverride"), + new RuntimePermission("setSecurityManager"), + new RuntimePermission("createSecurityManager"), + new RuntimePermission("shutdownHooks"), + new RuntimePermission("exitVM"), + new RuntimePermission("setFactory"), + new RuntimePermission("setIO"), + new RuntimePermission("getStackWalkerWithClassReference"), + new RuntimePermission("setDefaultUncaughtExceptionHandler"), + new RuntimePermission("manageProcess"), + new ReflectPermission("suppressAccessChecks"), + new SecurityPermission("createAccessControlContext"), + new SecurityPermission("createAccessControlContext"), + new SecurityPermission("setPolicy"), + new SecurityPermission("createPolicy.JavaPolicy") + ); + + static final List EXCLUDELHS = List.of( + new RuntimePermission("exitVM.*"), + new RuntimePermission("defineClassInPackage.*"), + new ReflectPermission("newProxyInPackage.*"), + new SecurityPermission("setProperty.*") + ); + } + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/policy/package-info.java b/pljava/src/main/java/org/postgresql/pljava/policy/package-info.java new file mode 100644 index 00000000..dc411b58 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/policy/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 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: + * Tada AB + * Purdue University + */ +/** + * Package implementing custom Java security policy useful while migrating + * existing code to policy-based PL/Java; allows permission checks denied by the + * main policy to succeed, while logging them so any needed permission grants + * can be identified and added to the main policy. + *

    + * This package is exported to {@code java.base} to provide a custom + * {@code Permission} that can be granted in policy. + */ +package org.postgresql.pljava.policy; From b03e86f07bbf4e48690590b329671b2a451d91db Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 22 Nov 2020 17:08:31 -0500 Subject: [PATCH 0842/1087] Old typo, benign but spams the TrialPolicy log The provider() method on a service provider has to be static. Misdeclaring this one never broke anything, as the service loader simply fell back on the constructor, but later service loader passes after the policy gets installed would cause false-alarm log entries from the constructor. --- pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java index a0bd626b..268c6be0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Handler.java @@ -29,7 +29,7 @@ public class Handler extends URLStreamHandlerProvider { private static final Handler INSTANCE = new Handler(); - public URLStreamHandlerProvider provider() + public static URLStreamHandlerProvider provider() { return INSTANCE; } From d2d03a320f149fcf3bd61073be2d94e821bda4a4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 22 Nov 2020 23:15:22 -0500 Subject: [PATCH 0843/1087] Avoid blocking to log from a non-PG thread There isn't a perfect method available (yet) to inquire "could I doInPG() or log() at this moment without blocking?", so this check may unnecessarily send the message off to System.err on occasions when log() would have worked fine. But it's better than blocking. In passing, add a permission to read another property that the Java runtime started using in Java 14 but forgot to give itself permission to read. It's at the false-alarm level of severity, as the runtime behaves gracefully when unable to read the property, but again, if using TrialPolicy, it kind of spams the log. --- .../src/main/resources/pljava.policy | 5 +++++ .../postgresql/pljava/policy/TrialPolicy.java | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index 7924dc98..c360dcdb 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -42,6 +42,11 @@ grant { // permission java.util.PropertyPermission "jdk.lang.ref.disableClearBeforeEnqueue", "read"; + + // Something similar happened in Java 14 (not yet fixed in 15). + // + permission java.util.PropertyPermission + "java.util.concurrent.ForkJoinPool.common.maximumSpares", "read"; }; diff --git a/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java b/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java index 4f81ef50..baf8a171 100644 --- a/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java +++ b/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java @@ -34,6 +34,7 @@ import static org.postgresql.pljava.elog.ELogHandler.LOG_LOG; import static org.postgresql.pljava.internal.Backend.log; +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; import static org.postgresql.pljava.internal.Privilege.doPrivileged; /** @@ -203,7 +204,8 @@ public boolean implies( /* * Construct a string representation of the trace. */ - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder( + "POLICY DENIES/TRIAL POLICY ALLOWS: " + permission + '\n'); Iterator it = stack.iterator(); int i = 0; for ( ;; ) @@ -222,8 +224,19 @@ public boolean implies( sb.append('\n'); } - log(LOG_LOG, - "POLICY DENIES/TRIAL POLICY ALLOWS: " + permission + '\n' + sb); + /* + * This is not the best way to avoid blocking on log(); in some flavors + * of pljava.java_thread_pg_entry, threadMayEnterPG can return false + * simply because it's not /known/ that PG could be entered right now, + * and this could send the message off to System.err at times even if + * log() would have completed with no blocking. But the always accurate + * "could I enter PG right now without blocking?" method isn't provided + * yet. + */ + if ( threadMayEnterPG() ) + log(LOG_LOG, sb.toString()); + else + System.err.println(sb); return true; } From 3b25cf45b292bd5e8ba2cf2e7794d94497899cbf Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 23 Nov 2020 00:53:01 -0500 Subject: [PATCH 0844/1087] A duplicate line snuck in somehow --- .../src/main/java/org/postgresql/pljava/policy/TrialPolicy.java | 1 - 1 file changed, 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java b/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java index baf8a171..320a66c4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java +++ b/pljava/src/main/java/org/postgresql/pljava/policy/TrialPolicy.java @@ -424,7 +424,6 @@ static class Holder new RuntimePermission("manageProcess"), new ReflectPermission("suppressAccessChecks"), new SecurityPermission("createAccessControlContext"), - new SecurityPermission("createAccessControlContext"), new SecurityPermission("setPolicy"), new SecurityPermission("createPolicy.JavaPolicy") ); From 1d51fcfa15cf82c7cdacb6d41d318d10953077b4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 23 Nov 2020 19:34:02 -0500 Subject: [PATCH 0845/1087] Relent also on MinGW PG 13 in AppVeyor Such a build appears to work, and is listed as PG 13, but on closer inspection turns out to have built with 12 anyway, which apparently is what's in pacman. --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 22ed7b83..e99cb61f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,10 +20,10 @@ environment: PG: 12 - SYS: MINGW JDK: 14 - PG: 13 + PG: 12 - SYS: MINGW JDK: 15 - PG: 13 + PG: 12 - SYS: MSVC JDK: 15 PG: 12 From 6092da7f33cfd04a6d37faa7969603ce1460ed6b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 23 Nov 2020 21:00:15 -0500 Subject: [PATCH 0846/1087] Add Travis and AppVeyor tests of TrialPolicy Factoring out some of the common elements of the Travis and AppVeyor (and, one day, GitHub Actions) scripts can be a project for another day. --- .travis.yml | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ appveyor.yml | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/.travis.yml b/.travis.yml index e362f691..68099e57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -155,8 +155,12 @@ script: | 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 @@ -256,6 +260,70 @@ script: | (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. */ diff --git a/appveyor.yml b/appveyor.yml index e99cb61f..849223b7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -88,8 +88,12 @@ test_script: @' 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 @@ -196,6 +200,70 @@ test_script: (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. */ From bb41b3373bea389393077787312ec8e3ab6fb148 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 24 Nov 2020 20:17:10 -0500 Subject: [PATCH 0847/1087] Document TrialPolicy --- src/site/markdown/use/policy.md | 21 +++- src/site/markdown/use/trial.md | 186 ++++++++++++++++++++++++++++++++ src/site/markdown/use/use.md | 9 ++ 3 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/site/markdown/use/trial.md diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index 02cdf8b0..8ce12948 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -318,8 +318,21 @@ such as `java.version` or `org.postgresql.pljava.version`._ ## Troubleshooting -When in doubt what permissions are needed to get some existing PL/Java code -working again, it may be helpful to add `-Djava.security.debug=access` in +When in doubt what permissions may need to be granted in `pljava.policy` to run +some existing PL/Java code, these techniques may be helpful. + +### Running PL/Java with a 'trial' policy + +To simplify the job of finding the permissions needed by some existing code, +it is possible to run PL/Java at first with a 'trial' policy, allowing code to +run while logging permissions that `pljava.policy` has not granted. The log +entries have a condensed format meant to be convenient for this use. +Trial policy configuration is described [here][trial]. + +### Using policy debug features provided by Java + +Java itself offers a number of debugging switches to reveal details of +permission decisions. It may be useful to add `-Djava.security.debug=access` in the setting of `pljava.vmoptions`, and observe the messages on the PostgreSQL backend's standard error (which should be included in the log file, if `logging_collector` is `on`). It is not necessary to change the @@ -330,6 +343,9 @@ Other options for `java.security.debug` can be found in [Troubleshooting Security][tssec]. Some can be used to filter the logging down to requests for specific permissions or from a specific codebase. +The log output produced by Java's debug options can be voluminous compared to +the condensed output of PL/Java's trial policy. + ## Forward compatibility The current implementation makes use of the Java classes @@ -344,3 +360,4 @@ release, so relying on it is not recommended. [dopriv]: https://docs.oracle.com/en/java/javase/14/security/java-se-platform-security-architecture.html#GUID-E8898CB5-65BB-4D1A-A574-8F7112FC353F [sqljajl]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#alias_java_language [tssec]: https://docs.oracle.com/en/java/javase/14/security/troubleshooting-security.html +[trial]: trial.html diff --git a/src/site/markdown/use/trial.md b/src/site/markdown/use/trial.md new file mode 100644 index 00000000..79d4bffd --- /dev/null +++ b/src/site/markdown/use/trial.md @@ -0,0 +1,186 @@ +# Migrating to policy-based permissions from an earlier PL/Java release + +When migrating existing code from a PL/Java 1.5 or earlier release to 1.6, +it may be necessary to add permission grants in the new `pljava.policy` file, +which grants few permissions by default. PL/Java's security policy configuration +is described [here][policy]. + +To simplify migration, it is possible to run with a 'trial' policy initially, +allowing code to run but logging permissions that may need to be added in +`pljava.policy`. + +## Configuring a trial policy + +Even when running with a trial policy, the [configuration variable][vbls] +`pljava.policy_urls` should point to the normal policy file(s), as usual. +That is where the ultimate policy for production will be developed. + +The trial policy is configured by creating another policy file somewhere, using +the same policy file syntax, and pointing to it with +`-Dorg.postgresql.pljava.policy.trial=`_url_ added to the configuration variable +`pljava.vmoptions`. + +Anything _this_ policy allows will be allowed, but will be logged if the regular +policy would have denied it. So you can make this one more generous than the +regular policy, and use the log entries to identify grants that might belong in +the regular policy. As you add the missing ones to the real policy, they stop +getting logged by this one, and the log gets quieter. You can make this one as +generous as you are comfortable making it during the period of testing and +tuning. + +At the very extreme of generosity it could be this: + +``` +grant { + permission java.security.AllPermission; +}; +``` + +and it would happily allow the code under test to do _anything at all_, while +logging whatever permissions aren't in the regular policy. (A side effect of +this would be to erase any distinction between `java` and `javaU` for as long as +the trial policy is in place.) Such a setting would be difficult to recommend in +general, but it might suffice if the only code being tested has already been in +use for years under PL/Java 1.5 and is well trusted, users of the database have +not been granted permission to install more PL/Java functions, and if +the purpose of testing is only to learn what permissions the code uses that +may need to be granted in the 1.6 policy. + +### Granting `TrialPolicy$Permission` + +When `AllPermission` is too broad, there is the difficulty that Java's +permission model does not have a subtractive mode; it is not simple to say +"grant `AllPermission` except for this list of the ones I'd really rather not." +Therefore, PL/Java offers a custom "meta-permission" with roughly that meaning: + +``` +grant { + permission org.postgresql.pljava.policy.TrialPolicy$Permission; +}; +``` + +`TrialPolicy$Permission` is effectively `AllPermission` but excluding any +`FilePermission` (so that `java`/`javaU` distinction stays meaningful) as well +as a couple dozen other various +`SecurityPermission`/`ReflectPermission`/`RuntimePermission` instances in the +"really rather not" category. If its hard-coded exclusion list excludes +any permissions that some unusual code under test might legitimately need, +those can be explicitly added to the trial policy too. + +Configuring a trial policy can be a bit of a balancing act: if it is very +generous, that minimizes the chance of breaking the code under test because of +a denied permission, but increases potential exposure if that code misbehaves. +A more limited trial policy decreases exposure but increase the risk of +service interruption if the code under test really does need some permission +that you weren't comfortable putting in the trial policy. Somewhere near +the sweet spot is where `TrialPolicy$Permission` is aimed. + +All other normal policy features also work in the trial policy. If your +code is installed in several different jars, you can use `grant codebase` +separately to put different outer limits around different jars, and completely +remove the grants for one jar after another as you are satisfied you have added +the right things for each one in the regular policy. You could also set +different limits for `java` and `javaU` by granting to the `PLPrincipal`, +just as you can in the regular policy. + +## About false positives + +One thing to be aware of is that the trial policy can give false alarms. It is +not uncommon for software to include configuration-dependent bits that +tentatively try certain actions, catch exceptions, and then proceed normally, +having discovered what the configuration allows. The trial policy can log +permission denials that happen in the course of such checks, even if the denial +has no functional impact on the code. + +There may be no perfect way to tell which denials being logged by the trial +policy are false alarms. One approach would be to collect a sampling of log +entries, figure out what user-visible functions of the code they were coming +from, and then start a dedicated session without the +`-Dorg.postgresql.pljava.policy.trial` setting (or with it pointing to a +different, more restrictive version of the policy, not granting the permissions +you're curious about), then exercise those functions of the code and see if +anything breaks. Other users could still have the more generous trial setting in +their sessions, so as not to be affected by your experiments. + +False positives, of course, are also affected by the choice of how generous to +make the trial policy. Log entries are only produced for permissions that the +regular policy denies but the trial policy allows. If the permissions being +silently checked by benign code are not granted in the trial policy, they will +be silently denied, just as they would in normal operation, and produce no +log entries. + +## Format of the log entries + +To avoid bloating logs too much, `TrialPolicy` emits an abbreviated form of +stack trace for each entry. The approach is to keep one stack frame above and +one below each crossing of a module or protection-domain boundary, with `...` +replacing intermediate frames within the same module/domain, and the code +source/principals of the denied domain shown wrapped in `>> <<`at +the appropriate position in the trace. For the purpose of identifying the +source of a permission request and the appropriate domain(s) to be granted +the permission, this is probably more usable than the very long full traces +available with `java.security.debug`. + +The messages are sent through the PostgreSQL log if the thread making the +permission check knows it can do so without blocking; otherwise they just go to +standard error, which should wind up in the PostgreSQL log anyway, if +`logging_collector` is on; otherwise it may be system-dependent where they go. + +There isn't really a reliable "can I do so without blocking?" check for every +setting of the `pljava.java_thread_pg_entry` configuration variable. +If it is set to `throw` (and that is a workable setting for the code under +test), the logging behavior will be more predictable; entries from the main +thread will go through PostgreSQL's log facility always, and those from any +other thread will go to standard error. + +Here is an example of two log entries, generated by the same permission check: + +``` +POLICY DENIES/TRIAL POLICY ALLOWS: ("java.net.SocketPermission" "127.0.0.1:5432" "connect,resolve") +java.base/java.security.ProtectionDomain.implies(ProtectionDomain.java:321) +... +java.base/java.net.Socket.(Socket.java:294) +>> null [PLPrincipal.Sandboxed: java] << +jdk.translet/die.verwandlung.GregorSamsa.template$dot$0() +... +jdk.translet/die.verwandlung.GregorSamsa.transform() +java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.transform(AbstractTranslet.java:624) +... +java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:383) +org.postgresql.pljava.example.annotation.PassXML.transformXML(PassXML.java:561) + +POLICY DENIES/TRIAL POLICY ALLOWS: ("java.net.SocketPermission" "127.0.0.1:5432" "connect,resolve") +java.base/java.security.ProtectionDomain.implies(ProtectionDomain.java:321) +... +java.base/java.net.Socket.(Socket.java:294) +jdk.translet/die.verwandlung.GregorSamsa.template$dot$0() +... +jdk.translet/die.verwandlung.GregorSamsa.transform() +java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.transform(AbstractTranslet.java:624) +... +java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:383) +>> sqlj:examples [PLPrincipal.Sandboxed: java] << +org.postgresql.pljava.example.annotation.PassXML.transformXML(PassXML.java:561) +``` + +The example shows the use of an XSLT 1.0 transform that appears to +make use of the Java XSLT ability to call out to arbitrary Java, and is trying +to make a network connection back to PostgreSQL on `localhost`. Java's XSLTC +implementation compiles the transform to a class in `jdk.translet` with null +as its codebase, and the first log entry shows permission is denied at that +level (the protection domain shown as +`>> null [PLPrincipal.Sandboxed: java] <<`). + +A second log entry results because `TrialPolicy` turns the first failure to +success, allowing the permission check to continue, and it next fails at +the PL/Java function being called, in the `sqlj:examples` jar. Under the trial +policy, that also is logged and then allowed to succeed. + +The simplest way to allow this connection in the production policy would be +to grant the needed `java.net.SocketPermission` to `PLPrincipal$Sandboxed`, +as that is present in both denied domains. It would be possible to grant +the permission by codebase to `sqlj:examples` instead, but not to +the nameless codebase of the compiled XSLT transform. + +[policy]: policy.html +[vbls]: variables.html diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index 5ed1cec5..27b16e47 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -42,6 +42,15 @@ The permissions in effect for PL/Java functions can be tailored, independently for functions declared to the `TRUSTED` or untrusted language, as described [here](policy.html). +#### Tailoring permissions for code migrated from PL/Java pre-1.6 + +When migrating existing code from a PL/Java 1.5 or earlier release to 1.6, +it may be necessary to add permission grants in the new `pljava.policy` file, +which grants few permissions by default. To simplify migration, it is possible +to run with a 'trial' policy initially, allowing code to run but logging +permissions that may need to be added in `pljava.policy`. How to do that is +described [here](trial.html). + ### Choices when mapping data types #### Date and time types From 2c8d99282da80c3372b77c7ea162480eb36ed8f8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 20 Nov 2020 20:34:58 -0500 Subject: [PATCH 0848/1087] Add FALLTHROUGH comments in C switch statements CI logs indicate some PG 13 builds, anyway, are giving -Wimplicit-fallthrough=3 to gcc. The gcc docs say with =3, a /*FALLTHROUGH*/ comment should be recognized. Warnings are also being generated for a switch in Type.c where they should not be: elog(ERROR is already marked with pg_unreachable in elog.h. I wonder whether adding an explicit pg_unreachable will work any better. --- pljava-so/src/main/c/Backend.c | 13 +++++++++++++ pljava-so/src/main/c/type/Type.c | 3 +++ 2 files changed, 16 insertions(+) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 4d3b5258..073709be 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -518,6 +518,7 @@ static void initsequencer(enum initstage is, bool tolerant) initstage = IS_GUCS_REGISTERED; if ( deferInit ) return; + /*FALLTHROUGH*/ case IS_GUCS_REGISTERED: if ( NULL == libjvmlocation ) @@ -530,6 +531,7 @@ static void initsequencer(enum initstage is, bool tolerant) goto check_tolerant; } initstage = IS_CAND_JVMLOCATION; + /*FALLTHROUGH*/ case IS_CAND_JVMLOCATION: if ( NULL == policy_urls ) @@ -542,6 +544,7 @@ static void initsequencer(enum initstage is, bool tolerant) goto check_tolerant; } initstage = IS_CAND_POLICYURLS; + /*FALLTHROUGH*/ case IS_CAND_POLICYURLS: if ( ! pljavaEnabled ) @@ -556,6 +559,7 @@ static void initsequencer(enum initstage is, bool tolerant) goto check_tolerant; } initstage = IS_PLJAVA_ENABLED; + /*FALLTHROUGH*/ case IS_PLJAVA_ENABLED: libjvm_handle = pg_dlopen(libjvmlocation); @@ -569,6 +573,7 @@ static void initsequencer(enum initstage is, bool tolerant) goto check_tolerant; } initstage = IS_CAND_JVMOPENED; + /*FALLTHROUGH*/ case IS_CAND_JVMOPENED: pljava_createvm = @@ -592,6 +597,7 @@ static void initsequencer(enum initstage is, bool tolerant) goto check_tolerant; } initstage = IS_CREATEVM_SYM_FOUND; + /*FALLTHROUGH*/ case IS_CREATEVM_SYM_FOUND: s_javaLogLevel = INFO; @@ -605,6 +611,7 @@ static void initsequencer(enum initstage is, bool tolerant) pljavaDebug = 1; #endif initstage = IS_MISC_ONCE_DONE; + /*FALLTHROUGH*/ case IS_MISC_ONCE_DONE: JVMOptList_init(&optList); /* uses CurrentMemoryContext */ @@ -625,6 +632,7 @@ static void initsequencer(enum initstage is, bool tolerant) JVMOptList_add(&optList, effectiveModulePath, 0, true); } initstage = IS_JAVAVM_OPTLIST; + /*FALLTHROUGH*/ case IS_JAVAVM_OPTLIST: JNIresult = initializeJavaVM(&optList); /* frees the optList */ @@ -648,6 +656,7 @@ static void initsequencer(enum initstage is, bool tolerant) jvmStartedAtLeastOnce = true; elog(DEBUG2, "successfully created Java virtual machine"); initstage = IS_JAVAVM_STARTED; + /*FALLTHROUGH*/ case IS_JAVAVM_STARTED: #ifdef USE_PLJAVA_SIGHANDLERS @@ -659,6 +668,7 @@ static void initsequencer(enum initstage is, bool tolerant) */ on_proc_exit(_destroyJavaVM, 0); initstage = IS_SIGHANDLERS; + /*FALLTHROUGH*/ case IS_SIGHANDLERS: Invocation_pushBootContext(&ctx); @@ -709,6 +719,7 @@ static void initsequencer(enum initstage is, bool tolerant) _destroyJavaVM(0, 0); goto check_tolerant; } + /*FALLTHROUGH*/ case IS_PLJAVA_FOUND: greeting = InstallHelper_hello(); @@ -717,11 +728,13 @@ static void initsequencer(enum initstage is, bool tolerant) errdetail("versions:\n%s", greeting))); pfree(greeting); initstage = IS_PLJAVA_INSTALLING; + /*FALLTHROUGH*/ case IS_PLJAVA_INSTALLING: if ( NULL != pljavaLoadPath ) InstallHelper_groundwork(); /* sqlj schema, language handlers, ...*/ initstage = IS_COMPLETE; + /*FALLTHROUGH*/ case IS_COMPLETE: pljavaLoadingAsExtension = false; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index ef9a8849..5cd700cf 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -243,6 +243,7 @@ static Type _getCoerce(Type self, Type other, Oid fromOid, Oid toOid, case COERCION_PATH_NONE: elog(ERROR, "no conversion function from (regtype) %d to %d", fromOid, toOid); + pg_unreachable(); /*elog(ERROR is already so marked; what's with gcc?*/ case COERCION_PATH_RELABELTYPE: /* * Binary compatible type. No need for a special coercer. @@ -255,9 +256,11 @@ static Type _getCoerce(Type self, Type other, Oid fromOid, Oid toOid, case COERCION_PATH_COERCEVIAIO: elog(ERROR, "COERCEVIAIO not implemented from (regtype) %d to %d", fromOid, toOid); + pg_unreachable(); case COERCION_PATH_ARRAYCOERCE: elog(ERROR, "ARRAYCOERCE not implemented from (regtype) %d to %d", fromOid, toOid); + pg_unreachable(); case COERCION_PATH_FUNC: break; } From 2081148d5ed080ecea66efe9608051e9709d8472 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 24 Nov 2020 23:20:17 -0500 Subject: [PATCH 0849/1087] Two bits of Java lint Not strictly related to the fallthrough business, but of the general nature of lint, and hardly worth a pull request of its own. --- .../org/postgresql/pljava/example/annotation/PassXML.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 89ef40e6..1e61893d 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 @@ -1243,7 +1243,7 @@ private static Source sxToSource(SQLXML sx, int how, ResultSet adjust) } if ( s instanceof Adjusting.XML.Source ) - return applyAdjustments(adjust, (Adjusting.XML.Source)s).get(); + return applyAdjustments(adjust, (Adjusting.XML.Source)s).get(); return s; } @@ -1275,7 +1275,7 @@ private static Result sxToResult(SQLXML sx, int how, ResultSet adjust) } if ( r instanceof Adjusting.XML.Result ) - return applyAdjustments(adjust, (Adjusting.XML.Result)r).get(); + return applyAdjustments(adjust, (Adjusting.XML.Result)r).get(); return r; } From 19a0d6d4cd7d3719923ea34f7497118ea175054f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 25 Nov 2020 21:53:52 -0500 Subject: [PATCH 0850/1087] Get some GitHub Actions coverage vice Travis-CI As Travis-CI has announced a "new pricing model"[1],[2] and vanished from PL/Java's CI checks ("does not have enough credits"[3]), quickly get at least some minimal coverage using GitHub Actions to plug the hole. Linux x86_64 and Mac OS are covered easily. There are also GitHub Actions runners for Windows, which would allow migrating perhaps from AppVeyor also, though that's less urgent at the moment as AppVeyor has not gone away. The unavoidable loss, it appears, will be Linux ppc64le; there is no GitHub Actions hosted runner for that. This workflow so far only tests with one PostgreSQL version: whatever version is preinstalled on the runner. Similar workflows can be developed to install other versions from packages or source; it's just a small matter of scripting, after all. [1] https://www.jeffgeerling.com/blog/2020/travis-cis-new-pricing-plan-threw-wrench-my-open-source-works [2] https://blog.travis-ci.com/2020-11-02-travis-ci-new-billing [3] https://travis-ci.com/github/tada/pljava/requests --- .github/workflows/ci-runnerpg.yml | 409 ++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 .github/workflows/ci-runnerpg.yml diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml new file mode 100644 index 00000000..8d95a0d2 --- /dev/null +++ b/.github/workflows/ci-runnerpg.yml @@ -0,0 +1,409 @@ +# 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: PL/Java CI with PostgreSQL version supplied by the runner + +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-latest + cc: clang +# - os: windows-latest +# cc: msvc +# - os: windows-latest +# cc: mingw + java: [9, 11] # , 12, 14, 15] + + 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: | + sudo apt-get update + sudo apt-get install postgresql-server-dev-13 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" \ + "-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 + + 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" + + 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]; + results.compute(type, (k,v) -> 1 + v); + if ( whatIsNG.contains(type) ) + 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, + "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, + + // 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: 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() ) + { + 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, 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 + } + + /* + * 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 + ); + } + } catch ( Throwable t ) + { + succeeding = false; + throw t; + } + + System.out.println(results); + succeeding &= (0 == results.get("ng")); + System.exit(succeeding ? 0 : 1) + ENDJSHELL From 8a6b1b45bc57ce59adf2a7f8fa1ac1dcf1999ca9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 26 Nov 2020 23:04:04 -0500 Subject: [PATCH 0851/1087] Test some other behaviors subject to bit rot If not routinely tested, there are some other corners that may be prone to bit rot, such as: - make sure DROP EXTENSION works cleanly, and leaves nothing behind. - make sure LOAD 'pljava-so-...'; works to load as a non-extension (that was introduced for use in the pre-extension versions of PG, but still covers one possibly important use case: it works when you don't have access to install PL/Java in the specific server directories that CREATE EXTENSION requires, but you can install in some other directory readable by the server, set pljava.module_path to correctly locate the jar files, and pass LOAD the correct path to the shared object). - Make sure the after-the-fact CREATE EXTENSION FROM unpackaged; still works. PG upstream considers that obsolete and it is removed from PG 13 (but there an equivalent two-step sequence, CREATE EXTENSION pljava VERSION unpackaged; followed in a new session by ALTER EXTENSION pljava UPDATE; will still work). For PL/Java there is still a possibility that CREATE EXTENSION fails on the first try if some settings aren't right, and shifts to an exploratory installation that succeeds when the wrong settings are fixed, and then prints the CREATE FROM unpackaged command(s) that will be needed to finish the job. The from-unpackaged extension script had bit-rotted; with the addition of validators back in a81ef5e, the CREATE LANGUAGE commands needed to be removed from the script, and the alias_java_language function born in 5565a3c should have been added. Update Node.java to record the installed location of the shared object and offer a loadPLJava() method that issues the right LOAD command. Other small fixes. Apparently the pgjdbc-ng ResultSetMetaData can return precision = -1 for some data types, which JDBC does not consider valid. WebRowSet.populate() takes that in stride and just presents it as 0 instead, but RowSetMetaDataImpl.setPrecision() rejects it. On Windows, I think installJar was constructing URIs with Windows backslashes as separators. And it was working .... With any luck, constructing them as proper URIs will also work. These blobs of jshell in the three different CI service configs are by now getting long enough, and similar enough, to be ripe for comparing one last time and factoring out one common version, but that can be a chore for another day. --- .github/workflows/ci-runnerpg.yml | 144 +++++++++++++++++- .travis.yml | 129 ++++++++++++++-- appveyor.yml | 130 ++++++++++++++-- pljava-packaging/src/main/java/Node.java | 38 ++++- .../main/resources/pljava--unpackaged--.sql | 8 +- 5 files changed, 419 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 8d95a0d2..420ecbf4 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -266,7 +266,7 @@ jobs: "create extension no result", null, - q(c, "create extension pljava") + q(c, "CREATE EXTENSION pljava") .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), @@ -396,6 +396,148 @@ jobs: (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 ) { diff --git a/.travis.yml b/.travis.yml index 68099e57..542a978e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -209,7 +209,7 @@ script: | "create extension no result", null, - q(c, "create extension pljava") + q(c, "CREATE EXTENSION pljava") .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), @@ -339,22 +339,88 @@ script: | (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 make sure the extension can be loaded - * in a non-superuser session. + * 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( - "become non-superuser", + "load as non-extension", null, - q(c, - "CREATE ROLE alice;" + - "GRANT USAGE ON SCHEMA sqlj TO alice;" + - "SET SESSION AUTHORIZATION alice") + Node.loadPLJava(c) .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), @@ -362,16 +428,57 @@ script: | (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( - "load as non-superuser", + "package after loading", null, - q(c, "SELECT null::pg_catalog.void FROM sqlj.get_classpath('public')") + 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) -> isVoidResultSet(o, 1, 1) ? 3 : false, (o,p,q) -> null == o ); } diff --git a/appveyor.yml b/appveyor.yml index 849223b7..657f80ce 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -149,7 +149,7 @@ test_script: "create extension no result", null, - q(c, "create extension pljava") + q(c, "CREATE EXTENSION pljava") .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), @@ -279,22 +279,88 @@ test_script: (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 make sure the extension can be loaded - * in a non-superuser session. + * 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( - "become non-superuser", + "load as non-extension", null, - q(c, - "CREATE ROLE alice;" + - "GRANT USAGE ON SCHEMA sqlj TO alice;" + - "SET SESSION AUTHORIZATION alice") + Node.loadPLJava(c) .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), @@ -302,17 +368,57 @@ test_script: (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( - "load as non-superuser", + "package after loading", null, - q(c, - "SELECT null::pg_catalog.void FROM sqlj.get_classpath('public')") + 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) -> isVoidResultSet(o, 1, 1) ? 3 : false, (o,p,q) -> null == o ); } diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index f1cb0117..24272759 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -148,6 +148,7 @@ public class Node extends JarX { private static Node s_jarxHelper = new Node(null, 0, null, null); private static boolean s_jarProcessed = false; private static String s_examplesJar; + private static String s_sharedObject; /** * Perform an ordinary installation, using {@code pg_config} or the @@ -256,6 +257,9 @@ public String resolve(String storedPath, String platformPath) replacement += platformPath.substring(plen); if ( -1 != storedPath.indexOf("/pljava-examples-") ) s_examplesJar = replacement; + else if ( storedPath.matches( + "pljava/pkglibdir/(?:lib)?+pljava-so-.*") ) + s_sharedObject = replacement; if ( ! m_dryrun ) return replacement; return null; @@ -1016,6 +1020,30 @@ public static Stream setConfig( return q(ps, ps::execute); } + /** + * Load PL/Java (with a {@code LOAD} command, not {@code CREATE EXTENSION}). + *

    + * This was standard procedure in PostgreSQL versions that pre-dated the + * extension support. It is largely obsolete with the advent of + * {@code CREATE EXTENSION}, but still has one distinct use case: + * this is what will work if you do not have administrative access + * to install PL/Java's files in the standard directories where + * {@code CREATE EXTENSION} expects them, but can only place them in some + * other location the server can read. Then you simply have to make sure + * that {@code pljava.module_path} is set correctly to locate the jar files, + * and give the correct shared-object path to {@code LOAD} (which this + * method does). + * @return a {@link #q(Statement,Callable) result stream} from executing + * the statement + */ + public static Stream loadPLJava(Connection c) throws Exception + { + dryExtract(); + Statement s = c.createStatement(); + String sql = "LOAD " + s.enquoteLiteral(s_sharedObject); + return q(s, () -> s.execute(sql)); + } + /** * Install a jar. * @return a {@link #q(Statement,Callable) result stream} from executing @@ -1508,7 +1536,9 @@ public static Stream installExamples(Connection c, boolean deploy) throws Exception { dryExtract(); - return installJar(c, "file:"+s_examplesJar, "examples", deploy); + String uri = Paths.get(s_examplesJar).toUri() + .toString().replaceFirst("^file:///", "file:/"); + return installJar(c, uri, "examples", deploy); } /** @@ -1782,7 +1812,8 @@ public static void qp(ParameterMetaData md) throws Exception { mdi.setColumnType(i, md.getParameterType(i)); mdi.setColumnTypeName(i, md.getParameterTypeName(i)); - mdi.setPrecision(i, md.getPrecision(i)); + int precision = md.getPrecision(i); + mdi.setPrecision(i, precision > 0 ? precision : 0); mdi.setScale(i, md.getScale(i)); mdi.setNullable(i, md.isNullable(i)); mdi.setSigned(i, md.isSigned(i)); @@ -1809,7 +1840,8 @@ public static void qp(ResultSetMetaData md) throws Exception { mdi.setColumnType(i, md.getColumnType(i)); mdi.setColumnTypeName(i, md.getColumnTypeName(i)); - mdi.setPrecision(i, md.getPrecision(i)); + int precision = md.getPrecision(i); + mdi.setPrecision(i, precision > 0 ? precision : 0); mdi.setScale(i, md.getScale(i)); mdi.setNullable(i, md.isNullable(i)); mdi.setSigned(i, md.isSigned(i)); diff --git a/pljava-packaging/src/main/resources/pljava--unpackaged--.sql b/pljava-packaging/src/main/resources/pljava--unpackaged--.sql index 86767df4..9d56fb94 100644 --- a/pljava-packaging/src/main/resources/pljava--unpackaged--.sql +++ b/pljava-packaging/src/main/resources/pljava--unpackaged--.sql @@ -39,13 +39,15 @@ DROP TABLE /* The language-hander functions do not need to be explicitly added, because the LOAD actions always CREATE OR REPLACE them, which makes them extension members. + Since the validators were added for 1.6.0, the language entries are also always + CREATE OR REPLACEd, so they don't have to be mentioned here either. */ -ALTER EXTENSION pljava ADD LANGUAGE java; -ALTER EXTENSION pljava ADD LANGUAGE javau; - ALTER EXTENSION pljava ADD FUNCTION sqlj.add_type_mapping(character varying,character varying); +ALTER EXTENSION pljava ADD + FUNCTION sqlj.alias_java_language( + character varying,boolean,boolean,character varying); ALTER EXTENSION pljava ADD FUNCTION sqlj.drop_type_mapping(character varying); ALTER EXTENSION pljava ADD From 91b85c49d5f55657cbd4193c161b2529efa2e8bb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 27 Nov 2020 12:48:59 -0500 Subject: [PATCH 0852/1087] Make loadPLJava() work on both Windows flavors The computation of module.pathname in -packaging/pom.xml drops the .lib suffix on Windows. That has to happen in loadPLJava() also. MinGW-w64 accepts either spelling, but MSVC is fussier. --- pljava-packaging/src/main/java/Node.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index 24272759..fa4e0b00 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1040,7 +1040,16 @@ public static Stream loadPLJava(Connection c) throws Exception { dryExtract(); Statement s = c.createStatement(); - String sql = "LOAD " + s.enquoteLiteral(s_sharedObject); + String whatToLoad = s_sharedObject; + + /* + * MinGW-w64 does not fail if the .lib suffix is left in place, but + * MSVC does, and MinGW-w64 also allows it to be removed. + */ + if ( s_isWindows ) + whatToLoad = whatToLoad.replaceFirst("\\.lib$", ""); + + String sql = "LOAD " + s.enquoteLiteral(whatToLoad); return q(s, () -> s.execute(sql)); } From 62535fdb6f15c3db249e665a790fc5810c646669 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 27 Nov 2020 18:05:29 -0500 Subject: [PATCH 0853/1087] Name a schema classloader after its schema Since Java 9, class loaders can have names, They show up in stack traces. That could be useful. --- .../src/main/java/org/postgresql/pljava/sqlj/Loader.java | 9 ++++++--- src/site/markdown/use/trial.md | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 7dbf84b2..11de3746 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -263,8 +263,11 @@ public static ClassLoader getSchemaLoader(Identifier.Simple schema) loader = schema.equals(PUBLIC_SCHEMA) ? parent : getSchemaLoader(PUBLIC_SCHEMA); else + { + String name = "schema:" + schema.nonFolded(); loader = doPrivileged(() -> - new Loader(classImages, codeSources, parent)); + new Loader(classImages, codeSources, parent, name)); + } s_schemaLoaders.put(schema, loader); return loader; @@ -370,9 +373,9 @@ private static URL entryURL(int entryId) */ Loader( Map entries, - Map sources, ClassLoader parent) + Map sources, ClassLoader parent, String name) { - super(parent); + super(name, parent); m_entries = entries; m_j9Helper = ifJ9getHelper(); // null if not under OpenJ9 with sharing diff --git a/src/site/markdown/use/trial.md b/src/site/markdown/use/trial.md index 79d4bffd..d6d81788 100644 --- a/src/site/markdown/use/trial.md +++ b/src/site/markdown/use/trial.md @@ -160,7 +160,7 @@ java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.transf ... java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:383) >> sqlj:examples [PLPrincipal.Sandboxed: java] << -org.postgresql.pljava.example.annotation.PassXML.transformXML(PassXML.java:561) +schema:public//org.postgresql.pljava.example.annotation.PassXML.transformXML(PassXML.java:561) ``` The example shows the use of an XSLT 1.0 transform that appears to From cf9badc2d1139e4835e8e3171161487a776be3b5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 27 Nov 2020 18:05:29 -0500 Subject: [PATCH 0854/1087] Update release notes --- src/site/markdown/releasenotes.md.vm | 85 ++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index a2a76f77..12b892ab 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,7 +10,76 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') -$h2 PL/Java 1.6.1 +$h2 PL/Java 1.6.2 + +This is the second minor update in the PL/Java 1.6 series, with two bugs fixed +(including one that was likely to be a blocker for many applications). It adds +a 'trial' security policy, useful when migrating code from PL/Java 1.5 to +identify permission grants that may be needed, and some minor but useful +example functionality. + +$h3 Changes + +$h4 Bug blocking use from an unprivileged PostgreSQL role fixed + +In 1.6.0 and 1.6.1, PL/Java could fail to start in a backend process if the +effective PostgreSQL role was not a superuser or a member of +`pg_read_all_settings`. That is fixed in this release. + +$h4 Trial security policy for migrating code from PL/Java pre-1.6.0 + +When migrating code from pre-1.6.0 PL/Java versions, it may be necessary to +add permission grants to the PL/Java 1.6 security policy, which is distributed +as a minimal starting point. + +[Configuring permissions in PL/Java](use/policy.html) covers the topic in +general, and [Migrating to policy-based permissions](use/trial.html) covers +the new 'trial' policy introduced in 1.6.2 to simplify the process. + +$h4 Example functions using XSLT 1.0 polished for simpler use + +Although 1.0 is obsolete and very limited compared to current versions of XSLT, +the implementation in Java has two attractive properties. First, it is +provided in the Java runtime with no need of a separate download such as Saxon, +and second, it allows use of any accessible Java methods or constructors from +XPath expressions, which can often make it more useful than the limitations +of strict XSLT 1.0 would suggest. + +The [example functions][PassXML] `prepareXMLTransform`, +`prepareXMLTransformWithJava`, and `transformXML`, which demonstrate how such +functions can be coded, have also been given streamlined parameter lists with +defaults to make them simple to use for real work. + +One small but useful result is an easy way to indent XML for readability, +simply by passing a null transform name and `indent => true` to +`transformXML`. + +$h3 Bugs fixed + +* [must be superuser or a member of `pg_read_all_settings`](${ghbug}331) +* [Operator annotation can reject explicit operand types in error](${ghbug}330) + +$h3 Credits + +Thanks to Francisco Biete for the report of [#331](${ghbug}331). + +[PassXML]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/PassXML.html#method.summary + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + +$h2 PL/Java 1.6.1 (16 November 2020) + +_Note: 1.6.1 was released with [a bug](${ghbug}331) likely to be a blocker +for many applications. It was fixed in 1.6.2._ This is the first minor update in the PL/Java 1.6 series, with two bugs fixed. It also adds functionality in the SQL generator, allowing automated @@ -81,19 +150,11 @@ Thanks to Bear Giles for the `pljava-udt-type-extension` example, which not only illustrates the SQL generation improvements in this release, but also exposed both of the bugs fixed here. -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 PL/Java 1.6.0 (18 October 2020) +_Note: 1.6.0 was released with [a bug](${ghbug}331) likely to be a blocker +for many applications. It was fixed in 1.6.2._ + This is the first release of a significantly refactored PL/Java 1.6 branch with a number of new features and changes. It requires Java 9 or later at build and run time, but retains the ability to run PL/Java application code From 93a9ef0dbe5919c0b22fbc898faeb646ee2200da Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 27 Nov 2020 18:05:29 -0500 Subject: [PATCH 0855/1087] Test more Java versions in ci-runnerpg This workflow offers no choice of PostgreSQL version, so may as well confirm operation with several Java versions. --- .github/workflows/ci-runnerpg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 420ecbf4..59d45f92 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -28,7 +28,7 @@ jobs: # cc: msvc # - os: windows-latest # cc: mingw - java: [9, 11] # , 12, 14, 15] + java: [9, 11, 12, 13, 14, 15] steps: From 5049282f76c9b30336848c2c20c24a62351cad64 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 27 Nov 2020 18:05:29 -0500 Subject: [PATCH 0856/1087] Poke migration-management versions for 1.6.2 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 42ed012b..522a5849 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -691,6 +691,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_6_2 = REL_1_5_0; static final SchemaVariant REL_1_6_1 = REL_1_5_0; static final SchemaVariant REL_1_6_0 = REL_1_5_0; From 48ec486a027a8930e2b3f1e81098df8c28ddef71 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 27 Nov 2020 18:21:11 -0500 Subject: [PATCH 0857/1087] Add control file in preparation for next release Now that 1.6.2 is released, the next release should include an extension SQL file allowing upgrade from 1.6.2. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index cc4fed95..5b1f0150 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -255,6 +255,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Tue, 23 Mar 2021 21:51:02 -0400 Subject: [PATCH 0858/1087] Of course there would be a typo in the docs to be discovered right after a release. Well, it'll be right in the next one. May as well fix a couple other style issues in passing. --- src/site/markdown/develop/node.md | 2 +- src/site/markdown/use/policy.md | 8 ++++---- src/site/markdown/use/trial.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/site/markdown/develop/node.md b/src/site/markdown/develop/node.md index abd7f185..1011182d 100644 --- a/src/site/markdown/develop/node.md +++ b/src/site/markdown/develop/node.md @@ -261,7 +261,7 @@ try ( { try ( Connection c = n1.connect() ) { - qp(c, "create extension pljava"); + qp(c, "CREATE EXTENSION pljava"); } /* diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index 8ce12948..e9f9cc31 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -74,6 +74,10 @@ grant principal org.postgresql.pljava.PLPrincipal$Sandboxed * { }; grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed * { + + // Java does not circumvent operating system access controls; + // this grant will still be limited to what the OS allows a + // PostgreSQL backend process to do. permission java.io.FilePermission "<>", "read,write,delete,readlink"; }; @@ -193,10 +197,6 @@ grant principal org.postgresql.pljava.PLPrincipal$Sandboxed "java" { }; grant principal org.postgresql.pljava.PLPrincipal$Unsandboxed "javaU" { - - // Java does not circumvent operating system access controls; - // this grant will still be limited to what the OS allows a - // PostgreSQL backend process to do. permission java.io.FilePermission "<>", "read,readlink,write,delete"; }; diff --git a/src/site/markdown/use/trial.md b/src/site/markdown/use/trial.md index d6d81788..4c0d1706 100644 --- a/src/site/markdown/use/trial.md +++ b/src/site/markdown/use/trial.md @@ -147,7 +147,7 @@ jdk.translet/die.verwandlung.GregorSamsa.transform() java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.transform(AbstractTranslet.java:624) ... java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:383) -org.postgresql.pljava.example.annotation.PassXML.transformXML(PassXML.java:561) +schema:public//org.postgresql.pljava.example.annotation.PassXML.transformXML(PassXML.java:561) POLICY DENIES/TRIAL POLICY ALLOWS: ("java.net.SocketPermission" "127.0.0.1:5432" "connect,resolve") java.base/java.security.ProtectionDomain.implies(ProtectionDomain.java:321) From 1434f34c1e859bda64a3d81502ee09dde498c433 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 18:07:39 -0400 Subject: [PATCH 0859/1087] Install 1.6.0's new sqlj function when updating The addition of a new function in the sqlj schema should have occasioned a new SchemaVariant in InstallHelper that would make sure the same function gets installed during an update from an earlier installed version. That was overlooked. As the necessary SQL to create the function is exactly the same as what is generated automatically into the deployment descriptor, refactor slightly to allow a SchemaVariant's migrateFrom to also apply deployment-descriptor commands, selectively, by replacing the default recognized implementor tag set. The new function added for 1.6.0 is now given a distinct implementor tag so it can be selectively applied. A new SQLAction is added to the o.p.p.management.Commands class, under the default implementor tag, that unconditionally adds the new tag to the recognized set. So in a full ex-nihilo install, everything gets installed. The 1.5 to 1.6 schema migration now simply executes the deployment descriptor with the default recognized tag set replaced, and containing only the specific tag for the new function, so only that gets installed. Addresses #341. --- .../pljava/internal/InstallHelper.java | 84 +++++++++++++++---- .../pljava/management/Commands.java | 9 +- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 522a5849..f301dc0e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2021 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 @@ -16,6 +16,8 @@ import java.io.IOException; import java.net.URL; import java.net.MalformedURLException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.Charset; import java.security.NoSuchAlgorithmException; import java.security.Policy; @@ -509,22 +511,55 @@ private static void deployment( Connection c, Statement s, SchemaVariant sv) return; } - StringBuilder sb; + deployViaDescriptor( c); + } + + /** + * Only execute the deployment descriptor for PL/Java itself; factored out + * of {@code deployment()} so it can be used also from schema migration to + * avoid duplicating SQL that appears there. + *

    + * Schema migration will use the wrapper method that changes the effective + * set of recognized implementor tags. + */ + private static void deployViaDescriptor( Connection c) + throws SQLException + { + SQLDeploymentDescriptor sdd; try(InputStream is = - InstallHelper.class.getResourceAsStream("/pljava.ddr"); - InputStreamReader isr = - new InputStreamReader(is, UTF_8.newDecoder())) + InstallHelper.class.getResourceAsStream("/pljava.ddr")) { - sb = new StringBuilder(); - char[] buf = new char[512]; - for ( int got; -1 != (got = isr.read(buf)); ) - sb.append(buf, 0, got); + CharBuffer cb = + UTF_8.newDecoder().decode( ByteBuffer.wrap( is.readAllBytes())); + sdd = new SQLDeploymentDescriptor(cb.toString()); } - SQLDeploymentDescriptor sdd = - new SQLDeploymentDescriptor(sb.toString()); + catch ( ParseException | IOException e ) + { + throw new SQLException( + "Could not load PL/Java's deployment descriptor: " + + e.getMessage(), "XX000", e); + } + sdd.install(c); } + /** + * Only execute the deployment descriptor for PL/Java itself, temporarily + * replacing the default set of implementor tags with a specified set, to + * selectively apply commands appearing in the descriptor. + */ + private static void deployViaDescriptor( + Connection c, Statement s, String implementors) + throws SQLException + { + s.execute( "SET LOCAL pljava.implementors TO " + + s.enquoteLiteral(implementors)); + + deployViaDescriptor( c); + + s.execute( "RESET pljava.implementors"); + } + /** * Detect an existing PL/Java sqlj schema. Tests for changes between schema * variants that have appeared in PL/Java's git history and will return a @@ -543,9 +578,15 @@ private static SchemaVariant recognizeSchema( throws SQLException { DatabaseMetaData md = c.getMetaData(); - ResultSet rs = md.getColumns( null, "sqlj", "jar_descriptor", null); + ResultSet rs = md.getProcedures( null, "sqlj", "alias_java_language"); boolean seen = rs.next(); rs.close(); + if ( seen ) + return SchemaVariant.REL_1_6_0; + + rs = md.getColumns( null, "sqlj", "jar_descriptor", null); + seen = rs.next(); + rs.close(); if ( seen ) return SchemaVariant.UNREL20130301b; @@ -638,10 +679,22 @@ private static SchemaVariant recognizeSchema( * up to date. */ private static final SchemaVariant currentSchema = - SchemaVariant.REL_1_5_0; + SchemaVariant.REL_1_6_0; private enum SchemaVariant { + REL_1_6_0 ("5565a3c9c4b8d6dd0b0f7fff4090d4e8120dc10a") + { + @Override + void migrateFrom( SchemaVariant sv, Connection c, Statement s) + throws SQLException + { + if ( REL_1_5_0 != sv ) + REL_1_5_0.migrateFrom( sv, c, s); + + deployViaDescriptor( c, s, "alias_java_language"); + } + }, REL_1_5_0 ("c51cffa34acd5a228325143ec29563174891a873") { @Override @@ -691,9 +744,8 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); - static final SchemaVariant REL_1_6_2 = REL_1_5_0; - static final SchemaVariant REL_1_6_1 = REL_1_5_0; - static final SchemaVariant REL_1_6_0 = REL_1_5_0; + static final SchemaVariant REL_1_6_2 = REL_1_6_0; + static final SchemaVariant REL_1_6_1 = REL_1_6_0; static final SchemaVariant REL_1_5_7 = REL_1_5_0; static final SchemaVariant REL_1_5_6 = REL_1_5_0; diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index c7b65971..519051f5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -374,6 +374,11 @@ " DROP TABLE sqlj.typemap_entry", " DROP TABLE sqlj.jar_repository CASCADE" }) +@SQLAction(provides="alias_java_language", install={ +" SELECT " + +" pg_catalog.set_config('pljava.implementors', 'alias_java_language,' " + +" || pg_catalog.current_setting('pljava.implementors'), true)" +}) public class Commands { private final static Logger s_logger = Logger.getLogger(Commands.class @@ -1031,7 +1036,7 @@ private static void withJarInPath(String jarName, boolean schemaMayVanish, */ @Function( schema="sqlj", name="alias_java_language", onNullInput=CALLED, - requires="sqlj.tables" + requires="sqlj.tables", implementor="alias_java_language" ) public static void aliasJavaLanguage( String alias, From b8a5144a69107975bf011e80f5150805230b0c7c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 18:12:07 -0400 Subject: [PATCH 0860/1087] Add test needing permission in class initializer Under issue #342, this will fail at CREATE FUNCTION time (if validation is being done, check_function_bodies is on) even though the java_tzset language is granted the needed permission, because the validator doesn't run the class initializer in the same access control context that would be used for calling the function itself, so it doesn't have the permission. --- .../pljava/example/annotation/PreJSR310.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java index c0409c0d..e430e25d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2021 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 @@ -47,6 +47,21 @@ public class PreJSR310 { private static final String TZPRAGUE = "Europe/Prague"; + static + { + TimeZone oldZone = TimeZone.getDefault(); + TimeZone tzPrague = TimeZone.getTimeZone(TZPRAGUE); + + try + { + TimeZone.setDefault(tzPrague); + } + finally + { + TimeZone.setDefault(oldZone); + } + } + /** * Test for a regression in PG date to/from java.sql.Date conversion * identified in issue #199. From 46780c2a79708ba9cdddfec7865dbf534d8ca670 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 18:13:03 -0400 Subject: [PATCH 0861/1087] Mollify the -Xcheck:jni exception-check checker Nearly all JNI calls-into-Java are made through the wrappers in JNICalls.c, which ensure that each one is followed by the obligatory was-an-exception-thrown check. But this handful of uses in JNICalls itself weren't followed by checks. As it happens, the methods being called wouldn't be expected to throw exceptions. But the -Xcheck:jni linter doesn't know that, so add the checks here to make it happy. After 15 years of not having exception checks here, it's certainly not necessary to implement any elaborate recovery strategies for them, other than maybe not using the method's result. They're all "shouldn't happen" cases. --- pljava-so/src/main/c/JNICalls.c | 43 ++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index d9d82ea1..eefc59f0 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -60,18 +60,34 @@ static void elogExceptionMessage(JNIEnv* env, jthrowable exh, int logLevel) { StringInfoData buf; int sqlState = ERRCODE_INTERNAL_ERROR; - jclass exhClass = (*env)->GetObjectClass(env, exh); - jstring jtmp = (jstring)(*env)->CallObjectMethod(env, exhClass, Class_getName); JNIEnv* saveEnv = jniEnv; + jclass exhClass = (*env)->GetObjectClass(env, exh); + jstring jtmp = + (jstring)(*env)->CallObjectMethod(env, exhClass, Class_getName); + /* ExceptionOccurred check is found below ... */ initStringInfo(&buf); jniEnv = env; /* Used by the String operations */ - String_appendJavaString(&buf, jtmp); + + if ( 0 == (*env)->ExceptionOccurred(env) ) /* ... here */ + String_appendJavaString(&buf, jtmp); + else + { + (*env)->ExceptionClear(env); + appendStringInfoString(&buf, ""); + } + (*env)->DeleteLocalRef(env, exhClass); (*env)->DeleteLocalRef(env, jtmp); jtmp = (jstring)(*env)->CallObjectMethod(env, exh, Throwable_getMessage); + if ( 0 != (*env)->ExceptionOccurred(env) ) + { + (*env)->ExceptionClear(env); + jtmp = 0; + } + if(jtmp != 0) { appendStringInfoString(&buf, ": "); @@ -82,6 +98,12 @@ static void elogExceptionMessage(JNIEnv* env, jthrowable exh, int logLevel) if((*env)->IsInstanceOf(env, exh, SQLException_class)) { jtmp = (*env)->CallObjectMethod(env, exh, SQLException_getSQLState); + if ( 0 != (*env)->ExceptionOccurred(env) ) + { + (*env)->ExceptionClear(env); + jtmp = 0; + } + if(jtmp != 0) { char* s = String_createNTS(jtmp); @@ -112,6 +134,7 @@ static void printStacktrace(JNIEnv* env, jobject exh) { int currLevel = Backend_setJavaLogLevel(DEBUG1); (*env)->CallVoidMethod(env, exh, Throwable_printStackTrace); + (*env)->ExceptionOccurred(env); /* sop for JNI exception-check check */ Backend_setJavaLogLevel(currLevel); } } @@ -134,6 +157,12 @@ static void endCall(JNIEnv* env) /* Rethrow the server error. */ jobject jed = (*env)->CallObjectMethod(env, exh, ServerException_getErrorData); + if ( 0 != (*env)->ExceptionOccurred(env) ) + { + (*env)->ExceptionClear(env); + jed = 0; + } + if(jed != 0) ReThrowError(pljava_ErrorData_getErrorData(jed)); } @@ -158,6 +187,12 @@ static void endCallMonitorHeld(JNIEnv* env) /* Rethrow the server error. */ jobject jed = (*env)->CallObjectMethod(env, exh, ServerException_getErrorData); + if ( 0 != (*env)->ExceptionOccurred(env) ) + { + (*env)->ExceptionClear(env); + jed = 0; + } + if(jed != 0) ReThrowError(pljava_ErrorData_getErrorData(jed)); } From 54fc8a31d0509598a20bad0e6666f61d546e4c70 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 19:27:55 -0400 Subject: [PATCH 0862/1087] Run clinit at validation in the right context If validating (check_function_bodies is on), class initializers will be run at CREATE FUNCTION time in a bid to catch as many issues early as possible. Run them in the same access control context that would be used at run time to call the function being declared. Other classes besides the one containing the target method can also be loaded and initialized: all those corresponding to the method's parameter and return types, for example. Run those class initializers also under the context that would be used for the function being declared; that also mirrors what will happen at runtime when the function is called. If a class contains several methods that would be given different access control contexts (will be declared with different trust or language attributes, say), the permissions available when the class initializer runs will be those of whichever function is called first in a given session (or, by the validator, for whichever CREATE FUNCTION is seen first in a session). If there are initial actions needed that require permissions that might not always be available, they should be moved out of the class initializer and handled in the first call of a method that will have the needed permissions. Or, perhaps, things can be arranged so that the more-privileged function will be the first one called in any session, and have its CREATE FUNCTION first in the deployment descriptor, so that the class initializer will always run with its permissions. Addresses #342. In passing, unwrap ExceptionInInitializerError payloads when returning from a call; it's more useful to have the ereport show what actually went wrong. --- .../pljava/internal/EntryPoints.java | 44 ++++++++++++- .../postgresql/pljava/internal/Function.java | 61 +++++++++++++------ 2 files changed, 83 insertions(+), 22 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java index 7df1b22a..fb0ff15c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2021 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 @@ -21,6 +21,7 @@ import java.sql.SQLData; import java.sql.SQLException; +import java.sql.SQLNonTransientException; import java.sql.SQLSyntaxErrorException; import java.sql.SQLInput; import java.sql.SQLOutput; @@ -299,13 +300,17 @@ private static SQLData udtParseInvoke( */ private static T doPrivilegedAndUnwrap( PrivilegedAction action, AccessControlContext context) - throws Throwable + throws SQLException { Throwable t; try { return doPrivileged(action, context); } + catch ( ExceptionInInitializerError e ) + { + t = e.getCause(); + } catch ( Error e ) { throw e; @@ -320,7 +325,7 @@ private static T doPrivilegedAndUnwrap( } if ( t instanceof SQLException ) - throw t; + throw (SQLException)t; if ( t instanceof SecurityException ) /* @@ -332,6 +337,39 @@ private static T doPrivilegedAndUnwrap( throw new SQLException(t.getMessage(), t); } + /** + * Called from {@code Function} to perform the initialization of a class, + * under a selected access control context. + */ + static Class loadAndInitWithACC( + String className, ClassLoader schemaLoader, AccessControlContext acc) + throws SQLException + { + PrivilegedAction> action = () -> + { + try + { + return Class.forName(className, true, schemaLoader); + } + catch ( ExceptionInInitializerError e ) + { + throw e; + } + catch ( LinkageError | ClassNotFoundException e ) + { + /* + * It would be odd to get a ClassNotFoundException here, as + * the caller had to look it up once already to decide what acc + * to use. But try telling that to javac. + */ + throw unchecked(new SQLNonTransientException( + "Initializing class " + className + ": " + e, "46103", e)); + } + }; + + return doPrivilegedAndUnwrap(action, acc); + } + /** * A class carrying a payload of some kind and an access control context * to impose when it is invoked. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 3a0274a8..53572d99 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2016-2021 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 @@ -84,6 +84,7 @@ import org.postgresql.pljava.internal.EntryPoints; import org.postgresql.pljava.internal.EntryPoints.Invocable; import static org.postgresql.pljava.internal.EntryPoints.invocable; +import static org.postgresql.pljava.internal.EntryPoints.loadAndInitWithACC; import static org.postgresql.pljava.internal.Privilege.doPrivileged; import static org.postgresql.pljava.jdbc.TypeOid.INVALID; import static org.postgresql.pljava.jdbc.TypeOid.TRIGGEROID; @@ -161,7 +162,7 @@ public static Class getClassIfUDT( Identifier.Simple schema = Identifier.Simple.fromCatalog(schemaName); return - loadClass(Loader.getSchemaLoader(schema), className, false) + loadClass(Loader.getSchemaLoader(schema), className, null) .asSubclass(SQLData.class); } @@ -170,9 +171,11 @@ public static Class getClassIfUDT( * inputs, but producing a {@code MethodType} instead of a JNI signature. *

    * The return type is the last element of {@code jTypes}. + *

    + * {@code acc} is null except when validating; see {@code loadClass}. */ private static MethodType buildSignature( - ClassLoader schemaLoader, String[] jTypes, boolean forValidator, + ClassLoader schemaLoader, String[] jTypes, AccessControlContext acc, boolean commute, boolean retTypeIsOutParameter, boolean isMultiCall, boolean altForm) throws SQLException @@ -200,7 +203,7 @@ private static MethodType buildSignature( Class[] pTypes = new Class[ rtIdx ]; for ( int i = 0 ; i < rtIdx ; ++ i ) - pTypes[i] = loadClass(schemaLoader, jTypes[i], forValidator); + pTypes[i] = loadClass(schemaLoader, jTypes[i], acc); if ( commute ) { @@ -210,7 +213,7 @@ private static MethodType buildSignature( } Class returnType = - getReturnSignature(schemaLoader, retJType, forValidator, + getReturnSignature(schemaLoader, retJType, acc, retTypeIsOutParameter, isMultiCall, altForm); return methodType(returnType, pTypes); @@ -228,9 +231,11 @@ private static MethodType buildSignature( * The overridden behavior for a composite type is to return boolean in the * non-multicall case, else one of {@code ResultSetHandle} or * {@code ResultSetProvider} depending on {@code altForm}. + *

    + * {@code acc} is null except when validating; see {@code loadClass}. */ private static Class getReturnSignature( - ClassLoader schemaLoader, String retJType, boolean forValidator, + ClassLoader schemaLoader, String retJType, AccessControlContext acc, boolean isComposite, boolean isMultiCall, boolean altForm) throws SQLException { @@ -238,7 +243,7 @@ private static Class getReturnSignature( { if ( isMultiCall ) return Iterator.class; - return loadClass(schemaLoader, retJType, forValidator); + return loadClass(schemaLoader, retJType, acc); } /* The composite case */ @@ -280,15 +285,17 @@ private static Lookup lookupFor(Class clazz) *

    * For now, this is a near-facsimile of the C implementation. A further step * of refactoring into clearer idiomatic Java can come later. + *

    + * {@code acc} is null except when validating; see {@code loadClass}. */ private static MethodHandle getMethodHandle( ClassLoader schemaLoader, Class clazz, String methodName, - boolean forValidator, boolean commute, + AccessControlContext acc, boolean commute, String[] jTypes, boolean retTypeIsOutParameter, boolean isMultiCall) throws SQLException { MethodType mt = - buildSignature(schemaLoader, jTypes, forValidator, commute, + buildSignature(schemaLoader, jTypes, acc, commute, retTypeIsOutParameter, isMultiCall, false); // try altForm false ReflectiveOperationException ex1 = null; @@ -304,7 +311,7 @@ private static MethodHandle getMethodHandle( MethodType origMT = mt; Class altType = null; Class realRetType = - loadClass(schemaLoader, jTypes[jTypes.length-1], forValidator); + loadClass(schemaLoader, jTypes[jTypes.length-1], acc); /* COPIED COMMENT: * One valid reason for not finding the method is when @@ -330,7 +337,7 @@ private static MethodHandle getMethodHandle( if ( null != altType ) { jTypes[jTypes.length - 1] = altType.getCanonicalName(); - mt = buildSignature(schemaLoader, jTypes, forValidator, commute, + mt = buildSignature(schemaLoader, jTypes, acc, commute, retTypeIsOutParameter, isMultiCall, true); // retry altForm true try { @@ -1316,7 +1323,15 @@ private static Invocable init( boolean readOnly = ((byte)'v' != procTup.getByte("provolatile")); ClassLoader schemaLoader = Loader.getSchemaLoader(schema); - Class clazz = loadClass(schemaLoader, className, forValidator); + Class clazz = loadClass(schemaLoader, className, null); + + AccessControlContext acc = + accessControlContextFor(clazz, language, trusted); + + if ( forValidator && clazz != loadClass(schemaLoader, className, acc) ) + throw new SQLException( + "Initialization of class \"" + className + "\" produced a " + + "different class object"); if ( isUDT ) { @@ -1353,7 +1368,8 @@ private static Invocable init( String methodName = info.group("meth"); MethodHandle handle = - getMethodHandle(schemaLoader, clazz, methodName, forValidator, + getMethodHandle(schemaLoader, clazz, methodName, + forValidator ? acc : null, commute, resolvedTypes, retTypeIsOutParameter, isMultiCall) .asFixedArity(); MethodType mt = handle.type(); @@ -1394,8 +1410,7 @@ else if ( Boolean.class == rt ) else handle = dropArguments(handle, 0, AccessControlContext.class); - return invocable(handle, - accessControlContextFor(clazz, language, trusted)); + return invocable(handle, acc); } /** @@ -1661,13 +1676,15 @@ private static void parseParameters( * turn that form of name into the right class, including for primitives, * void, and arrays. * - * @param forValidator if true, force initialization of the loaded class, in - * an effort to bring forward as many possible errors as can be. + * @param valACC if non-null, force initialization of the loaded class, in + * an effort to bring forward as many possible errors as can be during + * validation. Initialization will run in this access control context. */ private static Class loadClass( - ClassLoader schemaLoader, String className, boolean forValidator) + ClassLoader schemaLoader, String className, AccessControlContext valACC) throws SQLException { + boolean withoutInit = null == valACC; Matcher m = typeNameInAS.matcher(className); m.matches(); className = m.group(1); @@ -1687,7 +1704,9 @@ private static Class loadClass( default: try { - c = Class.forName(className, forValidator, schemaLoader); + c = withoutInit + ? Class.forName(className, false, schemaLoader) + : loadAndInitWithACC(className, schemaLoader, valACC); } catch ( ClassNotFoundException | LinkageError e ) { @@ -1773,6 +1792,10 @@ private static String getAS(ResultSet procTup) throws SQLException /** * The recognized forms of an "AS" string, distinguishable and broken out * by named capturing groups. + *

    + * Array brackets are of course not included in the {@code } group, so + * the caller will not have to check for the receiver class being an array. + * A check that it isn't a primitive may be in order, though. */ private static final Pattern specForms = compile(String.format( /* the UDT notation, which is case insensitive */ From b8dc7527a2675b79a1500bcef1280570ed435641 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 19:55:36 -0400 Subject: [PATCH 0863/1087] Give SQLType an optional= element Because Java disallows null as an annotation element value, it has never been possible to say defaultValue=null. Allow optional=true as an alternative meaning just that. Use it to tweak the PassXML.transformXML function so it doesn't set indent-related properties on the transformer unless the caller specified them explicitly. --- .../postgresql/pljava/annotation/SQLType.java | 20 ++++++++++- .../annotation/processing/DDRProcessor.java | 36 ++++++++++++++++--- .../pljava/example/annotation/PassXML.java | 19 +++++----- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java index 40b608a3..45805ac5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -57,8 +57,26 @@ * needs to match the number and order of components of the row type (which * cannot be checked at compile time, but will cause the deployment * descriptor code to fail at jar install time if it does not). + *

    + * A Java annotation value cannot be null. If null is what the default value + * should be, use {@code optional=true}. */ String[] defaultValue() default {}; + + /** + * What {@code optional=true} means is just what {@code defaultValue=null} + * would mean, if Java permitted null values in annotations. + *

    + * There is no difference between {@code optional=false} and simply having + * no {@code optional} or {@code defaultValue} element at all. + *

    + * Only one of {@code optional} or {@code defaultValue} may be present + * in one annotation. + *

    + * If {@code optional=true}, the function must not be annotated with + * {@code onNullInput=RETURNS_NULL}. + */ + boolean optional() default false; // Is it worth having a defaultRaw() for rare cases wanting some // arbitrary SQL expression for the default? 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 a2f9fcd4..6e79dbf1 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -1411,11 +1411,13 @@ class SQLTypeImpl extends AbstractAnnotationImpl implements SQLType { public String value() { return _value; } public String[] defaultValue() { return _defaultValue; } + public boolean optional() { return Boolean.TRUE.equals(_optional); } public String name() { return _name; } String _value; String[] _defaultValue; String _name; + Boolean _optional; // boxed so it can be null if not explicit public void setValue( Object o, boolean explicit, Element e) { @@ -1429,6 +1431,12 @@ public void setDefaultValue( Object o, boolean explicit, Element e) _defaultValue = avToArray( o, String.class); } + public void setOptional( Object o, boolean explicit, Element e) + { + if ( explicit ) + _optional = (Boolean)o; + } + public void setName( Object o, boolean explicit, Element e) { if ( ! explicit ) @@ -2022,6 +2030,7 @@ void collectParameterTypeAnnotations() List ves = func.getParameters(); paramTypeAnnotations = new SQLType [ ves.size() ]; int i = 0; + boolean anyOptional = false; for ( VariableElement ve : ves ) { for ( AnnotationMirror am : elmu.getAllAnnotationMirrors( ve) ) @@ -2031,10 +2040,21 @@ void collectParameterTypeAnnotations() SQLTypeImpl sti = new SQLTypeImpl(); populateAnnotationImpl( sti, ve, am); paramTypeAnnotations[i] = sti; + + if (null != sti._optional && null != sti._defaultValue) + msg(Kind.ERROR, ve, "Only one of optional= or " + + "defaultValue= may be given"); + + anyOptional |= sti.optional(); } } ++ i; } + + if ( anyOptional && OnNullInput.RETURNS_NULL.equals(_onNullInput) ) + msg(Kind.ERROR, func, "A PL/Java function with " + + "onNullInput=RETURNS_NULL may not have parameters with " + + "optional=true"); } /** @@ -5430,6 +5450,7 @@ DBType getSQLType(TypeMirror tm, Element e, SQLType st, DBType rslt = null; String[] defaults = null; + boolean optional = false; if ( null != st ) { @@ -5437,6 +5458,7 @@ DBType getSQLType(TypeMirror tm, Element e, SQLType st, if ( null != s ) rslt = DBType.fromSQLTypeAnnotation(s); defaults = st.defaultValue(); + optional = st.optional(); } if ( tm.getKind().equals( TypeKind.ARRAY) ) @@ -5455,7 +5477,7 @@ DBType getSQLType(TypeMirror tm, Element e, SQLType st, if ( null != rslt ) return typeWithDefault( - e, rslt, array, row, defaults, withDefault); + e, rslt, array, row, defaults, optional, withDefault); if ( tm.getKind().equals( TypeKind.VOID) ) return DT_VOID; // return type only; no defaults apply @@ -5512,7 +5534,8 @@ DBType getSQLType(TypeMirror tm, Element e, SQLType st, if ( array ) rslt = rslt.asArray("[]"); - return typeWithDefault( e, rslt, array, row, defaults, withDefault); + return typeWithDefault( + e, rslt, array, row, defaults, optional, withDefault); } /** @@ -5537,10 +5560,13 @@ DBType getSQLType(TypeMirror tm, Element e, SQLType st, */ DBType typeWithDefault( Element e, DBType rslt, boolean array, boolean row, - String[] defaults, boolean withDefault) + String[] defaults, boolean optional, boolean withDefault) { - if ( null == defaults || ! withDefault ) + if ( ! withDefault || null == defaults && ! optional ) return rslt; + + if ( optional ) + return rslt.withDefault("DEFAULT NULL"); int n = defaults.length; if ( row ) 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 1e61893d..6a2ef2f8 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2021 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 @@ -524,14 +524,14 @@ public static SQLXML transformXML( @SQLType(defaultValue="0") int howin, @SQLType(defaultValue="0") int howout, @SQLType(defaultValue={}) ResultSet adjust, - @SQLType(defaultValue="false") boolean indent, - @SQLType(defaultValue="4") int indentWidth) + @SQLType(optional=true) Boolean indent, + @SQLType(optional=true) Integer indentWidth) throws SQLException { Templates tpl = null == transformName? null: s_tpls.get(transformName); Source src = sxToSource(source, howin, adjust); - if ( indent && 0 == howout ) + if ( Boolean.TRUE.equals(indent) && 0 == howout ) howout = 4; // transformer only indents if writing a StreamResult Connection c = DriverManager.getConnection("jdbc:default:connection"); @@ -549,14 +549,17 @@ public static SQLXML transformXML( if ( rlt instanceof StreamResult ) t.setOutputProperty(ENCODING, System.getProperty("org.postgresql.server.encoding")); - else if ( indent ) + else if ( Boolean.TRUE.equals(indent) ) logMessage("WARNING", "indent requested, but howout specifies a non-stream " + "Result type; no indenting will happen"); - t.setOutputProperty("indent", indent ? "yes" : "no"); - t.setOutputProperty( - "{http://xml.apache.org/xalan}indent-amount", "" + indentWidth); + if ( null != indent ) + t.setOutputProperty("indent", indent ? "yes" : "no"); + if ( null != indentWidth ) + t.setOutputProperty( + "{http://xml.apache.org/xalan}indent-amount", + "" + indentWidth); t.transform(src, rlt); } From a2980b7e36b47ce2e2917926d1b3041e162e0221 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 21:38:03 -0400 Subject: [PATCH 0864/1087] Fix a bug in preliminary path generation This code fell behind the addition of synthetic=TWIN (now, until characterize, synthetic==null in that case, so isSynthetic is the right way to tell if the operator is synthetic). With this fixed, the right paths are generated, which can include multiple ones that differ by commutation and can't be pruned until characterize time when the operand types have been resolved. Next needed step is an algorithm to do that characterize-time pruning. Also, in case of multiple operator names for the same operation, synthesized functions should be remembered and shared, not duplicated. --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a2f9fcd4..97032c76 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -3427,7 +3427,7 @@ boolean maybeAddPath( if ( null == to.paths ) to.paths = new ArrayList<>(); - if ( null == from.synthetic ) + if ( ! from.isSynthetic ) to.paths.add(new OperatorPath(from, from, null, EnumSet.of(how))); else { From 2ec0b3e59bd533b603f2d1e9af8c1e615873e95e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 22:06:53 -0400 Subject: [PATCH 0865/1087] Prune during path generation for synthetic=TWIN The earlier idea to generate more paths and prune them at characterize() time proves unnecessary; a simple rule at generation time does the job. Also, remember and share FunctionImpl.Transformed instances rather than creating duplicates, as noted in the previous commit. --- .../annotation/processing/DDRProcessor.java | 107 ++++++++++++++---- 1 file changed, 84 insertions(+), 23 deletions(-) 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 97032c76..993a2c50 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 @@ -2359,6 +2359,27 @@ List specialization( return Collections.emptyList(); } + private Map m_variants= new HashMap<>(); + + /** + * Return an instance representing a transformation of this function, + * or null on second and subsequent requests for the same + * transformation (so the caller will not register the variant more + * than once). + */ + Transformed transformed( + Identifier.Qualified qname, + boolean commute, boolean negate) + { + Transformed prospect = new Transformed(qname, commute, negate); + DependTag.Function tag = + (DependTag.Function)prospect.provideTags().iterator().next(); + Transformed found = m_variants.putIfAbsent(tag, prospect); + if ( null == found ) + return prospect; + return null; + } + class Transformed implements Snippet { final Identifier.Qualified m_qname; @@ -2368,13 +2389,21 @@ class Transformed implements Snippet Transformed( Identifier.Qualified qname, - boolean commute, boolean negate, String comment) - { - assert commute || negate : "no transformation to apply"; + boolean commute, boolean negate) + { + EnumSet how = + EnumSet.noneOf(OperatorPath.Transform.class); + if ( commute ) + how.add(OperatorPath.Transform.COMMUTATION); + if ( negate ) + how.add(OperatorPath.Transform.NEGATION); + assert ! how.isEmpty() : "no transformation to apply"; m_qname = requireNonNull(qname); m_commute = commute; m_negate = negate; - m_comment = comment; + m_comment = "Function automatically derived by " + how + + " from " + qnameFrom( + FunctionImpl.this.name(), FunctionImpl.this.schema()); } List parameterInfo() @@ -3340,10 +3369,10 @@ void operatorPreSynthesize( Element e, OperatorImpl snip) * pending: contains all synthetic snippets * Step: * A snippet s is removed from ready and added to processed. - * If s.commutator or s.negator matches a synthetic snippet in pending - * or ready, a corresponding path is recorded on that snippet. If it is - * the first path recorded on that snippet (which must have been found - * on pending), that snippet is moved to ready. + * If s.commutator or s.negator matches a synthetic snippet in pending, + * a corresponding path is recorded on that snippet. If it is + * the first path recorded on that snippet, the snippet is moved + * to ready. */ List processed = @@ -3358,9 +3387,6 @@ void operatorPreSynthesize( Element e, OperatorImpl snip) processed.add(snip); if ( null != snip.commutator ) { - for ( OperatorImpl other : ready ) - maybeAddPath(snip, other, - OperatorPath.Transform.COMMUTATION); ListIterator it = pending.listIterator(); while ( it.hasNext() ) { @@ -3375,9 +3401,6 @@ void operatorPreSynthesize( Element e, OperatorImpl snip) } if ( null != snip.negator ) { - for ( OperatorImpl other : ready ) - maybeAddPath(snip, other, - OperatorPath.Transform.NEGATION); ListIterator it = pending.listIterator(); while ( it.hasNext() ) { @@ -3408,6 +3431,17 @@ boolean maybeAddPath( if ( ! to.isSynthetic ) return false; // don't add paths to a non-synthetic operator + /* + * setSynthetic will have left synthetic null in the synthetic=TWIN + * case. That case imposes more constraints on what paths can be added: + * an acceptable path must involve commutation (and only commutation) + * from another operator that will have a function name (so, either + * a non-synthetic one, or a synthetic one given an actual name, other + * than TWIN). In the latter case, copy the name here (for the former, + * it will be copied from the function's name, in characterize()). + */ + boolean syntheticTwin = null == to.synthetic; + switch ( how ) { case COMMUTATION: @@ -3421,9 +3455,28 @@ boolean maybeAddPath( return false; // move along if ( null != to.negator && ! to.negator.equals(from.qname) ) return false; // move along + if ( syntheticTwin ) + return false; break; } + if ( syntheticTwin ) + { + /* + * We will apply commutation to 'from' (the negation case + * would have been rejected above). Either 'from' is nonsynthetic + * and its function name will be copied in characterize(), or it is + * synthetic and must have a name or we reject it here. If not + * rejected, copy the name. + */ + if ( from.isSynthetic ) + { + if ( null == from.synthetic ) + return false; + to.synthetic = from.synthetic; + } + } + if ( null == to.paths ) to.paths = new ArrayList<>(); @@ -3441,6 +3494,20 @@ boolean maybeAddPath( return true; } + /** + * Why has {@code Set} or at least {@code EnumSet} not got this? + */ + static > EnumSet symmetricDifference( + EnumSet a, EnumSet b) + { + EnumSet result = a.clone(); + result.removeAll(b); + b = b.clone(); + b.removeAll(a); + result.addAll(b); + return result; + } + List m_nonSynthetic = new ArrayList<>(); List m_synthetic = new ArrayList<>(); @@ -3463,15 +3530,9 @@ enum Transform { NEGATION, COMMUTATION } fromProximate = proximateToNew.clone(); if ( base == proximate ) - fromBase = proximateToNew; + fromBase = fromProximate; else - { - fromBase = baseToProximate.clone(); - fromBase.removeAll(proximateToNew); - proximateToNew = proximateToNew.clone(); - proximateToNew.removeAll(fromBase); - fromBase.addAll(proximateToNew); - } + fromBase = symmetricDifference(baseToProximate, proximateToNew); } public String toString() @@ -4089,7 +4150,7 @@ else if ( ! (operandTypesDiffer || selfCommutates) ) } syntheticFunction = - func.new Transformed(synthetic, commute, negate, comment()); + func.transformed(synthetic, commute, negate); } recordImplicitTags(); From 6bd5aa0d93fae865b3088a9176552a66c0111615 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 28 Mar 2021 22:10:31 -0400 Subject: [PATCH 0866/1087] Example: 4 cross-type operators from one function The SQL generator can infer it all except what name to give the synthetic negator function. The human has to choose that. --- .../example/annotation/ComplexScalar.java | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java index 064bbea6..32e65830 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ComplexScalar.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -242,6 +242,41 @@ public static boolean componentsEQ(ComplexScalar a, ComplexScalar b) return a.m_x == b.m_x && a.m_y == b.m_y; } + /** + * True if the complex argument is real-valued and equal to the real + * argument. + *

    + * From one equality method on (complex,double) can be synthesized all four + * cross-type operators, {@code =} and {@code <>} for that pair of types and + * their {@code TWIN} commutators. One of the {@code <>} twins does need to + * specify what its synthetic function should be named. + */ + @Operator( + name = "javatest.=", + commutator = TWIN, negator = "javatest.<>", + provides = "complex:double relationals" + ) + @Operator( + name = "javatest.=", + synthetic = TWIN, negator = "javatest.<>", + provides = "complex:double relationals" + ) + @Operator( + name = "javatest.<>", synthetic = "javatest.neToReal", + commutator = TWIN, provides = "complex:double relationals" + ) + @Operator( + name = "javatest.<>", synthetic = TWIN, + provides = "complex:double relationals" + ) + @Function( + schema = "javatest", effects = IMMUTABLE, onNullInput = RETURNS_NULL + ) + public static boolean eqToReal(ComplexScalar a, double b) + { + return a.m_x == b && 0. == a.m_y; + } + /** * As an ordinary function, returns the lesser in magnitude of two * arguments; as a simple aggregate, returns the least in magnitude over its From c81eb6bd6f06579bc15226a1bc6ea17f13985ad6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 Mar 2021 18:24:14 -0400 Subject: [PATCH 0867/1087] Revisit getPgConfigPropertyAsList The change in f8e475b was a hasty measure on discovering that the single quotes in a quoted argument reported by pg_config may appear within the argument rather than only surrounding it. It solved that problem, but can be fooled into empty arguments where multiple spaces separate arguments in the pg_config output (issue #347). Revert to the regex-based approach from before f8e475b and just tweak that. This implementation openly does not handle the case of an argument containing a single quote as content. PostgreSQL's own build process failed in a simple test of such a case, so supporting it here is probably not necessary for now. --- .../postgresql/pljava/pgxs/AbstractPGXS.java | 41 +++++++++---------- .../test/java/PgConfigPropertyAsListTest.java | 13 +++++- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java index 4b51e26e..642e0675 100644 --- a/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java +++ b/pljava-pgxs/src/main/java/org/postgresql/pljava/pgxs/AbstractPGXS.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2021 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 @@ -8,13 +8,16 @@ * * Contributors: * Kartik Ohri + * Chapman Flack */ package org.postgresql.pljava.pgxs; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -75,27 +78,21 @@ public List formatDefines(Map definesMap) /** * Returns the input pg_config property as a list of individual flags split * at whitespace, except when quoted, and the quotes removed. + *

    + * The assumed quoting convention is single straight quotes around regions + * to be protected, which do not have to be the entire argument. This method + * doesn't handle a value that contains a single quote as content; + * the intended convention for that case doesn't seem to be documented, and + * PostgreSQL's own build breaks in such a case, so there is little need, + * for now, to support it here. We don't know, for now, whether the + * convention implemented here is also right on Windows. */ public List getPgConfigPropertyAsList(String properties) { - List propertyList = new ArrayList<>(); - StringBuilder builder = new StringBuilder(); - boolean isInsideQuotes = false; - - for (char x : properties.toCharArray()) - { - if (x == '\'') - isInsideQuotes = !isInsideQuotes; - else if (!isInsideQuotes && x == ' ') - { - propertyList.add(builder.toString()); - builder.setLength(0); - } - else - builder.append(x); - } - - if (builder.length() != 0) - propertyList.add(builder.toString()); - return propertyList; + Pattern pattern = Pattern.compile("(?:[^\\s']++|'(?:[^']*+)')++"); + Matcher matcher = pattern.matcher(properties); + return matcher.results() + .map(MatchResult::group) + .map(s -> s.replace("'", "")) + .collect(Collectors.toList()); } } diff --git a/pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java b/pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java index 38116d93..2455e08b 100644 --- a/pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java +++ b/pljava-pgxs/src/test/java/PgConfigPropertyAsListTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2021 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 @@ -8,6 +8,7 @@ * * Contributors: * Kartik Ohri + * Chapman Flack */ import org.junit.Before; @@ -55,4 +56,14 @@ public void testWhitespaceInQuotes() assertEquals(expectedResult, actualResult); } + @Test + public void testMultipleSpaceSeparator() + { + List actualResult = pgxs.getPgConfigPropertyAsList( + "-Wl,--as-needed -Wl,-rpath,'/usr/local test/pgsql/lib',--enable-new-dtags"); + List expectedResult = List.of("-Wl,--as-needed", + "-Wl,-rpath,/usr/local test/pgsql/lib,--enable-new-dtags"); + assertEquals(expectedResult, actualResult); + } + } From 3bc8579b61570242dacc7f3272dfb935470af643 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 Mar 2021 19:13:31 -0400 Subject: [PATCH 0868/1087] Simplify: no clinit for parameter/return classes In documenting the behavior of the validator, reserving class initialization for the method's own class only, and not for the classes of its parameter/return types, leads to simpler prose. The less-aggressive initialization is still sufficient to catch the missing dependencies OpenJ9 wasn't flagging before 677f433. --- .../postgresql/pljava/internal/Function.java | 14 +++++-- src/site/markdown/use/policy.md | 39 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 53572d99..1151da85 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -172,7 +172,9 @@ public static Class getClassIfUDT( *

    * The return type is the last element of {@code jTypes}. *

    - * {@code acc} is null except when validating; see {@code loadClass}. + * {@code acc} is non-null if validating and class initializers should + * be run for parameter and return-type classes; in any other case it is + * null (see {@code loadClass}). */ private static MethodType buildSignature( ClassLoader schemaLoader, String[] jTypes, AccessControlContext acc, @@ -232,7 +234,9 @@ private static MethodType buildSignature( * non-multicall case, else one of {@code ResultSetHandle} or * {@code ResultSetProvider} depending on {@code altForm}. *

    - * {@code acc} is null except when validating; see {@code loadClass}. + * {@code acc} is non-null if validating and class initializers should + * be run for parameter and return-type classes; in any other case it is + * null (see {@code loadClass}). */ private static Class getReturnSignature( ClassLoader schemaLoader, String retJType, AccessControlContext acc, @@ -286,7 +290,9 @@ private static Lookup lookupFor(Class clazz) * For now, this is a near-facsimile of the C implementation. A further step * of refactoring into clearer idiomatic Java can come later. *

    - * {@code acc} is null except when validating; see {@code loadClass}. + * {@code acc} is non-null if validating and class initializers should + * be run for parameter and return-type classes; in any other case it is + * null (see {@code loadClass}). */ private static MethodHandle getMethodHandle( ClassLoader schemaLoader, Class clazz, String methodName, @@ -1369,7 +1375,7 @@ private static Invocable init( MethodHandle handle = getMethodHandle(schemaLoader, clazz, methodName, - forValidator ? acc : null, + null, // or acc to initialize parameter classes; overkill. commute, resolvedTypes, retTypeIsOutParameter, isMultiCall) .asFixedArity(); MethodType mt = handle.type(); diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index e9f9cc31..f7bd8cdf 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -316,6 +316,45 @@ on some property that isn't readable under Java's default policy. Those examples should be changed to use a property that is normally readable, such as `java.version` or `org.postgresql.pljava.version`._ +### Class static initializers + +If validating (`check_function_bodies` is `on`), class static initializers +will be run at `CREATE FUNCTION` time in a bid to catch as many +issues early as possible. They will be run in the same access control +context that would be used at run time to call the function being declared. + +Classes besides the one containing the target method can also be loaded and +resolved: all those corresponding to the method's parameter and return types, +for example. Their initializers will not be forced to run by PL/Java's +validator, but will be run if the static initializer of the class being +validated accesses them in any of the ways that trigger class initialization +in Java. + +If a class contains several methods that would be given different +access control contexts (declared with different `trust` or +`language` attributes, say), the permissions available when the class +initializer runs will be those of whichever function is called first +in a given session (or, by the validator, for whichever `CREATE FUNCTION` +is seen first in a session). Therefore, when putting actions that require +permissions into a class's static initializer, those actions should require +only the common subset of permissions that the initializer could be run with +no matter which function is called or declared first. Actions that require +other specific permissions could be deferred until the first call of +a function known to be granted those permissions. + +Such actions can be left in the static initializer if a function granted +the needed permissions is known to always be the first one that the application +will call in any given session. Likewise, `provides`/`requires` ordering can +be used to generate the `CREATE FUNCTION` commands in the deployment descriptor +in such an order that the first function declared has the permissions the +class initializer will need. + +A way to ensure early detection of permission/policy problems could be to +deliberately put operations requiring the needed permissions, or even +simple `AccessController.checkPermission` calls, into a class's static +initializer. Problems with the policy granting insufficient permissions +can then be caught at `CREATE FUNCTION` time when validation is enabled. + ## Troubleshooting When in doubt what permissions may need to be granted in `pljava.policy` to run From ee62343711ed52d9df9b58ec915524736645390b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 30 Mar 2021 20:54:29 -0400 Subject: [PATCH 0869/1087] Avoid segfault from early _destroyJavaVM This was broken by bbbb75c. Fixing it restores reasonably graceful reporting when the implementation jar in pljava.module_path isn't found. When the API jar is the one that's not found, the reporting is not so good. "Error occurred during initialization of boot layer" is reported early and not being caught, followed by an unexplained exit. The message can be lost if the backend's standard error is not being collected for the log. --- pljava-so/src/main/c/Backend.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 073709be..bf8bc5c3 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -1320,12 +1320,13 @@ static void _destroyJavaVM(int status, Datum dummy) pqsigfunc saveSigAlrm; #endif - Invocation_pushInvocation(&ctx); + Invocation_pushBootContext(&ctx); if(sigsetjmp(recoverBuf, 1) != 0) { elog(DEBUG2, "needed to forcibly shut down the Java virtual machine"); s_javaVM = 0; + currentInvocation = 0; return; } @@ -1347,7 +1348,7 @@ static void _destroyJavaVM(int status, Datum dummy) #endif #else - Invocation_pushInvocation(&ctx); + Invocation_pushBootContext(&ctx); elog(DEBUG2, "shutting down the Java virtual machine"); JNI_destroyVM(s_javaVM); #endif From 0a704cbf3010bd05026fb12370a2f89f6b1b767d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 30 Mar 2021 20:48:40 -0400 Subject: [PATCH 0870/1087] Add message if Java VM calls exit() at start This seems to be the best that can be done if the initial module layer can't be constructed because one module's declared dependency can't be resolved. The VM just writes a message to standard error and immediately exits in that case. Use the on_proc_exit hook to make sure a logged message results. The VM's message to standard error only gets to the log if logging_collector is active. --- pljava-so/src/main/c/Backend.c | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index bf8bc5c3..69603c18 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -108,6 +108,7 @@ MemoryContext JavaMemoryContext; static JavaVM* s_javaVM = 0; static jclass s_Backend_class; +static bool s_startingVM; /* * GUC states @@ -635,7 +636,14 @@ static void initsequencer(enum initstage is, bool tolerant) /*FALLTHROUGH*/ case IS_JAVAVM_OPTLIST: + /* Register an on_proc_exit handler that destroys the VM if it has + * been started. It will also log a last-ditch message if the VM happens + * to rudely call exit() rather than returning a non-OK result. + */ + on_proc_exit(_destroyJavaVM, 0); + s_startingVM = true; JNIresult = initializeJavaVM(&optList); /* frees the optList */ + s_startingVM = false; if( JNI_OK != JNIresult ) { initstage = IS_MISC_ONCE_DONE; /* optList has been freed */ @@ -664,9 +672,6 @@ static void initsequencer(enum initstage is, bool tolerant) pqsignal(SIGTERM, pljavaDieHandler); pqsignal(SIGQUIT, pljavaQuickDieHandler); #endif - /* Register an on_proc_exit handler that destroys the VM - */ - on_proc_exit(_destroyJavaVM, 0); initstage = IS_SIGHANDLERS; /*FALLTHROUGH*/ @@ -1309,7 +1314,25 @@ static void terminationTimeoutHandler( */ static void _destroyJavaVM(int status, Datum dummy) { - if(s_javaVM != 0) + if(s_javaVM == 0) + { + if ( s_startingVM ) + { + ereport(FATAL, ( + errcode(ERRCODE_INTERNAL_ERROR), + errmsg("the Java VM exited while loading PL/Java"), + errdetail( + "The Java VM's exit forces this session to end."), + errhint( + "This has been known to happen when the entry in " + "pljava.module_path for the pljava-api jar has been " + "misspelled or the jar cannot be opened. If " + "logging_collector is active, there may be useful " + "information in the log.") + )); + } + } + else { Invocation ctx; #ifdef USE_PLJAVA_SIGHANDLERS From 7bea45b8689e40c3834faa083ce7b5a8241e83bc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 30 Mar 2021 22:01:42 -0400 Subject: [PATCH 0871/1087] Log an early DEBUG2 message with .so version That is already included in the "PL/Java loaded" message emitted later, but is of little help if something breaks before then, which is when you might especially like to know. --- pljava-so/src/main/c/Backend.c | 2 ++ pljava-so/src/main/c/InstallHelper.c | 8 ++++++- .../src/main/include/pljava/InstallHelper.h | 21 ++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 69603c18..87943dd0 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -924,6 +924,8 @@ void _PG_init() if ( IS_PLJAVA_INSTALLING == initstage ) return; /* creating handler functions will cause recursive call */ + InstallHelper_earlyHello(); + /* * Find the platform's path separator. Java knows it, but that's no help in * preparing the launch options before it is launched. PostgreSQL knows what diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 74a9735c..d0caf27f 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2021 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 @@ -519,6 +519,12 @@ char const *InstallHelper_defaultModulePath(char *pathbuf, char pathsep) return pathbuf; } +void InstallHelper_earlyHello() +{ + elog(DEBUG2, + "pljava-so-" SO_VERSION_STRING " built for (" PG_VERSION_STR ")"); +} + char *InstallHelper_hello() { char pathbuf[MAXPGPATH]; diff --git a/pljava-so/src/main/include/pljava/InstallHelper.h b/pljava-so/src/main/include/pljava/InstallHelper.h index 2edf837e..a9fbea6b 100644 --- a/pljava-so/src/main/include/pljava/InstallHelper.h +++ b/pljava-so/src/main/include/pljava/InstallHelper.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2021 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 @@ -129,8 +129,27 @@ extern bool pljavaViableXact(void); */ extern bool InstallHelper_shouldDeferInit(void); +/* + * Emit a debug message as early as possible with the native code's version + * and build information. A nicer message is produced later by hello and + * includes both the native and Java versions, but that's too late if something + * goes wrong first. + */ +extern void InstallHelper_earlyHello(void); + +/* + * Perform early setup needed on every start (properties, security policy, etc.) + * and also construct and return a string of native code, Java code, and JVM + * version and build information, to be included in the "PL/Java loaded" + * message. + */ extern char *InstallHelper_hello(void); +/* + * Called only when the loading is directly due to CREATE EXTENSION or LOAD, and + * not simply to service a PL/Java function; checks for, and populates or brings + * up to date, as needed, the sqlj schema and its contents. + */ extern void InstallHelper_groundwork(void); extern void InstallHelper_initialize(void); From e5ac9a836ecd415a78b9aab6fb5152330400efc0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 30 Apr 2021 19:34:15 -0400 Subject: [PATCH 0872/1087] Trigger no sqlj schema groundwork from validator A thinko in the validator handler sought to avoid one problem by inviting another; if the validator handler is called before initialization is complete, its preemptive early setting of pljavaLoadPath could be interpreted in the initsequencer as indicating an explicit LOAD or CREATE EXTENSION, calling for the sqlj schema to be populated or migrated. During a pg_upgrade, that schema might not be fully populated, foiling the schema recognition/migration code. So, rather than preemptively setting the load path, just avoid the exact problem the code was meant to avoid, in a more direct manner. Addresses #352. --- pljava-so/src/main/c/Backend.c | 47 +++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 87943dd0..fc0a8565 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -493,6 +493,17 @@ ASSIGNENUMHOOK(java_thread_pg_entry) * to succeed. * 3. From a GUC assign hook, if the user has updated a setting that might allow * initialization to succeed. It resumes from where it left off. + * 4. From the validator handler, if initialization isn't complete yet. That + * will definitely happen during pg_upgrade, which is a case where deferInit + * will have been set. The validator will then clear deferInit and try to get + * further in the init sequence. Importantly, pg_upgrade also sets + * check_function_bodies false, which limits the validator's work to a syntax + * check of the AS string. The validator therefore will not need to obtain a + * schemaLoader or do anything else that requires the sqlj schema to be fully + * populated (as, during pg_upgrade, it may not yet be). However, the + * validator handler must avoid any action that sets pljavaLoadPath, as a + * non-NULL value there would be treated below as case 1a, and trigger an + * attempt to set up the sqlj schema. * * In all cases, the sequence must progress as far as starting the VM and * initializing the PL/Java classes. In all cases except 1a, that's enough, @@ -1892,21 +1903,33 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) { Oid funcoid = PG_GETARG_OID(0); Invocation ctx; + Oid *oidSaveLocation = NULL; if (!CheckFunctionValidatorAccess(fcinfo->flinfo->fn_oid, funcoid)) PG_RETURN_VOID(); - *(trusted ? &pljavaTrustedOid : &pljavaUntrustedOid) = funcoid; /* - * The result of this call isn't particularly interesting (if it is false, - * I'll ne'er trust CheckFunctionValidatorAccess). But its side effect will - * be to make sure InstallHelper saves our library load path now, in case - * we decide we don't like this function, which would make the Oid we just - * stashed for it invalid, and frustrate getting the load path later. + * In the call handler, which could be called heavily, funcoid gets + * unconditionally stored to one of these two locations, rather than + * spending extra cycles deciding whether to store it or not. A validator + * will not be called as heavily, and can afford to check here whether + * an Oid needs to be stored or not. The situation to avoid is where + * funcoid gets stored here, as an Oid from which PL/Java's library path can + * be found, but the function then gets rejected by the validator, leaving + * the stored Oid invalid and useless for that purpose. Therefore, choose + * here whether and where to store it, but store it only within the PG_TRY + * block, and replace with InvalidOid again in the PG_CATCH. */ - if ( ! InstallHelper_isPLJavaFunction(funcoid, NULL, NULL) ) - elog(ERROR, "unexpected error validating PL/Java function"); - + if ( trusted ) + { + if ( InvalidOid == pljavaTrustedOid ) + oidSaveLocation = &pljavaTrustedOid; + } + else + { + if ( InvalidOid == pljavaUntrustedOid ) + oidSaveLocation = &pljavaUntrustedOid; + } if ( IS_PLJAVA_INSTALLING > initstage ) { @@ -1926,12 +1949,18 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) Invocation_pushInvocation(&ctx); PG_TRY(); { + if ( NULL != oidSaveLocation ) + *oidSaveLocation = funcoid; + Function_getFunction( funcoid, trusted, false, true, check_function_bodies); Invocation_popInvocation(false); } PG_CATCH(); { + if ( NULL != oidSaveLocation ) + *oidSaveLocation = InvalidOid; + Invocation_popInvocation(true); PG_RE_THROW(); } From 102b71d04ee7a52b939466b8d1f9172f5e521203 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 18 Jul 2021 11:17:20 -0400 Subject: [PATCH 0873/1087] Treat an autovacuum worker process specially PL/Java can be invoked in such a process if there is a functional index declared that uses a PL/Java function. Like the background worker case, there may not be a MyProcPort structure available. The "can't support background worker before PG 9.5" check was too tersely explained in 97ddb5e where it was added. While earlier PG versions maintained an AuthenticatedUserId value, there was no accessible getter for it before PG 9.5. 9.5 also added the two-argument form of GetUserNameFromId used here, but if that were the only problem, it could be handled (the former one-argument behavior was the same as specified here with second argument false). The inability to obtain the authenticated user id at all is what's hopeless in earlier versions. The limitation also applies in the autovacuum worker case. In versions prior to PG 9.5, it may be necessary to use false as the per-table autovacuum_enabled storage parameter on any table having a Java-based functional index, and run explicit VACUUM on such tables over a normal connection. Prior to PG 8.4, there was no such per-table setting, and the only workaround would be to forego autovacuum database-wide and use explicit VACUUM on some schedule. --- pljava-so/src/main/c/InstallHelper.c | 31 +++++++++---------- .../src/main/include/pljava/InstallHelper.h | 7 +++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 68203700..0d579862 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2021 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 @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -84,13 +85,11 @@ * for 9.3.0 through 9.3.2. * * One thing it's needed for is to avoid dereferencing MyProcPort in a - * background worker, where it's not set. Define BGW_HAS_NO_MYPROCPORT if that - * has to be (and can be) checked. + * background worker, where it's not set. */ #if PG_VERSION_NUM < 90300 || defined(_MSC_VER) && PG_VERSION_NUM < 90303 #define IsBackgroundWorker false #else -#define BGW_HAS_NO_MYPROCPORT #include #if defined(_MSC_VER) #include @@ -98,6 +97,10 @@ #endif #endif +#if PG_VERSION_NUM < 80300 +#define IsAutoVacuumWorkerProcess IsAutoVacuumProcess +#endif + #ifndef PLJAVA_SO_VERSION #error "PLJAVA_SO_VERSION needs to be defined to compile this file." #else @@ -137,11 +140,10 @@ bool pljavaViableXact() char *pljavaDbName() { -#ifdef BGW_HAS_NO_MYPROCPORT - char *shortlived; - static char *longlived; - if ( IsBackgroundWorker ) + if ( IsAutoVacuumWorkerProcess() || IsBackgroundWorker ) { + char *shortlived; + static char *longlived; if ( NULL == longlived ) { shortlived = get_database_name(MyDatabaseId); @@ -153,14 +155,12 @@ char *pljavaDbName() } return longlived; } -#endif return MyProcPort->database_name; } static char *origUserName() { -#ifdef BGW_HAS_NO_MYPROCPORT - if ( IsBackgroundWorker ) + if ( IsAutoVacuumWorkerProcess() || IsBackgroundWorker ) { #if PG_VERSION_NUM >= 90500 char *shortlived; @@ -175,13 +175,12 @@ static char *origUserName() #else ereport(ERROR, ( errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("PL/Java in a background worker not supported " + errmsg("PL/Java in a background or autovacuum worker not supported " "in this PostgreSQL version"), - errhint("PostgreSQL 9.5 is the first version to support " - "PL/Java in a background worker."))); + errhint("PostgreSQL 9.5 is the first version in which " + "such usage is supported."))); #endif } -#endif return MyProcPort->user_name; } @@ -440,7 +439,7 @@ char *pljavaFnOidToLibPath(Oid fnOid) bool InstallHelper_shouldDeferInit() { - return IsBackgroundWorker || IsBinaryUpgrade; + return IsBackgroundWorker || IsBinaryUpgrade || IsAutoVacuumWorkerProcess(); } bool InstallHelper_isPLJavaFunction(Oid fn) diff --git a/pljava-so/src/main/include/pljava/InstallHelper.h b/pljava-so/src/main/include/pljava/InstallHelper.h index c80cb8c8..d744f3c8 100644 --- a/pljava-so/src/main/include/pljava/InstallHelper.h +++ b/pljava-so/src/main/include/pljava/InstallHelper.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2021 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 @@ -73,8 +73,9 @@ extern bool InstallHelper_isPLJavaFunction(Oid fn); /* * Return the name of the current database, from MyProcPort ... don't free it. - * In a background worker, there's no MyProcPort, and the name is found another - * way and strdup'd in TopMemoryContext, it'll keep, don't bother freeing it. + * In a background or autovacuum worker, there's no MyProcPort, and the name is + * found another way and strdup'd in TopMemoryContext. It'll keep; don't bother + * freeing it. */ extern char *pljavaDbName(); From 4b08bbb6937dce203bd7bedbe477c1cc1df21f80 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Sep 2021 12:32:29 -0400 Subject: [PATCH 0874/1087] Regularize pg_type OID symbols postgres/postgres@f90149e removed some special-case spellings of pg_type oid symbols, in favor of sticking with the predictable ones genbki.pl comes up with. That changed the spelling of one used here. --- pljava-so/src/main/c/TypeOid.c | 4 ++-- pljava-so/src/main/c/type/SQLXMLImpl.c | 8 ++++---- pljava-so/src/main/include/pljava/pljava.h | 9 ++++++++- .../main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java | 6 +++--- .../main/java/org/postgresql/pljava/jdbc/TypeOid.java | 4 ++-- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/pljava-so/src/main/c/TypeOid.c b/pljava-so/src/main/c/TypeOid.c index 2cb01eff..af6d1556 100644 --- a/pljava-so/src/main/c/TypeOid.c +++ b/pljava-so/src/main/c/TypeOid.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2021 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 @@ -52,6 +52,6 @@ JNIEXPORT void JNICALL Java_org_postgresql_pljava_jdbc_TypeOid__1dummy(JNIEnv * CONFIRMCONST(BPCHAROID); #if PG_VERSION_NUM >= 90100 - CONFIRMCONST(PGNODETREEOID); + CONFIRMCONST(PG_NODE_TREEOID); #endif } diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 5fc73860..afc7ebfd 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2021 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 @@ -51,7 +51,7 @@ static bool _SQLXML_canReplaceType(Type self, Type other) Type_getOid(other) == XMLOID || #endif #if PG_VERSION_NUM >= 90100 - Type_getOid(other) == PGNODETREEOID || /* a synthetic rendering */ + Type_getOid(other) == PG_NODE_TREEOID || /* a synthetic rendering */ #endif Type_getOid(other) == TEXTOID; } @@ -140,8 +140,8 @@ static Type _SQLXML_obtain(Oid typeId) switch ( typeId ) { #if PG_VERSION_NUM >= 90100 - case PGNODETREEOID: - allowedId = PGNODETREEOID; + case PG_NODE_TREEOID: + allowedId = PG_NODE_TREEOID; synthetic = true; cache = &pgNodeTreeInstance; break; diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index e9b82373..97f1785f 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -82,6 +82,13 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); #define PG_INT32_MAX (0x7FFFFFFF) #endif +/* + * This symbol was spelled without the underscores prior to PG 14. + */ +#if PG_VERSION_NUM < 140000 +#define PG_NODE_TREEOID PGNODETREEOID +#endif + extern void* mainThreadId; extern bool pljavaEntryFence(JNIEnv* env); extern JNIEnv* currentJNIEnv; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index f182dbbe..fa1c5727 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2021 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 @@ -172,7 +172,7 @@ /* ... for SQLXMLImpl.Readable.Synthetic */ import org.postgresql.pljava.internal.VarlenaXMLRenderer; -import static org.postgresql.pljava.jdbc.TypeOid.PGNODETREEOID; +import static org.postgresql.pljava.jdbc.TypeOid.PG_NODE_TREEOID; public abstract class SQLXMLImpl implements SQLXML { @@ -1023,7 +1023,7 @@ private static VarlenaXMLRenderer xmlRenderer( { switch ( oid ) { - case PGNODETREEOID: return new PgNodeTreeAsXML(vwi); + case PG_NODE_TREEOID: return new PgNodeTreeAsXML(vwi); default: throw new SQLNonTransientException( "no synthetic SQLXML support for Oid " + oid, "0A000"); diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java index 6d78ac2b..6b95fa05 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/TypeOid.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -65,7 +65,7 @@ public class TypeOid * Added in 2019. The numeric constant will be used, but no need is foreseen * for an Oid-reference constant. */ - public static final int PGNODETREEOID = 194; + public static final int PG_NODE_TREEOID = 194; /* * Before Java 8 with the @Native annotation, a class needs at least one From 9baf3b7bf24b54a377b3070b047fec8be06ca8e4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 21 Sep 2021 19:20:24 -0400 Subject: [PATCH 0875/1087] Accommodate change to TOAST pointer format With the advent of selectable TOAST compression methods in postgres/postgres@bbe0a81, there's a new macro for extracting the compressed size. --- pljava-so/src/main/c/VarlenaWrapper.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index d3f5e3c0..65b904fe 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2021 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 @@ -73,6 +73,10 @@ do { \ #define _VL_TYPE struct varlena * #endif +#if PG_VERSION_NUM < 140000 +#define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) ((toast_pointer).va_extsize) +#endif + #define INITIALSIZE 1024 static jclass s_VarlenaWrapper_class; @@ -219,7 +223,7 @@ jobject pljava_VarlenaWrapper_Input( */ struct varatt_external toast_pointer; VARATT_EXTERNAL_GET_POINTER(toast_pointer, vl); - parked = toast_pointer.va_extsize + VARHDRSZ; + parked = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) + VARHDRSZ; if ( (actual >> 1) < parked ) /* not compressed enough to bother */ goto justDetoastEagerly; vl = detoast_external_attr(vl); /* fetch without decompressing */ From be8cbb35073c982614e42f184fc5f3edc9b21cd9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 21 Sep 2021 19:57:35 -0400 Subject: [PATCH 0876/1087] Goodbye to ErrorData.show_funcname postgres/postgres@3174d69 eliminated code that supported the v2 frontend/backend protocol, which was superseded by v3 in PG 7.4. The show_funcname flag was a vestige of the v2 support. Deprecate the PL/Java method that returned it, which will always return false when on PG 14 or later. --- pljava-so/src/main/c/type/ErrorData.c | 9 +++++++-- .../java/org/postgresql/pljava/internal/ErrorData.java | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/type/ErrorData.c b/pljava-so/src/main/c/type/ErrorData.c index b8fb90d0..85116fb9 100644 --- a/pljava-so/src/main/c/type/ErrorData.c +++ b/pljava-so/src/main/c/type/ErrorData.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -243,7 +243,12 @@ JNIEXPORT jboolean JNICALL Java_org_postgresql_pljava_internal_ErrorData__1isSho { Ptr2Long p2l; p2l.longVal = _this; - return (jboolean)((ErrorData*)p2l.ptrVal)->show_funcname; + return +#if PG_VERSION_NUM < 140000 + (jboolean)((ErrorData*)p2l.ptrVal)->show_funcname; +#else + JNI_FALSE; +#endif } /* diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java index 3030b79d..782aee0c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -119,7 +119,13 @@ public boolean isOutputToClient() /** * Returns true if funcname inclusion is set + * @deprecated The property queried by this method was only used + * in PostgreSQL when communicating with old clients over the v2 + * frontend/backend protocol, superseded in PostgreSQL 7.4. In PG 14 + * and later, there is no such property, and this method will always + * return false. */ + @Deprecated public boolean isShowFuncname() { synchronized(Backend.THREADLOCK) From 2122a5b0a0965d61224a21f1f7aab407629af501 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Sep 2021 14:09:41 -0400 Subject: [PATCH 0877/1087] No blind strlen after pg_do_encoding_conversion Those calls have been in there since 25b81a2, apparently having missed the "don't do that" comment that postgres/postgres@3be2448 added, first released in PG 8.4. --- pljava-so/src/main/c/type/String.c | 34 +++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index 094f257a..44e4c699 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB - Thomas Hallgren + * Chapman Flack */ #include "pljava/type/String_priv.h" #include "pljava/HashMap.h" @@ -127,7 +131,12 @@ jstring String_createJavaString(text* t) { utf8 = (char*)pg_do_encoding_conversion((unsigned char*)src, (int)srcLen, s_server_encoding, PG_UTF8); - srcLen = strlen(utf8); + /* pg_do_encoding_conversion may return the source argument + * unchanged in more circumstances than you'd expect. As the source + * argument isn't NUL-terminated, don't call strlen on it. + */ + if (utf8 != src) + srcLen = strlen(utf8); } bytebuf = JNI_newDirectByteBuffer(utf8, srcLen); charbuf = JNI_callObjectMethodLocked(s_CharsetDecoder_instance, @@ -159,7 +168,11 @@ jstring String_createJavaStringFromNTS(const char* cp) { utf8 = (char*)pg_do_encoding_conversion((unsigned char*)cp, (int)sz, s_server_encoding, PG_UTF8); - sz = strlen(utf8); + /* Here the source is NUL-terminated, so calling strlen on it + * would be safe, but unnecessary all the same. + */ + if ( utf8 != cp ) + sz = strlen(utf8); } bytebuf = JNI_newDirectByteBuffer((void *)utf8, sz); charbuf = JNI_callObjectMethodLocked(s_CharsetDecoder_instance, @@ -200,7 +213,12 @@ text* String_createText(jstring javaString) { denc = (char*)pg_do_encoding_conversion( (unsigned char*)denc, (int)dencLen, PG_UTF8, s_server_encoding); - dencLen = strlen(denc); + /* pg_do_encoding_conversion may return the source argument + * unchanged in more circumstances than you'd expect. As the source + * argument isn't NUL-terminated, don't call strlen on it. + */ + if (denc != sid.data) + dencLen = strlen(denc); } varSize = dencLen + VARHDRSZ; From 88a7c88b3b66e0c733bf6323d476491b07051ee5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Sep 2021 10:56:14 -0400 Subject: [PATCH 0878/1087] Backport b747a4c, add an SQL_ASCII encoding. It turns out one of the cases where pg_do_encoding_conversion returns the argument unchanged is when SQL_ASCII is the server encoding. The result was then being blindly treated as if it were UTF-8. That's a pretty broken behavior, a good argument for backporting this from 1.6. --- backported comment --- This is a principled Java take on the PostgreSQL definition of SQL_ASCII as an encoding where the seven-bit ASCII values are themselves and the remaining eight-bit values are who-knows-what. It isn't appropriate to just copy byte values with no conversion, as that would amount to saying we know the values correspond to LATIN-1, which would be lying. Java strings are by definition Unicode, so it's not ok to go stuffing code points in that do not mean what Unicode defines those code points to mean. What this decoder does is decode the seven-bit ASCII values as themselves, and decode each eight-bit value into a pair of Unicode noncharacters, one from the range u+fdd8 to u+fddf, followed by one from u+fde0 to u+fdef, where the first one's four low bits are the four high bits of the original value, and the second has the low four. The encoder transparently reverses that. Those noncharacter code points are permanently defined in Unicode to have no glyphs, no correspondence to specific characters, and no interesting properties. Implementing this charset allows PL/Java code to work usefully in a database with SQL_ASCII encoding, when the expectation is that whatever the code needs to recognize, act on, or edit will be in ASCII, and any non-ASCII content can be passed along uninterpreted and unchanged. --- .../postgresql/pljava/internal/SQL_ASCII.java | 227 ++++++++++++++++++ .../java.nio.charset.spi.CharsetProvider | 1 + 2 files changed, 228 insertions(+) create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java create mode 100644 pljava/src/main/resources/META-INF/services/java.nio.charset.spi.CharsetProvider diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java b/pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java new file mode 100644 index 00000000..1da8d6f2 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SQL_ASCII.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2020-2021 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.internal; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.spi.CharsetProvider; + +import static java.util.Collections.singletonList; +import java.util.Iterator; +import java.util.List; + +/** + * An {@code SQL_ASCII}, a/k/a {@code X-PGSQL_ASCII}, "character set". + *

    + * This is a principled Java take on the PostgreSQL definition of + * SQL_ASCII as an encoding where the seven-bit ASCII values are + * themselves and the remaining eight-bit values are who-knows-what. + * It isn't appropriate to just copy byte values with no conversion, + * as that would amount to saying we know the values correspond to + * LATIN-1, which would be lying. Java strings are by definition Unicode, + * so it's not ok to go stuffing code points in that do not mean what + * Unicode defines those code points to mean. + *

    + * What this decoder does is decode the seven-bit ASCII values as + * themselves, and decode each eight-bit value into a pair of Unicode + * noncharacters, one from the range u+fdd8 to u+fddf, followed by one + * from u+fde0 to u+fdef, where the first one's four low bits are the + * four high bits of the original value, and the second has the low four. + * The encoder transparently reverses that. + *

    + * Those noncharacter code points are permanently defined in Unicode + * to have no glyphs, no correspondence to specific characters, and + * no interesting properties. Implementing this charset allows PL/Java + * code to work usefully in a database with SQL_ASCII encoding, when the + * expectation is that whatever the code needs to recognize, act on, or + * edit will be in ASCII, and any non-ASCII content can be passed along + * uninterpreted and unchanged. + */ +class SQL_ASCII extends Charset +{ + static class Holder + { + static final List s_list = + singletonList((Charset)new SQL_ASCII()); + } + + + static final Charset US_ASCII; + + static + { + try + { + US_ASCII = Charset.forName("US-ASCII"); + } + catch ( IllegalArgumentException e ) // I'll take this chance + { + throw new ExceptionInInitializerError(e); + } + } + + public static class Provider extends CharsetProvider + { + static final String s_canonName = "X-PGSQL_ASCII"; + static final String[] s_aliases = { "SQL_ASCII" }; + + @Override + public Charset charsetForName(String charsetName) + { + if ( s_canonName.equalsIgnoreCase(charsetName) ) + return Holder.s_list.get(0); + for ( String s : s_aliases ) + if ( s.equalsIgnoreCase(charsetName) ) + return Holder.s_list.get(0); + return null; + } + + @Override + public Iterator charsets() + { + return Holder.s_list.iterator(); + } + } + + + private SQL_ASCII() + { + super(Provider.s_canonName, Provider.s_aliases); + } + + @Override + public boolean contains(Charset cs) + { + return this.equals(cs) || US_ASCII.equals(cs); + } + + @Override + public CharsetDecoder newDecoder() + { + return new Decoder(); + } + + @Override + public CharsetEncoder newEncoder() + { + return new Encoder(); + } + + + static class Decoder extends CharsetDecoder + { + Decoder() + { + super(Holder.s_list.get(0), 1.002f, 2.0f); + } + + @Override + protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) + { + int ipos = in.position(); + int opos = out.position(); + int ilim = in.limit(); + int olim = out.limit(); + + for ( ; ipos < ilim ; ++ ipos ) + { + char b = (char)(0xff & in.get(ipos)); + + if ( b < 128 ) + { + if ( opos == olim ) + { + in.position(ipos); + out.position(opos); + return CoderResult.OVERFLOW; + } + out.put(opos++, b); + } + else + { + if ( opos + 1 >= olim ) + { + in.position(ipos); + out.position(opos); + return CoderResult.OVERFLOW; + } + out.put(opos++, (char)(0xFDD0 | (b >> 4))); + out.put(opos++, (char)(0xFDE0 | (b & 0xf))); + } + } + in.position(ipos); + out.position(opos); + return CoderResult.UNDERFLOW; + } + } + + static class Encoder extends CharsetEncoder + { + Encoder() + { + super(Holder.s_list.get(0), 0.998f, 1.0f); + } + + @Override + protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) + { + int ipos = in.position(); + int opos = out.position(); + int ilim = in.limit(); + int olim = out.limit(); + + for ( ; ipos < ilim ; ++ ipos ) + { + if ( opos == olim ) + { + in.position(ipos); + out.position(opos); + return CoderResult.OVERFLOW; + } + + char c = in.get(ipos); + + if ( '\uFDD8' <= c && c < '\uFDE0' ) + { + if ( ipos + 1 == ilim ) + break; + + char d = in.get(ipos + 1); + + if ( '\uFDE0' > d || d > '\uFDEF' ) + { + in.position(ipos); + out.position(opos); + return CoderResult.malformedForLength(2); + } + c = (char)(( (c & 0xf) << 4 ) | (d & 0xf)); + ++ ipos; + } + else if ( c >= 128 ) + { + in.position(ipos); + out.position(opos); + return CoderResult.unmappableForLength(1); + } + out.put(opos++, (byte)c); + } + in.position(ipos); + out.position(opos); + return CoderResult.UNDERFLOW; + } + } +} diff --git a/pljava/src/main/resources/META-INF/services/java.nio.charset.spi.CharsetProvider b/pljava/src/main/resources/META-INF/services/java.nio.charset.spi.CharsetProvider new file mode 100644 index 00000000..cf35159c --- /dev/null +++ b/pljava/src/main/resources/META-INF/services/java.nio.charset.spi.CharsetProvider @@ -0,0 +1 @@ +org.postgresql.pljava.internal.SQL_ASCII$Provider From ff555df94665aad9e71692c1d20575707f4e2602 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Sep 2021 12:21:38 -0400 Subject: [PATCH 0879/1087] Don't get in the way of Charset.forName() A minimal tweak to the 'trusted' behavior so forName can succeed. See PL/Java 1.6 for how it should be done. This is just to get one backported feature working in an otherwise little-maintained version. --- .../postgresql/pljava/internal/Backend.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index c6fc1541..15245b19 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2015 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -16,8 +16,10 @@ import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; +import java.security.AccessController; import java.security.Permission; import java.sql.SQLException; import java.util.PropertyPermission; @@ -188,6 +190,9 @@ else if(perm instanceof PropertyPermission) */ private static final SecurityManager s_trustedSecurityManager = new PLJavaSecurityManager() { + final URL m_pljavaJar = Backend.class.getProtectionDomain() + .getCodeSource().getLocation(); + void assertPermission(Permission perm) { if(perm instanceof FilePermission) @@ -220,6 +225,21 @@ void assertPermission(Permission perm) return; fileDir = fileDir.getParentFile(); } + + // Adding a CharsetProvider to our own jar means that system + // code (Charset.forName and friends, calling ServiceLoader + // under their own protection domain) has to read it. + // + try + { + if ( new URL("file", "", fileName) + .equals(m_pljavaJar) ) + { + AccessController.checkPermission(perm); + return; + } + } + catch ( MalformedURLException e ) { } } throw new SecurityException(perm.getActions() + " on " + perm.getName()); } From 717d0879dec4e97e6ffe0b785dc497b20401fe48 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 23 Sep 2021 14:41:07 -0400 Subject: [PATCH 0880/1087] Backport 46715ab: String.c SQL_ASCII special case String.c now treats both UTF-8 and SQL_ASCII server encodings as special cases, where the Java codec of the same name will be used directly on the native bytes, while all other encodings are handled as two-step conversions using the PostgreSQL native conversion between the server encoding and UTF-8, and the Java UTF-8 codec. Future project: simplify all this by assuming (as SQLXML does) that there is a Java charset corresponding to the server encoding, found either by name match or by manual configuration (-Dorg.postgresql.server.encoding in pljava.vmoptions), so that all supported server encodings are handled the same way. The current addition of SQL_ASCII as a Java charset is a step down that path. Perhaps there could also be a CharsetProvider that "provides" any missing codecs by wrapping the native ones. The future work probably won't be backported to 1.5 though. --- pljava-so/src/main/c/type/String.c | 31 +++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index 44e4c699..d2056960 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -395,10 +395,14 @@ void String_initialize(void) static void String_initialize_codec() { + /* + * Wondering why this function doesn't bother deleting its many local refs? + * The call is wrapped in pushLocalFrame/popLocalFrame in the caller. + */ jmethodID string_intern = PgObject_getJavaMethod(s_String_class, "intern", "()Ljava/lang/String;"); jstring empty = JNI_newStringUTF( ""); - jstring u8Name = JNI_newStringUTF( "UTF-8"); + char const *charset_name; jclass charset_class = PgObject_getJavaClass("java/nio/charset/Charset"); jmethodID charset_forName = PgObject_getStaticJavaMethod(charset_class, "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;"); @@ -406,8 +410,6 @@ static void String_initialize_codec() "newDecoder", "()Ljava/nio/charset/CharsetDecoder;"); jmethodID charset_newEncoder = PgObject_getJavaMethod(charset_class, "newEncoder", "()Ljava/nio/charset/CharsetEncoder;"); - jobject u8cs = JNI_callStaticObjectMethod(charset_class, charset_forName, - u8Name); jclass decoder_class = PgObject_getJavaClass("java/nio/charset/CharsetDecoder"); jclass encoder_class = @@ -420,11 +422,28 @@ static void String_initialize_codec() jfieldID underflow = PgObject_getStaticJavaField(result_class, "UNDERFLOW", "Ljava/nio/charset/CoderResult;"); jclass buffer_class = PgObject_getJavaClass("java/nio/Buffer"); + jobject servercs; + + s_server_encoding = GetDatabaseEncoding(); + + if ( PG_SQL_ASCII == s_server_encoding ) + { + charset_name = "X-PGSQL_ASCII"; + s_two_step_conversion = false; + } + else + { + charset_name = "UTF-8"; + s_two_step_conversion = PG_UTF8 != s_server_encoding; + } + + servercs = JNI_callStaticObjectMethodLocked(charset_class, + charset_forName, JNI_newStringUTF(charset_name)); s_CharsetDecoder_instance = - JNI_newGlobalRef(JNI_callObjectMethod(u8cs, charset_newDecoder)); + JNI_newGlobalRef(JNI_callObjectMethod(servercs, charset_newDecoder)); s_CharsetEncoder_instance = - JNI_newGlobalRef(JNI_callObjectMethod(u8cs, charset_newEncoder)); + JNI_newGlobalRef(JNI_callObjectMethod(servercs, charset_newEncoder)); s_CharsetDecoder_decode = PgObject_getJavaMethod(decoder_class, "decode", "(Ljava/nio/ByteBuffer;)Ljava/nio/CharBuffer;"); s_CharsetEncoder_encode = PgObject_getJavaMethod(encoder_class, "encode", @@ -450,7 +469,5 @@ static void String_initialize_codec() s_the_empty_string = JNI_newGlobalRef( JNI_callObjectMethod(empty, string_intern)); - s_server_encoding = GetDatabaseEncoding(); - s_two_step_conversion = PG_UTF8 != s_server_encoding; uninitialized = false; } From f82b3f1b5914deba95f7fbb156654d6726411e7c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 1 Feb 2020 00:01:55 -0500 Subject: [PATCH 0881/1087] Backport d081a8c: update character encodings doc --- src/site/markdown/use/charsets.md | 37 ++++++++++++++++++++++-------- src/site/markdown/use/variables.md | 12 ++++------ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/site/markdown/use/charsets.md b/src/site/markdown/use/charsets.md index f0f41cb5..d22a86eb 100644 --- a/src/site/markdown/use/charsets.md +++ b/src/site/markdown/use/charsets.md @@ -39,11 +39,26 @@ The encoding `SQL_ASCII`, as described [in the PostgreSQL documentation][mbc], ### Using PL/Java with server encoding `SQL_ASCII` -When the server encoding is `SQL_ASCII`, the only safe way for PL/Java to treat -it is as strict ASCII, throwing exceptions if asked to convert any string -involving characters outside of ASCII. - -If PL/Java must be used with `SQL_ASCII` as the server encoding, the cases are +Java strings are Unicode by definition; PL/Java must not create strings where +some of the characters have their Unicode meanings while others mean something +else. PL/Java does supply a `Charset` with encoder and decoder for `SQL_ASCII`, +which behaves as follows: + +* Encoded bytes in the ASCII range map to the corresponding Unicode characters. +* Other encoded bytes are stuffed, two `char`s for each byte, into a range of + codepoints Unicode defines as permanently unassigned. For those codepoints, + `Character.getType` returns `UNASSIGNED`, `Character.getName` returns null, + `Character.UnicodeScript.of` returns `UNKNOWN`, and they will not match + patterns for letters, digits, punctuation, or generally anything else + interesting (other than `\p{Cn}`, the exact test for noncharacters). + +The mapping is transparently reversed when such a Java string is returned +to PostgreSQL. With this convention, Java code can work usefully with +`SQL_ASCII` encoded data, matching and manipulating the ASCII parts, while +treating the non-defined subsequences as opaque and returning them to PostgreSQL +unchanged. + +If PL/Java is used with `SQL_ASCII` as the server encoding, the cases are (by increasing complexity): 0. The database contains no non-ASCII data (or none that will be touched @@ -52,15 +67,19 @@ If PL/Java must be used with `SQL_ASCII` as the server encoding, the cases are 0. The database contains non-ASCII data all known to be in one standard encoding. It would be simplest for the database to be recreated with this encoding selected, but that may be impractical for various reasons. - In that case, this can be handled in the same way as the next case. + In that case, this can be handled in the same way as the next case, or + PL/Java can be 'lied to' about the server encoding by including a + `-Dorg.postgresql.server.encoding=...` in `pljava.vmoptions` that names + the known correct encoding instead. 0. The database contains non-ASCII data in _more than one_ encoding, with the application somehow knowing which encoding is used where. That is completely possible because `SQL_ASCII` does not guarantee or validate anything (which means it can also happen over time without being intended). - The rigorous approach in this case is to write Java code expecting and - returning `bytea` and some indication of the encoding to be used, and - perform explicit conversions. + Java code can find regions of strings that match the pattern + `(?:[\ufdd8-\ufddf][\ufde0-\ufdef])++` and pass those regions back through + the `SQL_ASCII` encoder, and then the decoder for whatever other encoding + it determines should apply. ## Using PL/Java with standard (not `SQL_ASCII`) encodings other than `UTF8` diff --git a/src/site/markdown/use/variables.md b/src/site/markdown/use/variables.md index 617cbebb..f349058f 100644 --- a/src/site/markdown/use/variables.md +++ b/src/site/markdown/use/variables.md @@ -10,14 +10,11 @@ These PostgreSQL configuration variables can influence PL/Java's operation: `server_encoding` : Another non-PL/Java variable, this affects all text/character strings exchanged between PostgreSQL and Java. `UTF8` as the database and server - encoding is _strongly_ recommended. If a different encoding is used, it + encoding is strongly recommended. If a different encoding is used, it should be any of the available _fully defined_ character encodings. In - particular, the PostgreSQL pseudo-encoding `SQL_ASCII` (which means - "characters within ASCII are ASCII, others are no-one-knows-what") will - _not_ work well with PL/Java, raising exceptions whenever strings contain - non-ASCII characters. (PL/Java can still be used in such a database, but - the application code needs to know what it's doing and use the right - conversion functions where needed.) + particular, the PostgreSQL pseudo-encoding `SQL_ASCII` does not fully + define what any values outside ASCII represent; it is usable, but + [subject to limitations][sqlascii]. `pljava.classpath` : The class path to be passed to the Java application class loader. There @@ -123,3 +120,4 @@ These PostgreSQL configuration variables can influence PL/Java's operation: [jow]: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html [jou]: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html [vmop]: ../install/vmoptions.html +[sqlascii]: charsets.html#Using_PLJava_with_server_encoding_SQL_ASCII From 9372ae21b530378d5c4fbe304bfefea3768df2f6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 25 Sep 2021 19:27:42 -0400 Subject: [PATCH 0882/1087] Detect if running in a post-JEP411 JVM PL/Java 1.5's build system already breaks as of Java 15, so it won't be necessary to add SuppressWarnings("removal") anywhere in the 1.5 sources. It also will not start by default on a JVM later than 17, because it does not use -Djava.security.manager=allow among the invocation options by default. The logic here is only to detect if it has been run with that option explicitly added (in pljava.vmoptions) and is running on a Java version where the functionality has been gutted. --- .../postgresql/pljava/internal/Backend.java | 19 ++++++++++++++++++- src/site/markdown/install/install.md.vm | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index 15245b19..66b62d63 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -22,6 +22,7 @@ import java.security.AccessController; import java.security.Permission; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.util.PropertyPermission; import java.util.logging.Level; import java.util.logging.Logger; @@ -300,19 +301,35 @@ public static boolean isCreatingExtension() * function. */ private static void setTrusted(boolean trusted) + throws SQLException { + SecurityManager mgr = + trusted ? s_trustedSecurityManager : s_untrustedSecurityManager; s_inSetTrusted = true; try { Logger log = Logger.getAnonymousLogger(); if(log.isLoggable(Level.FINER)) log.finer("Using SecurityManager for " + (trusted ? "trusted" : "untrusted") + " language"); - System.setSecurityManager(trusted ? s_trustedSecurityManager : s_untrustedSecurityManager); + System.setSecurityManager(mgr); + if ( System.getSecurityManager() == mgr ) + return; } + catch ( UnsupportedOperationException e ) { } finally { s_inSetTrusted = false; } + if ( ! trusted ) + return; + + throw new SQLFeatureNotSupportedException( + "PL/Java 1.5, when running on this version of Java, cannot " + + "execute trusted functions. Possible solutions: (1) upgrade to " + + "a post-JEP411 PL/Java version; (2) downgrade Java to a version " + + "before the functionality was removed; (3) redeclare functions " + + "using the 'javau' (untrusted) PL/Java language. For more: " + + "https://github.com/tada/pljava/wiki/JEP-411", "0A000"); } /** diff --git a/src/site/markdown/install/install.md.vm b/src/site/markdown/install/install.md.vm index 52c9a1a4..ab61f646 100644 --- a/src/site/markdown/install/install.md.vm +++ b/src/site/markdown/install/install.md.vm @@ -217,6 +217,9 @@ and are subject to very few restrictions at runtime. Functions that specify `USAGE` permission `ON LANGUAGE java`. They run under a security manager that denies access to the host filesystem. +__Note: For implications when running on Java 17 or later, +please see [JEP 411][jep411]__. + PostgreSQL, by default, would grant `USAGE` to `PUBLIC` on the `java` language, but PL/Java takes a more conservative approach on a new installation. In keeping with the principle of least privilege, @@ -393,3 +396,5 @@ $h3 PostgreSQL superuser, OS user distinct from the user running postgres In this case, simply place the files in any location where you can make them readable by the user running `postgres`, and set the `pljava.*` variables accordingly. + +[jep411]: https://github.com/tada/pljava/wiki/JEP-411 From 01446c9ef8f5db50eaa7eec79e152c08f409b945 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 26 Sep 2021 13:46:12 -0400 Subject: [PATCH 0883/1087] Un-orphan the runner-supplied PostgreSQL It seems the GitHub Actions virtual-environments team made a decision to remove repositories other than Ubuntu default (even repositories that supplied packages the runner provides). So in order to be able to install related dev packages, the repository has to be added to a .list file. Discussion: actions/virtual-environments#3397 --- .github/workflows/ci-runnerpg.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 59d45f92..6c550d9a 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -59,6 +59,12 @@ jobs: - name: Obtain PG development files (Ubuntu, PGDG) if: ${{ 'Linux' == runner.os }} run: | + 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-13 libkrb5-dev From dc6527422fddccab106f1b5603f3d3d487d1d82e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 26 Sep 2021 14:41:50 -0400 Subject: [PATCH 0884/1087] Unclear how the wrong commit got cherry-picked If this were not the repository of record, I would force-push this. --- .github/workflows/ci-runnerpg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 6c550d9a..13fffab4 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -62,7 +62,7 @@ jobs: echo \ deb \ http://apt.postgresql.org/pub/repos/apt \ - "$(lsb-release -cs)-pgdg" \ + "$(lsb_release -cs)-pgdg" \ main | sudo tee /etc/apt/sources.list.d/pgdg.list sudo apt-get update From 49454436839d093e8ac7465cfd6d59382a85f8d7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 26 Sep 2021 16:54:15 -0400 Subject: [PATCH 0885/1087] More CI break-fixing pgjdbc-ng (only needed for the CI scripts) released a bad uber-jar in 0.8.8 (impossibl/pgjdbc-ng#543), fixed in 0.8.9. Exclude the 0.8.8 version (which the Travis build used, and failed; does Travis use a locally-cached Maven repo that falls behind?). For that matter, just turn off the Travis builds for the indefinite future. Having them run sometimes and not other times based on when Travis thinks tada has "enough credits" isn't helpful. AppVeyor builds are failing only with JDK 10; the error suggests it may not include a trust anchor needed to verify the Maven repository cert (though GHA Ubuntu builds with JDK 9 work?). Exclude the AppVeyor builds with JDK 10. --- .travis.yml | 1 + appveyor.yml | 12 ++++++------ pljava-packaging/pom.xml | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 542a978e..307b5b7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +if: false language: minimal os: - linux diff --git a/appveyor.yml b/appveyor.yml index 657f80ce..03422f38 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,9 +6,9 @@ environment: # - SYS: MINGW # JDK: 9 # PG: 12 - - SYS: MINGW - JDK: 10 - PG: 12 +# - SYS: MINGW +# JDK: 10 +# PG: 12 - SYS: MINGW JDK: 11 PG: 12 @@ -39,9 +39,9 @@ environment: - SYS: MSVC JDK: 11 PG: 12 - - SYS: MSVC - JDK: 10 - PG: 12 +# - SYS: MSVC +# JDK: 10 +# PG: 12 # - SYS: MSVC # JDK: 9 # PG: 12 diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 95343656..c6b06181 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -76,7 +76,7 @@ com.impossibl.pgjdbc-ng pgjdbc-ng-all - [0.8.4,) + [0.8.4,0.8.8),(0.8.8,) From edee802337dd5ea1b0a90152411dc694ed4097cb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 24 Sep 2021 13:54:25 -0400 Subject: [PATCH 0886/1087] Preserve 1.5's buildability back to PG 8.2 --- pljava-so/src/main/c/InstallHelper.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 0d579862..13f52167 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -46,6 +45,12 @@ #define GetNamespaceOid(k1) GetSysCacheOid(NAMESPACENAME, k1, 0, 0, 0) #endif +#if PG_VERSION_NUM >= 80300 +#include +#else +#define IsAutoVacuumWorkerProcess IsAutoVacuumProcess +#endif + #include "pljava/InstallHelper.h" #include "pljava/Backend.h" #include "pljava/Function.h" @@ -97,10 +102,6 @@ #endif #endif -#if PG_VERSION_NUM < 80300 -#define IsAutoVacuumWorkerProcess IsAutoVacuumProcess -#endif - #ifndef PLJAVA_SO_VERSION #error "PLJAVA_SO_VERSION needs to be defined to compile this file." #else From 0804e80134febf70207a798e4c379a65201cc7b9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 27 Sep 2021 11:36:11 -0400 Subject: [PATCH 0887/1087] Tighten deserialization of Identifier I don't have any PL/Java application in mind that would involve serializing identifiers (or Principals containing them), but the Subject class requires Principals that are serializable, hence this make-work. (It might be simpler in the long run to implement another DomainCombiner that doesn't use the Subject class.) The only serialVersionUID to change will be for Identifier itself. The other changes merely tighten exclusions of cases that shouldn't happen anyway. I'm going to switch to a policy, where a serialVersionUID must change, of changing to a small integer that will simply increment. The default calculation makes ugly numbers and offers no advantage if there's a compatibility break anyway. --- .../postgresql/pljava/sqlgen/Lexicals.java | 24 ++++++++-- pljava-api/src/test/java/LexicalsTest.java | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index f6335e13..6be147df 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -440,7 +440,7 @@ public static Identifier.Simple identifierFrom(Matcher m) */ public static abstract class Identifier implements Serializable { - private static final long serialVersionUID = 8963213648466350967L; + private static final long serialVersionUID = 1L; Identifier() { } // not API @@ -498,6 +498,7 @@ public String toString() private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); Class c = getClass(); if ( c != Simple.class && c != Foldable.class && c != Folding.class && c != Pseudo.class && c != Operator.class @@ -839,6 +840,13 @@ private void readObject(ObjectInputStream in) String diag = checkLength(m_nonFolded); if ( null != diag ) throw new InvalidObjectException(diag); + if ( ! ( this instanceof Foldable ) + && ! ( this instanceof Pseudo ) + && ISO_AND_PG_REGULAR_IDENTIFIER.matcher(m_nonFolded) + .matches() ) + throw new InvalidObjectException( + "foldable identifier deserialized as not foldable: " + + m_nonFolded); } } @@ -1006,7 +1014,7 @@ public boolean equals(Object other, Messager msgr) * of this class matches anything but itself, to represent * pseudo-identifiers like {@code PUBLIC} as a privilege grantee. */ - public static class Pseudo extends Simple + public static final class Pseudo extends Simple { private static final long serialVersionUID = 4760344682650087583L; @@ -1454,7 +1462,17 @@ else if ( 0 != opStart ) private Qualified(Simple qualifier, T local) { m_qualifier = qualifier; - m_local = local; + m_local = requireNonNull(local); + } + + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + if ( null == m_local ) + throw new InvalidObjectException( + "Identifier.Qualified deserialized with " + + "null local part"); } @Override diff --git a/pljava-api/src/test/java/LexicalsTest.java b/pljava-api/src/test/java/LexicalsTest.java index e12ca967..17425811 100644 --- a/pljava-api/src/test/java/LexicalsTest.java +++ b/pljava-api/src/test/java/LexicalsTest.java @@ -11,6 +11,11 @@ */ package org.postgresql.pljava; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -273,4 +278,44 @@ public void testIdentifierOperatorFrom() throws Exception Qualified q3 = o1.withQualifier(s2); assertEquals("eq5", q3.toString(), "OPERATOR(\"foo\".!@#%*)"); } + + public void testIdentifierSerialization() throws Exception + { + Identifier[] orig = { + Simple.from("Foo", false), + Simple.from("foo", true), + Simple.from("I do not fold", true), + + Identifier.Pseudo.PUBLIC, + + Operator.from("!@#%*"), + + null, + null + }; + + orig[5] = (( Simple )orig[2]).withQualifier((Simple)orig[1]); + orig[6] = ((Operator)orig[4]).withQualifier((Simple)orig[1]); + + Identifier[] got; + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(bos)) + { + oos.writeObject(orig); + } + + bos.close(); + + try ( + ByteArrayInputStream bis = + new ByteArrayInputStream(bos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bis) + ) + { + got = (Identifier[])ois.readObject(); + } + + assertArrayEquals("identifier serialization", orig, got); + } } From a4a95a5863d51aadf0a20c8938f99f5cc979b369 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 27 Sep 2021 11:57:58 -0400 Subject: [PATCH 0888/1087] Update release notes --- src/site/markdown/releasenotes.md.vm | 59 +++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index a2639413..3bb772c9 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,19 +10,52 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') -$h2 PL/Java 1.5.7 +$h2 PL/Java 1.5.8 -1.5.7 is a bug-fix release, with a single issue backpatched from the 1.6 -branch, correcting a problem in XML Schema validation in some non-`en_US` -locales. +1.5.8 adds support for PostgreSQL 14, fixes two bugs, and begins preparation +for the impact of changes to Java's permission enforcement coming in Java 17 +and later with JEP 411. + +$h3 PL/Java with Java 17 and later: JEP 411 + +Current versions of PL/Java rely on Java security features that will be affected +by JEP 411, beginning with Java 17. Java 17 itself will continue to provide the +needed capabilities, with only deprecation marks and warnings added. Java 17 is +also positioned as a long-term support release, so the option of continuing to +run a current (1.5.8 or latest 1.6) PL/Java release without loss of function +will be available, if needed, by continuing to run with Java 17. + +The PL/Java 1.5 series has had a good run, and is not expected to receive +backports of future, post-JEP 411 functionality. + +For more on how PL/Java will adapt, please read about [JEP 411][jep411] on +the PL/Java wiki. $h3 Bugs fixed -* [XML Schema regression-test failure in de_DE locale](${ghbug}312) +* [MalformedInputException: Input length = 1 in 1.5](${ghbug}340) + + The bug was exercised in a database where `server_encoding` was `SQL_ASCII`. + The more-principled treatment of `SQL_ASCII` in PL/Java 1.6 has been + backported, and is described [here][sqlascii]. + +* [Crash in autovacuum if a PL/Java functional index exists](${ghbug}355) + + Functional indexes over PL/Java functions can now be autovacuumed + in PG 9.5 or later. In earlier versions, a `feature_not_supported` error + will be reported instead of crashing. That can be avoided by setting the + `autovacuum_enabled` storage parameter to `false` on any table having + a Java-based functional index, and running explicit `VACUUM` periodically + on such tables. Prior to PG 8.4, there was no such per-table setting, and + the only workaround would be to forego autovacuum database-wide and use + explicit `VACUUM` on some schedule. + +[jep411]: https://github.com/tada/pljava/wiki/JEP-411 +[sqlascii]: use/charsets.html#The_special_encoding_SQL_ASCII $h3 Credits -Thanks to Christoph Berg for the report. +Thanks to `yazun` and `ricdhen` for reporting the bugs fixed in this release. $h2 Earlier releases @@ -35,6 +68,20 @@ $h2 Earlier releases #set($h4 = '#####') #set($h5 = '######') +$h2 PL/Java 1.5.7 (16 November 2020) + +1.5.7 is a bug-fix release, with a single issue backpatched from the 1.6 +branch, correcting a problem in XML Schema validation in some non-`en_US` +locales. + +$h3 Bugs fixed + +* [XML Schema regression-test failure in de_DE locale](${ghbug}312) + +$h3 Credits + +Thanks to Christoph Berg for the report. + $h2 PL/Java 1.5.6 (4 October 2020) This release adds support for PostgreSQL 13. From 35260fe3274c3bd9737365a318268e0ee89f5471 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 27 Sep 2021 12:17:02 -0400 Subject: [PATCH 0889/1087] Poke migration-management versions for 1.5.8 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index f52baf7d..433c0326 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -495,6 +495,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_5_8 = REL_1_5_0; static final SchemaVariant REL_1_5_7 = REL_1_5_0; static final SchemaVariant REL_1_5_6 = REL_1_5_0; static final SchemaVariant REL_1_5_5 = REL_1_5_0; From ec688dc140fdec1a434e3e537e3974cc9ff7fb09 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 27 Sep 2021 12:18:35 -0400 Subject: [PATCH 0890/1087] Add control file in preparation for next release Now that 1.5.8 is released, the next release should include an extension SQL file allowing upgrade from 1.5.8. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index cb83152b..9962de94 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -281,6 +281,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Tue, 28 Sep 2021 14:48:14 -0400 Subject: [PATCH 0891/1087] Take the JEP 411 warning out back and shoot it PL/Java has never, until now, implemented anything by reaching into unexposed JDK innards. The need to do so now is fairly irksome. Java, any language, is classic infrastructure. Other layers, like this, are built atop it, and others in turn use those layers. The idea that the language developers would arrogate to themselves the act of sending an inappropriately low-level message directly to ultimate users, insisting that the stack layers above cannot intercept it and notify the higher-level users in terms that fit the abstractions meaningful there, leaves an uneasy picture of how a development team can begin to lose sight of who is providing what to whom and why. --- pljava-so/src/main/c/Backend.c | 62 +++++++++++++++++++ .../postgresql/pljava/internal/Backend.java | 11 ++++ .../pljava/internal/InstallHelper.java | 29 +++++---- 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index fc0a8565..8eef2841 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1003,6 +1003,11 @@ static void initPLJavaClasses(void) "()Ljava/lang/String;", Java_org_postgresql_pljava_internal_Backend__1myLibraryPath }, + { + "_pokeJEP411", + "(Ljava/lang/Class;Ljava/lang/Object;)V", + Java_org_postgresql_pljava_internal_Backend__1pokeJEP411 + }, { 0, 0, 0 } }; @@ -2132,6 +2137,63 @@ Java_org_postgresql_pljava_internal_Backend__1myLibraryPath(JNIEnv *env, jclass return result; } +/* + * Class: org_postgresql_pljava_internal_Backend + * Method: _pokeJEP411 + * Signature: (Ljava/lang/Class;Ljava/lang/Object;)V + * + * This method is hideously dependent on unexposed JDK internals. But then, + * the fact that it's needed at all is hideous already. Java, any language, + * is classic infrastructure. Other layers, like this, are built atop it, and + * others in turn use those layers. The idea that the language developers would + * arrogate to themselves the act of sending an inappropriately low-level + * message directly to ultimate users, insisting that the stack layers above + * cannot intercept it and notify the higher-level users in terms that fit + * the abstractions meaningful there, leaves an uneasy picture of how + * a development team can begin to lose sight of who is providing what to whom + * and why. + * + * At least as of the time of this writing, System has a CallersHolder class + * holding a map recording classes for which the warning has already been sent. + * Poking the 'caller' class into that map works to suppress the warning. + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_Backend__1pokeJEP411(JNIEnv *env, jclass cls, jclass caller, jobject token) +{ + jclass callersHolder; + jfieldID callers; + jobject map; + jclass mapClass; + jmethodID put; + + BEGIN_NATIVE + + callersHolder = JNI_findClass("java/lang/System$CallersHolder"); + if ( NULL == callersHolder ) + goto failed; + + callers = JNI_getStaticFieldID(callersHolder, "callers", "Ljava/util/Map;"); + if ( NULL == callers ) + goto failed; + + map = JNI_getStaticObjectField(callersHolder, callers); + if ( NULL == map ) + goto failed; + + mapClass = JNI_getObjectClass(map); + put = JNI_getMethodID(mapClass, + "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + + JNI_callObjectMethodLocked(map, put, caller, token); + goto done; + +failed: + JNI_exceptionClear(); + +done: + END_NATIVE +} + /* * Class: org_postgresql_pljava_internal_Backend_EarlyNatives * Method: _forbidOtherThreads diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index d6b18560..006822ad 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -310,6 +310,16 @@ public static String myLibraryPath() throws SQLException throw new SQLException("Unable to retrieve PL/Java's library path"); } + /** + * Attempt (best effort, unexposed JDK internals) to suppress + * the layer-inappropriate JEP 411 warning when {@code InstallHelper} + * sets up permission enforcement. + */ + static void pokeJEP411() + { + _pokeJEP411(InstallHelper.class, true); + } + /** * Returns true if the backend is awaiting a return from a * call into the JVM. This method will only return false @@ -331,6 +341,7 @@ public static String myLibraryPath() throws SQLException private static native void _clearFunctionCache(); private static native boolean _isCreatingExtension(); private static native String _myLibraryPath(); + private static native void _pokeJEP411(Class caller, Object token); private static class EarlyNatives { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 8584f4db..38f10337 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -166,9 +166,7 @@ public static String hello( e); } - setTrialPolicyIfSpecified(); - - System.setSecurityManager( new SecurityManager()); + beginEnforcing(); StringBuilder sb = new StringBuilder(); sb.append( "PL/Java native code (").append( nativeVer).append( ")\n"); @@ -262,22 +260,27 @@ private static void setPolicyURLs() } } - private static void setTrialPolicyIfSpecified() throws SQLException + private static void beginEnforcing() throws SQLException { String trialURI = System.getProperty( "org.postgresql.pljava.policy.trial"); - if ( null == trialURI ) - return; - - try - { - Policy.setPolicy( new TrialPolicy( trialURI)); - } - catch ( NoSuchAlgorithmException e ) + if ( null != trialURI ) { - throw new SQLException(e.getMessage(), e); + try + { + Policy.setPolicy( new TrialPolicy( trialURI)); + } + catch ( NoSuchAlgorithmException e ) + { + throw new SQLException(e.getMessage(), e); + } } + + if ( 17 <= Runtime.version().major() ) + Backend.pokeJEP411(); + + System.setSecurityManager( new SecurityManager()); } public static void groundwork( From 273b8dea1b52734fbbb80ab95de42b5aa89000c4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Sep 2021 13:33:11 -0400 Subject: [PATCH 0892/1087] React to surprises in post-JEP 411 Java versions Adapt beginEnforcing() to throw exceptions with decent explanations on Java > 17 if UnsupportedOperationException has been caught (suggest adding -Djava.security.manager=allow, for whatever good it might do), or if getSecurityManager() returns the wrong thing (suggest trying -Dorg.postgresql.pljava.policy.enforcement=none to ignore the fact, though again this may do little good or lead simply to a different later failure). Add doc comments to various InstallHelper methods lacking them. --- .../pljava/internal/InstallHelper.java | 128 +++++++++++++++++- 1 file changed, 125 insertions(+), 3 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 38f10337..f91633fc 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -58,6 +58,18 @@ private static void setPropertyIfNull( String property, String value) System.setProperty( property, value); } + /** + * Perform miscellaneous early PL/Java initialization, and return a string + * detailing the versions of PL/Java, PostgreSQL, and Java in use, which the + * native caller can use in its "PL/Java loaded" (a/k/a "hello") + * triumphant {@code ereport}. + *

    + * This method calls {@code beginEnforcing} rather late, so that the policy + * needn't be cluttered with permissions for the operations only needed + * before that point. Policy is being enforced by the time this method + * returns (except in case of JEP 411 fallback as described at + * {@code beginEnforcing}). + */ public static String hello( String nativeVer, String serverBuiltVer, String serverRunningVer, String user, String dbname, String clustername, @@ -260,11 +272,37 @@ private static void setPolicyURLs() } } + /** + * From the point of successful call of this method, PL/Java is enforcing + * security policy (except in JEP 411 fallback case described below). + *

    + * This method handles applying the {@code TrialPolicy} if that has been + * selected, and setting the security manager, which thereafter cannot be + * unset or changed (unless the policy has been edited to allow it). + *

    + * In the advent of JEP 411, this method also must also head off the + * layer-inappropriate boilerplate warning message when running on Java 17 + * or later, and react if the operation has been disallowed or "degraded". + *

    + * If {@code getSecurityManager} still returns null after being set, and + * the Java major version is greater than 17, this can be a sign of + * "degradation" of the security API proposed in JEP 411. It may be ignored + * by setting {@code -Dorg.postgresql.pljava.policy.enforcement=none} in + * {@code pljava.vmoptions}. That may permit PL/Java to run, but + * without enforcing any policy at all, no distinction between trusted and + * untrusted functions, and so on. However, given uncertainty around exactly + * how the Java developers will "degrade" the API in a given Java release, + * the result may simply be a different failure of PL/Java to start or + * properly function. + */ private static void beginEnforcing() throws SQLException { String trialURI = System.getProperty( "org.postgresql.pljava.policy.trial"); + String enforcement = System.getProperty( + "org.postgresql.pljava.policy.enforcement"); + if ( null != trialURI ) { try @@ -277,12 +315,65 @@ private static void beginEnforcing() throws SQLException } } - if ( 17 <= Runtime.version().major() ) + int major = Runtime.version().major(); + + if ( 17 <= major ) Backend.pokeJEP411(); - System.setSecurityManager( new SecurityManager()); + try + { + SecurityManager sm = new SecurityManager(); + System.setSecurityManager( sm); + if ( sm == System.getSecurityManager() ) + return; + } + catch ( UnsupportedOperationException e ) + { + if ( 17 >= major ) + throw new SQLException( + "Unexpected failure enabling permission enforcement", e); + throw new SQLNonTransientException( + "[JEP 411] The Java version selected, " + Runtime.version() + + ", has not allowed PL/Java to enforce security policy. " + + "It may help to add -Djava.security.manager=allow in " + + "the pljava.vmoptions setting. However, that may require " + + "allowing PL/Java functions to execute with no policy " + + "enforcement, or simply lead to a different failure " + + "to start. If that is unacceptable, " + jepSuffix, "58000", e); + } + + if ( 17 >= major ) + throw new SQLException( + "Unexpected failure enabling permission enforcement"); + + if ( "none".equals(enforcement) ) + return; + + throw new SQLNonTransientException( + "[JEP 411] The Java version selected, " + Runtime.version() + + ", cannot enforce security policy as this PL/Java version " + + "requires. To allow PL/Java to run with no enforcement of " + + "security (for example, trusted functions as untrusted), add " + + "-Dorg.postgresql.pljava.policy.enforcement=none in the " + + "pljava.vmoptions setting. However, this may lead only to a " + + "different failure to start. In that case, " + + jepSuffix, "58000"); } + private static final String jepSuffix = + "pljava.libjvm_location should be pointed to an earlier version " + + "of Java, or a newer PL/Java version should be used. For more " + + "explanation, please see " + + "https://github.com/tada/pljava/wiki/JEP-411"; + + /** + * When PL/Java is loaded as an end-in-itself (that is, by {@code LOAD} + * on its own or from its extension script on {@code CREATE EXTENSION} or + * {@code ALTER EXTENSION}, not just in the course of handling a call of a + * Java function), this method will be called to ensure there is + * a schema {@code sqlj} and that it contains the right, possibly updated, + * stuff. + */ public static void groundwork( String module_pathname, String loadpath_tbl, String loadpath_tbl_quoted, boolean asExtension, boolean exNihilo) @@ -321,6 +412,13 @@ public static void groundwork( } } + /** + * Create the {@code sqlj} schema, adding an appropriate comment and + * granting {@code USAGE} to {@code public}. + *

    + * If the schema already exists, whatever comment and permissions it + * may have will not be disturbed. + */ private static void schema( Connection c, Statement s) throws SQLException { @@ -344,6 +442,18 @@ private static void schema( Connection c, Statement s) } } + /** + * Declare PL/Java's language handler functions. + *

    + * {@code CREATE OR REPLACE} is used so that the library path can be altered + * if this is an upgrade. + *

    + * All privileges are unconditionally revoked on the handler functions. + * PostgreSQL does not need permissions when it invokes them + * as language handlers. + *

    + * Each function will have a default comment added if no comment is present. + */ private static void handlers( Connection c, Statement s, String module_path) throws SQLException { @@ -436,6 +546,16 @@ private static void handlers( Connection c, Statement s, String module_path) "trusted/sandboxed language.'"); } + /** + * Declare PL/Java's basic two (trusted and untrusted) languages. + *

    + * If not declared already, they will have default permissions and comments + * applied. + *

    + * If they exist, {@code CREATE OR REPLACE} will be used, which takes care + * of adding the validator handler during an upgrade from a version that + * lacked it. No permission or comment changes are made in this case. + */ private static void languages( Connection c, Statement s) throws SQLException { @@ -498,7 +618,9 @@ private static void languages( Connection c, Statement s) /** * Execute the deployment descriptor for PL/Java itself, creating the - * expected tables, functions, etc. Will be skipped if tables conforming + * expected tables, functions, etc. + *

    + * Will be skipped if tables conforming * to the currently expected schema already seem to be there. If an earlier * schema variant is detected, attempt to migrate to the current one. */ From d483ff77190272bf6e2e10e0f0fba83ab54067c3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Sep 2021 13:33:11 -0400 Subject: [PATCH 0893/1087] About the JEP 411 deprecation warnings Because PL/Java 1.6 builds with --release 9, no @SuppressWarnings annotations need to be added for JEP 411, as the deprecation markings introduced in the JEP are not visible as of release 9. I will turn on the maven-compiler-plugin's anyway, as its default of false just seems wrong to me. I'll create a branch from this point that has the @SuppressWarnings annotations that would otherwise need to be added, so they will be on record, but there is no need to merge them in for PL/Java 1.6. May as well not clutter the code with them unnecessarily. --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 9a7e0f40..f5bb6576 100644 --- a/pom.xml +++ b/pom.xml @@ -153,6 +153,7 @@ ${project.build.sourceEncoding} 9 + true true From 37273b33f79000f78cae46de958e3a31727ebbe7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Sep 2021 15:42:03 -0400 Subject: [PATCH 0894/1087] Supply advisory warnings appropriate to PL/Java Having suppressed the boilerplate warning from Java itself (which was going to spam standard error for every backend that starts PL/Java, ending up in the server log if logging_collector is set), set about supplying a more usefully worded warning at more strategically chosen times, that will mention the PL/Java wiki/JEP-411 page for details on the road map ahead. The idea is to produce no more than one warning per backend, and only at times when something PL/Java-administrative in nature is happening, to maximize the chance that the person who occasions the warning is a person it will mean something to. So, deliver the warning: 1. when PL/Java itself is installed or upgraded (really, any time its library is loaded by LOAD, directly or by the extension script, even if it's the version already installed and nothing else happens). 2. when functions are declared or redeclared that use it (as detected by the validator handler). 3. when pg_upgrade is run on a database that has PL/Java installed. 1 and 2 are only delivered if the Java major version is 12 or later. The idea is that if a site tends to stick with long-term-support Java releases, there will be plenty of time on 17 LTS for warnings, and no need to deliver them while running 11 LTS. At another site that follows along at a faster pace with non-LTS releases, start with the warnings as soon as running anything later than 11. 3 is unconditional because under pg_upgrade a JVM doesn't get launched to determine its version. 1 and 2 also defer the warning to the commit of a top-level transaction that contained the triggering event(s). If the transaction doesn't commit, the warning is squelched to avoid adding noise to other messages possibly indicating something went wrong. --- pljava-so/src/main/c/Backend.c | 84 ++++++++++++++++++- pljava-so/src/main/c/DualState.c | 4 + pljava-so/src/main/c/InstallHelper.c | 15 ++-- pljava-so/src/main/c/JNICalls.c | 9 ++ pljava-so/src/main/include/pljava/Backend.h | 19 ++++- pljava-so/src/main/include/pljava/JNICalls.h | 1 + .../postgresql/pljava/internal/Backend.java | 2 + 7 files changed, 123 insertions(+), 11 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 8eef2841..6a8d4608 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -225,6 +225,28 @@ static char const policyUrlsGUC[] = "pljava.policy_urls"; */ static bool deferInit = false; +/* + * Whether Backend_warnJEP411() should emit a warning when called. + * Initially true, because it may be called very early from the deferInit check, + * if pg_upgrade is happening, and should always warn in that case. Thereafter + * false, unless set true in the initsequencer because InstallHelper_groundwork + * will be called (PL/Java being installed or upgraded), or in the validator + * handler because a PL/Java function has been declared or redeclared. + */ +static bool warnJEP411 = true; + +/* + * Don't bother with the warning unless the JVM in use is later than Java 11. + * 11 is the LTS release prior to the one where JEP 411 gets interesting (17). + * If a site is sticking to LTS releases, there will be plenty of time to warn + * on 17. If a site moves with non-LTS releases, start warning as soon as + * anything > 11 is used. + * + * Initially true, so there will be a warning unconditionally in a case + * (pg_upgrade) where a JVM hasn't been launched to learn its version). + */ +static bool javaGT11 = true; + static void initsequencer(enum initstage is, bool tolerant); #if PG_VERSION_NUM >= 90100 @@ -530,6 +552,7 @@ static void initsequencer(enum initstage is, bool tolerant) initstage = IS_GUCS_REGISTERED; if ( deferInit ) return; + warnJEP411 = false; /*FALLTHROUGH*/ case IS_GUCS_REGISTERED: @@ -748,7 +771,10 @@ static void initsequencer(enum initstage is, bool tolerant) case IS_PLJAVA_INSTALLING: if ( NULL != pljavaLoadPath ) + { + warnJEP411 = javaGT11; InstallHelper_groundwork(); /* sqlj schema, language handlers, ...*/ + } initstage = IS_COMPLETE; /*FALLTHROUGH*/ @@ -960,7 +986,7 @@ void _PG_init() static void initPLJavaClasses(void) { - jfieldID tlField; + jfieldID fID; JNINativeMethod backendMethods[] = { { @@ -1045,9 +1071,12 @@ static void initPLJavaClasses(void) s_Backend_class = JNI_newGlobalRef(cls); PgObject_registerNatives2(s_Backend_class, backendMethods); - tlField = PgObject_getStaticJavaField(s_Backend_class, + fID = PgObject_getStaticJavaField(s_Backend_class, "JAVA_MAJOR", "I"); + javaGT11 = 11 < JNI_getStaticIntField(s_Backend_class, fID); + + fID = PgObject_getStaticJavaField(s_Backend_class, "THREADLOCK", "Ljava/lang/Object;"); - JNI_setThreadLock(JNI_getStaticObjectField(s_Backend_class, tlField)); + JNI_setThreadLock(JNI_getStaticObjectField(s_Backend_class, fID)); Invocation_initialize(); Exception_initialize2(); @@ -1947,7 +1976,11 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) { initsequencer( initstage, true); if ( IS_PLJAVA_INSTALLING > initstage ) + { + if ( javaGT11 ) + warnJEP411 = true; PG_RETURN_VOID(); + } } } @@ -1970,9 +2003,54 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) PG_RE_THROW(); } PG_END_TRY(); + + if ( javaGT11 ) + warnJEP411 = true; PG_RETURN_VOID(); } +/* + * Called at the ends of committing transactions to emit a warning about future + * JEP 411 impacts, at most once per session, if any PL/Java functions were + * declared or redeclared in the transaction, or if PL/Java was installed or + * upgraded. Also called from InstallHelper, if pg_upgrade is happening. + * Yes, this is a bit tangled. The tracking of function declaration happens + * above in the validator handler, and PL/Java installation/upgrade is detected + * in the initsequencer. + */ +void Backend_warnJEP411(bool isCommit) +{ + static bool warningEmitted = false; /* once only per session */ + + if ( warningEmitted || ! warnJEP411 ) + return; + + if ( ! isCommit ) + { + warnJEP411 = false; + return; + } + + warningEmitted = true; + + ereport(WARNING, ( + errmsg( + "[JEP 411] migration advisory: there will be a Java version " + "(after Java 17) that will be unable to run PL/Java %s " + "with policy enforcement", SO_VERSION_STRING), + errdetail( + "Future Java releases will phase out important features used " + "by this PL/Java version to enforce security policy. Those " + "changes will come in releases after Java 17."), + errhint( + "For migration planning, Java versions up to and including 17 " + "remain fully usable with this version of PL/Java, and Java 17 " + "is positioned as a long-term support release. For details on " + "how PL/Java will adapt, please visit " + "https://github.com/tada/pljava/wiki/JEP-411") + )); +} + /**************************************** * JNI methods ****************************************/ diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 66f4477c..e557231e 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -20,6 +20,7 @@ #include "org_postgresql_pljava_internal_DualState_SingleSPIcursorClose.h" #include "pljava/DualState.h" +#include "pljava/Backend.h" #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/PgObject.h" @@ -268,6 +269,9 @@ static void resourceReleaseCB(ResourceReleasePhase phase, return; pljava_DualState_nativeRelease(CurrentResourceOwner); + + if ( isTopLevel ) + Backend_warnJEP411(isCommit); } diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index cb303479..9ae88b8d 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -97,12 +97,6 @@ #endif #endif -#ifndef PLJAVA_SO_VERSION -#error "PLJAVA_SO_VERSION needs to be defined to compile this file." -#else -#define SO_VERSION_STRING CppAsString2(PLJAVA_SO_VERSION) -#endif - /* * The name of the table the extension scripts will create to pass information * here. The table name is phrased as an error message because it will appear @@ -445,7 +439,14 @@ char *pljavaFnOidToLibPath(Oid fnOid, char **langName, bool *trusted) bool InstallHelper_shouldDeferInit() { - return IsBackgroundWorker || IsBinaryUpgrade || IsAutoVacuumWorkerProcess(); + if ( IsBackgroundWorker || IsAutoVacuumWorkerProcess() ) + return true; + + if ( ! IsBinaryUpgrade ) + return false; + + Backend_warnJEP411(true); + return true; } bool InstallHelper_isPLJavaFunction(Oid fn, char **langName, bool *trusted) diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index eefc59f0..63d5d51a 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -1109,6 +1109,15 @@ jmethodID JNI_getStaticMethodIDOrNull(jclass clazz, const char* name, const char return result; } +jint JNI_getStaticIntField(jclass clazz, jfieldID field) +{ + jint result; + BEGIN_JAVA + result = (*env)->GetStaticIntField(env, clazz, field); + END_JAVA + return result; +} + jobject JNI_getStaticObjectField(jclass clazz, jfieldID field) { jobject result; diff --git a/pljava-so/src/main/include/pljava/Backend.h b/pljava-so/src/main/include/pljava/Backend.h index cb31b289..16c29cdb 100644 --- a/pljava-so/src/main/include/pljava/Backend.h +++ b/pljava-so/src/main/include/pljava/Backend.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -26,12 +26,29 @@ extern "C" { * * @author Thomas Hallgren *****************************************************************/ +#ifndef PLJAVA_SO_VERSION +#error "PLJAVA_SO_VERSION needs to be defined to compile this file." +#else +#define SO_VERSION_STRING CppAsString2(PLJAVA_SO_VERSION) +#endif + #if PG_VERSION_NUM < 100000 extern bool integerDateTimes; #endif int Backend_setJavaLogLevel(int logLevel); +/* + * Called at the ends of committing transactions to emit a warning about future + * JEP 411 impacts, at most once per session, if any PL/Java functions were + * declared or redeclared in the transaction, or if PL/Java was installed or + * upgraded. Also called from InstallHelper, if pg_upgrade is happening. Yes, + * this is a bit tangled. The tracking of function declaration and + * install/upgrade is encapsulated in Backend.c. If isCommit is false, + * no warning is emitted, and the tracking bit is reset. + */ +void Backend_warnJEP411(bool isCommit); + #ifdef PG_GETCONFIGOPTION #error The macro PG_GETCONFIGOPTION needs to be renamed. #endif diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index 1e033515..17ba967a 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -196,6 +196,7 @@ extern void JNI_getShortArrayRegion(jshortArray array, jsize start, jsiz extern jfieldID JNI_getStaticFieldID(jclass clazz, const char* name, const char* sig); extern jmethodID JNI_getStaticMethodID(jclass clazz, const char* name, const char* sig); extern jmethodID JNI_getStaticMethodIDOrNull(jclass clazz, const char* name, const char* sig); +extern jint JNI_getStaticIntField(jclass clazz, jfieldID field); extern jobject JNI_getStaticObjectField(jclass clazz, jfieldID field); extern const char* JNI_getStringUTFChars(jstring string, jboolean* isCopy); extern jboolean JNI_hasNullArrayElement(jobjectArray array); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index 006822ad..e2673694 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -47,6 +47,8 @@ public class Backend */ public static final ThreadLocal IAMPGTHREAD = new ThreadLocal<>(); + static final int JAVA_MAJOR = Runtime.version().major(); + static { IAMPGTHREAD.set(Boolean.TRUE); From dc4d9a49844210461cca9f096eda4b1ce895474f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Sep 2021 15:55:38 -0400 Subject: [PATCH 0895/1087] Teach CI tests about [JEP 411] advisory warnings They should not be counted as ng test results. --- .github/workflows/ci-runnerpg.yml | 4 +++- .travis.yml | 3 ++- appveyor.yml | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 13fffab4..7ca18b9f 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -251,7 +251,9 @@ jobs: String type = parts[0]; results.compute(type, (k,v) -> 1 + v); if ( whatIsNG.contains(type) ) - results.compute("ng", (k,v) -> 1 + v); + if ( ! "warning".equals(type) + || ! message.startsWith("[JEP 411]") ) + results.compute("ng", (k,v) -> 1 + v); return true; } diff --git a/.travis.yml b/.travis.yml index 307b5b7f..fd72fedb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -190,7 +190,8 @@ script: | String type = parts[0]; results.compute(type, (k,v) -> 1 + v); if ( whatIsNG.contains(type) ) - results.compute("ng", (k,v) -> 1 + v); + if ( ! "warning".equals(type) || ! message.startsWith("[JEP 411]") ) + results.compute("ng", (k,v) -> 1 + v); return true; } diff --git a/appveyor.yml b/appveyor.yml index 03422f38..d1075053 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -128,7 +128,8 @@ test_script: String type = parts[0]; results.compute(type, (k,v) -> 1 + v); if ( whatIsNG.contains(type) ) - results.compute("ng", (k,v) -> 1 + v); + if ( ! "warning".equals(type) || ! message.startsWith("[JEP 411]") ) + results.compute("ng", (k,v) -> 1 + v); return true; } From 036e7ff7a9629d831d729b3f9cc5de7c48cbb8df Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 29 Sep 2021 17:33:29 -0400 Subject: [PATCH 0896/1087] Imperfectly copy-pasted CI script changes --- .github/workflows/ci-runnerpg.yml | 1 + .travis.yml | 1 + appveyor.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 7ca18b9f..7906982f 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -249,6 +249,7 @@ jobs: 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) diff --git a/.travis.yml b/.travis.yml index fd72fedb..3f3d9f36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -188,6 +188,7 @@ script: | 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]") ) diff --git a/appveyor.yml b/appveyor.yml index d1075053..bf404ff0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -126,6 +126,7 @@ test_script: 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]") ) From 4b5b1d66fac2769d887af255caebf3098c8ac8fb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 30 Sep 2021 21:02:00 -0400 Subject: [PATCH 0897/1087] Bit of refactoring around UDTs Factor the check for a MappedUDT into a checkTypeMappedUDT function, for symmetry with Function_checkTypeBaseUDT. Should make it simpler to grasp what's going on with the two distinct flavors of UDT. Do not expose global functions for obtaining handles for the two support functions that only BaseUDTs have, which are only obtained within Function.c anyway. --- pljava-so/src/main/c/Function.c | 94 +++++++---- pljava-so/src/main/c/Invocation.c | 9 +- pljava-so/src/main/c/type/Type.c | 151 ++++++++++++------ pljava-so/src/main/c/type/UDT.c | 28 ++-- pljava-so/src/main/include/pljava/Function.h | 22 ++- .../src/main/include/pljava/Invocation.h | 9 +- 6 files changed, 219 insertions(+), 94 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index b9ae2f6c..b246ddaf 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -139,7 +139,9 @@ struct Function_ /* * The type map used when mapping parameter and return types. We * need to store it here in order to cope with dynamic types (any - * and anyarray) + * and anyarray). This is now slightly redundant, as it could be got + * from schemaLoader at the cost of a couple JNI calls, but this was + * here first. */ jobject typeMap; @@ -376,7 +378,8 @@ jdouble pljava_Function_doubleInvoke(Function self) /* * 'Reserve' the static parameter frame for (refArgCount,primArgCount) reference * and primitive parameters, respectively, pushing temporarily out of the way - * any current contents, detected by a non-(0,0) existing reservation. + * any current contents, detected by a non-(0,0) existing reservation. Returns + * the sum of its two arguments. * * The corresponding pop of the earlier contents will happen at * Invocation_popInvocation time, so this scheme is only appropriately used for @@ -396,7 +399,7 @@ jdouble pljava_Function_doubleInvoke(Function self) * jvalue slot for returns, though, must handle its own normal and exceptional * cleanup. */ -static void reserveParameterFrame(jsize refArgCount, jsize primArgCount) +static jsize reserveParameterFrame(jsize refArgCount, jsize primArgCount) { jshort newCounts = COUNTCHECK(refArgCount, primArgCount); @@ -420,6 +423,8 @@ static void reserveParameterFrame(jsize refArgCount, jsize primArgCount) currentInvocation->frameLimits = FRAME_LIMITS_PUSHED; } *s_countCheck = newCounts; + + return refArgCount + primArgCount; } /* @@ -490,13 +495,6 @@ jobject pljava_Function_udtReadHandle( s_Function_udtReadHandle, clazz, langName, trusted); } -jobject pljava_Function_udtParseHandle( - jclass clazz, char *langName, bool trusted) -{ - return obtainUDTHandle( - s_Function_udtParseHandle, clazz, langName, trusted); -} - jobject pljava_Function_udtWriteHandle( jclass clazz, char *langName, bool trusted) { @@ -504,13 +502,6 @@ jobject pljava_Function_udtWriteHandle( s_Function_udtWriteHandle, clazz, langName, trusted); } -jobject pljava_Function_udtToStringHandle( - jclass clazz, char *langName, bool trusted) -{ - return obtainUDTHandle( - s_Function_udtToStringHandle, clazz, langName, trusted); -} - static jobject obtainUDTHandle( jmethodID which, jclass clazz, char *langName, bool trusted) { @@ -549,14 +540,14 @@ Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct) typeStruct->typinput, typeStruct->typreceive, typeStruct->typsend, typeStruct->typoutput }; - jobject (*getter[4])(jclass, char *, bool) = + jmethodID getter[4] = { - pljava_Function_udtParseHandle, pljava_Function_udtReadHandle, - pljava_Function_udtWriteHandle, pljava_Function_udtToStringHandle + s_Function_udtParseHandle, s_Function_udtReadHandle, + s_Function_udtWriteHandle, s_Function_udtToStringHandle }; char *langName[4] = { NULL, NULL, NULL, NULL }; bool trusted[4]; - jobject handle[4]; + jobject handle[4] = { NULL, NULL, NULL, NULL }; int i; for ( i = 0; i < 4; ++ i ) @@ -566,6 +557,10 @@ Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct) break; } + /* + * If that loop did not find all four support functions to be PL/Java ones, + * we have struck out; pfree anything it did find and return the bad news. + */ if ( i < 4 ) { for ( ; i >= 0 ; -- i ) @@ -574,19 +569,44 @@ Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct) return NULL; } + /* + * At this point, it is looking like a PL/Java BaseUDT; we have the four + * support function oids and the language names and trusted flags to go + * with them. + * + * Must still confirm that (1) each one is declared with a UDT[classname] + * style of AS string (getClassIfUDT returns null if not), (2) the named + * class inherits from SQLData (ClassCastException happens if not), and + * (3) that's the same class for all four of them (bail from this loop + * and goto classMismatch if not). We'll also consider it a classMismatch + * if some but not all getClassIfUDT results are null. + * + * Provided none of that goes wrong, obtain their handles in this loop too. + */ for ( i = 0; i < 4; ++ i ) { + /* + * Get the pg_proc info corresponding to support function i, + * needed by getClassIfUDT(). + */ procTup = PgObject_getValidTuple(PROCOID, procId[i], "function"); procStruct = (Form_pg_proc)GETSTRUCT(procTup); schemaName = getSchemaName(procStruct->pronamespace); d = heap_copy_tuple_as_datum( procTup, Type_getTupleDesc(s_pgproc_Type, 0)); + t_clazz = (jclass)JNI_callStaticObjectMethod(s_Function_class, s_Function_getClassIfUDT, Type_coerceDatum(s_pgproc_Type, d), schemaName); + pfree((void *)d); JNI_deleteLocalRef(schemaName); ReleaseSysCache(procTup); + + /* + * Save the first clazz returned; for subsequent ones, just confirm it's + * not different, then delete the extra local ref. + */ if ( 0 == i ) clazz = t_clazz; else @@ -595,15 +615,23 @@ Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct) goto classMismatch; JNI_deleteLocalRef(t_clazz); } - handle[i] = (getter[i])(clazz, langName[i], trusted[i]); + + if ( NULL == clazz ) + continue; + + handle[i] = obtainUDTHandle(getter[i], clazz, langName[i], trusted[i]); } + /* + * We can only be here if getClassIfUDT returned the same value for clazz + * all four times. But that value could have been NULL; no UDT if so. + */ if ( NULL != clazz ) t = (Type)UDT_registerUDT(clazz, typeId, typeStruct, 0, true, handle[0], handle[1], handle[2], handle[3]); /* * UDT_registerUDT will already have called JNI_deleteLocalRef on the - * four handles. + * four handles. (Or clazz was NULL and there aren't any to delete anyway.) */ JNI_deleteLocalRef(clazz); for ( i = 0; i < 4; ++ i ) @@ -847,13 +875,20 @@ Datum Function_invoke(Function self, PG_FUNCTION_ARGS) } } + passedArgCount = PG_NARGS(); + if ( ! skipParameterConversion ) - reserveParameterFrame( + { + jsize reservedArgCount = reserveParameterFrame( self->func.nonudt.numRefParams, self->func.nonudt.numPrimParams); - invokerType = self->func.nonudt.returnType; + if ( passedArgCount != reservedArgCount + && passedArgCount + 1 != reservedArgCount ) /* the OUT arg case */ + elog(ERROR, "function expecting %u arguments passed %u", + (unsigned int)reservedArgCount, (unsigned int)passedArgCount); + } - passedArgCount = PG_NARGS(); + invokerType = self->func.nonudt.returnType; if ( passedArgCount > 0 && ! skipParameterConversion ) { @@ -977,6 +1012,11 @@ void pljava_Function_setParameter(Function self, int index, jvalue value) int numRefs = self->func.nonudt.numRefParams; if ( -1 != index || 1 > numRefs ) elog(ERROR, "unsupported index in pljava_Function_setParameter"); + /* + * Thinking to Assert(!passAsPrimitive(self->func.nonudt.paramTypes[...]))? + * Nice idea, but you would index beyond paramTypes; that synthetic last + * OUT tuple entry isn't represented there. + */ JNI_setObjectArrayElement(s_referenceParameters, numRefs - 1, value.l); } diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 1154c8e5..52b1cc10 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -121,6 +121,13 @@ void Invocation_assertDisconnect(void) } } +/* + * Return the type map held by the innermost executing PL/Java function's + * schema loader (the initiating loader that was used to resolve the function). + * The type map is a map from Java Oid objects to Class class objects, + * as resolved by that loader. This is effectively Function_currentLoader() + * followed by JNI-invoking getTypeMap on the loader, but cached to avoid JNI). + */ jobject Invocation_getTypeMap(void) { Function f = currentInvocation->function; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 5cd700cf..fcdf440c 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -638,6 +638,70 @@ Type Type_fromOidCache(Oid typeId) return (Type)HashMap_getByOid(s_typeByOid, typeId); } +/* + * Return NULL unless typeId represents a MappedUDT as found in the typeMap, + * in which case return a freshly-registered UDT Type. + * + * A MappedUDT's supporting functions don't have SQL declarations, from which + * an ordinary function's PLPrincipal and initiating class loader would be + * determined, so when obtaining the support function handles below, NULL will + * be passed as the language name, indicating that information isn't available, + * and won't be baked into the handles. + * + * A MappedUDT only has the two support functions readSQL and writeSQL. + * The I/O support functions parse and toString are only for a BaseUDT, so + * they do not need to be looked up here. + * + * The typeStruct argument supplies the type's name and namespace to + * UDT_registerUDT, as well as the by-value, length, and alignment common to + * any registered Type. + * + * A complication, though: in principle, this is a function on two variables, + * typeId and typeMap. (The typeStruct is functionally dependent on typeId.) + * But registration of the first one to be encountered will enter it in caches + * that depend only on the typeId (or Java class name, for the other direction) + * from that point on. This is longstanding PL/Java behavior, but XXX. + */ +static Type checkTypeMappedUDT( + Oid typeId, jobject typeMap, Form_pg_type typeStruct) +{ + jobject joid; + jclass typeClass; + Type type; + jobject readMH; + jobject writeMH; + TupleDesc tupleDesc; + bool hasTupleDesc; + + if ( NULL == typeMap ) + return NULL; + + joid = Oid_create(typeId); + typeClass = (jclass)JNI_callObjectMethod(typeMap, s_Map_get, joid); + JNI_deleteLocalRef(joid); + + if ( NULL == typeClass ) + return NULL; + + readMH = pljava_Function_udtReadHandle( typeClass, NULL, true); + writeMH = pljava_Function_udtWriteHandle(typeClass, NULL, true); + + tupleDesc = lookup_rowtype_tupdesc_noerror(typeId, -1, true); + hasTupleDesc = NULL != tupleDesc; + if ( hasTupleDesc ) + ReleaseTupleDesc(tupleDesc); + + type = (Type)UDT_registerUDT( + typeClass, typeId, typeStruct, hasTupleDesc, false, + NULL, readMH, writeMH, NULL); + /* + * UDT_registerUDT calls JNI_deleteLocalRef on readMH and writeMH. + */ + + JNI_deleteLocalRef(typeClass); + return type; +} + Type Type_fromOid(Oid typeId, jobject typeMap) { CacheEntry ce; @@ -645,7 +709,7 @@ Type Type_fromOid(Oid typeId, jobject typeMap) Form_pg_type typeStruct; Type type = Type_fromOidCache(typeId); - if(type != 0) + if ( NULL != type ) return type; typeTup = PgObject_getValidTuple(TYPEOID, typeId, "type"); @@ -653,12 +717,14 @@ Type Type_fromOid(Oid typeId, jobject typeMap) if(typeStruct->typelem != 0 && typeStruct->typlen == -1) { - type = Type_getArrayType(Type_fromOid(typeStruct->typelem, typeMap), typeId); + type = Type_getArrayType( + Type_fromOid(typeStruct->typelem, typeMap), typeId); goto finally; } /* For some reason, the anyarray is *not* an array with anyelement as the * element type. We'd like to see it that way though. + * XXX would we, or does that mistake something intended in PostgreSQL? */ if(typeId == ANYARRAYOID) { @@ -675,40 +741,13 @@ Type Type_fromOid(Oid typeId, jobject typeMap) goto finally; } - if(typeMap != 0) - { - jobject joid = Oid_create(typeId); - jclass typeClass = - (jclass)JNI_callObjectMethod(typeMap, s_Map_get, joid); - - JNI_deleteLocalRef(joid); - if(typeClass != 0) - { - /* - * We have found a MappedUDT. It doesn't have SQL-declared I/O - * functions, so we need to look up only the read and write handles, - * and there will be no PLPrincipal to associate them with, - * indicated by passing NULL as the language name. - */ - jobject readMH = - pljava_Function_udtReadHandle(typeClass, NULL, true); - jobject writeMH = - pljava_Function_udtWriteHandle(typeClass, NULL, true); - TupleDesc tupleDesc = - lookup_rowtype_tupdesc_noerror(typeId, -1, true); - bool hasTupleDesc = NULL != tupleDesc; - if ( hasTupleDesc ) - ReleaseTupleDesc(tupleDesc); - type = (Type)UDT_registerUDT( - typeClass, typeId, typeStruct, hasTupleDesc, false, - NULL, readMH, writeMH, NULL); - /* - * UDT_registerUDT calls JNI_deleteLocalRef on readMH and writeMH. - */ - JNI_deleteLocalRef(typeClass); - goto finally; - } - } + /* + * Perhaps we have found a MappedUDT. If so, this check will register and + * return it. + */ + type = checkTypeMappedUDT(typeId, typeMap, typeStruct); + if ( NULL != type ) + goto finally; /* Composite and record types will not have a TypeObtainer registered */ @@ -720,14 +759,14 @@ Type Type_fromOid(Oid typeId, jobject typeMap) } ce = (CacheEntry)HashMap_getByOid(s_obtainerByOid, typeId); - if(ce == 0) + if ( NULL == ce ) { /* * Perhaps we have found a BaseUDT. If so, this check will register and * return it. */ type = Function_checkTypeBaseUDT(typeId, typeStruct); - if ( 0 != type ) + if ( NULL != type ) goto finally; /* * Default to String and standard textin/textout coercion. @@ -962,11 +1001,15 @@ void Type_initialize(void) pljava_SQLXMLImpl_initialize(); s_Map_class = JNI_newGlobalRef(PgObject_getJavaClass("java/util/Map")); - s_Map_get = PgObject_getJavaMethod(s_Map_class, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); + s_Map_get = PgObject_getJavaMethod( + s_Map_class, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); - s_Iterator_class = JNI_newGlobalRef(PgObject_getJavaClass("java/util/Iterator")); - s_Iterator_hasNext = PgObject_getJavaMethod(s_Iterator_class, "hasNext", "()Z"); - s_Iterator_next = PgObject_getJavaMethod(s_Iterator_class, "next", "()Ljava/lang/Object;"); + s_Iterator_class = JNI_newGlobalRef( + PgObject_getJavaClass("java/util/Iterator")); + s_Iterator_hasNext = PgObject_getJavaMethod( + s_Iterator_class, "hasNext", "()Z"); + s_Iterator_next = PgObject_getJavaMethod( + s_Iterator_class, "next", "()Ljava/lang/Object;"); #if PG_VERSION_NUM < 110000 BOOLARRAYOID = get_array_type(BOOLOID); @@ -986,10 +1029,12 @@ void Type_initialize(void) */ TypeClass TypeClass_alloc(const char* typeName) { - return TypeClass_alloc2(typeName, sizeof(struct TypeClass_), sizeof(struct Type_)); + return TypeClass_alloc2( + typeName, sizeof(struct TypeClass_), sizeof(struct Type_)); } -TypeClass TypeClass_alloc2(const char* typeName, Size classSize, Size instanceSize) +TypeClass TypeClass_alloc2( + const char* typeName, Size classSize, Size instanceSize) { TypeClass self = (TypeClass)MemoryContextAlloc(TopMemoryContext, classSize); PgObjectClass_init((PgObjectClass)self, typeName, instanceSize, 0); @@ -1024,7 +1069,8 @@ Type TypeClass_allocInstance(TypeClass cls, Oid typeId) */ Type TypeClass_allocInstance2(TypeClass cls, Oid typeId, Form_pg_type pgType) { - Type t = (Type)PgObjectClass_allocInstance((PgObjectClass)(cls), TopMemoryContext); + Type t = (Type) + PgObjectClass_allocInstance((PgObjectClass)(cls), TopMemoryContext); t->typeId = typeId; t->arrayType = 0; t->elementType = 0; @@ -1056,9 +1102,11 @@ Type TypeClass_allocInstance2(TypeClass cls, Oid typeId, Form_pg_type pgType) /* * Register this type. */ -static void _registerType(Oid typeId, const char* javaTypeName, Type type, TypeObtainer obtainer) +static void _registerType( + Oid typeId, const char* javaTypeName, Type type, TypeObtainer obtainer) { - CacheEntry ce = (CacheEntry)MemoryContextAlloc(TopMemoryContext, sizeof(CacheEntryData)); + CacheEntry ce = (CacheEntry) + MemoryContextAlloc(TopMemoryContext, sizeof(CacheEntryData)); ce->typeId = typeId; ce->type = type; ce->obtainer = obtainer; @@ -1084,10 +1132,13 @@ static void _registerType(Oid typeId, const char* javaTypeName, Type type, TypeO void Type_registerType(const char* javaTypeName, Type type) { - _registerType(type->typeId, javaTypeName, type, (TypeObtainer)_PgObject_pureVirtualCalled); + _registerType( + type->typeId, javaTypeName, type, + (TypeObtainer)_PgObject_pureVirtualCalled); } -void Type_registerType2(Oid typeId, const char* javaTypeName, TypeObtainer obtainer) +void Type_registerType2( + Oid typeId, const char* javaTypeName, TypeObtainer obtainer) { _registerType(typeId, javaTypeName, 0, obtainer); } diff --git a/pljava-so/src/main/c/type/UDT.c b/pljava-so/src/main/c/type/UDT.c index 0f824c58..d351038f 100644 --- a/pljava-so/src/main/c/type/UDT.c +++ b/pljava-so/src/main/c/type/UDT.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -193,8 +193,11 @@ static Datum coerceScalarObject(UDT self, jobject value) { ereport(ERROR, ( errcode(ERRCODE_CANNOT_COERCE), - errmsg("UDT for Oid %d produced image with incorrect size. Expected %d, was %d", - Type_getOid((Type)self), dataLen, buffer.len))); + errmsg( + "UDT for Oid %d produced image with incorrect size. " + "Expected %d, was %d", + Type_getOid((Type)self), dataLen, buffer.len) + )); } if (passByValue) { memset(&result, 0, SIZEOF_DATUM); @@ -455,7 +458,11 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, { ereport(ERROR, ( errcode(ERRCODE_CANNOT_COERCE), - errmsg("Attempt to register UDT with Oid %d failed. Oid appoints a non UDT type", typeId))); + errmsg( + "Attempt to register UDT with Oid %d failed. " + "Oid appoints a non UDT type", + typeId) + )); } JNI_deleteLocalRef(parseMH); JNI_deleteLocalRef(readMH); @@ -464,7 +471,8 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, return (UDT)existing; } - nspTup = PgObject_getValidTuple(NAMESPACEOID, pgType->typnamespace, "namespace"); + nspTup = PgObject_getValidTuple( + NAMESPACEOID, pgType->typnamespace, "namespace"); nspStruct = (Form_pg_namespace)GETSTRUCT(nspTup); /* Concatenate namespace + '.' + typename @@ -501,7 +509,8 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, *sp++ = ';'; *sp = 0; - udtClass = TypeClass_alloc2("type.UDT", sizeof(struct TypeClass_), sizeof(struct UDT_)); + udtClass = TypeClass_alloc2( + "type.UDT", sizeof(struct TypeClass_), sizeof(struct UDT_)); udtClass->JNISignature = classSignature; udtClass->javaTypeName = className; @@ -516,7 +525,8 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, if(isJavaBasedScalar) { - /* A scalar mapping that is implemented in Java will have the static method: + /* A scalar mapping that is implemented in Java (a BaseUDT, as declared + * in Java source annotations) will have the static method: * * T parse(String stringRep, String sqlTypeName); * @@ -524,8 +534,8 @@ UDT UDT_registerUDT(jclass clazz, Oid typeId, Form_pg_type pgType, * * String toString(); * - * instance method. A pure mapping (i.e. no Java I/O methods) will not - * have this. + * instance method. A MappedUDT (i.e. no Java I/O methods) will not + * have them. */ /* The parse method is a static method on the class with the signature diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 5d0e1857..e580b693 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -124,6 +124,10 @@ extern jboolean pljava_Function_vpcInvoke( jobject invocable, jobject rowcollect, jlong call_cntr, jboolean close, jobject *result); +/* + * These are exposed so they can be called back from type/UDT.c. + * There is one for each flavor of UDT supporting function. + */ extern void pljava_Function_udtWriteInvoke( jobject invocable, jobject value, jobject stream); extern jstring pljava_Function_udtToStringInvoke( @@ -133,17 +137,20 @@ extern jobject pljava_Function_udtReadInvoke( extern jobject pljava_Function_udtParseInvoke( jobject invocable, jstring stringRep, jstring typeName); +/* + * These are exposed so they can be called back from type/Type.c when it is + * registering a MappedUDT. A MappedUDT has these two support functions, + * but never the parse/toString ones a BaseUDT has. + */ extern jobject pljava_Function_udtWriteHandle( jclass clazz, char *langName, bool trusted); -extern jobject pljava_Function_udtToStringHandle( - jclass clazz, char *langName, bool trusted); extern jobject pljava_Function_udtReadHandle( jclass clazz, char *langName, bool trusted); -extern jobject pljava_Function_udtParseHandle( - jclass clazz, char *langName, bool trusted); /* - * Returns the Type Map that is associated with the function + * Returns the type map that is held by the function's schema loader (the + * initiating loader that was used when the function was resolved). It is a map + * from Java Oid objects to Class objects, as resolved by that loader. */ extern jobject Function_getTypeMap(Function self); @@ -157,6 +164,9 @@ extern bool Function_isCurrentReadOnly(void); * Return a local reference to the initiating (schema) class loader used to load * the currently-executing function, or NULL if there is no currently-executing * function or the schema loaders have been cleared and that loader is gone. + * + * Invocation_getTypeMap is equivalent to calling this and then JNI-invoking + * getTypeMap on the returned loader (cast to PL/Java's loader subclass). */ extern jobject Function_currentLoader(void); diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index e3226a4e..bc572795 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -115,6 +115,13 @@ extern void Invocation_pushInvocation(Invocation* ctx); extern void Invocation_popInvocation(bool wasException); +/* + * Return the type map held by the innermost executing PL/Java function's + * schema loader (the initiating loader that was used to resolve the function). + * The type map is a map from Java Oid objects to Class class objects, + * as resolved by that loader. This is effectively Function_currentLoader() + * followed by JNI-invoking getTypeMap on the loader, but cached to avoid JNI). + */ extern jobject Invocation_getTypeMap(void); /* From 52548f6306ff0e14b61384b9e105d8017883867a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 3 Oct 2021 12:30:34 -0400 Subject: [PATCH 0898/1087] Some low-hanging fruit for static inline Includes a bit of refactoring so internal_call_handler doesn't separately call Function_getFunction and Function_invoke. --- pljava-so/src/main/c/Backend.c | 25 +++------ pljava-so/src/main/c/Function.c | 53 ++++++++++++++++---- pljava-so/src/main/c/type/Type.c | 4 +- pljava-so/src/main/include/pljava/Function.h | 44 ++++++---------- 4 files changed, 68 insertions(+), 58 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 6a8d4608..04d53a88 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1843,7 +1843,7 @@ static void registerGUCOptions(void) #undef PLJAVA_ENABLE_DEFAULT #undef PLJAVA_IMPLEMENTOR_FLAGS -static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS); +static inline Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS); extern PLJAVADLLEXPORT Datum javau_call_handler(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(javau_call_handler); @@ -1867,7 +1867,8 @@ Datum java_call_handler(PG_FUNCTION_ARGS) return internalCallHandler(true, fcinfo); } -static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS) +static inline Datum +internalCallHandler(bool trusted, PG_FUNCTION_ARGS) { Invocation ctx; Datum retval = 0; @@ -1890,20 +1891,8 @@ static Datum internalCallHandler(bool trusted, PG_FUNCTION_ARGS) Invocation_pushInvocation(&ctx); PG_TRY(); { - Function function = - Function_getFunction(funcoid, trusted, forTrigger, false, true); - if(forTrigger) - { - /* Called as a trigger procedure - */ - retval = Function_invokeTrigger(function, fcinfo); - } - else - { - /* Called as a function - */ - retval = Function_invoke(function, fcinfo); - } + retval = Function_invoke( + funcoid, trusted, forTrigger, false, true, fcinfo); Invocation_popInvocation(false); } PG_CATCH(); @@ -1990,8 +1979,8 @@ static Datum internalValidator(bool trusted, PG_FUNCTION_ARGS) if ( NULL != oidSaveLocation ) *oidSaveLocation = funcoid; - Function_getFunction( - funcoid, trusted, false, true, check_function_bodies); + Function_invoke( + funcoid, trusted, false, true, check_function_bodies, NULL); Invocation_popInvocation(false); } PG_CATCH(); diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index b246ddaf..bbc2ac7b 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -71,6 +71,8 @@ static jmethodID s_EntryPoints_udtParseInvoke; static PgObjectClass s_FunctionClass; static Type s_pgproc_Type; +static inline Datum invokeTrigger(Function self, PG_FUNCTION_ARGS); + static jobjectArray s_referenceParameters; static jvalue s_primitiveParameters [ 1 + 255 ]; @@ -399,7 +401,8 @@ jdouble pljava_Function_doubleInvoke(Function self) * jvalue slot for returns, though, must handle its own normal and exceptional * cleanup. */ -static jsize reserveParameterFrame(jsize refArgCount, jsize primArgCount) +static inline jsize +reserveParameterFrame(jsize refArgCount, jsize primArgCount) { jshort newCounts = COUNTCHECK(refArgCount, primArgCount); @@ -502,7 +505,8 @@ jobject pljava_Function_udtWriteHandle( s_Function_udtWriteHandle, clazz, langName, trusted); } -static jobject obtainUDTHandle( +static inline jobject +obtainUDTHandle( jmethodID which, jclass clazz, char *langName, bool trusted) { jstring jname = String_createJavaStringFromNTS(langName); @@ -512,7 +516,8 @@ static jobject obtainUDTHandle( return result; } -static jstring getSchemaName(int namespaceOid) +static inline jstring +getSchemaName(int namespaceOid) { HeapTuple nspTup = PgObject_getValidTuple(NAMESPACEOID, namespaceOid, "namespace"); Form_pg_namespace nspStruct = (Form_pg_namespace)GETSTRUCT(nspTup); @@ -752,13 +757,25 @@ static Function Function_create( } /* - * In all cases, this Function has been stored in currentInvocation->function - * upon successful return from here. + * Get a Function using a function Oid. If the function is not found, one + * will be created based on the class and method name denoted in the "AS" + * clause, the parameter types, and the return value of the function + * description. If "forTrigger" is true, the parameter type and + * return value of the function will be fixed to: + * void (org.postgresql.pljava.TriggerData td) + * + * If forValidator is true, forTrigger is disregarded, and will be determined + * from the function's pg_proc entry. If forValidator is false, checkBody has no + * meaning. * * If called with forValidator true, may return NULL. The validator doesn't * use the result. + * + * In all other cases, this Function has been stored + * in currentInvocation->function upon successful return from here. */ -Function Function_getFunction( +static inline Function +getFunction( Oid funcOid, bool trusted, bool forTrigger, bool forValidator, bool checkBody) { @@ -835,18 +852,31 @@ void Function_clearFunctionCache(void) * primitive. Hence this method, which requires both Type_isPrimitive to be true * and that the type is not an array. */ -static bool passAsPrimitive(Type t) +static inline bool +passAsPrimitive(Type t) { return Type_isPrimitive(t) && (NULL == Type_getElementType(t)); } -Datum Function_invoke(Function self, PG_FUNCTION_ARGS) +Datum +Function_invoke( + Oid funcoid, bool trusted, bool forTrigger, bool forValidator, + bool checkBody, PG_FUNCTION_ARGS) { + Function self; Datum retVal; Size passedArgCount; Type invokerType; bool skipParameterConversion = false; + self = getFunction(funcoid, trusted, forTrigger, forValidator, checkBody); + + if ( forValidator ) + PG_RETURN_VOID(); + + if ( forTrigger ) + return invokeTrigger(self, fcinfo); + fcinfo->isnull = false; if(self->isUDT) @@ -940,7 +970,12 @@ Datum Function_invoke(Function self, PG_FUNCTION_ARGS) return retVal; } -Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS) +/* + * Invoke a trigger. Wrap the TriggerData in org.postgresql.pljava.TriggerData + * object, make the call, and unwrap the resulting Tuple. + */ +static inline Datum +invokeTrigger(Function self, PG_FUNCTION_ARGS) { jobject jtd; Datum ret; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index fcdf440c..5eda8581 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -662,8 +662,8 @@ Type Type_fromOidCache(Oid typeId) * that depend only on the typeId (or Java class name, for the other direction) * from that point on. This is longstanding PL/Java behavior, but XXX. */ -static Type checkTypeMappedUDT( - Oid typeId, jobject typeMap, Form_pg_type typeStruct) +static inline Type +checkTypeMappedUDT(Oid typeId, jobject typeMap, Form_pg_type typeStruct) { jobject joid; jclass typeClass; diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index e580b693..6a0a30f6 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -42,22 +42,6 @@ extern "C" { */ extern void Function_clearFunctionCache(void); -/* - * Get a Function using a function Oid. If the function is not found, one - * will be created based on the class and method name denoted in the "AS" - * clause, the parameter types, and the return value of the function - * description. If "forTrigger" is true, the parameter type and - * return value of the function will be fixed to: - * void (org.postgresql.pljava.TriggerData td) - * - * If forValidator is true, forTrigger is disregarded, and will be determined - * from the function's pg_proc entry. If forValidator is false, checkBody has no - * meaning. - */ -extern Function Function_getFunction( - Oid funcOid, bool trusted, bool forTrigger, - bool forValidator, bool checkBody); - /* * Determine whether the type represented by typeId is declared as a * "Java-based scalar" a/k/a BaseUDT and, if so, return a freshly-registered @@ -66,20 +50,22 @@ extern Function Function_getFunction( extern Type Function_checkTypeBaseUDT(Oid typeId, Form_pg_type typeStruct); /* - * Invoke a trigger. Wrap the TriggerData in org.postgresql.pljava.TriggerData - * object, make the call, and unwrap the resulting Tuple. - */ -extern Datum Function_invokeTrigger(Function self, PG_FUNCTION_ARGS); - -/* - * Invoke a function, i.e. coerce the parameters, call the java method, and - * coerce the return value back to a Datum. The return-value coercion is handled - * by a convention where this call will delegate to the Type representing the - * SQL return type. That will call back on one of the flavors of fooInvoke below - * corresponding to the return type of the Java method, and then coerce that to - * the intended SQL type. + * First translate a function Oid to a Function (looking it up according to the + * trusted, forTrigger, forValidator, and checkBody parameters), and then + * (unless forValidator is true) invoke it: i.e. coerce the parameters, call the + * java method, and coerce the return value back to a Datum. The return-value + * coercion is handled by a convention where this call will delegate to the Type + * representing the SQL return type. That will call back on one of the flavors + * of fooInvoke below corresponding to the return type of the Java method, and + * then coerce that to the intended SQL type. + * + * If forValidator is true, NULL may be passed in the PG_FUNCTION_ARGS position. + * and NULL is returned immediately on successful validation. */ -extern Datum Function_invoke(Function self, PG_FUNCTION_ARGS); +extern Datum Function_invoke( + Oid funcoid, + bool trusted, bool forTrigger, bool forValidator, bool checkBody, + PG_FUNCTION_ARGS); /* * Most slots in the parameter area are set directly in invoke() or From fda2d90b9938f3dffed9ae080fa1de58bcb9dd64 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 6 Oct 2021 12:22:02 -0400 Subject: [PATCH 0899/1087] Manage ContextClassLoader around PL/Java functions This is done from the C side and, perhaps more controversially, using JNI to poke a non-exposed field in java.lang.Thread. PL/Java had not engaged in such tomfoolery before the JEP 411 warning situation required it, and this is now the second instance. It bears mentioning that, from ongoing discussion of JEP 411, the Java developers seem to be taking the position that any project wishing to offer some kind of permission enforcement post-JEP 411 is going to be more or less expected to do so by poking at non-exposed innards using JNI, JVMTI, or the like. In some post-1.6 PL/Java version, this kind of thing will have to become more common. In this case, it is simply to finally start setting the context loader appropriately (I do think it's arguably a correctness issue, as discussed in #361) while adding as little overhead as possible to the hot path of invoking functions. This code will detect, and fall back to not managing the context loader, if the target JRE lacks the expected field. It can also be ineffective if an application has subclassed Thread with a different behavior for getContextClassLoader. It seems tolerable to leave such cases unhandled. Because in PL/Java 1.6 the division of labor still has parameter and return value conversions done from the C side before and after the target method is invoked, and those conversions could involve Java UDTs whose support methods are not bound to context loaders in their own right, it makes sense to have the C code impose the target method's context loader in advance of the parameter conversions, with restoration only after return value conversion. Any UDT methods then consistently see the target method's context loader, whether they are being invoked 'outside' it for parameter or return conversion, or 'inside' it during PreparedStatement / ResultSet / SQLInput / SQLOutput operations. Likewise, leave class initialization to happen, as it normally would, on first use of a function from the class, when the right context loader will be in place. It had been pulled up to validation time to address a small quirk where OpenJ9's class resolution is so lazy it can overlook missing-dependency problems HotSpot would flag, but it's more fuss than the small quirk is worth. This commit introduces some C code in the typedef-function-signature pattern, rather than the typedef-function-pointer pattern prevalent in PostgreSQL upstream, as discussed in [0]. Being valid C89/"ANSI", and with PostgreSQL's documentation stipulating C89 or ANSI compilers as early as PG 8.4 [1], this is expected to cause no problems in an environment that can build any PG version PL/Java 1.6 supports. [0] https://www.postgresql.org/message-id/615C847A.7070609%40anastigmatix.net [1] https://www.postgresql.org/docs/8.4/source-format.html --- pljava-so/src/main/c/Backend.c | 1 + pljava-so/src/main/c/Exception.c | 4 +- pljava-so/src/main/c/Function.c | 67 ++++-- pljava-so/src/main/c/Invocation.c | 10 +- pljava-so/src/main/c/JNICalls.c | 210 ++++++++++++++++++ pljava-so/src/main/c/type/Type.c | 49 +++- pljava-so/src/main/include/pljava/Exception.h | 17 +- pljava-so/src/main/include/pljava/Function.h | 13 +- .../src/main/include/pljava/Invocation.h | 5 + pljava-so/src/main/include/pljava/JNICalls.h | 18 +- .../pljava/internal/EntryPoints.java | 20 +- .../postgresql/pljava/internal/Function.java | 9 +- 12 files changed, 367 insertions(+), 56 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 04d53a88..0d4ae9ff 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -761,6 +761,7 @@ static void initsequencer(enum initstage is, bool tolerant) /*FALLTHROUGH*/ case IS_PLJAVA_FOUND: + pljava_JNI_threadInitialize(); /* depends on thread GUC now committed */ greeting = InstallHelper_hello(); ereport(NULL != pljavaLoadPath ? NOTICE : DEBUG1, ( errmsg("PL/Java loaded"), diff --git a/pljava-so/src/main/c/Exception.c b/pljava-so/src/main/c/Exception.c index a3aecab2..071b8cf4 100644 --- a/pljava-so/src/main/c/Exception.c +++ b/pljava-so/src/main/c/Exception.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -43,6 +43,7 @@ jmethodID SQLException_getSQLState; jclass UnsupportedOperationException_class; jmethodID UnsupportedOperationException_init; +jclass NoSuchFieldError_class; jclass NoSuchMethodError_class; void @@ -197,6 +198,7 @@ void Exception_initialize(void) UnsupportedOperationException_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass("java/lang/UnsupportedOperationException")); UnsupportedOperationException_init = PgObject_getJavaMethod(UnsupportedOperationException_class, "", "(Ljava/lang/String;)V"); + NoSuchFieldError_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass("java/lang/NoSuchFieldError")); NoSuchMethodError_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass("java/lang/NoSuchMethodError")); Class_getName = PgObject_getJavaMethod(Class_class, "getName", "()Ljava/lang/String;"); diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index bbc2ac7b..010dcbce 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -103,10 +103,10 @@ struct Function_ jclass clazz; /** - * Weak global reference to the class loader for the schema in which this + * Global reference to the class loader for the schema in which this * function is declared. */ - jweak schemaLoader; + jobject schemaLoader; union { @@ -183,6 +183,7 @@ static void _Function_finalize(PgObject func) { Function self = (Function)func; JNI_deleteGlobalRef(self->clazz); + JNI_deleteGlobalRef(self->schemaLoader); if(!self->isUDT) { JNI_deleteGlobalRef(self->func.nonudt.invocable); @@ -430,6 +431,37 @@ reserveParameterFrame(jsize refArgCount, jsize primArgCount) return refArgCount + primArgCount; } +/* + * This should happen everywhere reserveParameterFrame happens, but is factored + * out to allow a couple of call sites to optimize out one or the other. As with + * reserveParameterFrame, the undoing of this happens in popFrame below. + * + * currentInvocation->savedLoader can have a "not known" value (which has to be + * distinct from null, because null is a perfectly cromulent context classloader + * as far as Java is concerned). Leaving it that way will mean no restoration at + * popInvocation time. The loaderUpdater may leave it that way in some cases, + * in a bid to reduce overhead if the same loader's wanted again. + */ +static inline void +installContextLoader(Function self) +{ + (*JNI_loaderUpdater)(self->schemaLoader); +} + +/* + * Not intended for any caller but Invocation_popInvocation. + */ +void pljava_Function_popFrame(bool heavy) +{ + if ( heavy ) + JNI_callStaticVoidMethod(s_ParameterFrame_class, s_ParameterFrame_pop); + + if ( (void *)-1 == currentInvocation->savedLoader ) + return; + + (*JNI_loaderRestorer)(); +} + /* * Invoke an Invocable that was obtained by invoking an Invocable for a * set-returning-function that returns results in value-per-call style. @@ -438,8 +470,8 @@ reserveParameterFrame(jsize refArgCount, jsize primArgCount) * result (true) or the end of results was reached (false). */ jboolean pljava_Function_vpcInvoke( - jobject invocable, jobject rowcollect, jlong call_cntr, jboolean close, - jobject *result) + Function self, jobject invocable, jobject rowcollect, jlong call_cntr, + jboolean close, jobject *result) { /* * When retrieving the very first row, this call happens under the same @@ -451,8 +483,14 @@ jboolean pljava_Function_vpcInvoke( * static area parameter counts; this reservation will therefore not see a * need to push a frame. If one was pushed for the user function itself, it * remains on top, to be popped when the Invocation is. + * + * It is better to avoid calling installContextLoader a second time under + * the same Invocation. */ reserveParameterFrame(1, 2); + if ( 0 != call_cntr ) + installContextLoader(self); + JNI_setObjectArrayElement(s_referenceParameters, 0, rowcollect); s_primitiveParameters[0].j = call_cntr; s_primitiveParameters[1].z = close; @@ -918,6 +956,8 @@ Function_invoke( (unsigned int)reservedArgCount, (unsigned int)passedArgCount); } + installContextLoader(self); + invokerType = self->func.nonudt.returnType; if ( passedArgCount > 0 && ! skipParameterConversion ) @@ -986,6 +1026,7 @@ invokeTrigger(Function self, PG_FUNCTION_ARGS) return 0; reserveParameterFrame(1, 0); + installContextLoader(self); JNI_setObjectArrayElement(s_referenceParameters, 0, jtd); @@ -1055,14 +1096,6 @@ void pljava_Function_setParameter(Function self, int index, jvalue value) JNI_setObjectArrayElement(s_referenceParameters, numRefs - 1, value.l); } -/* - * Not intended for any caller but Invocation_popInvocation. - */ -void pljava_Function_popFrame() -{ - JNI_callStaticVoidMethod(s_ParameterFrame_class, s_ParameterFrame_pop); -} - bool Function_isCurrentReadOnly(void) { /* function will be 0 during resolve of class and java function. At @@ -1076,17 +1109,13 @@ bool Function_isCurrentReadOnly(void) jobject Function_currentLoader(void) { Function f; - jweak weakRef; if ( NULL == currentInvocation ) return NULL; f = currentInvocation->function; if ( NULL == f ) return NULL; - weakRef = f->schemaLoader; - if ( NULL == weakRef ) - return NULL; - return JNI_newLocalRef(weakRef); + return f->schemaLoader; } /* @@ -1138,7 +1167,7 @@ JNIEXPORT jboolean JNICALL { self->isUDT = false; self->readOnly = (JNI_TRUE == readOnly); - self->schemaLoader = JNI_newWeakGlobalRef(schemaLoader); + self->schemaLoader = JNI_newGlobalRef(schemaLoader); self->clazz = JNI_newGlobalRef(clazz); self->func.nonudt.isMultiCall = (JNI_TRUE == isMultiCall); self->func.nonudt.typeMap = @@ -1256,7 +1285,7 @@ JNIEXPORT void JNICALL { self->isUDT = true; self->readOnly = (JNI_TRUE == readOnly); - self->schemaLoader = JNI_newWeakGlobalRef(schemaLoader); + self->schemaLoader = JNI_newGlobalRef(schemaLoader); self->clazz = JNI_newGlobalRef(clazz); /* diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 52b1cc10..25c4b621 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -167,6 +167,7 @@ void Invocation_pushInvocation(Invocation* ctx) ctx->function = 0; ctx->frameLimits = *s_frameLimits; ctx->primSlot0 = *s_primSlot0; + ctx->savedLoader = (void *)-1; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; @@ -182,13 +183,13 @@ void Invocation_pushInvocation(Invocation* ctx) void Invocation_popInvocation(bool wasException) { Invocation* ctx = currentInvocation->previous; + bool heavy = FRAME_LIMITS_PUSHED == currentInvocation->frameLimits; /* - * If the more heavyweight parameter-frame push got done, undo it. + * If the more heavyweight parameter-frame push wasn't done, do + * the lighter cleanup here. */ - if ( FRAME_LIMITS_PUSHED == currentInvocation->frameLimits ) - pljava_Function_popFrame(); - else + if ( ! heavy ) { /* * The lighter-weight cleanup. @@ -196,6 +197,7 @@ void Invocation_popInvocation(bool wasException) *s_frameLimits = currentInvocation->frameLimits; *s_primSlot0 = currentInvocation->primSlot0; } + pljava_Function_popFrame(heavy); /* * If a Java Invocation instance was created and associated with this diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 63d5d51a..49f3df0e 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -34,13 +34,91 @@ static jobject s_threadLock; static bool s_refuseOtherThreads = false; static bool s_doMonitorOps = true; +static jclass s_Thread_class; +static jmethodID s_Thread_currentThread; +static jfieldID s_Thread_contextLoader; + +static jobject s_threadObject; + void pljava_JNI_setThreadPolicy(bool refuseOtherThreads, bool doMonitorOps) { s_refuseOtherThreads = refuseOtherThreads; s_doMonitorOps = doMonitorOps; } +/* + * This file contains very specialized methods for updating the context + * class loader of a thread, because this is where they can be implemented + * without the overhead of several calls to wrappers defined here. + * + * More lightweight implementations of those can be chosen if the selected + * thread policy precludes native access from any but the primordial thread. + */ +JNI_ContextLoaderUpdater *JNI_loaderUpdater; +JNI_ContextLoaderRestorer *JNI_loaderRestorer; + +static JNI_ContextLoaderUpdater _noopUpdater; +static JNI_ContextLoaderRestorer _noopRestorer; + +static JNI_ContextLoaderUpdater _lightUpdater; +static JNI_ContextLoaderRestorer _lightRestorer; + +static JNI_ContextLoaderUpdater _heavyUpdater; +static JNI_ContextLoaderRestorer _heavyRestorer; + +void pljava_JNI_threadInitialize() +{ + s_Thread_class = JNI_newGlobalRef(PgObject_getJavaClass( + "java/lang/Thread")); + s_Thread_currentThread = PgObject_getStaticJavaMethod( + s_Thread_class, + "currentThread", + "()" + "Ljava/lang/Thread;"); + s_Thread_contextLoader = JNI_getFieldIDOrNull(s_Thread_class, + "contextClassLoader", "Ljava/lang/ClassLoader;"); + + if ( NULL == s_Thread_contextLoader ) + { + ereport(WARNING, ( + errmsg("unable to manage thread context classloaders in this JVM") + )); + JNI_loaderUpdater = _noopUpdater; + JNI_loaderRestorer = _noopRestorer; + } + else if ( s_refuseOtherThreads || ! s_doMonitorOps ) + { + s_threadObject = + JNI_newGlobalRef( + JNI_callStaticObjectMethod( + s_Thread_class, s_Thread_currentThread)); + JNI_loaderUpdater = _lightUpdater; + JNI_loaderRestorer = _lightRestorer; + } + else + { + JNI_loaderUpdater = _heavyUpdater; + JNI_loaderRestorer = _heavyRestorer; + } +} + +/* + * BEGIN_JAVA and END_JAVA are used in JNI wrappers that are not expected to + * invoke Java methods; all they do is play the game with the scope of the JNI + * env value that was devised to fail fast if the intended pattern for PL/Java's + * JNI usage isn't followed. + * + * BEGIN_CALL and END_CALL add to that the releasing of the "THREADLOCK" + * monitor when calling into Java, and reacquiring it on return, that support + * the java_thread_pg_entry=allow mode of operation, and also checking for + * exceptions and turning them into PostgreSQL ereports. + * + * The _MONITOR_HELD flavors of those skip the monitor operations but still do + * the exception checks. They are used in a select few *Locked flavors of + * method call wrappers used where only known and lightweight Java methods will + * be invoked and not arbitrary methods of user code. + */ #define BEGIN_JAVA { JNIEnv* env = jniEnv; jniEnv = 0; #define END_JAVA jniEnv = env; } @@ -955,6 +1033,33 @@ jfieldID JNI_getFieldID(jclass clazz, const char* name, const char* sig) return result; } +jfieldID JNI_getFieldIDOrNull(jclass clazz, const char* name, const char* sig) +{ + jfieldID result; + jobject exh; + BEGIN_CALL + result = (*env)->GetFieldID(env, clazz, name, sig); + if(result == 0) { + exh = (*env)->ExceptionOccurred(env); + if ( 0 != exh ) + { + /* + * Ignore a NoSuchFieldError, but not any other exception. + * This operation order (first clear the pending exception, then + * do the IsInstanceOf check, then Throw again if not the expected + * class) avoids a benign -Xcheck:JNI warning about calling + * IsInstanceOf while an exception is pending. + */ + (*env)->ExceptionClear(env); + if ( ! (*env)->IsInstanceOf(env, exh, NoSuchFieldError_class) ) + (*env)->Throw(env, exh); + (*env)->DeleteLocalRef(env, exh); + } + } + END_CALL + return result; +} + jfloat* JNI_getFloatArrayElements(jfloatArray array, jboolean* isCopy) { jfloat* result; @@ -1508,3 +1613,108 @@ jint JNI_throw(jthrowable obj) END_JAVA return result; } + +/* + * Implementations of the context class loader updater and restorer. + * The loader reference passed in is not to be deleted. If saved anywhere, + * a new global ref is to be taken, and later deleted when restored. + */ + +static inline void _updaterCommon(JNIEnv *env, jobject thread, jobject loader) +{ + jobject old = (*env)->GetObjectField(env, thread, s_Thread_contextLoader); + + /* + * If it is not already the loader we want, change it, and set + * currentInvocation->savedLoader to restore it later. If this is + * a top-level invocation, we don't care what it gets restored to, so lie, + * and save loader there instead of old. If there are many consecutive + * top-level calls with the same context loader, that will save work later. + * + * If it is already the loader we want, again we check for a top-level call, + * and can leave currentInvocation->savedLoader completely unset in that + * case, so the restore call will be skipped completely. If not a top-level + * call, though, see that it gets restored to what the caller might expect, + * even if it somehow got changed. + */ + + if ( ! (*env)->IsSameObject(env, old, loader) ) + { + (*env)->SetObjectField(env, thread, s_Thread_contextLoader, loader); + + currentInvocation->savedLoader = (*env)->NewGlobalRef(env, + ( NULL == currentInvocation->previous ) ? loader : old); + } + else if ( NULL != currentInvocation->previous ) + currentInvocation->savedLoader = (*env)->NewGlobalRef(env, old); + + (*env)->DeleteLocalRef(env, old); +} + +static void _heavyUpdater(jobject loader) +{ + jobject thread; + + BEGIN_JAVA + + thread = + (*env)->CallStaticObjectMethod(env, + s_Thread_class, s_Thread_currentThread); + + _updaterCommon(env, thread, loader); + + (*env)->DeleteLocalRef(env, thread); + + END_JAVA +} + +void _heavyRestorer() +{ + jobject thread; + jobject value; + + BEGIN_JAVA + + thread = + (*env)->CallStaticObjectMethod(env, + s_Thread_class, s_Thread_currentThread); + + value = currentInvocation->savedLoader; + + (*env)->SetObjectField(env, thread, s_Thread_contextLoader, value); + (*env)->DeleteGlobalRef(env, value); + (*env)->DeleteLocalRef(env, thread); + + END_JAVA +} + +static void _lightUpdater(jobject loader) +{ + BEGIN_JAVA + + _updaterCommon(env, s_threadObject, loader); + + END_JAVA +} + +void _lightRestorer() +{ + jobject value; + + BEGIN_JAVA + + value = currentInvocation->savedLoader; + + (*env)->SetObjectField(env, s_threadObject, s_Thread_contextLoader, value); + (*env)->DeleteGlobalRef(env, value); + + END_JAVA +} + +static void _noopUpdater(jobject loader) +{ +} + +void _noopRestorer() +{ +} diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 5eda8581..61bbc56a 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -107,6 +107,7 @@ static jmethodID s_TypeBridge_Holder_payload; typedef struct { Type elemType; + Function fn; jobject rowProducer; jobject rowCollector; /* @@ -163,7 +164,19 @@ static void _closeIteration(CallContextData* ctxData) currentInvocation->hasConnected = ctxData->hasConnected; currentInvocation->invocation = ctxData->invocation; - pljava_Function_vpcInvoke(ctxData->rowProducer, NULL, 0, JNI_TRUE, &dummy); + /* + * Why pass 1 as the call_cntr? We won't always have the actual call_cntr + * value at _closeIteration time (the _endOfSetCB isn't passed it), and the + * Java interfaces being used don't need it (close() isn't passed a row + * number), but vpcInvoke needs a way to know when to skip its call to + * installContextLoader (which shouldn't happen on its very first call, + * the one with call_cntr of zero, which always happens as part of the first + * invocation of the SRF so the work has already been done). So passing any + * fixed value here that isn't zero is enough to distinguish the cases. + */ + pljava_Function_vpcInvoke( + ctxData->fn, ctxData->rowProducer, NULL, 1, JNI_TRUE, &dummy); + JNI_deleteGlobalRef(ctxData->rowProducer); if(ctxData->rowCollector != 0) JNI_deleteGlobalRef(ctxData->rowCollector); @@ -193,16 +206,31 @@ static void _closeIteration(CallContextData* ctxData) */ static void _endOfSetCB(Datum arg) { - Invocation topCall; - bool saveInExprCtxCB; + Invocation ctx; CallContextData* ctxData = (CallContextData*)DatumGetPointer(arg); - if(currentInvocation == 0) - Invocation_pushInvocation(&topCall); - saveInExprCtxCB = currentInvocation->inExprContextCB; - currentInvocation->inExprContextCB = true; - _closeIteration(ctxData); - currentInvocation->inExprContextCB = saveInExprCtxCB; + /* + * Even if there is an invocation already on the stack, there is no + * convincing reason to think this callback belongs to it; PostgreSQL + * will make this callback when the expression context we did belong to + * is being torn down. This is not a hot operation; it only happens in + * rare cases when an SRF has been called and not completely consumed. + * So just unconditionally set up a context for this call, and clean up + * our own mess. + */ + PG_TRY(); + { + Invocation_pushInvocation(&ctx); + currentInvocation->inExprContextCB = true; + _closeIteration(ctxData); + Invocation_popInvocation(false); + } + PG_CATCH(); + { + Invocation_popInvocation(true); + PG_RE_THROW(); + } + PG_END_TRY(); } static Type _getCoerce(Type self, Type other, Oid fromOid, Oid toOid, @@ -503,6 +531,7 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) context->user_fctx = ctxData; ctxData->elemType = self; + ctxData->fn = fn; ctxData->rowProducer = JNI_newGlobalRef(tmp); JNI_deleteLocalRef(tmp); @@ -545,7 +574,7 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) currentInvocation->hasConnected = ctxData->hasConnected; currentInvocation->invocation = ctxData->invocation; - if(JNI_TRUE == pljava_Function_vpcInvoke( + if(JNI_TRUE == pljava_Function_vpcInvoke(ctxData->fn, ctxData->rowProducer, ctxData->rowCollector, (jlong)context->call_cntr, JNI_FALSE, &row)) { diff --git a/pljava-so/src/main/include/pljava/Exception.h b/pljava-so/src/main/include/pljava/Exception.h index e9b10a9e..a2edad91 100644 --- a/pljava-so/src/main/include/pljava/Exception.h +++ b/pljava-so/src/main/include/pljava/Exception.h @@ -1,8 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2021 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: + * Tada AB + * Chapman Flack * * @author Thomas Hallgren */ @@ -79,6 +85,9 @@ extern void Exception_throw_ERROR(const char* function); */ extern void Exception_throwMemberError(const char* memberName, const char* signature, bool isMethod, bool isStatic); +extern jclass NoSuchFieldError_class; +extern jclass NoSuchMethodError_class; + #ifdef __cplusplus } #endif diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 6a0a30f6..1dcc8262 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -78,8 +78,10 @@ extern void pljava_Function_setParameter(Function self, int idx, jvalue val); /* * Not intended for any caller other than Invocation_popInvocation. + * 'heavy' indicates that the heavy form of parameter-frame saving has been used + * and must be undone. */ -extern void pljava_Function_popFrame(void); +extern void pljava_Function_popFrame(bool heavy); /* * These actually invoke a target Java method (returning, respectively, a @@ -107,8 +109,8 @@ extern jdouble pljava_Function_doubleInvoke(Function self); * indicate whether a row was retrieved, AND puts a value (or null) in *result. */ extern jboolean pljava_Function_vpcInvoke( - jobject invocable, jobject rowcollect, jlong call_cntr, jboolean close, - jobject *result); + Function self, jobject invocable, jobject rowcollect, jlong call_cntr, + jboolean close, jobject *result); /* * These are exposed so they can be called back from type/UDT.c. @@ -147,9 +149,8 @@ extern jobject Function_getTypeMap(Function self); extern bool Function_isCurrentReadOnly(void); /* - * Return a local reference to the initiating (schema) class loader used to load - * the currently-executing function, or NULL if there is no currently-executing - * function or the schema loaders have been cleared and that loader is gone. + * Return a global reference to the initiating (schema) class loader used + * to load the currently-executing function. * * Invocation_getTypeMap is equivalent to calling this and then JNI-invoking * getTypeMap on the returned loader (cast to PL/Java's loader subclass). diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index bc572795..28bebe41 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -71,6 +71,11 @@ struct Invocation_ */ jvalue primSlot0; + /** + * The saved thread context classloader from before this invocation + */ + jobject savedLoader; + /** * The currently executing Function. */ diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index 17ba967a..e8d1c38b 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -65,8 +65,6 @@ extern jmethodID SQLException_getSQLState; extern jclass UnsupportedOperationException_class; extern jmethodID UnsupportedOperationException_init; -extern jclass NoSuchMethodError_class; - /* * Method called from Backend.c to set the thread policy. The first parameter * indicates whether to throw an exception if a thread other than the main one @@ -86,6 +84,19 @@ extern jclass NoSuchMethodError_class; */ extern void pljava_JNI_setThreadPolicy(bool,bool); +/* + * Two specialized wrappers to reduce the overhead of multiple wrapped calls + * for a frequent sequence of operations. The threadInitialize method, called + * from Backend.c once the java_thread_pg_entry GUC setting is frozen in place, + * populates the function pointers with the appropriate implementations. + */ +extern void pljava_JNI_threadInitialize(void); +typedef void JNI_ContextLoaderUpdater(jobject loader); +typedef void JNI_ContextLoaderRestorer(void); + +extern JNI_ContextLoaderUpdater *JNI_loaderUpdater; +extern JNI_ContextLoaderRestorer *JNI_loaderRestorer; + /* * A few very specialized JNI method-invocation wrappers, that do NOT do * one thing all the rest of the method wrappers do. These do NOT release the @@ -178,6 +189,7 @@ extern void JNI_getByteArrayRegion(jbyteArray array, jsize start, jsize extern jboolean* JNI_getBooleanArrayElements(jbooleanArray array, jboolean* isCopy); extern void JNI_getBooleanArrayRegion(jbooleanArray array, jsize start, jsize len, jboolean* buf); extern jfieldID JNI_getFieldID(jclass clazz, const char* name, const char* sig); +extern jfieldID JNI_getFieldIDOrNull(jclass clazz, const char* name, const char* sig); extern jdouble* JNI_getDoubleArrayElements(jdoubleArray array, jboolean* isCopy); extern void JNI_getDoubleArrayRegion(jdoubleArray array, jsize start, jsize len, jdouble* buf); extern jfloat* JNI_getFloatArrayElements(jfloatArray array, jboolean* isCopy); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java index fb0ff15c..70151cc0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/EntryPoints.java @@ -46,9 +46,10 @@ * The *invoke methods in this class can be private, as they are invoked only * from C via JNI, not from Java. *

    - * The primary entry point is {@code invoke}. The supplied - * {@code PrivilegedAction}, as - * obtained from {@code Function.create}, may have bound references to static + * The primary entry point is {@code invoke}. The supplied {@code Invocable}, + * created by {@code invocable} below for its caller {@code Function.create}, + * may contain a {@code MethodHandle}, or a {@code PrivilegedAction} that + * wraps one. The {@code MethodHandle} may have bound references to static * parameter areas, and will fetch the actual parameters from there to the * stack before invoking the (potentially reentrant) target method. Primitive * return values are then stored (after the potentially reentrant method has @@ -58,11 +59,14 @@ * The {@code PrivilegedAction} is expected to return null for a {@code void} * or primitive-typed target. *

    - * The remaining methods here are for user-defined type (UDT) support. For now, - * those are not consolidated into the {@code invoke}/{@code refInvoke} pattern, - * as UDTs may need to be constructed from the C code while it is populating the - * static parameter area for the ultimate target method, so the UDT methods - * must be invocable without using the same area. + * The remaining {@code fooInvoke} methods here are for user-defined type (UDT) + * support. For now, those are not consolidated into the general {@code invoke} + * pattern, as UDT support methods may need to be called from the C code + * while it is populating the static parameter area for an ultimate target + * method, so they must be invocable without using the same area. + *

    + * An {@code Invocable} carries the {@code AccessControlContext} under which the + * invocation target will execute. */ class EntryPoints { diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 1151da85..33332522 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1334,7 +1334,14 @@ private static Invocable init( AccessControlContext acc = accessControlContextFor(clazz, language, trusted); - if ( forValidator && clazz != loadClass(schemaLoader, className, acc) ) + /* + * false, to leave initialization until the function's first invocation, + * when naturally the right ContextClassLoader and AccessControlContext + * will be in place. Overkill to do more just for a low-impact OpenJ9 + * quirk. + */ + if ( false && forValidator + && clazz != loadClass(schemaLoader, className, acc) ) throw new SQLException( "Initialization of class \"" + className + "\" produced a " + "different class object"); From e48e719f3615ee1e2069f57aa10d4034df566b8d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 6 Oct 2021 12:31:28 -0400 Subject: [PATCH 0900/1087] Quiet -Xcheck:jni for a should-never-happen case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slightly increases overhead for the java_thread_pg_entry == allow case. Bottom lines now (X5650, relative to 7a7f394): java_thread_pg_entry == allow: 1.6 µs per call, every call requires setting 0.7 µs per call, consecutive calls use same loader java_thread_pg_entry != allow: 0.7 µs per call, every call requires setting 0.2 µs per call, consecutive calls use same loader --- pljava-so/src/main/c/JNICalls.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 49f3df0e..725fa5e8 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -1654,12 +1654,20 @@ static inline void _updaterCommon(JNIEnv *env, jobject thread, jobject loader) static void _heavyUpdater(jobject loader) { jobject thread; + jobject exh; BEGIN_JAVA thread = (*env)->CallStaticObjectMethod(env, - s_Thread_class, s_Thread_currentThread); + s_Thread_class, s_Thread_currentThread); /* should never fail */ + + exh = (*env)->ExceptionOccurred(env); /* but mollify -Xcheck:jni anyway */ + if(exh != 0) + { + (*env)->ExceptionClear(env); + elogExceptionMessage(env, exh, ERROR); + } _updaterCommon(env, thread, loader); @@ -1672,12 +1680,20 @@ void _heavyRestorer() { jobject thread; jobject value; + jobject exh; BEGIN_JAVA thread = (*env)->CallStaticObjectMethod(env, - s_Thread_class, s_Thread_currentThread); + s_Thread_class, s_Thread_currentThread); /* should never fail */ + + exh = (*env)->ExceptionOccurred(env); /* but mollify -Xcheck:jni anyway */ + if(exh != 0) + { + (*env)->ExceptionClear(env); + elogExceptionMessage(env, exh, ERROR); + } value = currentInvocation->savedLoader; From 13b28418dd5770cff80b73f7aa551981a2b5853c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 6 Oct 2021 12:33:23 -0400 Subject: [PATCH 0901/1087] Allow opt-out from context loader management Done as a system property interpreted in InstallHelper, rather than as a GUC, in (a) recognition that it's obscure and may be little used, and (b) anticipation that other allowed settings may evolve in ways that might be easier to parse/validate in Java. --- pljava-so/src/main/c/Backend.c | 1 - pljava-so/src/main/c/InstallHelper.c | 7 +++++++ pljava-so/src/main/c/JNICalls.c | 18 ++++++++++++++++- pljava-so/src/main/include/pljava/JNICalls.h | 3 ++- .../pljava/internal/InstallHelper.java | 20 +++++++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 0d4ae9ff..04d53a88 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -761,7 +761,6 @@ static void initsequencer(enum initstage is, bool tolerant) /*FALLTHROUGH*/ case IS_PLJAVA_FOUND: - pljava_JNI_threadInitialize(); /* depends on thread GUC now committed */ greeting = InstallHelper_hello(); ereport(NULL != pljavaLoadPath ? NOTICE : DEBUG1, ( errmsg("PL/Java loaded"), diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 9ae88b8d..30af880a 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -108,6 +108,7 @@ static jclass s_InstallHelper_class; static jmethodID s_InstallHelper_hello; static jmethodID s_InstallHelper_groundwork; +static jfieldID s_InstallHelper_MANAGE_CONTEXT_LOADER; static bool extensionExNihilo = false; @@ -545,6 +546,10 @@ char *InstallHelper_hello() jstring greeting; char *greetingC; char const *clusternameC = pljavaClusterName(); + jboolean manageContext = JNI_getStaticBooleanField(s_InstallHelper_class, + s_InstallHelper_MANAGE_CONTEXT_LOADER); + + pljava_JNI_threadInitialize(JNI_TRUE == manageContext); Invocation_pushBootContext(&ctx); nativeVer = String_createJavaStringFromNTS(SO_VERSION_STRING); @@ -667,6 +672,8 @@ void InstallHelper_initialize() { s_InstallHelper_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/InstallHelper")); + s_InstallHelper_MANAGE_CONTEXT_LOADER = PgObject_getStaticJavaField( + s_InstallHelper_class, "MANAGE_CONTEXT_LOADER", "Z"); s_InstallHelper_hello = PgObject_getStaticJavaMethod(s_InstallHelper_class, "hello", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 725fa5e8..ba807c51 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -66,8 +66,15 @@ static JNI_ContextLoaderRestorer _lightRestorer; static JNI_ContextLoaderUpdater _heavyUpdater; static JNI_ContextLoaderRestorer _heavyRestorer; -void pljava_JNI_threadInitialize() +void pljava_JNI_threadInitialize(bool manageLoader) { + if ( ! manageLoader ) + { + JNI_loaderUpdater = _noopUpdater; + JNI_loaderRestorer = _noopRestorer; + return; + } + s_Thread_class = JNI_newGlobalRef(PgObject_getJavaClass( "java/lang/Thread")); s_Thread_currentThread = PgObject_getStaticJavaMethod( @@ -1214,6 +1221,15 @@ jmethodID JNI_getStaticMethodIDOrNull(jclass clazz, const char* name, const char return result; } +jboolean JNI_getStaticBooleanField(jclass clazz, jfieldID field) +{ + jboolean result; + BEGIN_JAVA + result = (*env)->GetStaticBooleanField(env, clazz, field); + END_JAVA + return result; +} + jint JNI_getStaticIntField(jclass clazz, jfieldID field) { jint result; diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index e8d1c38b..98cf10ff 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -90,7 +90,7 @@ extern void pljava_JNI_setThreadPolicy(bool,bool); * from Backend.c once the java_thread_pg_entry GUC setting is frozen in place, * populates the function pointers with the appropriate implementations. */ -extern void pljava_JNI_threadInitialize(void); +extern void pljava_JNI_threadInitialize(bool manageLoader); typedef void JNI_ContextLoaderUpdater(jobject loader); typedef void JNI_ContextLoaderRestorer(void); @@ -208,6 +208,7 @@ extern void JNI_getShortArrayRegion(jshortArray array, jsize start, jsiz extern jfieldID JNI_getStaticFieldID(jclass clazz, const char* name, const char* sig); extern jmethodID JNI_getStaticMethodID(jclass clazz, const char* name, const char* sig); extern jmethodID JNI_getStaticMethodIDOrNull(jclass clazz, const char* name, const char* sig); +extern jboolean JNI_getStaticBooleanField(jclass clazz, jfieldID field); extern jint JNI_getStaticIntField(jclass clazz, jfieldID field); extern jobject JNI_getStaticObjectField(jclass clazz, jfieldID field); extern const char* JNI_getStringUTFChars(jstring string, jboolean* isCopy); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index f91633fc..99a0f5ca 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -42,6 +42,7 @@ import org.postgresql.pljava.management.SQLDeploymentDescriptor; import org.postgresql.pljava.policy.TrialPolicy; import static org.postgresql.pljava.annotation.processing.DDRWriter.eQuote; +import static org.postgresql.pljava.elog.ELogHandler.LOG_WARNING; import static org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; /** @@ -52,6 +53,25 @@ */ public class InstallHelper { + static final boolean MANAGE_CONTEXT_LOADER; + + static + { + String manageLoaderProp = "org.postgresql.pljava.context.loader"; + String s = System.getProperty(manageLoaderProp); + if ( null == s ) + MANAGE_CONTEXT_LOADER = true; + else if ( "unmanaged".equals(s) ) + MANAGE_CONTEXT_LOADER = false; + else + { + MANAGE_CONTEXT_LOADER = false; + Backend.log(LOG_WARNING, + "value \"" + s + "\" for " + manageLoaderProp + + " unrecognized; using \"unmanaged\""); + } + } + private static void setPropertyIfNull( String property, String value) { if ( null == System.getProperty( property) ) From 98f683006ae9541d0b277d1448a40013460eddbb Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 6 Oct 2021 12:34:47 -0400 Subject: [PATCH 0902/1087] Insulate SQLXMLImpl behavior from context loader Arguably, these changes should have been made as soon as 1.6.x targeted Java 9 as baseline. --- .../org/postgresql/pljava/jdbc/SQLXMLImpl.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 28f58636..1b3bb2a0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -634,7 +634,7 @@ private static boolean useWrappingElement(InputStream is, Reader r) boolean mustBeDocument = false; boolean cantBeDocument = false; - XMLInputFactory xif = XMLInputFactory.newInstance(); + XMLInputFactory xif = XMLInputFactory.newDefaultFactory(); xif.setProperty(xif.IS_NAMESPACE_AWARE, true); xif.setProperty(xif.SUPPORT_DTD, false);// will still report one it sees xif.setProperty(xif.IS_REPLACING_ENTITY_REFERENCES, false); @@ -1409,7 +1409,7 @@ private T setResult( || rc.isAssignableFrom(AdjustingSAXResult.class) ) { SAXTransformerFactory saxtf = (SAXTransformerFactory) - SAXTransformerFactory.newInstance(); + SAXTransformerFactory.newDefaultInstance(); TransformerHandler th = saxtf.newTransformerHandler(); th.getTransformer().setOutputProperty( ENCODING, m_serverCS.name()); @@ -1426,7 +1426,7 @@ private T setResult( if ( rc.isAssignableFrom(StAXResult.class) ) { - XMLOutputFactory xof = XMLOutputFactory.newInstance(); + XMLOutputFactory xof = XMLOutputFactory.newDefaultFactory(); os = new DeclCheckedOutputStream(os, m_serverCS); XMLStreamWriter xsw = xof.createXMLStreamWriter( os, m_serverCS.name()); @@ -1459,7 +1459,7 @@ private void serializeDOM(DOMResult r, OutputStream os) DOMSource src = new DOMSource(r.getNode()); try { - TransformerFactory tf = TransformerFactory.newInstance(); + TransformerFactory tf = TransformerFactory.newDefaultInstance(); Transformer t = tf.newTransformer(); t.setOutputProperty(ENCODING, m_serverCS.name()); os = new DeclCheckedOutputStream(os, m_serverCS); @@ -4018,9 +4018,9 @@ Writable finish() throws IOException, SQLException { StAXResult str = m_tgt.setResult( m_tgt.backingIfNotFreed(), StAXResult.class); - XMLInputFactory xif = XMLInputFactory.newInstance(); + XMLInputFactory xif = XMLInputFactory.newDefaultFactory(); xif.setProperty(xif.IS_NAMESPACE_AWARE, true); - XMLOutputFactory xof = XMLOutputFactory.newInstance(); + XMLOutputFactory xof = XMLOutputFactory.newDefaultFactory(); /* * The Source has either an event reader or a stream reader. Use * the event reader directly, or create one around the stream @@ -4904,7 +4904,7 @@ private AdjustingSAXSource() // only for Dummy { m_is = is; m_wrapped = wrapped; - m_spf = SAXParserFactory.newInstance(); + m_spf = SAXParserFactory.newDefaultInstance(); m_spf.setNamespaceAware(true); } @@ -5192,7 +5192,7 @@ static class AdjustingStAXSource AdjustingStAXSource(InputStream is, Charset serverCS, boolean wrapped) throws XMLStreamException { - m_xif = XMLInputFactory.newInstance(); + m_xif = XMLInputFactory.newDefaultFactory(); m_xif.setProperty(m_xif.IS_NAMESPACE_AWARE, true); m_is = is; m_serverCS = serverCS; @@ -5333,7 +5333,7 @@ static class AdjustingDOMSource AdjustingDOMSource(InputStream is, boolean wrapped) { - m_dbf = DocumentBuilderFactory.newInstance(); + m_dbf = DocumentBuilderFactory.newDefaultInstance(); m_dbf.setNamespaceAware(true); m_is = is; m_wrapped = wrapped; From 1d31baf2aa550d3d7e085cc13c0fef4b8546d52c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 6 Oct 2021 12:34:47 -0400 Subject: [PATCH 0903/1087] Allow XSLT examples to use builtin impl or not Add a 'builtin' argument (default true) requiring Java to find its own in-the-box XSLT implementation; when false, an implementation via the (now properly managed!) context classloader may be found; for example, XSLT 3.0 transforms can be used if the schema class path includes Saxon. When not using the builtin implementation, treat as warnings, rather than errors, any failure to recognize specific features/attributes the builin implementation is expected to recognize. It's appropriate for the static transformer factory s_tf to be the builtin one. --- .../pljava/example/annotation/PassXML.java | 69 ++++++++++++++++--- 1 file changed, 58 insertions(+), 11 deletions(-) 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 6a2ef2f8..e735376c 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 @@ -50,6 +50,7 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; @@ -215,7 +216,7 @@ requires={"prepareXMLTransform", "transformXML"}, install={ "REVOKE EXECUTE ON FUNCTION javatest.prepareXMLTransformWithJava" + - " (pg_catalog.varchar, pg_catalog.xml, integer, boolean," + + " (pg_catalog.varchar, pg_catalog.xml, integer, boolean, boolean," + " pg_catalog.RECORD)" + " FROM PUBLIC", @@ -240,7 +241,7 @@ " " + " " + " " + - "', 5, true)", + "', how => 5, enableExtensionFunctions => true)", "SELECT" + " javatest.prepareXMLTransformWithJava('getPLJavaVersion'," + @@ -285,7 +286,7 @@ public class PassXML implements SQLData { static SQLXML s_sx; - static TransformerFactory s_tf = TransformerFactory.newInstance(); + static TransformerFactory s_tf = TransformerFactory.newDefaultInstance(); static Map s_tpls = new HashMap<>(); @@ -429,6 +430,17 @@ public static SQLXML castTextXML(@SQLType("text") SQLXML sx) * transform to use extensions that the Java XSLT implementation supports, * such as functions from EXSLT. Those are disabled by default. *

    + * Passing {@code false} for {@code builtin} will allow a + * {@code TransformerFactory} other than Java's built-in one to be found + * using the usual search order and the context class loader (normally + * the PL/Java class path for the schema where this function is declared). + * The default of {@code true} ensures that the built-in Java XSLT 1.0 + * implementation is used. A transformer implementation other than Xalan + * may not recognize the feature controlled by + * {@code enableExtensionFunctions}, so failure to configure that feature + * will be logged as a warning if {@code builtin} is {@code false}, instead + * of thrown as an exception. + *

    * Out of the box, Java's transformers only support XSLT 1.0. See the S9 * example for more capabilities (at the cost of downloading the Saxon jar). */ @@ -437,11 +449,13 @@ public static SQLXML castTextXML(@SQLType("text") SQLXML sx) public static void prepareXMLTransform(String name, SQLXML source, @SQLType(defaultValue="0") int how, @SQLType(defaultValue="false") boolean enableExtensionFunctions, + @SQLType(defaultValue="true") boolean builtin, @SQLType(defaultValue={}) ResultSet adjust) throws SQLException { prepareXMLTransform( - name, source, how, enableExtensionFunctions, adjust, false); + name, source, how, enableExtensionFunctions, adjust, builtin, + /* withJava */ false); } /** @@ -450,12 +464,18 @@ public static void prepareXMLTransform(String name, SQLXML source, * methods. *

    * Otherwise identical to {@code prepareXMLTransform}, this version sets the - * {@code TransformerFactory}'s {@code extensionClassLoader} (to the same - * loader that loads this class), so the transform will be able to use + * {@code TransformerFactory}'s {@code extensionClassLoader} (to the context + * class loader, normally the PL/Java class path for the schema where this + * function is declared), so the transform will be able to use * xalan's Java call syntax to call any public Java methods that would be * accessible to this class. (That can make a big difference in usefulness * for the otherwise rather limited XSLT 1.0.) *

    + * As with {@code enableExtensionFunctions}, failure by the transformer + * implementation to recognize or allow the {@code extensionClassLoader} + * property will be logged as a warning if {@code builtin} is {@code false}, + * rather than thrown as an exception. + *

    * This example function will be installed with {@code EXECUTE} permission * revoked from {@code PUBLIC}, as it essentially confers the ability to * create arbitrary new Java functions, so should only be granted to roles @@ -480,27 +500,54 @@ public static void prepareXMLTransform(String name, SQLXML source, public static void prepareXMLTransformWithJava(String name, SQLXML source, @SQLType(defaultValue="0") int how, @SQLType(defaultValue="false") boolean enableExtensionFunctions, + @SQLType(defaultValue="true") boolean builtin, @SQLType(defaultValue={}) ResultSet adjust) throws SQLException { prepareXMLTransform( - name, source, how, enableExtensionFunctions, adjust, true); + name, source, how, enableExtensionFunctions, adjust, builtin, + /* withJava */ true); } private static void prepareXMLTransform(String name, SQLXML source, int how, - boolean enableExtensionFunctions, ResultSet adjust, boolean withJava) + boolean enableExtensionFunctions, ResultSet adjust, boolean builtin, + boolean withJava) throws SQLException { - TransformerFactory tf = TransformerFactory.newInstance(); + TransformerFactory tf = + builtin + ? TransformerFactory.newDefaultInstance() + : TransformerFactory.newInstance(); String exf = "http://www.oracle.com/xml/jaxp/properties/enableExtensionFunctions"; String ecl = "jdk.xml.transform.extensionClassLoader"; Source src = sxToSource(source, how, adjust); try { - tf.setFeature(exf, enableExtensionFunctions); + try + { + tf.setFeature(exf, enableExtensionFunctions); + } + catch ( TransformerConfigurationException e ) + { + logMessage("WARNING", + "non-builtin transformer: ignoring " + e.getMessage()); + } + if ( withJava ) - tf.setAttribute(ecl, PassXML.class.getClassLoader()); + { + try + { + tf.setAttribute(ecl, + Thread.currentThread().getContextClassLoader()); + } + catch ( IllegalArgumentException e ) + { + logMessage("WARNING", + "non-builtin transformer: ignoring " + e.getMessage()); + } + } + s_tpls.put(name, tf.newTemplates(src)); } catch ( TransformerException te ) From 49628d8fea31f05d1104e362d72326957de171d1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 7 Oct 2021 19:47:02 -0400 Subject: [PATCH 0904/1087] Improve validator message on method resolution The earlier validator patch now backed out was motivated not just by the extreme laziness in OpenJ9 (which can happily give you a method handle before even trying to resolve any of the method's symbolic references), but also by getting an unhelpful message even on HotSpot, which is better at detecting such issues, but the exception that really shows what went wrong can be a couple getCause()s down. At least that second issue has an easy solution: just generate a better message. --- .../postgresql/pljava/internal/Function.java | 73 +++++++++++++++---- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 33332522..842ad815 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -353,32 +353,73 @@ private static MethodHandle getMethodHandle( } catch ( ReflectiveOperationException e ) { - SQLException sqle = - memberException(clazz, methodName, origMT,true/*isStatic*/); - sqle.initCause(ex1); - sqle.setNextException((SQLException) - memberException(clazz, methodName, mt, true /*isStatic*/) - .initCause(e)); - throw sqle; + SQLException sqe1 = + memberException(clazz, methodName, origMT, ex1, + true /*isStatic*/); + SQLException sqe2 = + memberException(clazz, methodName, mt, e, + true /*isStatic*/); + + /* + * If one of the exceptions is NoSuchMethodException and the + * other isn't, then the one that isn't carries news about + * a problem with a method that actually was found. If that's + * the second one, we'll just lie a little about the order and + * report it first. (We never promised what order we'd do the + * lookups in anyway, and the current Java-to-PG exception + * translation only preserves the "first" one's details.) + */ + if ( ex1 instanceof NoSuchMethodException + && ! (e instanceof NoSuchMethodException) ) + { + sqe2.setNextException(sqe1); + throw sqe2; + } + + sqe1.setNextException(sqe2); + throw sqe1; } } - throw (SQLException) - memberException(clazz, methodName, origMT, true /*isStatic*/) - .initCause(ex1); + throw + memberException(clazz, methodName, origMT, ex1, true /*isStatic*/); } /** - * Produce an exception for a class member not found, with a message similar - * to that of the C {@code PgObject_throwMemberError}. + * Produce an exception for a class member not found, with a message that + * may include details from further down an exception's chain of causes. */ private static SQLException memberException( - Class clazz, String name, MethodType mt, boolean isStatic) + Class clazz, String name, MethodType mt, + ReflectiveOperationException e, boolean isStatic) { + /* + * The most useful detail message to include may not be that + * of e itself, but further down the chain of causes, particularly + * if e is IllegalAccessException, which handle lookup can throw even + * for causes that aren't illegal access but rather linkage errors. + */ + Throwable t, prev; + t = prev = e; + for ( Class c : List.of( + IllegalAccessException.class, LinkageError.class, + ClassNotFoundException.class, Void.class) ) + { + if ( ! c.isInstance(t) ) + { + t = prev; + break; + } + prev = t; + t = t.getCause(); + } + + String detail = (null == t) ? "" : (": " + t); + return new SQLNonTransientException( - String.format("Unable to find%s method %s.%s with signature %s", - (isStatic ? " static" : ""), - clazz.getCanonicalName(), name, mt), + String.format("resolving %smethod %s.%s with signature %s%s", + (isStatic ? "static " : ""), + clazz.getCanonicalName(), name, mt, detail), "38000"); } From 5553edf2119fea7f12636adc72c810392d9d705b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 7 Oct 2021 19:47:19 -0400 Subject: [PATCH 0905/1087] Avoid arbitrary bits as a sentinel object ref Because the Java API treats null as a meaningful value for setContextClassLoader, something other than null must be used as the sentinel "didn't set one" value. But choosing some arbitrary int bits other than NULL to use as a sentinel pointer value could be trouble on some architectures. Assuming no logic errors, whatever sentinel value is used here will never get passed to JNI and used as an object reference, but should such an error exist, the results could be undefined unless the value is an actual JNI global reference to a ClassLoader instance. So, to play it safe, just make a dummy one of those to use as the sentinel. If any sequence of mishaps leads Java to use it as a class loader, it will throw null pointer exceptions, but that beats undefined behavior. Invocation_pushBootContext can just use null; its uses may precede the allocation of the sentinel, and popBootContext doesn't try to restore anything anyway. --- pljava-so/src/main/c/Function.c | 14 +++++++++-- pljava-so/src/main/c/Invocation.c | 9 ++++++- pljava-so/src/main/include/pljava/Function.h | 12 +++++++++ .../org/postgresql/pljava/sqlj/Loader.java | 25 +++++++++++++++++-- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 010dcbce..0f67a8a5 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -52,6 +52,8 @@ #define COUNTCHECK(refs, prims) ((jshort)(((refs) << 8) | ((prims) & 0xff))) +jobject pljava_Function_NO_LOADER; + static jclass s_Function_class; static jclass s_ParameterFrame_class; static jclass s_EntryPoints_class; @@ -228,7 +230,8 @@ void Function_initialize(void) { 0, 0, 0 } }; - jclass cls; + jclass cls; + jfieldID fld; StaticAssertStmt(org_postgresql_pljava_internal_Function_s_sizeof_jvalue == sizeof (jvalue), "Function.java has wrong size for Java JNI jvalue"); @@ -305,6 +308,13 @@ void Function_initialize(void) PgObject_registerNatives2(s_Function_class, functionMethods); + cls = PgObject_getJavaClass("org/postgresql/pljava/sqlj/Loader"); + fld = PgObject_getStaticJavaField(cls, + "SENTINEL", "Ljava/lang/ClassLoader;"); + pljava_Function_NO_LOADER = + JNI_newGlobalRef(JNI_getStaticObjectField(cls, fld)); + JNI_deleteLocalRef(cls); + s_FunctionClass = PgObjectClass_create("Function", sizeof(struct Function_), _Function_finalize); s_pgproc_Type = Composite_obtain(ProcedureRelation_Rowtype_Id); @@ -456,7 +466,7 @@ void pljava_Function_popFrame(bool heavy) if ( heavy ) JNI_callStaticVoidMethod(s_ParameterFrame_class, s_ParameterFrame_pop); - if ( (void *)-1 == currentInvocation->savedLoader ) + if ( pljava_Function_NO_LOADER == currentInvocation->savedLoader ) return; (*JNI_loaderRestorer)(); diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 25c4b621..8fe4aaff 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -141,6 +141,7 @@ void Invocation_pushBootContext(Invocation* ctx) ctx->function = 0; ctx->frameLimits = 0; ctx->primSlot0.j = 0L; + ctx->savedLoader = 0; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; @@ -158,6 +159,12 @@ void Invocation_popBootContext(void) JNI_popLocalFrame(0); currentInvocation = 0; --s_callLevel; + /* + * Nothing is done here with savedLoader. It is just set to 0 in + * pushBootContext (uses can precede allocation of the sentinel value), + * and PL/Java functions (which could save a value) aren't called in a + * boot context. + */ } void Invocation_pushInvocation(Invocation* ctx) @@ -167,7 +174,7 @@ void Invocation_pushInvocation(Invocation* ctx) ctx->function = 0; ctx->frameLimits = *s_frameLimits; ctx->primSlot0 = *s_primSlot0; - ctx->savedLoader = (void *)-1; + ctx->savedLoader = pljava_Function_NO_LOADER; ctx->hasConnected = false; ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; diff --git a/pljava-so/src/main/include/pljava/Function.h b/pljava-so/src/main/include/pljava/Function.h index 1dcc8262..005d0e5f 100644 --- a/pljava-so/src/main/include/pljava/Function.h +++ b/pljava-so/src/main/include/pljava/Function.h @@ -162,6 +162,18 @@ extern jobject Function_currentLoader(void); */ extern Function Function_INIT_WRITER; +/* + * A distinguished single JNI global classloader reference, to be used as + * a "no loader" sentinel value in context classloader management (as Java + * considers null to be a meaningful setContextClassLoader argument). Should any + * logic error lead to Java trying to use this object as a loader, null pointer + * exceptions will result, rather than the arbitrary behavior possible if using + * an arbitrary value or object of the wrong type. + * + * As this is a global reference and the only one, it can be compared with ==. + */ +extern jobject pljava_Function_NO_LOADER; + #ifdef __cplusplus } #endif diff --git a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java index 11de3746..0301bd97 100644 --- a/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java +++ b/pljava/src/main/java/org/postgresql/pljava/sqlj/Loader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2021 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 @@ -89,7 +89,15 @@ */ public class Loader extends ClassLoader { - private final static Logger s_logger = Logger.getLogger(Loader.class.getName()); + private static final Logger s_logger = + Logger.getLogger(Loader.class.getName()); + + /** + * A distinguished singleton instance to serve as a type-safe "sentinel" + * reference in context classloader management (as Java considers null to be + * a meaningful {@code setContextClassLoader} argument). + */ + public static final ClassLoader SENTINEL = new Loader(); /** * The enumeration of URLs returned by {@code findResources}. @@ -366,6 +374,19 @@ private static URL entryURL(int entryId) private final Map m_entries; private final Map m_domains; + /** + * Private constructor used only to create the "sentinel" (non-)loader. + *

    + * Any attempt to use it will incur null pointer exceptions, but it would be + * a bug already for such use to be attempted. + */ + private Loader() + { + m_entries = null; + m_domains = null; + m_j9Helper = null; + } + /** * Create a new Loader. * @param entries From 0f4fb0c58becfc8efa88f515085953b617e59380 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 8 Oct 2021 14:40:23 -0400 Subject: [PATCH 0906/1087] Update documentation --- src/site/markdown/develop/coercion.md | 14 +++ src/site/markdown/develop/contextloader.md | 99 ++++++++++++++++++++++ src/site/markdown/develop/develop.md | 1 + src/site/markdown/examples/examples.md.vm | 4 +- src/site/markdown/use/policy.md | 26 +----- src/site/markdown/use/use.md | 22 +++++ 6 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 src/site/markdown/develop/contextloader.md diff --git a/src/site/markdown/develop/coercion.md b/src/site/markdown/develop/coercion.md index edd86bf3..97655620 100644 --- a/src/site/markdown/develop/coercion.md +++ b/src/site/markdown/develop/coercion.md @@ -68,6 +68,20 @@ a PL/Java function, the type map for the schema in which the target function is declared and, at other times, the map for the schema in which the innermost executing PL/Java function on the call stack is declared. +Starting in PL/Java 1.6.3, a PL/Java function is entered with the current +thread's [context class loader][ccl] set according to the schema where the +function is declared, and therefore the rules for applying the type map +just described can be simplified: the type map is the one maintained by +the current context class loader, provided Java code has not changed the +context loader from the initial setting. To date, the code actually obtaining +the type map has not been changed to get it _from_ the context class loader, +so the type map would not be affected by Java code changing the context loader. + +There are [more details](contextloader.html) on the management of +the context class loader. + +[ccl]: https://docs.oracle.com/javase/9/docs/api/java/lang/Thread.html#getContextClassLoader-- + ### PL/Java's object system implemented in C In PL/Java, some behavior is implemented in Java using familiar Java diff --git a/src/site/markdown/develop/contextloader.md b/src/site/markdown/develop/contextloader.md new file mode 100644 index 00000000..fda8b120 --- /dev/null +++ b/src/site/markdown/develop/contextloader.md @@ -0,0 +1,99 @@ +# The thread context class loader + +Starting with PL/Java 1.6.3, within an SQL-declared PL/Java function, the +class loader returned by `Thread.currentThread().getContextClassLoader` +is the one that corresponds to the per-schema classpath that has been set +with [`SQLJ.SET_CLASSPATH`][scp] for the schema where the function is +declared (assuming no Java code uses `setContextClassLoader` to change it). + +Many available Java libraries, as well as built-in Java facilities using the +[`ServiceLoader`][slo], refer to the context class loader, so this behavior +ensures they will see the classes that are available on the classpath that was +set up for the PL/Java function. In versions where PL/Java did not set the +context loader, awkward arrangements could be needed in user code for the +desired classes or services to be found. + +## Limits on the implementation + +To set this loader with minimal overhead on function entry, PL/Java uses native +access to a `Thread` field. It is possible that some Java runtimes can exist +where the expected field is not present, and PL/Java will fall back (with a +warning) to not managing the context loader. The warning can be suppressed +by explicitly configuring PL/Java not to manage the context loader, as described +below. + +It is also possible for an application or library to create subclasses +of `Thread` that override the behavior of `getContextClassLoader` so that +the value set by PL/Java will have no effect. PL/Java does not detect or work +around such a case. A clear sign of code that does subclass `Thread` +in this way is that it will need the `enableContextClassLoaderOverride` +[`RuntimePermission`][runtimeperm] to be granted in +the [policy](../use/policy.html). + +## Effects on application code + +With this change as of PL/Java 1.6.3, application or library code that uses +the [`ServiceLoader`][slo], or otherwise refers to the context class loader, +will find services or resources available on the class path that was set up +for the function. Typically, this behavior is wanted. In prior PL/Java versions, +services and resources might be found only if they were available to the +system class loader. + +For example, a call like `javax.xml.transform.TransformerFactory.newInstance()` +might return Java's built-in XSLT 1.0 implementation if there is nothing else +on the class path, but return an XSLT 3.0 implementation if the configured +PL/Java class path includes a Saxon jar. + +If there are cases where an application intends to use a built-in Java +implementation regardless of the class path, there may be a method available +that specifies that behavior. For example, +[`TransformerFactory.newDefaultInstance()`][tfndi] will always return Java's +own `Transformer` implementation. + +If an application misbehaves as a result of finding implementations on the +class path it was not finding before, and cannot be conveniently fixed by +adjusting the class path or changing to `newDefaultInstance`-like methods +in the code, PL/Java can be configured for its old behavior of not setting +the context class loader, as described below. + +## Effects on UDT methods + +User-defined types implemented in PL/Java have support methods that are +transparently invoked to convert database values to Java values and back. +This can happen within a PL/Java function, when it gets or sets values in +`ResultSet`, `PreparedStatement`, `SQLInput`, or `SQLOutput` objects, and +also conceptually "before" or "after" the function proper, to convert its +incoming parameters and its return value(s). In all such contexts, the UDT +methods are considered to act on behalf of that target PL/Java function, +and the context class loader they see is the one for the schema where the +target function is declared. + +A [`BaseUDT`][baseudt] implemented in PL/Java has support methods that are +declared to PostgreSQL as SQL functions in their own right. In addition to being +transparently called on behalf of another PL/Java function, with the behavior +described above, they can be called directly by PostgreSQL like any other +SQL function. When that happens, like any other declared function, they will +have the context class loader set according to the schema containing the +declaration. + +## Suppressing context loader management + +Some circumstances may call for keeping the pre-1.6.3 behavior +where no management of the context class loader was done. That could be to +avoid unplanned effects on applications as described above, or to suppress +the warning message if running on a JVM where PL/Java's technique doesn't work. + +To suppress the loader management, add + +``` +-Dorg.postgresql.pljava.context.loader=unmanaged +``` + +in the `pljava.vmoptions` [setting](../use/variables.html). + + +[scp]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#set_classpath +[slo]: https://docs.oracle.com/javase/9/docs/api/java/util/ServiceLoader.html +[tfndi]: https://docs.oracle.com/javase/9/docs/api/javax/xml/transform/TransformerFactory.html#newDefaultInstance-- +[runtimeperm]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/RuntimePermission.html +[baseudt]: ../pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/BaseUDT.html diff --git a/src/site/markdown/develop/develop.md b/src/site/markdown/develop/develop.md index e09d8f59..5044ad49 100644 --- a/src/site/markdown/develop/develop.md +++ b/src/site/markdown/develop/develop.md @@ -7,3 +7,4 @@ understanding of some PL/Java behavior is needed. * [The testing harness `Node.class` in PL/Java's self-installer jar](node.html) * [Passing of data types between PostgreSQL and Java](coercion.html) +* [The thread context class loader in a PL/Java function](contextloader.html) diff --git a/src/site/markdown/examples/examples.md.vm b/src/site/markdown/examples/examples.md.vm index 84ded104..24eb3e65 100644 --- a/src/site/markdown/examples/examples.md.vm +++ b/src/site/markdown/examples/examples.md.vm @@ -134,9 +134,9 @@ by default because they depend on the Saxon jar. If your examples jar was built with the optional examples enabled, then PL/Java will normally validate that all of the functions it creates can be used. -That validation will fail if a needed dependency, such as the Saxon jar, is +That validation can fail if a needed dependency, such as the Saxon jar, is not already installed and on the classpath. The error message will not directly -say the Saxon jar is missing, but will say it failed to find a class (whose name +say the Saxon jar is missing, but may say it failed to find a class (whose name will suggest it should be part of Saxon). There are two ways to proceed: diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index f7bd8cdf..6d032043 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -318,24 +318,11 @@ such as `java.version` or `org.postgresql.pljava.version`._ ### Class static initializers -If validating (`check_function_bodies` is `on`), class static initializers -will be run at `CREATE FUNCTION` time in a bid to catch as many -issues early as possible. They will be run in the same access control -context that would be used at run time to call the function being declared. - -Classes besides the one containing the target method can also be loaded and -resolved: all those corresponding to the method's parameter and return types, -for example. Their initializers will not be forced to run by PL/Java's -validator, but will be run if the static initializer of the class being -validated accesses them in any of the ways that trigger class initialization -in Java. - If a class contains several methods that would be given different access control contexts (declared with different `trust` or `language` attributes, say), the permissions available when the class initializer runs will be those of whichever function is called first -in a given session (or, by the validator, for whichever `CREATE FUNCTION` -is seen first in a session). Therefore, when putting actions that require +in a given session. Therefore, when putting actions that require permissions into a class's static initializer, those actions should require only the common subset of permissions that the initializer could be run with no matter which function is called or declared first. Actions that require @@ -344,16 +331,7 @@ a function known to be granted those permissions. Such actions can be left in the static initializer if a function granted the needed permissions is known to always be the first one that the application -will call in any given session. Likewise, `provides`/`requires` ordering can -be used to generate the `CREATE FUNCTION` commands in the deployment descriptor -in such an order that the first function declared has the permissions the -class initializer will need. - -A way to ensure early detection of permission/policy problems could be to -deliberately put operations requiring the needed permissions, or even -simple `AccessController.checkPermission` calls, into a class's static -initializer. Problems with the policy granting insufficient permissions -can then be caught at `CREATE FUNCTION` time when validation is enabled. +will call in any given session. ## Troubleshooting diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index 27b16e47..c25bef79 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -51,6 +51,28 @@ to run with a 'trial' policy initially, allowing code to run but logging permissions that may need to be added in `pljava.policy`. How to do that is described [here](trial.html). +### The thread context class loader + +Starting with PL/Java 1.6.3, within an SQL-declared PL/Java function, the +class loader returned by `Thread.currentThread().getContextClassLoader` +is the one that corresponds to the per-schema classpath that has been set +with [`SQLJ.SET_CLASSPATH`][scp] for the schema where the function is +declared (assuming no Java code uses `setContextClassLoader` to change it). + +Many available Java libraries, as well as built-in Java facilities using the +[`ServiceLoader`][slo], refer to the context class loader, so this behavior +ensures they will see the classes that are available on the classpath that was +set up for the PL/Java function. In versions where PL/Java did not set the +context loader, awkward arrangements could be needed in user code for the +desired classes or services to be found. + +There are some limits on the implementation, and some applications may want +the former behavior where PL/Java did not touch the thread context loader. +More details are available [here](../develop/contextloader.html). + +[scp]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#set_classpath +[slo]: https://docs.oracle.com/javase/9/docs/api/java/util/ServiceLoader.html + ### Choices when mapping data types #### Date and time types From 192c6e69703be21d5bc63da451ba5e8077257758 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 8 Oct 2021 16:37:21 -0400 Subject: [PATCH 0907/1087] Say, GitHub's Ubuntu runner is on PG 14 already --- .github/workflows/ci-runnerpg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 7906982f..d77d5a53 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -66,7 +66,7 @@ jobs: main | sudo tee /etc/apt/sources.list.d/pgdg.list sudo apt-get update - sudo apt-get install postgresql-server-dev-13 libkrb5-dev + sudo apt-get install postgresql-server-dev-14 libkrb5-dev - name: Build PL/Java (Linux, macOS) if: ${{ 'Windows' != runner.os }} From 2a2d6da8d8818fc579b21cb479a5a784926fe960 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 9 Oct 2021 15:56:16 -0400 Subject: [PATCH 0908/1087] Further wordsmithing of JEP 411 warnings No functional change. --- pljava-so/src/main/c/Backend.c | 8 ++++---- src/site/markdown/use/policy.md | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 04d53a88..125132e5 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -2028,14 +2028,14 @@ void Backend_warnJEP411(bool isCommit) "(after Java 17) that will be unable to run PL/Java %s " "with policy enforcement", SO_VERSION_STRING), errdetail( - "Future Java releases will phase out important features used " - "by this PL/Java version to enforce security policy. Those " - "changes will come in releases after Java 17."), + "This PL/Java version enforces security policy using important " + "Java features that will be phased out in future Java versions. " + "Those changes will come in releases after Java 17."), errhint( "For migration planning, Java versions up to and including 17 " "remain fully usable with this version of PL/Java, and Java 17 " "is positioned as a long-term support release. For details on " - "how PL/Java will adapt, please visit " + "how PL/Java will adapt, please bookmark " "https://github.com/tada/pljava/wiki/JEP-411") )); } diff --git a/src/site/markdown/use/policy.md b/src/site/markdown/use/policy.md index 6d032043..566743c4 100644 --- a/src/site/markdown/use/policy.md +++ b/src/site/markdown/use/policy.md @@ -370,6 +370,14 @@ The current implementation makes use of the Java classes That should be regarded as an implementation detail; it may change in a future release, so relying on it is not recommended. +The developers of Java have elected to phase out important language features +used by PL/Java to enforce policy. The changes will come in releases after +Java 17. For migration planning, Java versions up to and including 17 +remain fully usable with this version of PL/Java, and Java 17 +is positioned as a long-term support release. For details on +how PL/Java will adapt, please bookmark [the JEP 411 topic][jep411] +on the PL/Java wiki. + [pfsyn]: https://docs.oracle.com/en/java/javase/14/security/permissions-jdk1.html#GUID-7942E6F8-8AAB-4404-9FE9-E08DD6FFCFFA [jdkperms]: https://docs.oracle.com/en/java/javase/14/security/permissions-jdk1.html#GUID-1E8E213A-D7F2-49F1-A2F0-EFB3397A8C95 @@ -378,3 +386,4 @@ release, so relying on it is not recommended. [sqljajl]: ../pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#alias_java_language [tssec]: https://docs.oracle.com/en/java/javase/14/security/troubleshooting-security.html [trial]: trial.html +[jep411]: https://github.com/tada/pljava/wiki/JEP-411 From 29eb0610eec0a9c2c3d914106230124d0b894688 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 9 Oct 2021 15:57:35 -0400 Subject: [PATCH 0909/1087] Document and enforce MappedUDT/typlen-2 exclusion Check at runtime whether a MappedUDT is being applied to a PostgreSQL type declared with typlen of -2 (a variable-length, NUL-terminated representation), and report an error if so. Explain in the developer documentation why the implementation cannot support the use. It already didn't work, but an explanatory error message is a better way of not working. Addresses #370. --- pljava-so/src/main/c/type/Type.c | 12 ++++++++++++ src/site/markdown/develop/coercion.md | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 61bbc56a..7f169e31 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -712,6 +712,18 @@ checkTypeMappedUDT(Oid typeId, jobject typeMap, Form_pg_type typeStruct) if ( NULL == typeClass ) return NULL; + if ( -2 == typeStruct->typlen ) + { + JNI_deleteLocalRef(typeClass); + ereport(ERROR, ( + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg( + "type mapping in PL/Java for %s with NUL-terminated(-2) " + "storage not supported", + format_type_be_qualified(typeId)) + )); + } + readMH = pljava_Function_udtReadHandle( typeClass, NULL, true); writeMH = pljava_Function_udtWriteHandle(typeClass, NULL, true); diff --git a/src/site/markdown/develop/coercion.md b/src/site/markdown/develop/coercion.md index 97655620..113545b5 100644 --- a/src/site/markdown/develop/coercion.md +++ b/src/site/markdown/develop/coercion.md @@ -600,6 +600,17 @@ PostgreSQL does call through those slots, PL/Java always does a raw binary transfer using the `libpq` API directly (for fixed-size representations), `bytearecv`/`byteasend` for `varlena` representations, or `unknownrecv`/`unknownsend` for C string representations. +Responsible code in `type/UDT.c` is commented with "Assumption 2". A future version could revisit this limitation, and allow PL/Java UDTs to specify custom binary transfer formats also. + +"Assumption 1" in `UDT.c` is that any PostgreSQL type declared with +`internallength=-2` (meaning it is stored as a variable number of nonzero +bytes terminated by a zero byte) must have a human-readable representation +identical to its stored form, and must be converted to and from Java using +the `INPUT` and `OUTPUT` slots. A `MappedUDT` does not have functions in +those slots, and therefore "Assumption 1" rules out any such type as target +of a `MappedUDT`. + +A future version could revisit this limitation also. From 5475e2746ec01a067c3f055d4540521406f4ba47 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 9 Oct 2021 18:37:00 -0400 Subject: [PATCH 0910/1087] Bump ant to 1.10.11, suggested by dependabot Making this change manually, as dependabot would probably make it in the wrong branch. 1.10 requires Java 8, so no backpatching to REL1_5_STABLE. This whole dependency on ant can probably be removed easily now, by moving the build.xml logic into pom.xml as a scripted-goal. --- pljava-packaging/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index c6b06181..b7262e70 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -167,7 +167,7 @@ function execute() org.apache.ant ant - [1.8.3,1.10.0) + 1.10.11 From b57ae71f92137a11965a4e237c41f90788828616 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Oct 2021 17:44:29 -0400 Subject: [PATCH 0911/1087] Update release notes ... in the course of which, noticing that a change being described didn't have enough detail in the JavaDoc, added that too. --- .../pljava/annotation/Operator.java | 10 ++ src/site/markdown/releasenotes.md.vm | 137 ++++++++++++++++-- 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java index cf2b4771..7e7fe0df 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Operator.java @@ -129,6 +129,16 @@ * (which must be different) reversed. A typical case would be the twin of a * cross-type operator like {@code +} that is commutative, so using the same * name makes sense. + *

    + * When derived by commutation, the synthetic function simply calls the + * base function with the parameters swapped. For negation, the base + * function must return {@code boolean} or {@code Boolean}, and the + * synthetic function returns true for false, false for true, and null + * for null. This will give familiar SQL behavior in many cases. For a base + * function with {@code onNullInput=CALLED}, if it can return non-null + * boolean results on some null inputs, it may be necessary to code + * a negator or commutator by hand if the synthetic one would not have + * the intended semantics. */ String[] commutator() default {}; diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 12b892ab..1db08cc0 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,6 +10,132 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') +$h2 PL/Java 1.6.3 + +This is the third minor update in the PL/Java 1.6 series. It adds support +for PostgreSQL 14, continues to improve the runtime behavior and +the annotation-driven SQL generator, and fixes several bugs. Further information +on the changes may be found below. + +$h3 PL/Java with Java 17 and later: JEP 411 + +Current versions of PL/Java rely on Java security features that will be affected +by JEP 411, beginning with Java 17. Java 17 itself will continue to provide the +needed capabilities, with only deprecation marks and warnings added. Java 17 is +also positioned as a long-term support release, so the option of continuing to +run this PL/Java release will be available, with no loss of function, +by continuing to run with Java versions up to and including 17. + +For more on how PL/Java will adapt, please bookmark [the JEP 411 topic][jep411] +on the PL/Java wiki. + +For this release, PL/Java will suppress a JEP 411-related warning from the +Java runtime itself that would otherwise be emitted for every PostgreSQL backend +that starts Java, and instead will issue a more informative "migration advisory" +warning only on certain administrative actions no more than once per session, +with a goal of ensuring that responsible administrators are aware of the +expected future developments. The advisory message includes the URL to +the wiki topic above. + +This release also adds code to detect if it is on a future, post-Java 17 runtime +where the needed functionality is not available, and throw informative +exceptions with suggested corrective actions. + +[jep411]: https://github.com/tada/pljava/wiki/JEP-411 + +$h3 Changes + +$h4 Changes affecting administration + +$h5 New function now created on update + +The new [`SQLJ.ALIAS_JAVA_LANGUAGE`][sqljajl] function introduced with 1.6 will now +be present after an `ALTER EXTENSION UPDATE` if it was not before. + +$h5 Message if the Java runtime ends the backend process during startup + +The Java runtime can behave antisocially for some startup issues, such +as a misspelled jar in `pljava.module_path`, and silently terminate the +backend process rather than reporting an error. (It writes a message, not +to standard error, but to standard output, which from a PostgreSQL backend +is not captured for logging, and may never be seen.) A message is now generated +in that case, to provide at least some clue what has gone wrong. + +[sqljajl]: pljava/apidocs/org.postgresql.pljava.internal/org/postgresql/pljava/management/Commands.html#alias_java_language + +$h4 Changes to runtime behavior + +$h5 Java's thread context class loader + +PL/Java now supplies a known value for the current Java thread's context +class loader on entry to a PL/Java function. It is the class loader for the +PostgreSQL schema where the function is declared. The context class loader is +referred to by numerous Java libraries and by Java's `ServiceLoader` +class, which may expect to find services and resources along the PL/Java class +path that has been configured for the function. Neglecting to set the context +class loader opened the door to unexpected loading failures like +[issue #361](${ghbug}361) that can require awkward or slow contortions +to work around in user Java code. + +This change is [documented in more detail](develop/contextloader.html). +An opt-out setting is available. + +$h4 Improvements to the annotation-driven SQL generator + +$h5 Now able to declare function parameters that default to null + +It has been possible to annotate a function parameter with +[`@SQLType(defaultValue=...)`][sqlt] to give it any non-null default value, +but because Java disallows null annotation values, that notation wasn't +usable to declare a parameter that defaults to null. The new notation +[`@SQLType(optional=true)`][sqlt] means exactly that. + +$h5 Fixes a bug in synthesis of `COMMUTATOR`/`NEGATOR` operators + +It is now possible to code one method in Java and declare a full +complement of operators based on it by combining commutation and negation. +A [new example][egsyn4] is provided. + +[egsyn4]: https://github.com/tada/pljava/commit/6bd5aa0 +[sqlt]: pljava-api/apidocs/org.postgresql.pljava/org/postgresql/pljava/annotation/SQLType.html + +$h3 Bugs fixed + +* [`ALTER EXTENSION UPDATE` does not create `sqlj.alias_java_language`](${ghbug}341) +* [... fails on 4 cross-type operators by commute/negate from 1 function](${ghbug}343) +* [Build failure dependent on `pg_config` output](${ghbug}347) +* [Protection domain when validator runs initializer](${ghbug}342) +* [Improve experience when `pljava.module_path` is incorrect](${ghbug}350) +* ["Failed to recognize schema" possible during `pg_upgrade`](${ghbug}352) +* Fixes and test coverage in `Lexicals.Identifier` serialization +* [Segmentation fault during `autovacuum`](${ghbug}355) +* [`java.nio.charset.MalformedInputException: Input length = 1`](${ghbug}340) +* [Thread context class loader](${ghbug}361) +* [`MappedUDT` and types with `typlen=-2`](${ghbug}370) + +$h3 Updated PostgreSQL APIs tracked + +* Regularized spelling of `pg_type` OID symbols +* Changed TOAST pointer format supporting selectable TOAST compression methods +* Demise of `ErrorData.show_funcname` (it was a vestige of frontend/backend + protocol v2, long obsolete + +$h3 Credits + +Thanks to Krzysztof Nienartowicz, `ricdhen`, and `JanaParthasarathy`, who +among them reported five of the issues fixed in this release. + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + $h2 PL/Java 1.6.2 This is the second minor update in the PL/Java 1.6 series, with two bugs fixed @@ -65,17 +191,6 @@ Thanks to Francisco Biete for the report of [#331](${ghbug}331). [PassXML]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/PassXML.html#method.summary -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 PL/Java 1.6.1 (16 November 2020) _Note: 1.6.1 was released with [a bug](${ghbug}331) likely to be a blocker From 1ea41e69ddee921c33c3961353b368153e19073c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Oct 2021 17:53:07 -0400 Subject: [PATCH 0912/1087] Poke migration-management versions for 1.6.3 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 99a0f5ca..17eef8d9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -889,6 +889,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_6_3 = REL_1_6_0; static final SchemaVariant REL_1_6_2 = REL_1_6_0; static final SchemaVariant REL_1_6_1 = REL_1_6_0; From 2ec6fc64c04067e30b8e70c476e031c9ce03d553 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 10 Oct 2021 18:44:59 -0400 Subject: [PATCH 0913/1087] Add control file in preparation for next release Now that 1.6.3 is released, the next release should include an extension SQL file allowing upgrade from 1.6.3. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 0eb756a2..335ba7d9 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -255,6 +255,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Mon, 11 Oct 2021 12:57:44 -0400 Subject: [PATCH 0914/1087] Of course there would be a typo in the docs to be discovered right after a release. Well, it'll be right in the next one. --- src/site/markdown/releasenotes.md.vm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 1db08cc0..1af1d5d7 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -118,7 +118,7 @@ $h3 Updated PostgreSQL APIs tracked * Regularized spelling of `pg_type` OID symbols * Changed TOAST pointer format supporting selectable TOAST compression methods * Demise of `ErrorData.show_funcname` (it was a vestige of frontend/backend - protocol v2, long obsolete + protocol v2, long obsolete) $h3 Credits From 231642ce3775b1492b2ce871a9e244c8771d460f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 17 Jan 2022 19:21:33 -0500 Subject: [PATCH 0915/1087] Expose predefined category codes in BaseUDT The PostgreSQL type 'category' is a bit awkward, because there are several predefined, but other codes can be used for custom purposes. So an enumeration of the predefined ones cannot be used as the type of the 'category' annotation element, but it still can improve readability when one of the predefined categories is what's wanted. Also add a warning from the DDR processor if an upper-case category code has been given (that's the range reserved for PostgreSQL's predefined categories) but it doesn't correspond to any value of the PredefinedCategory enum. --- .../postgresql/pljava/annotation/BaseUDT.java | 124 +++++++++++++++++- .../annotation/processing/DDRProcessor.java | 9 +- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java index 87fb5550..e416f2af 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2022 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 @@ -80,7 +80,120 @@ enum Alignment { CHAR, INT2, INT4, DOUBLE } >TOAST strategies for the type's stored representation. * Only {@code PLAIN} is applicable to fixed-length types. */ - enum Storage { PLAIN, EXTERNAL, EXTENDED, MAIN } + enum Storage + { + /** + * Never compressed or stored out-of-line. + */ + PLAIN, + + /** + * Can be moved out-of-line but not compressed. + */ + EXTERNAL, + + /** + * Can be compressed, and moved out-of-line if still too big. + */ + EXTENDED, + + /** + * Can be compressed, but moved out-of-line only if there is no other + * way to make the containing tuple fit a page. + */ + MAIN + } + + /** + * The type categories that are predefined in PostgreSQL. + *

    + * This enumeration is not used as the type of the + * {@link #category category} element below, because PostgreSQL allows use + * of other single-ASCII-character category codes for custom purposes. + * Therefore, the annotation can be any such character. PostgreSQL reserves + * all of the upper-case ASCII letters to represent current or future + * predefined categories, and this enumeration allows mapping between those + * and their more readable names. + *

    + * When one of the predefined categories is wanted for the + * {@link #category category} element, the corresponding character constant + * in {@link Code PredefinedCategory.Code} can be used in the annotation as + * a more readable alternative to the one-character code. + */ + enum PredefinedCategory + { + ARRAY (Code.ARRAY), + BOOLEAN (Code.BOOLEAN), + COMPOSITE (Code.COMPOSITE), + DATETIME (Code.DATETIME), + ENUM (Code.ENUM), + GEOMETRIC (Code.GEOMETRIC), + NETWORK (Code.NETWORK), + NUMERIC (Code.NUMERIC), + PSEUDOTYPE (Code.PSEUDOTYPE), + RANGE (Code.RANGE), + STRING (Code.STRING), + TIMESPAN (Code.TIMESPAN), + USER (Code.USER), + BITSTRING (Code.BITSTRING), + UNKNOWN (Code.UNKNOWN), + INTERNAL (Code.INTERNAL); + + private final char code; + + PredefinedCategory(char code) + { + this.code = code; + } + + /** + * Return this category's single-character code. + */ + public char code() + { + return code; + } + + /** + * Return the {@code PredefinedCategory} corresponding to a + * single-character code as found in the system catalogs, or null + * if the character represents a custom category (or a predefined one in + * a PostgreSQL version newer than this class). + */ + public static PredefinedCategory valueOf(char code) + { + for ( PredefinedCategory c : values() ) + if ( c.code == code ) + return c; + return null; + } + + /** + * Character constants corresponding to the predefined categories, + * for use in the {@link #category} annotation element. + */ + public interface Code + { + char ARRAY = 'A'; + char BOOLEAN = 'B'; + char COMPOSITE = 'C'; + char DATETIME = 'D'; + char ENUM = 'E'; + char GEOMETRIC = 'G'; + char NETWORK = 'I'; + char NUMERIC = 'N'; + char PSEUDOTYPE = 'P'; + char RANGE = 'R'; + char STRING = 'S'; + char TIMESPAN = 'T'; + char USER = 'U'; + char BITSTRING = 'V'; + char UNKNOWN = 'X'; + char INTERNAL = 'Z'; + } + } /** * Name of the new type in SQL, if it is not to be the simple name of @@ -222,8 +335,13 @@ enum Storage { PLAIN, EXTERNAL, EXTENDED, MAIN } * This must be a single character, which PostgreSQL calls "simple ASCII" * and really forces to be in {@code [ -~]}, that is, space to tilde, * inclusive. + *

    + * The upper-case ASCII letters are reserved for PostgreSQL's predefined + * categories, which can be found in the + * {@link PredefinedCategory PredefinedCategory} enumeration. The default is + * {@link PredefinedCategory.Code#USER PredefinedCategory.Code.USER}. */ - char category() default 'U'; + char category() default PredefinedCategory.Code.USER; /** * Whether this type is to be "preferred" in its {@link #category}, 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 93b38e69..1616c0fa 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-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -3000,6 +3000,13 @@ public Set characterize() msg( Kind.ERROR, tclass, "UDT category must be a printable ASCII character"); + if ( categoryExplicit && Character.isUpperCase(category()) ) + if ( null == PredefinedCategory.valueOf(category()) ) + msg( Kind.WARNING, tclass, + "upper-case letters are reserved for PostgreSQL's " + + "predefined UDT categories, but '%c' is not recognized", + category()); + recordImplicitTags(); recordExplicitTags(_provides, _requires); From a53e0eda8a4647647670e2c724e83d7f43ac8265 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 18 Jan 2022 20:05:01 -0500 Subject: [PATCH 0916/1087] Emit correct SQL declaration for one OUT parameter When generating a function declaration for a function with exactly one OUT parameter, the declaration must have the scalar form where RETURNS identifies the single parameter's own type instead of RECORD. Discussion: https://www.postgresql.org/message-id/619BBE78.7040009%40anastigmatix.net Added more checks on co-appearance of type= and out= and a way to use @SQLType to disambiguate in cases where the method "shape" would fit a composite-returning method or an ordinary method with a row-typed trailing input parameter. In cases where PL/Java has relied on an assumption in generating a function's declaration (some of the assumptions have long standing in PL/Java history), it still proceeds silently at compile time, but generates a comment into the deployment descriptor identifying the assumption and how to annotate the method if a different interpretation is intended. Addresses #386. --- .../pljava/annotation/Function.java | 17 +- .../annotation/processing/DDRProcessor.java | 284 ++++++++++++++++-- 2 files changed, 276 insertions(+), 25 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java index 9e9ad07d..705d53e6 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -79,7 +79,8 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; * or can be used to specify the return type of any function if the * compiler hasn't inferred it correctly. *

    - * Only one of {@code type} or {@code out} may appear. + * Only one of {@code type} or {@code out} may appear, except as described + * for {@code out} below. */ String type() default ""; @@ -96,6 +97,18 @@ enum Parallel { UNSAFE, RESTRICTED, SAFE }; * name, only a type. The name is an ordinary SQL identifier; if it would * be quoted in SQL, naturally each double-quote must be represented as * {@code \"} in Java. + *

    + * If there is exactly one {@code OUT} parameter declared, PostgreSQL treats + * the function as returning that parameter's type, rather than + * a one-element composite; therefore, the Java method must have the + * corresponding form (returning the result type directly, or an + * {@code Iterator} of that type, rather than expecting a {@code ResultSet} + * final parameter. + *

    + * If a one-element composite type is wanted, PL/Java will allow + * {@code type="pg_catalog.RECORD"} along with a one-element {@code out}, + * and will generate the corresponding declaration in SQL. As of + * this writing, however, no version of PostgreSQL will accept it. */ String[] out() default {}; 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 93b38e69..b6caeb4c 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-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -1772,6 +1772,95 @@ public String[] undeployStrings() } } + /** + * Enumeration of different method "shapes" and the treatment of + * {@code type=} and {@code out=} annotation elements they need. + *

    + * Each member has a {@code setComposite} method that will be invoked + * by {@code checkOutType} if the method is judged to have a composite + * return type according to the annotations present. + *

    + * There is one case (no {@code out} and a {@code type} other than + * {@code RECORD}) where {@code checkOutType} will resolve the + * ambiguity by assuming composite, and will have set + * {@code assumedComposite} accordingly. The {@code MAYBECOMPOSITE} + * shape checks that assumption against the presence of a countervailing + * {@code SQLType} annotation, the {@code ITERATOR} shape clears it and + * behaves as noncomposite as always, and the {@code PROVIDER} shape + * clears it because that shape is unambiguously composite. + */ + enum MethodShape + { + /** + * Method has the shape {@code boolean foo(..., ResultSet)], which + * could be an ordinary method with an incoming record parameter and + * boolean return, or a composite-returning method whose last + * a writable ResultSet supplied by PL/Java for the return value. + */ + MAYBECOMPOSITE((f,msgr) -> + { + boolean sqlTyped = null != + f.paramTypeAnnotations[f.paramTypeAnnotations.length - 1]; + if ( ! sqlTyped ) + f.complexViaInOut = true; + else if ( f.assumedComposite ) + f.assumedComposite = false; // SQLType cancels assumption + else + msgr.printMessage(Kind.ERROR, + "no @SQLType annotation may appear on " + + "the return-value ResultSet parameter", f.func); + }), + + /** + * Method has the shape {@code Iterator foo(...)} and represents + * a set-returning function with a non-composite return type. + *

    + * If the shape has been merely assumed composite, clear + * that flag and proceed as if it is not. Otherwise, issue an error + * that it can't be composite. + */ + ITERATOR((f,msgr) -> + { + if ( f.assumedComposite ) + f.assumedComposite = false; + else + msgr.printMessage(Kind.ERROR, + "the iterator style cannot return a row-typed result", + f.func); + }), + + /** + * Method has the shape {@code ResultSetProvider foo(...)} or + * {@code ResultSetHandle foo(...)} and represents + * a set-returning function with a non-composite return type. + *

    + * If the shape has been merely assumed composite, clear + * that flag; for this shape that assumption is not tentative. + */ + PROVIDER((f,msgr) -> f.assumedComposite = false), + + /** + * Method is something else (trigger, for example) for which no + * {@code type} or {@code out} is allowed. + *

    + * The {@code setComposite} method for this shape will never + * be called. + */ + OTHER(null); + + private final BiConsumer compositeSetter; + + MethodShape(BiConsumer setter) + { + compositeSetter = setter; + } + + void setComposite(FunctionImpl f, Messager msgr) + { + compositeSetter.accept(f, msgr); + } + } + class FunctionImpl extends AbstractAnnotationImpl implements Function, Snippet, Commentable @@ -1830,6 +1919,8 @@ public String language() DBType returnType; DBType[] parameterTypes; List> outParameters; + boolean assumedComposite = false; + boolean forceResultRecord = false; boolean subsumed = false; @@ -1916,19 +2007,36 @@ public Set characterize() List typeArgs; int arity = ptms.size(); - if ( ( null != _type || null != _out ) - && ret.getKind().equals( TypeKind.BOOLEAN) ) + /* + * Collect the parameter type annotations now, in case needed below + * in checkOutType(MAYBECOMPOSITE) to disambiguate. + */ + + collectParameterTypeAnnotations(); + + /* + * If a type= annotation is present, provisionally set returnType + * accordingly. Otherwise, leave it null, to be filled in by + * resolveParameterAndReturnTypes below. + */ + + if ( null != _type ) + returnType = DBType.fromSQLTypeAnnotation(_type); + + /* + * Take a first look according to the method's Java return type. + */ + if ( ret.getKind().equals( TypeKind.BOOLEAN) ) { - complexViaInOut = true; - TypeMirror tm = ptms.get( arity - 1); - if ( tm.getKind().equals( TypeKind.ERROR) - // unresolved things seem assignable to anything - || ! typu.isSameType( tm, TY_RESULTSET) ) + if ( 0 < arity ) { - msg( Kind.ERROR, func.getParameters().get( arity - 1), - "Last parameter of complex-type-returning function " + - "must be ResultSet"); - return Set.of(); + TypeMirror tm = ptms.get( arity - 1); + if ( ! tm.getKind().equals( TypeKind.ERROR) + // unresolved things seem assignable to anything + && typu.isSameType( tm, TY_RESULTSET) ) + { + checkOutType(MethodShape.MAYBECOMPOSITE); + } } } else if ( null != (typeArgs = specialization( ret, TY_ITERATOR)) ) @@ -1947,11 +2055,13 @@ else if ( null != (typeArgs = specialization( ret, TY_ITERATOR)) ) "Failed to find setof component type"); return Set.of(); } + checkOutType(MethodShape.ITERATOR); } else if ( typu.isAssignable( ret, TY_RESULTSETPROVIDER) || typu.isAssignable( ret, TY_RESULTSETHANDLE) ) { setof = true; + checkOutType(MethodShape.PROVIDER); } else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) { @@ -1961,6 +2071,7 @@ else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) && typu.isSameType( tm, TY_TRIGGERDATA) ) { trigger = true; + checkOutType(MethodShape.OTHER); } } @@ -1975,8 +2086,6 @@ else if ( ret.getKind().equals( TypeKind.VOID) && 1 == arity ) "a function with triggers needs void return and " + "one TriggerData parameter"); - collectParameterTypeAnnotations(); - /* * Report any unmappable types now that could appear in * deployStrings (return type or parameter types) ... so that the @@ -2057,6 +2166,117 @@ void collectParameterTypeAnnotations() "optional=true"); } + private static final int NOOUT = 0; + private static final int ONEOUT = 4; + private static final int MOREOUT = 8; + + private static final int NOTYPE = 0; + private static final int RECORDTYPE = 1; + private static final int OTHERTYPE = 2; + + /** + * Reads the tea leaves of the {@code type=} and {@code out=} + * annotation elements to decide whether the method has a composite + * or noncomposite return. + *

    + * This is complicated by the PostgreSQL behavior of treating a function + * declared with one {@code OUT} parameter, or as + * a one-element {@code TABLE} function, as not + * returning a row type. + *

    + * This method avoids rejecting the case of a one-element {@code out=} + * with an explicit {@code type=RECORD}, to provide a way to explicitly + * request composite behavior for that case, on the chance that some + * future PostgreSQL version may accept it, though as of this writing + * no current version does. + *

    + * If the {@code MAYBECOMPOSITE} shape is used with a single {@code out} + * parameter, it is likely a mistake (what are the odds the developer + * wanted a function with a row-typed input parameter and a named out + * parameter of boolean type?), and will be rejected unless the + * {@code ResultSet} final parameter has been given an {@code SQLType} + * annotation. + */ + void checkOutType(MethodShape shape) + { + int out = + null == _out ? NOOUT : 1 == _out.length ? ONEOUT : MOREOUT; + + /* + * The caller will have set returnType from _type if present, + * or left it null otherwise. We know RECORD is a composite type; + * we don't presume here to know whether any other type is or not. + */ + int type = + null == returnType ? NOTYPE : + DT_RECORD.equals(returnType) ? RECORDTYPE : OTHERTYPE; + + if ( MethodShape.OTHER == shape && 0 != (out | type) ) + { + msg( Kind.ERROR, func, + "no type= or out= element may be applied to this method"); + return; + } + + switch ( out | type ) + { + case NOOUT | OTHERTYPE: + assumedComposite = true; // annotations not definitive; assume + shape.setComposite(this, msgr); + return; + case NOOUT | RECORDTYPE: + case MOREOUT | NOTYPE: + shape.setComposite(this, msgr); + return; + case ONEOUT | RECORDTYPE: // in case PostgreSQL one day allows this + forceResultRecord = true; + shape.setComposite(this, msgr); + return; + case ONEOUT | NOTYPE: + /* + * No special action needed here except for the MAYBECOMPOSITE + * or PROVIDER shapes, to check for likely mistakes. + */ + if ( MethodShape.MAYBECOMPOSITE == shape + && null == + paramTypeAnnotations[paramTypeAnnotations.length - 1] ) + { + msg(Kind.ERROR, func, + "a function with one declared OUT parameter returns " + + "it normally, not through an extra ResultSet " + + "parameter. If the trailing ResultSet parameter is " + + "intended as an input, it can be marked with an " + + "@SQLType annotation"); + } + else if ( MethodShape.PROVIDER == shape ) + { + msg(Kind.ERROR, func, + "a set-returning function with one declared OUT " + + "parameter must return an Iterator, not a " + + "ResultSetProvider or ResultSetHandle"); + } + return; + case NOOUT | NOTYPE: + /* + * No special action; MAYBECOMPOSITE will treat as noncomposite, + * ITERATOR and PROVIDER will behave as they always do. + */ + return; + case ONEOUT | OTHERTYPE: + msg( Kind.ERROR, func, + "no type= allowed here (the out parameter " + + "declares its own type"); + return; + case MOREOUT | RECORDTYPE: + case MOREOUT | OTHERTYPE: + msg( Kind.ERROR, func, + "type= and out= may not be combined here"); + return; + default: + throw new AssertionError("unhandled case"); + } + } + /** * Return a stream of {@code ParameterInfo} 'records' for the function's * parameters in order. @@ -2100,12 +2320,8 @@ Stream parameterInfo() */ void resolveParameterAndReturnTypes() { - if ( null != _type && null != _out ) - msg( Kind.ERROR, func, "A PL/Java function may specify " + - "only one of type, out"); - - if ( null != _type ) - returnType = DBType.fromSQLTypeAnnotation( _type); + if ( null != returnType ) + /* it was already set from a type= attribute */; else if ( null != setofComponent ) returnType = tmpr.getSQLType( setofComponent, func); else if ( setof ) @@ -2119,10 +2335,13 @@ else if ( setof ) if ( null != _out ) { - returnType = DT_RECORD; outParameters = Arrays.stream(_out) .map(DBType::fromNameAndType) .collect(toList()); + if ( 1 < _out.length || forceResultRecord ) + returnType = DT_RECORD; + else + returnType = outParameters.get(0).getValue(); } } @@ -2170,6 +2389,9 @@ public void subsume() * appropriate) and parameters, either with any defaults indicated * (for use in CREATE FUNCTION) or without (for use in DROP FUNCTION). * + * @param sb StringBuilder in which to generate the SQL. + * @param names Whether to include the parameter names. + * @param outs Whether to include out parameters. * @param dflts Whether to include the defaults, if any. */ void appendNameAndParams( @@ -2181,7 +2403,7 @@ void appendNameAndParams( /** * Internal version taking name and parameter stream as extra arguments - * so they can be overridded from {@link Transformed}. + * so they can be overridden from {@link Transformed}. */ void appendNameAndParams( StringBuilder sb, boolean names, boolean outs, boolean dflts, @@ -2196,7 +2418,7 @@ void appendNameAndParams( /** * Takes the parameter stream as an extra argument - * so it can be overridded from {@link Transformed}. + * so it can be overridden from {@link Transformed}. */ void appendParams( StringBuilder sb, boolean names, boolean outs, boolean dflts, @@ -2272,6 +2494,22 @@ String[] deployStrings( { ArrayList al = new ArrayList<>(); StringBuilder sb = new StringBuilder(); + if ( assumedComposite ) + sb.append("/*\n * PL/Java generated this declaration assuming" + + "\n * a composite-returning function was intended." + + "\n * If a boolean function with a row-typed parameter" + + "\n * was intended, add any @SQLType annotation on the" + + "\n * ResultSet final parameter to make the intent clear." + + "\n */\n"); + if ( forceResultRecord ) + sb.append("/*\n * PL/Java generated this declaration for a" + + "\n * function with one OUT parameter that was annotated" + + "\n * to explicitly request treatment as a function that" + + "\n * returns RECORD. A given version of PostgreSQL might" + + "\n * not accept such a declaration. More at" + + "\n * https://www.postgresql.org/message-id/" + + "619BBE78.7040009%40anastigmatix.net" + + "\n */\n"); sb.append( "CREATE OR REPLACE FUNCTION "); appendNameAndParams( sb, true, true, true, qname, params); sb.append( "\n\tRETURNS "); From 3c6de64243d278d281a17509df9e175ce1a06479 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 18 Jan 2022 21:02:48 -0500 Subject: [PATCH 0917/1087] Add examples for single-OUT-parameter functions Add methods to the ReturnComposite example illustrating single-row and set-returning functions with a single OUT parameter, and also examples using @SQLType annotations to distinguish similar-looking composite- and non-composite-valued functions. --- .../example/annotation/ReturnComposite.java | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java index 9a5be5ed..fb9965d0 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ReturnComposite.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2022 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 @@ -21,6 +21,7 @@ import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLType; /** * Demonstrates {@code @Function(out={...})} for a function that returns a @@ -70,6 +71,29 @@ public static boolean helloOutParams(ResultSet out) throws SQLException return true; } + /** + * A function that does not return a composite type, despite having + * a similar Java form. + *

    + * Without the {@code type=} element, this would not be mistaken for + * composite. With the {@code type=} element (a contrived example, will cast + * the method's boolean result to text), PL/Java would normally match the + * method to the composite pattern (other than {@code pg_catalog.RECORD}, + * PL/Java does not pretend to know at compile time which types might be + * composite). The explicit {@code SQLType} annotation on the trailing + * {@code ResultSet} parameter forces it to be seen as an input, and the + * method to be seen as an ordinary method that happens to return boolean. + */ + @Function( + schema = "javatest", type = "text" + ) + public static boolean + notOutParams(@SQLType("pg_catalog.record") ResultSet in) + throws SQLException + { + return true; + } + /** * Returns a two-column table result that does not have to be * a predeclared composite type, or require the calling SQL query to @@ -105,4 +129,51 @@ public boolean assignRowValues(ResultSet out, long currentRow) public void close() { } + + /** + * Returns a result described by one {@code out} parameter. + *

    + * Such a method is written in the style of any method that returns + * a scalar value, rather than receiving a writable {@code ResultSet} + * as a parameter. + */ + @Function( + schema = "javatest", out = { "greeting text" } + ) + public static String helloOneOut() throws SQLException + { + return "Hello"; + } + + /** + * Has a boolean result described by one {@code out} parameter. + *

    + * Because this method returns boolean and has a trailing row-typed + * input parameter, that parameter must have an {@code SQLType} + * annotation so that the method will not look like the more-than-one-OUT + * composite form, which would be rejected as a likely mistake. + */ + @Function( + schema = "javatest", out = { "exquisite boolean" } + ) + public static boolean boolOneOut(@SQLType("pg_catalog.record") ResultSet in) + throws SQLException + { + return true; + } + + /** + * Returns a table result described by one {@code out} parameter. + *

    + * Such a method is written in the style of any method that returns a set + * of some scalar value, using an {@code Iterator} rather than a + * {@code ResultSetProvider} or {@code ResultSetHandle}. + */ + @Function( + schema = "javatest", out = { "addressee text" } + ) + public static Iterator helloOneOutTable() throws SQLException + { + return new ReturnComposite().addressees; + } } From ba59342151131a461c37bfe0e04dfce3ce9a6cca Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 18 Jan 2022 22:32:30 -0500 Subject: [PATCH 0918/1087] A few nits noticed after 1.6.3 Clarified comments in sqlgen.Lexicals, and a missing equals() short-circuit in BasePrincipal, and making its name field protected rather than private. --- .../org/postgresql/pljava/BasePrincipal.java | 6 ++++-- .../org/postgresql/pljava/sqlgen/Lexicals.java | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java b/pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java index fad251e6..ed20de77 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/BasePrincipal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2022 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 @@ -55,11 +55,13 @@ private void readObject(ObjectInputStream in) "deserializing a BasePrincipal with null name"); } - private Simple m_name; + protected final Simple m_name; @Override public boolean equals(Object other) { + if ( this == other ) + return true; if ( getClass().isInstance(other) ) return m_name.equals(((BasePrincipal)other).m_name); return false; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 6be147df..1f633853 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2022 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 @@ -456,7 +456,12 @@ public static abstract class Identifier implements Serializable public abstract String deparse(Charset cs); /** - * Indicates whether some other object is "equal to" this one. + * Equality test with the case-sensitivity rules of SQL. + * @param other Object to compare to + * @return true if two quoted Identifiers match exactly, or two + * non-quoted ones match in either the PostgreSQL or ISO SQL folded + * form, or a quoted one exactly matches either folded form of a + * non-quoted one. */ @Override public boolean equals(Object other) @@ -1027,6 +1032,13 @@ public static final class Pseudo extends Simple * {@code PUBLIC} schema. That is a real catalog object that has * the actual name {@code PUBLIC}, and should be represented as a * {@code Simple} with that name. + *

    + * Note: through PG 14 at least, the database itself does not treat + * the public grantee in the way anticipated here; it is, instead, + * treated as an ordinary folding name "public" and forbidden as the + * name of any role. Therefore, a model of grantee roles would not + * need this symbol after all, but the definition will remain here + * illustrating the concept. */ public static final Pseudo PUBLIC = new Pseudo("PUBLIC"); From 9e492c4d49d11d4dd6ab6f0c6823ad1e34a7f688 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 18 Jan 2022 22:51:36 -0500 Subject: [PATCH 0919/1087] Avoid Session split-brain syndrome 0772030 inadvertently created a chance for the user code to retrieve one Session instance from SessionManager.current() while implementation code gets a different one from Backend.getSession(). Oops. Well, it took this long to be noticed.... In passing, fix a typo in the Session API interface's javadocs. --- .../main/java/org/postgresql/pljava/Session.java | 4 ++-- .../org/postgresql/pljava/internal/Backend.java | 11 +---------- .../org/postgresql/pljava/internal/Session.java | 16 +++++++++++++++- .../pljava/internal/SubXactListener.java | 4 ++-- .../postgresql/pljava/internal/XactListener.java | 4 ++-- 5 files changed, 22 insertions(+), 17 deletions(-) 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 7cd2e5fa..4dbc9694 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -20,7 +20,7 @@ /** * A Session brings together some useful methods and data for the current * database session. It provides a set of attributes (a - * {@code String} to {@code Object} map. Until PL/Java 1.2.0, its attribute + * {@code String} to {@code Object} map). Until PL/Java 1.2.0, its attribute * store had transactional behavior (i.e., the data * added since the last commit would be lost on a transaction rollback, or kept * after a commit), but in 1.2.0 and later, it has not, and has functioned as a diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index e2673694..47bfe905 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -73,18 +73,9 @@ public class Backend } } - private static Session s_session; - private static final Pattern s_gucList = Pattern.compile(String.format( "\\G(?:%1$s)(?,\\s*+)?+", ISO_AND_PG_IDENTIFIER_CAPTURING)); - public static synchronized Session getSession() - { - if(s_session == null) - s_session = new Session(); - return s_session; - } - /** * Do an operation on a thread with serialized access to call into * PostgreSQL, returning a result. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java index 9b834963..cb44c12b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -44,6 +44,20 @@ */ public class Session implements org.postgresql.pljava.Session { + public static Session provider() + { + return Holder.INSTANCE; + } + + private Session() + { + } + + private static class Holder + { + static final Session INSTANCE = new Session(); + } + @SuppressWarnings("removal") private final TransactionalMap m_attributes = new TransactionalMap(new HashMap()); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java index 7dfa837f..0d72231f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SubXactListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -76,7 +76,7 @@ private static void invokeListeners( throws SQLException { Target target = s_refs[eventIndex]; - Session session = Backend.getSession(); + Session session = org.postgresql.pljava.internal.Session.provider(); // Take a snapshot. Handlers might unregister during event processing for ( Invocable listener : diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java index f24fda5f..5c61433e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/XactListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -77,7 +77,7 @@ private static void invokeListeners(int eventIndex) { Checked.BiConsumer target = s_refs.get(eventIndex); - Session session = Backend.getSession(); + Session session = Session.provider(); // Take a snapshot. Handlers might unregister during event processing for ( Invocable listener : From 2d89e5bb099f7eb77d2dd4802503718a1826ac0d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 18 Jan 2022 23:20:43 -0500 Subject: [PATCH 0920/1087] Fix a thinko in context classloader management The clever arrangements in Function_vpcInvoke to avoid duplicating the context classloader assignment on the first call were all wrongheaded, because every call to vpcInvoke will have come through Function_invoke, which already assigned it, and therefore the duplicate assignment was still happening on every call but the first. --- pljava-so/src/main/c/Function.c | 7 +------ pljava-so/src/main/c/type/Type.c | 10 ++++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index 0f67a8a5..a46c0e6d 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -493,13 +493,8 @@ jboolean pljava_Function_vpcInvoke( * static area parameter counts; this reservation will therefore not see a * need to push a frame. If one was pushed for the user function itself, it * remains on top, to be popped when the Invocation is. - * - * It is better to avoid calling installContextLoader a second time under - * the same Invocation. */ reserveParameterFrame(1, 2); - if ( 0 != call_cntr ) - installContextLoader(self); JNI_setObjectArrayElement(s_referenceParameters, 0, rowcollect); s_primitiveParameters[0].j = call_cntr; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 7f169e31..a0d181e2 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -168,11 +168,9 @@ static void _closeIteration(CallContextData* ctxData) * Why pass 1 as the call_cntr? We won't always have the actual call_cntr * value at _closeIteration time (the _endOfSetCB isn't passed it), and the * Java interfaces being used don't need it (close() isn't passed a row - * number), but vpcInvoke needs a way to know when to skip its call to - * installContextLoader (which shouldn't happen on its very first call, - * the one with call_cntr of zero, which always happens as part of the first - * invocation of the SRF so the work has already been done). So passing any - * fixed value here that isn't zero is enough to distinguish the cases. + * number), but at least 1 is different from zero, in case vpcInvoke has + * a reason to distinguish the first call (in the same invocation as the + * overall setup) from subsequent ones. */ pljava_Function_vpcInvoke( ctxData->fn, ctxData->rowProducer, NULL, 1, JNI_TRUE, &dummy); From c7b16a03f70cc19b84e0f4709af9c8971a1f8354 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 19 Jan 2022 00:38:08 -0500 Subject: [PATCH 0921/1087] Fix mapping errors to java.time dates and times The (documented, but not necessary) limitation in the mapping of date infinity/-infinity to java.time.LocalDate was an avoidable integer overflow. It only affected dates within 30 years of infinity or -infinity. Addresses #390. In passing, solve the problem that the valid PostgreSQL time value 24:00:00.000000 is not valid to java.time.LocalTime or OffsetTime. The Java classes will accept the preceding nanosecond, which is still distinguishable from any other mapped PostgreSQL value (as those have only microsecond resolution), so make that the mapping of 24:00:00. --- .../pljava/example/annotation/JDBC42_21.java | 18 ++++++++++++++---- pljava-so/src/main/c/type/Date.c | 4 ++-- pljava-so/src/main/c/type/Time.c | 6 ++++-- src/site/markdown/use/datetime.md | 13 +++++++------ 4 files changed, 27 insertions(+), 14 deletions(-) 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 212cb13b..1b0d35e2 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2022 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 @@ -36,9 +36,11 @@ " END" + " FROM" + " (VALUES" + + " (date 'infinity')," + " (date '2017-08-21')," + " (date '1970-03-07')," + - " (date '1919-05-29')" + + " (date '1919-05-29')," + + " (date '-infinity')" + " ) AS p(orig)," + " javatest.roundtrip(p, 'java.time.LocalDate')" + " AS r(roundtripped date)", @@ -49,7 +51,11 @@ " ELSE javatest.logmessage('WARNING', 'java.time.LocalTime fails')" + " END" + " FROM" + - " (SELECT current_time::time) AS p(orig)," + + " (VALUES" + + " (current_time::time)," + + " ('00:00:00')," + + " ('24:00:00')" + + " ) AS p(orig)," + " javatest.roundtrip(p, 'java.time.LocalTime')" + " AS r(roundtripped time)", @@ -59,7 +65,11 @@ " ELSE javatest.logmessage('WARNING', 'java.time.OffsetTime fails')" + " END" + " FROM" + - " (SELECT current_time::timetz) AS p(orig)," + + " (VALUES" + + " (current_time::timetz)," + + " ('00:00:00')," + + " ('24:00:00')" + + " ) AS p(orig)," + " javatest.roundtrip(p, 'java.time.OffsetTime')" + " AS r(roundtripped timetz)", diff --git a/pljava-so/src/main/c/type/Date.c b/pljava-so/src/main/c/type/Date.c index 9d1c68f6..a96ff682 100644 --- a/pljava-so/src/main/c/type/Date.c +++ b/pljava-so/src/main/c/type/Date.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -57,7 +57,7 @@ static bool _LocalDate_canReplaceType(Type self, Type other) static jvalue _LocalDate_coerceDatum(Type self, Datum arg) { DateADT pgDate = DatumGetDateADT(arg); - jlong days = (jlong)(pgDate + EPOCH_DIFF); + jlong days = (jlong)pgDate + EPOCH_DIFF; jvalue result; result.l = JNI_callStaticObjectMethod( s_LocalDate_class, s_LocalDate_ofEpochDay, days); diff --git a/pljava-so/src/main/c/type/Time.c b/pljava-so/src/main/c/type/Time.c index a274d59e..31fd16ed 100644 --- a/pljava-so/src/main/c/type/Time.c +++ b/pljava-so/src/main/c/type/Time.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -83,6 +83,8 @@ static jvalue _LocalTime_coerceDatum(Type self, Datum arg) #endif 1000 * DatumGetInt64(arg); jvalue result; + if ( 1000L * USECS_PER_DAY == nanos ) + -- nanos; result.l = JNI_callStaticObjectMethod( s_LocalTime_class, s_LocalTime_ofNanoOfDay, nanos); return result; @@ -95,7 +97,7 @@ static Datum _LocalTime_coerceObject(Type self, jobject time) #if PG_VERSION_NUM < 100000 (!integerDateTimes) ? Float8GetDatum(((double)nanos) / 1e9) : #endif - Int64GetDatum(nanos / 1000); + Int64GetDatum((nanos + 1) / 1000); } static Type _LocalTime_obtain(Oid typeId) diff --git a/src/site/markdown/use/datetime.md b/src/site/markdown/use/datetime.md index b56ada32..6a4f0481 100644 --- a/src/site/markdown/use/datetime.md +++ b/src/site/markdown/use/datetime.md @@ -73,6 +73,11 @@ zone, which can vary from session to session. Any code developed for PL/Java and Java 8 or newer is strongly encouraged to use these types for date/time manipulations, for their much better fit to the PostgreSQL types. +PostgreSQL accepts 24:00:00.000000 as a valid time, while a day for +`LocalTime` or `OffsetTime` maxes out at the preceding nanosecond. That is +still a distinguishable value (as the PostgreSQL resolution is only to +microseconds), so the PostgreSQL 24 value is bidirectionally mapped to that. + ### Mapping of time and timestamp with time zone When a `time with time zone` is mapped to a `java.time.OffsetTime`, the Java @@ -103,17 +108,13 @@ Java values to compare others against. It must compare with `equals()`; it cannot assume that the mapping will produce the very same Java objects repeatedly, but only objects with equal values. -When timestamps are mapped to the `java.time` classes, the mapping will have +When dates and timestamps are mapped to the `java.time` classes, +the mapping will have the useful property that `-infinity` really is earlier than other PostgreSQL-representable values, and `infinity` really is later. That does not hold under the old `java.sql.Timestamp` mapping, where both values will be distant from the present but not further specified. -The convenient relation does not hold for dates at all, under the `java.sql` or -`java.time` mappings; `infinity` and `-infinity` just have to be treated as two -special values. They come out as two consecutive days in the late Miocene, -as it happens, in the third week of June. - #### Infinite timestamps without `integer_datetimes` In PostgreSQL builds with `integer_datetimes` as `off` (a configuration that is From e60adcc093005b41c8942e10c64bd107466fecbe Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 19 Jan 2022 10:22:33 -0500 Subject: [PATCH 0922/1087] Downgrade JEP411 WARNING to NOTICE on < 17 Was originally made a warning so as not to be silenced by the elevel set within CREATE EXTENSION. But then it was moved to be emitted only at successful transaction end, so as not to clutter any actual error messages, and that also solves the suppressed-by-CREATE-EXTENSION issue, so it is possible to use the less-alarming NOTICE level when running on Java versions after 11 but before 17. --- pljava-so/src/main/c/Backend.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 125132e5..a02988b4 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 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 @@ -246,6 +246,7 @@ static bool warnJEP411 = true; * (pg_upgrade) where a JVM hasn't been launched to learn its version). */ static bool javaGT11 = true; +static bool javaGE17 = false; static void initsequencer(enum initstage is, bool tolerant); @@ -987,6 +988,7 @@ void _PG_init() static void initPLJavaClasses(void) { jfieldID fID; + int javaMajor; JNINativeMethod backendMethods[] = { { @@ -1072,7 +1074,9 @@ static void initPLJavaClasses(void) PgObject_registerNatives2(s_Backend_class, backendMethods); fID = PgObject_getStaticJavaField(s_Backend_class, "JAVA_MAJOR", "I"); - javaGT11 = 11 < JNI_getStaticIntField(s_Backend_class, fID); + javaMajor = JNI_getStaticIntField(s_Backend_class, fID); + javaGT11 = 11 < javaMajor; + javaGE17 = 17 <= javaMajor; fID = PgObject_getStaticJavaField(s_Backend_class, "THREADLOCK", "Ljava/lang/Object;"); @@ -2022,7 +2026,7 @@ void Backend_warnJEP411(bool isCommit) warningEmitted = true; - ereport(WARNING, ( + ereport(javaGE17 ? WARNING : NOTICE, ( errmsg( "[JEP 411] migration advisory: there will be a Java version " "(after Java 17) that will be unable to run PL/Java %s " From 8d401b6180d7022cce04d38f1d96b8f698a2c11b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 19 Jan 2022 18:02:11 -0500 Subject: [PATCH 0923/1087] Update release notes --- src/site/markdown/releasenotes.md.vm | 93 ++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index 1af1d5d7..cb10d1dc 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,7 +10,87 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') -$h2 PL/Java 1.6.3 +$h2 PL/Java 1.6.4 + +This is the fourth minor update in the PL/Java 1.6 series. It is a minor +bug-fix release with few other notable changes. Further information +on the changes may be found below. + +$h3 Changes + +$h4 Changes affecting administration + +$h5 JEP 411 advisory message downgraded + +PL/Java follows certain administrative actions with an advisory message +about upcoming Java changes that will affect policy enforcement. In 1.6.3, +that message was always at `WARNING` level. In 1.6.4, it is downgraded +to `NOTICE` level when running on Java versions before 17. + +As before, updates on how PL/Java will adapt to the future Java changes +can be watched by bookmarking [the JEP 411 topic][jep411] on the PL/Java wiki. + +$h4 Changes to runtime behavior + +$h5 Java's thread context class loader + +Starting in 1.6.3, PL/Java has made sure the thread context class loader +on entry to a function reflects the initiating loader for the implementing +class, but in 1.6.3, the loader could be set too many times when executing +a set-returning function. That could lead to the wrong loader being restored +on function exit, likely only noticeable in an application with jars in +multiple schemas and with nested Java function invocations. + +That is fixed in this release. + +$h4 Improvements to the annotation-driven SQL generator + +$h5 Correct SQL now emitted for a function with _one_ `OUT` parameter + +PostgreSQL requires a different form of declaration for a function with +one `OUT` parameter than for a function with two or more. PL/Java was +emitting the wrong form for the one-out-parameter case. + +The documentation has been clarified and new [examples][exoneout] added +to illustrate how Java methods should be structured and annotated for +both cases. + +The intended treatment can be ambiguous for some Java methods, without +explicit annotations. For example, a Java method that returns `boolean` +and has a trailing `ResultSet` parameter could be a single-row +composite-returning function, or an ordinary function with a row-typed +input parameter and `boolean` return. PL/Java has always silently chosen +which interpretation to apply in such cases, and still does, but it now +emits a comment into the generated deployment descriptor, explaining +what annotation to use if a different meaning was intended. + +$h5 `BaseUDT` annotation now has constants for known type categories + +PostgreSQL user-defined types can be declared in categories, some of which +are predefined. PL/Java's `BaseUDT` annotation now includes constants for +those, which can be used in place of the single-letter codes for readability. + +$h3 Bugs fixed + +* [Wrong SQL generated for function with one `OUT` parameter](${ghbug}386) +* [`Session` object not reliably singleton](${ghbug}388) +* [Set-returning function has context classloader set too many times](${ghbug}389) +* [`java.time.LocalDate` mismapping within 30 years of +/-infinity](${ghbug}390) + +[exoneout]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/ReturnComposite.html#method.summary + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + +$h2 PL/Java 1.6.3 (10 October 2021) This is the third minor update in the PL/Java 1.6 series. It adds support for PostgreSQL 14, continues to improve the runtime behavior and @@ -125,17 +205,6 @@ $h3 Credits Thanks to Krzysztof Nienartowicz, `ricdhen`, and `JanaParthasarathy`, who among them reported five of the issues fixed in this release. -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 PL/Java 1.6.2 This is the second minor update in the PL/Java 1.6 series, with two bugs fixed From 2c6fe36b3655520ca9363aa8eb8cdf46e67fe2a2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 19 Jan 2022 18:02:32 -0500 Subject: [PATCH 0924/1087] Poke migration-management versions for 1.6.4 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 17eef8d9..4e0de536 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -889,6 +889,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_6_4 = REL_1_6_0; static final SchemaVariant REL_1_6_3 = REL_1_6_0; static final SchemaVariant REL_1_6_2 = REL_1_6_0; static final SchemaVariant REL_1_6_1 = REL_1_6_0; From bc6fc6a4556f9550b444f7077dca0aa5b75b4de8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 19 Jan 2022 18:17:26 -0500 Subject: [PATCH 0925/1087] Add control file in preparation for next release Now that 1.6.4 is released, the next release should include an extension SQL file allowing upgrade from 1.6.4. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index 335ba7d9..c85fc478 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -255,6 +255,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Wed, 26 Jan 2022 14:58:49 -0500 Subject: [PATCH 0926/1087] Update obsolete javadoc information These changes should have been included in pull request #256, but were overlooked at the time. --- .../postgresql/pljava/annotation/BaseUDT.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java index e416f2af..3798c0a3 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java @@ -56,13 +56,15 @@ *

    * Other static methods in the class may be exported as SQL functions by * marking them with {@code @Function} in the usual way, and will not have any - * special treatment on account of being in a UDT class. If those function - * declarations will depend on the existence of this type, or the type must + * special treatment on account of being in a UDT class. Those function + * declarations will be correctly ordered before or after this type's, in common + * cases such as when this type appears in their signatures, or the type must * refer to the functions (as it must for * {@link #typeModifierInput typeModifierInput} or - * {@link #typeModifierOutput typeModifierOutput} functions, for example), - * appropriate {@link #provides provides}/{@link #requires requires} labels must - * be used in their {@code @Function} annotations and this annotation, to make + * {@link #typeModifierOutput typeModifierOutput} functions, for example). + * In a case that the automatic ordering does not handle correctly, + * appropriate {@link #provides provides}/{@link #requires requires} labels can + * be used in the {@code @Function} annotations and this annotation, to make * the order come out right. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) @Documented @@ -254,9 +256,8 @@ public interface Code *

    * Even if the method is defined on the UDT class marked by this annotation, * it is not automatically found or used. It will need its own - * {@link Function} annotation giving it a name and a {@code provides} - * label, and this annotation must refer to it by that name and include the - * label in {@code requires} to ensure the SQL is generated in the right + * {@link Function} annotation giving it a name, and this annotation must + * refer to it by that name to ensure the SQL is generated in the right * order. */ String typeModifierInput() default ""; @@ -274,9 +275,8 @@ public interface Code *

    * Even if the method is defined on the UDT class marked by this annotation, * it is not automatically found or used. It will need its own - * {@link Function} annotation giving it a name and a {@code provides} - * label, and this annotation must refer to it by that name and include the - * label in {@code requires} to ensure the SQL is generated in the right + * {@link Function} annotation giving it a name, and this annotation must + * refer to it by that name to ensure the SQL is generated in the right * order. */ String typeModifierOutput() default ""; @@ -288,6 +288,12 @@ public interface Code * The details of the necessary API are in {@code vacuum.h}. + *

    + * Even if the method is defined on the UDT class marked by this annotation, + * it is not automatically found or used. It will need its own + * {@link Function} annotation giving it a name, and this annotation must + * refer to it by that name to ensure the SQL is generated in the right + * order. */ String analyze() default ""; From fe7abe7998bce8b54e72d45172589a50eb0a7b7d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 8 Feb 2022 12:19:45 -0500 Subject: [PATCH 0927/1087] Fail validation if TRANSFORM FOR TYPE declared The TRANSFORM FOR TYPE mechanism is only meaningful for a PL whose handler function itself does the work of looking up and applying the transforms. https://www.postgresql.org/message-id/61FDBCFE.3090800%40anastigmatix.net This PL doesn't, so say so when validating a function declaration that includes a TRANSFORM clause. It would arguably be better to say so even earlier, at an attempted CREATE TRANSFORM for the PL, but PostgreSQL does not currently apply any PL-specific validator function at that time. --- .../org/postgresql/pljava/internal/Function.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 842ad815..0c7de5b0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2016-2022 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 @@ -55,6 +55,7 @@ import java.sql.ResultSet; import java.sql.SQLData; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLInput; import java.sql.SQLOutput; import java.sql.SQLNonTransientException; @@ -1318,6 +1319,18 @@ public static Invocable create( { Matcher info = parse(procTup); + /* + * Reject any TRANSFORM FOR TYPE clause at validation time, on + * the grounds that it will get ignored at invocation time anyway. + * The check could be made unconditional, and so catch at invocation + * time any function that might have been declared before this validator + * check was added. But simply ignoring the clause at invocation time + * (as promised...) keeps that path leaner. + */ + if ( forValidator && null != procTup.getObject("protrftypes") ) + throw new SQLFeatureNotSupportedException( + "a PL/Java function will not apply TRANSFORM FOR TYPE","0A000"); + if ( forValidator && ! checkBody ) return null; From 1fed9034d21cefe599ae03767b8ff2495e6330ec Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 15 Feb 2022 17:02:48 -0500 Subject: [PATCH 0928/1087] Compute supported source version in DDRProcessor Because PL/Java must be compilable on any Java back to release 9, the DDRProcessor cannot refer by name to any SourceVersion enum constant later than RELEASE_9. It also, arguably, should not, because its development and testing rely on the javax.lang.model API as of release 9. Happily, in practice, later Java releases do not often break the DDRProcessor code, so user Java code for releases later than 9 can be compiled with no difficulty, other than a compiler warning about the processor's source version being pegged at 9. But the warning is an obstacle if the user code is being compiled with a fail-on-warning policy, as in issue #403. This patch adopts a compromise position, and keeps track of the latest source version for which the annotation processor at least has been seen to pass the CI tests (fully understanding that such testing is no substitute for fully auditing any release-to-release changes in the javax.lang.model APIs and what impact they could have on the processor!). It will compute its "declared" supported source version to be the earlier of SourceVersion.latestSupported() and that latest tested version. As a result, it should eliminate the compiler warning when running on any Java version in that range. The warning will reappear when running a compile on a later Java version, and it should be easy to alleviate that with a PL/Java release that bumps the latest_tested version. Merely passing the CI tests as normally run isn't enough, because the project is built with a --release 9 option. Before actually bumping latest_tested, a test build (at least of the pljava-examples project) should be done on the Java release in question and without the limiting --release option. --- .../annotation/processing/DDRProcessor.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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 b78e321f..6dfaefb7 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 @@ -158,10 +158,31 @@ "ddr.implementor", // implementor when not annotated, default "PostgreSQL" "ddr.output" // name of ddr file to write }) -@SupportedSourceVersion(SourceVersion.RELEASE_9) public class DDRProcessor extends AbstractProcessor { private DDRProcessorImpl impl; + + @Override + public SourceVersion getSupportedSourceVersion() + { + /* + * Because this must compile on Java versions back to 9, it must not + * mention by name any SourceVersion constant later than RELEASE_9. + * + * Update latest_tested to be the latest Java release on which this + * annotation processor has been tested without problems. + */ + int latest_tested = 17; + int ordinal_9 = SourceVersion.RELEASE_9.ordinal(); + int ordinal_latest = latest_tested - 9 + ordinal_9; + + SourceVersion latestSupported = SourceVersion.latestSupported(); + + if ( latestSupported.ordinal() <= ordinal_latest ) + return latestSupported; + + return SourceVersion.values()[ordinal_latest]; + } @Override public void init( ProcessingEnvironment processingEnv) From 2167527bb874de26c53fd67049d770e3471bb968 Mon Sep 17 00:00:00 2001 From: sincatter Date: Fri, 24 Jun 2022 00:31:44 +0800 Subject: [PATCH 0929/1087] Fix the problem of invalid timer in _destroyJavaVM After registering the timer with RegisterTimeout(), call enable_timeout_after() to make the timer take effect --- pljava-so/src/main/c/Backend.c | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index a02988b4..9472e964 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1406,6 +1406,7 @@ static void _destroyJavaVM(int status, Datum dummy) #if PG_VERSION_NUM >= 90300 tid = RegisterTimeout(USER_TIMEOUT, terminationTimeoutHandler); + enable_timeout_after(tid, 5000); #else saveSigAlrm = pqsignal(SIGALRM, terminationTimeoutHandler); enable_sig_alarm(5000, false); From be307abe44050e7b89877a534d5b7684e2266e1c Mon Sep 17 00:00:00 2001 From: Francisco Miguel Biete Banon Date: Fri, 21 Oct 2022 13:54:36 +0100 Subject: [PATCH 0930/1087] Support PostgreSQL v15 String is a PostgreSQL type, rename pljava String to PLJString --- pljava-so/src/main/c/type/String.c | 16 ++++++++-------- pljava-so/src/main/include/pljava/type/String.h | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index 4b58f622..01e2a18b 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -59,9 +59,9 @@ jvalue _String_coerceDatum(Type self, Datum arg) { jvalue result; char* tmp = DatumGetCString(FunctionCall3( - &((String)self)->textOutput, + &((PLJString)self)->textOutput, arg, - ObjectIdGetDatum(((String)self)->elementType), + ObjectIdGetDatum(((PLJString)self)->elementType), Int32GetDatum(-1))); result.l = String_createJavaStringFromNTS(tmp); pfree(tmp); @@ -83,19 +83,19 @@ Datum _String_coerceObject(Type self, jobject jstr) JNI_deleteLocalRef(jstr); ret = FunctionCall3( - &((String)self)->textInput, + &((PLJString)self)->textInput, CStringGetDatum(tmp), - ObjectIdGetDatum(((String)self)->elementType), + ObjectIdGetDatum(((PLJString)self)->elementType), Int32GetDatum(-1)); pfree(tmp); return ret; } -static String String_create(TypeClass cls, Oid typeId) +static PLJString String_create(TypeClass cls, Oid typeId) { HeapTuple typeTup = PgObject_getValidTuple(TYPEOID, typeId, "type"); Form_pg_type pgType = (Form_pg_type)GETSTRUCT(typeTup); - String self = (String)TypeClass_allocInstance(cls, typeId); + PLJString self = (PLJString)TypeClass_allocInstance(cls, typeId); MemoryContext ctx = GetMemoryChunkContext(self); fmgr_info_cxt(pgType->typoutput, &self->textOutput, ctx); fmgr_info_cxt(pgType->typinput, &self->textInput, ctx); @@ -109,7 +109,7 @@ Type String_obtain(Oid typeId) return (Type)StringClass_obtain(s_StringClass, typeId); } -String StringClass_obtain(TypeClass self, Oid typeId) +PLJString StringClass_obtain(TypeClass self, Oid typeId) { return String_create(self, typeId); } @@ -126,7 +126,7 @@ jstring String_createJavaString(text* t) Size srcLen = VARSIZE(t) - VARHDRSZ; if(srcLen == 0) return s_the_empty_string; - + if ( s_two_step_conversion ) { utf8 = (char*)pg_do_encoding_conversion((unsigned char*)src, diff --git a/pljava-so/src/main/include/pljava/type/String.h b/pljava-so/src/main/include/pljava/type/String.h index 6abf0752..a03f915d 100644 --- a/pljava-so/src/main/include/pljava/type/String.h +++ b/pljava-so/src/main/include/pljava/type/String.h @@ -19,9 +19,9 @@ extern "C" { * The String class extends the Type and adds the members necessary to * perform standard Postgres textin/textout conversion. An instance of this * class will be used for all types that are not explicitly mapped. - * + * * The class also has some convenience routings for Java String manipulation. - * + * * @author Thomas Hallgren * **************************************************************************/ @@ -29,7 +29,7 @@ extern "C" { extern jclass s_Object_class; extern jclass s_String_class; struct String_; -typedef struct String_* String; +typedef struct String_* PLJString; /* * Create a Java String object from a null terminated string. Conversion is @@ -73,7 +73,7 @@ extern text* String_createText(jstring javaString); extern Type String_obtain(Oid typeId); -extern String StringClass_obtain(TypeClass self, Oid typeId); +extern PLJString StringClass_obtain(TypeClass self, Oid typeId); #ifdef __cplusplus } From 6fbe91bf975ec032d4bfa4b4105c18cd10963c22 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 May 2023 18:48:21 -0400 Subject: [PATCH 0931/1087] Disable failing CI test until an issue 434 fix In passing, also add semicolons where jshell is (sometimes) lax about them. It is sometimes handy to use copy/paste from these test scripts into a live jshell, and its tolerance of missing semicolons can be diminished then. Refactoring the github and appveyor CI scripts to share one copy of the jshell script would be a fine thing. --- .github/workflows/ci-runnerpg.yml | 42 +++++++++++++++++-------------- appveyor.yml | 40 ++++++++++++++++------------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index d77d5a53..9155acd6 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -200,18 +200,18 @@ jobs: 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 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; String javaHome = System.getProperty("java.home"); @@ -225,12 +225,13 @@ jobs: : javaLibDir.resolve(s_isWindows ? "jvm.dll" : "server/libjvm.so") ); - String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" + String vmopts = + "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; - Node n1 = Node.get_new_node("TestNode1") + Node n1 = Node.get_new_node("TestNode1"); if ( s_isWindows ) - n1.use_pg_ctl(true) + n1.use_pg_ctl(true); /* * Keep a tally of the three types of diagnostic notices that may be @@ -241,7 +242,7 @@ jobs: Map results = Stream.of("info", "warning", "error", "ng").collect( LinkedHashMap::new, - (m,k) -> m.put(k, 0), (r,s) -> {}) + (m,k) -> m.put(k, 0), (r,s) -> {}); boolean isDiagnostic(Object o, Set whatIsNG) { @@ -470,13 +471,14 @@ jobs: * 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 + * For now, until issue #434 has a fix, DO NOT + * 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. + * should (for now, also NOT) be tested instead. */ try ( Connection c = n1.connect() ) { @@ -515,6 +517,7 @@ jobs: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ + if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -548,6 +551,7 @@ jobs: (o,p,q) -> null == o ); } + } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; @@ -556,5 +560,5 @@ jobs: System.out.println(results); succeeding &= (0 == results.get("ng")); - System.exit(succeeding ? 0 : 1) + System.exit(succeeding ? 0 : 1); ENDJSHELL diff --git a/appveyor.yml b/appveyor.yml index bf404ff0..4f645a08 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -88,27 +88,27 @@ test_script: @' 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 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; 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" + String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; - Node n1 = Node.get_new_node("TestNode1") + Node n1 = Node.get_new_node("TestNode1"); - n1.use_pg_ctl(true) + n1.use_pg_ctl(true); /* * Keep a tally of the three types of diagnostic notices that may be @@ -118,7 +118,8 @@ test_script: */ Map results = Stream.of("info", "warning", "error", "ng").collect( - LinkedHashMap::new, (m,k) -> m.put(k, 0), (r,s) -> {}) + LinkedHashMap::new, + (m,k) -> m.put(k, 0), (r,s) -> {}); boolean isDiagnostic(Object o, Set whatIsNG) { @@ -346,13 +347,14 @@ test_script: * 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 + * For now, until issue #434 has a fix, DO NOT + * 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. + * should (for now, also NOT) be tested instead. */ try ( Connection c = n1.connect() ) { @@ -391,6 +393,7 @@ test_script: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ + if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -424,6 +427,7 @@ test_script: (o,p,q) -> null == o ); } + } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; @@ -432,7 +436,7 @@ test_script: System.out.println(results); succeeding &= (0 == results.get("ng")); - System.exit(succeeding ? 0 : 1) + System.exit(succeeding ? 0 : 1); '@ | jshell ` -execution local ` From e658009a09bf576168a20822fef9f6ddd6bd242a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 May 2023 20:33:49 -0400 Subject: [PATCH 0932/1087] DDRProcessor tests ok with Java 19 Not with Java 20. Likely related to issue #435. --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6dfaefb7..38046ae4 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 @@ -172,7 +172,7 @@ public SourceVersion getSupportedSourceVersion() * Update latest_tested to be the latest Java release on which this * annotation processor has been tested without problems. */ - int latest_tested = 17; + int latest_tested = 19; int ordinal_9 = SourceVersion.RELEASE_9.ordinal(); int ordinal_latest = latest_tested - 9 + ordinal_9; From 5f34cef0092324ad192057f94ae6ce6fe67c34ff Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 May 2023 21:56:07 -0400 Subject: [PATCH 0933/1087] Overlooked copyright year. --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 38046ae4..1d14ca66 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-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -9,6 +9,7 @@ * Contributors: * Tada AB * Purdue University + * Chapman Flack */ package org.postgresql.pljava.annotation.processing; From 1a32d91d90f276b7e6e9bc2204ffe9c3d2a2757f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 18:15:33 -0400 Subject: [PATCH 0934/1087] Unrelated message typo fixed in passing --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1d14ca66..50d6adfb 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 @@ -2287,7 +2287,7 @@ else if ( MethodShape.PROVIDER == shape ) case ONEOUT | OTHERTYPE: msg( Kind.ERROR, func, "no type= allowed here (the out parameter " + - "declares its own type"); + "declares its own type)"); return; case MOREOUT | RECORDTYPE: case MOREOUT | OTHERTYPE: From 4c420b78540f179473fd5a02708345e126176436 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 18:07:53 -0400 Subject: [PATCH 0935/1087] Relax the check of function's enclosing type The SQL/JRT standard has always just said class, but there is no technical obstacle to using static methods on an interface, so relax the annotation-processing-time check that was preventing that. Addresses #426. --- .../annotation/processing/DDRProcessor.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) 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 50d6adfb..3a479115 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 @@ -1089,16 +1089,29 @@ void processFunction( Element e) for ( Element ee = e; null != ( ee = ee.getEnclosingElement() ); ) { - if ( ElementKind.CLASS.equals( ee.getKind()) ) + ElementKind ek = ee.getKind(); + switch ( ek ) { - if ( ! ee.getModifiers().contains( Modifier.PUBLIC) ) - msg( Kind.ERROR, ee, - "A PL/Java function must not have a non-public " + - "enclosing class"); - if ( ((TypeElement)ee).getNestingKind().equals( - NestingKind.TOP_LEVEL) ) - break; + case CLASS: + case INTERFACE: + break; + default: + msg( Kind.ERROR, ee, + "A PL/Java function must not have an enclosing " + ek); + return; } + + // It's a class or interface, represented by TypeElement + TypeElement te = (TypeElement)ee; + mods = ee.getModifiers(); + + if ( ! mods.contains( Modifier.PUBLIC) ) + msg( Kind.ERROR, ee, + "A PL/Java function must not have a non-public " + + "enclosing class"); + + if ( ! te.getNestingKind().isNested() ) + break; // no need to look above top-level class } FunctionImpl f = getSnippet( e, FunctionImpl.class, () -> @@ -2489,10 +2502,7 @@ String makeAS() if ( ! ( complexViaInOut || setof || trigger ) ) sb.append( typu.erasure( func.getReturnType())).append( '='); Element e = func.getEnclosingElement(); - if ( ! e.getKind().equals( ElementKind.CLASS) ) - msg( Kind.ERROR, func, - "Somehow this method got enclosed by something other " + - "than a class"); + // e was earlier checked and ensured to be a class or interface sb.append( e.toString()).append( '.'); sb.append( trigger ? func.getSimpleName() : func.toString()); return sb.toString(); From a1eb0b2a583520ab8504874117396276a18a30b9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 19:39:33 -0400 Subject: [PATCH 0936/1087] Example: function on interface or nested type --- .../example/annotation/OnInterface.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java new file mode 100644 index 00000000..e98a8cfb --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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 org.postgresql.pljava.annotation.Function; + +/** + * Illustrates PL/Java functions on an interface instead of a class. + *

    + * The SQL/JRT standard has always just said "class", but there is no technical + * obstacle to permitting a PL/Java function to be a static interface method, so + * that earlier restriction has been relaxed. + */ +public interface OnInterface +{ + /** + * Returns the answer. + */ + @Function(schema = "javatest") + static int answer() + { + return 42; + } + + interface A + { + /** + * Again the answer. + */ + @Function(schema = "javatest") + static int nestedAnswer() + { + return 42; + } + } + + class B + { + /** + * Still the answer. + */ + @Function(schema = "javatest") + public static int nestedClassAnswer() + { + return 42; + } + + public static class C + { + /** + * That answer again. + */ + @Function(schema = "javatest") + public static int moreNestedAnswer() + { + return 42; + } + } + } +} From 2534c98520728e04aa9c4d73cd339c65f87a1275 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 18:48:21 -0400 Subject: [PATCH 0937/1087] Who ever noticed 1-letter identifiers don't work? The runtime parsing of function AS strings, since 1.6, has been requiring a Java identifier to be a single javaIdentifierStart plus one or more javaIdentifierPart, which of course should be zero or more. The result is an unexpected error if a one-letter identifier appears as a package-name component or a class or method name. Addresses #438. --- .../src/main/java/org/postgresql/pljava/internal/Function.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 0c7de5b0..1b8793ed 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1839,7 +1839,7 @@ private static String getAS(ResultSet procTup) throws SQLException * Uncompiled pattern to recognize a Java identifier. */ private static final String javaIdentifier = String.format( - "\\p{%1$sStart}\\p{%1sPart}++", "javaJavaIdentifier" + "\\p{%1$sStart}\\p{%1sPart}*+", "javaJavaIdentifier" ); /** From b7eff644901cfafc9fcf20741e3ff1d687a9259e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 19:38:41 -0400 Subject: [PATCH 0938/1087] Emit nested type names properly spelled --- .../pljava/annotation/processing/DDRProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 3a479115..aeb3139e 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 @@ -2503,7 +2503,7 @@ String makeAS() sb.append( typu.erasure( func.getReturnType())).append( '='); Element e = func.getEnclosingElement(); // e was earlier checked and ensured to be a class or interface - sb.append( e.toString()).append( '.'); + sb.append( elmu.getBinaryName((TypeElement)e)).append( '.'); sb.append( trigger ? func.getSimpleName() : func.toString()); return sb.toString(); } @@ -2833,7 +2833,7 @@ public String[] deployStrings() return deployStrings( qnameFrom(name(), schema()), null, // parameter iterable unused in appendParams below - "UDT[" + te + "] " + id.name(), + "UDT[" + elmu.getBinaryName(te) + "] " + id.name(), comment()); } @@ -3053,7 +3053,7 @@ public String[] deployStrings() } al.add( "SELECT sqlj.add_type_mapping(" + DDRWriter.eQuote( qname.toString()) + ", " + - DDRWriter.eQuote( tclass.toString()) + ')'); + DDRWriter.eQuote( elmu.getBinaryName(tclass)) + ')'); addComment( al); return al.toArray( new String [ al.size() ]); } From 8e3b6c96f197529ad932881a62464f6d7d9e3629 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 21:46:14 -0400 Subject: [PATCH 0939/1087] Don't over-narrow permissions fetching jar URL If an HTTP URL is used, either a SocketPermission or a URLPermission is supposed to work, but HttpURLConnection's getPermission() method predates URLPermission and just returns the other one. That can lead to an empty set of effective permissions if the policy granted one and the set is intersected with the other. Also, for HttpURLConnections, install an Authenticator that will try to find userinfo in the requesting URL, if the remote end sends a password authentication challenge. Beware, though, that sqlj.jar_repository has a jarorigin column, and URLs used to install jars show up there! Both of these items are from issue #425. --- .../pljava/management/Commands.java | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 519051f5..2170e240 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -18,12 +18,18 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.Authenticator; +import java.net.HttpURLConnection; +import java.net.PasswordAuthentication; +import java.net.URI; import java.net.URL; import java.net.URLConnection; +import java.net.URLPermission; import java.nio.ByteBuffer; import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharacterCodingException; +import java.security.Permission; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -36,6 +42,7 @@ import java.sql.Statement; import java.text.ParseException; import java.util.ArrayList; +import static java.util.Arrays.fill; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; @@ -387,6 +394,48 @@ public class Commands private static final Identifier.Simple s_public_schema = Identifier.Simple.fromCatalog("public"); + /** + * An {@link Authenticator} that will try the {@code userinfo} of the + * requesting URL if present. + *

    + * Beware that such URLs will appear in + * {@code sqlj.jar_repository.jarorigin} if used to install a jar! + */ + private static class EmbeddedPwdAuthenticator extends Authenticator + { + private EmbeddedPwdAuthenticator() { } + + static final EmbeddedPwdAuthenticator INSTANCE = + new EmbeddedPwdAuthenticator(); + + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + String userinfo = + URI.create(getRequestingURL().toString()).getUserInfo(); + if ( null == userinfo ) + return null; + int len = userinfo.length(); + int uend = userinfo.indexOf(':'); + int pstart; + if ( -1 == uend ) + uend = pstart = len; + else + pstart = 1 + uend; + String u = userinfo.substring(0, uend); + char[] p = new char[len - pstart]; + try + { + userinfo.getChars(pstart, len, p, 0); + return new PasswordAuthentication(u, p); + } + finally + { + fill(p, '\245'); // PasswordAuthentication clones it + } + } + } + /** * Reads the jar found at the specified URL and stores the entries in the * jar_entry table. @@ -402,6 +451,31 @@ static void addClassImages(int jarId, String urlString) URL url = new URL(urlString); URLConnection uc = url.openConnection(); long[] sz = new long[1]; + Permission[] least = { uc.getPermission() }; + + if ( uc instanceof HttpURLConnection ) + { + /* + * Augment what uc returned as the least privilege set needed + * to connect. HttpURLConnection's getPermission method is older + * than URLPermission, and it only returns a SocketPermission. + * Set up 'least' to include both, so as not to end up with an + * empty permission set when 'least' includes one and the policy + * granted the other. + */ + least = new Permission[] { + least[0], + new URLPermission(urlString, "GET") + }; + + /* + * In case authentication is needed, set an Authenticator that + * will try userinfo from the URL if present. (Beware that jar + * origin URLs are stored in sqlj.jar_repository.jarorigin!) + */ + ((HttpURLConnection)uc).setAuthenticator( + EmbeddedPwdAuthenticator.INSTANCE); + } /* * Do uc.connect() with PL/Java implementation's permissions, but @@ -413,7 +487,7 @@ static void addClassImages(int jarId, String urlString) uc.connect(); sz[0] = uc.getContentLengthLong(); return uc.getInputStream(); - }, null, uc.getPermission()) + }, null, least) ) { addClassImages(jarId, urlStream, sz[0]); From cc5c62d8f574a75e7d1ab825eb8cc30143b4f8e0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 14:29:42 -0400 Subject: [PATCH 0940/1087] Node.installSaxonAndExamplesAndPath thinko The method is documented to install Saxon, add it to the classpath, then install the examples, and then add it so the classpath had both. But the path was ending up with only examples. --- pljava-packaging/src/main/java/Node.java | 48 +++++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index fa4e0b00..dd658f9f 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -1102,6 +1102,42 @@ public static Stream setClasspath( return q(ps, ps::execute); } + /** + * Append a jar to a schema's class path if not already included. + * @return a {@link #q(Statement,Callable) result stream} that includes, on + * success, a one-column {@code void} result set with a single row if the + * jar was added to the path, and no rows if the jar was already included. + */ + public static Stream appendClasspathIf( + Connection c, String schema, String jarName) + throws Exception + { + PreparedStatement ps = c.prepareStatement( + "SELECT" + + " sqlj.set_classpath(" + + " schema," + + " pg_catalog.concat_ws(" + + " ':'," + + " VARIADIC oldpath OPERATOR(pg_catalog.||) ARRAY[jar]" + + " )" + + " )" + + "FROM" + + " (VALUES (?, ?)) AS p(schema, jar)," + + " COALESCE(" + + " pg_catalog.regexp_split_to_array(" + + " sqlj.get_classpath(schema)," + + " ':'" + + " )," + + " CAST (ARRAY[] AS pg_catalog.text[])" + + " ) AS t(oldpath)" + + "WHERE" + + " jar OPERATOR(pg_catalog.<>) ALL (oldpath)" + ); + ps.setString(1, schema); + ps.setString(2, jarName); + return q(ps, ps::execute); + } + /** * Execute some arbitrary SQL * @return a {@link #q(Statement,Callable) result stream} from executing @@ -1551,8 +1587,8 @@ public static Stream installExamples(Connection c, boolean deploy) } /** - * Install the examples jar, under the name {@code examples}, and place it - * on the class path for schema {@code public}. + * Install the examples jar, under the name {@code examples}, and append it + * to the class path for schema {@code public}. *

    * The return of a concatenated result stream from two consecutive * statements might be likely to fail in cases where the first @@ -1567,7 +1603,7 @@ public static Stream installExamplesAndPath( throws Exception { Stream s1 = installExamples(c, deploy); - Stream s2 = setClasspath(c, "public", "examples"); + Stream s2 = appendClasspathIf(c, "public", "examples"); return Stream.concat(s1, s2); } @@ -1589,7 +1625,7 @@ public static Stream installSaxon( } /** - * Install a Saxon jar under the name {@code saxon}, and place it on the + * Install a Saxon jar under the name {@code saxon}, and append it to the * class path for schema {@code public}. * @return a combined {@link #q(Statement,Callable) result stream} from * executing the statements @@ -1599,7 +1635,7 @@ public static Stream installSaxonAndPath( throws Exception { Stream s1 = installSaxon(c, repo, version); - Stream s2 = setClasspath(c, "public", "saxon"); + Stream s2 = appendClasspathIf(c, "public", "saxon"); return Stream.concat(s1, s2); } From c732326574e8a5aa8bc6d17e018076ef39e30501 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 14:32:25 -0400 Subject: [PATCH 0941/1087] Tweaks to CI scripts in passing May as well have them add the -Djava.security.manager=allow for Java > 17. Add a ; in one, because while jshell is normally tolerant of missing ones, it is less so when its input is from terminal paste ... and it's convenient to copy/paste from the CI scripts when testing. --- .github/workflows/ci-runnerpg.yml | 5 ++++- appveyor.yml | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 9155acd6..be649a75 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -215,7 +215,7 @@ jobs: String javaHome = System.getProperty("java.home"); - Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib") + Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib"); Path libjvm = ( "Mac OS X".equals(System.getProperty("os.name")) @@ -228,6 +228,9 @@ jobs: 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 ) diff --git a/appveyor.yml b/appveyor.yml index 4f645a08..3ca83331 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -106,6 +106,9 @@ test_script: 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); From 16f5802467fa6ebb8c9f42840a64e49070d7b1bc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 20:42:33 -0400 Subject: [PATCH 0942/1087] Test install_jar with an http URL --- .github/workflows/ci-runnerpg.yml | 101 +++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index be649a75..45b05f44 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -192,7 +192,7 @@ jobs: -execution local \ "-J--class-path=$packageJar$pathSep$jdbcJar" \ "--class-path=$packageJar" \ - "-J--add-modules=java.sql.rowset" \ + "-J--add-modules=java.sql.rowset,jdk.httpserver" \ "-J-Dpgconfig=$pgConfig" \ "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" \ "-J-DmavenRepo=$mavenRepo" \ @@ -212,6 +212,20 @@ jobs: 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; + /* + * 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"); @@ -394,6 +408,91 @@ jobs: // 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" + + "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(); + URL u = new URI( + "http", null, 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); + } + } + } + ); + + 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. */ From 28255017e318f51e6df82456b97ceeffc23de92e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 21:57:11 -0400 Subject: [PATCH 0943/1087] Factor out useTrialPolicy in CI script --- .github/workflows/ci-runnerpg.yml | 69 ++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 45b05f44..e0dc000a 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -276,6 +276,42 @@ jobs: 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( @@ -351,37 +387,13 @@ jobs: */ try ( Connection c2 = n1.connect() ) { - Path trialPolicy = - createTempFile(n1.data_dir().getParent(), "trial", "policy"); - - write(trialPolicy, List.of( + succeeding &= useTrialPolicy(n1, c2, 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')" + @@ -477,6 +489,13 @@ jobs: } ); + succeeding &= useTrialPolicy(n1, c2, List.of( + "grant codebase \"${org.postgresql.pljava.codesource}\" {", + " permission", + " java.net.URLPermission \"http:*\", \"GET\";", + "};" + )); + succeeding &= stateMachine( "install a jar over http", null, From 5b1dbecfa83fc06becd45c62cdda80bdf42a1a01 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 22:11:53 -0400 Subject: [PATCH 0944/1087] DDR parser dislikes an empty action group It also has a typo in an error message. --- .github/workflows/ci-runnerpg.yml | 2 ++ .../pljava/management/SQLDeploymentDescriptor.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index e0dc000a..cc2d1607 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -440,6 +440,8 @@ jobs: "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) ); diff --git a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java index 85c1c7ad..168ce5d6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -332,7 +332,7 @@ else if(inQuote == c) default: if(inQuote == 0 && Character.isWhitespace((char)c)) { - // Change multiple whitespace into one singe space. + // Change multiple whitespace into one single space. // m_buffer.append(' '); c = this.skipWhite(); @@ -345,7 +345,7 @@ else if(inQuote == c) } } if(inQuote != 0) - throw this.parseError("Untermintated " + (char)inQuote + + throw this.parseError("Unterminated " + (char)inQuote + " starting at position " + startQuotePos); throw this.parseError("Unexpected EOF. Expecting ';' to end command"); From 768e44e99dccb86e49b772992e3a408e4d41f582 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 09:19:52 -0400 Subject: [PATCH 0945/1087] Add Basic authentication to http test --- .github/workflows/ci-runnerpg.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index cc2d1607..82562c40 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -466,8 +466,11 @@ jobs: ) { InetSocketAddress addr = hs.getAddress(); + + String id = "bar", pw = "baz"; + URL u = new URI( - "http", null, addr.getHostString(), addr.getPort(), + "http", id+':'+pw, addr.getHostString(), addr.getPort(), "/foo.jar", null, null ).toURL(); @@ -491,6 +494,17 @@ jobs: } ); + hc.setAuthenticator( + new BasicAuthenticator("CI realm", UTF_8) + { + @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", From b98c40cce861ae85246f1feb740577d11b11c06c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 09:48:21 -0400 Subject: [PATCH 0946/1087] Duplicate CI changes into Appveyor script The time is ripe to factor out a common jshell script for the different CI scripts to refer to. But that feels like a different patch. --- appveyor.yml | 182 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 24 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 3ca83331..e48a35f4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,6 +99,20 @@ test_script: import static org.postgresql.pljava.packaging.Node.q; import static org.postgresql.pljava.packaging.Node.stateMachine; import static org.postgresql.pljava.packaging.Node.isVoidResultSet; + /* + * 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 @@ -138,6 +152,42 @@ test_script: 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)); @@ -213,53 +263,136 @@ test_script: */ try ( Connection c2 = n1.connect() ) { - Path trialPolicy = - createTempFile(n1.data_dir().getParent(), "trial", "policy"); - - write(trialPolicy, List.of( + succeeding &= useTrialPolicy(n1, c2, 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)" + 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" ); - setVmOpts.setString(1, vmopts + - " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); + tryForbiddenRead.setString(1, System.getProperty("java.home")); succeeding &= stateMachine( - "change pljava.vmoptions", + "try to read a forbidden property", null, - q(setVmOpts, setVmOpts::execute) + q(tryForbiddenRead, tryForbiddenRead::execute) .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + (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 + } - 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" + /* + * 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); + } + } + } ); - tryForbiddenRead.setString(1, System.getProperty("java.home")); + hc.setAuthenticator( + new BasicAuthenticator("CI realm", UTF_8) + { + @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\";", + "};" + )); succeeding &= stateMachine( - "try to read a forbidden property", + "install a jar over http", null, - q(tryForbiddenRead, tryForbiddenRead::execute) + Node.installJar(c2, u.toString(), "foo", true) .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), @@ -267,7 +400,8 @@ test_script: (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, (o,p,q) -> null == o ); - // done with connection c2 + + // done with connection c2 again, and the http server } /* @@ -445,7 +579,7 @@ test_script: -execution local ` "-J--class-path=$packageJar;$jdbcJar" ` "--class-path=$packageJar" ` - "-J--add-modules=java.sql.rowset" ` + "-J--add-modules=java.sql.rowset,jdk.httpserver" ` "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` "-J-Dpgconfig=$pgConfig" ` "-J-DmavenRepo=$mavenRepo" ` From a1ff17bf716c20fe09e3c67975a5c2e28e0d3cf4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 11:52:47 -0400 Subject: [PATCH 0947/1087] Bump copyright date. This patch from sincatter addresses #407. --- pljava-so/src/main/c/Backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 9472e964..287652fd 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License From 0311278dd74d85fa7df4e308675bc27aeb997808 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 14:48:30 -0400 Subject: [PATCH 0948/1087] Fetch the PGDG apt key for GitHub Actions ... thanks to a tip from Rodrigo Alvarado a little tidier than what's on the wiki.postgresql.org Apt page. Also, for now, comment out all the Appveyor tests that have been reliably falling over for some reason of their own. --- .github/workflows/ci-runnerpg.yml | 2 + appveyor.yml | 78 +++++++++++++++---------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 9155acd6..e766c635 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -59,6 +59,8 @@ jobs: - name: Obtain PG development files (Ubuntu, PGDG) if: ${{ 'Linux' == runner.os }} run: | + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | + sudo apt-key add - echo \ deb \ http://apt.postgresql.org/pub/repos/apt \ diff --git a/appveyor.yml b/appveyor.yml index 4f645a08..cd98a166 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,51 +9,51 @@ environment: # - SYS: MINGW # JDK: 10 # PG: 12 - - SYS: MINGW - JDK: 11 - PG: 12 - - SYS: MINGW - JDK: 12 - PG: 12 - - SYS: MINGW - JDK: 13 - PG: 12 - - SYS: MINGW - JDK: 14 - PG: 12 - - SYS: MINGW - JDK: 15 - PG: 12 - - SYS: MSVC - JDK: 15 - PG: 12 - - SYS: MSVC - JDK: 14 - PG: 12 - - SYS: MSVC - JDK: 13 - PG: 12 - - SYS: MSVC - JDK: 12 - PG: 12 - - SYS: MSVC - JDK: 11 - PG: 12 + # - SYS: MINGW + # JDK: 11 + # PG: 12 + # - SYS: MINGW + # JDK: 12 + # PG: 12 + # - SYS: MINGW + # JDK: 13 + # PG: 12 + # - SYS: MINGW + # JDK: 14 + # PG: 12 + # - SYS: MINGW + # JDK: 15 + # PG: 12 + # - SYS: MSVC + # JDK: 15 + # PG: 12 + # - SYS: MSVC + # JDK: 14 + # PG: 12 + # - SYS: MSVC + # JDK: 13 + # PG: 12 + # - SYS: MSVC + # JDK: 12 + # PG: 12 + # - SYS: MSVC + # JDK: 11 + # PG: 12 # - SYS: MSVC # JDK: 10 # PG: 12 # - SYS: MSVC # JDK: 9 # PG: 12 - - SYS: MSVC - JDK: 14 - PG: 11 - - SYS: MSVC - JDK: 14 - PG: 10 - - SYS: MSVC - JDK: 14 - PG: 9.6 + # - SYS: MSVC + # JDK: 14 + # PG: 11 + # - SYS: MSVC + # JDK: 14 + # PG: 10 + # - SYS: MSVC + # JDK: 14 + # PG: 9.6 before_build: - ps: .appveyor/appveyor_download_java.ps1 - set JAVA_HOME=%ProgramFiles%\Java\jdk%JDK% From 581390889510fc552c2d67c0280400f3dfb72ebc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 15:00:48 -0400 Subject: [PATCH 0949/1087] Try another way to hush up app, umm, veyor --- appveyor.yml | 80 +++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index cd98a166..beb7babc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,5 @@ +only_commits: + message: /appveyor/ image: Visual Studio 2019 environment: APPVEYOR_RDP_PASSWORD: MrRobot@2020 @@ -9,51 +11,51 @@ environment: # - SYS: MINGW # JDK: 10 # PG: 12 - # - SYS: MINGW - # JDK: 11 - # PG: 12 - # - SYS: MINGW - # JDK: 12 - # PG: 12 - # - SYS: MINGW - # JDK: 13 - # PG: 12 - # - SYS: MINGW - # JDK: 14 - # PG: 12 - # - SYS: MINGW - # JDK: 15 - # PG: 12 - # - SYS: MSVC - # JDK: 15 - # PG: 12 - # - SYS: MSVC - # JDK: 14 - # PG: 12 - # - SYS: MSVC - # JDK: 13 - # PG: 12 - # - SYS: MSVC - # JDK: 12 - # PG: 12 - # - SYS: MSVC - # JDK: 11 - # PG: 12 + - SYS: MINGW + JDK: 11 + PG: 12 + - SYS: MINGW + JDK: 12 + PG: 12 + - SYS: MINGW + JDK: 13 + PG: 12 + - SYS: MINGW + JDK: 14 + PG: 12 + - SYS: MINGW + JDK: 15 + PG: 12 + - SYS: MSVC + JDK: 15 + PG: 12 + - SYS: MSVC + JDK: 14 + PG: 12 + - SYS: MSVC + JDK: 13 + PG: 12 + - SYS: MSVC + JDK: 12 + PG: 12 + - SYS: MSVC + JDK: 11 + PG: 12 # - SYS: MSVC # JDK: 10 # PG: 12 # - SYS: MSVC # JDK: 9 # PG: 12 - # - SYS: MSVC - # JDK: 14 - # PG: 11 - # - SYS: MSVC - # JDK: 14 - # PG: 10 - # - SYS: MSVC - # JDK: 14 - # PG: 9.6 + - SYS: MSVC + JDK: 14 + PG: 11 + - SYS: MSVC + JDK: 14 + PG: 10 + - SYS: MSVC + JDK: 14 + PG: 9.6 before_build: - ps: .appveyor/appveyor_download_java.ps1 - set JAVA_HOME=%ProgramFiles%\Java\jdk%JDK% From 4a168917f94c54d55f007169ffb534f6d95d3e43 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 15:23:54 -0400 Subject: [PATCH 0950/1087] Add a branches: only: for good measure ... as it would be frustrating to get a quiet commit and then see appveyor spring to life and fail again on a pull request. --- appveyor.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index beb7babc..6f800f20 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,12 @@ +# These only_commits and branches settings ought to pretty much suppress +# Appveyor, whose runs have all been failing lately because of Maven repository +# connection resets that don't seem reproducible locally. This can be revisited +# later to see if things might be working again. only_commits: message: /appveyor/ +branches: + only: + - appveyor image: Visual Studio 2019 environment: APPVEYOR_RDP_PASSWORD: MrRobot@2020 From 1283031759f7100e0c7642b271f07ed8bd06a667 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 15:53:07 -0400 Subject: [PATCH 0951/1087] Update Java versions tested Keep 9 because it's the earliest 1.6 supports, 11 because LTS, 12 because some 10/11 weirdness got fixed then, 17 because LTS, and later ones. Copy the pljava.vmoptions tweak from the issue425 branch for Java > 17. --- .github/workflows/ci-runnerpg.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index e766c635..d619778c 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -28,7 +28,7 @@ jobs: # cc: msvc # - os: windows-latest # cc: mingw - java: [9, 11, 12, 13, 14, 15] + java: [9, 11, 12, 17, 18, 19] steps: @@ -59,8 +59,9 @@ jobs: - name: Obtain PG development files (Ubuntu, PGDG) if: ${{ 'Linux' == runner.os }} run: | - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | - sudo apt-key add - + 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 \ @@ -217,7 +218,7 @@ jobs: String javaHome = System.getProperty("java.home"); - Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib") + Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib"); Path libjvm = ( "Mac OS X".equals(System.getProperty("os.name")) @@ -230,6 +231,9 @@ jobs: 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 ) From efd1ce5c3b0ec0f28dc7925f748b7805621b57c3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 16:25:54 -0400 Subject: [PATCH 0952/1087] The problem may have been a tab in yaml --- .github/workflows/ci-runnerpg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index d619778c..b7a84cb7 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -61,7 +61,7 @@ jobs: 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 + sudo dd of=/etc/apt/trusted.gpg.d/apt.postgresql.org.gpg echo \ deb \ http://apt.postgresql.org/pub/repos/apt \ From e124899bed5197f4b76aaf2298abe0d8f077f070 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 17:25:11 -0400 Subject: [PATCH 0953/1087] Bump copyright year --- .../src/main/java/org/postgresql/pljava/internal/Function.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 1b8793ed..6015bde7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2016-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License From 6cf1b3ab94c5696b2898a0d10a1a54fad22211e2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 17:48:27 -0400 Subject: [PATCH 0954/1087] Cherrypick changes to hush app(cough)veyor --- appveyor.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index e48a35f4..dcaae034 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,12 @@ +# These only_commits and branches settings ought to pretty much suppress +# Appveyor, whose runs have all been failing lately because of Maven repository +# connection resets that don't seem reproducible locally. This can be revisited +# later to see if things might be working again. +only_commits: + message: /appveyor/ +branches: + only: + - appveyor image: Visual Studio 2019 environment: APPVEYOR_RDP_PASSWORD: MrRobot@2020 From efe76916f05fc784ba9adf91531c009ed09d3dee Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 18:30:20 -0400 Subject: [PATCH 0955/1087] Fix CI tests for Java < 14 The BasicAuthenticator constructor that takes a Charset only appeared in Java 14, but hasn't got a Since: in its javadoc. --- .github/workflows/ci-runnerpg.yml | 3 ++- appveyor.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 82562c40..ee4819c9 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -495,7 +495,8 @@ jobs: ); hc.setAuthenticator( - new BasicAuthenticator("CI realm", UTF_8) + 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) diff --git a/appveyor.yml b/appveyor.yml index dcaae034..2195e74c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -380,7 +380,8 @@ test_script: ); hc.setAuthenticator( - new BasicAuthenticator("CI realm", UTF_8) + 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) From 9f2e3419c37d07df474c9b9920616a92d2564ccf Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Jun 2023 13:06:42 -0400 Subject: [PATCH 0956/1087] Document the use of --add-modules PL/Java code loaded with install_jar is treated as unnamed-module, legacy classpath code. Such code is advertised as having access to all readable modules, without having to declare dependencies (as it can't declare dependencies; it would have to be a named module to do that). Ah, but what are "all readable modules"? Only the ones enumerated at JVM startup, using as root modules PL/Java itself and anything named with --add-modules. So that option must be used if any Java modules will be needed beyond the ones PL/Java itself depends on. This added documentation addresses #419. --- src/site/markdown/install/vmoptions.md | 25 ++++++++++++++++ src/site/markdown/use/jpms.md | 40 ++++++++++++++++++++++---- src/site/markdown/use/use.md | 8 ++++++ src/site/markdown/use/variables.md | 6 ++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/site/markdown/install/vmoptions.md b/src/site/markdown/install/vmoptions.md index 87ed91fe..9a32b203 100644 --- a/src/site/markdown/install/vmoptions.md +++ b/src/site/markdown/install/vmoptions.md @@ -7,6 +7,31 @@ options are likely to be worth setting. If using [the OpenJ9 JVM][hsj9], be sure to look also at the [VM options specific to OpenJ9][vmoptJ9]. +## Adding to the set of readable modules + +By default, a small set of Java modules (including `java.base`, +`org.postgresql.pljava`, and `java.sql` and its transitive dependencies, +which include `java.xml`) will be readable to any Java code installed with +`install_jar`. + +While those modules may be enough for many uses, other modules are easily added +using `--add-modules` within `pljava.vmoptions`. For example, +`--add-modules=java.net.http,java.rmi` would make the HTTP Client and WebSocket +APIs readable, along with the Remote Method Invocation API. + +For convenience, the module `java.se` simply transitively requires all the +modules that make up the full Java SE API, so `--add-modules=java.se` will make +that full API available to PL/Java code without further thought. The cost, +however, may be that PL/Java uses more memory and starts more slowly than if +only a few needed modules were named. + +Third-party modular code can be made available by adding the modular jars +to `pljava.module_path` (see [configuration variables](../use/variables.html)) +and naming those modules in `--add-modules`. PL/Java currently treats all jars +loaded with `install_jar` as unnamed-module, legacy classpath code. + +For more, see [PL/Java and the Java Platform Module System](../use/jpms.html). + ## Byte order for PL/Java-implemented user-defined types PL/Java is free of byte-order issues except when using its features for building diff --git a/src/site/markdown/use/jpms.md b/src/site/markdown/use/jpms.md index 7c7890a1..dc635d0d 100644 --- a/src/site/markdown/use/jpms.md +++ b/src/site/markdown/use/jpms.md @@ -36,10 +36,11 @@ legacy code can be migrated over time: * A jar file containing legacy, non-modular code should be placed on the class path, and is treated as part of an unnamed module that has access - to the exports and opens of any other modules, so it will continue to work - as it did before Java 9. (Even a jar containing Java 9+ modular code - will be treated this way, if found on the class path rather than the - module path.) + to the exports and opens of all other _readable_ modules. Such code will, + therefore, continue to work as it did before Java 9, provided the needed + modules are _readable_, as explained below. (Even a jar containing Java 9+ + modular code will be treated this way, if found on the class path rather + than the module path.) * A jar file can be placed on the module path even if it does not contain an explicit named module. In that case, it becomes an "automatic" module, @@ -55,6 +56,33 @@ that does not include Java module system concepts. Its `sqlj.set_classpath` function manipulates an internal class path, not a module path, and a jar installed with `sqlj.install_jar` behaves as legacy code in an unnamed module. +## Readable versus observable modules + +Using Java's terminology, modules that can be found on the module path are +_observable_. Not all of those are automatically _readable_; the _readable_ +ones in a JVM instance are initially those encountered, at JVM start-up, in +the "recursive enumeration" step of [module resolution][resolution]. + +Recursive enumeration begins with some root modules, and proceeds until all of +the modules on which they (transitively) depend have been added to the readable +set. When PL/Java is launched in a session, PL/Java's own module is a root, +and so the readable modules will include those PL/Java itself depends on, +such as `java.base`, `java.sql`, and the other modules `java.sql` names with +`requires transitive` directives. + +Those modules may be enough for many uses of PL/Java. However, if code for use +in PL/Java will refer to other modules, +[`--add-modules` in `pljava.vmoptions`][addm] can be used to add more roots. +Because of recursive enumeration, it is enough to add just one module, or a few +modules, whose dependencies recursively cover whatever modules will be needed. + +At one extreme for convenience, Java provides a module, `java.se`, that simply +declares dependencies on the other modules that make up the full Java SE API. +Therefore, `--add-modules=java.se` will ensure that any PL/Java code is able to +refer to any of the Java SE API. However, PL/Java instances may use less memory +and start up more quickly if an effort is made to add only modules actually +needed. + ## Configuring the launch-time module path The configuration variable `pljava.module_path` controls the @@ -84,4 +112,6 @@ the usual way. It can be set by adding a `-Djava.class.path=...` in the is simply the jar file pathnames, separated by the platform's path separator character. -[jpms]: http://cr.openjdk.java.net/~mr/jigsaw/spec/ +[jpms]: https://cr.openjdk.java.net/~mr/jigsaw/spec/ +[resolution]: https://docs.oracle.com/javase/9/docs/api/java/lang/module/package-summary.html#resolution +[addm]: ../install/vmoptions.html#Adding_to_the_set_of_readable_modules diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index c25bef79..0f486d3b 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -34,6 +34,14 @@ Several [configuration variables](variables.html) can affect PL/Java's operation, including some common PostgreSQL variables as well as PL/Java's own. +### Enabling additional Java modules + +By default, PL/Java code can see a small set of Java modules, including +`java.base` and `java.sql` and a few others. To include others, use +[`--add-modules` in `pljava.vmoptions`][addm]. + +[addm]: ../install/vmoptions.html#Adding_to_the_set_of_readable_modules + ## Special topics ### Configuring permissions diff --git a/src/site/markdown/use/variables.md b/src/site/markdown/use/variables.md index 867b847b..cb465c1f 100644 --- a/src/site/markdown/use/variables.md +++ b/src/site/markdown/use/variables.md @@ -101,6 +101,11 @@ These PostgreSQL configuration variables can influence PL/Java's operation: PL/Java API jar file and the PL/Java internals jar file. To determine the proper setting, see [finding the files produced by a PL/Java build](../install/locate.html). + + If additional modular jars are added to the module path, + `--add-modules` in [`pljava.vmoptions`][addm] will make them readable by + PL/Java code. + For more on PL/Java's "module path" and "class path", see [PL/Java and the Java Platform Module System](jpms.html). @@ -181,3 +186,4 @@ These PostgreSQL configuration variables can influence PL/Java's operation: [jou]: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html [vmop]: ../install/vmoptions.html [sqlascii]: charsets.html#Using_PLJava_with_server_encoding_SQL_ASCII +[addm]: ../install/vmoptions.html#Adding_to_the_set_of_readable_modules From b74c56bbb5bd58293dbfb3bc7566016ec15a7f8a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Jun 2023 17:45:25 -0400 Subject: [PATCH 0957/1087] Improve exceptions when fetching jar URL Adding the underlying exception to the cause chain won't help much (until there is more progress on the Thoughts on Logging topic, anyway). More useful, for now, will be just including the underlying exception's class name in the new exception's message. Then at least you will see FileNotFoundException for a 404, etc. Manual testing confirms that connections to https URLs will work with no extra ceremony, as long as the server's certificate chains up to a Java-recognized issuer. No such test has been added for CI; too much fuss to spin up a test server with such a certificate. A test could, I suppose, be added to 'install' a nonexistent URL from some well-known https: server and at least confirm the connection is made and 404 is returned, but such use of an unsuspecting server from a CI workflow might be a little antisocial. For debugging an attempt that fails, although it will be hard to get enough useful information into the exception, testing shows that Java's logging at the log_min_messages=debug1 level will include plenty about the certificate chain received, TLS handshake, and request and response headers. --- .../java/org/postgresql/pljava/management/Commands.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 2170e240..8aa086a7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -495,8 +495,8 @@ static void addClassImages(int jarId, String urlString) } catch(IOException e) { - throw new SQLException("I/O exception reading jar file: " + - e.getMessage()); + throw new SQLException("reading jar file: " + + e.toString(), "58030", e); } } @@ -602,8 +602,8 @@ static void addClassImages(int jarId, InputStream urlStream, long sz) } catch(IOException e) { - throw new SQLException("I/O exception reading jar file: " - + e.getMessage(), "58030", e); + throw new SQLException("reading jar file: " + + e.toString(), "58030", e); } } From 627d1bc7a65b1e35958d59123c55c16cb1f3824a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 15:28:31 -0400 Subject: [PATCH 0958/1087] Private methods to bypass buggy namedGroups cache PL/Java builds and passes its tests on Java 20 this way. Of course, this code relies on Java 20 API and can only be built by bumping to 20 in the pljava-api POM. To do: make it reflective so it can build on earlier versions, and apply the workaround only if needed. Begins to address #435. --- .../postgresql/pljava/sqlgen/Lexicals.java | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 1f633853..081e5bee 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -47,6 +47,37 @@ public abstract class Lexicals { private Lexicals() { } // do not instantiate + /** + * Maps a capturing-group name to its group index, bypassing the buggy cache + * introduced in Java 20 (PL/Java issue 435). + */ + private static int gi(Matcher m, String gn) + { + Integer i = + m.pattern().namedGroups().get(requireNonNull(gn, "group name")); + if ( null != i ) + return i; + throw new IllegalArgumentException("No group with name <" + gn + ">"); + } + + /** + * Returns the equivalent of {@code m.start(gn)} but bypassing the buggy + * cache introduced in Java 20. + */ + private static int start(Matcher m, String gn) + { + return m.start(gi(m, gn)); + } + + /** + * Returns the equivalent of {@code m.group(gn)} but bypassing the buggy + * cache introduced in Java 20. + */ + private static String group(Matcher m, String gn) + { + return m.group(gi(m, gn)); + } + /** Allowed as the first character of a regular identifier by ISO. */ public static final Pattern ISO_REGULAR_IDENTIFIER_START = Pattern.compile( @@ -342,9 +373,9 @@ public static boolean separator(Matcher m, boolean significant) m.usePattern(SEPARATOR); if ( ! m.lookingAt() ) return result; // leave matcher region alone - if ( significant || -1 != m.start("nl") ) + if ( significant || -1 != start(m, "nl") ) result = true; - if ( -1 != m.start("nest") ) + if ( -1 != start(m, "nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * m.usePattern(BRACKETED_COMMENT_INSIDE); @@ -357,7 +388,7 @@ public static boolean separator(Matcher m, boolean significant) case 1: if ( ! m.lookingAt() ) throw new InputMismatchException("unclosed comment"); - if ( -1 != m.start("nest") ) + if ( -1 != start(m, "nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * ++ level; @@ -384,17 +415,17 @@ else if ( 0 == -- level ) */ public static Identifier.Simple identifierFrom(Matcher m) { - String s = m.group("i"); + String s = group(m, "i"); if ( null != s ) return Identifier.Simple.from(s, false); - s = m.group("xd"); + s = group(m, "xd"); if ( null != s ) return Identifier.Simple.from(s.replace("\"\"", "\""), true); - s = m.group("xui"); + s = group(m, "xui"); if ( null == s ) return null; // XXX? s = s.replace("\"\"", "\""); - String uec = m.group("uec"); + String uec = group(m, "uec"); if ( null == uec ) uec = "\\"; int uecp = uec.codePointAt(0); @@ -407,9 +438,9 @@ public static Identifier.Simple identifierFrom(Matcher m) { replacer.appendReplacement(sb, ""); int cp; - String uev = replacer.group("u4d"); + String uev = group(replacer, "u4d"); if ( null == uev ) - uev = replacer.group("u6d"); + uev = group(replacer, "u6d"); if ( null != uev ) cp = Integer.parseInt(uev, 16); else @@ -692,7 +723,7 @@ public static Simple fromJava(String s, Messager msgr) if ( m.find() ) { if ( 0 == m.start() && s.length() == m.end() ) - s = m.group("xd").replace("\"\"", "\""); + s = group(m, "xd").replace("\"\"", "\""); else warn = true; } From e81da5202b2f0236fa45d2c0ea7df258e555f430 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Jun 2023 19:30:55 -0400 Subject: [PATCH 0959/1087] Simply reject Java with JDK-8309515 bug Trying to include a workaround is simply too brittle; static methods that do the right thing in place of Matcher.{start,end,group} would have to be used not only in PL/Java's own code, but in anybody else's code that might use the Lexicals class; such workaround methods might have to be exposed and clutter the API, and there would still be no guarantee they'd be used everywhere needed. JDK-8309515 has been fixed for Java 21 (in openjdk/jdk@90027ff) and will presumably be backpatched to some build of 20. So simply test for it, and if necessary, advise using a working Java version. --- .../postgresql/pljava/sqlgen/Lexicals.java | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 081e5bee..85bdc66b 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -47,35 +47,42 @@ public abstract class Lexicals { private Lexicals() { } // do not instantiate - /** - * Maps a capturing-group name to its group index, bypassing the buggy cache - * introduced in Java 20 (PL/Java issue 435). - */ - private static int gi(Matcher m, String gn) + static { - Integer i = - m.pattern().namedGroups().get(requireNonNull(gn, "group name")); - if ( null != i ) - return i; - throw new IllegalArgumentException("No group with name <" + gn + ">"); - } + /* + * Reject a Java version affected by JDK-8309515 bug. + */ + Boolean hasBug = null; + Pattern p1 = Pattern.compile("(?.)(?.)"); + Pattern p2 = Pattern.compile("(?.)(?.)"); + Matcher m = p1.matcher("xy"); - /** - * Returns the equivalent of {@code m.start(gn)} but bypassing the buggy - * cache introduced in Java 20. - */ - private static int start(Matcher m, String gn) - { - return m.start(gi(m, gn)); - } + if ( m.matches() && 0 == m.start("a") ) + { + m.usePattern(p2); + if ( m.matches() ) + { + switch ( m.start("a") ) + { + case 0: + hasBug = true; + break; + case 1: + hasBug = false; + break; + } + } + } - /** - * Returns the equivalent of {@code m.group(gn)} but bypassing the buggy - * cache introduced in Java 20. - */ - private static String group(Matcher m, String gn) - { - return m.group(gi(m, gn)); + if ( null == hasBug ) + throw new ExceptionInInitializerError( + "Unexpected result while testing for bug JDK-8309515"); + + if ( hasBug ) + throw new ExceptionInInitializerError( + "Java bug JDK-8309515 affects this version of Java. PL/Java " + + "requires a Java version earlier than 20 (when the bug first " + + "appears) or recent enough to have had the bug fixed."); } /** Allowed as the first character of a regular identifier by ISO. @@ -373,9 +380,9 @@ public static boolean separator(Matcher m, boolean significant) m.usePattern(SEPARATOR); if ( ! m.lookingAt() ) return result; // leave matcher region alone - if ( significant || -1 != start(m, "nl") ) + if ( significant || -1 != m.start("nl") ) result = true; - if ( -1 != start(m, "nest") ) + if ( -1 != m.start("nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * m.usePattern(BRACKETED_COMMENT_INSIDE); @@ -388,7 +395,7 @@ public static boolean separator(Matcher m, boolean significant) case 1: if ( ! m.lookingAt() ) throw new InputMismatchException("unclosed comment"); - if ( -1 != start(m, "nest") ) + if ( -1 != m.start("nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * ++ level; @@ -415,17 +422,17 @@ else if ( 0 == -- level ) */ public static Identifier.Simple identifierFrom(Matcher m) { - String s = group(m, "i"); + String s = m.group("i"); if ( null != s ) return Identifier.Simple.from(s, false); - s = group(m, "xd"); + s = m.group("xd"); if ( null != s ) return Identifier.Simple.from(s.replace("\"\"", "\""), true); - s = group(m, "xui"); + s = m.group("xui"); if ( null == s ) return null; // XXX? s = s.replace("\"\"", "\""); - String uec = group(m, "uec"); + String uec = m.group("uec"); if ( null == uec ) uec = "\\"; int uecp = uec.codePointAt(0); @@ -438,9 +445,9 @@ public static Identifier.Simple identifierFrom(Matcher m) { replacer.appendReplacement(sb, ""); int cp; - String uev = group(replacer, "u4d"); + String uev = replacer.group("u4d"); if ( null == uev ) - uev = group(replacer, "u6d"); + uev = replacer.group("u6d"); if ( null != uev ) cp = Integer.parseInt(uev, 16); else @@ -723,7 +730,7 @@ public static Simple fromJava(String s, Messager msgr) if ( m.find() ) { if ( 0 == m.start() && s.length() == m.end() ) - s = group(m, "xd").replace("\"\"", "\""); + s = m.group("xd").replace("\"\"", "\""); else warn = true; } From a2bf6f72e14150a2006d2a4bf2c304fe225e5371 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 10:10:06 -0400 Subject: [PATCH 0960/1087] But for JDK-8309515, Java 20's ok for DDRProcessor --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1d14ca66..20f58cb5 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 @@ -173,7 +173,7 @@ public SourceVersion getSupportedSourceVersion() * Update latest_tested to be the latest Java release on which this * annotation processor has been tested without problems. */ - int latest_tested = 19; + int latest_tested = 20; int ordinal_9 = SourceVersion.RELEASE_9.ordinal(); int ordinal_latest = latest_tested - 9 + ordinal_9; From d417a9e516b5727b210069070e77ea5a3dd8e8f4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 12:37:27 -0400 Subject: [PATCH 0961/1087] InstallHelper, more java7ification/java8ification In passing, streamline recognizeSchema a bit. --- .../pljava/internal/InstallHelper.java | 167 ++++++++++-------- 1 file changed, 95 insertions(+), 72 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 4e0de536..6ec8b4d2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -705,6 +705,23 @@ private static void deployViaDescriptor( s.execute( "RESET pljava.implementors"); } + /** + * Query the database metadata for existence of a column in a table in the + * {@code sqlj} schema. Pass null for the column to simply check the table's + * existence. + */ + private static boolean hasColumn( + DatabaseMetaData md, String table, String column) + throws SQLException + { + try ( + ResultSet rs = md.getColumns( null, "sqlj", table, column) + ) + { + return rs.next(); + } + } + /** * Detect an existing PL/Java sqlj schema. Tests for changes between schema * variants that have appeared in PL/Java's git history and will return a @@ -723,97 +740,103 @@ private static SchemaVariant recognizeSchema( throws SQLException { DatabaseMetaData md = c.getMetaData(); - ResultSet rs = md.getProcedures( null, "sqlj", "alias_java_language"); - boolean seen = rs.next(); - rs.close(); - if ( seen ) - return SchemaVariant.REL_1_6_0; + try ( + ResultSet rs = + md.getProcedures( null, "sqlj", "alias_java_language") + ) + { + if ( rs.next() ) + return SchemaVariant.REL_1_6_0; + } - rs = md.getColumns( null, "sqlj", "jar_descriptor", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_descriptor", null) ) return SchemaVariant.UNREL20130301b; - rs = md.getColumns( null, "sqlj", "jar_descriptors", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_descriptors", null) ) return SchemaVariant.UNREL20130301a; - rs = md.getColumns( null, "sqlj", "jar_repository", "jarmanifest"); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_repository", "jarmanifest") ) return SchemaVariant.REL_1_3_0; - rs = md.getColumns( null, "sqlj", "typemap_entry", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "typemap_entry", null) ) return SchemaVariant.UNREL20060212; - rs = md.getColumns( null, "sqlj", "jar_repository", "jarowner"); - if ( rs.next() ) + try ( + ResultSet rs = + md.getColumns( null, "sqlj", "jar_repository", "jarowner") + ) { - int t = rs.getInt("DATA_TYPE"); - rs.close(); - if ( VARCHAR == t ) - return SchemaVariant.UNREL20060125; - return SchemaVariant.REL_1_1_0; + if ( rs.next() ) + { + if ( VARCHAR == rs.getInt("DATA_TYPE") ) + return SchemaVariant.UNREL20060125; + return SchemaVariant.REL_1_1_0; + } } - rs.close(); - rs = md.getColumns( null, "sqlj", "jar_repository", "deploymentdesc"); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_repository", "deploymentdesc") ) return SchemaVariant.REL_1_0_0; - rs = md.getColumns( null, "sqlj", "jar_entry", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_entry", null) ) return SchemaVariant.UNREL20040121; - rs = md.getColumns( null, "sqlj", "jar_repository", "jarimage"); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_repository", "jarimage") ) return SchemaVariant.UNREL20040120; - PreparedStatement ps = c.prepareStatement( "SELECT count(*) " + - "FROM pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + - "WHERE" + - " refclassid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_namespace'::regclass " + - " AND refobjid OPERATOR(pg_catalog.=) n.oid" + - " AND nspname OPERATOR(pg_catalog.=) 'sqlj' " + - " AND deptype OPERATOR(pg_catalog.=) 'n' " + - " AND NOT EXISTS ( " + - " SELECT 1 FROM " + - " pg_catalog.pg_class sqc JOIN pg_catalog.pg_namespace sqn " + - " ON relnamespace OPERATOR(pg_catalog.=) sqn.oid " + - " WHERE " + - " nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + - " AND relname OPERATOR(pg_catalog.=) 'pg_extension' " + - " AND classid OPERATOR(pg_catalog.=) sqc.oid " + - " OR " + - " nspname OPERATOR(pg_catalog.=) 'sqlj'" + - " AND relname OPERATOR(pg_catalog.=) ?" + - " AND classid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_class'::regclass " + - " AND objid OPERATOR(pg_catalog.=) sqc.oid)"); - ps.setString(1, loadpath_tbl); - rs = ps.executeQuery(); - if ( rs.next() && 0 == rs.getInt(1) ) + try ( + PreparedStatement stmt = Checked.Supplier.use((() -> + { + PreparedStatement ps = c.prepareStatement( + /* + * Is the sqlj schema 'empty'? Count the pg_depend + * type 'n' dependency entries referring to the sqlj + * namespace ... + */ + "SELECT count(*)" + + "FROM" + + " pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + + "WHERE" + + " refclassid OPERATOR(pg_catalog.=)" + + " 'pg_catalog.pg_namespace'::regclass " + + " AND refobjid OPERATOR(pg_catalog.=) n.oid" + + " AND nspname OPERATOR(pg_catalog.=) 'sqlj' " + + " AND deptype OPERATOR(pg_catalog.=) 'n' " + + /* + * ... but exclude from the count, if present: + */ + " AND NOT EXISTS ( " + + " SELECT 1 FROM " + + " pg_catalog.pg_class sqc" + + " JOIN pg_catalog.pg_namespace sqn" + + " ON relnamespace OPERATOR(pg_catalog.=) sqn.oid " + + " WHERE " + + /* + * (1) any dependency that is an extension (d.classid + * identifies pg_catalog.pg_extension) ... + */ + " nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + + " AND" + + " relname OPERATOR(pg_catalog.=) 'pg_extension' " + + " AND d.classid OPERATOR(pg_catalog.=) sqc.oid " + + " OR " + + /* + * (2) any dependency that is the loadpath_tbl table + * we temporarily create in the extension script. + */ + " nspname OPERATOR(pg_catalog.=) 'sqlj'" + + " AND relname OPERATOR(pg_catalog.=) ?" + + " AND classid OPERATOR(pg_catalog.=)" + + " 'pg_catalog.pg_class'::regclass " + + " AND objid OPERATOR(pg_catalog.=) sqc.oid)"); + ps.setString(1, loadpath_tbl); + return ps; + })).get(); + ResultSet rs = stmt.executeQuery(); + ) { - rs.close(); - ps.close(); - return SchemaVariant.EMPTY; + if ( rs.next() && 0 == rs.getInt(1) ) + return SchemaVariant.EMPTY; } - rs.close(); - ps.close(); return null; } From bb86d4dd7b44966b7a0d767e562566e1d10de287 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 16:11:51 -0400 Subject: [PATCH 0962/1087] Add objects to extension before CREATE OR REPLACE Until postgres/postgres@b9b21ac, CREATE OR REPLACE would silently absorb the object, if preexisting, into the extension being created. Since that change, those CREATE OR REPLACE operations now fail if the object exists but is not yet a member of the extension. Therefore, ALTER EXTENSION ADD them first. Because it's possible to be updating from an older PL/Java version (for example, one without the validator functions), this should be done in Java code where a failure because the expected object isn't there yet can be treated as benign. This special treatment is only needed for the few objects that get specially CREATE OR REPLACEd within the Java code. The rest can be handled with ALTER EXTENSION ADD commands in the extension script, as for any other extension. In passing, also use the tableoid system column to simplify an older query slightly. --- .../pljava/internal/InstallHelper.java | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 6ec8b4d2..86ef04d1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -413,6 +413,9 @@ public static void groundwork( throw new SQLNonTransientException( "sqlj schema not empty for CREATE EXTENSION pljava", "55000"); + if ( asExtension && ! exNihilo ) + preAbsorb(c, s); // handle possible update from unpackaged + handlers(c, s, module_pathname); languages(c, s); deployment(c, s, sv); @@ -432,6 +435,84 @@ public static void groundwork( } } + /** + * Absorb a few key objects into the extension, if they exist, before the + * operations that CREATE OR REPLACE them. + * + * Until postgres/postgres@b9b21ac, CREATE OR REPLACE would silently absorb + * the object, if preexisting, into the extension being created. Since that + * change, those CREATE OR REPLACE operations now fail if the object exists + * but is not yet a member of the extension. Therefore, this method is + * called first, to absorb those objects if they exist. Because this only + * matters when the objects do not yet belong to the extension (the old + * "FROM unpackaged" case), this method first checks and returns with no + * effect if javau_call_handler is already an extension member. + * + * Because it's possible to be updating from an older PL/Java version + * (for example, one without the validator functions), failure to add an + * expected object to the extension because the object doesn't exist yet + * is not treated here as an error. + */ + private static void preAbsorb( Connection c, Statement s) + throws SQLException + { + /* + * Do nothing if javau_call_handler is already an extension member. + */ + try ( + ResultSet rs = s.executeQuery( + "SELECT d.refobjid" + + " FROM" + + " pg_catalog.pg_namespace n" + + " JOIN pg_catalog.pg_proc p" + + " ON pronamespace OPERATOR(pg_catalog.=) n.oid" + + " JOIN pg_catalog.pg_depend d" + + " ON d.classid OPERATOR(pg_catalog.=) p.tableoid" + + " AND d.objid OPERATOR(pg_catalog.=) p.oid" + + " WHERE" + + " nspname OPERATOR(pg_catalog.=) 'sqlj'" + + " AND proname OPERATOR(pg_catalog.=) 'javau_call_handler'" + + " AND deptype OPERATOR(pg_catalog.=) 'e'" + ) + ) + { + if ( rs.next() ) + return; + } + + addExtensionUnless(c, s, "42883", "FUNCTION sqlj.java_call_handler()"); + addExtensionUnless(c, s, "42883", "FUNCTION sqlj.javau_call_handler()"); + addExtensionUnless(c, s, "42883", + "FUNCTION sqlj.java_validator(pg_catalog.oid)"); + addExtensionUnless(c, s, "42883", + "FUNCTION sqlj.javau_validator(pg_catalog.oid)"); + addExtensionUnless(c, s, "42704", "LANGUAGE java"); + addExtensionUnless(c, s, "42704", "LANGUAGE javaU"); + } + + /** + * Absorb obj into the pljava extension, unless it doesn't exist. + * Pass the sqlState expected when an obj of that type doesn't exist. + */ + private static void addExtensionUnless( + Connection c, Statement s, String sqlState, String obj) + throws SQLException + { + Savepoint p = null; + try + { + p = c.setSavepoint(); + s.execute("ALTER EXTENSION pljava ADD " + obj); + c.releaseSavepoint(p); + } + catch ( SQLException sqle ) + { + c.rollback(p); + if ( ! sqlState.equals(sqle.getSQLState()) ) + throw sqle; + } + } + /** * Create the {@code sqlj} schema, adding an appropriate comment and * granting {@code USAGE} to {@code public}. @@ -796,8 +877,7 @@ private static SchemaVariant recognizeSchema( "FROM" + " pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + "WHERE" + - " refclassid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_namespace'::regclass " + + " refclassid OPERATOR(pg_catalog.=) n.tableoid " + " AND refobjid OPERATOR(pg_catalog.=) n.oid" + " AND nspname OPERATOR(pg_catalog.=) 'sqlj' " + " AND deptype OPERATOR(pg_catalog.=) 'n' " + @@ -825,8 +905,7 @@ private static SchemaVariant recognizeSchema( */ " nspname OPERATOR(pg_catalog.=) 'sqlj'" + " AND relname OPERATOR(pg_catalog.=) ?" + - " AND classid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_class'::regclass " + + " AND classid OPERATOR(pg_catalog.=) sqc.tableoid" + " AND objid OPERATOR(pg_catalog.=) sqc.oid)"); ps.setString(1, loadpath_tbl); return ps; From 38e637bc35f9bc6d40bcebabb2cc0c0ac730db54 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 16:26:34 -0400 Subject: [PATCH 0963/1087] Re-enable the "from unpackaged" CI tests Addresses issue #434. --- .github/workflows/ci-runnerpg.yml | 7 ++----- appveyor.yml | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index b7a84cb7..08420142 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -477,14 +477,13 @@ jobs: * pljava.module_path set to the right locations of the jars, and the * correct shared-object path given to LOAD. * - * For now, until issue #434 has a fix, DO NOT - * also test the after-the-fact packaging up with CREATE EXTENSION + * 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 (for now, also NOT) be tested instead. + * should be tested instead. */ try ( Connection c = n1.connect() ) { @@ -523,7 +522,6 @@ jobs: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ - if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -557,7 +555,6 @@ jobs: (o,p,q) -> null == o ); } - } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; diff --git a/appveyor.yml b/appveyor.yml index 6f800f20..604c5fc3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -356,14 +356,13 @@ test_script: * pljava.module_path set to the right locations of the jars, and the * correct shared-object path given to LOAD. * - * For now, until issue #434 has a fix, DO NOT - * also test the after-the-fact packaging up with CREATE EXTENSION + * 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 (for now, also NOT) be tested instead. + * should be tested instead. */ try ( Connection c = n1.connect() ) { @@ -402,7 +401,6 @@ test_script: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ - if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -436,7 +434,6 @@ test_script: (o,p,q) -> null == o ); } - } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; From 49682f7a5ade8d9f4221e993cc5334c6d0427da6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 18:23:29 -0400 Subject: [PATCH 0964/1087] Ask for suitable MIME types when fetching a jar Watching DEBUG1 output revealed that the Accept header used by default was asking for other document types. As of pretty recently, application/java-archive is the registered IANA MIME type for a jar. Also accept the "deprecated alias names" listed in the registration. Setting the Accept header requires any URLPermission granted to have an actions string of "GET:Accept". --- .github/workflows/ci-runnerpg.yml | 2 +- appveyor.yml | 2 +- pljava-packaging/src/main/resources/pljava.policy | 5 +++-- .../java/org/postgresql/pljava/management/Commands.java | 8 +++++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index ee4819c9..61d406e7 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -509,7 +509,7 @@ jobs: succeeding &= useTrialPolicy(n1, c2, List.of( "grant codebase \"${org.postgresql.pljava.codesource}\" {", " permission", - " java.net.URLPermission \"http:*\", \"GET\";", + " java.net.URLPermission \"http:*\", \"GET:Accept\";", "};" )); diff --git a/appveyor.yml b/appveyor.yml index 2195e74c..e8c602a8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -394,7 +394,7 @@ test_script: succeeding &= useTrialPolicy(n1, c2, List.of( "grant codebase \"${org.postgresql.pljava.codesource}\" {", " permission", - " java.net.URLPermission \"http:*\", \"GET\";", + " java.net.URLPermission \"http:*\", \"GET:Accept\";", "};" )); diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index c360dcdb..4c7c078f 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -78,8 +78,9 @@ grant codebase "${org.postgresql.pljava.codesource}" { // // There would be nothing wrong with restricting this permission to // a specific directory, if all jar files to be loaded will be found there, - // or replacing it with a URLPermission if they will be hosted on a remote - // server, etc. + // or, if they will be hosted on a remote server, a permission like + // java.net.URLPermission "https://example.com/jars/*", "GET:Accept" + // etc. // permission java.io.FilePermission "<>", "read"; diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 8aa086a7..0cdafa25 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -450,6 +450,12 @@ static void addClassImages(int jarId, String urlString) { URL url = new URL(urlString); URLConnection uc = url.openConnection(); + uc.setRequestProperty("Accept", + "application/java-archive, " + + "application/jar;q=0.9, application/jar-archive;q=0.9, " + + "application/x-java-archive;q=0.9, " + + "application/*;q=0.3, */*;q=0.2" + ); long[] sz = new long[1]; Permission[] least = { uc.getPermission() }; @@ -465,7 +471,7 @@ static void addClassImages(int jarId, String urlString) */ least = new Permission[] { least[0], - new URLPermission(urlString, "GET") + new URLPermission(urlString, "GET:Accept") }; /* From b302ad37fcbaead70da8216527f82f10a11df439 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 21:08:37 -0400 Subject: [PATCH 0965/1087] Avoid crash on exception in String initialization Addresses #416, an unlikely scenario that combined a database set for SQL_ASCII with an unworkable setting in pljava.vmoptions, where the combination caused an UnsupportedCharsetException during initialization in String.c, and elogExceptionMessage tried to use the incompletely-initialized String routines, causing a crash. Effort had been put in String.c to avoid such outcomes, by having initial values of uninitialized and s_two_step_conversion that provide a fallback capability before initialization is complete. However, the initial value of s_two_step_conversion could be changed to its runtime-determined value too early, with some other initialization steps yet to be done. --- pljava-so/src/main/c/type/String.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index 63f77c5f..e380be32 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -423,6 +423,12 @@ static void String_initialize_codec() jclass buffer_class = PgObject_getJavaClass("java/nio/Buffer"); jobject servercs; + /* + * Records what the final state of s_two_step_conversion will be, but the + * static is left at its initial value until all preparations are complete. + */ + bool two_step_when_ready = s_two_step_conversion; + s_server_encoding = GetDatabaseEncoding(); if ( PG_SQL_ASCII == s_server_encoding ) @@ -432,7 +438,7 @@ static void String_initialize_codec() "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;"); jstring sql_ascii = JNI_newStringUTF("X-PGSQL_ASCII"); - s_two_step_conversion = false; + two_step_when_ready = false; servercs = JNI_callStaticObjectMethodLocked(charset_class, forname, sql_ascii); @@ -444,7 +450,7 @@ static void String_initialize_codec() jfieldID scharset_UTF_8 = PgObject_getStaticJavaField(scharset_class, "UTF_8", "Ljava/nio/charset/Charset;"); - s_two_step_conversion = PG_UTF8 != s_server_encoding; + two_step_when_ready = PG_UTF8 != s_server_encoding; servercs = JNI_getStaticObjectField(scharset_class, scharset_UTF_8); } @@ -478,5 +484,6 @@ static void String_initialize_codec() s_the_empty_string = JNI_newGlobalRef( JNI_callObjectMethod(empty, string_intern)); + s_two_step_conversion = two_step_when_ready; uninitialized = false; } From a55ec9093b02f2a6679710d167a406ebc5c9c321 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Jun 2023 20:10:45 -0400 Subject: [PATCH 0966/1087] Pre-PG14, this cast is needed Earlier versions don't supply a text[] || varchar[] operator. --- pljava-packaging/src/main/java/Node.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index dd658f9f..48deda9d 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1122,7 +1122,7 @@ public static Stream appendClasspathIf( " )" + " )" + "FROM" + - " (VALUES (?, ?)) AS p(schema, jar)," + + " (VALUES (?, CAST (? AS pg_catalog.text))) AS p(schema, jar)," + " COALESCE(" + " pg_catalog.regexp_split_to_array(" + " sqlj.get_classpath(schema)," + From 59afb0fea1142a577d295d1aee0af0f12ac07b2d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 13 Jun 2023 18:38:20 -0400 Subject: [PATCH 0967/1087] Update release notes, and versions.md --- src/site/markdown/build/versions.md | 16 ++-- src/site/markdown/releasenotes.md.vm | 109 ++++++++++++++++++++++++--- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index d5d36fb9..a759a20b 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -1,6 +1,6 @@ # Versions of external packages needed to build and use PL/Java -As of mid-2020, the following version constraints are known. +As of mid-2023, the following version constraints are known. ## Java @@ -24,6 +24,11 @@ itself was built with, as long as that later JRE version is used at run time. That also allows PL/Java to take advantage of recent Java implementation advances such as [class data sharing][cds]. +Some builds of Java 20 are affected by a bug, [JDK-8309515][]. PL/Java will +report an error if detects it is affected by that bug, and the solution can be +to use a Java version earlier than 20, or one recent enough to have the bug +fixed. + PL/Java has been successfully used with [Oracle Java][orj] and with [OpenJDK][], which is available with [either the Hotspot or the OpenJ9 JVM][hsj9]. It can also be built and used @@ -38,6 +43,7 @@ the `mvn` command line. [OpenJDK]: https://adoptopenjdk.net/ [hsj9]: https://www.eclipse.org/openj9/oj9_faq.html [GraalVM]: https://www.graalvm.org/ +[JDK-8309515]: https://bugs.openjdk.org/browse/JDK-8309515 ## Maven @@ -56,13 +62,11 @@ versions 4.3.0 or later are recommended in order to avoid a ## PostgreSQL -PL/Java 1.6.0 does not commit to support PostgreSQL earlier than 9.5. -(Support for 9.4 or even 9.3 might be feasible to add if there is a pressing -need.) +The PL/Java 1.6 series does not commit to support PostgreSQL earlier than 9.5. More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. -PL/Java 1.6.0 has been successfully built and run on at least one platform -with PostgreSQL versions from 13 to 9.5, the latest maintenance +PL/Java 1.6.5 has been successfully built and run on at least one platform +with PostgreSQL versions from 15 to 9.5, the latest maintenance release for each. diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index cb10d1dc..8f1e1756 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,6 +10,104 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') +$h2 PL/Java 1.6.5 + +This is the fifth minor update in the PL/Java 1.6 series. It adds support +for PostgreSQL 15 and fixes some bugs, with few other notable changes. Further +information on the changes may be found below. + +$h3 Version compatibility + +PL/Java 1.6.5 can be built against recent PostgreSQL versions including 15, and +older ones back to 9.5, using Java SE 9 or later. The Java version used at +runtime does not have to be the same version used for building. PL/Java itself +can run on any Java version 9 or later. PL/Java functions can be +written for, and use features of, whatever Java version will be loaded at run +time. See [version compatibility][versions] for more detail. + +Some builds of Java 20 are affected by a bug, [JDK-8309515][]. PL/Java will +report an error if detects it is affected by that bug, and the solution can be +to use a Java version earlier than 20, or one recent enough to have the bug +fixed. + +$h3 Changes + +$h4 Changes affecting administration + +$h5 Bugs affecting `install_jar` from http/https URLs fixed + +CI testing now makes sure that http URLs work and the appropriate +`java.net.URLPermission` can be granted in `pljava.policy` where the comments +indicate. + +$h4 Improvements to the annotation-driven SQL generator + +$h5 PL/Java functions can be declared on interfaces as well as classes + +The SQL/JRT specification has always only said 'class', but it could be +debated whether 'class' was intended strictly or inclusively. As there is +no technical obstacle to using static methods declared on an interface, +and PL/Java's runtime already could do so, the SQL generator no longer +disallows `@Function` annotations on them. + +$h5 SQL generator reports compatibility with a more recent Java source version + +Because PL/Java 1.6 retains compatibility for building on Java versions +back to 9, the SQL generator previously reported 9 as the source version +supported. This produced warnings building user code to target a later version +of Java, which were only an issue for sites using a fail-on-warning policy. + +The SQL generator now reports its supported source version as the earlier of: +the Java version being used, or the latest Java version on which it has been +successfully tested. In this release, that is Java 20. + +$h4 Improvements to documentation + +$h5 Use of `--add-modules` to access Java modules not read by default, explained + +By default, PL/Java starts up with a fairly small set of Java modules readable. +The documentation did not explain the use of `--add-modules` in +`pljava.vmoptions` to expand that set when user code will refer to other +modules. That is [now documented][addm]. + +$h3 Enhancement requests addressed + +* [Allow functions from an interface](${ghbug}426) + +$h3 Bugs fixed + +* [Crash on startup with `SQL_ASCII` database and bad `vmoptions`](${ghbug}416) +* [Installed by `LOAD` then packaged as extension broken in recent PostgreSQL updates](${ghbug}434) +* [Java 20 breaks `LexicalsTest.testSeparator`](${ghbug}435) +* [Not found JDK 11 `java.net.http.HttpClient`](${ghbug}419) (documentation added) +* [`SocketPermission` on `install_jar`](${ghbug}425) +* [The timer in `_destroyJavaVM` does not take effect](${ghbug}407) +* PostgreSQL 15 support [410](${ghbug}410), [412](${ghbug}412) +* [Cannot specify java release other than '9' for `maven-compiler-plugin`](${ghbug}403) +* [Fail validation if function declares `TRANSFORM FOR TYPE`](${ghbug}402) +* ["cannot parse AS string" for 1-letter identifiers](${ghbug}438) + +$h3 Credits + +Thanks in release 1.6.5 to Francisco Miguel Biete Banon, Christoph Berg, Frank +Blanning, Stephen Frost, Casey Lai, Krzysztof Nienartowicz, Yuril Rashkovskii, +Tim Van Holder, `aadrian`, `sincatter`, `tayalrun1`. + +[addm]: install/vmoptions.html#Adding_to_the_set_of_readable_modules +[versions]: build/versions.html +[JDK-8309515]: https://bugs.openjdk.org/browse/JDK-8309515 + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + $h2 PL/Java 1.6.4 This is the fourth minor update in the PL/Java 1.6 series. It is a minor @@ -79,17 +177,6 @@ $h3 Bugs fixed [exoneout]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/ReturnComposite.html#method.summary -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 PL/Java 1.6.3 (10 October 2021) This is the third minor update in the PL/Java 1.6 series. It adds support From 01761235f8fec8b3fd94333cca3f60a69b6cce3e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 13 Jun 2023 18:39:58 -0400 Subject: [PATCH 0968/1087] Poke migration-management versions for 1.6.5 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 86ef04d1..67c95912 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -991,6 +991,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_6_5 = REL_1_6_0; static final SchemaVariant REL_1_6_4 = REL_1_6_0; static final SchemaVariant REL_1_6_3 = REL_1_6_0; static final SchemaVariant REL_1_6_2 = REL_1_6_0; From 18147d984041eda4bb7942976031752cc9d1bc95 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 13 Jun 2023 19:11:53 -0400 Subject: [PATCH 0969/1087] Add control file in preparation for next release Now that 1.6.5 is released, the next release should include an extension SQL file allowing upgrade from 1.6.5. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index c85fc478..0df763e7 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -255,6 +255,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Sun, 20 Aug 2023 20:32:46 -0400 Subject: [PATCH 0970/1087] Assume dlfcn.h exists except on Windows postgres/postgres@ca1e855 --- pljava-so/src/main/c/Backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 287652fd..9c60e5c7 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -28,7 +28,7 @@ #include #if PG_VERSION_NUM >= 120000 - #ifdef HAVE_DLOPEN + #if defined(HAVE_DLOPEN) || PG_VERSION_NUM >= 160000 && ! defined(WIN32) #include #endif #define pg_dlopen(f) dlopen((f), RTLD_NOW | RTLD_GLOBAL) From 2511321530cf1ccdb0e4a95cdf35ab8a64c40f78 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 20 Aug 2023 20:41:35 -0400 Subject: [PATCH 0971/1087] Rely on __func__ being supported postgres/postgres@320f92b --- pljava-so/src/main/c/Function.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index a46c0e6d..f975fbe5 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -34,6 +34,10 @@ #include #include +#if PG_VERSION_NUM >= 160000 +#define PG_FUNCNAME_MACRO __func__ +#endif + #ifdef _MSC_VER # define strcasecmp _stricmp # define strncasecmp _strnicmp From e004f0a25e0b8e503c0c2377ade9def969aff48b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 22 Aug 2023 16:24:29 -0400 Subject: [PATCH 0972/1087] Upstream has refactored aclcheck functions postgres/postgres@c727f51 --- pljava-so/src/main/c/Function.c | 2 +- pljava-so/src/main/c/type/AclId.c | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index f975fbe5..3a2bd5b1 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 diff --git a/pljava-so/src/main/c/type/AclId.c b/pljava-so/src/main/c/type/AclId.c index 504b8cf6..81b31538 100644 --- a/pljava-so/src/main/c/type/AclId.c +++ b/pljava-so/src/main/c/type/AclId.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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,6 +22,12 @@ #include "org_postgresql_pljava_internal_AclId.h" #include "pljava/Exception.h" +#if PG_VERSION_NUM >= 160000 +#include +#define pg_namespace_aclcheck(oid,rid,mode) \ + object_aclcheck(NamespaceRelationId, (oid), (rid), (mode)) +#endif + static jclass s_AclId_class; static jmethodID s_AclId_init; static jfieldID s_AclId_m_native; From 5559b8a13b6d91e34136dc59133c1c2042cda4f3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Aug 2023 11:10:50 -0400 Subject: [PATCH 0973/1087] Add Java 17 names for XML implementation props Java 17 adds (and prominently documents in the java.xml module-info javadocs), new, standardized, easy-to-remember names for a dozen or so of the XML implementation-specific properties and features. And the standardized names are ... different from the old ones. More names to feed into setFirstSupported{Feature,Property} when trying to adjust things. The naming headaches are now visible outside of just the implementation module, because there are new standardized names for a Transformer property and feature also, which are used in the XSLT example code in PassXML. So this is a good time to factor a setFirstSupported(...) generic function out of the various copies buried in SQLXMLImpl, and expose it in the Adjusting.XML API so client code can use it too, and use it that way in that example. The Adjusting.XML API was added in some haste, and used to say "the adjusting methods are best-effort and do not provide an indication of whether the requested adjustment was made". In fact it was pushed with some debug code doing exception stacktraces to standard error, which may have been an annoyance at any site that was using the feature heavily. Take this opportunity to do something more systematic, and add a lax(boolean) method allowing it to be tailored. Stacktraces will still be logged if no tailoring is done, but may be more concise, as all of the exceptions that might be encountered in a chain of adjustments will be chained together with addSuppressed(), and common stacktrace elements should be elided when those are logged. In passing, copyedit some Adjusting.XML javadoc to use styleguide-favored third-person rather than second-person phrasing. --- .../java/org/postgresql/pljava/Adjusting.java | 200 +++++++++++-- .../pljava/example/annotation/PassXML.java | 58 ++-- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 271 +++++++++++------- 3 files changed, 383 insertions(+), 146 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java b/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java index 9a2d379b..b8e32256 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2023 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 @@ -14,6 +14,9 @@ import java.io.Reader; import java.sql.SQLException; import java.sql.SQLXML; +import java.util.List; +import static java.util.Objects.requireNonNull; +import java.util.function.Consumer; import javax.xml.stream.XMLInputFactory; // for javadoc import javax.xml.stream.XMLResolver; // for javadoc import javax.xml.stream.XMLStreamReader; @@ -126,16 +129,139 @@ public static final class XML { private XML() { } // no instances + /** + * Attempts a given action (typically to set something) using a given + * value, trying one or more supplied keys in order until the action + * succeeds with no exception. + *

    + * This logic is common to the + * {@link Parsing#setFirstSupportedFeature setFirstSupportedFeature} + * and + * {@link Parsing#setFirstSupportedProperty setFirstSupportedProperty} + * methods, and is exposed here because it may be useful for other + * tasks in Java's XML APIs, such as configuring {@code Transformer}s. + *

    + * If any attempt succeeds, null is returned. If no attempt + * succeeds, the first exception caught is returned, with any + * exceptions from the subsequent attempts retrievable from it with + * {@link Exception#getSuppressed getSuppressed}. The return is + * immediate, without any remaining names being tried, if an exception + * is caught that is not assignable to a class in the + * expected list. Such an exception will be passed to the + * onUnexpected handler if that is non-null; otherwise, + * it will be returned (or added to the suppressed list of the + * exception to be returned) just as expected exceptions are. + * @param setter typically a method reference for a method that + * takes a string key and some value. + * @param value the value to pass to the setter + * @param expected a list of exception classes that can be foreseen + * to indicate that a key was not recognized, and the operation + * should be retried with the next possible key. + * @param onUnexpected invoked, if non-null, on an {@code Exception} + * that is caught and matches nothing in the expected list, instead + * of returning it. If this parameter is null, such an exception is + * returned (or added to the suppressed list of the exception to be + * returned), just as for expected exceptions, but the return is + * immediate, without trying remaining names, if any. + * @param names one or more String keys to be tried in order until + * the action succeeds. + * @return null if any attempt succeeded, otherwise an exception, + * which may have further exceptions in its suppressed list. + */ + public static Exception setFirstSupported( + SetMethod setter, V value, + List> expected, + Consumer onUnexpected, String... names) + { + requireNonNull(expected); + Exception caught = null; + for ( String name : names ) + { + try + { + setter.set(name, value); + return null; + } + catch ( Exception e ) + { + boolean benign = + expected.stream().anyMatch(c -> c.isInstance(e)); + + if ( benign || null == onUnexpected ) + { + if ( null == caught ) + caught = e; + else + caught.addSuppressed(e); + } + else + onUnexpected.accept(e); + + if ( ! benign ) + break; + } + } + return caught; + } + + /** + * A functional interface fitting various {@code setFeature} or + * {@code setProperty} methods in Java XML APIs. + *

    + * The XML APIs have a number of methods on various interfaces that can + * be used to set some property or feature, and can generally be + * assigned to this functional interface by bound method reference, and + * used with {@link #setFirstSupported setFirstSupported}. + */ + @FunctionalInterface + public interface SetMethod + { + void set(String key, T value) throws Exception; + } + /** * Interface with methods to adjust the restrictions on XML parsing * that are commonly considered when XML content might be from untrusted * sources. *

    - * The adjusting methods are best-effort and do not provide an - * indication of whether the requested adjustment was made. Not all of + * The adjusting methods are best-effort; not all of * the adjustments are available for all flavors of {@code Source} or * {@code Result} or for all parser implementations or versions the Java - * runtime may supply. + * runtime may supply. Cases where a requested adjustment has not been + * made are handled as follows: + *

    + * Any sequence of adjustment calls will ultimately be followed by a + * {@code get}. During the sequence of adjustments, exceptions caught + * are added to a signaling list or to a quiet list, where "added to" + * means that if either list has a first exception, any caught later are + * attached to that exception with + * {@link Exception#addSuppressed addSuppressed}. + *

    + * For each adjustment (and depending on the type of underlying + * {@code Source} or {@code Result}), one or more exception types will + * be 'expected' as indications that an identifying key or value for + * that adjustment was not recognized. This implementation may continue + * trying to apply the adjustment, using other keys that have at times + * been used to identify it. Expected exceptions caught during these + * attempts form a temporary list (a first exception and those attached + * to it by {@code addSuppressed}). Once any such attempt succeeds, the + * adjustment is considered made, and any temporary expected exceptions + * list from the adjustment is discarded. If no attempt succeeded, the + * temporary list is retained, by adding its head exception to the quiet + * list. + *

    + * Any exceptions caught that are not instances of any of the 'expected' + * types are added to the signaling list. + *

    + * When {@code get} is called, the head exception on the signaling list, + * if any, is thrown. Otherwise, the head exception on the quiet list, + * if any, is logged at [@code WARNING} level. + *

    + * During a chain of adjustments, {@link #lax lax()} can be called to + * tailor the handling of the quiet list. A {@code lax()} call applies + * to whatever exceptions have been added to the quiet list up to that + * point. To discard them, call {@code lax(true)}; to move them to the + * signaling list, call {@code lax(false)}. */ public interface Parsing> { @@ -173,14 +299,14 @@ public interface Parsing> /** * For a feature that may have been identified by more than one URI - * in different parsers or versions, try passing the supplied + * in different parsers or versions, tries passing the supplied * value with each URI from names in order until * one is not rejected by the underlying parser. */ T setFirstSupportedFeature(boolean value, String... names); /** - * Make a best effort to apply the recommended, restrictive + * Makes a best effort to apply the recommended, restrictive * defaults from the OWASP cheat sheet, to the extent they are * supported by the underlying parser, runtime, and version. *

    @@ -196,7 +322,7 @@ public interface Parsing> /** * For a parser property (in DOM parlance, attribute) that may have * been identified by more than one URI in different parsers or - * versions, try passing the supplied value with each URI + * versions, tries passing the supplied value with each URI * from names in order until one is not rejected by the * underlying parser. *

    @@ -278,7 +404,7 @@ public interface Parsing> T accessExternalSchema(String protocols); /** - * Set an {@link EntityResolver} of the type used by SAX and DOM + * Sets an {@link EntityResolver} of the type used by SAX and DOM * (optional operation). *

    * This method only succeeds for a {@code SAXSource} or @@ -297,7 +423,7 @@ public interface Parsing> T entityResolver(EntityResolver resolver); /** - * Set a {@link Schema} to be applied during SAX or DOM parsing + * Sets a {@link Schema} to be applied during SAX or DOM parsing *(optional operation). *

    * This method only succeeds for a {@code SAXSource} or @@ -316,6 +442,31 @@ public interface Parsing> * already. */ T schema(Schema schema); + + /** + * Tailors the treatment of 'quiet' exceptions during a chain of + * best-effort adjustments. + *

    + * See {@link Parsing the class description} for an explanation of + * the signaling and quiet lists. + *

    + * This method applies to whatever exceptions may have been added to + * the quiet list by best-effort adjustments made up to that point. + * They can be moved to the signaling list with {@code lax(false)}, + * or simply discarded with {@code lax(true)}. In either case, the + * quiet list is left empty when {@code lax} returns. + *

    + * At the time a {@code get} method is later called, any exception + * at the head of the signaling list will be thrown (possibly + * wrapped in an exception permitted by {@code get}'s {@code throws} + * clause), with any later exceptions on that list retrievable from + * the head exception with + * {@link Exception#getSuppressed getSuppressed}. Otherwise, any + * exception at the head of the quiet list (again with any later + * ones attached as its suppressed list) will be logged at + * {@code WARNING} level. + */ + T lax(boolean discard); } /** @@ -347,12 +498,17 @@ public interface Source extends Parsing>, javax.xml.transform.Source { /** - * Return an object of the expected {@code Source} subtype + * Returns an object of the expected {@code Source} subtype * reflecting any adjustments made with the other methods. + *

    + * Refer to {@link Parsing the {@code Parsing} class description} + * and the {@link Parsing#lax lax()} method for how any exceptions + * caught while applying best-effort adjustments are handled. * @return an implementing object of the expected Source subtype * @throws SQLException for any reason that {@code getSource} might * have thrown when supplying the corresponding non-Adjusting - * subtype of Source. + * subtype of Source, or for reasons saved while applying + * adjustments. */ T get() throws SQLException; } @@ -392,12 +548,16 @@ public interface Result extends Parsing>, javax.xml.transform.Result { /** - * Return an object of the expected {@code Result} subtype + * Returns an object of the expected {@code Result} subtype * reflecting any adjustments made with the other methods. + * Refer to {@link Parsing the {@code Parsing} class description} + * and the {@link Parsing#lax lax()} method for how any exceptions + * caught while applying best-effort adjustments are handled. * @return an implementing object of the expected Result subtype * @throws SQLException for any reason that {@code getResult} might * have thrown when supplying the corresponding non-Adjusting - * subtype of Result. + * subtype of Result, or for reasons saved while applying + * adjustments. */ T get() throws SQLException; } @@ -428,7 +588,7 @@ public interface Result public interface SourceResult extends Result { /** - * Supply the {@code Source} instance that is the source of the + * Supplies the {@code Source} instance that is the source of the * content. *

    * This method must be called before any of the inherited adjustment @@ -484,7 +644,8 @@ SourceResult set(javax.xml.transform.stax.StAXSource source) throws SQLException; /** - * Provide the content to be copied in the form of a {@code String}. + * Provides the content to be copied in the form of a + * {@code String}. *

    * An exception from the pattern of {@code Source}-typed arguments, * this method simplifies retrofitting adjustments into code that @@ -507,11 +668,14 @@ SourceResult set(javax.xml.transform.dom.DOMSource source) throws SQLException; /** - * Return the result {@code SQLXML} instance ready for handing off + * Returns the result {@code SQLXML} instance ready for handing off * to PostgreSQL. *

    - * This method must be called after any of the inherited adjustment - * methods. + * The handling/logging of exceptions normally handled in a + * {@code get} method happens here for a {@code SourceResult}. + *

    + * Any necessary calls of the inherited adjustment methods must be + * made before this method is called. */ SQLXML getSQLXML() throws SQLException; } 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 e735376c..3c060575 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-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -36,6 +36,7 @@ import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.HashMap; @@ -65,6 +66,7 @@ import javax.xml.validation.SchemaFactory; import org.postgresql.pljava.Adjusting; +import static org.postgresql.pljava.Adjusting.XML.setFirstSupported; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; @@ -518,33 +520,51 @@ private static void prepareXMLTransform(String name, SQLXML source, int how, builtin ? TransformerFactory.newDefaultInstance() : TransformerFactory.newInstance(); - String exf = - "http://www.oracle.com/xml/jaxp/properties/enableExtensionFunctions"; - String ecl = "jdk.xml.transform.extensionClassLoader"; + + String legacy_pfx = "http://www.oracle.com/xml/jaxp/properties/"; + String java17_pfx = "jdk.xml."; + String exf_sfx = "enableExtensionFunctions"; + + String ecl_legacy = "jdk.xml.transform.extensionClassLoader"; + String ecl_java17 = "jdk.xml.extensionClassLoader"; + Source src = sxToSource(source, how, adjust); + try { - try - { - tf.setFeature(exf, enableExtensionFunctions); - } - catch ( TransformerConfigurationException e ) + Exception e; + + e = setFirstSupported(tf::setFeature, enableExtensionFunctions, + List.of(TransformerConfigurationException.class), null, + java17_pfx + exf_sfx, legacy_pfx + exf_sfx); + + if ( null != e ) { - logMessage("WARNING", - "non-builtin transformer: ignoring " + e.getMessage()); + if ( builtin ) + throw new SQLException( + "Configuring XML transformation: " + e.getMessage(), e); + else + logMessage("WARNING", + "non-builtin transformer: ignoring " + e.getMessage()); } if ( withJava ) { - try - { - tf.setAttribute(ecl, - Thread.currentThread().getContextClassLoader()); - } - catch ( IllegalArgumentException e ) + e = setFirstSupported(tf::setAttribute, + Thread.currentThread().getContextClassLoader(), + List.of(IllegalArgumentException.class), null, + ecl_java17, ecl_legacy); + + if ( null != e ) { - logMessage("WARNING", - "non-builtin transformer: ignoring " + e.getMessage()); + if ( builtin ) + throw new SQLException( + "Configuring XML transformation: " + + e.getMessage(), e); + else + logMessage("WARNING", + "non-builtin transformer: ignoring " + + e.getMessage()); } } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 1b3bb2a0..95655ccd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -157,9 +157,13 @@ /* ... for Adjusting API for Source / Result */ import java.io.StringReader; +import java.util.List; +import static javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD; +import static javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA; import javax.xml.parsers.ParserConfigurationException; import javax.xml.validation.Schema; import org.postgresql.pljava.Adjusting; +import static org.postgresql.pljava.Adjusting.XML.setFirstSupported; import org.xml.sax.EntityResolver; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; @@ -4144,14 +4148,72 @@ Writable finish() throws IOException, SQLException AdjustingJAXPParser> implements Adjusting.XML.Parsing { + static final Logger s_logger = + Logger.getLogger("org.postgresql.pljava.jdbc"); + + private static final String JDK17 = "jdk.xml."; private static final String LIMIT = - "http://www.oracle.com/xml/jaxp/properties/"; + "http://www.oracle.com/xml/jaxp/properties/"; // "legacy" since 17 - /* - * Can get these from javax.xml.XMLConstants once assuming Java >= 7. + private Exception m_signaling; + private Exception m_quiet; + + protected void addSignaling(Exception e) + { + if ( null == e ) + return; + if ( null == m_signaling ) + m_signaling = e; + else + m_signaling.addSuppressed(e); + } + + protected void addQuiet(Exception e) + { + if ( null == e ) + return; + if ( null == m_quiet ) + m_quiet = e; + else + m_quiet.addSuppressed(e); + } + + protected boolean anySignaling() + { + return null != m_signaling; + } + + /** + * Returns whatever is on the signaling list, while logging (at + * {@code WARNING} level) whatever is on the quiet list. + *

    + * Both lists are left cleared. + * @return the head exception on the signaling list, or null if none */ - private static final String ACCESS = - "http://javax.xml.XMLConstants/property/accessExternal"; + protected Exception exceptions() + { + Exception e = m_quiet; + m_quiet = null; + if ( null != e ) + s_logger.log(WARNING, + "some XML processing limits were not successfully adjusted", + e); + e = m_signaling; + m_signaling = null; + return e; + } + + @Override + public T lax(boolean discard) + { + if ( null != m_quiet ) + { + if ( ! discard ) + addSignaling(m_quiet); + m_quiet = null; + } + return (T)this; + } @Override public T defaults() @@ -4165,6 +4227,7 @@ public T defaults() public T elementAttributeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "elementAttributeLimit", LIMIT + "elementAttributeLimit"); } @@ -4172,6 +4235,7 @@ public T elementAttributeLimit(int limit) public T entityExpansionLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "entityExpansionLimit", LIMIT + "entityExpansionLimit"); } @@ -4179,6 +4243,7 @@ public T entityExpansionLimit(int limit) public T entityReplacementLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "entityReplacementLimit", LIMIT + "entityReplacementLimit"); } @@ -4186,6 +4251,7 @@ public T entityReplacementLimit(int limit) public T maxElementDepth(int depth) { return setFirstSupportedProperty(depth, + JDK17 + "maxElementDepth", LIMIT + "maxElementDepth"); } @@ -4193,6 +4259,7 @@ public T maxElementDepth(int depth) public T maxGeneralEntitySizeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "maxGeneralEntitySizeLimit", LIMIT + "maxGeneralEntitySizeLimit"); } @@ -4200,6 +4267,7 @@ public T maxGeneralEntitySizeLimit(int limit) public T maxParameterEntitySizeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "maxParameterEntitySizeLimit", LIMIT + "maxParameterEntitySizeLimit"); } @@ -4207,6 +4275,7 @@ public T maxParameterEntitySizeLimit(int limit) public T maxXMLNameLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "maxXMLNameLimit", LIMIT + "maxXMLNameLimit"); } @@ -4214,19 +4283,20 @@ public T maxXMLNameLimit(int limit) public T totalEntitySizeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "totalEntitySizeLimit", LIMIT + "totalEntitySizeLimit"); } @Override public T accessExternalDTD(String protocols) { - return setFirstSupportedProperty(protocols, ACCESS + "DTD"); + return setFirstSupportedProperty(protocols, ACCESS_EXTERNAL_DTD); } @Override public T accessExternalSchema(String protocols) { - return setFirstSupportedProperty(protocols, ACCESS + "Schema"); + return setFirstSupportedProperty(protocols, ACCESS_EXTERNAL_SCHEMA); } @Override @@ -4314,7 +4384,6 @@ static class SAXDOMErrorHandler implements ErrorHandler */ static final Pattern s_wrapelement = Pattern.compile( "^cvc-elt\\.1(?:\\.a)?+:.*pljava-content-wrap"); - final Logger m_logger = Logger.getLogger("org.postgresql.pljava.jdbc"); private int m_wrapCount; static SAXDOMErrorHandler instance(boolean wrapped) @@ -4364,7 +4433,8 @@ public void fatalError(SAXParseException exception) throws SAXException @Override public void warning(SAXParseException exception) throws SAXException { - m_logger.log(WARNING, exception.getMessage(), exception); + AdjustingJAXPParser.s_logger + .log(WARNING, exception.getMessage(), exception); } } @@ -4526,6 +4596,19 @@ public AdjustingSourceResult set(String source) return set(new StreamSource(new StringReader(source))); } + @Override + public AdjustingSourceResult get() throws SQLException + { + return this; // for this class, get is a noop + } + + @Override + public AdjustingSourceResult lax(boolean discard) + { + theAdjustable().lax(discard); + return this; + } + @Override public SQLXML getSQLXML() throws SQLException { @@ -4535,6 +4618,10 @@ public SQLXML getSQLXML() throws SQLException if ( null == m_copier ) throw new IllegalStateException( "AdjustingSourceResult getSQLXML called before set"); + + // Exception handling/logging for adjustments will happen in + // theAdjustable().get(), during finish() here. + Writable result = null; try { @@ -4578,12 +4665,6 @@ private Adjusting.XML.Source theAdjustable() return m_copier.getAdjustable(); } - @Override - public AdjustingSourceResult get() throws SQLException - { - return this; // for this class, get is a noop - } - @Override public AdjustingSourceResult allowDTD(boolean v) { @@ -4748,8 +4829,11 @@ public StreamResult get() throws SQLException throw new IllegalStateException( "AdjustingStreamResult get() called more than once"); + // Exception handling/logging for theVerifierSource happens here XMLReader xr = theVerifierSource().get().getXMLReader(); + OutputStream os; + try { m_vwo.setVerifier(new Verifier(xr)); @@ -4759,18 +4843,29 @@ public StreamResult get() throws SQLException { throw normalizedException(e); } + StreamResult sr; + if ( m_preferWriter ) sr = new StreamResult( new OutputStreamWriter(os, m_serverCS.newEncoder())); else sr = new StreamResult(os); + m_vwo = null; m_verifierSource = null; m_serverCS = null; + return sr; } + @Override + public AdjustingStreamResult lax(boolean discard) + { + theVerifierSource().lax(discard); + return this; + } + @Override public AdjustingStreamResult allowDTD(boolean v) { @@ -4860,7 +4955,6 @@ static class AdjustingSAXSource private XMLReader m_xr; private InputSource m_is; private boolean m_wrapped; - private SAXException m_except; private boolean m_hasCalledDefaults; static class Dummy extends AdjustingSAXSource @@ -4940,7 +5034,7 @@ private SAXParserFactory theFactory() private XMLReader theReader() { - if ( null != m_except ) + if ( anySignaling() ) return null; if ( null != m_spf ) @@ -4949,16 +5043,12 @@ private XMLReader theReader() { m_xr = m_spf.newSAXParser().getXMLReader(); } - catch ( SAXException e ) - { - m_except = e; - return null; - } - catch ( ParserConfigurationException e ) + catch ( SAXException | ParserConfigurationException e ) { - m_except = new SAXException(e.getMessage(), e); + addSignaling(e); return null; } + m_spf = null; if ( m_wrapped ) m_xr = new SAXUnwrapFilter(m_xr); @@ -4991,9 +5081,11 @@ public SAXSource get() throws SQLException throw new IllegalStateException( "AdjustingSAXSource get() called more than once"); - XMLReader xr; - if ( null != m_except || null == (xr = theReader()) ) - throw normalizedException(m_except); + XMLReader xr = theReader(); + + Exception e = exceptions(); + if ( null != e ) + throw normalizedException(e); SAXSource ss = new SAXSource(xr, m_is); m_xr = null; @@ -5031,18 +5123,11 @@ public AdjustingSAXSource setFirstSupportedFeature( if ( null == r ) // pending exception, nothing to be done return this; - for ( String name : names ) - { - try - { - r.setFeature(name, value); - break; - } - catch ( SAXNotRecognizedException | SAXNotSupportedException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(r::setFeature, value, + List.of(SAXNotRecognizedException.class, + SAXNotSupportedException.class), + this::addSignaling, names)); + return this; } @@ -5054,22 +5139,11 @@ public AdjustingSAXSource setFirstSupportedProperty( if ( null == r ) // pending exception, nothing to be done return this; - for ( String name : names ) - { - try - { - r.setProperty(name, value); - break; - } - catch ( SAXNotRecognizedException e ) - { - e.printStackTrace(); // XXX - } - catch ( SAXNotSupportedException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(r::setProperty, value, + List.of(SAXNotRecognizedException.class, + SAXNotSupportedException.class), + this::addSignaling, names)); + return this; } @@ -5227,6 +5301,8 @@ public StAXSource get() throws SQLException if ( null == m_xif ) throw new IllegalStateException( "AdjustingStAXSource get() called more than once"); + + StAXSource ss = null; try { XMLStreamReader xsr = m_xif.createXMLStreamReader( @@ -5234,12 +5310,18 @@ public StAXSource get() throws SQLException if ( m_wrapped ) xsr = new StAXUnwrapFilter(xsr); m_xif = null; // too late for any more adjustments - return new StAXSource(xsr); + ss = new StAXSource(xsr); } catch ( Exception e ) { - throw normalizedException(e); + addSignaling(e); } + + Exception e = exceptions(); + if ( null != e ) + throw normalizedException(e); + + return ss; } @Override @@ -5286,18 +5368,9 @@ public AdjustingStAXSource setFirstSupportedFeature( boolean value, String... names) { XMLInputFactory xif = theFactory(); - for ( String name : names ) - { - try - { - xif.setProperty(name, value); - break; - } - catch ( IllegalArgumentException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(xif::setProperty, value, + List.of(IllegalArgumentException.class), + this::addSignaling, names)); return this; } @@ -5306,18 +5379,9 @@ public AdjustingStAXSource setFirstSupportedProperty( Object value, String... names) { XMLInputFactory xif = theFactory(); - for ( String name : names ) - { - try - { - xif.setProperty(name, value); - break; - } - catch ( IllegalArgumentException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(xif::setProperty, value, + List.of(IllegalArgumentException.class), + this::addSignaling, names)); return this; } } @@ -5367,23 +5431,30 @@ public DOMSource get() throws SQLException if ( null == m_dbf ) throw new IllegalStateException( "AdjustingDOMSource get() called more than once"); + + DOMSource ds = null; try { DocumentBuilder db = m_dbf.newDocumentBuilder(); db.setErrorHandler(SAXDOMErrorHandler.instance(m_wrapped)); if ( null != m_resolver ) db.setEntityResolver(m_resolver); - DOMSource ds = new DOMSource(db.parse(m_is)); + ds = new DOMSource(db.parse(m_is)); if ( m_wrapped ) domUnwrap(ds); m_dbf = null; m_is = null; - return ds; } catch ( Exception e ) { - throw normalizedException(e); + addSignaling(e); } + + Exception e = exceptions(); + if ( null != e ) + throw normalizedException(e); + + return ds; } @Override @@ -5405,18 +5476,9 @@ public AdjustingDOMSource setFirstSupportedFeature( boolean value, String... names) { DocumentBuilderFactory dbf = theFactory(); - for ( String name : names ) - { - try - { - dbf.setFeature(name, value); - break; - } - catch ( ParserConfigurationException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(dbf::setFeature, value, + List.of(ParserConfigurationException.class), + this::addSignaling, names)); return this; } @@ -5425,18 +5487,9 @@ public AdjustingDOMSource setFirstSupportedProperty( Object value, String... names) { DocumentBuilderFactory dbf = theFactory(); - for ( String name : names ) - { - try - { - dbf.setAttribute(name, value); - break; - } - catch ( IllegalArgumentException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(dbf::setAttribute, value, + List.of(IllegalArgumentException.class), + this::addSignaling, names)); return this; } From 17b7d7cc70a521b490ec09498d68567a9b282761 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Aug 2023 11:53:56 -0400 Subject: [PATCH 0974/1087] PG compatibility horizon to 9.5 PostgreSQL 9.5 has been advertised as the oldest supported version for PL/Java 1.6.x since 1.6.0. Declutter by removing conditional code lingering from support of older versions. In passing, add a couple SPI constants that showed up later while my back was turned. --- .../example/annotation/ConditionalDDR.java | 49 +-- .../example/annotation/Enumeration.java | 22 +- .../pljava/example/annotation/JDBC42_21.java | 9 +- .../pljava/example/annotation/PGF1010962.java | 9 +- .../pljava/example/annotation/PassXML.java | 24 +- .../pljava/example/annotation/PreJSR310.java | 7 +- .../annotation/RecordParameterDefaults.java | 7 +- .../example/annotation/SetOfRecordTest.java | 8 +- .../pljava/example/annotation/Triggers.java | 8 +- .../example/annotation/TypeRoundTripper.java | 12 +- .../annotation/UnicodeRoundTripTest.java | 9 +- .../example/annotation/VarlenaUDTTest.java | 12 +- .../example/annotation/XMLRenderedTypes.java | 13 +- pljava-so/src/main/c/Backend.c | 354 +++++++----------- pljava-so/src/main/c/DualState.c | 6 +- pljava-so/src/main/c/Exception.c | 10 +- pljava-so/src/main/c/ExecutionPlan.c | 10 +- pljava-so/src/main/c/Function.c | 11 +- pljava-so/src/main/c/InstallHelper.c | 129 +------ pljava-so/src/main/c/SPI.c | 11 +- pljava-so/src/main/c/Session.c | 26 +- pljava-so/src/main/c/TypeOid.c | 6 +- pljava-so/src/main/c/VarlenaWrapper.c | 116 +----- pljava-so/src/main/c/XactListener.c | 4 +- pljava-so/src/main/c/type/AclId.c | 6 +- pljava-so/src/main/c/type/Array.c | 12 +- pljava-so/src/main/c/type/Oid.c | 18 +- pljava-so/src/main/c/type/SQLXMLImpl.c | 18 +- pljava-so/src/main/c/type/String.c | 4 - pljava-so/src/main/c/type/Timestamp.c | 18 +- pljava-so/src/main/c/type/Type.c | 36 +- pljava-so/src/main/c/type/UDT.c | 11 +- pljava-so/src/main/c/type/byte_array.c | 22 +- pljava-so/src/main/include/pljava/Backend.h | 8 +- pljava-so/src/main/include/pljava/Exception.h | 10 +- pljava-so/src/main/include/pljava/pljava.h | 38 +- .../org/postgresql/pljava/internal/SPI.java | 5 +- src/site/markdown/build/versions.md | 2 +- 38 files changed, 255 insertions(+), 825 deletions(-) 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 0140f437..253a0308 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -64,7 +64,7 @@ * local, so it is reverted when the transaction completes. *

    * In addition to the goodness-of-life examples, this file also generates - * several statements setting PostgreSQL-version-based implementor tags that + * one or more statements setting PostgreSQL-version-based implementor tags that * are relied on by various other examples in this directory. */ @SQLAction(provides={"LifeIsGood","LifeIsNotGood"}, install= @@ -78,51 +78,12 @@ ) @SQLAction(implementor="LifeIsGood", install= - "SELECT javatest.logmessage('INFO', 'Looking good!')" + "SELECT javatest.logmessage('INFO', 'ConditionlDDR looking good!')" ) @SQLAction(implementor="LifeIsNotGood", install= - "SELECT javatest.logmessage('WARNING', 'This should not be executed')" -) - -@SQLAction(provides="postgresql_ge_80300", install= - "SELECT CASE WHEN" + - " 80300 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_80300,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_80400", install= - "SELECT CASE WHEN" + - " 80400 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_80400,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_90000", install= - "SELECT CASE WHEN" + - " 90000 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90000,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_90100", install= - "SELECT CASE WHEN" + - " 90100 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90100,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_90300", install= - "SELECT CASE WHEN" + - " 90300 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90300,' || " + - " current_setting('pljava.implementors'), true) " + - "END" + "SELECT javatest.logmessage('WARNING', " + + " 'ConditionalDDR: This should not be executed')" ) @SQLAction(provides="postgresql_ge_100000", install= diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java index 2fcee7df..359bb287 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -21,16 +21,12 @@ /** * Confirms the mapping of PG enum and Java String, and arrays of each, as * parameter and return types. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. PostgreSQL before 8.3 - * did not have enum types. */ -@SQLAction(provides="mood type", implementor="postgresql_ge_80300", +@SQLAction(provides="mood type", install="CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", remove="DROP TYPE mood" ) -@SQLAction(implementor="postgresql_ge_80300", +@SQLAction( requires={"textToMood", "moodToText", "textsToMoods", "moodsToTexts"}, install={ "SELECT textToMood('happy')", @@ -41,26 +37,22 @@ ) public class Enumeration { - @Function(requires="mood type", provides="textToMood", type="mood", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="textToMood", type="mood") public static String textToMood(String s) { return s; } - @Function(requires="mood type", provides="moodToText", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="moodToText") public static String moodToText(@SQLType("mood")String s) { return s; } - @Function(requires="mood type", provides="textsToMoods", type="mood", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="textsToMoods", type="mood") public static Iterator textsToMoods(String[] ss) { return Arrays.asList(ss).iterator(); } - @Function(requires="mood type", provides="moodsToTexts", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="moodsToTexts") public static Iterator moodsToTexts(@SQLType("mood[]")String[] ss) { return Arrays.asList(ss).iterator(); 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 1b0d35e2..ff2c2311 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-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -14,20 +14,15 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.example.annotation.ConditionalDDR; // for javadoc - /** * Exercise new mappings between date/time types and java.time classes * (JDBC 4.2 change 21). *

    * Defines a method {@link #javaSpecificationGE javaSpecificationGE} that may be * of use for other examples. - *

    - * Relies on PostgreSQL-version-specific implementor tags set up in the - * {@link ConditionalDDR} example. */ @SQLAction( - implementor="postgresql_ge_90300",requires="TypeRoundTripper.roundTrip", + requires="TypeRoundTripper.roundTrip", install={ " SELECT" + " CASE WHEN every(orig = roundtripped)" + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java index 2b06f481..747f2ef6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java @@ -10,12 +10,8 @@ /** * A gnarly test of TupleDesc reference management, crafted by Johann Oskarsson * for bug report 1010962 on pgFoundry. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. Before PostgreSQL 8.4, - * there is no array of {@code RECORD}, which this test requires. */ -@SQLAction(requires="1010962 func", implementor="postgresql_ge_80400", +@SQLAction(requires="1010962 func", install={ "CREATE TYPE javatest.B1010962 AS ( b1_val float8, b2_val int)", @@ -51,8 +47,7 @@ public class PGF1010962 * @param receiver Looks polymorphic, but expects an array of A1010962 * @return 0 */ - @Function(schema="javatest", provides="1010962 func", - implementor="postgresql_ge_80400") + @Function(schema="javatest", provides="1010962 func") public static int complexParam( ResultSet receiver[] ) throws SQLException { 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 e735376c..d7d8c2a9 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-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -112,16 +112,7 @@ "END" ) -@SQLAction(implementor="postgresql_ge_80400", - provides="postgresql_xml_ge84", - install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml_ge84,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(implementor="postgresql_xml_ge84", requires="echoXMLParameter", +@SQLAction(implementor="postgresql_xml", requires="echoXMLParameter", install= "WITH" + " s(how) AS (SELECT generate_series(1, 7))," + @@ -146,7 +137,7 @@ " r" ) -@SQLAction(implementor="postgresql_xml_ge84", requires="proxiedXMLEcho", +@SQLAction(implementor="postgresql_xml", requires="proxiedXMLEcho", install= "WITH" + " s(how) AS (SELECT unnest('{1,2,4,5,6,7}'::int[]))," + @@ -170,7 +161,7 @@ " r" ) -@SQLAction(implementor="postgresql_xml_ge84", requires="lowLevelXMLEcho", +@SQLAction(implementor="postgresql_xml", requires="lowLevelXMLEcho", install={ "SELECT" + " preparexmlschema('schematest', $$" + @@ -702,7 +693,7 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) * still be exercised by calling this method, explicitly passing * {@code adjust => NULL}. */ - @Function(schema="javatest", implementor="postgresql_xml_ge84", + @Function(schema="javatest", implementor="postgresql_xml", provides="lowLevelXMLEcho") public static SQLXML lowLevelXMLEcho( SQLXML sx, int how, @SQLType(defaultValue={}) ResultSet adjust) @@ -1046,12 +1037,9 @@ public static SQLXML mockedXMLEcho(String chars) /** * Text-typed variant of lowLevelXMLEcho (does not require XML type). - *

    - * It does declare a parameter default, limiting it to PostgreSQL 8.4 or - * later. */ @Function(schema="javatest", name="lowLevelXMLEcho", - type="text", implementor="postgresql_ge_80400") + type="text") public static SQLXML lowLevelXMLEcho_(@SQLType("text") SQLXML sx, int how, @SQLType(defaultValue={}) ResultSet adjust) throws SQLException diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java index e430e25d..726d46a5 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -29,9 +29,6 @@ * Some tests of pre-JSR 310 date/time/timestamp conversions. *

    * For now, just {@code java.sql.Date}, thanks to issue #199. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ @SQLAction(provides="language java_tzset", install={ "SELECT sqlj.alias_java_language('java_tzset', true)" @@ -39,7 +36,7 @@ "DROP LANGUAGE java_tzset" }) -@SQLAction(implementor="postgresql_ge_90300", // needs LATERAL +@SQLAction( requires="issue199", install={ "SELECT javatest.issue199()" }) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index 09d3dbbe..34e1aeb7 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -28,9 +28,6 @@ * function. *

    * Also tests the proper DDR generation of defaults for such parameters. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ @SQLAction( provides = "paramtypeinfo type", // created in Triggers.java @@ -64,7 +61,6 @@ public class RecordParameterDefaults implements ResultSetProvider @Function( requires = "paramtypeinfo type", schema = "javatest", - implementor = "postgresql_ge_80400", // supports function param DEFAULTs type = "javatest.paramtypeinfo" ) public static ResultSetProvider paramDefaultsRecord( @@ -87,7 +83,6 @@ public static ResultSetProvider paramDefaultsRecord( */ @Function( requires = "foobar tables", // created in Triggers.java - implementor = "postgresql_ge_80400", // supports function param DEFAULTs schema = "javatest" ) public static String paramDefaultsNamedRow( diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java index 13fce44d..49abf138 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -26,12 +26,8 @@ * Example implementing the {@code ResultSetHandle} interface, to return * the {@link ResultSet} from any SQL {@code SELECT} query passed as a string * to the {@link #executeSelect executeSelect} function. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. Before PostgreSQL 8.4, - * there was no {@code =} or {@code DISTINCT FROM} operator between row types. */ -@SQLAction(requires="selecttorecords fn", implementor="postgresql_ge_80400", +@SQLAction(requires="selecttorecords fn", install= " SELECT " + " CASE WHEN r IS DISTINCT FROM ROW('Foo'::varchar, 1::integer, 1.5::float, " + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index 804ef9d8..bfdbf8c0 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -38,8 +38,8 @@ * also create a function and trigger that uses transition tables. *

    * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. Constraint triggers - * appear in PG 9.1, transition tables in PG 10. + * version, set up in the {@link ConditionalDDR} example. Transition tables + * appear in PG 10. */ @SQLAction( provides = "foobar tables", @@ -135,10 +135,8 @@ public static void examineRows(TriggerData td) /** * Throw exception if value to be inserted is 44. - * Constraint triggers first became available in PostgreSQL 9.1. */ @Function( - implementor = "postgresql_ge_90100", requires = "foobar tables", provides = "constraint triggers", schema = "javatest", diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index df613c26..8c315155 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -36,8 +36,6 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; -import org.postgresql.pljava.example.annotation.ConditionalDDR; // for javadoc - /** * A class to simplify testing of PL/Java's mappings between PostgreSQL and * Java/JDBC types. @@ -94,11 +92,8 @@ * (VALUES (timestamptz '2017-08-21 18:25:29.900005Z')) AS p(orig), * roundtrip(p) AS (roundtripped timestamptz); * - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ -@SQLAction(implementor = "postgresql_ge_90300", // funcs see earlier FROM items +@SQLAction( requires = {"TypeRoundTripper.roundTrip", "point mirror type"}, install = { " SELECT" + @@ -309,8 +304,7 @@ private TypeRoundTripper() { } @Function( schema = "javatest", type = "RECORD", - provides = "TypeRoundTripper.roundTrip", - implementor = "postgresql_ge_80400" // supports function param DEFAULTs + provides = "TypeRoundTripper.roundTrip" ) public static boolean roundTrip( ResultSet in, @SQLType(defaultValue="") String classname, diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 4f2c0ec4..6b06d4d9 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -35,11 +35,10 @@ * if {@code matched} is false or the original and returned arrays or strings * do not match as seen in SQL. *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example, and also sets its own. + * This example sets an {@code implementor} tag based on a PostgreSQL condition, + * as further explained in the {@link ConditionalDDR} example. */ -@SQLAction(provides="postgresql_unicodetest", - implementor="postgresql_ge_90000", install= +@SQLAction(provides="postgresql_unicodetest", install= "SELECT CASE" + " WHEN 'UTF8' = current_setting('server_encoding')" + " THEN set_config('pljava.implementors', 'postgresql_unicodetest,' ||" + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java index ae1be899..c9982490 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -29,14 +29,12 @@ * characters. That makes it easy to test how big a value gets correctly stored * and retrieved. It should be about a GB, but in issue 52 was failing at 32768 * because of a narrowing assignment in the native code. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ -@SQLAction(requires="varlena UDT", implementor="postgresql_ge_80300", install= +@SQLAction(requires="varlena UDT", install= " SELECT CASE v::text = v::javatest.VarlenaUDTTest::text " + -" WHEN true THEN javatest.logmessage('INFO', 'works for ' || v) " + -" ELSE javatest.logmessage('WARNING', 'fails for ' || v) " + +" WHEN true " + +" THEN javatest.logmessage('INFO', 'VarlenaUDTTest works for ' || v) " + +" ELSE javatest.logmessage('WARNING', 'VarlenaUDTTest fails for ' || v) " + " END " + " FROM (VALUES (('32767')), (('32768')), (('65536')), (('1048576'))) " + " AS t ( v )" diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java index 871e9644..5cd21326 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -27,19 +27,10 @@ *

    * Everything mentioning the type XML here needs a conditional implementor tag * in case of being loaded into a PostgreSQL instance built without that type. - * The {@code pg_node_tree} type appears in 9.1. */ -@SQLAction(implementor="postgresql_ge_90100", - provides="postgresql_xml_ge91", - install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml_ge91,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) public class XMLRenderedTypes { - @Function(schema="javatest", implementor="postgresql_xml_ge91") + @Function(schema="javatest", implementor="postgresql_xml") public static SQLXML pgNodeTreeAsXML(@SQLType("pg_node_tree") SQLXML pgt) throws SQLException { diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 287652fd..5a3512d9 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -58,10 +59,6 @@ #include "pljava/SPI.h" #include "pljava/type/String.h" -#if PG_VERSION_NUM >= 90300 -#include "utils/timeout.h" -#endif - /* Include the 'magic block' that PostgreSQL 8.2 and up will use to ensure * that a module is not loaded into an incompatible server. */ @@ -250,152 +247,133 @@ static bool javaGE17 = false; static void initsequencer(enum initstage is, bool tolerant); -#if PG_VERSION_NUM >= 90100 - static bool check_libjvm_location( - char **newval, void **extra, GucSource source); - static bool check_vmoptions( - char **newval, void **extra, GucSource source); - static bool check_modulepath( - char **newval, void **extra, GucSource source); - static bool check_policy_urls( - char **newval, void **extra, GucSource source); - static bool check_enabled( - bool *newval, void **extra, GucSource source); - static bool check_java_thread_pg_entry( - int *newval, void **extra, GucSource source); - - /* Check hooks will always allow "setting" a value that is the same as - * current; otherwise, it would be frustrating to have just found settings - * that work, and be unable to save them with ALTER DATABASE SET ... because - * the check hook is called for that too, and would say it is too late.... - */ +static bool check_libjvm_location( + char **newval, void **extra, GucSource source); +static bool check_vmoptions( + char **newval, void **extra, GucSource source); +static bool check_modulepath( + char **newval, void **extra, GucSource source); +static bool check_policy_urls( + char **newval, void **extra, GucSource source); +static bool check_enabled( + bool *newval, void **extra, GucSource source); +static bool check_java_thread_pg_entry( + int *newval, void **extra, GucSource source); + +/* Check hooks will always allow "setting" a value that is the same as + * current; otherwise, it would be frustrating to have just found settings + * that work, and be unable to save them with ALTER DATABASE SET ... because + * the check hook is called for that too, and would say it is too late.... + */ - static bool check_libjvm_location( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_CAND_JVMOPENED ) - return true; - if ( libjvmlocation == *newval ) - return true; - if ( libjvmlocation && *newval && 0 == strcmp(libjvmlocation, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.libjvm_location\" setting"); - GUC_check_errdetail( - "Changing the setting can have no effect after " - "PL/Java has found and opened the library it points to."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_libjvm_location( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_CAND_JVMOPENED ) + return true; + if ( libjvmlocation == *newval ) + return true; + if ( libjvmlocation && *newval && 0 == strcmp(libjvmlocation, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.libjvm_location\" setting"); + GUC_check_errdetail( + "Changing the setting can have no effect after " + "PL/Java has found and opened the library it points to."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_vmoptions( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_JAVAVM_OPTLIST ) - return true; - if ( vmoptions == *newval ) - return true; - if ( vmoptions && *newval && 0 == strcmp(vmoptions, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.vmoptions\" setting"); - GUC_check_errdetail( - "Changing the setting can have no effect after " - "PL/Java has started the Java virtual machine."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_vmoptions( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_JAVAVM_OPTLIST ) + return true; + if ( vmoptions == *newval ) + return true; + if ( vmoptions && *newval && 0 == strcmp(vmoptions, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.vmoptions\" setting"); + GUC_check_errdetail( + "Changing the setting can have no effect after " + "PL/Java has started the Java virtual machine."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_modulepath( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_JAVAVM_OPTLIST ) - return true; - if ( modulepath == *newval ) - return true; - if ( modulepath && *newval && 0 == strcmp(modulepath, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.module_path\" setting"); - GUC_check_errdetail( - "Changing the setting has no effect after " - "PL/Java has started the Java virtual machine."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_modulepath( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_JAVAVM_OPTLIST ) + return true; + if ( modulepath == *newval ) + return true; + if ( modulepath && *newval && 0 == strcmp(modulepath, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.module_path\" setting"); + GUC_check_errdetail( + "Changing the setting has no effect after " + "PL/Java has started the Java virtual machine."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_policy_urls( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_JAVAVM_OPTLIST ) - return true; - if ( policy_urls == *newval ) - return true; - if ( policy_urls && *newval && 0 == strcmp(policy_urls, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.policy_urls\" setting"); - GUC_check_errdetail( - "Changing the setting has no effect after " - "PL/Java has started the Java virtual machine."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_policy_urls( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_JAVAVM_OPTLIST ) + return true; + if ( policy_urls == *newval ) + return true; + if ( policy_urls && *newval && 0 == strcmp(policy_urls, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.policy_urls\" setting"); + GUC_check_errdetail( + "Changing the setting has no effect after " + "PL/Java has started the Java virtual machine."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_enabled( - bool *newval, void **extra, GucSource source) - { - if ( initstage < IS_PLJAVA_ENABLED ) - return true; - if ( *newval ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.enable\" setting"); - GUC_check_errdetail( - "Start-up has progressed past the point where it is checked."); - GUC_check_errhint( - "For another chance, exit this session and start a new one."); - return false; - } +static bool check_enabled( + bool *newval, void **extra, GucSource source) +{ + if ( initstage < IS_PLJAVA_ENABLED ) + return true; + if ( *newval ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.enable\" setting"); + GUC_check_errdetail( + "Start-up has progressed past the point where it is checked."); + GUC_check_errhint( + "For another chance, exit this session and start a new one."); + return false; +} - static bool check_java_thread_pg_entry( - int *newval, void **extra, GucSource source) - { - if ( initstage < IS_PLJAVA_FOUND ) - return true; - if ( java_thread_pg_entry == *newval ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.java_thread_pg_entry\" setting"); - GUC_check_errdetail( - "Start-up has progressed past the point where it is checked."); - GUC_check_errhint( - "For another chance, exit this session and start a new one."); - return false; - } -#endif +static bool check_java_thread_pg_entry( + int *newval, void **extra, GucSource source) +{ + if ( initstage < IS_PLJAVA_FOUND ) + return true; + if ( java_thread_pg_entry == *newval ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.java_thread_pg_entry\" setting"); + GUC_check_errdetail( + "Start-up has progressed past the point where it is checked."); + GUC_check_errhint( + "For another chance, exit this session and start a new one."); + return false; +} -#if PG_VERSION_NUM < 90100 -#define errdetail_internal errdetail -#define ASSIGNHOOK(name,type) \ - static bool \ - CppConcat(assign_,name)(type newval, bool doit, GucSource source); \ - static bool \ - CppConcat(assign_,name)(type newval, bool doit, GucSource source) -#define ASSIGNRETURN(thing) return (thing) -#define ASSIGNRETURNIFCHECK(thing) if (doit) ; else return (thing) -#define ASSIGNRETURNIFNXACT(thing) \ - if (! deferInit && pljavaViableXact()) ; else return (thing) -#define ASSIGNSTRINGHOOK(name) \ - static const char * \ - CppConcat(assign_,name)(const char *newval, bool doit, GucSource source); \ - static const char * \ - CppConcat(assign_,name)(const char *newval, bool doit, GucSource source) -#else #define ASSIGNHOOK(name,type) \ static void \ CppConcat(assign_,name)(type newval, void *extra); \ @@ -406,7 +384,6 @@ static void initsequencer(enum initstage is, bool tolerant); #define ASSIGNRETURNIFNXACT(thing) \ if (! deferInit && pljavaViableXact()) ; else return #define ASSIGNSTRINGHOOK(name) ASSIGNHOOK(name, const char *) -#endif #define ASSIGNENUMHOOK(name) ASSIGNHOOK(name,int) #define ENUMBOOTVAL(entry) ((entry).val) @@ -796,18 +773,13 @@ static void initsequencer(enum initstage is, bool tolerant) * are just function parameters with evaluation order unknown. */ StringInfoData buf; -#if PG_VERSION_NUM >= 90200 -#define MOREHINT \ - appendStringInfo(&buf, \ - "using ALTER DATABASE %s SET ... FROM CURRENT or ", \ - pljavaDbName()), -#else -#define MOREHINT -#endif + ereport(NOTICE, ( errmsg("PL/Java successfully started after adjusting settings"), (initStringInfo(&buf), - MOREHINT + appendStringInfo(&buf, \ + "using ALTER DATABASE %s SET ... FROM CURRENT or ", \ + pljavaDbName()), errhint("The settings that worked should be saved (%s" "in the \"%s\" file). For a reminder of what has been set, " "try: SELECT name, setting FROM pg_settings WHERE name LIKE" @@ -816,7 +788,7 @@ static void initsequencer(enum initstage is, bool tolerant) superuser() ? PG_GETCONFIGOPTION("config_file") : "postgresql.conf")))); -#undef MOREHINT + if ( loadAsExtensionFailed ) { #if PG_VERSION_NUM < 130000 @@ -902,7 +874,7 @@ static void reLogWithChangedLevel(int level) else if ( ERRCODE_WARNING == category || ERRCODE_NO_DATA == category || ERRCODE_SUCCESSFUL_COMPLETION == category ) sqlstate = ERRCODE_INTERNAL_ERROR; -#if PG_VERSION_NUM >= 90500 + edata->elevel = level; edata->sqlerrcode = sqlstate; PG_TRY(); @@ -916,43 +888,6 @@ static void reLogWithChangedLevel(int level) } PG_END_TRY(); FreeErrorData(edata); -#else - if (!errstart(level, edata->filename, edata->lineno, - edata->funcname, NULL)) - { - FreeErrorData(edata); - return; - } - - errcode(sqlstate); - if (edata->message) - errmsg("%s", edata->message); - if (edata->detail) - errdetail("%s", edata->detail); - if (edata->detail_log) - errdetail_log("%s", edata->detail_log); - if (edata->hint) - errhint("%s", edata->hint); - if (edata->context) - errcontext("%s", edata->context); /* this may need to be trimmed */ -#if PG_VERSION_NUM >= 90300 - if (edata->schema_name) - err_generic_string(PG_DIAG_SCHEMA_NAME, edata->schema_name); - if (edata->table_name) - err_generic_string(PG_DIAG_TABLE_NAME, edata->table_name); - if (edata->column_name) - err_generic_string(PG_DIAG_COLUMN_NAME, edata->column_name); - if (edata->datatype_name) - err_generic_string(PG_DIAG_DATATYPE_NAME, edata->datatype_name); - if (edata->constraint_name) - err_generic_string(PG_DIAG_CONSTRAINT_NAME, edata->constraint_name); -#endif - if (edata->internalquery) - internalerrquery(edata->internalquery); - - FreeErrorData(edata); - errfinish(0); -#endif } void _PG_init() @@ -969,8 +904,7 @@ void _PG_init() * preparing the launch options before it is launched. PostgreSQL knows what * it is, but won't directly say; give it some choices and it'll pick one. * Alternatively, let Maven or Ant determine and add a -D at build time from - * the path.separator property. Maybe that's cleaner? This only works for - * PG_VERSION_NUM >= 90100. + * the path.separator property. Maybe that's cleaner? */ sep = first_path_var_separator(":;"); if ( NULL == sep ) @@ -1340,12 +1274,7 @@ static void pljavaQuickDieHandler(int signum) } static sigjmp_buf recoverBuf; -static void terminationTimeoutHandler( -#if PG_VERSION_NUM >= 90300 -#else - int signum -#endif -) +static void terminationTimeoutHandler() { kill(MyProcPid, SIGQUIT); @@ -1387,12 +1316,7 @@ static void _destroyJavaVM(int status, Datum dummy) { Invocation ctx; #ifdef USE_PLJAVA_SIGHANDLERS - -#if PG_VERSION_NUM >= 90300 TimeoutId tid; -#else - pqsigfunc saveSigAlrm; -#endif Invocation_pushBootContext(&ctx); if(sigsetjmp(recoverBuf, 1) != 0) @@ -1404,24 +1328,13 @@ static void _destroyJavaVM(int status, Datum dummy) return; } -#if PG_VERSION_NUM >= 90300 tid = RegisterTimeout(USER_TIMEOUT, terminationTimeoutHandler); enable_timeout_after(tid, 5000); -#else - saveSigAlrm = pqsignal(SIGALRM, terminationTimeoutHandler); - enable_sig_alarm(5000, false); -#endif elog(DEBUG2, "shutting down the Java virtual machine"); JNI_destroyVM(s_javaVM); -#if PG_VERSION_NUM >= 90300 disable_timeout(tid, false); -#else - disable_sig_alarm(false); - pqsignal(SIGALRM, saveSigAlrm); -#endif - #else Invocation_pushBootContext(&ctx); elog(DEBUG2, "shutting down the Java virtual machine"); @@ -1646,12 +1559,7 @@ static jint initializeJavaVM(JVMOptList *optList) #define GUCBOOTVAL(v) (v), #define GUCBOOTASSIGN(a, v) #define GUCFLAGS(f) (f), - -#if PG_VERSION_NUM >= 90100 #define GUCCHECK(h) (h), -#else -#define GUCCHECK(h) -#endif #define BOOL_GUC(name, short_desc, long_desc, valueAddr, bootValue, context, \ flags, check_hook, assign_hook, show_hook) \ @@ -1685,11 +1593,7 @@ static jint initializeJavaVM(JVMOptList *optList) #define PLJAVA_LIBJVMDEFAULT "libjvm" #endif -#if PG_VERSION_NUM >= 90200 #define PLJAVA_ENABLE_DEFAULT true -#else -#define PLJAVA_ENABLE_DEFAULT false -#endif #if PG_VERSION_NUM < 110000 #define PLJAVA_IMPLEMENTOR_FLAGS GUC_LIST_INPUT | GUC_LIST_QUOTE diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index e557231e..17d32ab2 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -44,10 +44,6 @@ extern void pljava_ExecutionPlan_initialize(void); #include "pljava/SQLInputFromTuple.h" #include "pljava/VarlenaWrapper.h" -#if PG_VERSION_NUM < 80400 -#include /* heap_freetuple was there then */ -#endif - static jclass s_DualState_class; static jmethodID s_DualState_resourceOwnerRelease; diff --git a/pljava-so/src/main/c/Exception.c b/pljava-so/src/main/c/Exception.c index 071b8cf4..ccac4e7b 100644 --- a/pljava-so/src/main/c/Exception.c +++ b/pljava-so/src/main/c/Exception.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -59,7 +59,13 @@ Exception_featureNotSupported(const char* requestedFeature, const char* introVer appendStringInfoString(&buf, requestedFeature); appendStringInfoString(&buf, " lacks support in PostgreSQL version "); appendStringInfo(&buf, "%d.%d", - PG_VERSION_NUM / 10000, (PG_VERSION_NUM / 100) % 100); + PG_VERSION_NUM / 10000, +#if PG_VERSION_NUM >= 100000 + (PG_VERSION_NUM) % 10000 +#else + (PG_VERSION_NUM / 100) % 100 +#endif + ); appendStringInfoString(&buf, ". It was introduced in version "); appendStringInfoString(&buf, introVersion); diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index 6ff53eb4..ed39bc0f 100644 --- a/pljava-so/src/main/c/ExecutionPlan.c +++ b/pljava-so/src/main/c/ExecutionPlan.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -278,9 +278,7 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass clazz, jobject key, jstring jcmd, jobjectArray paramTypes) { jobject result = 0; -#if PG_VERSION_NUM >= 90200 int spi_ret; -#endif BEGIN_NATIVE STACK_BASE_VARS STACK_BASE_PUSH(env) @@ -321,16 +319,12 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass /* Make the plan durable */ p2l.longVal = 0L; /* ensure that the rest is zeroed out */ -#if PG_VERSION_NUM >= 90200 spi_ret = SPI_keepplan(ePlan); if ( 0 == spi_ret ) p2l.ptrVal = ePlan; else Exception_throwSPI("keepplan", spi_ret); -#else - p2l.ptrVal = SPI_saveplan(ePlan); - SPI_freeplan(ePlan); /* Get rid of original, nobody can see it */ -#endif + result = JNI_newObjectLocked( s_ExecutionPlan_class, s_ExecutionPlan_init, /* (jlong)0 as resource owner: the saved plan isn't transient */ diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index a46c0e6d..4428f240 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -41,15 +41,6 @@ #define PARAM_OIDS(procStruct) (procStruct)->proargtypes.values -#if 90305<=PG_VERSION_NUM || \ - 90209<=PG_VERSION_NUM && PG_VERSION_NUM<90300 || \ - 90114<=PG_VERSION_NUM && PG_VERSION_NUM<90200 || \ - 90018<=PG_VERSION_NUM && PG_VERSION_NUM<90100 || \ - 80422<=PG_VERSION_NUM && PG_VERSION_NUM<90000 -#else -#error "Need fallback for heap_copy_tuple_as_datum" -#endif - #define COUNTCHECK(refs, prims) ((jshort)(((refs) << 8) | ((prims) & 0xff))) jobject pljava_Function_NO_LOADER; diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 30af880a..2cb5a9cb 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -10,17 +10,11 @@ * Chapman Flack */ #include -#if PG_VERSION_NUM >= 90300 #include -#else -#include -#endif #include #include #include -#if PG_VERSION_NUM >= 90100 #include -#endif #include #include #include @@ -30,20 +24,15 @@ #include #include #include -#if PG_VERSION_NUM >= 80400 #include -#endif #include #if PG_VERSION_NUM >= 120000 #include #define GetNamespaceOid(k1) \ GetSysCacheOid1(NAMESPACENAME, Anum_pg_namespace_oid, k1) -#elif PG_VERSION_NUM >= 90000 -#define GetNamespaceOid(k1) GetSysCacheOid1(NAMESPACENAME, k1) #else -#define SearchSysCache1(cid, k1) SearchSysCache(cid, k1, 0, 0, 0) -#define GetNamespaceOid(k1) GetSysCacheOid(NAMESPACENAME, k1, 0, 0, 0) +#define GetNamespaceOid(k1) GetSysCacheOid1(NAMESPACENAME, k1) #endif #include "pljava/InstallHelper.h" @@ -55,47 +44,17 @@ #include "pljava/type/String.h" /* - * Before 9.1, there was no creating_extension. Before 9.5, it did not have - * PGDLLIMPORT and so was not visible in Windows. In either case, just define - * it to be false, but also define CREATING_EXTENSION_HACK if on Windows and - * it needs to be tested for in some roundabout way. - */ -#if PG_VERSION_NUM < 90100 || defined(_MSC_VER) && PG_VERSION_NUM < 90500 -#define creating_extension false -#if PG_VERSION_NUM >= 90100 -#define CREATING_EXTENSION_HACK -#endif -#endif - -/* - * Before 9.1, there was no IsBinaryUpgrade. Before 9.5, it did not have - * PGDLLIMPORT and so was not visible in Windows. In either case, just define - * it to be false; Windows users may have trouble using pg_upgrade to versions - * earlier than 9.5, but with the current version being 9.6 that should be rare. - */ -#if PG_VERSION_NUM < 90100 || defined(_MSC_VER) && PG_VERSION_NUM < 90500 -#define IsBinaryUpgrade false -#endif - -/* - * Before 9.3, there was no IsBackgroundWorker. As of 9.6.1 it still does not + * As of 9.6.1, IsBackgroundWorker still does not * have PGDLLIMPORT, but MyBgworkerEntry != NULL can be used in MSVC instead. - * However, until 9.3.3, even that did not have PGDLLIMPORT, and there's not - * much to be done about it. BackgroundWorkerness won't be detected in MSVC - * for 9.3.0 through 9.3.2. * * One thing it's needed for is to avoid dereferencing MyProcPort in a * background worker, where it's not set. */ -#if PG_VERSION_NUM < 90300 || defined(_MSC_VER) && PG_VERSION_NUM < 90303 -#define IsBackgroundWorker false -#else #include #if defined(_MSC_VER) #include #define IsBackgroundWorker (MyBgworkerEntry != NULL) #endif -#endif /* * The name of the table the extension scripts will create to pass information @@ -112,7 +71,7 @@ static jfieldID s_InstallHelper_MANAGE_CONTEXT_LOADER; static bool extensionExNihilo = false; -static void checkLoadPath( bool *livecheck); +static void checkLoadPath(void); static void getExtensionLoadPath(void); static char *origUserName(); @@ -153,7 +112,6 @@ static char *origUserName() { if ( IsAutoVacuumWorkerProcess() || IsBackgroundWorker ) { -#if PG_VERSION_NUM >= 90500 char *shortlived; static char *longlived; if ( NULL == longlived ) @@ -163,14 +121,6 @@ static char *origUserName() pfree(shortlived); } return longlived; -#else - ereport(ERROR, ( - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("PL/Java in a background or autovacuum worker not supported " - "in this PostgreSQL version"), - errhint("PostgreSQL 9.5 is the first version in which " - "such usage is supported."))); -#endif } return MyProcPort->user_name; } @@ -178,22 +128,17 @@ static char *origUserName() char const *pljavaClusterName() { /* - * If PostgreSQL isn't at least 9.5, there can't BE a cluster name, and if - * it is, then there's always one (even if it is an empty string), so - * PG_GETCONFIGOPTION is safe. + * In PostgreSQL of at least 9.5, there's always one (even if it is an empty + * string), so PG_GETCONFIGOPTION is safe. */ -#if PG_VERSION_NUM < 90500 - return ""; -#else return PG_GETCONFIGOPTION("cluster_name"); -#endif } void pljavaCheckExtension( bool *livecheck) { if ( ! creating_extension ) { - checkLoadPath( livecheck); + checkLoadPath(); return; } if ( NULL != livecheck ) @@ -216,29 +161,17 @@ void pljavaCheckExtension( bool *livecheck) * on Windows. So if livecheck isn't null, this function only needs to proceed * as far as the CREATING_EXTENSION_HACK and then return. */ -static void checkLoadPath( bool *livecheck) +static void checkLoadPath() { List *l; Node *ut; LoadStmt *ls; -#if PG_VERSION_NUM >= 80300 PlannedStmt *ps; -#else - Query *ps; -#endif -#ifndef CREATING_EXTENSION_HACK - if ( NULL != livecheck ) - return; -#endif if ( NULL == ActivePortal ) return; - l = ActivePortal-> -#if PG_VERSION_NUM >= 80300 - stmts; -#else - parseTrees; -#endif + l = ActivePortal->stmts; + if ( NULL == l ) return; if ( 1 < list_length( l) ) @@ -249,15 +182,10 @@ static void checkLoadPath( bool *livecheck) elog(DEBUG2, "got null for first statement from ActivePortal"); return; } -#if PG_VERSION_NUM >= 80300 + if ( T_PlannedStmt == nodeTag(ut) ) { ps = (PlannedStmt *)ut; -#else - if ( T_Query == nodeTag(ut) ) - { - ps = (Query *)ut; -#endif if ( CMD_UTILITY != ps->commandType ) { elog(DEBUG2, "ActivePortal has PlannedStmt command type %u", @@ -272,22 +200,8 @@ static void checkLoadPath( bool *livecheck) } } if ( T_LoadStmt != nodeTag(ut) ) -#ifdef CREATING_EXTENSION_HACK - if ( T_CreateExtensionStmt == nodeTag(ut) ) - { - if ( NULL != livecheck ) - { - *livecheck = true; - return; - } - getExtensionLoadPath(); - if ( NULL != pljavaLoadPath ) - pljavaLoadingAsExtension = true; - } -#endif - return; - if ( NULL != livecheck ) return; + ls = (LoadStmt *)ut; if ( NULL == ls->filename ) { @@ -555,14 +469,9 @@ char *InstallHelper_hello() nativeVer = String_createJavaStringFromNTS(SO_VERSION_STRING); serverBuiltVer = String_createJavaStringFromNTS(PG_VERSION_STR); -#if PG_VERSION_NUM >= 90100 InitFunctionCallInfoData(fcinfo, NULL, 0, InvalidOid, /* collation */ NULL, NULL); -#else - InitFunctionCallInfoData(fcinfo, NULL, 0, - NULL, NULL); -#endif runningVer = DatumGetTextP(pgsql_version(&fcinfo)); serverRunningVer = String_createJavaString(runningVer); pfree(runningVer); @@ -614,15 +523,9 @@ void InstallHelper_groundwork() bool snapshot_set = false; Invocation_pushInvocation(&ctx); ctx.function = Function_INIT_WRITER; -#if PG_VERSION_NUM >= 80400 if ( ! ActiveSnapshotSet() ) { PushActiveSnapshot(GetTransactionSnapshot()); -#else - if ( NULL == ActiveSnapshot ) - { - ActiveSnapshot = CopySnapshot(GetTransactionSnapshot()); -#endif snapshot_set = true; } PG_TRY(); @@ -644,11 +547,7 @@ void InstallHelper_groundwork() JNI_deleteLocalRef(jlptq); if ( snapshot_set ) { -#if PG_VERSION_NUM >= 80400 PopActiveSnapshot(); -#else - ActiveSnapshot = NULL; -#endif } Invocation_popInvocation(false); } @@ -656,11 +555,7 @@ void InstallHelper_groundwork() { if ( snapshot_set ) { -#if PG_VERSION_NUM >= 80400 PopActiveSnapshot(); -#else - ActiveSnapshot = NULL; -#endif } Invocation_popInvocation(true); PG_RE_THROW(); diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index ea104bd9..3d891b30 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -94,14 +94,19 @@ void SPI_initialize(void) CONFIRMCONST(SPI_OK_INSERT_RETURNING); CONFIRMCONST(SPI_OK_DELETE_RETURNING); CONFIRMCONST(SPI_OK_UPDATE_RETURNING); -#if PG_VERSION_NUM >= 80400 CONFIRMCONST(SPI_OK_REWRITTEN); -#endif #if PG_VERSION_NUM >= 100000 CONFIRMCONST(SPI_OK_REL_REGISTER); CONFIRMCONST(SPI_OK_REL_UNREGISTER); CONFIRMCONST(SPI_OK_TD_REGISTER); #endif +#if PG_VERSION_NUM >= 150000 + CONFIRMCONST(SPI_OK_MERGE); +#endif + +#if PG_VERSION_NUM >= 110000 + CONFIRMCONST(SPI_OPT_NONATOMIC); +#endif } /**************************************** diff --git a/pljava-so/src/main/c/Session.c b/pljava-so/src/main/c/Session.c index be4a2cca..f551cb5b 100644 --- a/pljava-so/src/main/c/Session.c +++ b/pljava-so/src/main/c/Session.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include #include @@ -45,9 +49,6 @@ Java_org_postgresql_pljava_internal_Session__1setUser( * a finally block after an exception. */ BEGIN_NATIVE_NO_ERRCHECK -#if 80402<=PG_VERSION_NUM || \ - 80309<=PG_VERSION_NUM && PG_VERSION_NUM<80400 || \ - 80215<=PG_VERSION_NUM && PG_VERSION_NUM<80300 if (InSecurityRestrictedOperation()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg( "cannot set parameter \"%s\" within security-restricted operation", @@ -59,15 +60,6 @@ Java_org_postgresql_pljava_internal_Session__1setUser( else secContext &= ~SECURITY_LOCAL_USERID_CHANGE; SetUserIdAndSecContext(AclId_getAclId(aclId), secContext); -#elif PG_VERSION_NUM>=80206 - (void)secContext; /* away with your unused-variable warnings! */ - GetUserIdAndContext(&dummy, &wasLocalChange); - SetUserIdAndContext(AclId_getAclId(aclId), (bool)isLocalChange); -#else - (void)secContext; - (void)dummy; - SetUserId(AclId_getAclId(aclId)); -#endif END_NATIVE return wasLocalChange ? JNI_TRUE : JNI_FALSE; } diff --git a/pljava-so/src/main/c/TypeOid.c b/pljava-so/src/main/c/TypeOid.c index 0fce9ad1..81005326 100644 --- a/pljava-so/src/main/c/TypeOid.c +++ b/pljava-so/src/main/c/TypeOid.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2023 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 @@ -50,10 +50,6 @@ JNIEXPORT void JNICALL Java_org_postgresql_pljava_jdbc_TypeOid__1dummy(JNIEnv * CONFIRMCONST(VARCHAROID); CONFIRMCONST(OIDOID); CONFIRMCONST(BPCHAROID); - -#if PG_VERSION_NUM >= 90100 CONFIRMCONST(PG_NODE_TREEOID); -#endif - CONFIRMCONST(TRIGGEROID); } diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 5805ace1..cb1d9e7a 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -21,13 +21,8 @@ #endif #include - -#if PG_VERSION_NUM < 80400 -#define RegisterSnapshotOnOwner(s,o) NULL -#else #include #include -#endif #include "org_postgresql_pljava_internal_VarlenaWrapper_Input_State.h" #include "org_postgresql_pljava_internal_VarlenaWrapper_Output_State.h" @@ -41,37 +36,7 @@ #define GetOldestSnapshot() NULL #endif -#if PG_VERSION_NUM < 90400 -/* - * There aren't 'indirect' varlenas yet, IS_EXTERNAL_ONDISK is just IS_EXTERNAL, - * and VARATT_EXTERNAL_GET_POINTER is private inside tuptoaster.c; copy it here. - */ -#define VARATT_IS_EXTERNAL_ONDISK VARATT_IS_EXTERNAL -#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \ -do { \ - varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \ - memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \ -} while (0) -#endif - -#if PG_VERSION_NUM < 80300 -#define VARSIZE_ANY(PTR) VARSIZE(PTR) -#define VARSIZE_ANY_EXHDR(PTR) (VARSIZE(PTR) - VARHDRSZ) -#define SET_VARSIZE(PTR, len) VARATT_SIZEP(PTR) = len & VARATT_MASK_SIZE -struct varatt_external -{ - int32 va_extsize; /* the only piece used here */ -}; -#undef VARATT_EXTERNAL_GET_POINTER -#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \ -do { \ - (toast_pointer).va_extsize = \ - ((varattrib *)(attr))->va_content.va_external.va_extsize; \ -} while (0) -#define _VL_TYPE varattrib * -#else #define _VL_TYPE struct varlena * -#endif #if PG_VERSION_NUM < 140000 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) ((toast_pointer).va_extsize) @@ -102,37 +67,6 @@ static jfieldID s_VarlenaWrapper_Input_State_varlena; * final reallocation and copy will happen. */ -#if PG_VERSION_NUM < 90500 -/* - * There aren't 'expanded' varlenas yet. Copy some defs (in simplified form) - * and pretend there are. - */ -typedef struct ExpandedObjectHeader ExpandedObjectHeader; - -typedef Size (*EOM_get_flat_size_method) (ExpandedObjectHeader *eohptr); -typedef void (*EOM_flatten_into_method) (ExpandedObjectHeader *eohptr, - void *result, Size allocated_size); - -typedef struct ExpandedObjectMethods -{ - EOM_get_flat_size_method get_flat_size; - EOM_flatten_into_method flatten_into; -} ExpandedObjectMethods; - -struct ExpandedObjectHeader -{ - int32 magic; - MemoryContext eoh_context; -}; - -#define EOH_init_header(eohptr, methods, obj_context) \ - do {(eohptr)->magic = -1; (eohptr)->eoh_context = (obj_context);} while (0) - -#define EOHPGetRWDatum(eohptr) (eohptr) -#define DatumGetEOHP(d) (d) -#define VARATT_IS_EXTERNAL_EXPANDED(attr) false -#endif - static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr); static void VOS_flatten_into(ExpandedObjectHeader *eohptr, void *result, Size allocated_size); @@ -189,7 +123,6 @@ jobject pljava_VarlenaWrapper_Input( vl = (_VL_TYPE) DatumGetPointer(d); -#if PG_VERSION_NUM >= 90400 if ( VARATT_IS_EXTERNAL_INDIRECT(vl) ) /* at most once; can't be nested */ { struct varatt_indirect redirect; @@ -197,7 +130,6 @@ jobject pljava_VarlenaWrapper_Input( vl = (_VL_TYPE)redirect.pointer; d = PointerGetDatum(vl); } -#endif parked = VARSIZE_ANY(vl); actual = toast_raw_datum_size(d) - VARHDRSZ; @@ -342,25 +274,11 @@ jobject pljava_VarlenaWrapper_Output(MemoryContext parent, ResourceOwner ro) Datum pljava_VarlenaWrapper_adopt(jobject vlw) { Ptr2Long p2l; -#if PG_VERSION_NUM < 90500 - ExpandedObjectHeader *eohptr; - Size final_size; - void *final_result; -#endif p2l.longVal = JNI_callLongMethodLocked(vlw, s_VarlenaWrapper_adopt, pljava_DualState_key()); -#if PG_VERSION_NUM >= 90500 + return PointerGetDatum(p2l.ptrVal); -#else - eohptr = p2l.ptrVal; - if ( -1 != eohptr->magic ) - return PointerGetDatum(eohptr); - final_size = VOS_get_flat_size(eohptr); - final_result = MemoryContextAlloc(eohptr->eoh_context, final_size); - VOS_flatten_into(eohptr, final_result, final_size); - return PointerGetDatum(final_result); -#endif } static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr) @@ -376,9 +294,6 @@ static void VOS_flatten_into(ExpandedObjectHeader *eohptr, ExpandedVarlenaOutputStreamHeader *evosh = (ExpandedVarlenaOutputStreamHeader *)eohptr; ExpandedVarlenaOutputStreamNode *node = evosh->tail; -#if PG_VERSION_NUM < 90500 - ExpandedVarlenaOutputStreamNode *next; -#endif Assert(allocated_size == evosh->total_size); SET_VARSIZE(result, allocated_size); @@ -391,25 +306,6 @@ static void VOS_flatten_into(ExpandedObjectHeader *eohptr, result = (char *)result + node->size; } while ( node != evosh->tail ); - -#if PG_VERSION_NUM < 90500 - /* - * It's been flattened into the same context; the original nodes can be - * freed so the 2x memory usage doesn't last longer than necessary. Freeing - * them retail isn't ideal, but this is back-compatibility code. Remember - * the first one wasn't a separate allocation. - */ - node = node->next; /* this is the head, the one that can't be pfreed */ - evosh->tail = node; /* tail is now head, the non-pfreeable node */ - node = node->next; - while ( node != evosh->tail ) - { - next = node->next; - pfree(node); - node = next; - } - pfree(evosh); -#endif } void pljava_VarlenaWrapper_initialize(void) @@ -495,7 +391,6 @@ JNIEXPORT void JNICALL Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unregisterSnapshot (JNIEnv *env, jobject _this, jlong snapshot, jlong ro) { -#if PG_VERSION_NUM >= 80400 BEGIN_NATIVE_NO_ERRCHECK Ptr2Long p2lsnap; Ptr2Long p2lro; @@ -503,7 +398,6 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unreg p2lro.longVal = ro; UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); END_NATIVE -#endif } /* @@ -517,10 +411,8 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa { Ptr2Long p2lvl; Ptr2Long p2lcxt; -#if PG_VERSION_NUM >= 80400 Ptr2Long p2lsnap; Ptr2Long p2lro; -#endif Ptr2Long p2ldetoasted; _VL_TYPE detoasted; MemoryContext prevcxt; @@ -530,10 +422,8 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa p2lvl.longVal = vl; p2lcxt.longVal = cxt; -#if PG_VERSION_NUM >= 80400 p2lsnap.longVal = snap; p2lro.longVal = resOwner; -#endif prevcxt = MemoryContextSwitchTo((MemoryContext)p2lcxt.ptrVal); @@ -547,10 +437,8 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa s_VarlenaWrapper_Input_State_varlena, p2ldetoasted.longVal); pfree(p2lvl.ptrVal); -#if PG_VERSION_NUM >= 80400 if ( 0 != snap ) UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); -#endif dbb = JNI_newDirectByteBuffer( VARDATA(detoasted), VARSIZE_ANY_EXHDR(detoasted)); diff --git a/pljava-so/src/main/c/XactListener.c b/pljava-so/src/main/c/XactListener.c index 543c2cd4..09632618 100644 --- a/pljava-so/src/main/c/XactListener.c +++ b/pljava-so/src/main/c/XactListener.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -41,11 +41,9 @@ case XACT_EVENT_##c: \ CASE( PREPARE ); CASE( PRE_COMMIT ); CASE( PRE_PREPARE ); -#if PG_VERSION_NUM >= 90500 CASE( PARALLEL_COMMIT ); CASE( PARALLEL_ABORT ); CASE( PARALLEL_PRE_COMMIT ); -#endif } JNI_callStaticVoidMethod(s_XactListener_class, diff --git a/pljava-so/src/main/c/type/AclId.c b/pljava-so/src/main/c/type/AclId.c index 504b8cf6..a9f57066 100644 --- a/pljava-so/src/main/c/type/AclId.c +++ b/pljava-so/src/main/c/type/AclId.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -184,11 +184,7 @@ Java_org_postgresql_pljava_internal_AclId__1getName(JNIEnv* env, jobject aclId) { result = String_createJavaStringFromNTS( GetUserNameFromId( -#if PG_VERSION_NUM >= 90500 AclId_getAclId(aclId), /* noerr= */ false -#else - AclId_getAclId(aclId) -#endif ) ); } diff --git a/pljava-so/src/main/c/type/Array.c b/pljava-so/src/main/c/type/Array.c index 232fc05a..b2e65448 100644 --- a/pljava-so/src/main/c/type/Array.c +++ b/pljava-so/src/main/c/type/Array.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -51,11 +51,7 @@ ArrayType* createArrayType(jsize nElems, size_t elemSize, Oid elemType, bool wit v->dataoffset = (int32)dataoffset; MemoryContextSwitchTo(currCtx); -#if PG_VERSION_NUM < 80300 - ARR_SIZE(v) = nBytes; -#else SET_VARSIZE(v, nBytes); -#endif ARR_NDIM(v) = 1; ARR_ELEMTYPE(v) = elemType; *((int*)ARR_DIMS(v)) = nElems; @@ -88,14 +84,8 @@ static jvalue _Array_coerceDatum(Type self, Datum arg) JNI_setObjectArrayElement(objArray, idx, obj.l); JNI_deleteLocalRef(obj.l); -#if PG_VERSION_NUM < 80300 - values = att_addlength(values, elemLength, PointerGetDatum(values)); - values = (char*)att_align(values, elemAlign); -#else values = att_addlength_datum(values, elemLength, PointerGetDatum(values)); values = (char*)att_align_nominal(values, elemAlign); -#endif - } } result.l = (jobject)objArray; diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index aecb67e0..37bb89b2 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include @@ -256,11 +260,7 @@ Java_org_postgresql_pljava_internal_Oid__1forTypeName(JNIEnv* env, jclass cls, j PG_TRY(); { int32 typmod = 0; -#if PG_VERSION_NUM < 90400 - parseTypeString(typeNameOrOid, &typeId, &typmod); -#else parseTypeString(typeNameOrOid, &typeId, &typmod, 0); -#endif } PG_CATCH(); { diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 16b483b3..157f4251 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 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 @@ -50,9 +50,7 @@ static bool _SQLXML_canReplaceType(Type self, Type other) #if defined(XMLOID) Type_getOid(other) == XMLOID || #endif -#if PG_VERSION_NUM >= 90100 Type_getOid(other) == PG_NODE_TREEOID || /* a synthetic rendering */ -#endif Type_getOid(other) == TEXTOID; } @@ -86,17 +84,12 @@ static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) s_SQLXML_class, s_SQLXML_adopt, sqlxml, Type_getOid(self)); Datum d = pljava_VarlenaWrapper_adopt(vw); JNI_deleteLocalRef(vw); -#if PG_VERSION_NUM >= 90500 if ( VARATT_IS_EXTERNAL_EXPANDED_RW(DatumGetPointer(d)) ) return TransferExpandedObject(d, CurrentMemoryContext); -#endif -#if PG_VERSION_NUM >= 90200 + MemoryContextSetParent( GetMemoryChunkContext(DatumGetPointer(d)), CurrentMemoryContext); -#else - if ( CurrentMemoryContext != GetMemoryChunkContext(DatumGetPointer(d)) ) - d = PointerGetDatum(PG_DETOAST_DATUM_COPY(d)); -#endif + return d; } @@ -134,18 +127,15 @@ static Type _SQLXML_obtain(Oid typeId) #if defined(XMLOID) static Type xmlInstance; #endif -#if PG_VERSION_NUM >= 90100 static Type pgNodeTreeInstance; -#endif + switch ( typeId ) { -#if PG_VERSION_NUM >= 90100 case PG_NODE_TREEOID: allowedId = PG_NODE_TREEOID; synthetic = true; cache = &pgNodeTreeInstance; break; -#endif default: if ( TEXTOID == typeId ) { diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index e380be32..8b462394 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -226,11 +226,7 @@ text* String_createText(jstring javaString) /* Allocate and initialize the text structure. */ result = (text*)palloc(varSize); -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(result) = varSize; /* Total size of structure, not just data */ -#else SET_VARSIZE(result, varSize); /* Total size of structure, not just data */ -#endif memcpy(VARDATA(result), denc, dencLen); if(denc != sid.data) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 2cbdba9e..7cd76a6c 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -405,23 +405,7 @@ static Datum _Timestamptz_coerceObject(Type self, jobject ts) */ static int32 Timestamp_getTimeZone(pg_time_t time) { -#if defined(_MSC_VER) && ( \ - 100000<=PG_VERSION_NUM && PG_VERSION_NUM<100002 || \ - 90600<=PG_VERSION_NUM && PG_VERSION_NUM< 90607 || \ - 90500<=PG_VERSION_NUM && PG_VERSION_NUM< 90511 || \ - 90400<=PG_VERSION_NUM && PG_VERSION_NUM< 90416 || \ - PG_VERSION_NUM < 90321 ) - /* This is gross, but pg_tzset has a cache, so not as gross as you think. - * There is some renewed interest on pgsql-hackers to find a good answer for - * the MSVC PGDLLIMPORT nonsense, so this may not have to stay gross. - */ - char const *tzname = PG_GETCONFIGOPTION("timezone"); - struct pg_tm* tx = pg_localtime(&time, pg_tzset(tzname)); -#elif PG_VERSION_NUM < 80300 - struct pg_tm* tx = pg_localtime(&time, global_timezone); -#else struct pg_tm* tx = pg_localtime(&time, session_timezone); -#endif if ( NULL == tx ) ereport(ERROR, ( errcode(ERRCODE_DATA_EXCEPTION), diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index a0d181e2..188e86e7 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -30,42 +30,11 @@ #include "pljava/HashMap.h" #include "pljava/SPI.h" -#if PG_VERSION_NUM < 80300 -typedef enum CoercionPathType -{ - COERCION_PATH_NONE, /* failed to find any coercion pathway */ - COERCION_PATH_FUNC, /* apply the specified coercion function */ - COERCION_PATH_RELABELTYPE, /* binary-compatible cast, no function */ - COERCION_PATH_ARRAYCOERCE, /* need an ArrayCoerceExpr node */ - COERCION_PATH_COERCEVIAIO /* need a CoerceViaIO node */ -} CoercionPathType; - -static CoercionPathType fcp(Oid targetTypeId, Oid sourceTypeId, - CoercionContext ccontext, Oid *funcid); -static CoercionPathType fcp(Oid targetTypeId, Oid sourceTypeId, - CoercionContext ccontext, Oid *funcid) -{ - if ( find_coercion_pathway(targetTypeId, sourceTypeId, ccontext, funcid) ) - return *funcid != InvalidOid ? - COERCION_PATH_FUNC : COERCION_PATH_RELABELTYPE; - else - return COERCION_PATH_NONE; -} -#define find_coercion_pathway fcp -#endif - -#if PG_VERSION_NUM < 90500 -#define DomainHasConstraints(x) true -#endif - #if PG_VERSION_NUM < 110000 static Oid BOOLARRAYOID; static Oid CHARARRAYOID; static Oid FLOAT8ARRAYOID; static Oid INT8ARRAYOID; -#if PG_VERSION_NUM < 80400 -static Oid INT2ARRAYOID; -#endif #endif static HashMap s_typeByOid; @@ -1055,9 +1024,6 @@ void Type_initialize(void) CHARARRAYOID = get_array_type(CHAROID); FLOAT8ARRAYOID = get_array_type(FLOAT8OID); INT8ARRAYOID = get_array_type(INT8OID); -#if PG_VERSION_NUM < 80400 - INT2ARRAYOID = get_array_type(INT2OID); -#endif #endif initializeTypeBridges(); diff --git a/pljava-so/src/main/c/type/UDT.c b/pljava-so/src/main/c/type/UDT.c index d351038f..1f33b0af 100644 --- a/pljava-so/src/main/c/type/UDT.c +++ b/pljava-so/src/main/c/type/UDT.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -29,10 +30,6 @@ #include "pljava/SQLInputFromTuple.h" #include "pljava/SQLOutputToTuple.h" -#if PG_VERSION_NUM >= 90000 -#include -#endif - /* * This code, as currently constituted, makes these assumptions that limit how * Java can implement a (scalar) UDT: @@ -183,11 +180,7 @@ static Datum coerceScalarObject(UDT self, jobject value) { /* Assign the correct length. */ -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(buffer.data) = buffer.len; -#else SET_VARSIZE(buffer.data, buffer.len); -#endif } else if(dataLen != buffer.len) { diff --git a/pljava-so/src/main/c/type/byte_array.c b/pljava-so/src/main/c/type/byte_array.c index d1763084..91e3103a 100644 --- a/pljava-so/src/main/c/type/byte_array.c +++ b/pljava-so/src/main/c/type/byte_array.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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: + * Tada AB + * Chapman Flack */ #include "pljava/Exception.h" #include "pljava/type/Type_priv.h" @@ -40,11 +44,7 @@ static Datum _byte_array_coerceObject(Type self, jobject byteArray) int32 byteaSize = length + VARHDRSZ; bytes = (bytea*)palloc(byteaSize); -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(bytes) = byteaSize; -#else SET_VARSIZE(bytes, byteaSize); -#endif JNI_getByteArrayRegion((jbyteArray)byteArray, 0, length, (jbyte*)VARDATA(bytes)); } else if(JNI_isInstanceOf(byteArray, s_BlobValue_class)) @@ -55,11 +55,7 @@ static Datum _byte_array_coerceObject(Type self, jobject byteArray) byteaSize = (int32)(length + VARHDRSZ); bytes = (bytea*)palloc(byteaSize); -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(bytes) = byteaSize; -#else SET_VARSIZE(bytes, byteaSize); -#endif byteBuffer = JNI_newDirectByteBuffer((void*)VARDATA(bytes), length); if(byteBuffer != 0) diff --git a/pljava-so/src/main/include/pljava/Backend.h b/pljava-so/src/main/include/pljava/Backend.h index 16c29cdb..e0e8ca30 100644 --- a/pljava-so/src/main/include/pljava/Backend.h +++ b/pljava-so/src/main/include/pljava/Backend.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -53,13 +53,7 @@ void Backend_warnJEP411(bool isCommit); #error The macro PG_GETCONFIGOPTION needs to be renamed. #endif -#if PG_VERSION_NUM >= 90100 #define PG_GETCONFIGOPTION(key) GetConfigOption(key, false, true) -#elif PG_VERSION_NUM >= 90000 -#define PG_GETCONFIGOPTION(key) GetConfigOption(key, true) -#else -#define PG_GETCONFIGOPTION(key) GetConfigOption(key) -#endif #ifdef __cplusplus } diff --git a/pljava-so/src/main/include/pljava/Exception.h b/pljava-so/src/main/include/pljava/Exception.h index a2edad91..d524dd06 100644 --- a/pljava-so/src/main/include/pljava/Exception.h +++ b/pljava-so/src/main/include/pljava/Exception.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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,14 +17,6 @@ #include "pljava/PgObject.h" -#if PG_VERSION_NUM < 90500 -#ifdef __GNUC__ -#define pg_attribute_printf(f,a) __attribute__((format(printf, f, a))) -#else -#define pg_attribute_printf(f,a) -#endif -#endif - #ifdef __cplusplus extern "C" { #endif diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index 8da93ff6..6a2e298c 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -37,6 +37,7 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); #include #include #include +#include /* * AssertVariableIsOfType appeared in PG9.3. Can test for the macro directly. @@ -65,23 +66,6 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); ALLOCSET_SMALL_MINSIZE, ALLOCSET_SMALL_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE #endif -/* - * GETSTRUCT require "access/htup_details.h" to be included in PG9.3 - */ -#if PG_VERSION_NUM >= 90300 -#include "access/htup_details.h" -#endif - -/* - * PG_*_{MIN,MAX} macros (which happen, conveniently, to match Java's datatypes - * (the signed ones, anyway), appear in PG 9.5. Could test for them directly, - * but explicit version conditionals may be easier to find and prune when the - * back-compatibility horizon passes them. Here are only the ones being used. - */ -#if PG_VERSION_NUM < 90500 -#define PG_INT32_MAX (0x7FFFFFFF) -#endif - /* * This symbol was spelled without the underscores prior to PG 14. */ @@ -118,28 +102,10 @@ extern MemoryContext JavaMemoryContext; * stack_base_ptr was static before PG 8.1. By executive decision, PL/Java now * has 8.1 as a back compatibility limit; no empty #defines here for earlier. */ -#if 90104<=PG_VERSION_NUM || \ - 90008<=PG_VERSION_NUM && PG_VERSION_NUM<90100 || \ - 80412<=PG_VERSION_NUM && PG_VERSION_NUM<90000 || \ - 80319<=PG_VERSION_NUM && PG_VERSION_NUM<80400 #define NEED_MISCADMIN_FOR_STACK_BASE #define _STACK_BASE_TYPE pg_stack_base_t #define _STACK_BASE_SET saveStackBasePtr = set_stack_base() #define _STACK_BASE_RESTORE restore_stack_base(saveStackBasePtr) -#else -extern -#if PG_VERSION_NUM < 80300 -DLLIMPORT -#else -PGDLLIMPORT -#endif -char* stack_base_ptr; -#define _STACK_BASE_TYPE char* -#define _STACK_BASE_SET \ - saveStackBasePtr = stack_base_ptr; \ - stack_base_ptr = (char*)&saveMainThreadId -#define _STACK_BASE_RESTORE stack_base_ptr = saveStackBasePtr -#endif #define STACK_BASE_VARS \ void* saveMainThreadId = 0; \ diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java index 9151ad10..98ac1993 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 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 @@ -53,6 +53,9 @@ public class SPI public static final int OK_REL_REGISTER = 15; public static final int OK_REL_UNREGISTER = 16; public static final int OK_TD_REGISTER = 17; + public static final int OK_MERGE = 18; + + public static final int OPT_NONATOMIC = 1 << 0; /** * Execute a command using the internal SPI_exec function. diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index a759a20b..1c201a21 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -62,7 +62,7 @@ versions 4.3.0 or later are recommended in order to avoid a ## PostgreSQL -The PL/Java 1.6 series does not commit to support PostgreSQL earlier than 9.5. +The PL/Java 1.6 series does not support PostgreSQL earlier than 9.5. More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. From 46a41cd18c95e520ed042916c691fa2b125d735b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Aug 2023 12:02:08 -0400 Subject: [PATCH 0975/1087] Run another example as a regression test Going through all the examples for version-conditional bits showed at least one more deserving inclusion as a regression test. Also tidy a couple examples to use out={...} instead of some throwaway composite type. And put a query in more of the traditional uppercase-keywords form. --- .../annotation/RecordParameterDefaults.java | 19 ++---- .../annotation/UnicodeRoundTripTest.java | 65 +++++++++---------- .../example/annotation/XMLRenderedTypes.java | 15 ++++- 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index 34e1aeb7..291eb990 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -19,7 +19,6 @@ import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.annotation.Function; -import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; /** @@ -29,17 +28,6 @@ *

    * Also tests the proper DDR generation of defaults for such parameters. */ -@SQLAction( - provides = "paramtypeinfo type", // created in Triggers.java - install = { - "CREATE TYPE javatest.paramtypeinfo AS (" + - " name text, pgtypename text, javaclass text, tostring text" + - ")" - }, - remove = { - "DROP TYPE javatest.paramtypeinfo" - } -) public class RecordParameterDefaults implements ResultSetProvider { /** @@ -59,10 +47,11 @@ public class RecordParameterDefaults implements ResultSetProvider * */ @Function( - requires = "paramtypeinfo type", schema = "javatest", - type = "javatest.paramtypeinfo" - ) + out = { + "name text", "pgtypename text", "javaclass text", "tostring text" + } + ) public static ResultSetProvider paramDefaultsRecord( @SQLType(defaultValue={})ResultSet params) throws SQLException diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 6b06d4d9..c317dab2 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -48,50 +48,43 @@ @SQLAction(requires="unicodetest fn", implementor="postgresql_unicodetest", install= -" with " + -" usable_codepoints ( cp ) as ( " + -" select generate_series(1,x'd7ff'::int) " + -" union all " + -" select generate_series(x'e000'::int,x'10ffff'::int) " + +" WITH " + +" usable_codepoints ( cp ) AS ( " + +" SELECT generate_series(1,x'd7ff'::int) " + +" UNION ALL " + +" SELECT generate_series(x'e000'::int,x'10ffff'::int) " + " ), " + -" test_inputs ( groupnum, cparray, s ) as ( " + -" select " + -" cp / 1024 as groupnum, " + -" array_agg(cp order by cp), string_agg(chr(cp), '' order by cp) " + -" from usable_codepoints " + -" group by groupnum " + +" test_inputs ( groupnum, cparray, s ) AS ( " + +" SELECT " + +" cp / 1024 AS groupnum, " + +" array_agg(cp ORDER BY cp), string_agg(chr(cp), '' ORDER BY cp) " + +" FROM usable_codepoints " + +" GROUP BY groupnum " + " ), " + -" test_outputs as ( " + -" select groupnum, cparray, s, unicodetest(s, cparray) as roundtrip " + -" from test_inputs " + +" test_outputs AS ( " + +" SELECT groupnum, cparray, s, unicodetest(s, cparray) AS roundtrip " + +" FROM test_inputs " + " ), " + -" test_failures as ( " + -" select * " + -" from test_outputs " + -" where " + -" cparray != (roundtrip).cparray or s != (roundtrip).s " + -" or not (roundtrip).matched " + +" test_failures AS ( " + +" SELECT * " + +" FROM test_outputs " + +" WHERE " + +" cparray != (roundtrip).cparray OR s != (roundtrip).s " + +" OR NOT (roundtrip).matched " + " ), " + -" test_summary ( n_failing_groups, first_failing_group ) as ( " + -" select count(*), min(groupnum) from test_failures " + +" test_summary ( n_failing_groups, first_failing_group ) AS ( " + +" SELECT count(*), min(groupnum) FROM test_failures " + " ) " + -" select " + -" case when n_failing_groups > 0 then " + +" SELECT " + +" CASE WHEN n_failing_groups > 0 THEN " + " javatest.logmessage('WARNING', n_failing_groups || " + " ' 1k codepoint ranges had mismatches, first is block starting 0x' || " + " to_hex(1024 * first_failing_group)) " + -" else " + +" ELSE " + " javatest.logmessage('INFO', " + " 'all Unicode codepoint ranges roundtripped successfully.') " + -" end " + -" from test_summary" -) -@SQLAction( - install= - "CREATE TYPE unicodetestrow AS " + - "(matched boolean, cparray integer[], s text)", - remove="DROP TYPE unicodetestrow", - provides="unicodetestrow type" +" END " + +" FROM test_summary" ) public class UnicodeRoundTripTest { /** @@ -110,8 +103,8 @@ public class UnicodeRoundTripTest { * @param rs OUT (matched, cparray, s) as described above * @return true to indicate the OUT tuple is not null */ - @Function(type="unicodetestrow", - requires="unicodetestrow type", provides="unicodetest fn") + @Function(out={"matched boolean", "cparray integer[]", "s text"}, + provides="unicodetest fn") public static boolean unicodetest(String s, int[] ints, ResultSet rs) throws SQLException { boolean ok = true; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java index 5cd21326..812233d1 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java @@ -28,9 +28,22 @@ * Everything mentioning the type XML here needs a conditional implementor tag * in case of being loaded into a PostgreSQL instance built without that type. */ +@SQLAction(implementor="postgresql_xml", requires="pgNodeTreeAsXML", install= +"WITH" + +" a(t) AS (SELECT adbin FROM pg_catalog.pg_attrdef LIMIT 1)" + +" SELECT" + +" CASE WHEN pgNodeTreeAsXML(t) IS DOCUMENT" + +" THEN javatest.logmessage('INFO', 'pgNodeTreeAsXML ok')" + +" ELSE javatest.logmessage('WARNING', 'pgNodeTreeAsXML ng')" + +" END" + +" FROM a" +) public class XMLRenderedTypes { - @Function(schema="javatest", implementor="postgresql_xml") + @Function( + schema="javatest", implementor="postgresql_xml", + provides="pgNodeTreeAsXML" + ) public static SQLXML pgNodeTreeAsXML(@SQLType("pg_node_tree") SQLXML pgt) throws SQLException { From 57e9609b538c611089c6f2d29e0074ea56560d35 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Aug 2023 18:02:55 -0400 Subject: [PATCH 0976/1087] Expose the lax() method in the example code A couple of the supplied examples strive to be usable for real work, so this new method should be available there too. --- .../org/postgresql/pljava/example/annotation/PassXML.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 3c060575..f2a70658 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 @@ -809,7 +809,9 @@ T applyAdjustments(ResultSet adjust, T axp) for ( int i = 1; i <= n; ++i ) { String k = rsmd.getColumnLabel(i); - if ( "allowDTD".equalsIgnoreCase(k) ) + if ( "lax".equalsIgnoreCase(k) ) + axp.lax(adjust.getBoolean(i)); + else if ( "allowDTD".equalsIgnoreCase(k) ) axp.allowDTD(adjust.getBoolean(i)); else if ( "externalGeneralEntities".equalsIgnoreCase(k) ) axp.externalGeneralEntities(adjust.getBoolean(i)); From e25b7c40bec6f0caa58f9267a76ccb689b5fff96 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Aug 2023 18:32:00 -0400 Subject: [PATCH 0977/1087] Clean up a bogus javadoc comment That was one I must have written on far too little coffee. Hardly any of it bore any connection to reality. Ah. In my defense, it had been more coherent once, but did not get updated with c9f6a20. --- .../postgresql/pljava/internal/VarlenaWrapper.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 89594488..44432000 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -1182,12 +1182,11 @@ public Thread newThread(Runnable r) * wraps a {@code ByteBuffer} and the {@link Output.State Output.State} * that protects it. *

    - * {@code BufferWrapper} installs itself as the inherited - * {@code m_state} field, so {@code ByteBufferInputStream}'s methods - * synchronize on it rather than the {@code State} object, for no - * interference with the writing thread. The {@code pin} and - * {@code unpin} methods, of course, forward to those of the - * native state object. + * {@code BufferWrapper} installs itself as the + * {@code ByteBufferInputStream}'s lock object, so its methods + * synchronize on this rather than anything that would interfere with + * the writing thread. The {@code pin} and {@code unpin} methods, + * of course, forward to those of the native state object. */ static class BufferWrapper extends ByteBufferInputStream From 8353f6fe8eef1b55b9569200c0b3d19303afc346 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Aug 2023 14:33:05 -0400 Subject: [PATCH 0978/1087] Ax ancient Java version in PL/Java 1.6.x docs As PL/Java 1.6.x requires Java >= 9, it makes little sense for a lingering javadoc comment to say you need Java 1.6 for something. --- .../postgresql/pljava/annotation/package-info.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 de7309e5..ca5af21c 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-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2023 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 @@ -33,11 +33,10 @@ *

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