blob: cc24d16376988fa476bacb5d47e16f03136b83ba [file] [log] [blame]
// Copyright 2024 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.remote.disk;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.jni.JniLoader;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;
import java.util.HashSet;
import javax.annotation.Nullable;
/**
* A wrapper around the SQLite C API.
*
* <p>Exposes only the subset of the SQLite C API required by Bazel.
*/
public final class Sqlite {
static {
JniLoader.loadJni();
}
private Sqlite() {}
/**
* Opens a connection to a database, creating it in an empty state if it doesn't yet exist.
*
* @param path the path to the database
* @throws IOException if an error occurred while opening the connection
*/
public static Connection newConnection(Path path) throws IOException {
checkNotNull(path);
return new Connection(path);
}
/**
* A connection to a database.
*
* <p>This class is *not* thread-safe. A single connection should be used by a single thread. Use
* separate connections to the same database for multithreaded access.
*/
public static final class Connection implements AutoCloseable {
// C pointer to the `sqlite3` handle. Zero means the connection has been closed.
private long connPtr;
// Tracks open statements so they can also be closed when the connection is closed.
private final HashSet<Statement> openStatements = new HashSet<>();
private Connection(Path path) throws IOException {
this.connPtr = openConn(path.getPathString());
}
/**
* Closes the connection, rendering it unusable.
*
* <p>As a convenience, every {@link Statement} associated with the connection is also closed,
* with any errors silently ignored.
*
* <p>Multiple calls have no further effect.
*
* @throws IOException if an error occurred while closing the connection
*/
@Override
public void close() throws IOException {
if (connPtr != 0) {
// SQLite won't let us close the connection before first closing associated statements.
for (Statement stmt : ImmutableList.copyOf(openStatements)) {
stmt.close();
}
closeConn(connPtr);
connPtr = 0;
}
}
/**
* Creates a new {@link Statement}.
*
* @param sql a string containing a single SQL statement
* @throws IOException if the string contains multiple SQL statements, or the single SQL
* statement could not be parsed and validated
*/
public Statement newStatement(String sql) throws IOException {
checkState(connPtr != 0, "newStatement() called in invalid state");
Statement stmt = new Statement(this, sql);
openStatements.add(stmt);
return stmt;
}
/**
* Executes a statement not expected to return a result.
*
* <p>For statements expected to return a result, or statements that will be executed multiple
* times, use {@link #newStatement}.
*
* @throws IOException if the string contains multiple SQL statements; or the single SQL
* statement could not be parsed and validated; or the statement returned a non-empty
* result; or an execution error occurred
*/
public void executeUpdate(String sql) throws IOException {
try (Statement stmt = new Statement(this, sql)) {
stmt.executeUpdate();
}
}
}
/**
* A prepared statement.
*
* <p>Provides a facility to bind values to parameters and execute the statement by calling one of
* {@link #executeQuery} or {@link #executeUpdate}. The same statement may be executed multiple
* times, with its parameters possibly bound to different values, but there can be at most one
* ongoing execution at a time.
*
* <p>Parameters that haven't been bound or whose binding has been cleared behave as null.
*/
public static final class Statement implements AutoCloseable {
// The connection owning this statement.
private Connection conn;
// C pointer to te `sqlite3_stmt` handle. Zero means the statement has been closed.
private long stmtPtr;
// The result of current execution, or null if no execution is ongoing.
@Nullable private Result currentResult;
private Statement(Connection conn, String sql) throws IOException {
this.conn = conn;
stmtPtr = prepareStmt(conn.connPtr, sql);
if (stmtPtr == -1) {
// Special value returned when a multi-statement string is detected.
throw new IOException("unsupported multi-statement string");
}
}
/**
* Closes the statement, rendering it unusable.
*
* <p>A {@link Result} previously returned by {@link #executeQuery} is also closed, with any
* error silently ignored.
*
* <p>Multiple calls have no additional effect.
*/
@Override
public void close() {
if (stmtPtr != 0) {
if (currentResult != null) {
try {
currentResult.close();
} catch (IOException e) {
// Intentionally ignored: an error always pertains to a particular execution, and should
// only be reported when Result#close is called directly.
}
}
try {
finalizeStmt(stmtPtr);
} catch (IOException e) {
// Cannot occur since the statement has been reset by Result#close.
throw new IllegalStateException("unexpected exception thrown by finalize", e);
}
conn.openStatements.remove(this);
conn = null;
stmtPtr = 0;
}
}
/**
* Binds a long value to the statement's i-th parameter, counting from 1.
*
* <p>The binding remains in effect until it is cleared or another value is bound to the same
* parameter.
*
* <p>Must not be called after {@link #executeQuery} returns a {@link Result} and before the
* {@link Result} is closed.
*/
public void bindLong(int i, long val) throws IOException {
checkState(stmtPtr != 0 && currentResult == null, "bindLong() called in invalid state");
bindStmtLong(stmtPtr, i, val);
}
/**
* Binds a double value to the statement's i-th parameter, counting from 1.
*
* <p>The binding remains in effect until it is cleared or another value is bound to the same
* parameter.
*
* <p>Must not be called after {@link #executeQuery} returns a {@link Result} and before the
* {@link Result} is closed.
*/
public void bindDouble(int i, double val) throws IOException {
checkState(stmtPtr != 0 && currentResult == null, "bindDouble() called in invalid state");
bindStmtDouble(stmtPtr, i, val);
}
/**
* Binds a non-null string value to the statement's i-th parameter, counting from 1.
*
* <p>The binding remains in effect until it is cleared or another value is bound to the same
* parameter.
*
* <p>Must not be called after {@link #executeQuery} returns a {@link Result} and before the
* {@link Result} is closed.
*/
public void bindString(int i, String val) throws IOException {
checkState(stmtPtr != 0 && currentResult == null, "bindString() called in invalid state");
checkNotNull(val);
bindStmtString(stmtPtr, i, val);
}
/**
* Clears the i-th binding.
*
* <p>Must not be called after {@link #executeQuery} returns a {@link Result} and before the
* {@link Result} is closed.
*/
public void clearBinding(int i) throws IOException {
checkState(stmtPtr != 0 && currentResult == null, "clearBinding() called in invalid state");
clearStmtBinding(stmtPtr, i);
}
/**
* Clears all bindings.
*
* <p>Must not be called after {@link #executeQuery} returns a {@link Result} and before the
* {@link Result} is closed.
*/
public void clearBindings() throws IOException {
checkState(stmtPtr != 0 && currentResult == null, "clearBindings() called in invalid state");
clearStmtBindings(stmtPtr);
}
/**
* Executes a statement expected to return a result.
*
* <p>Execution doesn't actually start until the first call to {@link Result#next}.
*
* <p>Must not be called again until the returned {@link Result} has been closed.
*/
public Result executeQuery() {
checkState(stmtPtr != 0 && currentResult == null, "executeQuery() called in invalid state");
currentResult = new Result(this);
return currentResult;
}
/**
* Executes a statement not expected to return a result.
*
* <p>Must not be called after {@link #executeQuery} until the returned {@link Result} has been
* closed.
*
* @throws IOException if the statement returned a non-empty result or an execution error
* occurred
*/
public void executeUpdate() throws IOException {
checkState(stmtPtr != 0 && currentResult == null, "executeUpdate() called in invalid state");
currentResult = new Result(this);
try {
if (currentResult.next()) {
throw new IOException("unexpected non-empty result");
}
} finally {
currentResult.close();
}
}
}
/**
* The result of executing a statement.
*
* <p>Acts as a cursor to iterate over result rows and obtain the corresponding column values. The
* cursor is initially positioned before the first row. A call to {@link #next} moves the cursor
* to the next row, returning {@code false} once no more results are available. If a call to
* {@link #next} returns {@code true}, the getter methods may be called to retrieve the column
* values for the current row.
*/
public static final class Result implements AutoCloseable {
// The statement owning this result.
private Statement stmt;
enum State {
START, // next() not yet called
CONTINUE, // last call to next() returned true
DONE, // last call to next() returned false, but close() not yet called
ERROR, // last call to next() threw, but close() not yet called
CLOSED // close() was called
}
private State state = State.START;
private Result(Statement stmt) {
this.stmt = stmt;
}
/**
* Closes the result, rendering it unusable.
*
* <p>Multiple calls have no additional effect.
*
* @throws IOException if an error occurred while finishing execution
*/
@Override
public void close() throws IOException {
if (state != State.CLOSED) {
try {
resetStmt(stmt.stmtPtr);
} catch (IOException e) {
// Some statements may throw an error only after the result has been fully consumed.
// However, if the error has already been thrown by next(), don't throw it again.
if (state != State.ERROR) {
throw e;
}
} finally {
stmt.currentResult = null;
stmt = null;
state = State.CLOSED;
}
}
}
/**
* Advances the cursor the next row.
*
* <p>Must not be called further after {@code false} is returned or an exception is thrown.
*
* @return whether another row was available
* @throws IOException if an error occurred while executing the statement
*/
public boolean next() throws IOException {
checkState(state == State.START || state == State.CONTINUE, "next() called in invalid state");
try {
boolean available = stepStmt(stmt.stmtPtr);
state = available ? State.CONTINUE : State.DONE;
return available;
} catch (IOException e) {
state = State.ERROR;
throw e;
}
}
/**
* Returns whether the i-th column for the current result row is null.
*
* <p>WARNING: this must be called before any of the getter methods. Calling a getter method may
* cause a conversion to occur, after which the return value of this method is unspecified.
*/
public boolean isNull(int i) throws IOException {
checkState(state == State.CONTINUE, "isNull() called in invalid state");
return columnIsNull(stmt.stmtPtr, i);
}
/**
* Reads the i-th column for the current result row, starting from 0, as a long.
*
* <p>If the column is not of long type, a conversion occurs. In particular, null is converted
* to 0.
*
* <p>Must not be called unless the last call to {@link #next} returned {@code true}.
*/
public long getLong(int i) throws IOException {
checkState(state == State.CONTINUE, "getLong() called in invalid state");
return columnLong(stmt.stmtPtr, i);
}
/**
* Reads the i-th column for the current result row, starting from 0, as a double.
*
* <p>If the column is not of double type, a conversion occurs. In particular, null is converted
* to 0.0.
*
* <p>Must not be called unless the last call to {@link #next} returned {@code true}.
*/
public double getDouble(int i) throws IOException {
checkState(state == State.CONTINUE, "getDouble() called in invalid state");
return columnDouble(stmt.stmtPtr, i);
}
/**
* Reads the i-th column for the current result row, starting from 0, as a string.
*
* <p>If the column is not of string type, a conversion occurs. In particular, null is converted
* to the empty string.
*
* <p>Must not be called unless the last call to {@link #next} returned {@code true}.
*/
public String getString(int i) throws IOException {
checkState(state == State.CONTINUE, "getString() called in invalid state");
return columnString(stmt.stmtPtr, i);
}
}
private static native long openConn(String path) throws IOException;
private static native void closeConn(long conn) throws IOException;
private static native long prepareStmt(long conn, String sql) throws IOException;
private static native void bindStmtLong(long stmt, int i, long value) throws IOException;
private static native void bindStmtDouble(long stmt, int i, double value) throws IOException;
private static native void bindStmtString(long stmt, int i, String value) throws IOException;
private static native void clearStmtBinding(long stmt, int i) throws IOException;
private static native void clearStmtBindings(long stmt) throws IOException;
private static native boolean stepStmt(long stmt) throws IOException;
private static native boolean columnIsNull(long stmt, int i) throws IOException;
private static native long columnLong(long stmt, int i) throws IOException;
private static native double columnDouble(long stmt, int i) throws IOException;
private static native String columnString(long stmt, int i) throws IOException;
private static native void resetStmt(long stmt) throws IOException;
private static native void finalizeStmt(long stmt) throws IOException;
}