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; 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..5ecc33d02 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyBlob.java @@ -0,0 +1,348 @@ +/* + * 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.SQLAction; +import org.postgresql.pljava.annotation.SQLType; + +/** + * 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 + + 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", provides = "LegacyBlob members") + 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" }, + provides = "LegacyBlob members") + 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" }, + provides = "LegacyBlob members") + 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", provides = "LegacyBlob members") + 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)" + + " AS params"); + ) + { + 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", provides = "LegacyBlob members") + 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)" + + " AS params"); + ) + { + 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); + } + } + } + + /** + * 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; + 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); + } + } + + /** + * 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; + 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)); + } + } +} 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..d209052d9 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/LegacyClob.java @@ -0,0 +1,515 @@ +/* + * 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.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 + + 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", provides = "LegacyClob members") + 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", provides = "LegacyClob members") + 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) + } + */ + + // Now works! Formerly: + // 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" }, + provides = "LegacyClob members") + 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" }, + provides = "LegacyClob members") + 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; + } + + // Now works! Formerly: + // XXX returns false; Clob probably rendered by Object.toString + /** + * Exercises setCharacterStream on PreparedStatement, + * returning true for success. + */ + @Function(schema = "javatest", provides = "LegacyClob members") + 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)" + + " AS params"); + ) + { + 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); + } + } + } + + // Now works! Formerly: + // XXX returns false; Clob probably rendered by Object.toString + /** + * Exercises setAsciiStream on PreparedStatement, + * returning true for success. + */ + @Function(schema = "javatest", provides = "LegacyClob members") + 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)" + + " AS params"); + ) + { + 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)" + + " AS params"); + ) + { + 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 + /** + * A mapped user-defined-type used in testing legacy Clob support. + */ + @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); + } + } + + // Now works! Formerly: + // 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; + 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)); + } + } + + // Now works! Formerly: + // OutOfMemoryError: Requested array size exceeds VM limit + /** + * 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; + 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))); + } + } +} diff --git a/pljava-so/src/main/c/type/byte_array.c b/pljava-so/src/main/c/type/byte_array.c index 91e3103a0..9d4334715 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 " INT64_FORMAT, + 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/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 0393c5d13..dfae4794a 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,18 @@ 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 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}, @@ -102,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)); } /** @@ -133,14 +143,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 +161,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); } /** @@ -187,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); } /** @@ -205,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()); } /** @@ -413,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)); } /** @@ -435,14 +444,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); } /** @@ -486,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 02ccd4070..0edf141ad 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; @@ -42,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 @@ -179,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 @@ -193,9 +197,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 @@ -378,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 2b99e01ba..a29c0f761 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,10 +12,17 @@ */ package org.postgresql.pljava.jdbc; +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; @@ -31,6 +38,9 @@ import java.sql.Time; 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; import org.postgresql.pljava.internal.TupleDesc; @@ -78,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)); } /** @@ -97,13 +109,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 +125,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); } /** @@ -150,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); } /** @@ -160,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 ec2365bb1..7ab09f2c1 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; @@ -40,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 @@ -86,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 @@ -97,9 +99,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 @@ -124,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 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);