Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
Expand All @@ -40,16 +39,16 @@
* so there is no post-close execute that could expose a result set. sqlite-jdbc 3.53.4.0 instead
* returns generated keys, and rejects {@code addBatch()} with a different message; see issue #87.
*
* <p>Parameter metadata is derived locally. The placeholder count comes from the SQL text, but the
* parameter type, precision, and scale stay unknown because Hrana has no describe request. The
* stream and {@link Blob}/{@link Clob} setters convert to a TEXT or BLOB value locally. An unset
* parameter, including one released by {@link #clearParameters()}, binds SQL NULL.
* <p>The stream and {@link Blob}/{@link Clob} setters convert to a TEXT or BLOB value locally. An
* unset parameter, including one released by {@link #clearParameters()}, binds SQL NULL. Parameter
* metadata lives in {@link ParameterMetaDataView}.
*/
public class LibsqlPreparedStatement extends LibsqlStatement implements PreparedStatement, ParameterMetaData {

private final String sql;
private final boolean returnsResultSet;
private final int parameterCount;
private final ParameterMetaDataView parameterMetaData;
// Sequential list indexed by JDBC parameter position; no map or boxing overhead.
private final List<Value> parameters = new ArrayList<>();

Expand All @@ -58,6 +57,7 @@
this.sql = sql;
this.returnsResultSet = returnsResultSet(sql);
this.parameterCount = SqlTokenizer.placeholderCount(sql);
this.parameterMetaData = new ParameterMetaDataView(this, parameterCount);

Check warning on line 60 in libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java

View workflow job for this annotation

GitHub Actions / Xerial Control (reference probe)

possible 'this' escape before subclass is fully initialized

Check warning on line 60 in libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 26)

possible 'this' escape before subclass is fully initialized

Check warning on line 60 in libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 25)

possible 'this' escape before subclass is fully initialized
}

private void setParam(int parameterIndex, Value value) throws SQLException {
Expand Down Expand Up @@ -363,73 +363,18 @@
setString(parameterIndex, x != null ? x.toString() : null);
}

@Override
public ParameterMetaData getParameterMetaData() throws SQLException {
checkClosed();
return this;
}

@Override
public int getParameterCount() throws SQLException {
checkClosed();
return parameterCount;
}

@Override
public int isNullable(int param) throws SQLException {
checkParameterIndex(param);
return ParameterMetaData.parameterNullableUnknown;
}

@Override
public boolean isSigned(int param) throws SQLException {
checkParameterIndex(param);
return true;
}
@Override public ParameterMetaData getParameterMetaData() throws SQLException { checkClosed(); return parameterMetaData; }

@Override
public int getPrecision(int param) throws SQLException {
checkParameterIndex(param);
return 0;
}

@Override
public int getScale(int param) throws SQLException {
checkParameterIndex(param);
return 0;
}

@Override
public int getParameterType(int param) throws SQLException {
checkParameterIndex(param);
// Hrana has no describe statement, so a parameter type is unknown until it is executed.
return Types.OTHER;
}

@Override
public String getParameterTypeName(int param) throws SQLException {
checkParameterIndex(param);
return "OTHER";
}

@Override
public String getParameterClassName(int param) throws SQLException {
checkParameterIndex(param);
return Object.class.getName();
}

@Override
public int getParameterMode(int param) throws SQLException {
checkParameterIndex(param);
return ParameterMetaData.parameterModeIn;
}

private void checkParameterIndex(int param) throws SQLException {
checkClosed();
if (param < 1 || param > parameterCount) {
throw new SQLException("Parameter index out of range: " + param);
}
}
// ParameterMetaData stays on the statement for instanceof compatibility; the view holds the checks.
@Override public int getParameterCount() throws SQLException { return parameterMetaData.getParameterCount(); }
@Override public int isNullable(int param) throws SQLException { return parameterMetaData.isNullable(param); }
@Override public boolean isSigned(int param) throws SQLException { return parameterMetaData.isSigned(param); }
@Override public int getPrecision(int param) throws SQLException { return parameterMetaData.getPrecision(param); }
@Override public int getScale(int param) throws SQLException { return parameterMetaData.getScale(param); }
@Override public int getParameterType(int param) throws SQLException { return parameterMetaData.getParameterType(param); }
@Override public String getParameterTypeName(int param) throws SQLException { return parameterMetaData.getParameterTypeName(param); }
@Override public String getParameterClassName(int param) throws SQLException { return parameterMetaData.getParameterClassName(param); }
@Override public int getParameterMode(int param) throws SQLException { return parameterMetaData.getParameterMode(param); }

@Override
public void setRowId(int parameterIndex, RowId x) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.github.magnusp.libsql.jdbc;

import java.sql.ParameterMetaData;
import java.sql.SQLException;
import java.sql.Types;

/**
* Parameter metadata for a prepared statement over the Hrana transport.
*
* <p>The placeholder count is derived locally from the SQL text, but the parameter type, precision,
* and scale stay unknown because Hrana has no describe request. Every call checks the owning
* statement first, so metadata obtained before {@link LibsqlStatement#close()} still throws once
* the statement is closed.
*/
final class ParameterMetaDataView implements ParameterMetaData {

private final LibsqlStatement statement;
private final int parameterCount;

ParameterMetaDataView(LibsqlStatement statement, int parameterCount) {
this.statement = statement;
this.parameterCount = parameterCount;
}

@Override
public int getParameterCount() throws SQLException {
statement.checkClosed();
return parameterCount;
}

@Override
public int isNullable(int param) throws SQLException {
checkParameterIndex(param);
return parameterNullableUnknown;
}

@Override
public boolean isSigned(int param) throws SQLException {
checkParameterIndex(param);
return true;
}

@Override
public int getPrecision(int param) throws SQLException {
checkParameterIndex(param);
return 0;
}

@Override
public int getScale(int param) throws SQLException {
checkParameterIndex(param);
return 0;
}

@Override
public int getParameterType(int param) throws SQLException {
checkParameterIndex(param);
// Hrana has no describe statement, so a parameter type is unknown until it is executed.
return Types.OTHER;
}

@Override
public String getParameterTypeName(int param) throws SQLException {
checkParameterIndex(param);
return "OTHER";
}

@Override
public String getParameterClassName(int param) throws SQLException {
checkParameterIndex(param);
return Object.class.getName();
}

@Override
public int getParameterMode(int param) throws SQLException {
checkParameterIndex(param);
return parameterModeIn;
}

private void checkParameterIndex(int param) throws SQLException {
statement.checkClosed();
if (param < 1 || param > parameterCount) {
throw new SQLException("Parameter index out of range: " + param);
}
}

// The statement, not the view, owns the Wrapper surface, so unwrap still reaches the statement.

@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return statement.unwrap(iface);
}

@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return statement.isWrapperFor(iface);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,92 @@
}
}

@Test
void getParameterMetaDataReturnsAViewWithThePlaceholderCount() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
try (LibsqlPreparedStatement statement = new LibsqlPreparedStatement(
connection, "select id from t where id = ? and name = ?")) {
ParameterMetaData meta = statement.getParameterMetaData();

assertThat(meta).isNotNull();
assertThat(meta.getParameterCount()).isEqualTo(2);
}
}

@Test
void getParameterMetaDataAfterCloseThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select ?");
statement.close();

assertThatThrownBy(statement::getParameterMetaData)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
}

@Test
void parameterMetadataViewTakenBeforeCloseStillThrowsAfterClose() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select ?");
ParameterMetaData meta = statement.getParameterMetaData();
statement.close();

assertThatThrownBy(meta::getParameterCount)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
assertThatThrownBy(() -> meta.getParameterType(1))
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
}

@Test
void parameterMetadataRejectsAnOutOfRangeIndex() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
try (LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select ?")) {
ParameterMetaData meta = statement.getParameterMetaData();

assertThatThrownBy(() -> meta.getParameterType(0))
.isInstanceOf(SQLException.class)
.hasMessage("Parameter index out of range: 0");
}
}

@Test
void preparedStatementRemainsAParameterMetaData() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
try (LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select ?")) {
assertThat(statement).isInstanceOf(ParameterMetaData.class);
}
}

@Test
void getParameterMetaDataReturnsTheSameCachedView() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
try (LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select ?")) {
ParameterMetaData first = statement.getParameterMetaData();
ParameterMetaData second = statement.getParameterMetaData();

assertThat(first).isSameAs(second);
}
}

@Test
void parameterMetadataUnwrapReachesTheStatement() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
try (LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select ?")) {
ParameterMetaData meta = statement.getParameterMetaData();

assertThat(meta.isWrapperFor(ParameterMetaData.class)).isTrue();
assertThat(meta.unwrap(ParameterMetaData.class)).isSameAs(statement);
}
}

@Test
void clearParametersBindsNullArguments() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
Expand Down Expand Up @@ -325,7 +411,7 @@
4,
new ByteArrayInputStream("ascii".getBytes(StandardCharsets.US_ASCII)),
"ascii".length());
statement.setUnicodeStream(

Check warning on line 414 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java

View workflow job for this annotation

GitHub Actions / Xerial Control (reference probe)

setUnicodeStream(int,java.io.InputStream,int) in com.github.magnusp.libsql.jdbc.LibsqlPreparedStatement has been deprecated

Check warning on line 414 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 26)

setUnicodeStream(int,java.io.InputStream,int) in com.github.magnusp.libsql.jdbc.LibsqlPreparedStatement has been deprecated

Check warning on line 414 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 25)

setUnicodeStream(int,java.io.InputStream,int) in com.github.magnusp.libsql.jdbc.LibsqlPreparedStatement has been deprecated
5,
new ByteArrayInputStream(supplementary.getBytes(StandardCharsets.UTF_8)),
supplementary.getBytes(StandardCharsets.UTF_8).length);
Expand Down
Loading