qortal/src/database/DB.java
catbref 4ce499c444 More database work
No need for DB.executeUsingBytes as it was only a specific use-case for DB.checkedExecute.
Callers now refactored to use DB.checkedExecute instead.
Minor tidying up of BlockTransactions in light of above.

In the HSQLDB database, asset keys/IDs are now "asset_id" (previously: "asset").
Added initial Asset/Order/Trade classes.
Added CreateOrderTransaction class.
Renamed some asset-related fields back to old gen1 names, e.g. haveAmount -> amount, wantAmount -> price.

Added Accounts and AccountBalances to database.

Added get/set confirmed balance support to Account.
Added get/set last reference support to Account.

Added Block.toJSON() - untested at this time.

Fleshed out some Transaction sub-classes' process() and orphan() methods.
Fleshed out PaymentTransaction.isValid().
Added Transaction.delete() - untested.
2018-05-27 14:59:30 +01:00

241 lines
6.5 KiB
Java

package database;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import org.hsqldb.jdbc.JDBCPool;
import com.google.common.primitives.Bytes;
/**
* Helper methods for common database actions.
*
*/
public class DB {
private static JDBCPool connectionPool;
private static String connectionUrl;
/**
* Open connection pool to database using prior set connection URL.
* <p>
* The connection URL <b>must</b> be set via {@link DB#setUrl(String)} before using this call.
*
* @throws SQLException
* @see DB#setUrl(String)
*/
public static void open() throws SQLException {
connectionPool = new JDBCPool();
connectionPool.setUrl(connectionUrl);
}
/**
* Set the database connection URL.
* <p>
* Typical example:
* <p>
* {@code setUrl("jdbc:hsqldb:file:db/qora")}
*
* @param url
*/
public static void setUrl(String url) {
connectionUrl = url;
}
/**
* Return an on-demand Connection from connection pool.
* <p>
* Mostly used in database-read scenarios whereas database-write scenarios, especially multi-statement transactions, are likely to pass around a Connection
* object.
* <p>
* By default HSQLDB will wait up to 30 seconds for a pooled connection to become free.
*
* @return Connection
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
return connectionPool.getConnection();
}
public static void startTransaction(Connection c) throws SQLException {
c.prepareStatement("START TRANSACTION").execute();
}
public static void commit(Connection c) throws SQLException {
c.prepareStatement("COMMIT").execute();
}
public static void rollback(Connection c) throws SQLException {
c.prepareStatement("ROLLBACK").execute();
}
/**
* Shutdown database and close all connections in connection pool.
* <p>
* Note: any attempts to use an existing connection after this point will fail. Also, any attempts to request a connection using {@link DB#getConnection()}
* will fail.
* <p>
* After this method returns, the database <i>can</i> be reopened using {@link DB#open()}.
*
* @throws SQLException
*/
public static void close() throws SQLException {
getConnection().createStatement().execute("SHUTDOWN");
connectionPool.close(0);
}
/**
* Shutdown and delete database, then rebuild it.
* <p>
* See {@link DB#close()} for warnings about connections.
* <p>
* Note that this only rebuilds the database schema, not the data itself.
*
* @throws SQLException
*/
public static void rebuild() throws SQLException {
// Shutdown database and close any access
DB.close();
// Wipe files (if any)
// TODO
// Re-open clean database
DB.open();
// Apply schema updates
DatabaseUpdates.updateDatabase();
}
/**
* Convert InputStream, from ResultSet.getBinaryStream(), into byte[] of set length.
*
* @param inputStream
* @param length
* @return byte[length]
*/
public static byte[] getResultSetBytes(InputStream inputStream, int length) {
// inputStream could be null if database's column's value is null
if (inputStream == null)
return null;
byte[] result = new byte[length];
try {
if (inputStream.read(result) == length)
return result;
} catch (IOException e) {
// Fall-through to return null
}
return null;
}
/**
* Convert InputStream, from ResultSet.getBinaryStream(), into byte[] of unknown length.
*
* @param inputStream
* @return byte[]
*/
public static byte[] getResultSetBytes(InputStream inputStream) {
final int BYTE_BUFFER_LENGTH = 1024;
// inputStream could be null if database's column's value is null
if (inputStream == null)
return null;
byte[] result = new byte[0];
while (true) {
try {
byte[] buffer = new byte[BYTE_BUFFER_LENGTH];
int length = inputStream.read(buffer);
if (length == -1)
break;
result = Bytes.concat(result, Arrays.copyOf(buffer, length));
} catch (IOException e) {
// No more bytes
break;
}
}
return result;
}
/**
* Execute SQL and return ResultSet with but added checking.
* <p>
* <b>Note: calls ResultSet.next()</b> therefore returned ResultSet is already pointing to first row.
*
* @param sql
* @param objects
* @return ResultSet, or null if there are no found rows
* @throws SQLException
*/
public static ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
try (final Connection connection = DB.getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < objects.length; ++i)
// Special treatment for BigDecimals so that they retain their "scale",
// which would otherwise be assumed as 0.
if (objects[i] instanceof BigDecimal)
preparedStatement.setBigDecimal(i + 1, (BigDecimal) objects[i]);
else
preparedStatement.setObject(i + 1, objects[i]);
return checkedExecute(preparedStatement);
}
}
/**
* Execute PreparedStatement and return ResultSet with but added checking.
* <p>
* <b>Note: calls ResultSet.next()</b> therefore returned ResultSet is already pointing to first row.
*
* @param preparedStatement
* @return ResultSet, or null if there are no found rows
* @throws SQLException
*/
public static ResultSet checkedExecute(PreparedStatement preparedStatement) throws SQLException {
if (!preparedStatement.execute())
throw new SQLException("Fetching from database produced no results");
ResultSet resultSet = preparedStatement.getResultSet();
if (resultSet == null)
throw new SQLException("Fetching results from database produced no ResultSet");
if (!resultSet.next())
return null;
return resultSet;
}
/**
* Fetch last value of IDENTITY column after an INSERT statement.
* <p>
* Performs "CALL IDENTITY()" SQL statement to retrieve last value used when INSERTing into a table that has an IDENTITY column.
* <p>
* Typically used after INSERTing NULL as the IDENTIY column's value to fetch what value was actually stored by HSQLDB.
*
* @param connection
* @return Long
* @throws SQLException
*/
public static Long callIdentity(Connection connection) throws SQLException {
PreparedStatement preparedStatement = connection.prepareStatement("CALL IDENTITY()");
ResultSet resultSet = DB.checkedExecute(preparedStatement);
if (resultSet == null)
return null;
return resultSet.getLong(1);
}
}