From ece1b9a82fab41c21474afad09ad94595b1066a1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 13:28:06 -0400 Subject: [PATCH 1/8] Examples to capture current Blob behavior These examples preserve a snapshot of how PL/Java's existing, ancient, Blob support can (and can't) be used. --- .../pljava/example/annotation/LegacyBlob.java | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java new file mode 100644 index 000000000..7c6958d8c --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2026 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.IOException; + +import java.sql.Blob; +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.SQLData; +import java.sql.SQLException; +import java.sql.SQLInput; +import java.sql.SQLOutput; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +import java.util.Arrays; + +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.MappedUDT; +import org.postgresql.pljava.annotation.SQLType; + +/** + * Captures how PL/Java's Blob implementation has and hasn't worked. + */ +public class LegacyBlob +{ + private LegacyBlob() { } // do not instantiate + + static final byte[] BYTES = { (byte)1, (byte)2, (byte)3, (byte)4 }; + + static Connection connect() throws SQLException + { + return getConnection("jdbc:default:connection"); + } + + /** + * Exercises getBinaryStream on ResultSet, returning true for success. + */ + @Function(schema = "javatest") + public static boolean resultSetGetBinaryStream() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement("SELECT CAST ( ? AS bytea )"); + ) + { + ps.setBytes(1, BYTES); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + try + ( + InputStream is = rs.getBinaryStream(1); + ) + { + return Arrays.equals(BYTES, is.readAllBytes()); + } + } + } + } + + /** + * Exercises Blob in a composite return value, returning two bytea columns + * that should be equal; also tests getBlob. + */ + @Function(schema = "javatest", out = { "c1 bytea", "c2 bytea" }) + public static boolean compositeReturnBlob(ResultSet toReturn) + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement("SELECT CAST ( ? AS bytea )"); + ) + { + ps.setBytes(1, BYTES); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + Blob b = rs.getBlob(1); + toReturn.updateBytes("c1", BYTES); + toReturn.updateBlob("c2", b); + return true; + } + } + } + + /* + * Exercises Blob as a scalar return value. + @Function(schema = "javatest", type="bytea") + public static Blob scalarReturnBlob() throws SQLException, IOException + { + Without type="bytea", rejected at compile time (no compile-time mapping) + With type="bytea", rejected at validation time (no run-time mapping) + } + */ + + /** + * Exercises setting a composite return column using updateBinaryStream, + * returning two bytea columns that should be equal. + */ + @Function(schema = "javatest", out = { "c1 bytea", "c2 bytea" }) + public static boolean compositeReturnBinaryStream(ResultSet toReturn) + throws SQLException, IOException + { + toReturn.updateBytes("c1", BYTES); + // toReturn.updateBinaryStream("c2", new ByteArrayInputStream(BYTES)); + // toReturn.updateBinaryStream(2, new ByteArrayInputStream(BYTES)); + toReturn.updateBinaryStream( + 2, new ByteArrayInputStream(BYTES), BYTES.length); + return true; + } + + /** + * Exercises setBinaryStream on PreparedStatement, + * returning true for success. + */ + @Function(schema = "javatest") + public static boolean preparedStmtSetBinaryStream() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement( + "SELECT a = b FROM (SELECT" + + " CAST ( ? AS bytea ) AS a, CAST ( ? AS bytea ) AS b)"); + ) + { + ps.setBytes(1, BYTES); + // ps.setBinaryStream(2, new ByteArrayInputStream(BYTES)); + ps.setBinaryStream( + 2, new ByteArrayInputStream(BYTES), BYTES.length); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + return rs.getBoolean(1); + } + } + } + + /** + * Exercises setBlob on PreparedStatement, returning true for success. + */ + @Function(schema = "javatest") + public static boolean preparedStmtSetBlob() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps1 = + c.prepareStatement("SELECT CAST ( ? AS bytea )"); + PreparedStatement ps2 = + c.prepareStatement( + "SELECT a = b FROM (SELECT" + + " CAST ( ? AS bytea ) AS a, CAST ( ? AS bytea ) AS b)"); + ) + { + ps1.setBytes(1, BYTES); + + try + ( + ResultSet rs = ps1.executeQuery(); + ) + { + rs.next(); + Blob b = rs.getBlob(1); + ps2.setBytes(1, BYTES); + ps2.setBlob(2, b); + } + + try + ( + ResultSet rs = ps2.executeQuery(); + ) + { + rs.next(); + return rs.getBoolean(1); + } + } + } + + @MappedUDT(schema = "javatest", structure = { "b bytea" }) + public static class BlobbedBlob implements SQLData + { + private String name; + private Blob blob; + + @Override + public String getSQLTypeName() + { + return name; + } + + @Override + public void readSQL(SQLInput stream, String typeName) + throws SQLException + { + name = typeName; + blob = stream.readBlob(); + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException + { + stream.writeBlob(blob); + } + } + + @MappedUDT(schema = "javatest", structure = { "b bytea" }) + public static class StreamedBlob implements SQLData + { + private String name; + private byte[] bytes; + + @Override + public String getSQLTypeName() + { + return name; + } + + @Override + public void readSQL(SQLInput stream, String typeName) + throws SQLException + { + name = typeName; + try + { + bytes = stream.readBinaryStream().readAllBytes(); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException + { + stream.writeBinaryStream(new ByteArrayInputStream(bytes)); + } + } +} From f921642ea0b79c4cfb4b65b151b7ded4638e7a71 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 13:28:17 -0400 Subject: [PATCH 2/8] Examples to capture current Clob behavior These examples preserve a snapshot of how PL/Java's existing, ancient, Clob support can (and can't, mostly can't) be used. --- .../pljava/example/annotation/LegacyClob.java | 426 ++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java new file mode 100644 index 000000000..3b86a6540 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2026 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.IOException; + +import java.nio.CharBuffer; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +import java.sql.Clob; +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.SQLData; +import java.sql.SQLException; +import java.sql.SQLInput; +import java.sql.SQLOutput; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +import java.util.Arrays; + +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.MappedUDT; +import org.postgresql.pljava.annotation.SQLType; + +/** + * Captures how PL/Java's Clob implementation has and hasn't (hasn't, mostly) + * worked. + */ +public class LegacyClob +{ + private LegacyClob() { } // do not instantiate + + static final String CHARS = "ABCD"; + static final byte[] BYTES = CHARS.getBytes(US_ASCII); + + static Connection connect() throws SQLException + { + return getConnection("jdbc:default:connection"); + } + + static String readAllAsString(Reader r) throws IOException + { + // Java >= 10: can use r.transferTo(a StringWriter) + // Java >= 25: can use r.readAllAsString() + CharBuffer cb = CharBuffer.allocate(128); + StringBuilder sb = new StringBuilder(); + while ( -1 != r.read(cb) ) + { + sb.append(cb.flip()); + cb.clear(); + } + return sb.toString(); + } + + static String readAsciiString(InputStream is) throws IOException + { + return new String(is.readAllBytes(), US_ASCII); + } + + // works + /** + * Exercises getCharacterStream on ResultSet, returning true for success. + */ + @Function(schema = "javatest") + public static boolean resultSetGetCharacterStream() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement("SELECT CAST ( ? AS text )"); + ) + { + ps.setString(1, CHARS); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + try + ( + Reader r = rs.getCharacterStream(1); + ) + { + return CHARS.equals(readAllAsString(r)); + } + } + } + } + + // works + /** + * Exercises getAsciiStream on ResultSet, returning true for success. + */ + @Function(schema = "javatest") + public static boolean resultSetGetAsciiStream() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement("SELECT CAST ( ? AS text )"); + ) + { + ps.setString(1, CHARS); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + try + ( + InputStream is = rs.getAsciiStream(1); + ) + { + return CHARS.equals(readAsciiString(is)); + } + } + } + } + + // SQLException: Cannot derive a value of class java.lang.String from + // an object of class org.postgresql.pljava.jdbc.ClobValue + /** + * Exercises Clob in a composite return value, returning two text columns + * that should be equal; also tests getClob. + */ + @Function(schema = "javatest", out = { "c1 text", "c2 text" }) + public static boolean compositeReturnClob(ResultSet toReturn) + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement("SELECT CAST ( ? AS text )"); + ) + { + ps.setString(1, CHARS); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + Clob clob = rs.getClob(1); + toReturn.updateString("c1", CHARS); + toReturn.updateClob("c2", clob); + return true; + } + } + } + + /* + * Exercises Clob as a scalar return value. + @Function(schema = "javatest", type = "text") + public static Clob scalarReturnClob() throws SQLException, IOException + { + Without type="text", rejected at compile time (no compile-time mapping) + With type="text", rejected at validation time (no run-time mapping) + } + */ + + // SQLException: Cannot derive a value of class java.lang.String from + // an object of class org.postgresql.pljava.jdbc.ClobValue + /** + * Exercises setting a composite return column using updateCharacterStream, + * returning two text columns that should be equal. + */ + @Function(schema = "javatest", out = { "c1 text", "c2 text" }) + public static boolean compositeReturnCharacterStream(ResultSet toReturn) + throws SQLException, IOException + { + toReturn.updateString("c1", CHARS); + // toReturn.updateCharacterStream("c2", new StringReader(CHARS)); + // toReturn.updateCharacterStream(2, new StringReader(CHARS)); + toReturn.updateCharacterStream( + 2, new StringReader(CHARS), CHARS.length()); + return true; + } + + /** + * Exercises setting a composite return column using updateAsciiStream, + * returning two text columns that should be equal. + */ + @Function(schema = "javatest", out = { "c1 text", "c2 text" }) + public static boolean compositeReturnAsciiStream(ResultSet toReturn) + throws SQLException, IOException + { + toReturn.updateString("c1", CHARS); + + InputStream is = new ByteArrayInputStream(BYTES); + + // toReturn.updateAsciiStream("c2", is); + // toReturn.updateAsciiStream(2, is); + toReturn.updateAsciiStream(2, is, BYTES.length); + return true; + } + + // XXX returns false; Clob probably rendered by Object.toString + /** + * Exercises setCharacterStream on PreparedStatement, + * returning true for success. + */ + @Function(schema = "javatest") + public static boolean preparedStmtSetCharacterStream() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement( + "SELECT a = b FROM (SELECT" + + " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)"); + ) + { + ps.setString(1, CHARS); + // ps.setCharacterStream(2, new StringReader(CHARS)); + ps.setCharacterStream( + 2, new StringReader(CHARS), CHARS.length()); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + return rs.getBoolean(1); + } + } + } + + // XXX returns false; Clob probably rendered by Object.toString + /** + * Exercises setAsciiStream on PreparedStatement, + * returning true for success. + */ + @Function(schema = "javatest") + public static boolean preparedStmtSetAsciiStream() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps = + c.prepareStatement( + "SELECT a = b FROM (SELECT" + + " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)"); + ) + { + ps.setString(1, CHARS); + // ps.setAsciiStream(2, new ByteArrayInputStream(BYTES)); + ps.setAsciiStream( + 2, new ByteArrayInputStream(BYTES), BYTES.length); + try + ( + ResultSet rs = ps.executeQuery(); + ) + { + rs.next(); + return rs.getBoolean(1); + } + } + } + + // XXX returns false; Clob probably rendered by Object.toString + /** + * Exercises setClob on PreparedStatement, returning true for success. + */ + @Function(schema = "javatest") + public static boolean preparedStmtSetClob() + throws SQLException, IOException + { + try + ( + Connection c = connect(); + PreparedStatement ps1 = + c.prepareStatement("SELECT CAST ( ? AS text )"); + PreparedStatement ps2 = + c.prepareStatement( + "SELECT a = b FROM (SELECT" + + " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)"); + ) + { + ps1.setString(1, CHARS); + + try + ( + ResultSet rs = ps1.executeQuery(); + ) + { + rs.next(); + Clob clob = rs.getClob(1); + ps2.setString(1, CHARS); + ps2.setClob(2, clob); + } + + try + ( + ResultSet rs = ps2.executeQuery(); + ) + { + rs.next(); + return rs.getBoolean(1); + } + } + } + + // XXX writeClob produces Object.toString of the Clob instance + @MappedUDT(schema = "javatest", structure = { "t text" }) + public static class ClobbedClob implements SQLData + { + private String name; + private Clob clob; + + @Override + public String getSQLTypeName() + { + return name; + } + + @Override + public void readSQL(SQLInput stream, String typeName) + throws SQLException + { + name = typeName; + clob = stream.readClob(); + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException + { + stream.writeClob(clob); + } + } + + // XXX writeCharacterStream produces Object.toString of the Clob instance + @MappedUDT(schema = "javatest", structure = { "b text" }) + public static class StreamedClob implements SQLData + { + private String name; + private String chars; + + @Override + public String getSQLTypeName() + { + return name; + } + + @Override + public void readSQL(SQLInput stream, String typeName) + throws SQLException + { + name = typeName; + try + { + chars = readAllAsString(stream.readCharacterStream()); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException + { + stream.writeCharacterStream(new StringReader(chars)); + } + } + + // OutOfMemoryError: Requested array size exceeds VM limit + @MappedUDT(schema = "javatest", structure = { "b text" }) + public static class AsciiedClob implements SQLData + { + private String name; + private String chars; + + @Override + public String getSQLTypeName() + { + return name; + } + + @Override + public void readSQL(SQLInput stream, String typeName) + throws SQLException + { + name = typeName; + try + { + chars = readAsciiString(stream.readAsciiStream()); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException + { + stream.writeAsciiStream( + new ByteArrayInputStream(chars.getBytes(US_ASCII))); + } + } +} From ed7b7319f617baa3def164a13a9f74dbd278589f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 13:28:30 -0400 Subject: [PATCH 3/8] Eliminate BlobValue and use existing Java classes BlobValue was implemented as a subclass of InputStream, which cannot be compiled under Java 26 because JDBC 4.5 makes Blob AutoCloseable with incompatible exception typing for close(). It was a quite incomplete implementation anyway: limited to 2GB and with several methods unimplemented. The offline SerialBlob found in the javax.sql.rowset.serial package is also limited to 2GB and with some methods unusable (at least when instantiated from a byte[], as here), but it is more complete than BlobValue. The C code was depending on an internal getContents method of BlobValue, Now it uses the documented interface methods, so should be able to use objects of any class that (correctly) implements Blob (provided, still, that the length doesn't exceed 2GB). This is not an effort to improve the java.sql.Blob support significantly, but just to make it no worse and also compatible with Java 26. This implementation remains essentially a veneer that doesn't do anything better than using the {get,update,set,read,write}Bytes methods, and probably has never seen much use for exactly that reason. --- pljava-so/src/main/c/type/byte_array.c | 64 ++-- pljava/src/main/java/module-info.java | 3 +- .../org/postgresql/pljava/jdbc/BlobValue.java | 280 ------------------ .../pljava/jdbc/ObjectResultSet.java | 35 ++- .../pljava/jdbc/SPIPreparedStatement.java | 15 +- .../pljava/jdbc/SQLInputFromTuple.java | 13 +- .../pljava/jdbc/SQLOutputToTuple.java | 14 +- .../pljava/jdbc/SingleRowWriter.java | 7 +- 8 files changed, 99 insertions(+), 332 deletions(-) delete mode 100644 pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java diff --git a/pljava-so/src/main/c/type/byte_array.c b/pljava-so/src/main/c/type/byte_array.c index 91e3103a0..d32d59898 100644 --- a/pljava-so/src/main/c/type/byte_array.c +++ b/pljava-so/src/main/c/type/byte_array.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -14,9 +14,9 @@ #include "pljava/type/Type_priv.h" static jclass s_byteArray_class; -static jclass s_BlobValue_class; -static jmethodID s_BlobValue_length; -static jmethodID s_BlobValue_getContents; +static jclass s_Blob_class; +static jmethodID s_Blob_length; +static jmethodID s_Blob_getBytes; /* * byte[] type. Copies data to/from a bytea struct. @@ -35,38 +35,44 @@ static jvalue _byte_array_coerceDatum(Type self, Datum arg) static Datum _byte_array_coerceObject(Type self, jobject byteArray) { bytea* bytes = 0; - if(byteArray == 0) + jlong length; + int32 byteaSize; + + if ( byteArray == 0 ) return 0; - if(JNI_isInstanceOf(byteArray, s_byteArray_class)) + if ( JNI_isInstanceOf(byteArray, s_byteArray_class) ) { - jsize length = JNI_getArrayLength((jarray)byteArray); - int32 byteaSize = length + VARHDRSZ; - - bytes = (bytea*)palloc(byteaSize); - SET_VARSIZE(bytes, byteaSize); - JNI_getByteArrayRegion((jbyteArray)byteArray, 0, length, (jbyte*)VARDATA(bytes)); + length = JNI_getArrayLength((jarray)byteArray); } - else if(JNI_isInstanceOf(byteArray, s_BlobValue_class)) + else if ( JNI_isInstanceOf(byteArray, s_Blob_class)) { - jobject byteBuffer; - int32 byteaSize; - jlong length = JNI_callLongMethod(byteArray, s_BlobValue_length); - - byteaSize = (int32)(length + VARHDRSZ); - bytes = (bytea*)palloc(byteaSize); - SET_VARSIZE(bytes, byteaSize); + length = JNI_callLongMethod(byteArray, s_Blob_length); - byteBuffer = JNI_newDirectByteBuffer((void*)VARDATA(bytes), length); - if(byteBuffer != 0) - JNI_callVoidMethod(byteArray, s_BlobValue_getContents, byteBuffer); - JNI_deleteLocalRef(byteBuffer); + if ( 0 > length || length > PG_INT32_MAX - VARHDRSZ ) + { + ereport(ERROR, ( + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("cannot accommodate reported Blob length %" PRId64, + length) + )); + } + byteArray = + JNI_callObjectMethod(byteArray, s_Blob_getBytes, (jlong)1, length); } else { - Exception_throwIllegalArgument("Not coercable to bytea"); + elog(ERROR, "cannot coerce this Java class to bytea"); } + byteaSize = length + VARHDRSZ; + + bytes = (bytea*)palloc(byteaSize); + SET_VARSIZE(bytes, byteaSize); + + JNI_getByteArrayRegion( + (jbyteArray)byteArray, 0, length, (jbyte*)VARDATA(bytes)); + PG_RETURN_BYTEA_P(bytes); } @@ -83,8 +89,10 @@ void byte_array_initialize(void) Type_registerType("byte[]", TypeClass_allocInstance(cls, BYTEAOID)); s_byteArray_class = JNI_newGlobalRef(PgObject_getJavaClass("[B")); - s_BlobValue_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/jdbc/BlobValue")); - s_BlobValue_length = PgObject_getJavaMethod(s_BlobValue_class, "length", "()J"); - s_BlobValue_getContents = PgObject_getJavaMethod(s_BlobValue_class, "getContents", "(Ljava/nio/ByteBuffer;)V"); + s_Blob_class = + JNI_newGlobalRef(PgObject_getJavaClass("java/sql/Blob")); + s_Blob_length = PgObject_getJavaMethod(s_Blob_class, "length", "()J"); + s_Blob_getBytes = + PgObject_getJavaMethod(s_Blob_class, "getBytes", "(JI)[B"); } diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 68923bbe4..4086bda17 100644 --- a/pljava/src/main/java/module-info.java +++ b/pljava/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -17,6 +17,7 @@ { requires java.base; requires java.management; + requires java.sql.rowset; requires org.postgresql.pljava; exports org.postgresql.pljava.mbeans; // bothers me, but only interfaces diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java deleted file mode 100644 index 26e46238e..000000000 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/BlobValue.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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 - * PostgreSQL Global Development Group - * Chapman Flack - */ -package org.postgresql.pljava.jdbc; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Reader; -import java.nio.ByteBuffer; -import java.sql.Blob; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; - -/** - * Implementation of {@link Blob} for the SPI connection. - * @author Thomas Hallgren - */ -public class BlobValue extends InputStream implements Blob -{ - public static int getStreamLength(InputStream value) throws SQLException - { - try - { - value.mark(Integer.MAX_VALUE); - long length = value.skip(Long.MAX_VALUE); - if(length > Integer.MAX_VALUE) - throw new SQLException("stream content too large"); - value.reset(); - return (int)length; - } - catch(IOException e) - { - throw new SQLException(e.getMessage()); - } - } - - private long m_markPos; - private final long m_nBytes; - private final InputStream m_stream; - - private long m_streamPos; - - public BlobValue(byte[] bytes) - { - this(new ByteArrayInputStream(bytes), bytes.length); - } - public BlobValue(InputStream stream, long nBytes) - { - m_stream = stream; - m_nBytes = nBytes; - m_streamPos = 0L; - m_markPos = 0L; - } - - - //*************************************** - // Implementation of java.io.InputStream - //*************************************** - public int available() - throws IOException - { - return m_stream.available(); - } - - public InputStream getBinaryStream() - { - return this; - } - - public byte[] getBytes(long pos, int length) - throws SQLException - { - if(pos < 0L || length < 0) - throw new IllegalArgumentException(); - if(length == 0) - return new byte[0]; - - if(pos + length > m_nBytes) - throw new SQLException("Attempt to read beyond end of Blob data"); - - long skip = pos - m_streamPos; - if(skip < 0) - throw new SQLException("Cannot position Blob stream backwards"); - - try - { - if(skip > 0) - this.skip(skip); - - byte[] buf = new byte[length]; - this.read(buf); - return buf; - } - catch(IOException e) - { - throw new SQLException("Error reading Blob data: " + e.getMessage()); - } - } - - /** - * Called from within... - * @param buf a buffer that reflects the internally allocated bytea buffer. - * This size of this buffer will be exactly the size returned by a call to - * {@link #length()}. - * @throws IOException - */ - public void getContents(ByteBuffer buf) - throws IOException - { - int rs = 0; - if(buf.hasArray()) - { - byte[] bytes = buf.array(); - rs = m_stream.read(bytes); - } - else - { - byte[] trBuf = new byte[1024]; - int br; - while((br = m_stream.read(trBuf)) > 0) - { - buf.put(trBuf, 0, br); - rs += br; - } - } - if(rs != m_nBytes) - throw new IOException("Not all bytes could be read"); - m_streamPos += rs; - } - - //*************************************** - // Implementation of java.sql.Blob - //*************************************** - public long length() - { - return m_nBytes; - } - - public synchronized void mark(int readLimit) - { - m_stream.mark(readLimit); - m_markPos = m_streamPos; - } - - public boolean markSupported() - { - return m_stream.markSupported(); - } - - /** - * Not supported. - */ - public long position(Blob pattern, long start) - { - throw new UnsupportedOperationException(); - } - - /** - * Not supported. - */ - public long position(byte[] pattern, long start) - { - throw new UnsupportedOperationException(); - } - - public synchronized int read() - throws IOException - { - int rs = m_stream.read(); - m_streamPos++; - return rs; - } - - public synchronized int read(byte[] b) - throws IOException - { - int rs = m_stream.read(b); - m_streamPos += rs; - return rs; - } - - public synchronized int read(byte[] b, int off, int len) - throws IOException - { - int rs = m_stream.read(b, off, len); - m_streamPos += rs; - return rs; - } - - public synchronized void reset() - throws IOException - { - m_stream.reset(); - m_streamPos = m_markPos; - } - - //************************************************************************* - // Implementation of java.sql.Blob JDK 1.4 methods - // - // Those method are intended to provide a channel to the underlying data - // storage as an alternatvie to the setBinaryStream - // on the preparedStatement and are not implemented by the BlobValue. - // - //************************************************************************* - /** - * In this method is not supported by BlobValue - */ - public OutputStream setBinaryStream(long pos) - { - throw new UnsupportedOperationException(); - } - - /** - * In this method is not supported by BlobValue - */ - public int setBytes(long pos, byte[] bytes) - { - throw new UnsupportedOperationException(); - } - - /** - * In this method is not supported by BlobValue - */ - public int setBytes(long pos, byte[] bytes, int offset, int len) - { - throw new UnsupportedOperationException(); - } - - public synchronized long skip(long nBytes) - throws IOException - { - long skipped = m_stream.skip(nBytes); - m_streamPos += skipped; - return skipped; - } - - /** - * In this method is not supported by BlobValue - */ - public void truncate(long len) - { - throw new UnsupportedOperationException(); - } - - // ************************************************************ - // Non-implementation of JDBC 4 methods. - // ************************************************************ - - public InputStream getBinaryStream(long pos, - long length) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".getBinaryStream( long,long ) not " - + "implemented yet.", - "0A000" ); - } - - public void free() - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".free() not implemented yet.", - "0A000" ); - } -} 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 0393c5d13..14321a7fc 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -20,6 +20,7 @@ import java.sql.Ref; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.SQLNonTransientException; import java.sql.SQLWarning; import java.sql.Time; import java.sql.Timestamp; @@ -28,11 +29,15 @@ import java.util.Calendar; import java.util.Map; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.IOException; import java.io.Reader; import static java.nio.charset.StandardCharsets.US_ASCII; +import javax.sql.rowset.serial.SerialBlob; + /** * Implements most getters in terms of {@link #getValue}, {@link #getNumber}, @@ -133,14 +138,14 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) } /** - * Implemented over {@link #getBlob(int) getBlob}. + * Implemented over {@link #getBytes(int) getBytes}. */ @Override public InputStream getBinaryStream(int columnIndex) throws SQLException { - Blob b = getBlob(columnIndex); - return (b == null) ? null : b.getBinaryStream(); + byte[] bytes = getBytes(columnIndex); + return (bytes == null) ? null : new ByteArrayInputStream(bytes); } /** @@ -151,7 +156,7 @@ public Blob getBlob(int columnIndex) throws SQLException { byte[] bytes = getBytes(columnIndex); - return (bytes == null) ? null : new BlobValue(bytes); + return (bytes == null) ? null : new SerialBlob(bytes); } /** @@ -435,14 +440,28 @@ public void updateBigDecimal(int columnIndex, BigDecimal x) } /** - * Implemented over {@link BlobValue} and - * {@link #updateBlob updateBlob}. + * Implemented over {@link #updateBytes updateBytes}. */ @Override public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { - updateBlob(columnIndex, (Blob) new BlobValue(x, length)); + // Java >= 11: bytes = x.readNBytes(length) + byte[] bytes = new byte[length]; + try + { + int got = x.readNBytes(bytes, 0, length); + if ( got != length || -1 != x.read() ) + { + throw new SQLNonTransientException( + "updateBinaryStream explicit length incorrect", "38000"); + } + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } + updateBytes(columnIndex, bytes); } /** 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 02ccd4070..ed95bb4a6 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -15,6 +15,7 @@ import java.io.InputStream; import java.io.InputStreamReader; +import java.io.IOException; import java.io.Reader; import java.math.BigDecimal; import java.net.URL; @@ -193,9 +194,17 @@ public void setUnicodeStream(int columnIndex, InputStream value, int arg2) throw } @Override - public void setBinaryStream(int columnIndex, InputStream value, int length) throws SQLException + public void setBinaryStream(int columnIndex, InputStream value, int length) + throws SQLException { - setObject(columnIndex, new BlobValue(value, length), Types.BLOB); + try + { + setObject(columnIndex, value.readAllBytes(), Types.VARBINARY); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } } @Override 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 2b99e01ba..a09cac166 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-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -12,6 +12,7 @@ */ package org.postgresql.pljava.jdbc; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; @@ -31,6 +32,8 @@ import java.sql.Time; import java.sql.Timestamp; +import javax.sql.rowset.serial.SerialBlob; + import org.postgresql.pljava.internal.Backend; import org.postgresql.pljava.internal.DualState; import org.postgresql.pljava.internal.TupleDesc; @@ -97,13 +100,13 @@ public BigDecimal readBigDecimal() throws SQLException } /** - * Implemented over {@link #readBlob}. + * Implemented over {@link #readBytes}. */ @Override public InputStream readBinaryStream() throws SQLException { - Blob b = readBlob(); - return (b == null) ? null : b.getBinaryStream(); + byte[] bytes = readBytes(); + return (bytes == null) ? null : new ByteArrayInputStream(bytes); } /** @@ -113,7 +116,7 @@ public InputStream readBinaryStream() throws SQLException public Blob readBlob() throws SQLException { byte[] bytes = readBytes(); - return (bytes == null) ? null : new BlobValue(bytes); + return (bytes == null) ? null : new SerialBlob(bytes); } /** 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 ec2365bb1..b76b8de69 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 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.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.IOException; import java.io.Reader; import java.math.BigDecimal; import java.net.URL; @@ -97,9 +98,14 @@ public void writeBigDecimal(BigDecimal value) throws SQLException public void writeBinaryStream(InputStream value) throws SQLException { - if(!value.markSupported()) - value = new BufferedInputStream(value); - writeBlob(new BlobValue(value, BlobValue.getStreamLength(value))); + try + { + writeBytes(value.readAllBytes()); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } } public void writeBlob(Blob value) 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 99f21341c..ac54fe9d9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 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 @@ -13,6 +13,7 @@ */ package org.postgresql.pljava.jdbc; +import java.sql.Blob; import java.sql.Date; import java.sql.ResultSet; import java.sql.SQLException; @@ -91,8 +92,8 @@ public void updateObject(int columnIndex, Object x) Class c = m_tupleDesc.getColumnClass(columnIndex); TypeBridge.Holder xAlt = TypeBridge.wrap(x); - if(null == xAlt && !c.isInstance(x) - && !(c == byte[].class && (x instanceof BlobValue))) + if ( null == xAlt && !c.isInstance(x) + && !( c == byte[].class && (x instanceof Blob) ) ) { if(Number.class.isAssignableFrom(c)) x = SPIConnection.basicNumericCoercion(c, x); From 95c3a01289e831775fa9fd5360a8437aa365016f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 13:28:51 -0400 Subject: [PATCH 4/8] Eliminate ClobValue and use existing Java classes ClobValue was implemented as a subclass of Reader, which cannot be compiled under Java 26 because JDBC 4.5 makes Clob AutoCloseable with incompatible exception typing for close(). It was a badly incomplete implementation anyway: limited to 2Gchars and with several methods unimplemented. The offline SerialClob found in the javax.sql.rowset.serial package is also limited to 2Gchars and with some methods unusable (at least when instantiated from a char[], as here), but it is more complete than ClobValue. Even the partial work that was done to integrate BlobValue into PL/Java's legacy type system had not been done for ClobValue; compared to the working tests in the LegacyBlob example, several of the corresponding tests in the LegacyClob example did not work. Some still don't, but a few more do work now, thanks to having the AsciiStream and CharacterStream methods no longer going through Clob now at all. That's good enough for now: this is not an effort to improve the java.sql.Clob support significantly, but just to make it no worse and also compatible with Java 26. This implementation remains essentially a veneer that doesn't do anything better than using the {get,update,set,read,write} String methods, and probably has never seen much use for exactly that reason. --- .../pljava/example/annotation/LegacyClob.java | 5 + .../org/postgresql/pljava/jdbc/ClobValue.java | 263 ------------------ .../pljava/jdbc/ObjectResultSet.java | 31 ++- .../pljava/jdbc/SPIDatabaseMetaData.java | 112 +++++++- .../pljava/jdbc/SPIPreparedStatement.java | 13 +- .../pljava/jdbc/SQLInputFromTuple.java | 21 +- .../pljava/jdbc/SQLOutputToTuple.java | 9 +- 7 files changed, 160 insertions(+), 294 deletions(-) delete mode 100644 pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java index 3b86a6540..5bd5bd17e 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java @@ -180,6 +180,7 @@ public static Clob scalarReturnClob() throws SQLException, IOException } */ + // Now works! Formerly: // SQLException: Cannot derive a value of class java.lang.String from // an object of class org.postgresql.pljava.jdbc.ClobValue /** @@ -216,6 +217,7 @@ public static boolean compositeReturnAsciiStream(ResultSet toReturn) return true; } + // Now works! Formerly: // XXX returns false; Clob probably rendered by Object.toString /** * Exercises setCharacterStream on PreparedStatement, @@ -249,6 +251,7 @@ public static boolean preparedStmtSetCharacterStream() } } + // Now works! Formerly: // XXX returns false; Clob probably rendered by Object.toString /** * Exercises setAsciiStream on PreparedStatement, @@ -353,6 +356,7 @@ public void writeSQL(SQLOutput stream) throws SQLException } } + // Now works! Formerly: // XXX writeCharacterStream produces Object.toString of the Clob instance @MappedUDT(schema = "javatest", structure = { "b text" }) public static class StreamedClob implements SQLData @@ -388,6 +392,7 @@ public void writeSQL(SQLOutput stream) throws SQLException } } + // Now works! Formerly: // OutOfMemoryError: Requested array size exceeds VM limit @MappedUDT(schema = "javatest", structure = { "b text" }) public static class AsciiedClob implements SQLData diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java deleted file mode 100644 index 9e594967d..000000000 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ClobValue.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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 - * PostgreSQL Global Development Group - * Chapman Flack - */ -package org.postgresql.pljava.jdbc; - -import java.io.BufferedInputStream; -import java.io.CharConversionException; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.Writer; -import java.sql.Clob; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; - -/** - * Implementation of {@link Clob} for the SPI connection. - * @author Thomas Hallgren - */ -public class ClobValue extends Reader implements Clob -{ - public static int getReaderLength(Reader value) throws SQLException - { - try - { - value.mark(Integer.MAX_VALUE); - long length = value.skip(Long.MAX_VALUE); - if(length > Integer.MAX_VALUE) - throw new SQLException("stream content too large"); - value.reset(); - return (int)length; - } - catch(IOException e) - { - throw new SQLException(e.getMessage()); - } - } - - private long m_markPos; - - private final long m_nChars; - - private final Reader m_reader; - - private long m_readerPos; - - public ClobValue(Reader reader, long nChars) - { - m_reader = reader; - m_nChars = nChars; - m_readerPos = 0L; - m_markPos = 0L; - } - - public ClobValue(String value) - { - this(new StringReader(value), value.length()); - } - - public void close() throws IOException - { - m_reader.close(); - m_readerPos = 0; - m_markPos = 0; - } - - public InputStream getAsciiStream() - { - return new BufferedInputStream(new InputStream() - { - public int read() throws IOException - { - int nextChar = ClobValue.this.read(); - if(nextChar > 127) - throw new CharConversionException( - "Non ascii character in Clob data"); - return nextChar; - } - }); - } - - public Reader getCharacterStream() - { - return this; - } - - public String getSubString(long pos, int length) throws SQLException - { - if(pos < 0L || length < 0) - throw new IllegalArgumentException(); - if(length == 0) - return ""; - - if(pos + length > m_nChars) - throw new SQLException("Attempt to read beyond end of Clob data"); - - long skip = pos - m_readerPos; - if(skip < 0) - throw new SQLException("Cannot position Clob stream backwards"); - - try - { - if(skip > 0) - this.skip(skip); - - char[] buf = new char[length]; - int nr = this.read(buf); - if(nr < length) - throw new SQLException("Clob data read not fulfilled"); - return new String(buf); - } - catch(IOException e) - { - throw new SQLException("Error reading Blob data: " + e.getMessage()); - } - } - - public long length() - { - return m_nChars; - } - - public synchronized void mark(int readLimit) throws IOException - { - m_reader.mark(readLimit); - m_markPos = m_readerPos; - } - - public boolean markSupported() - { - return m_reader.markSupported(); - } - - /** - * Not supported. - */ - public long position(Clob pattern, long start) - { - throw new UnsupportedOperationException(); - } - - /** - * In this method is not supported by ClobValue - */ - public long position(String pattern, long start) - { - throw new UnsupportedOperationException(); - } - - public synchronized int read() throws IOException - { - int rs = m_reader.read(); - m_readerPos++; - return rs; - } - - public synchronized int read(char[] b) throws IOException - { - int rs = m_reader.read(b); - m_readerPos += rs; - return rs; - } - - public synchronized int read(char[] b, int off, int len) throws IOException - { - int rs = m_reader.read(b, off, len); - m_readerPos += rs; - return rs; - } - - public synchronized boolean ready() throws IOException - { - return m_reader.ready(); - } - - public synchronized void reset() throws IOException - { - m_reader.reset(); - m_readerPos = m_markPos; - } - - /** - * In this method is not supported by ClobValue - */ - public OutputStream setAsciiStream(long pos) - { - throw new UnsupportedOperationException(); - } - - /** - * In this method is not supported by ClobValue - */ - public Writer setCharacterStream(long pos) - { - throw new UnsupportedOperationException(); - } - - /** - * In this method is not supported by ClobValue - */ - public int setString(long pos, String str) - { - throw new UnsupportedOperationException(); - } - - /** - * In this method is not supported by ClobValue - */ - public int setString(long pos, String str, int offset, int len) - { - throw new UnsupportedOperationException(); - } - - public synchronized long skip(long nBytes) throws IOException - { - long skipped = m_reader.skip(nBytes); - m_readerPos += skipped; - return skipped; - } - - /** - * In this method is not supported by ClobValue - */ - public void truncate(long len) - { - throw new UnsupportedOperationException(); - } - - //************************************************************ - // Non-implementation of JDBC 4 methods. - //************************************************************ - - public Reader getCharacterStream(long pos, - long length) - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".getCharacterStream( long,long ) not implemented yet.", - "0A000" ); - } - - public void free() - throws SQLException - { - throw new SQLFeatureNotSupportedException - ( this.getClass() - + ".free() not implemented yet.", - "0A000" ); - } -} 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 14321a7fc..dfae4794a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/ObjectResultSet.java @@ -34,10 +34,13 @@ import java.io.InputStreamReader; import java.io.IOException; import java.io.Reader; +import java.io.StringReader; import static java.nio.charset.StandardCharsets.US_ASCII; import javax.sql.rowset.serial.SerialBlob; +import javax.sql.rowset.serial.SerialClob; +import static org.postgresql.pljava.jdbc.SPIDatabaseMetaData.readNCharsAsString; /** * Implements most getters in terms of {@link #getValue}, {@link #getNumber}, @@ -107,14 +110,16 @@ public Array getArray(int columnIndex) } /** - * Implemented over {@link #getClob(int) getClob}. + * Implemented over {@link #getString(int) getString}. */ @Override public InputStream getAsciiStream(int columnIndex) throws SQLException { - Clob c = getClob(columnIndex); - return (c == null) ? null : c.getAsciiStream(); + String s = getString(columnIndex); + if ( null == s ) + return null; + return new ByteArrayInputStream(s.getBytes(US_ASCII)); } /** @@ -192,14 +197,14 @@ public byte[] getBytes(int columnIndex) } /** - * Implemented over {@link #getClob(int) getClob}. + * Implemented over {@link #getString(int) getString}. */ @Override public Reader getCharacterStream(int columnIndex) throws SQLException { - Clob c = getClob(columnIndex); - return (c == null) ? null : c.getCharacterStream(); + String s = getString(columnIndex); + return (s == null) ? null : new StringReader(s); } /** @@ -210,7 +215,7 @@ public Clob getClob(int columnIndex) throws SQLException { String str = getString(columnIndex); - return (str == null) ? null : new ClobValue(str); + return (str == null) ? null : new SerialClob(str.toCharArray()); } /** @@ -418,15 +423,14 @@ public void updateArray(int columnIndex, Array x) throws SQLException } /** - * Implemented over {@link ClobValue} and - * {@link #updateObject updateObject}. + * Implemented over {@link #updateObject updateObject}. */ @Override public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { - updateObject(columnIndex, - new ClobValue(new InputStreamReader(x, US_ASCII), length)); + updateObject(columnIndex, null == x ? null : + readNCharsAsString(new InputStreamReader(x, US_ASCII), length)); } /** @@ -505,14 +509,13 @@ public void updateBytes(int columnIndex, byte[] x) } /** - * Implemented over {@link ClobValue} and - * {@link #updateClob updateClob}. + * Implemented over {@link #updateString updateString}. */ @Override public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { - updateClob(columnIndex, (Clob) new ClobValue(x, length)); + updateString(columnIndex, readNCharsAsString(x, length)); } /** 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 067bfa0fa..c579f36b0 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-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2005-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -14,6 +14,12 @@ package org.postgresql.pljava.jdbc; +import java.io.InputStream; +import java.io.IOException; +import java.io.Reader; + +import java.nio.CharBuffer; + import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; @@ -21,6 +27,7 @@ import java.sql.RowIdLifetime; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLNonTransientException; import java.sql.Statement; import java.util.ArrayList; import static java.util.Arrays.sort; @@ -58,6 +65,109 @@ public SPIDatabaseMetaData(SPIConnection conn) private int INDEX_MAX_KEYS = 0; // maximum number of keys in an index. + /* + * Common methods used in the (mostly legacy, largely broken) Blob/Clob + * implementation. Located here for their dependence, logically, on some + * "database metadata" like VARHDRSZ. + */ + + static byte[] readNBytes(InputStream is, long length) + throws SQLException + { + if ( null == is ) + return null; + + if ( 0 > length ) + { + throw new SQLNonTransientException( + "explicit length passed with an InputStream is negative", + "22000"); + } + else if ( length > Integer.MAX_VALUE - VARHDRSZ ) + { + throw new SQLNonTransientException( + "explicit length passed with an InputStream is too large", + "54000"); + } + + // Java >= 11: bytes = x.readNBytes(length) + byte[] bytes = new byte[(int)length]; + try + { + int got = is.readNBytes(bytes, 0, bytes.length); + if ( bytes.length == got && -1 == is.read() ) + return bytes; + throw new SQLNonTransientException( + "explicit length passed with an InputStream is incorrect", + "22000"); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } + } + + public static String readAllAsString(Reader r) throws SQLException + { + if ( null == r ) + return null; + + // Java >= 10: can use r.transferTo(...a StringWriter...) + // Java >= 25: can use r.readAllAsString() + try + { + CharBuffer cb = CharBuffer.allocate(2048); + StringBuilder sb = new StringBuilder(); + while ( -1 != r.read(cb) ) + { + sb.append(cb.flip()); + cb.clear(); + } + return sb.toString(); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } + } + + public static String readNCharsAsString(Reader r, long length) + throws SQLException + { + if ( null == r ) + return null; + + if ( 0 > length ) + { + throw new SQLNonTransientException( + "explicit length passed with a Reader is negative", + "22000"); + } + else if ( length > Integer.MAX_VALUE ) + { + throw new SQLNonTransientException( + "explicit length passed with a Reader is too large", + "54000"); + } + + try + { + CharBuffer cb = CharBuffer.allocate((int)length); + int got; + while ( 0 < (got = r.read(cb)) ) + ; + if ( 0 == cb.remaining() && -1 == r.read() ) + return cb.flip().toString(); + throw new SQLNonTransientException( + "explicit length passed with a Reader is incorrect", + "22000"); + } + catch ( IOException e ) + { + throw new SQLException(e.getMessage(), e); + } + } + protected int getMaxIndexKeys() throws SQLException { if(INDEX_MAX_KEYS == 0) 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 ed95bb4a6..0edf141ad 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIPreparedStatement.java @@ -43,6 +43,9 @@ import org.postgresql.pljava.internal.ExecutionPlan; import org.postgresql.pljava.internal.Oid; +import static org.postgresql.pljava.jdbc.SPIDatabaseMetaData.readNBytes; +import static org.postgresql.pljava.jdbc.SPIDatabaseMetaData.readNCharsAsString; + /** * Implementation of {@link PreparedStatement} for the SPI connection. * @author Thomas Hallgren @@ -180,11 +183,11 @@ public void setTimestamp(int columnIndex, Timestamp value) throws SQLException } @Override - public void setAsciiStream(int columnIndex, InputStream value, int length) throws SQLException + public void setAsciiStream(int columnIndex, InputStream value, int length) + throws SQLException { - setObject(columnIndex, - new ClobValue(new InputStreamReader(value, US_ASCII), length), - Types.CLOB); + setObject(columnIndex, null == value ? null : + new String(readNBytes(value, length), US_ASCII), Types.CLOB); } @SuppressWarnings("deprecation") @Override @@ -387,7 +390,7 @@ public void addBatch(String statement) public void setCharacterStream(int columnIndex, Reader value, int length) throws SQLException { - setObject(columnIndex, new ClobValue(value, length), Types.CLOB); + setObject(columnIndex, readNCharsAsString(value, length), Types.CLOB); } @Override 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 a09cac166..a29c0f761 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java @@ -15,8 +15,14 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; +import java.io.StringReader; + 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; @@ -33,6 +39,7 @@ import java.sql.Timestamp; import javax.sql.rowset.serial.SerialBlob; +import javax.sql.rowset.serial.SerialClob; import org.postgresql.pljava.internal.Backend; import org.postgresql.pljava.internal.DualState; @@ -81,13 +88,15 @@ public Array readArray() throws SQLException } /** - * Implemented over {@link #readClob}. + * Implemented over {@link #readString}. */ @Override public InputStream readAsciiStream() throws SQLException { - Clob c = readClob(); - return (c == null) ? null : c.getAsciiStream(); + String s = readString(); + if ( null == s ) + return null; + return new ByteArrayInputStream(s.getBytes(US_ASCII)); } /** @@ -153,8 +162,8 @@ public byte[] readBytes() throws SQLException */ public Reader readCharacterStream() throws SQLException { - Clob c = readClob(); - return (c == null) ? null : c.getCharacterStream(); + String s = readString(); + return (s == null) ? null : new StringReader(s); } /** @@ -163,7 +172,7 @@ public Reader readCharacterStream() throws SQLException public Clob readClob() throws SQLException { String str = readString(); - return (str == null) ? null : new ClobValue(str); + return (str == null) ? null : new SerialClob(str.toCharArray()); } /** 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 b76b8de69..7ab09f2c1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLOutputToTuple.java @@ -41,6 +41,8 @@ import org.postgresql.pljava.internal.Tuple; import org.postgresql.pljava.internal.TupleDesc; +import static org.postgresql.pljava.jdbc.SPIDatabaseMetaData.readAllAsString; + /** * Implementation of {@link SQLOutput} for the case of a composite data type. * @author Thomas Hallgren @@ -87,8 +89,7 @@ public void writeArray(Array value) throws SQLException public void writeAsciiStream(InputStream value) throws SQLException { - Reader rdr = new BufferedReader(new InputStreamReader(value, US_ASCII)); - writeClob(new ClobValue(rdr, ClobValue.getReaderLength(rdr))); + writeCharacterStream(new InputStreamReader(value, US_ASCII)); } public void writeBigDecimal(BigDecimal value) throws SQLException @@ -130,9 +131,7 @@ public void writeBytes(byte[] value) throws SQLException public void writeCharacterStream(Reader value) throws SQLException { - if(!value.markSupported()) - value = new BufferedReader(value); - writeClob(new ClobValue(value, ClobValue.getReaderLength(value))); + writeString(readAllAsString(value)); } public void writeClob(Clob value) throws SQLException From d37eb2770a16c3289240553bcd6f8249e964b1b9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 13:29:05 -0400 Subject: [PATCH 5/8] Automate working Blob/Clob examples, and document Those examples that do work after this refactoring, including ones that didn't before, are now run with SQLAction in the deployment descriptor so that at least the current state of limited utility can be preserved. --- .../pljava/example/annotation/LegacyBlob.java | 92 +++++++++++++++-- .../pljava/example/annotation/LegacyClob.java | 99 +++++++++++++++++-- 2 files changed, 174 insertions(+), 17 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java index 7c6958d8c..7790a113d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java @@ -29,11 +29,76 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; +import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; /** - * Captures how PL/Java's Blob implementation has and hasn't worked. + * Captures how PL/Java's {@code Blob} implementation has and hasn't worked. + *

+ * The {@link Blob} implementation in PL/Java, from inception and as currently + * found in the 1.6 series releases, has never been especially useful. It has + * used {@code Blob} objects as an alternative interface to binary byte + * sequences stored inline in a tuple, such as could also be accessed + * using, for example, {@link ResultSet#getBytes getBytes}. In this legacy + * design, you would apply {@code getBlob} to a column containing a large byte + * string, and be able to manipulate that content using the methods of {@code + * Blob} instead of as a byte array. + *

+ * That contrasts with the function of {@code Blob} in the PGJDBC client-side + * driver: with that driver, you would apply {@code getBlob} to a column + * containing the oid of a PostgreSQL + * large + * object, and the {@code Blob} object returned would allow you + * to manipulate the content of that out-of-tuple large object. That is almost + * certainly the way the JDBC {@code Blob} API was intended to be used, and + * the legacy PL/Java approach is not. On top of that, even the rather less + * useful PL/Java realization has never been quite fully implemented. It has, + * therefore, probably never been widely used, if at all. + *

+ * These are not shortcomings to be corrected in the middle of a release series; + * some future PL/Java major release will need to include all-new {@code Blob} + * support in a thoroughly-revamped JDBC layer. The purpose of this example code + * is simply to document the current working (and non-working) of the current + * {@code Blob} support, as a guard against bit-rot making it even worse, just + * in case anyone anywhere has used it for something. + *

The interim solution for using actual PostgreSQL large objects

+ * All is not lost for code that needs to manipulate actual large objects + * in PL/Java. It simply needs to use normal, non-{@code Blob} JDBC methods + * to call PostgreSQL's server-side large-object + * functions directly. */ +@SQLAction( + requires = { "LegacyBlob members", "TypeRoundTripper.roundTrip" }, install = + "SELECT" + + " CASE WHEN" + + " rsgbs" + + " AND ( crb.c1 = crb.c2)" + + " AND (crbs.c1 = crbs.c2)" + + " AND pssbs" + + " AND pssb" + + " AND bbout.class =" + + " 'org.postgresql.pljava.example.annotation.LegacyBlob$BlobbedBlob'" + + " AND bbout.roundtripped = bbin.orig" + + " AND sbout.class =" + + " 'org.postgresql.pljava.example.annotation.LegacyBlob$StreamedBlob'"+ + " AND sbout.roundtripped = sbin.orig" + + " THEN javatest.logmessage('INFO', 'blob support has not grown worse')" + + " ELSE javatest.logmessage('WARNING', 'blob support has grown worse')" + + " END" + + " FROM" + + " javatest.resultSetGetBinaryStream() AS rsgbs," + + " javatest.compositeReturnBlob() AS crb," + + " javatest.compositeReturnBinaryStream() AS crbs," + + " javatest.preparedStmtSetBinaryStream() AS pssbs," + + " javatest.preparedStmtSetBlob() AS pssb," + + " (SELECT '(\\x01234567)'::javatest.blobbedblob) AS bbin(orig), " + + " javatest.roundtrip(bbin)" + + " AS bbout(class text, roundtripped javatest.blobbedblob)," + + " (SELECT '(\\x76543210)'::javatest.streamedblob) AS sbin(orig), " + + " javatest.roundtrip(sbin)" + + " AS sbout(class text, roundtripped javatest.streamedblob)" +) public class LegacyBlob { private LegacyBlob() { } // do not instantiate @@ -48,7 +113,7 @@ static Connection connect() throws SQLException /** * Exercises getBinaryStream on ResultSet, returning true for success. */ - @Function(schema = "javatest") + @Function(schema = "javatest", provides = "LegacyBlob members") public static boolean resultSetGetBinaryStream() throws SQLException, IOException { @@ -81,7 +146,8 @@ public static boolean resultSetGetBinaryStream() * Exercises Blob in a composite return value, returning two bytea columns * that should be equal; also tests getBlob. */ - @Function(schema = "javatest", out = { "c1 bytea", "c2 bytea" }) + @Function(schema = "javatest", out = { "c1 bytea", "c2 bytea" }, + provides = "LegacyBlob members") public static boolean compositeReturnBlob(ResultSet toReturn) throws SQLException, IOException { @@ -121,7 +187,8 @@ public static Blob scalarReturnBlob() throws SQLException, IOException * Exercises setting a composite return column using updateBinaryStream, * returning two bytea columns that should be equal. */ - @Function(schema = "javatest", out = { "c1 bytea", "c2 bytea" }) + @Function(schema = "javatest", out = { "c1 bytea", "c2 bytea" }, + provides = "LegacyBlob members") public static boolean compositeReturnBinaryStream(ResultSet toReturn) throws SQLException, IOException { @@ -137,7 +204,7 @@ public static boolean compositeReturnBinaryStream(ResultSet toReturn) * Exercises setBinaryStream on PreparedStatement, * returning true for success. */ - @Function(schema = "javatest") + @Function(schema = "javatest", provides = "LegacyBlob members") public static boolean preparedStmtSetBinaryStream() throws SQLException, IOException { @@ -168,7 +235,7 @@ public static boolean preparedStmtSetBinaryStream() /** * Exercises setBlob on PreparedStatement, returning true for success. */ - @Function(schema = "javatest") + @Function(schema = "javatest", provides = "LegacyBlob members") public static boolean preparedStmtSetBlob() throws SQLException, IOException { @@ -207,7 +274,11 @@ public static boolean preparedStmtSetBlob() } } - @MappedUDT(schema = "javatest", structure = { "b bytea" }) + /** + * A mapped user-defined-type used in testing legacy Blob support. + */ + @MappedUDT(schema = "javatest", structure = { "b bytea" }, + provides = "LegacyBlob members") public static class BlobbedBlob implements SQLData { private String name; @@ -234,7 +305,12 @@ public void writeSQL(SQLOutput stream) throws SQLException } } - @MappedUDT(schema = "javatest", structure = { "b bytea" }) + /** + * A mapped user-defined-type used in testing legacy + * (read/write)BinaryStream support. + */ + @MappedUDT(schema = "javatest", structure = { "b bytea" }, + provides = "LegacyBlob members") public static class StreamedBlob implements SQLData { private String name; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java index 5bd5bd17e..800c75163 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java @@ -35,12 +35,78 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; +import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; /** * Captures how PL/Java's Clob implementation has and hasn't (hasn't, mostly) * worked. + *

+ * The {@link Clob} implementation in PL/Java, from inception and as currently + * found in the 1.6 series releases, has never been especially useful. It has + * used {@code Clob} objects as an alternative interface to character strings + * stored inline in a tuple, such as could also be accessed + * using, for example, {@link ResultSet#getString getString}. In this legacy + * design, you would apply {@code getClob} to a column containing a large text + * string, and be able to manipulate that content using the methods of {@code + * Clob} instead of as a character string. + *

+ * That contrasts with the function of {@code Clob} in the PGJDBC client-side + * driver: with that driver, you would apply {@code getClob} to a column + * containing the oid of a PostgreSQL + * large + * object, and the {@code Clob} object returned would allow you + * to manipulate the content of that out-of-tuple large object. That is almost + * certainly the way the JDBC {@code Clob} API was intended to be used, and + * the legacy PL/Java approach is not. On top of that, even the rather less + * useful PL/Java realization has never been close to fully implemented. It has, + * therefore, probably never been widely used, if at all. + *

+ * These are not shortcomings to be corrected in the middle of a release series; + * some future PL/Java major release will need to include all-new {@code Clob} + * support in a thoroughly-revamped JDBC layer. The purpose of this example code + * is simply to document the current working (and non-working) of the current + * {@code Clob} support, as a guard against bit-rot making it even worse, just + * in case anyone anywhere has used it for something. + *

The interim solution for using actual PostgreSQL large objects

+ * All is not lost for code that needs to manipulate actual large objects + * in PL/Java. It simply needs to use normal, non-{@code Clob} JDBC methods + * to call PostgreSQL's server-side large-object + * functions directly and (in the case of a {@code Clob}) apply appropriate + * character-set encodings. */ +@SQLAction( + requires = { "LegacyClob members", "TypeRoundTripper.roundTrip" }, install = + "SELECT" + + " CASE WHEN" + + " rsgcs AND rsgas" + + " AND (crcs.c1 = crcs.c2)" + + " AND (cras.c1 = cras.c2)" + + " AND psscs AND pssas" + + " AND scout.class =" + + " 'org.postgresql.pljava.example.annotation.LegacyClob$StreamedClob'"+ + " AND scout.roundtripped = scin.orig" + + " AND acout.class =" + + " 'org.postgresql.pljava.example.annotation.LegacyClob$AsciiedClob'"+ + " AND acout.roundtripped = acin.orig" + + " THEN javatest.logmessage('INFO', 'clob support has not grown worse')" + + " ELSE javatest.logmessage('WARNING', 'clob support has grown worse')" + + " END" + + " FROM" + + " javatest.resultSetGetCharacterStream() AS rsgcs," + + " javatest.resultSetGetAsciiStream() AS rsgas," + + " javatest.compositeReturnCharacterStream() AS crcs," + + " javatest.compositeReturnAsciiStream() AS cras," + + " javatest.preparedStmtSetCharacterStream() AS psscs," + + " javatest.preparedStmtSetAsciiStream() AS pssas," + + " (SELECT '(PostgreSQL)'::javatest.streamedclob) AS scin(orig), " + + " javatest.roundtrip(scin)" + + " AS scout(class text, roundtripped javatest.streamedclob)," + + " (SELECT '(LQSergtsoP)'::javatest.asciiedclob) AS acin(orig), " + + " javatest.roundtrip(acin)" + + " AS acout(class text, roundtripped javatest.asciiedclob)" +) public class LegacyClob { private LegacyClob() { } // do not instantiate @@ -76,7 +142,7 @@ static String readAsciiString(InputStream is) throws IOException /** * Exercises getCharacterStream on ResultSet, returning true for success. */ - @Function(schema = "javatest") + @Function(schema = "javatest", provides = "LegacyClob members") public static boolean resultSetGetCharacterStream() throws SQLException, IOException { @@ -109,7 +175,7 @@ public static boolean resultSetGetCharacterStream() /** * Exercises getAsciiStream on ResultSet, returning true for success. */ - @Function(schema = "javatest") + @Function(schema = "javatest", provides = "LegacyClob members") public static boolean resultSetGetAsciiStream() throws SQLException, IOException { @@ -187,7 +253,8 @@ public static Clob scalarReturnClob() throws SQLException, IOException * Exercises setting a composite return column using updateCharacterStream, * returning two text columns that should be equal. */ - @Function(schema = "javatest", out = { "c1 text", "c2 text" }) + @Function(schema = "javatest", out = { "c1 text", "c2 text" }, + provides = "LegacyClob members") public static boolean compositeReturnCharacterStream(ResultSet toReturn) throws SQLException, IOException { @@ -203,7 +270,8 @@ public static boolean compositeReturnCharacterStream(ResultSet toReturn) * Exercises setting a composite return column using updateAsciiStream, * returning two text columns that should be equal. */ - @Function(schema = "javatest", out = { "c1 text", "c2 text" }) + @Function(schema = "javatest", out = { "c1 text", "c2 text" }, + provides = "LegacyClob members") public static boolean compositeReturnAsciiStream(ResultSet toReturn) throws SQLException, IOException { @@ -223,7 +291,7 @@ public static boolean compositeReturnAsciiStream(ResultSet toReturn) * Exercises setCharacterStream on PreparedStatement, * returning true for success. */ - @Function(schema = "javatest") + @Function(schema = "javatest", provides = "LegacyClob members") public static boolean preparedStmtSetCharacterStream() throws SQLException, IOException { @@ -257,7 +325,7 @@ public static boolean preparedStmtSetCharacterStream() * Exercises setAsciiStream on PreparedStatement, * returning true for success. */ - @Function(schema = "javatest") + @Function(schema = "javatest", provides = "LegacyClob members") public static boolean preparedStmtSetAsciiStream() throws SQLException, IOException { @@ -329,6 +397,9 @@ public static boolean preparedStmtSetClob() } // XXX writeClob produces Object.toString of the Clob instance + /** + * A mapped user-defined-type used in testing legacy Clob support. + */ @MappedUDT(schema = "javatest", structure = { "t text" }) public static class ClobbedClob implements SQLData { @@ -357,8 +428,13 @@ public void writeSQL(SQLOutput stream) throws SQLException } // Now works! Formerly: - // XXX writeCharacterStream produces Object.toString of the Clob instance - @MappedUDT(schema = "javatest", structure = { "b text" }) + // writeCharacterStream produces Object.toString of the Clob instance + /** + * A mapped user-defined-type used in testing legacy + * (read/write}CharacterStream support. + */ + @MappedUDT(schema = "javatest", structure = { "b text" }, + provides = "LegacyClob members") public static class StreamedClob implements SQLData { private String name; @@ -394,7 +470,12 @@ public void writeSQL(SQLOutput stream) throws SQLException // Now works! Formerly: // OutOfMemoryError: Requested array size exceeds VM limit - @MappedUDT(schema = "javatest", structure = { "b text" }) + /** + * A mapped user-defined-type used in testing legacy + * (read/write}AsciiStream support. + */ + @MappedUDT(schema = "javatest", structure = { "b text" }, + provides = "LegacyClob members") public static class AsciiedClob implements SQLData { private String name; From 67e572e5f0af51b08bf26cc8913cbbc44c351f1c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 13:29:22 -0400 Subject: [PATCH 6/8] Bump DDRProcessor latest_tested to 26 No issues observed in compiling manually (without the Maven directive specifying an earlier --release) on Oracle JDK 25 GA. cd pljava-api/src/main/java /var/tmp/jdk-26.0.1/bin/javac -d ../../../target/classes/ \ -Xlint:unchecked -Xlint:-removal --module-version 1.6-SNAPSHOT \ $(find . -name '*.java') cd ../../.. /var/tmp/jdk-26.0.1/bin/jar cf target/pljava-api-1.6-SNAPSHOT.jar \ -C target/classes . cd ../pljava/src/main/java /var/tmp/jdk-26.0.1/bin/javac --module-version 1.6-SNAPSHOT \ -d ../../../target/classes/ \ -h ../../../target/javah-include \ --module-path ../../../../pljava-api/target/pljava-api-1.6-SNAPSHOT.jar \ --processor-module-path \ ../../../../pljava-api/target/pljava-api-1.6-SNAPSHOT.jar \ -Xlint:unchecked -Xlint:-removal $(find . -name '*.java') cd ../../../ /var/tmp/jdk-26.0.1/bin/jar cf target/pljava-1.6-SNAPSHOT.jar \ -C target/classes . cd ../pljava-examples/src/main/java /var/tmp/jdk-26.0.1/bin/javac -d ../../../target/classes/ \ --module-path ../../../../pljava-api/target/pljava-api-1.6-SNAPSHOT.jar \ --processor-module-path \ ../../../../pljava-api/target/pljava-api-1.6-SNAPSHOT.jar \ --class-path \ ~/.m2/repository/net/sf/saxon/Saxon-HE/10.9/Saxon-HE-10.9.jar: \ -Xlint:unchecked -Xlint:-removal \ --add-modules org.postgresql.pljava $(find . -name '*.java') cd ../../../target/classes cp -r ../../src/main/resources/* . zip -r ../pljava-examples-1.6-SNAPSHOT.jar * # zip because jar m doesn't preserve order of manifest entries cd ../../../ # with dir of intended pg_config version on PATH: mvn clean install --projects pljava-pgxs,pljava-so,pljava-packaging --- .../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 e59c63c1a..92a5d68d7 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-2025 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -62,7 +62,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 = 25; + int latest_tested = 26; int ordinal_9 = SourceVersion.RELEASE_9.ordinal(); int ordinal_latest = latest_tested - 9 + ordinal_9; From 7aa683eb01e76788fb9ff19ab912655d70f21d0d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 18:50:16 -0400 Subject: [PATCH 7/8] Use INT64_FORMAT in preference to PRId64 PostgreSQL adopted the inttypes.h format macros in 18, but continues to define the older ..._FORMAT macros, so those are what should be used for compatibility across versions. --- pljava-so/src/main/c/type/byte_array.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/byte_array.c b/pljava-so/src/main/c/type/byte_array.c index d32d59898..9d4334715 100644 --- a/pljava-so/src/main/c/type/byte_array.c +++ b/pljava-so/src/main/c/type/byte_array.c @@ -53,7 +53,7 @@ static Datum _byte_array_coerceObject(Type self, jobject byteArray) { ereport(ERROR, ( errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("cannot accommodate reported Blob length %" PRId64, + errmsg("cannot accommodate reported Blob length " INT64_FORMAT, length) )); } From 258bf93f8e6876ed01f98d52068a72746095f30f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 12 Aug 2026 19:12:02 -0400 Subject: [PATCH 8/8] Use subquery aliases in added example code Aliases for subqueries in a FROM clause became optional in PG 16 (postgres/postgres@bcedd8f, as an extension to the standard), but are strictly required in earlier versions. --- .../postgresql/pljava/example/annotation/LegacyBlob.java | 6 ++++-- .../postgresql/pljava/example/annotation/LegacyClob.java | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java index 7790a113d..5ecc33d02 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java @@ -214,7 +214,8 @@ public static boolean preparedStmtSetBinaryStream() PreparedStatement ps = c.prepareStatement( "SELECT a = b FROM (SELECT" + - " CAST ( ? AS bytea ) AS a, CAST ( ? AS bytea ) AS b)"); + " CAST ( ? AS bytea ) AS a, CAST ( ? AS bytea ) AS b)" + + " AS params"); ) { ps.setBytes(1, BYTES); @@ -247,7 +248,8 @@ public static boolean preparedStmtSetBlob() PreparedStatement ps2 = c.prepareStatement( "SELECT a = b FROM (SELECT" + - " CAST ( ? AS bytea ) AS a, CAST ( ? AS bytea ) AS b)"); + " CAST ( ? AS bytea ) AS a, CAST ( ? AS bytea ) AS b)" + + " AS params"); ) { ps1.setBytes(1, BYTES); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java index 800c75163..d209052d9 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java @@ -301,7 +301,8 @@ public static boolean preparedStmtSetCharacterStream() PreparedStatement ps = c.prepareStatement( "SELECT a = b FROM (SELECT" + - " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)"); + " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)" + + " AS params"); ) { ps.setString(1, CHARS); @@ -335,7 +336,8 @@ public static boolean preparedStmtSetAsciiStream() PreparedStatement ps = c.prepareStatement( "SELECT a = b FROM (SELECT" + - " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)"); + " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)" + + " AS params"); ) { ps.setString(1, CHARS); @@ -369,7 +371,8 @@ public static boolean preparedStmtSetClob() PreparedStatement ps2 = c.prepareStatement( "SELECT a = b FROM (SELECT" + - " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)"); + " CAST ( ? AS text ) AS a, CAST ( ? AS text ) AS b)" + + " AS params"); ) { ps1.setString(1, CHARS);