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 @@ -72,7 +72,7 @@ public ResultSet executeQuery() throws SQLException {
if (result.cols() == null || result.cols().isEmpty()) {
throw new SQLException("Query does not return results");
}
LibsqlResultSet resultSet = new LibsqlResultSet(this, result, sql, false);
LibsqlResultSet resultSet = new LibsqlResultSet(this, limitRows(result), sql, false);
assignResultSet(resultSet, result, sql, true);
return resultSet;
}
Expand Down Expand Up @@ -229,6 +229,7 @@ public boolean execute() throws SQLException {
}

private StmtResult executeCurrent() throws SQLException {
checkEscapeProcessing(sql);
checkClosed();
beginExecution();
return connection.executeStatement(buildCurrentStmt());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@
* <li>{@link #closeOnCompletion()} and {@link #isCloseOnCompletion()} throw {@code
* SQLException("Statement is closed")} after the statement is closed.
* <li>{@link #close()} closes the current result set and generated-key results.
* <li>Optional statement settings are pinned per setter. {@link #setMaxRows(int)} stores a limit
* and drops the excess rows from the result set, matching {@code java.sql} and sqlite-jdbc
* 3.53.4.0. {@link #setEscapeProcessing(boolean)} stores the java.sql flag, which starts
* enabled: while it is enabled, SQL that contains a JDBC escape sequence throws {@link
* SQLFeatureNotSupportedException} rather than reaching sqld untranslated. {@link
* #setMaxFieldSize(int)} and {@link #setQueryTimeout(int)} accept the zero that means no
* limit and throw for a limit the driver cannot enforce. {@link #setCursorName(String)}
* always throws: Hrana has no positioned cursor. sqlite-jdbc 3.53.4.0 instead accepts the
* last three silently, and its query timeout drives SQLite's busy timeout, which is a lock
* wait rather than a query deadline.
* <li>Matching xerial's execute-after-close behavior, {@link #execute(String)}, {@link
* #executeQuery(String)}, {@link #executeUpdate(String)}, and {@link #getConnection()} keep
* working after {@link #close()}. This deliberately copies a likely xerial bug for drop-in
Expand Down Expand Up @@ -66,6 +76,8 @@ public class LibsqlStatement implements Statement {
private boolean resultSetRequested = false;
private boolean closing = false;
private int fetchSize = 0;
private int maxRows = 0;
private boolean escapeProcessing = true;

public LibsqlStatement(LibsqlConnection connection) {
this.connection = connection;
Expand Down Expand Up @@ -94,7 +106,7 @@ public ResultSet executeQuery(String sql) throws SQLException {
if (result.cols() == null || result.cols().isEmpty()) {
throw new SQLException("Query did not produce a ResultSet: " + sql);
}
LibsqlResultSet resultSet = new LibsqlResultSet(this, result, sql, false);
LibsqlResultSet resultSet = new LibsqlResultSet(this, limitRows(result), sql, false);
assignResultSet(resultSet, result, sql, true);
return resultSet;
}
Expand Down Expand Up @@ -139,7 +151,7 @@ public boolean execute(String sql) throws SQLException {
protected boolean executeResult(String sql, StmtResult result) throws SQLException {
boolean hasResultSet = result.cols() != null && !result.cols().isEmpty();
LibsqlResultSet resultSet =
hasResultSet ? new LibsqlResultSet(this, result, sql, false) : null;
hasResultSet ? new LibsqlResultSet(this, limitRows(result), sql, false) : null;

if (hasResultSet && !isClosed()) {
beforeResultSetAssignment();
Expand All @@ -165,10 +177,106 @@ protected boolean executeResult(String sql, StmtResult result) throws SQLExcepti
}

private StmtResult executeSql(String sql) throws SQLException {
checkEscapeProcessing(sql);
beginExecution();
return connection.executeStatement(new Stmt(sql));
}

/**
* Rejects SQL that needs JDBC escape substitution while escape processing is on. The driver
* sends SQL to sqld verbatim, so a sequence such as {@code {fn abs(-1)}} would fail as SQLite
* syntax instead of being translated. Turning escape processing off is how a caller states
* that it sends raw SQL.
*/
protected void checkEscapeProcessing(String sql) throws SQLException {
boolean enabled;
synchronized (stateLock) {
enabled = escapeProcessing;
}
if (enabled && hasEscapeSequence(sql)) {
throw new SQLFeatureNotSupportedException("JDBC escape sequences are not supported");
}
}

/**
* Whether the SQL contains a JDBC escape sequence. Braces inside string literals, quoted
* identifiers, and comments do not count, and a brace that does not start with an escape code
* is left to the server.
*/
private static boolean hasEscapeSequence(String sql) {
if (sql == null || sql.indexOf('{') < 0) {
return false;
}
int n = sql.length();
int i = 0;
while (i < n) {
char c = sql.charAt(i);
if (c == '\'' || c == '"' || c == '`') {
i++;
while (i < n) {
if (sql.charAt(i) == c) {
if (i + 1 < n && sql.charAt(i + 1) == c) {
i += 2;
continue;
}
i++;
break;
}
i++;
}
} else if (c == '[') {
i++;
while (i < n && sql.charAt(i) != ']') {
i++;
}
i++;
} else if (c == '-' && i + 1 < n && sql.charAt(i + 1) == '-') {
i += 2;
while (i < n && sql.charAt(i) != '\n') {
i++;
}
} else if (c == '/' && i + 1 < n && sql.charAt(i + 1) == '*') {
i += 2;
while (i + 1 < n && !(sql.charAt(i) == '*' && sql.charAt(i + 1) == '/')) {
i++;
}
i += 2;
} else if (c == '{') {
int code = i + 1;
while (code < n && Character.isWhitespace(sql.charAt(code))) {
code++;
}
if (code < n && "dDtTfFoOeEcC".indexOf(sql.charAt(code)) >= 0) {
return true;
}
i++;
} else {
i++;
}
}
return false;
}

/**
* Drops rows above the {@link #setMaxRows(int)} limit. java.sql requires the excess rows to be
* dropped silently from the result set, not from the statement that produced it, so the server
* still runs the full query.
*/
protected StmtResult limitRows(StmtResult result) {
int limit;
synchronized (stateLock) {
limit = maxRows;
}
if (limit <= 0 || result == null || result.rows() == null || result.rows().size() <= limit) {
return result;
}
return new StmtResult(
result.cols(),
new ArrayList<>(result.rows().subList(0, limit)),
result.affectedRowCount(),
result.lastInsertRowid());
}

/**
* Resets the state every execution path starts from: the previous result set is closed and the
* previous generated keys are discarded. Without this, a path that does not produce keys of
Expand Down Expand Up @@ -411,6 +519,7 @@ public boolean getMoreResults() throws SQLException {
@Override
public void addBatch(String sql) throws SQLException {
checkClosed();
checkEscapeProcessing(sql);
batchStatements.add(new Stmt(sql));
}

Expand Down Expand Up @@ -485,16 +594,53 @@ public boolean isClosed() throws SQLException {
}

@Override public int getMaxFieldSize() throws SQLException { return 0; }
@Override public void setMaxFieldSize(int max) throws SQLException {}
@Override public int getMaxRows() throws SQLException { return 0; }
@Override public void setMaxRows(int max) throws SQLException {}
@Override public void setEscapeProcessing(boolean enable) throws SQLException {}
@Override
public void setMaxFieldSize(int max) throws SQLException {
if (max < 0) {
throw new SQLException("max field size must be >= 0");
}
if (max > 0) {
throw new SQLFeatureNotSupportedException("Column size limits are not supported");
}
}
@Override
public int getMaxRows() throws SQLException {
synchronized (stateLock) {
return maxRows;
}
}
@Override
public void setMaxRows(int max) throws SQLException {
if (max < 0) {
throw new SQLException("max row count must be >= 0");
}
synchronized (stateLock) {
maxRows = max;
}
}
@Override
public void setEscapeProcessing(boolean enable) throws SQLException {
synchronized (stateLock) {
escapeProcessing = enable;
}
}
@Override public int getQueryTimeout() throws SQLException { return 0; }
@Override public void setQueryTimeout(int seconds) throws SQLException {}
@Override
public void setQueryTimeout(int seconds) throws SQLException {
if (seconds < 0) {
throw new SQLException("query timeout must be >= 0");
}
if (seconds > 0) {
throw new SQLFeatureNotSupportedException("Query timeouts are not supported");
}
}
@Override public void cancel() throws SQLException {}
@Override public SQLWarning getWarnings() throws SQLException { return null; }
@Override public void clearWarnings() throws SQLException {}
@Override public void setCursorName(String name) throws SQLException {}
@Override
public void setCursorName(String name) throws SQLException {
throw new SQLFeatureNotSupportedException("Cursor names are not supported");
}
@Override public void setFetchDirection(int direction) throws SQLException {}
@Override public int getFetchDirection() throws SQLException { return ResultSet.FETCH_FORWARD; }
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -24,6 +26,15 @@ class LibsqlPreparedStatementTest {

private static final StmtResult NO_ROWS = new StmtResult(List.of(), List.of(), 1, 1L);

private static final StmtResult THREE_ROWS = new StmtResult(
List.of(new Col("id", "INTEGER")),
List.of(
new Row(List.of(Value.of(1L))),
new Row(List.of(Value.of(2L))),
new Row(List.of(Value.of(3L)))),
0,
null);

@Test
void executeQueryOnInsertThrowsWithoutExecuting() throws SQLException {
RecordingConnection connection = new RecordingConnection(NO_ROWS);
Expand Down Expand Up @@ -102,8 +113,44 @@ void executeUpdateOnWithSelectThrowsWithoutExecuting() throws SQLException {
assertThat(connection.executeCalls).isZero();
}

@Test
void setMaxRowsLimitsPreparedQueryAndGetMaxRowsReadsItBack() throws SQLException {
RecordingConnection connection = new RecordingConnection(THREE_ROWS);
try (LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select id from t")) {
statement.setMaxRows(2);

assertThat(statement.getMaxRows()).isEqualTo(2);
ResultSet resultSet = statement.executeQuery();
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getLong(1)).isEqualTo(1L);
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getLong(1)).isEqualTo(2L);
assertThat(resultSet.next()).isFalse();
}
}

@Test
void escapeProcessingAppliesToPreparedSql() throws SQLException {
RecordingConnection connection = new RecordingConnection(THREE_ROWS);
try (LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select {fn abs(-1)} from t")) {
assertThatThrownBy(statement::executeQuery)
.isInstanceOf(SQLFeatureNotSupportedException.class)
.hasMessage("JDBC escape sequences are not supported");
assertThat(connection.executeCalls).isZero();

statement.setEscapeProcessing(false);
statement.executeQuery();

assertThat(connection.executedSql)
.containsExactly("select {fn abs(-1)} from t");
}
}

private static final class RecordingConnection extends LibsqlConnection {
private final StmtResult result;
private final List<String> executedSql = new ArrayList<>();
private int executeCalls;

private RecordingConnection(StmtResult result) {
Expand All @@ -114,6 +161,7 @@ private RecordingConnection(StmtResult result) {
@Override
public StmtResult executeStatement(Stmt stmt) {
executeCalls++;
executedSql.add(stmt.sql());
return result;
}
}
Expand Down
Loading
Loading