Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: EXPOSED-123 ExposedBlob.getBytes() fails on Oracle with IOException #1824

Merged
merged 1 commit into from
Aug 7, 2023

Conversation

bog-walk
Copy link
Member

@bog-walk bog-walk commented Aug 4, 2023

The following tests fail when run using Oracle:

DDLTests/testBlob()
DDLTests/testBlobDefault()
EntityTests/testBlobField()

They all fail with the same error:

Caused by: java.io.IOException: Mark invalid or stream not marked.
	at oracle.jdbc.driver.OracleBlobInputStream.reset(OracleBlobInputStream.java:308)
	at org.jetbrains.exposed.sql.statements.api.ExposedBlob.getBytes(ExposedBlob.kt:13)

This exception is thrown by inputStream.reset():

val bytes: ByteArray
  get() = inputStream.readBytes().also {
    if (inputStream.markSupported()) {
      inputStream.reset()
    } else {
      inputStream = it.inputStream()
    }
  }

Because the InputStream retrieved by the DB is of type oracle.jdbc.driver.OracleBlobInputStream, which throws, as per the documentation:

If the method mark has not been called since the stream was created, or the number of bytes read from the stream since mark was last called is larger than the argument to mark at that last call, then an IOException might be thrown.

Possible Solutions:

  1. Retrieve the column value from the DB as a ByteArray instead of an InputStream:
override fun readObject(rs: ResultSet, index: Int) = when (currentDialect) {
    is SQLServerDialect, is OracleDialect -> rs.getBytes(index)?.let(::ExposedBlob)
    else -> rs.getBinaryStream(index)?.let(::ExposedBlob)
}

This fixes the issue because an ExposedBlob is created using the secondary constructor:

constructor(bytes: ByteArray) : this (bytes.inputStream())

which instantiates a java.io.ByteArrayInputStream, with a reset() override that doesn't throw.

This works fine, but seems less than ideal especially since most recommendations about reading BLOBs from Oracle suggest using getBlob() or getBinaryStream() because of the potential size of a BLOB. The current code uses getBinaryStream():

Retrieves the value of the designated column in the current row of this ResultSet object as a stream of uninterpreted bytes. The value can then be read in chunks from the stream. This method is particularly suitable for retrieving large LONGVARBINARY values.

  1. Continue to retrieve the DB object as an InputStream, but ensure mark() is invoked:
override fun readObject(rs: ResultSet, index: Int) = when (currentDialect) {
    is SQLServerDialect -> rs.getBytes(index)?.let(::ExposedBlob)
    is OracleDialect -> rs.getBinaryStream(index)?.let(::ExposedBlob)?.also {
            it.inputStream.mark(it.inputStream.available())
    }
    else -> rs.getBinaryStream(index)?.let(::ExposedBlob)
}

Using it.inputStream.available() as the readlimit value tells the input stream to allow that many bytes to be read before the mark position gets invalidated. This works, but seems like a step too far especially since documentation shows this warning:

It is never correct to use the return value of this method to allocate a buffer intended to hold all data in this stream.

  1. [Implemented] Make no changes to readObject() and instead handle the IOException inside ExposedBlob.getBytes(). This ensures that the BLOB value is still being retrieved as a stream and does not affect the returned ByteArray value.

…ion Mark invalid

The following tests fail when run using Oracle:

DDLTests/testBlob()
DDLTests/testBlobDefault()
EntityTests/testBlobField()

They all fail with the same error:
Caused by: java.io.IOException: Mark invalid or stream not marked.
	at oracle.jdbc.driver.OracleBlobInputStream.reset(OracleBlobInputStream.java:308)
	at org.jetbrains.exposed.sql.statements.api.ExposedBlob.getBytes(ExposedBlob.kt:13)

The exception is thrown after a BLOB value is retrieved and read from the DB because
the type retrieved is oracle.jdbc.driver.OracleBlobInputStream, which either does
not set markedByte to not be default by calling mark() or invalidates mark. This
makes reset() throw.

This is unlike an ExposedBlob instantiated using the secondary constructor that
creates a ByteArrayInputStream from a ByteArray, which overrides reset() to not throw.

This exception is handled inside getBytes() to ensure that the retrieved BLOB
value is still read as a stream.
Comment on lines -458 to -462
// if (currentDialectTest.dataTypeProvider.blobAsStream) {
// SerialBlob(bytes)
// } else connection.createBlob().apply {
// setBytes(1, bytes)
// }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither blobAsStream nor createBlob exist in the codebase anymore.

@bog-walk bog-walk requested review from e5l and joc-a August 4, 2023 21:10
Copy link
Member

@e5l e5l left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Could you also log an issue about blob streaming data type support?

@bog-walk bog-walk merged commit 7211d42 into main Aug 7, 2023
3 checks passed
@bog-walk bog-walk deleted the bog-walk/fix-oracle-blob branch August 7, 2023 15:26
saral pushed a commit to saral/Exposed that referenced this pull request Oct 3, 2023
…ion Mark invalid (JetBrains#1824)

The following tests fail when run using Oracle:

DDLTests/testBlob()
DDLTests/testBlobDefault()
EntityTests/testBlobField()

They all fail with the same error:
Caused by: java.io.IOException: Mark invalid or stream not marked.
	at oracle.jdbc.driver.OracleBlobInputStream.reset(OracleBlobInputStream.java:308)
	at org.jetbrains.exposed.sql.statements.api.ExposedBlob.getBytes(ExposedBlob.kt:13)

The exception is thrown after a BLOB value is retrieved and read from the DB because
the type retrieved is oracle.jdbc.driver.OracleBlobInputStream, which either does
not set markedByte to not be default by calling mark() or invalidates mark. This
makes reset() throw.

This is unlike an ExposedBlob instantiated using the secondary constructor that
creates a ByteArrayInputStream from a ByteArray, which overrides reset() to not throw.

This exception is handled inside getBytes() to ensure that the retrieved BLOB
value is still read as a stream.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants