diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java
index a3fb301..4496eac 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java
@@ -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;
@@ -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.
*
- *
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.
+ *
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 parameters = new ArrayList<>();
@@ -58,6 +57,7 @@ public LibsqlPreparedStatement(LibsqlConnection connection, String sql) {
this.sql = sql;
this.returnsResultSet = returnsResultSet(sql);
this.parameterCount = SqlTokenizer.placeholderCount(sql);
+ this.parameterMetaData = new ParameterMetaDataView(this, parameterCount);
}
private void setParam(int parameterIndex, Value value) throws SQLException {
@@ -363,73 +363,18 @@ public void setURL(int parameterIndex, URL x) throws SQLException {
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 {
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/ParameterMetaDataView.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/ParameterMetaDataView.java
new file mode 100644
index 0000000..c984c82
--- /dev/null
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/ParameterMetaDataView.java
@@ -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.
+ *
+ * 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 unwrap(Class iface) throws SQLException {
+ return statement.unwrap(iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return statement.isWrapperFor(iface);
+ }
+}
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java
index aba6cf0..8546e2c 100644
--- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java
@@ -283,6 +283,92 @@ void parameterTypesAreUnknownWithoutADescription() throws SQLException {
}
}
+ @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);