From 3094ec3c26e194e223f9dc358fdc0d6b2719d6a2 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 7 May 2020 12:16:22 +0100 Subject: [PATCH] Massive clean-up of DB & conversion to long for timestamps Collated all development changes to DB so now we build initial DB structure directly with final layout. i.e. no ALTER TABLE, etc. Reordered HSQLDB 'CREATE TYPE' statements into alphabetical order for easier maintainability. Replaced TIMESTAMP WITH TIME ZONE with simple BIGINT ("EpochMillis"). Timezone conversion is now a presentation task, rather than having pretty values in database. Removed associated conversion methods, like toOffsetDateTime(), fromOffsetDateTime() and getZonedTimestampMilli(). Renamed some DB columns to make them more obviously timestamps, like: Names.registered is now Names.registered_when. Removed IFNULL(balance, 0) from HSQLDBAccountRepository as balances are never null, or actually never 0 either. Added more tests to increase API call, and hence repository, coverage. Removed unused "milestone block" from Transactions. --- .../repository/hsqldb/HSQLDBATRepository.java | 43 +- .../hsqldb/HSQLDBAccountRepository.java | 8 +- .../hsqldb/HSQLDBAssetRepository.java | 74 +- .../hsqldb/HSQLDBBlockRepository.java | 56 +- .../hsqldb/HSQLDBChatRepository.java | 10 +- .../hsqldb/HSQLDBDatabaseUpdates.java | 1071 ++++++----------- .../hsqldb/HSQLDBGroupRepository.java | 161 +-- .../hsqldb/HSQLDBNameRepository.java | 61 +- .../hsqldb/HSQLDBNetworkRepository.java | 24 +- .../repository/hsqldb/HSQLDBRepository.java | 54 +- .../qortal/repository/hsqldb/HSQLDBSaver.java | 7 +- .../hsqldb/HSQLDBVotingRepository.java | 8 +- .../HSQLDBTransactionRepository.java | 43 +- .../qortal/test/api/AddressesApiTests.java | 5 + .../org/qortal/test/api/AdminApiTests.java | 5 + .../qortal/test/api/ArbitraryApiTests.java | 43 + .../org/qortal/test/api/AssetsApiTests.java | 22 +- .../org/qortal/test/api/BlockApiTests.java | 88 +- .../org/qortal/test/api/NamesApiTests.java | 91 ++ .../org/qortal/test/common/ApiCommon.java | 2 + .../org/qortal/test/naming/MiscTests.java | 42 + 21 files changed, 904 insertions(+), 1014 deletions(-) create mode 100644 src/test/java/org/qortal/test/api/ArbitraryApiTests.java create mode 100644 src/test/java/org/qortal/test/api/NamesApiTests.java create mode 100644 src/test/java/org/qortal/test/naming/MiscTests.java diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index feae3b5c..f6de4fb4 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -25,7 +22,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATData fromATAddress(String atAddress) throws DataException { - String sql = "SELECT creator, creation, version, asset_id, code_bytes, code_hash, " + String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, " + "is_sleeping, sleep_until_height, is_finished, had_fatal_error, " + "is_frozen, frozen_balance " + "FROM ATs " @@ -36,7 +33,7 @@ public class HSQLDBATRepository implements ATRepository { return null; byte[] creatorPublicKey = resultSet.getBytes(1); - long creation = getZonedTimestampMilli(resultSet, 2); + long created = resultSet.getLong(2); int version = resultSet.getInt(3); long assetId = resultSet.getLong(4); byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB @@ -55,7 +52,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + return new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); } catch (SQLException e) { throw new DataException("Unable to fetch AT from repository", e); @@ -73,12 +70,12 @@ public class HSQLDBATRepository implements ATRepository { @Override public List getAllExecutableATs() throws DataException { - String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, code_hash, " + String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, " + "is_sleeping, sleep_until_height, had_fatal_error, " + "is_frozen, frozen_balance " + "FROM ATs " + "WHERE is_finished = false " - + "ORDER BY creation ASC"; + + "ORDER BY created_when ASC"; List executableATs = new ArrayList<>(); @@ -91,7 +88,7 @@ public class HSQLDBATRepository implements ATRepository { do { String atAddress = resultSet.getString(1); byte[] creatorPublicKey = resultSet.getBytes(2); - long creation = getZonedTimestampMilli(resultSet, 3); + long created = resultSet.getLong(3); int version = resultSet.getInt(4); long assetId = resultSet.getLong(5); byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB @@ -109,7 +106,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); executableATs.add(atData); @@ -124,7 +121,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public List getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT AT_address, creator, creation, version, asset_id, code_bytes, ") + sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ") .append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ") .append("is_frozen, frozen_balance ") .append("FROM ATs ") @@ -133,7 +130,7 @@ public class HSQLDBATRepository implements ATRepository { if (isExecutable != null) sql.append("AND is_finished = ").append(isExecutable ? "false" : "true"); - sql.append(" ORDER BY creation "); + sql.append(" ORDER BY created_when "); if (reverse != null && reverse) sql.append("DESC"); @@ -148,7 +145,7 @@ public class HSQLDBATRepository implements ATRepository { do { String atAddress = resultSet.getString(1); byte[] creatorPublicKey = resultSet.getBytes(2); - long creation = getZonedTimestampMilli(resultSet, 3); + long created = resultSet.getLong(3); int version = resultSet.getInt(4); long assetId = resultSet.getLong(5); byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB @@ -167,7 +164,7 @@ public class HSQLDBATRepository implements ATRepository { if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); matchingATs.add(atData); @@ -202,7 +199,7 @@ public class HSQLDBATRepository implements ATRepository { public void save(ATData atData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); - saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(atData.getCreation())) + saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("created_when", atData.getCreation()) .bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()) .bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash()) .bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight()) @@ -230,7 +227,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException { - String sql = "SELECT creation, state_data, state_hash, fees, is_initial " + String sql = "SELECT created_when, state_data, state_hash, fees, is_initial " + "FROM ATStates " + "WHERE AT_address = ? AND height = ? " + "LIMIT 1"; @@ -239,13 +236,13 @@ public class HSQLDBATRepository implements ATRepository { if (resultSet == null) return null; - long creation = getZonedTimestampMilli(resultSet, 1); + long created = resultSet.getLong(1); byte[] stateData = resultSet.getBytes(2); // Actually BLOB byte[] stateHash = resultSet.getBytes(3); long fees = resultSet.getLong(4); boolean isInitial = resultSet.getBoolean(5); - return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial); + return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial); } catch (SQLException e) { throw new DataException("Unable to fetch AT state from repository", e); } @@ -253,7 +250,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATStateData getLatestATState(String atAddress) throws DataException { - String sql = "SELECT height, creation, state_data, state_hash, fees, is_initial " + String sql = "SELECT height, created_when, state_data, state_hash, fees, is_initial " + "FROM ATStates " + "WHERE AT_address = ? " + "ORDER BY height DESC " @@ -264,13 +261,13 @@ public class HSQLDBATRepository implements ATRepository { return null; int height = resultSet.getInt(1); - long creation = getZonedTimestampMilli(resultSet, 2); + long created = resultSet.getLong(2); byte[] stateData = resultSet.getBytes(3); // Actually BLOB byte[] stateHash = resultSet.getBytes(4); long fees = resultSet.getLong(5); boolean isInitial = resultSet.getBoolean(6); - return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial); + return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial); } catch (SQLException e) { throw new DataException("Unable to fetch latest AT state from repository", e); } @@ -281,7 +278,7 @@ public class HSQLDBATRepository implements ATRepository { String sql = "SELECT AT_address, state_hash, fees, is_initial " + "FROM ATStates " + "WHERE height = ? " - + "ORDER BY creation ASC"; + + "ORDER BY created_when ASC"; List atStates = new ArrayList<>(); @@ -315,7 +312,7 @@ public class HSQLDBATRepository implements ATRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates"); saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) - .bind("creation", toOffsetDateTime(atStateData.getCreation())).bind("state_data", atStateData.getStateData()) + .bind("created_when", atStateData.getCreation()).bind("state_data", atStateData.getStateData()) .bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees()) .bind("is_initial", atStateData.isInitial()); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 93714de1..afcd12b5 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -290,7 +290,7 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public AccountBalanceData getBalance(String address, long assetId) throws DataException { - String sql = "SELECT IFNULL(balance, 0) FROM AccountBalances WHERE account = ? AND asset_id = ? LIMIT 1"; + String sql = "SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ? LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, address, assetId)) { if (resultSet == null) @@ -307,7 +307,8 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public List getAssetBalances(long assetId, Boolean excludeZero) throws DataException { StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT account, IFNULL(balance, 0) FROM AccountBalances WHERE asset_id = ?"); + + sql.append("SELECT account, balance FROM AccountBalances WHERE asset_id = ?"); if (excludeZero != null && excludeZero) sql.append(" AND balance != 0"); @@ -334,7 +335,8 @@ public class HSQLDBAccountRepository implements AccountRepository { public List getAssetBalances(List addresses, List assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT account, asset_id, IFNULL(balance, 0), asset_name FROM "); + + sql.append("SELECT account, asset_id, balance, asset_name FROM "); final boolean haveAddresses = addresses != null && !addresses.isEmpty(); final boolean haveAssetIds = assetIds != null && !assetIds.isEmpty(); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java index 30af058b..878bab7a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAssetRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -133,14 +130,14 @@ public class HSQLDBAssetRepository implements AssetRepository { } @Override - public List getRecentAssetIds(long start) throws DataException { + public List getRecentAssetIds(long startTimestamp) throws DataException { String sql = "SELECT asset_id FROM IssueAssetTransactions JOIN Assets USING (asset_id) " + "JOIN Transactions USING (signature) " - + "WHERE creation >= ?"; + + "WHERE created_when >= ?"; List assetIds = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute(sql, HSQLDBRepository.toOffsetDateTime(start))) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, startTimestamp)) { if (resultSet == null) return assetIds; @@ -199,7 +196,8 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public OrderData fromOrderId(byte[] orderId) throws DataException { - String sql = "SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + String sql = "SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, ordered_when, " + + "is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + "FROM AssetOrders " + "JOIN Assets AS HaveAsset ON HaveAsset.asset_id = have_asset_id " + "JOIN Assets AS WantAsset ON WantAsset.asset_id = want_asset_id " @@ -215,13 +213,14 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(4); long fulfilled = resultSet.getLong(5); long price = resultSet.getLong(6); - long timestamp = getZonedTimestampMilli(resultSet, 7); + long timestamp = resultSet.getLong(7); boolean isClosed = resultSet.getBoolean(8); boolean isFulfilled = resultSet.getBoolean(9); String haveAssetName = resultSet.getString(10); String wantAssetName = resultSet.getString(11); - return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled, haveAssetName, wantAssetName); + return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, + timestamp, isClosed, isFulfilled, haveAssetName, wantAssetName); } catch (SQLException e) { throw new DataException("Unable to fetch asset order from repository", e); } @@ -242,14 +241,14 @@ public class HSQLDBAssetRepository implements AssetRepository { return orders; StringBuilder sql = new StringBuilder(512); - sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered " + "FROM AssetOrders " + sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered_when FROM AssetOrders " + "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled "); sql.append("ORDER BY price"); if (reverse != null && reverse) sql.append(" DESC"); - sql.append(", ordered"); + sql.append(", ordered_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -265,7 +264,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(3); long fulfilled = resultSet.getLong(4); long price = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); boolean isClosed = false; boolean isFulfilled = false; @@ -285,7 +284,7 @@ public class HSQLDBAssetRepository implements AssetRepository { List bindParams = new ArrayList<>(3); StringBuilder sql = new StringBuilder(512); - sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered " + "FROM AssetOrders " + sql.append("SELECT creator, asset_order_id, amount, fulfilled, price, ordered_when FROM AssetOrders " + "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled "); Collections.addAll(bindParams, haveAssetId, wantAssetId); @@ -305,7 +304,7 @@ public class HSQLDBAssetRepository implements AssetRepository { if (minimumPrice != null && haveAssetId < wantAssetId) sql.append(" DESC"); - sql.append(", ordered"); + sql.append(", ordered_when"); List orders = new ArrayList<>(); @@ -319,7 +318,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(3); long fulfilled = resultSet.getLong(4); long price = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); boolean isClosed = false; boolean isFulfilled = false; @@ -350,7 +349,7 @@ public class HSQLDBAssetRepository implements AssetRepository { return orders; StringBuilder sql = new StringBuilder(512); - sql.append("SELECT price, SUM(amount - fulfilled), MAX(ordered) " + "FROM AssetOrders " + sql.append("SELECT price, SUM(amount - fulfilled), MAX(ordered_when) FROM AssetOrders " + "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled " + "GROUP BY price "); @@ -367,7 +366,7 @@ public class HSQLDBAssetRepository implements AssetRepository { do { long price = resultSet.getLong(1); long totalUnfulfilled = resultSet.getLong(2); - long timestamp = resultSet.getTimestamp(3).getTime(); + long timestamp = resultSet.getLong(3); OrderData order = new OrderData(null, null, haveAssetId, wantAssetId, totalUnfulfilled, 0L, price, timestamp, false, false, haveAssetData.getName(), wantAssetData.getName()); @@ -384,7 +383,8 @@ public class HSQLDBAssetRepository implements AssetRepository { public List getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + sql.append("SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered_when, " + + "is_closed, is_fulfilled, HaveAsset.asset_name, WantAsset.asset_name " + "FROM AssetOrders " + "JOIN Assets AS HaveAsset ON HaveAsset.asset_id = have_asset_id " + "JOIN Assets AS WantAsset ON WantAsset.asset_id = want_asset_id " @@ -400,7 +400,7 @@ public class HSQLDBAssetRepository implements AssetRepository { sql.append(optIsFulfilled ? "TRUE" : "FALSE"); } - sql.append(" ORDER BY ordered"); + sql.append(" ORDER BY ordered_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -419,7 +419,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(4); long fulfilled = resultSet.getLong(5); long price = resultSet.getLong(6); - long timestamp = getZonedTimestampMilli(resultSet, 7); + long timestamp = resultSet.getLong(7); boolean isClosed = resultSet.getBoolean(8); boolean isFulfilled = resultSet.getBoolean(9); String haveAssetName = resultSet.getString(10); @@ -451,7 +451,7 @@ public class HSQLDBAssetRepository implements AssetRepository { return orders; StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled " + sql.append("SELECT asset_order_id, amount, fulfilled, price, ordered_when, is_closed, is_fulfilled " + "FROM AssetOrders " + "WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?"); @@ -465,7 +465,7 @@ public class HSQLDBAssetRepository implements AssetRepository { sql.append(optIsFulfilled ? "TRUE" : "FALSE"); } - sql.append(" ORDER BY ordered"); + sql.append(" ORDER BY ordered_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -480,7 +480,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long amount = resultSet.getLong(2); long fulfilled = resultSet.getLong(3); long price = resultSet.getLong(4); - long timestamp = getZonedTimestampMilli(resultSet, 5); + long timestamp = resultSet.getLong(5); boolean isClosed = resultSet.getBoolean(6); boolean isFulfilled = resultSet.getBoolean(7); @@ -502,7 +502,7 @@ public class HSQLDBAssetRepository implements AssetRepository { saveHelper.bind("asset_order_id", orderData.getOrderId()).bind("creator", orderData.getCreatorPublicKey()) .bind("have_asset_id", orderData.getHaveAssetId()).bind("want_asset_id", orderData.getWantAssetId()) .bind("amount", orderData.getAmount()).bind("fulfilled", orderData.getFulfilled()) - .bind("price", orderData.getPrice()).bind("ordered", toOffsetDateTime(orderData.getTimestamp())) + .bind("price", orderData.getPrice()).bind("ordered_when", orderData.getTimestamp()) .bind("is_closed", orderData.getIsClosed()).bind("is_fulfilled", orderData.getIsFulfilled()); try { @@ -538,11 +538,11 @@ public class HSQLDBAssetRepository implements AssetRepository { return trades; StringBuilder sql = new StringBuilder(512); - sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded " + sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded_when " + "FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id " + "WHERE have_asset_id = ? AND want_asset_id = ? "); - sql.append("ORDER BY traded"); + sql.append("ORDER BY traded_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -558,7 +558,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long targetAmount = resultSet.getLong(3); long initiatorAmount = resultSet.getLong(4); long initiatorSaving = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); TradeData trade = new TradeData(initiatingOrderId, targetOrderId, targetAmount, initiatorAmount, initiatorSaving, timestamp, haveAssetId, haveAssetData.getName(), wantAssetId, wantAssetData.getName()); @@ -615,18 +615,18 @@ public class HSQLDBAssetRepository implements AssetRepository { tradedAssetsSubquery.append(" GROUP BY have_asset_id, want_asset_id"); // Find recent trades using "TradedAssets" assetID pairs - String recentTradesSubquery = "SELECT AssetTrades.target_amount, AssetTrades.initiator_amount, AssetTrades.traded " + String recentTradesSubquery = "SELECT AssetTrades.target_amount, AssetTrades.initiator_amount, AssetTrades.traded_when " + "FROM AssetOrders JOIN AssetTrades ON initiating_order_id = asset_order_id " + "WHERE AssetOrders.have_asset_id = TradedAssets.have_asset_id AND AssetOrders.want_asset_id = TradedAssets.want_asset_id " - + "ORDER BY traded DESC LIMIT 2"; + + "ORDER BY traded_when DESC LIMIT 2"; // Put it all together StringBuilder sql = new StringBuilder(4096); - sql.append("SELECT have_asset_id, want_asset_id, RecentTrades.target_amount, RecentTrades.initiator_amount, RecentTrades.traded FROM ("); + sql.append("SELECT have_asset_id, want_asset_id, RecentTrades.target_amount, RecentTrades.initiator_amount, RecentTrades.traded_when FROM ("); sql.append(tradedAssetsSubquery); sql.append(") AS TradedAssets, LATERAL ("); sql.append(recentTradesSubquery); - sql.append(") AS RecentTrades (target_amount, initiator_amount, traded) ORDER BY have_asset_id"); + sql.append(") AS RecentTrades (target_amount, initiator_amount, traded_when) ORDER BY have_asset_id"); if (reverse != null && reverse) sql.append(" DESC"); @@ -634,7 +634,7 @@ public class HSQLDBAssetRepository implements AssetRepository { if (reverse != null && reverse) sql.append(" DESC"); - sql.append(", RecentTrades.traded DESC "); + sql.append(", RecentTrades.traded_when DESC "); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -649,7 +649,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long wantAssetId = resultSet.getLong(2); long otherAmount = resultSet.getLong(3); long amount = resultSet.getLong(4); - long timestamp = getZonedTimestampMilli(resultSet, 5); + long timestamp = resultSet.getLong(5); RecentTradeData recentTrade = new RecentTradeData(haveAssetId, wantAssetId, otherAmount, amount, timestamp); @@ -665,7 +665,7 @@ public class HSQLDBAssetRepository implements AssetRepository { @Override public List getOrdersTrades(byte[] orderId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded, " + sql.append("SELECT initiating_order_id, target_order_id, target_amount, initiator_amount, initiator_saving, traded_when, " + "have_asset_id, HaveAsset.asset_name, want_asset_id, WantAsset.asset_name " + "FROM AssetTrades " + "JOIN AssetOrders ON asset_order_id = initiating_order_id " @@ -673,7 +673,7 @@ public class HSQLDBAssetRepository implements AssetRepository { + "JOIN Assets AS WantAsset ON WantAsset.asset_id = want_asset_id " + "WHERE ? IN (initiating_order_id, target_order_id) "); - sql.append("ORDER BY traded"); + sql.append("ORDER BY traded_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -691,7 +691,7 @@ public class HSQLDBAssetRepository implements AssetRepository { long targetAmount = resultSet.getLong(3); long initiatorAmount = resultSet.getLong(4); long initiatorSaving = resultSet.getLong(5); - long timestamp = getZonedTimestampMilli(resultSet, 6); + long timestamp = resultSet.getLong(6); long haveAssetId = resultSet.getLong(7); String haveAssetName = resultSet.getString(8); @@ -715,7 +715,7 @@ public class HSQLDBAssetRepository implements AssetRepository { saveHelper.bind("initiating_order_id", tradeData.getInitiator()).bind("target_order_id", tradeData.getTarget()) .bind("target_amount", tradeData.getTargetAmount()).bind("initiator_amount", tradeData.getInitiatorAmount()) - .bind("initiator_saving", tradeData.getInitiatorSaving()).bind("traded", toOffsetDateTime(tradeData.getTimestamp())); + .bind("initiator_saving", tradeData.getInitiatorSaving()).bind("traded_when", tradeData.getTimestamp()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java index 5984edaa..fc6477ad 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -21,7 +18,7 @@ import org.qortal.repository.TransactionRepository; public class HSQLDBBlockRepository implements BlockRepository { private static final String BLOCK_DB_COLUMNS = "version, reference, transaction_count, total_fees, " - + "transactions_signature, height, minted, minter, minter_signature, " + + "transactions_signature, height, minted_when, minter, minter_signature, " + "AT_count, AT_fees, online_accounts, online_accounts_count, online_accounts_timestamp, online_accounts_signatures"; protected HSQLDBRepository repository; @@ -41,14 +38,18 @@ public class HSQLDBBlockRepository implements BlockRepository { long totalFees = resultSet.getLong(4); byte[] transactionsSignature = resultSet.getBytes(5); int height = resultSet.getInt(6); - long timestamp = getZonedTimestampMilli(resultSet, 7); + long timestamp = resultSet.getLong(7); byte[] minterPublicKey = resultSet.getBytes(8); byte[] minterSignature = resultSet.getBytes(9); int atCount = resultSet.getInt(10); long atFees = resultSet.getLong(11); byte[] encodedOnlineAccounts = resultSet.getBytes(12); int onlineAccountsCount = resultSet.getInt(13); - Long onlineAccountsTimestamp = getZonedTimestampMilli(resultSet, 14); + + Long onlineAccountsTimestamp = resultSet.getLong(14); + if (onlineAccountsTimestamp == 0 && resultSet.wasNull()) + onlineAccountsTimestamp = null; + byte[] onlineAccountsSignatures = resultSet.getBytes(15); return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, @@ -61,7 +62,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData fromSignature(byte[] signature) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ? LIMIT 1", signature)) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching block by signature from repository", e); @@ -70,7 +73,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData fromReference(byte[] reference) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE reference = ? LIMIT 1", reference)) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE reference = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, reference)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching block by reference from repository", e); @@ -79,7 +84,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData fromHeight(int height) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ? LIMIT 1", height)) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching block by height from repository", e); @@ -97,7 +104,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int getHeightFromSignature(byte[] signature) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT height FROM Blocks WHERE signature = ? LIMIT 1", signature)) { + String sql = "SELECT height FROM Blocks WHERE signature = ? LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) { if (resultSet == null) return 0; @@ -109,9 +118,10 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int getHeightFromTimestamp(long timestamp) throws DataException { - // Uses (minted, height) index - try (ResultSet resultSet = this.repository.checkedExecute("SELECT height FROM Blocks WHERE minted <= ? ORDER BY minted DESC LIMIT 1", - toOffsetDateTime(timestamp))) { + // Uses (minted_when, height) index + String sql = "SELECT height FROM Blocks WHERE minted_when <= ? ORDER BY minted_when DESC LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, timestamp)) { if (resultSet == null) return 0; @@ -123,7 +133,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int getBlockchainHeight() throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1")) { + String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) return 0; @@ -135,7 +147,9 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData getLastBlock() throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks ORDER BY height DESC LIMIT 1")) { + String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks ORDER BY height DESC LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error fetching last block from repository", e); @@ -176,7 +190,9 @@ public class HSQLDBBlockRepository implements BlockRepository { public int countMintedBlocks(byte[] minterPublicKey) throws DataException { String directSql = "SELECT COUNT(*) FROM Blocks WHERE minter = ?"; - String rewardShareSql = "SELECT COUNT(*) FROM RewardShares JOIN Blocks ON minter = reward_share_public_key WHERE minter_public_key = ?"; + String rewardShareSql = "SELECT COUNT(*) FROM RewardShares " + + "JOIN Blocks ON minter = reward_share_public_key " + + "WHERE minter_public_key = ?"; int totalCount = 0; @@ -346,10 +362,10 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException { - String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted < ? AND online_accounts_signatures IS NOT NULL"; + String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL"; try { - return this.repository.checkedExecuteUpdateCount(sql, toOffsetDateTime(timestamp)); + return this.repository.checkedExecuteUpdateCount(sql, timestamp); } catch (SQLException e) { throw new DataException("Unable to trim old online accounts signatures in repository", e); } @@ -377,11 +393,11 @@ public class HSQLDBBlockRepository implements BlockRepository { saveHelper.bind("signature", blockData.getSignature()).bind("version", blockData.getVersion()).bind("reference", blockData.getReference()) .bind("transaction_count", blockData.getTransactionCount()).bind("total_fees", blockData.getTotalFees()) .bind("transactions_signature", blockData.getTransactionsSignature()).bind("height", blockData.getHeight()) - .bind("minted", toOffsetDateTime(blockData.getTimestamp())) + .bind("minted_when", blockData.getTimestamp()) .bind("minter", blockData.getMinterPublicKey()).bind("minter_signature", blockData.getMinterSignature()) .bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees()) .bind("online_accounts", blockData.getEncodedOnlineAccounts()).bind("online_accounts_count", blockData.getOnlineAccountsCount()) - .bind("online_accounts_timestamp", toOffsetDateTime(blockData.getOnlineAccountsTimestamp())) + .bind("online_accounts_timestamp", blockData.getOnlineAccountsTimestamp()) .bind("online_accounts_signatures", blockData.getOnlineAccountsSignatures()); try { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java index 01e7cafd..7ff7df21 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java @@ -43,13 +43,13 @@ public class HSQLDBChatRepository implements ChatRepository { // Timestamp range if (before != null) { - whereClauses.add("Transactions.creation < ?"); - bindParams.add(HSQLDBRepository.toOffsetDateTime(before)); + whereClauses.add("Transactions.created_when < ?"); + bindParams.add(before); } if (after != null) { - whereClauses.add("Transactions.creation > ?"); - bindParams.add(HSQLDBRepository.toOffsetDateTime(after)); + whereClauses.add("Transactions.created_when > ?"); + bindParams.add(after); } if (txGroupId != null) @@ -83,7 +83,7 @@ public class HSQLDBChatRepository implements ChatRepository { } } - sql.append(" ORDER BY Transactions.creation"); + sql.append(" ORDER BY Transactions.created_when"); sql.append((reverse == null || !reverse) ? " ASC" : " DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index d9ec4de8..399e0662 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -4,16 +4,17 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.qortal.block.BlockChain; public class HSQLDBDatabaseUpdates { private static final Logger LOGGER = LogManager.getLogger(HSQLDBDatabaseUpdates.class); + private static final String TRANSACTION_KEYS = "PRIMARY KEY (signature), " + + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE"; + /** * Apply any incremental changes to database schema. * @@ -89,74 +90,105 @@ public class HSQLDBDatabaseUpdates { stmt.execute("SET DATABASE TRANSACTION CONTROL MVCC"); // Use MVCC over default two-phase locking, a-k-a "LOCKS" stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED"); stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD"); // Do not pad strings to same length before comparison + stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD"); stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD"); + stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types stmt.execute("SET FILES LOB SCALE 1"); // LOB granularity is 1KB + // Slow down log fsync() calls from every 500ms to reduce I/O load + stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds + stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )"); stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )"); - stmt.execute("CREATE TYPE BlockSignature AS VARBINARY(128)"); - stmt.execute("CREATE TYPE Signature AS VARBINARY(64)"); - stmt.execute("CREATE TYPE QortalAddress AS VARCHAR(36)"); - stmt.execute("CREATE TYPE QortalPublicKey AS VARBINARY(32)"); - stmt.execute("CREATE TYPE QortalAmount AS BIGINT"); - stmt.execute("CREATE TYPE GenericDescription AS VARCHAR(4000)"); - stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); - stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)"); - stmt.execute("CREATE TYPE MessageData AS VARBINARY(4000)"); - stmt.execute("CREATE TYPE PollName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); - stmt.execute("CREATE TYPE PollOption AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); - stmt.execute("CREATE TYPE PollOptionIndex AS INTEGER"); - stmt.execute("CREATE TYPE DataHash AS VARBINARY(32)"); + + stmt.execute("CREATE TYPE ArbitraryData AS VARBINARY(256)"); + stmt.execute("CREATE TYPE AssetData AS CLOB(400K)"); stmt.execute("CREATE TYPE AssetID AS BIGINT"); stmt.execute("CREATE TYPE AssetName AS VARCHAR(34) COLLATE SQL_TEXT_NO_PAD"); stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)"); - stmt.execute("CREATE TYPE ATName AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); - stmt.execute("CREATE TYPE ATType AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); - stmt.execute("CREATE TYPE ATTags AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1 - stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4 stmt.execute("CREATE TYPE ATCreationBytes AS BLOB(576K)"); // 16bit * 1 + 16bit * 8 + stmt.execute("CREATE TYPE ATMessage AS VARBINARY(32)"); + stmt.execute("CREATE TYPE ATName AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4 + stmt.execute("CREATE TYPE ATTags AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE ATType AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)"); - stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)"); + stmt.execute("CREATE TYPE BlockSignature AS VARBINARY(128)"); + stmt.execute("CREATE TYPE DataHash AS VARBINARY(32)"); + stmt.execute("CREATE TYPE EpochMillis AS BIGINT"); + stmt.execute("CREATE TYPE GenericDescription AS VARCHAR(4000)"); stmt.execute("CREATE TYPE GroupID AS INTEGER"); stmt.execute("CREATE TYPE GroupName AS VARCHAR(400) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE GroupReason AS VARCHAR(128) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE MessageData AS VARBINARY(4000)"); + stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)"); + stmt.execute("CREATE TYPE PollName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); + stmt.execute("CREATE TYPE PollOption AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE PollOptionIndex AS TINYINT"); + stmt.execute("CREATE TYPE QortalAddress AS VARCHAR(36)"); + stmt.execute("CREATE TYPE QortalKeySeed AS VARBINARY(32)"); + stmt.execute("CREATE TYPE QortalPublicKey AS VARBINARY(32)"); + stmt.execute("CREATE TYPE QortalAmount AS BIGINT"); + stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(128) COLLATE SQL_TEXT_NO_PAD"); stmt.execute("CREATE TYPE RewardSharePercent AS INT"); + stmt.execute("CREATE TYPE Signature AS VARBINARY(64)"); break; case 1: // Blocks stmt.execute("CREATE TABLE Blocks (signature BlockSignature, version TINYINT NOT NULL, reference BlockSignature, " + "transaction_count INTEGER NOT NULL, total_fees QortalAmount NOT NULL, transactions_signature Signature NOT NULL, " - + "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, " - + "generator QortalPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, " + + "height INTEGER NOT NULL, minted_when EpochMillis NOT NULL, " + + "minter QortalPublicKey NOT NULL, minter_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, " + + "online_accounts BLOB(1M), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures BLOB(1M), " + "PRIMARY KEY (signature))"); // For finding blocks by height. stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); - // For finding blocks by the account that generated them. - stmt.execute("CREATE INDEX BlockGeneratorIndex ON Blocks (generator)"); + // For finding blocks by the account that minted them. + stmt.execute("CREATE INDEX BlockMinterIndex ON Blocks (minter)"); // For finding blocks by reference, e.g. child blocks. stmt.execute("CREATE INDEX BlockReferenceIndex ON Blocks (reference)"); - // For finding blocks by generation timestamp or finding height of latest block immediately before generation timestamp, etc. - stmt.execute("CREATE INDEX BlockGenerationHeightIndex ON Blocks (generation, height)"); + // For finding blocks by timestamp or finding height of latest block immediately before timestamp, etc. + stmt.execute("CREATE INDEX BlockTimestampHeightIndex ON Blocks (minted_when, height)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE Blocks NEW SPACE"); + + // Table to hold next block height. + stmt.execute("CREATE TABLE NextBlockHeight (height INT NOT NULL)"); + // Initial value - should work for empty DB or populated DB. + stmt.execute("INSERT INTO NextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)"); + // We use triggers on Blocks to update a simple "next block height" table + String blockUpdateSql = "UPDATE NextBlockHeight SET height = (SELECT height + 1 FROM Blocks ORDER BY height DESC LIMIT 1)"; + stmt.execute("CREATE TRIGGER Next_block_height_insert_trigger AFTER INSERT ON Blocks " + blockUpdateSql); + stmt.execute("CREATE TRIGGER Next_block_height_update_trigger AFTER UPDATE ON Blocks " + blockUpdateSql); + stmt.execute("CREATE TRIGGER Next_block_height_delete_trigger AFTER DELETE ON Blocks " + blockUpdateSql); break; case 2: // Generic transactions (null reference, creator and milestone_block for genesis transactions) stmt.execute("CREATE TABLE Transactions (signature Signature, reference Signature, type TINYINT NOT NULL, " - + "creator QortalPublicKey NOT NULL, creation TIMESTAMP WITH TIME ZONE NOT NULL, fee QortalAmount NOT NULL, milestone_block BlockSignature, " + + "creator QortalPublicKey NOT NULL, created_when EpochMillis NOT NULL, fee QortalAmount NOT NULL, " + + "tx_group_id GroupID NOT NULL, block_height INTEGER, " + + "approval_status TINYINT NOT NULL, approval_height INTEGER, " + "PRIMARY KEY (signature))"); // For finding transactions by transaction type. stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)"); // For finding transactions using creation timestamp. - stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); + stmt.execute("CREATE INDEX TransactionTimestampIndex ON Transactions (created_when)"); // For when a user wants to lookup ALL transactions they have created, with optional type. stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator, type)"); // For finding transactions by reference, e.g. child transactions. stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)"); + // For finding transactions by groupID + stmt.execute("CREATE INDEX TransactionGroupIndex ON Transactions (tx_group_id)"); + // For finding transactions by block height + stmt.execute("CREATE INDEX TransactionHeightIndex on Transactions (block_height)"); + // For searching transactions based on approval status + stmt.execute("CREATE INDEX TransactionApprovalStatusIndex on Transactions (approval_status, block_height)"); + // For searching transactions based on approval height + stmt.execute("CREATE INDEX TransactionApprovalHeightIndex on Transactions (approval_height)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE Transactions NEW SPACE"); @@ -169,828 +201,401 @@ public class HSQLDBDatabaseUpdates { // Unconfirmed transactions // We use this as searching for transactions with no corresponding mapping in BlockTransactions is much slower. - stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, creation TIMESTAMP WITH TIME ZONE NOT NULL)"); + stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, created_when EpochMillis NOT NULL)"); // Index to allow quick sorting by creation-else-signature - stmt.execute("CREATE INDEX UnconfirmedTransactionsIndex ON UnconfirmedTransactions (creation, signature)"); + stmt.execute("CREATE INDEX UnconfirmedTransactionsIndex ON UnconfirmedTransactions (created_when, signature)"); // Transaction participants // To allow lookup of all activity by an address - stmt.execute("CREATE TABLE TransactionParticipants (signature Signature, participant QortalAddress NOT NULL, " + stmt.execute("CREATE TABLE TransactionParticipants (signature Signature NOT NULL, participant QortalAddress NOT NULL, " + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Add index to TransactionParticipants to speed up queries + stmt.execute("CREATE INDEX TransactionParticipantsAddressIndex on TransactionParticipants (participant)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE TransactionParticipants NEW SPACE"); break; case 3: - // Genesis Transactions - stmt.execute("CREATE TABLE GenesisTransactions (signature Signature, recipient QortalAddress NOT NULL, " - + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, PRIMARY KEY (signature), " - + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Accounts + stmt.execute("CREATE TABLE Accounts (account QortalAddress, reference Signature, public_key QortalPublicKey, " + + "default_group_id GroupID NOT NULL DEFAULT 0, flags INTEGER NOT NULL DEFAULT 0, level INT NOT NULL DEFAULT 0, " + + "blocks_minted INTEGER NOT NULL DEFAULT 0, blocks_minted_adjustment INTEGER NOT NULL DEFAULT 0, " + + "PRIMARY KEY (account))"); + // For looking up an account by public key + stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)"); + + // Account balances + stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, " + + "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); + // Index for speeding up fetch legacy QORA holders for Block processing + stmt.execute("CREATE INDEX AccountBalancesAssetBalanceIndex ON AccountBalances (asset_id, balance)"); + // Add CHECK constraint to account balances + stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)"); + + // Keeping track of QORT gained from holding legacy QORA + stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, " + + "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); break; case 4: - // Payment Transactions - stmt.execute("CREATE TABLE PaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " - + "amount QortalAmount NOT NULL, PRIMARY KEY (signature), " - + "FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Genesis Transactions + stmt.execute("CREATE TABLE GenesisTransactions (signature Signature, recipient QortalAddress NOT NULL, " + + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Genesis-block-only transaction to set/clear flags + stmt.execute("CREATE TABLE AccountFlagsTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, " + + "and_mask INT NOT NULL, or_mask INT NOT NULL, xor_mask INT NOT NULL, previous_flags INT, " + TRANSACTION_KEYS + ")"); + + // Genesis-block-only transaction to set level + stmt.execute("CREATE TABLE AccountLevelTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, " + + "level INT NOT NULL, " + TRANSACTION_KEYS + ")"); break; case 5: - // Register Name Transactions - stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "owner QortalAddress NOT NULL, data NameData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Payments + // Arbitrary/Multi-payment/Message/Payment Transaction Payments + stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QortalAddress NOT NULL, " + + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Payment Transactions + stmt.execute("CREATE TABLE PaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "amount QortalAmount NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Multi-payment Transactions + stmt.execute("CREATE TABLE MultiPaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, " + + TRANSACTION_KEYS + ")"); break; case 6: - // Update Name Transactions - stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Message Transactions + stmt.execute("CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, " + + "sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, data MessageData NOT NULL, " + + TRANSACTION_KEYS + ")"); break; case 7: - // Sell Name Transactions - stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "amount QortalAmount NOT NULL, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 8: - // Cancel Sell Name Transactions - stmt.execute("CREATE TABLE CancelSellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 9: - // Buy Name Transactions - stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "seller QortalAddress NOT NULL, amount QortalAmount NOT NULL, name_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 10: - // Create Poll Transactions - stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " - + "poll_name PollName NOT NULL, description GenericDescription NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key - stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_index TINYINT NOT NULL, option_name PollOption, " - + "PRIMARY KEY (signature, option_index), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); - // For the future: add flag to polls to allow one or multiple votes per voter - break; - - case 11: - // Vote On Poll Transactions - stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QortalPublicKey NOT NULL, poll_name PollName NOT NULL, " - + "option_index PollOptionIndex NOT NULL, previous_option_index PollOptionIndex, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 12: - // Arbitrary/Multi-payment/Message/Payment Transaction Payments - stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QortalAddress NOT NULL, " - + "amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, " - + "PRIMARY KEY (signature, recipient, asset_id), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 13: // Arbitrary Transactions stmt.execute("CREATE TABLE ArbitraryTransactions (signature Signature, sender QortalPublicKey NOT NULL, version TINYINT NOT NULL, " - + "service TINYINT NOT NULL, data_hash DataHash NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "service SMALLINT NOT NULL, is_data_raw BOOLEAN NOT NULL, data ArbitraryData NOT NULL, " + + TRANSACTION_KEYS + ")"); // NB: Actual data payload stored elsewhere // For the future: data payload should be encrypted, at the very least with transaction's reference as the seed for the encryption key break; - case 14: - // Issue Asset Transactions - stmt.execute( - "CREATE TABLE IssueAssetTransactions (signature Signature, issuer QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, asset_name AssetName NOT NULL, " - + "description GenericDescription NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // For the future: maybe convert quantity from BIGINT to QortalAmount, regardless of divisibility + case 8: + // Name-related + stmt.execute("CREATE TABLE Names (name RegisteredName, owner QortalAddress NOT NULL, " + + "registered_when EpochMillis NOT NULL, updated_when EpochMillis, creation_group_id GroupID NOT NULL DEFAULT 0, " + + "reference Signature, is_for_sale BOOLEAN NOT NULL DEFAULT FALSE, sale_price QortalAmount, data NameData NOT NULL, " + + "PRIMARY KEY (name))"); + + // Register Name Transactions + stmt.execute("CREATE TABLE RegisterNameTransactions (signature Signature, registrant QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "owner QortalAddress NOT NULL, data NameData NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Update Name Transactions + stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "new_owner QortalAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); + + // Sell Name Transactions + stmt.execute("CREATE TABLE SellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "amount QortalAmount NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Cancel Sell Name Transactions + stmt.execute("CREATE TABLE CancelSellNameTransactions (signature Signature, owner QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + TRANSACTION_KEYS + ")"); + + // Buy Name Transactions + stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QortalPublicKey NOT NULL, name RegisteredName NOT NULL, " + + "seller QortalAddress NOT NULL, amount QortalAmount NOT NULL, name_reference Signature, " + TRANSACTION_KEYS + ")"); break; - case 15: - // Transfer Asset Transactions - stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " - + "asset_id AssetID NOT NULL, amount QortalAmount NOT NULL," - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + case 9: + // Polls/voting + stmt.execute("CREATE TABLE Polls (poll_name PollName, creator QortalPublicKey NOT NULL, " + + "owner QortalAddress NOT NULL, published_when EpochMillis NOT NULL, " + + "description GenericDescription NOT NULL, " + + "PRIMARY KEY (poll_name))"); + // For when a user wants to lookup poll they own + stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)"); - case 16: - // Create Asset Order Transactions - stmt.execute("CREATE TABLE CreateAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " - + "have_asset_id AssetID NOT NULL, amount QortalAmount NOT NULL, want_asset_id AssetID NOT NULL, price QortalAmount NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 17: - // Cancel Asset Order Transactions - stmt.execute("CREATE TABLE CancelAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " - + "asset_order_id AssetOrderID NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 18: - // Multi-payment Transactions - stmt.execute("CREATE TABLE MultiPaymentTransactions (signature Signature, sender QortalPublicKey NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 19: - // Deploy CIYAM AT Transactions - stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, " - + "description GenericDescription NOT NULL, AT_type ATType NOT NULL, AT_tags ATTags NOT NULL, " - + "creation_bytes ATCreationBytes NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, AT_address QortalAddress, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // For looking up the Deploy AT Transaction based on deployed AT address - stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)"); - break; - - case 20: - // Message Transactions - stmt.execute( - "CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " - + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, data MessageData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 21: - // Assets (including QORT coin itself) - stmt.execute("CREATE TABLE Assets (asset_id AssetID, owner QortalAddress NOT NULL, " - + "asset_name AssetName NOT NULL, description GenericDescription NOT NULL, " - + "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL, PRIMARY KEY (asset_id))"); - // We need a corresponding trigger to make sure new asset_id values are assigned sequentially start from 0 - stmt.execute( - "CREATE TRIGGER Asset_ID_Trigger BEFORE INSERT ON Assets REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.asset_id IS NULL) " - + "SET new_row.asset_id = (SELECT IFNULL(MAX(asset_id) + 1, 0) FROM Assets)"); - // For when a user wants to lookup an asset by name - stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)"); - break; - - case 22: - // Accounts - stmt.execute("CREATE TABLE Accounts (account QortalAddress, reference Signature, public_key QortalPublicKey, PRIMARY KEY (account))"); - stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, " - + "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); - // For looking up an account by public key - stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)"); - break; - - case 23: - // Asset Orders - stmt.execute( - "CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QortalPublicKey NOT NULL, have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, " - + "amount QortalAmount NOT NULL, fulfilled QortalAmount NOT NULL, price QortalAmount NOT NULL, " - + "ordered TIMESTAMP WITH TIME ZONE NOT NULL, is_closed BOOLEAN NOT NULL, is_fulfilled BOOLEAN NOT NULL, " - + "PRIMARY KEY (asset_order_id))"); - // For quick matching of orders. is_closed are is_fulfilled included so inactive orders can be filtered out. - stmt.execute("CREATE INDEX AssetOrderMatchingIndex on AssetOrders (have_asset_id, want_asset_id, is_closed, is_fulfilled, price, ordered)"); - // For when a user wants to look up their current/historic orders. is_closed included so user can filter by active/inactive orders. - stmt.execute("CREATE INDEX AssetOrderCreatorIndex on AssetOrders (creator, is_closed)"); - break; - - case 24: - // Asset Trades - stmt.execute("CREATE TABLE AssetTrades (initiating_order_id AssetOrderId NOT NULL, target_order_id AssetOrderId NOT NULL, " - + "amount QortalAmount NOT NULL, price QortalAmount NOT NULL, traded TIMESTAMP WITH TIME ZONE NOT NULL)"); - // For looking up historic trades based on orders - stmt.execute("CREATE INDEX AssetTradeBuyOrderIndex on AssetTrades (initiating_order_id, traded)"); - stmt.execute("CREATE INDEX AssetTradeSellOrderIndex on AssetTrades (target_order_id, traded)"); - break; - - case 25: - // Polls/Voting - stmt.execute( - "CREATE TABLE Polls (poll_name PollName, description GenericDescription NOT NULL, creator QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " - + "published TIMESTAMP WITH TIME ZONE NOT NULL, " + "PRIMARY KEY (poll_name))"); // Various options available on a poll - stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_index TINYINT NOT NULL, option_name PollOption, " + stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_index PollOptionIndex NOT NULL, option_name PollOption, " + "PRIMARY KEY (poll_name, option_index), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)"); + // Actual votes cast on a poll by voting users. NOTE: only one vote per user supported at this time. stmt.execute("CREATE TABLE PollVotes (poll_name PollName, voter QortalPublicKey, option_index PollOptionIndex NOT NULL, " + "PRIMARY KEY (poll_name, voter), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)"); - // For when a user wants to lookup poll they own - stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)"); + + // Create Poll Transactions + stmt.execute("CREATE TABLE CreatePollTransactions (signature Signature, creator QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " + + "poll_name PollName NOT NULL, description GenericDescription NOT NULL, " + TRANSACTION_KEYS + ")"); + + // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key + stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_index PollOptionIndex NOT NULL, option_name PollOption, " + + "PRIMARY KEY (signature, option_index), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); + // For the future: add flag to polls to allow one or multiple votes per voter + + // Vote On Poll Transactions + stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QortalPublicKey NOT NULL, poll_name PollName NOT NULL, " + + "option_index PollOptionIndex NOT NULL, previous_option_index PollOptionIndex, " + TRANSACTION_KEYS + ")"); break; - case 26: - // Registered Names - stmt.execute("CREATE TABLE Names (name RegisteredName, data NameData NOT NULL, owner QortalAddress NOT NULL, " - + "registered TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, reference Signature, is_for_sale BOOLEAN NOT NULL, sale_price QortalAmount, " - + "PRIMARY KEY (name))"); + case 10: + // Assets (including QORT coin itself) + stmt.execute("CREATE TABLE Assets (asset_id AssetID, owner QortalAddress NOT NULL, " + + "asset_name AssetName NOT NULL, description GenericDescription NOT NULL, " + + "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, " + + "is_unspendable BOOLEAN NOT NULL DEFAULT FALSE, creation_group_id GroupID NOT NULL DEFAULT 0, " + + "reference Signature NOT NULL, data AssetData NOT NULL DEFAULT '', " + + "PRIMARY KEY (asset_id))"); + // For when a user wants to lookup an asset by name + stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)"); + + // We need a corresponding trigger to make sure new asset_id values are assigned sequentially start from 0 + stmt.execute("CREATE TRIGGER Asset_ID_Trigger BEFORE INSERT ON Assets " + + "REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.asset_id IS NULL) " + + "SET new_row.asset_id = (SELECT IFNULL(MAX(asset_id) + 1, 0) FROM Assets)"); + + // Asset Orders + stmt.execute("CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QortalPublicKey NOT NULL, " + + "have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, " + + "amount QortalAmount NOT NULL, fulfilled QortalAmount NOT NULL, price QortalAmount NOT NULL, " + + "ordered_when EpochMillis NOT NULL, is_closed BOOLEAN NOT NULL, is_fulfilled BOOLEAN NOT NULL, " + + "PRIMARY KEY (asset_order_id))"); + // For quick matching of orders. is_closed are is_fulfilled included so inactive orders can be filtered out. + stmt.execute("CREATE INDEX AssetOrderMatchingIndex on AssetOrders (have_asset_id, want_asset_id, is_closed, is_fulfilled, price, ordered_when)"); + // For when a user wants to look up their current/historic orders. is_closed included so user can filter by active/inactive orders. + stmt.execute("CREATE INDEX AssetOrderCreatorIndex on AssetOrders (creator, is_closed)"); + + // Asset Trades + stmt.execute("CREATE TABLE AssetTrades (initiating_order_id AssetOrderId NOT NULL, target_order_id AssetOrderId NOT NULL, " + + "target_amount QortalAmount NOT NULL, initiator_amount QortalAmount NOT NULL, traded_when EpochMillis NOT NULL, " + + "initiator_saving QortalAmount NOT NULL DEFAULT 0)"); + // For looking up historic trades based on orders + stmt.execute("CREATE INDEX AssetTradeBuyOrderIndex on AssetTrades (initiating_order_id, traded_when)"); + stmt.execute("CREATE INDEX AssetTradeSellOrderIndex on AssetTrades (target_order_id, traded_when)"); + + // Issue Asset Transactions + stmt.execute("CREATE TABLE IssueAssetTransactions (signature Signature, issuer QortalPublicKey NOT NULL, owner QortalAddress NOT NULL, " + + "asset_name AssetName NOT NULL, " + + "description GenericDescription NOT NULL, quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, asset_id AssetID, " + + "is_unspendable BOOLEAN NOT NULL, data AssetData NOT NULL DEFAULT '', " + TRANSACTION_KEYS + ")"); + + // Transfer Asset Transactions + stmt.execute("CREATE TABLE TransferAssetTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "asset_id AssetID NOT NULL, amount QortalAmount NOT NULL," + TRANSACTION_KEYS + ")"); + + // Add support for UPDATE_ASSET transactions + stmt.execute("CREATE TABLE UpdateAssetTransactions (signature Signature, owner QortalPublicKey NOT NULL, asset_id AssetID NOT NULL, " + + "new_owner QortalAddress NOT NULL, new_description GenericDescription NOT NULL, new_data AssetData NOT NULL, " + + "orphan_reference Signature, " + TRANSACTION_KEYS + ")"); + + // Create Asset Order Transactions + stmt.execute("CREATE TABLE CreateAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " + + "have_asset_id AssetID NOT NULL, amount QortalAmount NOT NULL, want_asset_id AssetID NOT NULL, price QortalAmount NOT NULL, " + + TRANSACTION_KEYS + ")"); + + // Cancel Asset Order Transactions + stmt.execute("CREATE TABLE CancelAssetOrderTransactions (signature Signature, creator QortalPublicKey NOT NULL, " + + "asset_order_id AssetOrderID NOT NULL, " + TRANSACTION_KEYS + ")"); break; - case 27: + case 11: // CIYAM Automated Transactions - stmt.execute( - "CREATE TABLE ATs (AT_address QortalAddress, creator QortalPublicKey, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, " - + "asset_id AssetID NOT NULL, code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, " - + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QortalAmount, " - + "PRIMARY key (AT_address))"); + stmt.execute("CREATE TABLE ATs (AT_address QortalAddress, creator QortalPublicKey NOT NULL, created_when EpochMillis NOT NULL, " + + "version INTEGER NOT NULL, asset_id AssetID NOT NULL, code_bytes ATCode NOT NULL, code_hash VARBINARY(32) NOT NULL, " + + "creation_group_id GroupID NOT NULL DEFAULT 0, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, " + + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QortalAmount, " + + "PRIMARY key (AT_address))"); // For finding executable ATs, ordered by creation timestamp - stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation)"); + stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, created_when)"); // For finding ATs by creator stmt.execute("CREATE INDEX ATCreatorIndex on ATs (creator)"); // AT state on a per-block basis - stmt.execute("CREATE TABLE ATStates (AT_address QortalAddress, height INTEGER NOT NULL, creation TIMESTAMP WITH TIME ZONE, " - + "state_data ATState, state_hash ATStateHash NOT NULL, fees QortalAmount NOT NULL, " + stmt.execute("CREATE TABLE ATStates (AT_address QortalAddress, height INTEGER NOT NULL, created_when EpochMillis NOT NULL, " + + "state_data ATState, state_hash ATStateHash NOT NULL, fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, " + "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)"); // For finding per-block AT states, ordered by creation timestamp - stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation)"); + stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, created_when)"); + + // Deploy CIYAM AT Transactions + stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, " + + "description GenericDescription NOT NULL, AT_type ATType NOT NULL, AT_tags ATTags NOT NULL, " + + "creation_bytes ATCreationBytes NOT NULL, amount QortalAmount NOT NULL, asset_id AssetID NOT NULL, AT_address QortalAddress, " + + TRANSACTION_KEYS + ")"); + // For looking up the Deploy AT Transaction based on deployed AT address + stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)"); // Generated AT Transactions - stmt.execute( - "CREATE TABLE ATTransactions (signature Signature, AT_address QortalAddress NOT NULL, recipient QortalAddress, amount QortalAmount, asset_id AssetID, message ATMessage, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE ATTransactions (signature Signature, AT_address QortalAddress NOT NULL, recipient QortalAddress, " + + "amount QortalAmount, asset_id AssetID, message ATMessage, " + + TRANSACTION_KEYS + ")"); // For finding AT Transactions generated by a specific AT stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)"); break; - case 28: - // Account groups - stmt.execute( - "CREATE TABLE Groups (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName, description GenericDescription NOT NULL, " - + "created TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, is_open BOOLEAN NOT NULL, " - + "reference Signature, PRIMARY KEY (group_id))"); - // We need a corresponding trigger to make sure new group_id values are assigned sequentially starting from 1 - stmt.execute( - "CREATE TRIGGER Group_ID_Trigger BEFORE INSERT ON Groups REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.group_id IS NULL) " - + "SET new_row.group_id = (SELECT IFNULL(MAX(group_id) + 1, 1) FROM Groups)"); + case 12: + // Groups + stmt.execute("CREATE TABLE Groups (group_id GroupID, owner QortalAddress NOT NULL, group_name GroupName, " + + "created_when EpochMillis NOT NULL, updated_when EpochMillis, is_open BOOLEAN NOT NULL, " + + "approval_threshold TINYINT NOT NULL, min_block_delay INTEGER NOT NULL, max_block_delay INTEGER NOT NULL, " + + "reference Signature, creation_group_id GroupID, description GenericDescription NOT NULL, PRIMARY KEY (group_id))"); // For when a user wants to lookup an group by name stmt.execute("CREATE INDEX GroupNameIndex on Groups (group_name)"); // For finding groups by owner stmt.execute("CREATE INDEX GroupOwnerIndex ON Groups (owner)"); + // We need a corresponding trigger to make sure new group_id values are assigned sequentially starting from 1 + stmt.execute("CREATE TRIGGER Group_ID_Trigger BEFORE INSERT ON Groups " + + "REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.group_id IS NULL) " + + "SET new_row.group_id = (SELECT IFNULL(MAX(group_id) + 1, 1) FROM Groups)"); + // Admins stmt.execute("CREATE TABLE GroupAdmins (group_id GroupID, admin QortalAddress, reference Signature NOT NULL, " + "PRIMARY KEY (group_id, admin), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); - // For finding groups that address administrates + // For finding groups by admin address stmt.execute("CREATE INDEX GroupAdminIndex ON GroupAdmins (admin)"); // Members - stmt.execute( - "CREATE TABLE GroupMembers (group_id GroupID, address QortalAddress, joined TIMESTAMP WITH TIME ZONE NOT NULL, reference Signature NOT NULL, " - + "PRIMARY KEY (group_id, address), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); - // For finding groups that address is member + stmt.execute("CREATE TABLE GroupMembers (group_id GroupID, address QortalAddress, " + + "joined_when EpochMillis NOT NULL, reference Signature NOT NULL, " + + "PRIMARY KEY (group_id, address), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); + // For finding groups by member address stmt.execute("CREATE INDEX GroupMemberIndex ON GroupMembers (address)"); // Invites stmt.execute("CREATE TABLE GroupInvites (group_id GroupID, inviter QortalAddress, invitee QortalAddress, " - + "expiry TIMESTAMP WITH TIME ZONE NOT NULL, reference Signature, " + + "expires_when EpochMillis NOT NULL, reference Signature, " + "PRIMARY KEY (group_id, invitee), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); // For finding invites sent by inviter stmt.execute("CREATE INDEX GroupInviteInviterIndex ON GroupInvites (inviter)"); // For finding invites by group stmt.execute("CREATE INDEX GroupInviteInviteeIndex ON GroupInvites (invitee)"); // For expiry maintenance - stmt.execute("CREATE INDEX GroupInviteExpiryIndex ON GroupInvites (expiry)"); + stmt.execute("CREATE INDEX GroupInviteExpiryIndex ON GroupInvites (expires_when)"); // Pending "join requests" - stmt.execute( - "CREATE TABLE GroupJoinRequests (group_id GroupID, joiner QortalAddress, reference Signature NOT NULL, PRIMARY KEY (group_id, joiner))"); + stmt.execute("CREATE TABLE GroupJoinRequests (group_id GroupID, joiner QortalAddress, reference Signature NOT NULL, " + + "PRIMARY KEY (group_id, joiner))"); // Bans - // NULL expiry means does not expire! - stmt.execute( - "CREATE TABLE GroupBans (group_id GroupID, offender QortalAddress, admin QortalAddress NOT NULL, banned TIMESTAMP WITH TIME ZONE NOT NULL, " - + "reason GenericDescription NOT NULL, expiry TIMESTAMP WITH TIME ZONE, reference Signature NOT NULL, " - + "PRIMARY KEY (group_id, offender), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); + // NULL expires_when means does not expire! + stmt.execute("CREATE TABLE GroupBans (group_id GroupID, offender QortalAddress, admin QortalAddress NOT NULL, " + + "banned_when EpochMillis NOT NULL, reason GenericDescription NOT NULL, expires_when EpochMillis, reference Signature NOT NULL, " + + "PRIMARY KEY (group_id, offender), FOREIGN KEY (group_id) REFERENCES Groups (group_id) ON DELETE CASCADE)"); // For expiry maintenance - stmt.execute("CREATE INDEX GroupBanExpiryIndex ON GroupBans (expiry)"); + stmt.execute("CREATE INDEX GroupBanExpiryIndex ON GroupBans (expires_when)"); break; - case 29: - // Account group transactions + case 13: + // Group transactions + // Create group stmt.execute("CREATE TABLE CreateGroupTransactions (signature Signature, creator QortalPublicKey NOT NULL, group_name GroupName NOT NULL, " - + "owner QortalAddress NOT NULL, description GenericDescription NOT NULL, is_open BOOLEAN NOT NULL, group_id GroupID, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "owner QortalAddress NOT NULL, is_open BOOLEAN NOT NULL, approval_threshold TINYINT NOT NULL, " + + "min_block_delay INTEGER NOT NULL, max_block_delay INTEGER NOT NULL, group_id GroupID, description GenericDescription NOT NULL, " + + TRANSACTION_KEYS + ")"); + + // Update group stmt.execute("CREATE TABLE UpdateGroupTransactions (signature Signature, owner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_description GenericDescription NOT NULL, new_is_open BOOLEAN NOT NULL, group_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "new_owner QortalAddress NOT NULL, new_is_open BOOLEAN NOT NULL, new_approval_threshold TINYINT NOT NULL, " + + "new_min_block_delay INTEGER NOT NULL, new_max_block_delay INTEGER NOT NULL, " + + "group_reference Signature, new_description GenericDescription NOT NULL, " + TRANSACTION_KEYS + ")"); - // Account group add/remove admin transactions - stmt.execute( - "CREATE TABLE AddGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - stmt.execute( - "CREATE TABLE RemoveGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, admin QortalAddress NOT NULL, " - + "admin_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Promote to admin + stmt.execute("CREATE TABLE AddGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, " + + "group_id GroupID NOT NULL, address QortalAddress NOT NULL, " + TRANSACTION_KEYS + ")"); - // Account group join/leave transactions + // Demote from admin + stmt.execute("CREATE TABLE RemoveGroupAdminTransactions (signature Signature, owner QortalPublicKey NOT NULL, " + + "group_id GroupID NOT NULL, admin QortalAddress NOT NULL, admin_reference Signature, " + + TRANSACTION_KEYS + ")"); + + // Join group stmt.execute("CREATE TABLE JoinGroupTransactions (signature Signature, joiner QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " - + "invite_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "invite_reference Signature, previous_group_id GroupID, " + TRANSACTION_KEYS + ")"); + + // Leave group stmt.execute("CREATE TABLE LeaveGroupTransactions (signature Signature, leaver QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " - + "member_reference Signature, admin_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "member_reference Signature, admin_reference Signature, " + TRANSACTION_KEYS + ")"); - // Account group kick transaction - stmt.execute( - "CREATE TABLE GroupKickTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "reason GroupReason, member_reference Signature, admin_reference Signature, join_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // Kick from group + stmt.execute("CREATE TABLE GroupKickTransactions (signature Signature, admin QortalPublicKey NOT NULL, " + + "group_id GroupID NOT NULL, address QortalAddress NOT NULL, reason GroupReason, previous_group_id GroupID, " + + "member_reference Signature, admin_reference Signature, join_reference Signature, " + TRANSACTION_KEYS + ")"); + + // Invite to group + stmt.execute("CREATE TABLE GroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "invitee QortalAddress NOT NULL, time_to_live INTEGER NOT NULL, join_reference Signature, previous_group_id GroupID, " + + TRANSACTION_KEYS + ")"); - // Account group invite/cancel-invite transactions - stmt.execute( - "CREATE TABLE GroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, invitee QortalAddress NOT NULL, " - + "time_to_live INTEGER NOT NULL, join_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // Cancel group invite - stmt.execute( - "CREATE TABLE CancelGroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, invitee QortalAddress NOT NULL, " - + "invite_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE CancelGroupInviteTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "invitee QortalAddress NOT NULL, invite_reference Signature, " + TRANSACTION_KEYS + ")"); - // Account ban/cancel-ban transactions - stmt.execute( - "CREATE TABLE GroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "reason GroupReason, time_to_live INTEGER NOT NULL, " - + "member_reference Signature, admin_reference Signature, join_invite_reference Signature, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - stmt.execute( - "CREATE TABLE CancelGroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, address QortalAddress NOT NULL, " - + "ban_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; + // Ban from group + stmt.execute("CREATE TABLE GroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "address QortalAddress NOT NULL, reason GroupReason, time_to_live INTEGER NOT NULL, previous_group_id GroupID, " + + "member_reference Signature, admin_reference Signature, join_invite_reference Signature, " + + TRANSACTION_KEYS + ")"); - case 30: - // Networking - stmt.execute("CREATE TABLE Peers (hostname VARCHAR(255), port INTEGER, last_connected TIMESTAMP WITH TIME ZONE, last_attempted TIMESTAMP WITH TIME ZONE, " - + "last_height INTEGER, last_misbehaved TIMESTAMP WITH TIME ZONE, PRIMARY KEY (hostname, port))"); - break; + // Unban from group + stmt.execute("CREATE TABLE CancelGroupBanTransactions (signature Signature, admin QortalPublicKey NOT NULL, group_id GroupID NOT NULL, " + + "address QortalAddress NOT NULL, ban_reference Signature, " + TRANSACTION_KEYS + ")"); - case 31: - stmt.execute("SET DATABASE TRANSACTION CONTROL MVCC"); // Use MVCC over default two-phase locking, a-k-a "LOCKS" - break; - - case 32: - // Unified PeerAddress requires peer hostname & port stored as one string - stmt.execute("ALTER TABLE Peers ALTER COLUMN hostname RENAME TO address"); - // Make sure literal IPv6 addresses are enclosed in square brackets. - stmt.execute("UPDATE Peers SET address=CONCAT('[', address, ']') WHERE POSITION(':' IN address) != 0"); - stmt.execute("UPDATE Peers SET address=CONCAT(address, ':', port)"); - // We didn't name the PRIMARY KEY constraint when creating Peers table, so can't easily drop it - // Workaround is to create a new table with new constraint, drop old table, then rename. - stmt.execute("CREATE TABLE PeersTEMP AS (SELECT * FROM Peers) WITH DATA"); - stmt.execute("ALTER TABLE PeersTEMP DROP COLUMN port"); - stmt.execute("ALTER TABLE PeersTEMP ADD PRIMARY KEY (address)"); - stmt.execute("DROP TABLE Peers"); - stmt.execute("ALTER TABLE PeersTEMP RENAME TO Peers"); - break; - - case 33: - // Add groupID to all transactions - groupID 0 is default, which means groupless/no-group - stmt.execute("ALTER TABLE Transactions ADD COLUMN tx_group_id GroupID NOT NULL DEFAULT 0"); - stmt.execute("CREATE INDEX TransactionGroupIndex ON Transactions (tx_group_id)"); - - // Adding approval to group-based transactions - // Default approval threshold is 100% for existing groups but probably of no effect in production - stmt.execute("ALTER TABLE Groups ADD COLUMN approval_threshold TINYINT NOT NULL DEFAULT 100 BEFORE reference"); - stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN approval_threshold TINYINT NOT NULL DEFAULT 100 BEFORE group_id"); - stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_approval_threshold TINYINT NOT NULL DEFAULT 100 BEFORE group_reference"); - - // Approval transactions themselves + // Approval transactions // "pending_signature" contains signature of pending transaction requiring approval // "prior_reference" contains signature of previous approval transaction for orphaning purposes stmt.execute("CREATE TABLE GroupApprovalTransactions (signature Signature, admin QortalPublicKey NOT NULL, pending_signature Signature NOT NULL, approval BOOLEAN NOT NULL, " - + "prior_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "prior_reference Signature, " + TRANSACTION_KEYS + ")"); + // For finding transactions pending approval, and maybe decision by specific admin + stmt.execute("CREATE INDEX GroupApprovalLatestIndex on GroupApprovalTransactions (pending_signature, admin)"); - // Accounts have a default groupID to be used if transaction's txGroupId is 0 - stmt.execute("ALTER TABLE Accounts add default_group_id GroupID NOT NULL DEFAULT 0"); - break; - - case 34: // SET_GROUP transaction support stmt.execute("CREATE TABLE SetGroupTransactions (signature Signature, default_group_id GroupID NOT NULL, previous_default_group_id GroupID, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + TRANSACTION_KEYS + ")"); break; - case 35: - // Group-based transaction approval min/max block delay - stmt.execute("ALTER TABLE Groups ADD COLUMN min_block_delay INT NOT NULL DEFAULT 0 BEFORE reference"); - stmt.execute("ALTER TABLE Groups ADD COLUMN max_block_delay INT NOT NULL DEFAULT 1440 BEFORE reference"); - stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN min_block_delay INT NOT NULL DEFAULT 0 BEFORE group_id"); - stmt.execute("ALTER TABLE CreateGroupTransactions ADD COLUMN max_block_delay INT NOT NULL DEFAULT 1440 BEFORE group_id"); - stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_min_block_delay INT NOT NULL DEFAULT 0 BEFORE group_reference"); - stmt.execute("ALTER TABLE UpdateGroupTransactions ADD COLUMN new_max_block_delay INT NOT NULL DEFAULT 1440 BEFORE group_reference"); + case 14: + // Networking + stmt.execute("CREATE TABLE Peers (address VARCHAR(255), last_connected EpochMillis, last_attempted EpochMillis, " + + "last_misbehaved EpochMillis, added_when EpochMillis, added_by VARCHAR(255), PRIMARY KEY (address))"); break; - case 36: - // Adding group-ness to record types that could require approval for their related transactions - // e.g. REGISTER_NAME might require approval and so Names table requires groupID - // Registered Names - stmt.execute("ALTER TABLE Names ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // Assets aren't ever updated so don't need group-ness - // for future use: stmt.execute("ALTER TABLE Assets ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // Polls aren't ever updated, only voted upon using option index so don't need group-ness - // for future use: stmt.execute("ALTER TABLE Polls ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // CIYAM ATs - stmt.execute("ALTER TABLE ATs ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - // Groups can be updated but updates require approval from original groupID - stmt.execute("ALTER TABLE Groups ADD COLUMN creation_group_id GroupID NOT NULL DEFAULT 0"); - break; + case 15: + // Reward-shares + // Transaction emitted by minter announcing they are sharing with recipient + stmt.execute("CREATE TABLE RewardShareTransactions (signature Signature, minter_public_key QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + + "reward_share_public_key QortalPublicKey NOT NULL, share_percent RewardSharePercent NOT NULL, previous_share_percent RewardSharePercent, " + + TRANSACTION_KEYS + ")"); - case 37: - // Performance-improving INDEX - stmt.execute("CREATE INDEX IF NOT EXISTS BlockGenerationHeightIndex ON Blocks (generation, height)"); - // Asset orders now have isClosed=true when isFulfilled=true - stmt.execute("UPDATE AssetOrders SET is_closed = TRUE WHERE is_fulfilled = TRUE"); - break; - - case 38: - // Rename asset trade columns for clarity - stmt.execute("ALTER TABLE AssetTrades ALTER COLUMN amount RENAME TO target_amount"); - stmt.execute("ALTER TABLE AssetTrades ALTER COLUMN price RENAME TO initiator_amount"); - // Add support for asset "data" - typically JSON map like registered name data - stmt.execute("CREATE TYPE AssetData AS VARCHAR(4000)"); - stmt.execute("ALTER TABLE Assets ADD data AssetData NOT NULL DEFAULT '' BEFORE reference"); - stmt.execute("ALTER TABLE Assets ADD creation_group_id GroupID NOT NULL DEFAULT 0 BEFORE reference"); - // Add support for asset "data" to ISSUE_ASSET transaction - stmt.execute("ALTER TABLE IssueAssetTransactions ADD data AssetData NOT NULL DEFAULT '' BEFORE asset_id"); - // Add support for UPDATE_ASSET transactions - stmt.execute("CREATE TABLE UpdateAssetTransactions (signature Signature, owner QortalPublicKey NOT NULL, asset_id AssetID NOT NULL, " - + "new_owner QortalAddress NOT NULL, new_description GenericDescription NOT NULL, new_data AssetData NOT NULL, " - + "orphan_reference Signature, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Correct Assets.reference to use ISSUE_ASSET transaction's signature instead of reference. - // This is to help UPDATE_ASSET orphaning. - stmt.execute("MERGE INTO Assets USING (SELECT asset_id, signature FROM Assets JOIN Transactions USING (reference) JOIN IssueAssetTransactions USING (signature)) AS Updates " - + "ON Assets.asset_id = Updates.asset_id WHEN MATCHED THEN UPDATE SET Assets.reference = Updates.signature"); - break; - - case 39: - // Support for automatically setting joiner's default groupID when they join a group (by JOIN_GROUP or corresponding admin's INVITE_GROUP) - stmt.execute("ALTER TABLE JoinGroupTransactions ADD previous_group_id INTEGER"); - stmt.execute("ALTER TABLE GroupInviteTransactions ADD previous_group_id INTEGER"); - // Ditto for leaving - stmt.execute("ALTER TABLE LeaveGroupTransactions ADD previous_group_id INTEGER"); - stmt.execute("ALTER TABLE GroupKickTransactions ADD previous_group_id INTEGER"); - stmt.execute("ALTER TABLE GroupBanTransactions ADD previous_group_id INTEGER"); - break; - - case 40: - // Increase asset "data" size from 4K to 400K - stmt.execute("CREATE TYPE AssetDataLob AS CLOB(400K)"); - stmt.execute("ALTER TABLE Assets ALTER COLUMN data AssetDataLob"); - stmt.execute("ALTER TABLE IssueAssetTransactions ALTER COLUMN data AssetDataLob"); - stmt.execute("ALTER TABLE UpdateAssetTransactions ALTER COLUMN new_data AssetDataLob"); - break; - - case 41: - // New asset pricing - /* - * We store "unit price" for asset orders but need enough precision to accurately - * represent fractional values without loss. - * Asset quantities can be up to either 1_000_000_000_000_000_000 (19 digits) if indivisible, - * or 10_000_000_000.00000000 (11+8 = 19 digits) if divisible. - * Two 19-digit numbers need 38 integer and 38 fractional to cover extremes of unit price. - * However, we use another 10 more fractional digits to avoid rounding issues. - * 38 integer + 48 fractional gives 86, so: DECIMAL (86, 48) - */ - // Rename price to unit_price to preserve indexes - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN price RENAME TO unit_price"); - // Adjust precision - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN unit_price DECIMAL(76,48)"); - // Add want-amount column - stmt.execute("ALTER TABLE AssetOrders ADD want_amount QortalAmount BEFORE unit_price"); - // Calculate want-amount values - stmt.execute("UPDATE AssetOrders set want_amount = amount * unit_price"); - // want-amounts all set, so disallow NULL - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN want_amount SET NOT NULL"); - // Rename corresponding column in CreateAssetOrderTransactions - stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN price RENAME TO want_amount"); - break; - - case 42: - // New asset pricing #2 - /* - * Use "price" (discard want-amount) but enforce pricing units in one direction - * to avoid all the reciprocal and round issues. - */ - stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN want_amount RENAME TO price"); - stmt.execute("ALTER TABLE AssetOrders DROP COLUMN want_amount"); - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN unit_price RENAME TO price"); - stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN price QortalAmount"); - /* - * Normalize any 'old' orders to 'new' pricing. - * We must do this so that requesting open orders can be sorted by price. - */ - // Make sure new asset pricing timestamp (used below) is UTC - stmt.execute("SET TIME ZONE INTERVAL '0:00' HOUR TO MINUTE"); - // Normalize amount/fulfilled to asset with highest assetID, BEFORE price correction - stmt.execute("UPDATE AssetOrders SET amount = amount * price, fulfilled = fulfilled * price " - + "WHERE ordered < timestamp(" + 0 /* was BlockChain.getInstance().getNewAssetPricingTimestamp() */ + ") " - + "AND have_asset_id < want_asset_id"); - // Normalize price into lowest-assetID/highest-assetID price-pair, e.g. QORT/asset100 - // Note: HSQLDB uses BigDecimal's dividend.divide(divisor, RoundingMode.DOWN) too - stmt.execute("UPDATE AssetOrders SET price = CAST(1 AS QortalAmount) / price " - + "WHERE ordered < timestamp(" + 0 /* was BlockChain.getInstance().getNewAssetPricingTimestamp() */ + ") " - + "AND have_asset_id < want_asset_id"); - // Revert time zone change above - stmt.execute("SET TIME ZONE LOCAL"); - break; - - case 43: - // More work on 'new' asset pricing - refunds due to price improvement - stmt.execute("ALTER TABLE AssetTrades ADD initiator_saving QortalAmount NOT NULL DEFAULT 0"); - break; - - case 44: - // Account flags - stmt.execute("ALTER TABLE Accounts ADD COLUMN flags INT NOT NULL DEFAULT 0"); - // Corresponding transaction to set/clear flags - stmt.execute("CREATE TABLE AccountFlagsTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, and_mask INT NOT NULL, or_mask INT NOT NULL, xor_mask INT NOT NULL, " - + "previous_flags INT, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 45: - // Enabling other accounts to forge - // Transaction to allow one account to enable other account to forge - stmt.execute("CREATE TABLE EnableForgingTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Modification to accounts to record who enabled them to forge (useful for counting accounts and potentially orphaning) - stmt.execute("ALTER TABLE Accounts ADD COLUMN forging_enabler QortalAddress"); - break; - - case 46: - // Proxy forging - // Transaction emitted by forger announcing they are forging on behalf of recipient - stmt.execute("CREATE TABLE ProxyForgingTransactions (signature Signature, forger QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, proxy_public_key QortalPublicKey NOT NULL, share RewardSharePercent NOT NULL, " - + "previous_share RewardSharePercent, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - // Table of current shares - stmt.execute("CREATE TABLE ProxyForgers (forger QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, proxy_public_key QortalPublicKey NOT NULL, share RewardSharePercent NOT NULL, " - + "PRIMARY KEY (forger, recipient))"); - // Proxy-forged blocks will contain proxy public key, which will be used to look up block reward sharing, so create index for those lookups - stmt.execute("CREATE INDEX ProxyForgersProxyPublicKeyIndex ON ProxyForgers (proxy_public_key)"); - break; - - case 47: - // Stash of private keys used for generating blocks. These should be proxy keys! - stmt.execute("CREATE TYPE QortalKeySeed AS VARBINARY(32)"); - stmt.execute("CREATE TABLE ForgingAccounts (forger_seed QortalKeySeed NOT NULL, PRIMARY KEY (forger_seed))"); - break; - - case 48: - // Add index to TransactionParticipants to speed up queries - stmt.execute("CREATE INDEX TransactionParticipantsAddressIndex on TransactionParticipants (participant)"); - break; - - case 49: - // Additional peer information - stmt.execute("ALTER TABLE Peers ADD COLUMN last_block_signature BlockSignature BEFORE last_misbehaved"); - stmt.execute("ALTER TABLE Peers ADD COLUMN last_block_timestamp TIMESTAMP WITH TIME ZONE BEFORE last_misbehaved"); - stmt.execute("ALTER TABLE Peers ADD COLUMN last_block_generator QortalPublicKey BEFORE last_misbehaved"); - break; - - case 50: - // Cached block height in Transactions to save loads of JOINs - stmt.execute("ALTER TABLE Transactions ADD COLUMN block_height INT"); - // Add height-based index - stmt.execute("CREATE INDEX TransactionHeightIndex on Transactions (block_height)"); - break; - - case 51: - // Transaction group-approval rework - // Add index to GroupApprovalTransactions - stmt.execute("CREATE INDEX GroupApprovalLatestIndex on GroupApprovalTransactions (pending_signature, admin)"); - // Transaction's approval status (Java enum) stored as tiny integer for efficiency - stmt.execute("ALTER TABLE Transactions ADD COLUMN approval_status TINYINT NOT NULL"); - // For searching transactions based on approval status - stmt.execute("CREATE INDEX TransactionApprovalStatusIndex on Transactions (approval_status, block_height)"); - // Height when/if transaction is finally approved - stmt.execute("ALTER TABLE Transactions ADD COLUMN approval_height INT"); - // For searching transactions based on approval height - stmt.execute("CREATE INDEX TransactionApprovalHeightIndex on Transactions (approval_height)"); - break; - - case 52: - // Arbitrary transactions changes to allow storage of very small payloads locally - stmt.execute("CREATE TYPE ArbitraryData AS VARBINARY(255)"); - stmt.execute("ALTER TABLE ArbitraryTransactions ADD COLUMN is_data_raw BOOLEAN NOT NULL"); - stmt.execute("ALTER TABLE ArbitraryTransactions ALTER COLUMN data_hash ArbitraryData"); - stmt.execute("ALTER TABLE ArbitraryTransactions ALTER COLUMN data_hash RENAME TO data"); - break; - - case 53: - // Change what we store about peers (again) - stmt.execute("ALTER TABLE Peers DROP COLUMN last_block_signature"); - stmt.execute("ALTER TABLE Peers DROP COLUMN last_block_timestamp"); - stmt.execute("ALTER TABLE Peers DROP COLUMN last_block_generator"); - stmt.execute("ALTER TABLE Peers DROP COLUMN last_height"); - stmt.execute("ALTER TABLE Peers ADD COLUMN added_when TIMESTAMP WITH TIME ZONE"); - stmt.execute("ALTER TABLE Peers ADD COLUMN added_by VARCHAR(255)"); - break; - - case 54: - // Account 'level' - stmt.execute("ALTER TABLE Accounts ADD COLUMN initial_level TINYINT NOT NULL DEFAULT 0"); - stmt.execute("ALTER TABLE Accounts ADD COLUMN level TINYINT NOT NULL DEFAULT 0"); - // Corresponding transaction to set level - stmt.execute("CREATE TABLE AccountLevelTransactions (signature Signature, creator QortalPublicKey NOT NULL, target QortalAddress NOT NULL, level INT NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - break; - - case 55: - // Storage of which level 1+ accounts were 'online' for a particular block. Used to distribute block rewards. - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts VARBINARY(1048576)"); - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_count INT NOT NULL DEFAULT 0"); - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_timestamp TIMESTAMP WITH TIME ZONE"); - stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_signatures BLOB"); - break; - - case 56: - // Modify assets to support "unspendable" flag so we can implement the representative legacy QORA asset. - stmt.execute("ALTER TABLE Assets ADD COLUMN is_unspendable BOOLEAN NOT NULL DEFAULT FALSE BEFORE creation_group_id"); - stmt.execute("ALTER TABLE IssueAssetTransactions ADD COLUMN is_unspendable BOOLEAN NOT NULL DEFAULT FALSE BEFORE asset_id"); - break; - - case 57: - // Modify accounts to keep track of how many blocks generated - stmt.execute("ALTER TABLE Accounts ADD COLUMN blocks_generated INT NOT NULL DEFAULT 0"); - // Remove forging_enabler - stmt.execute("ALTER TABLE Accounts DROP COLUMN forging_enabler"); - // Remove corresponding ENABLE_FORGING transaction - stmt.execute("DROP TABLE EnableForgingTransactions"); - break; - - case 58: - // Refactoring to unify/clarify block forging/generation/proxy-forging to simply "minting" - // Account-related - stmt.execute("ALTER TABLE Accounts ALTER COLUMN blocks_generated RENAME TO blocks_minted"); - // "proxy-forging" is now "reward-share" - stmt.execute("ALTER TABLE ProxyForgers ALTER COLUMN proxy_public_key RENAME TO reward_share_public_key"); - stmt.execute("ALTER TABLE ProxyForgers ALTER COLUMN forger RENAME TO minter_public_key"); - stmt.execute("ALTER TABLE ProxyForgers ALTER COLUMN share RENAME TO share_percent"); - stmt.execute("ALTER TABLE ProxyForgers RENAME TO RewardShares"); + // Active reward-shares + stmt.execute("CREATE TABLE RewardShares (minter_public_key QortalPublicKey NOT NULL, minter QortalAddress NOT NULL, recipient QortalAddress NOT NULL, " + + "reward_share_public_key QortalPublicKey NOT NULL, share_percent RewardSharePercent NOT NULL, " + + "PRIMARY KEY (minter_public_key, recipient))"); + // For looking up reward-shares based on reward-share public key stmt.execute("CREATE INDEX RewardSharePublicKeyIndex ON RewardShares (reward_share_public_key)"); - stmt.execute("DROP INDEX ProxyForgersProxyPublicKeyIndex"); - // Reward-share transactions - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN forger RENAME TO minter_public_key"); - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN proxy_public_key RENAME TO reward_share_public_key"); - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN share RENAME TO share_percent"); - stmt.execute("ALTER TABLE ProxyForgingTransactions ALTER COLUMN previous_share RENAME TO previous_share_percent"); - stmt.execute("ALTER TABLE ProxyForgingTransactions RENAME TO RewardShareTransactions"); - // Accounts used by BlockMinter - stmt.execute("ALTER TABLE ForgingAccounts ALTER COLUMN forger_seed RENAME TO minter_private_key"); - stmt.execute("ALTER TABLE ForgingAccounts RENAME TO MintingAccounts"); - // Blocks - stmt.execute("ALTER TABLE Blocks ALTER COLUMN generation RENAME TO minted"); - stmt.execute("ALTER TABLE Blocks ALTER COLUMN generator RENAME TO minter"); - stmt.execute("ALTER TABLE Blocks ALTER COLUMN generator_signature RENAME TO minter_signature"); - // Block-indexes - stmt.execute("CREATE INDEX BlockMinterIndex ON Blocks (minter)"); - stmt.execute("DROP INDEX BlockGeneratorIndex"); - stmt.execute("CREATE INDEX BlockMintedHeightIndex ON Blocks (minted, height)"); - stmt.execute("DROP INDEX BlockGenerationHeightIndex"); break; - case 59: - // Keeping track of QORT gained from holding legacy QORA - stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, " - + "PRIMARY KEY (account), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); + case 16: + // Stash of private keys used for generating blocks. These should be proxy keys! + stmt.execute("CREATE TABLE MintingAccounts (minter_private_key QortalKeySeed NOT NULL, minter_public_key QortalPublicKey NOT NULL, PRIMARY KEY (minter_private_key))"); break; - case 60: - // Index for speeding up fetch legacy QORA holders for Block processing - stmt.execute("CREATE INDEX AccountBalances_Asset_Balance_Index ON AccountBalances (asset_id, balance)"); - // Tracking height-history to account balances - stmt.execute("CREATE TABLE HistoricAccountBalances (account QortalAddress, asset_id AssetID, height INT DEFAULT 1, balance QortalAmount NOT NULL, " - + "PRIMARY KEY (account, asset_id, height), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)"); - // Create triggers on changes to AccountBalances rows to update historic - stmt.execute("CREATE TRIGGER Historic_account_balance_insert_trigger AFTER INSERT ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW " - + "INSERT INTO HistoricAccountBalances VALUES (new_row.account, new_row.asset_id, (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - stmt.execute("CREATE TRIGGER Historic_account_balance_update_trigger AFTER UPDATE ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW " - + "INSERT INTO HistoricAccountBalances VALUES (new_row.account, new_row.asset_id, (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - break; - - case 61: - // Rework triggers on AccountBalances as their block-height sub-queries are too slow - stmt.execute("DROP TRIGGER Historic_account_balance_insert_trigger"); - stmt.execute("DROP TRIGGER Historic_account_balance_update_trigger"); - stmt.execute("CREATE TRIGGER Historic_account_balance_insert_trigger AFTER INSERT ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW " - + "INSERT INTO HistoricAccountBalances VALUES " - + "(new_row.account, new_row.asset_id, (SELECT IFNULL(height, 0) + 1 FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS BlockHeights), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - stmt.execute("CREATE TRIGGER Historic_account_balance_update_trigger AFTER UPDATE ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW " - + "INSERT INTO HistoricAccountBalances VALUES " - + "(new_row.account, new_row.asset_id, (SELECT IFNULL(height, 0) + 1 FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS BlockHeights), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - break; - - case 62: - // Rework sub-queries that need to know next block height as currently they fail for genesis block and/or are still too slow - // Table to hold next block height. - stmt.execute("CREATE TABLE NextBlockHeight (height INT NOT NULL)"); - // Initial value - should work for empty DB or populated DB. - stmt.execute("INSERT INTO NextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)"); - // We use triggers on Blocks to update a simple "next block height" table - String blockUpdateSql = "UPDATE NextBlockHeight SET height = (SELECT height + 1 FROM Blocks ORDER BY height DESC LIMIT 1)"; - stmt.execute("CREATE TRIGGER Next_block_height_insert_trigger AFTER INSERT ON Blocks " + blockUpdateSql); - stmt.execute("CREATE TRIGGER Next_block_height_update_trigger AFTER UPDATE ON Blocks " + blockUpdateSql); - stmt.execute("CREATE TRIGGER Next_block_height_delete_trigger AFTER DELETE ON Blocks " + blockUpdateSql); - // Now update previously slow/broken sub-queries - stmt.execute("DROP TRIGGER Historic_account_balance_insert_trigger"); - stmt.execute("DROP TRIGGER Historic_account_balance_update_trigger"); - stmt.execute("CREATE TRIGGER Historic_account_balance_insert_trigger AFTER INSERT ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW " - + "INSERT INTO HistoricAccountBalances VALUES " - + "(new_row.account, new_row.asset_id, (SELECT height from NextBlockHeight), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - stmt.execute("CREATE TRIGGER Historic_account_balance_update_trigger AFTER UPDATE ON AccountBalances REFERENCING NEW ROW AS new_row FOR EACH ROW " - + "INSERT INTO HistoricAccountBalances VALUES " - + "(new_row.account, new_row.asset_id, (SELECT height from NextBlockHeight), new_row.balance) " - + "ON DUPLICATE KEY UPDATE balance = new_row.balance"); - break; - - case 63: - // Group invites should allow NULL expiry column - stmt.execute("ALTER TABLE GroupInvites ALTER COLUMN expiry SET NULL"); - break; - - case 64: + case 17: // TRANSFER_PRIVS transaction stmt.execute("CREATE TABLE TransferPrivsTransactions (signature Signature, sender QortalPublicKey NOT NULL, recipient QortalAddress NOT NULL, " + "previous_sender_flags INT, previous_recipient_flags INT, " + "previous_sender_blocks_minted_adjustment INT, previous_sender_blocks_minted INT, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); - - // Convert Account's "initial_level" to "blocks_minted_adjustment" - stmt.execute("ALTER TABLE Accounts ADD blocks_minted_adjustment INT NOT NULL DEFAULT 0"); - - List blocksByLevel = BlockChain.getInstance().getBlocksNeededByLevel(); - for (int bbli = 0; bbli < blocksByLevel.size(); ++bbli) - stmt.execute("UPDATE Accounts SET blocks_minted_adjustment = " + blocksByLevel.get(bbli) + " WHERE initial_level = " + (bbli + 1)); - - stmt.execute("ALTER TABLE Accounts DROP initial_level"); + + TRANSACTION_KEYS + ")"); break; - case 65: - // Add INDEX to speed up very slow "DELETE FROM HistoricAccountBalances WHERE height >= ?" - stmt.execute("CREATE INDEX IF NOT EXISTS HistoricAccountBalancesHeightIndex ON HistoricAccountBalances (height)"); - break; - - case 66: - // Add CHECK constraint to account balances - stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)"); - break; - - case 67: - // Provide external function to convert private keys to public keys - stmt.execute("CREATE FUNCTION Ed25519_private_to_public_key (IN privateKey VARBINARY(32)) RETURNS VARBINARY(32) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PrivateToPublicKey'"); - - // Cache minting account public keys to save us recalculating them - stmt.execute("ALTER TABLE MintingAccounts ADD minter_public_key QortalPublicKey"); - stmt.execute("UPDATE MintingAccounts SET minter_public_key = Ed25519_private_to_public_key(minter_private_key)"); - stmt.execute("ALTER TABLE MintingAccounts ALTER COLUMN minter_public_key SET NOT NULL"); - - // Provide external function to convert public keys to addresses - stmt.execute("CREATE FUNCTION Ed25519_public_key_to_address (IN privateKey VARBINARY(32)) RETURNS VARCHAR(36) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:org.qortal.repository.hsqldb.HSQLDBRepository.ed25519PublicKeyToAddress'"); - - // Cache reward-share minting account's address - stmt.execute("ALTER TABLE RewardShares ADD minter QortalAddress BEFORE recipient"); - stmt.execute("UPDATE RewardShares SET minter = Ed25519_public_key_to_address(minter_public_key)"); - stmt.execute("ALTER TABLE RewardShares ALTER COLUMN minter SET NOT NULL"); - break; - - case 68: - // Slow down log fsync() calls from every 500ms to reduce I/O load - stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds - break; - - case 69: - // Get rid of historic account balances as they simply use up way too much space - stmt.execute("DROP TRIGGER Historic_Account_Balance_Insert_Trigger"); - stmt.execute("DROP TRIGGER Historic_Account_Balance_Update_Trigger"); - stmt.execute("DROP TABLE HistoricAccountBalances"); - // Reclaim space - stmt.execute("CHECKPOINT"); - stmt.execute("CHECKPOINT DEFRAG"); - break; - - case 70: - // Reduce space used for storing online account in Blocks - stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts BLOB(1M)"); - stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts_signatures BLOB(1M)"); - // Reclaim space - stmt.execute("CHECKPOINT"); - stmt.execute("CHECKPOINT DEFRAG"); - break; - - case 71: - // Add flag to AT state data to indicate 'initial deployment state' - stmt.execute("ALTER TABLE ATStates ADD COLUMN is_initial BOOLEAN NOT NULL DEFAULT TRUE"); - break; - - case 72: - // For ATs, add hash of code bytes to allow searching for specific function ATs, e.g. cross-chain trading - stmt.execute("ALTER TABLE ATs ADD COLUMN code_hash VARBINARY(32) NOT NULL BEFORE is_sleeping"); // Assuming something like SHA256 - break; - - case 73: + case 18: // Chat transactions stmt.execute("CREATE TABLE ChatTransactions (signature Signature, sender QortalAddress NOT NULL, nonce INT NOT NULL, recipient QortalAddress, " - + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, " - + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + + "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, " + TRANSACTION_KEYS + ")"); break; default: @@ -1000,7 +605,7 @@ public class HSQLDBDatabaseUpdates { } // database was updated - LOGGER.info(String.format("HSQLDB repository updated to version %d", databaseVersion + 1)); + LOGGER.info(() -> String.format("HSQLDB repository updated to version %d", databaseVersion + 1)); return true; } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java index 257c61c9..460b5641 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java @@ -2,9 +2,7 @@ package org.qortal.repository.hsqldb; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.qortal.data.group.GroupAdminData; @@ -29,7 +27,8 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupData fromGroupId(int groupId) throws DataException { - String sql = "SELECT group_name, owner, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_id = ?"; + String sql = "SELECT group_name, owner, description, created_when, updated_when, reference, is_open, " + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_id = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) @@ -38,11 +37,12 @@ public class HSQLDBGroupRepository implements GroupRepository { String groupName = resultSet.getString(1); String owner = resultSet.getString(2); String description = resultSet.getString(3); - long created = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(6); boolean isOpen = resultSet.getBoolean(7); @@ -54,7 +54,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(11); - return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch group info from repository", e); } @@ -62,7 +63,8 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupData fromGroupName(String groupName) throws DataException { - String sql = "SELECT group_id, owner, description, created, updated, reference, is_open, approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_name = ?"; + String sql = "SELECT group_id, owner, description, created_when, updated_when, reference, is_open, " + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE group_name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupName)) { if (resultSet == null) @@ -71,11 +73,12 @@ public class HSQLDBGroupRepository implements GroupRepository { int groupId = resultSet.getInt(1); String owner = resultSet.getString(2); String description = resultSet.getString(3); - long created = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(6); boolean isOpen = resultSet.getBoolean(7); @@ -87,7 +90,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(11); - return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + return new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); } catch (SQLException e) { throw new DataException("Unable to fetch group info from repository", e); } @@ -114,8 +118,10 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getAllGroups(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT group_id, owner, group_name, description, created, updated, reference, is_open, " + + sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups ORDER BY group_name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -132,11 +138,12 @@ public class HSQLDBGroupRepository implements GroupRepository { String owner = resultSet.getString(2); String groupName = resultSet.getString(3); String description = resultSet.getString(4); - long created = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(6); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(7); boolean isOpen = resultSet.getBoolean(8); @@ -148,7 +155,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(12); - groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); + groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); } while (resultSet.next()); return groups; @@ -160,8 +168,10 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupsByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT group_id, group_name, description, created, updated, reference, is_open, " + + sql.append("SELECT group_id, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id FROM Groups WHERE owner = ? ORDER BY group_name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -177,11 +187,12 @@ public class HSQLDBGroupRepository implements GroupRepository { int groupId = resultSet.getInt(1); String groupName = resultSet.getString(2); String description = resultSet.getString(3); - long created = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(4); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(6); boolean isOpen = resultSet.getBoolean(7); @@ -193,7 +204,8 @@ public class HSQLDBGroupRepository implements GroupRepository { int creationGroupId = resultSet.getInt(11); - groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); + groups.add(new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId)); } while (resultSet.next()); return groups; @@ -205,11 +217,13 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupsWithMember(String member, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT group_id, owner, group_name, description, created, updated, reference, is_open, " + + sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, admin FROM Groups " + "JOIN GroupMembers USING (group_id) " + "LEFT OUTER JOIN GroupAdmins ON GroupAdmins.group_id = GroupMembers.group_id AND GroupAdmins.admin = GroupMembers.address " + "WHERE address = ? ORDER BY group_name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -226,11 +240,12 @@ public class HSQLDBGroupRepository implements GroupRepository { String owner = resultSet.getString(2); String groupName = resultSet.getString(3); String description = resultSet.getString(4); - long created = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long created = resultSet.getLong(5); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(6); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(7); boolean isOpen = resultSet.getBoolean(8); @@ -241,10 +256,13 @@ public class HSQLDBGroupRepository implements GroupRepository { int maxBlockDelay = resultSet.getInt(11); int creationGroupId = resultSet.getInt(12); - resultSet.getString(13); + + resultSet.getString(13); // 'admin' boolean isAdmin = !resultSet.wasNull(); - GroupData groupData = new GroupData(groupId, owner, groupName, description, created, updated, isOpen, approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + GroupData groupData = new GroupData(groupId, owner, groupName, description, created, updated, isOpen, + approvalThreshold, minBlockDelay, maxBlockDelay, reference, creationGroupId); + groupData.setIsAdmin(isAdmin); groups.add(groupData); @@ -260,12 +278,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupData groupData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Groups"); - // Special handling for "updated" timestamp - Long updated = groupData.getUpdated(); - Timestamp updatedTimestamp = updated == null ? null : new Timestamp(updated); - saveHelper.bind("group_id", groupData.getGroupId()).bind("owner", groupData.getOwner()).bind("group_name", groupData.getGroupName()) - .bind("description", groupData.getDescription()).bind("created", new Timestamp(groupData.getCreated())).bind("updated", updatedTimestamp) + .bind("description", groupData.getDescription()).bind("created_when", groupData.getCreated()).bind("updated_when", groupData.getUpdated()) .bind("reference", groupData.getReference()).bind("is_open", groupData.getIsOpen()).bind("approval_threshold", groupData.getApprovalThreshold().value) .bind("min_block_delay", groupData.getMinimumBlockDelay()).bind("max_block_delay", groupData.getMaximumBlockDelay()) .bind("creation_group_id", groupData.getCreationGroupId()); @@ -350,7 +364,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupAdmins(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); + sql.append("SELECT admin, reference FROM GroupAdmins WHERE group_id = ? ORDER BY admin"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -416,14 +432,14 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupMemberData getMember(int groupId, String address) throws DataException { - String sql = "SELECT address, joined, reference FROM GroupMembers WHERE group_id = ?"; + String sql = "SELECT address, joined_when, reference FROM GroupMembers WHERE group_id = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) return null; String member = resultSet.getString(1); - long joined = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long joined = resultSet.getLong(2); byte[] reference = resultSet.getBytes(3); return new GroupMemberData(groupId, member, joined, reference); @@ -444,7 +460,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupMembers(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT address, joined, reference FROM GroupMembers WHERE group_id = ? ORDER BY address"); + + sql.append("SELECT address, joined_when, reference FROM GroupMembers WHERE group_id = ? ORDER BY address"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -458,7 +476,7 @@ public class HSQLDBGroupRepository implements GroupRepository { do { String member = resultSet.getString(1); - long joined = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long joined = resultSet.getLong(2); byte[] reference = resultSet.getBytes(3); members.add(new GroupMemberData(groupId, member, joined, reference)); @@ -490,7 +508,7 @@ public class HSQLDBGroupRepository implements GroupRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupMembers"); saveHelper.bind("group_id", groupMemberData.getGroupId()).bind("address", groupMemberData.getMember()) - .bind("joined", new Timestamp(groupMemberData.getJoined())).bind("reference", groupMemberData.getReference()); + .bind("joined_when", groupMemberData.getJoined()).bind("reference", groupMemberData.getReference()); try { saveHelper.execute(this.repository); @@ -512,15 +530,17 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupInviteData getInvite(int groupId, String invitee) throws DataException { - String sql = "SELECT inviter, expiry, reference FROM GroupInvites WHERE group_id = ? AND invitee = ?"; + String sql = "SELECT inviter, expires_when, reference FROM GroupInvites WHERE group_id = ? AND invitee = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId, invitee)) { if (resultSet == null) return null; String inviter = resultSet.getString(1); - Timestamp expiryTimestamp = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + + Long expiry = resultSet.getLong(2); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(3); @@ -542,7 +562,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getInvitesByGroupId(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT inviter, invitee, expiry, reference FROM GroupInvites WHERE group_id = ? ORDER BY invitee"); + + sql.append("SELECT inviter, invitee, expires_when, reference FROM GroupInvites WHERE group_id = ? ORDER BY invitee"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -558,8 +580,9 @@ public class HSQLDBGroupRepository implements GroupRepository { String inviter = resultSet.getString(1); String invitee = resultSet.getString(2); - Timestamp expiryTimestamp = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(3); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(4); @@ -575,7 +598,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getInvitesByInvitee(String invitee, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT group_id, inviter, expiry, reference FROM GroupInvites WHERE invitee = ? ORDER BY group_id"); + + sql.append("SELECT group_id, inviter, expires_when, reference FROM GroupInvites WHERE invitee = ? ORDER BY group_id"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -591,8 +616,9 @@ public class HSQLDBGroupRepository implements GroupRepository { int groupId = resultSet.getInt(1); String inviter = resultSet.getString(2); - Timestamp expiryTimestamp = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(3); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(4); @@ -609,12 +635,9 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupInviteData groupInviteData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupInvites"); - Timestamp expiryTimestamp = null; - if (groupInviteData.getExpiry() != null) - expiryTimestamp = new Timestamp(groupInviteData.getExpiry()); - - saveHelper.bind("group_id", groupInviteData.getGroupId()).bind("inviter", groupInviteData.getInviter()).bind("invitee", groupInviteData.getInvitee()) - .bind("expiry", expiryTimestamp).bind("reference", groupInviteData.getReference()); + saveHelper.bind("group_id", groupInviteData.getGroupId()).bind("inviter", groupInviteData.getInviter()) + .bind("invitee", groupInviteData.getInvitee()).bind("expires_when", groupInviteData.getExpiry()) + .bind("reference", groupInviteData.getReference()); try { saveHelper.execute(this.repository); @@ -662,7 +685,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupJoinRequests(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); + sql.append("SELECT joiner, reference FROM GroupJoinRequests WHERE group_id = ? ORDER BY joiner"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -691,8 +716,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupJoinRequestData groupJoinRequestData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupJoinRequests"); - saveHelper.bind("group_id", groupJoinRequestData.getGroupId()).bind("joiner", groupJoinRequestData.getJoiner()).bind("reference", - groupJoinRequestData.getReference()); + saveHelper.bind("group_id", groupJoinRequestData.getGroupId()).bind("joiner", groupJoinRequestData.getJoiner()) + .bind("reference", groupJoinRequestData.getReference()); try { saveHelper.execute(this.repository); @@ -714,15 +739,16 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public GroupBanData getBan(int groupId, String offender) throws DataException { - String sql = "SELECT admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ? AND offender = ?"; + String sql = "SELECT admin, banned_when, reason, expires_when, reference FROM GroupBans WHERE group_id = ? AND offender = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId, offender)) { String admin = resultSet.getString(1); - long banned = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long banned = resultSet.getLong(2); String reason = resultSet.getString(3); - Timestamp expiryTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(4); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(5); @@ -744,7 +770,9 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public List getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT offender, admin, banned, reason, expiry, reference FROM GroupBans WHERE group_id = ? ORDER BY offender"); + + sql.append("SELECT offender, admin, banned_when, reason, expires_when, reference FROM GroupBans WHERE group_id = ? ORDER BY offender"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -759,11 +787,12 @@ public class HSQLDBGroupRepository implements GroupRepository { do { String offender = resultSet.getString(1); String admin = resultSet.getString(2); - long banned = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long banned = resultSet.getLong(3); String reason = resultSet.getString(4); - Timestamp expiryTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); - Long expiry = expiryTimestamp == null ? null : expiryTimestamp.getTime(); + Long expiry = resultSet.getLong(5); + if (expiry == 0 && resultSet.wasNull()) + expiry = null; byte[] reference = resultSet.getBytes(6); @@ -780,12 +809,8 @@ public class HSQLDBGroupRepository implements GroupRepository { public void save(GroupBanData groupBanData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("GroupBans"); - Timestamp expiryTimestamp = null; - if (groupBanData.getExpiry() != null) - expiryTimestamp = new Timestamp(groupBanData.getExpiry()); - saveHelper.bind("group_id", groupBanData.getGroupId()).bind("offender", groupBanData.getOffender()).bind("admin", groupBanData.getAdmin()) - .bind("banned", new Timestamp(groupBanData.getBanned())).bind("reason", groupBanData.getReason()).bind("expiry", expiryTimestamp) + .bind("banned", groupBanData.getBanned()).bind("reason", groupBanData.getReason()).bind("expiry", groupBanData.getExpiry()) .bind("reference", groupBanData.getReference()); try { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java index a5b66115..3f6c9030 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNameRepository.java @@ -1,13 +1,8 @@ package org.qortal.repository.hsqldb; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.qortal.data.naming.NameData; @@ -24,7 +19,7 @@ public class HSQLDBNameRepository implements NameRepository { @Override public NameData fromName(String name) throws DataException { - String sql = "SELECT owner, data, registered, updated, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE name = ?"; + String sql = "SELECT owner, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, name)) { if (resultSet == null) @@ -32,11 +27,12 @@ public class HSQLDBNameRepository implements NameRepository { String owner = resultSet.getString(1); String data = resultSet.getString(2); - long registered = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long registered = resultSet.getLong(3); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); - Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); + Long updated = resultSet.getLong(4); + if (updated == 0 && resultSet.wasNull()) + updated = null; byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); @@ -65,7 +61,9 @@ public class HSQLDBNameRepository implements NameRepository { @Override public List getAllNames(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(256); - sql.append("SELECT name, data, owner, registered, updated, reference, is_for_sale, sale_price, creation_group_id FROM Names ORDER BY name"); + + sql.append("SELECT name, data, owner, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names ORDER BY name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -81,8 +79,13 @@ public class HSQLDBNameRepository implements NameRepository { String name = resultSet.getString(1); String data = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = getZonedTimestampMilli(resultSet, 4); - Long updated = getZonedTimestampMilli(resultSet, 5); // can be null + long registered = resultSet.getLong(4); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; + byte[] reference = resultSet.getBytes(6); boolean isForSale = resultSet.getBoolean(7); @@ -104,7 +107,9 @@ public class HSQLDBNameRepository implements NameRepository { @Override public List getNamesForSale(Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, owner, registered, updated, reference, sale_price, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); + + sql.append("SELECT name, data, owner, registered_when, updated_when, reference, sale_price, creation_group_id FROM Names WHERE is_for_sale = TRUE ORDER BY name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -120,8 +125,13 @@ public class HSQLDBNameRepository implements NameRepository { String name = resultSet.getString(1); String data = resultSet.getString(2); String owner = resultSet.getString(3); - long registered = getZonedTimestampMilli(resultSet, 4); - Long updated = getZonedTimestampMilli(resultSet, 5); // can be null + long registered = resultSet.getLong(4); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(5); + if (updated == 0 && resultSet.wasNull()) + updated = null; + byte[] reference = resultSet.getBytes(6); boolean isForSale = true; @@ -143,7 +153,9 @@ public class HSQLDBNameRepository implements NameRepository { @Override public List getNamesByOwner(String owner, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); - sql.append("SELECT name, data, registered, updated, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); + + sql.append("SELECT name, data, registered_when, updated_when, reference, is_for_sale, sale_price, creation_group_id FROM Names WHERE owner = ? ORDER BY name"); + if (reverse != null && reverse) sql.append(" DESC"); @@ -158,8 +170,13 @@ public class HSQLDBNameRepository implements NameRepository { do { String name = resultSet.getString(1); String data = resultSet.getString(2); - long registered = getZonedTimestampMilli(resultSet, 3); - Long updated = getZonedTimestampMilli(resultSet, 4); // can be null + long registered = resultSet.getLong(3); + + // Special handling for possibly-NULL "updated" column + Long updated = resultSet.getLong(4); + if (updated == 0 && resultSet.wasNull()) + updated = null; + byte[] reference = resultSet.getBytes(5); boolean isForSale = resultSet.getBoolean(6); @@ -179,14 +196,14 @@ public class HSQLDBNameRepository implements NameRepository { } @Override - public List getRecentNames(long start) throws DataException { + public List getRecentNames(long startTimestamp) throws DataException { String sql = "SELECT name FROM RegisterNameTransactions JOIN Names USING (name) " + "JOIN Transactions USING (signature) " - + "WHERE creation >= ?"; + + "WHERE created_when >= ?"; List names = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute(sql, HSQLDBRepository.toOffsetDateTime(start))) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, startTimestamp)) { if (resultSet == null) return names; @@ -207,7 +224,7 @@ public class HSQLDBNameRepository implements NameRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("Names"); saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()).bind("data", nameData.getData()) - .bind("registered", toOffsetDateTime(nameData.getRegistered())).bind("updated", toOffsetDateTime(nameData.getUpdated())) + .bind("registered_when", nameData.getRegistered()).bind("updated_when", nameData.getUpdated()) .bind("reference", nameData.getReference()) .bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()) .bind("creation_group_id", nameData.getCreationGroupId()); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java index ae84c612..9f865160 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBNetworkRepository.java @@ -33,13 +33,21 @@ public class HSQLDBNetworkRepository implements NetworkRepository { String address = resultSet.getString(1); PeerAddress peerAddress = PeerAddress.fromString(address); - Long lastConnected = HSQLDBRepository.getZonedTimestampMilli(resultSet, 2); + Long lastConnected = resultSet.getLong(2); + if (lastConnected == 0 && resultSet.wasNull()) + lastConnected = null; - Long lastAttempted = HSQLDBRepository.getZonedTimestampMilli(resultSet, 3); + Long lastAttempted = resultSet.getLong(3); + if (lastAttempted == 0 && resultSet.wasNull()) + lastAttempted = null; - Long lastMisbehaved = HSQLDBRepository.getZonedTimestampMilli(resultSet, 4); + Long lastMisbehaved = resultSet.getLong(4); + if (lastMisbehaved == 0 && resultSet.wasNull()) + lastMisbehaved = null; - Long addedWhen = HSQLDBRepository.getZonedTimestampMilli(resultSet, 5); + Long addedWhen = resultSet.getLong(5); + if (addedWhen == 0 && resultSet.wasNull()) + addedWhen = null; String addedBy = resultSet.getString(6); @@ -58,11 +66,9 @@ public class HSQLDBNetworkRepository implements NetworkRepository { public void save(PeerData peerData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Peers"); - saveHelper.bind("address", peerData.getAddress().toString()).bind("last_connected", HSQLDBRepository.toOffsetDateTime(peerData.getLastConnected())) - .bind("last_attempted", HSQLDBRepository.toOffsetDateTime(peerData.getLastAttempted())) - .bind("last_misbehaved", HSQLDBRepository.toOffsetDateTime(peerData.getLastMisbehaved())) - .bind("added_when", HSQLDBRepository.toOffsetDateTime(peerData.getAddedWhen())) - .bind("added_by", peerData.getAddedBy()); + saveHelper.bind("address", peerData.getAddress().toString()).bind("last_connected", peerData.getLastConnected()) + .bind("last_attempted", peerData.getLastAttempted()).bind("last_misbehaved", peerData.getLastMisbehaved()) + .bind("added_when", peerData.getAddedWhen()).bind("added_by", peerData.getAddedBy()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java index 9e1796ea..aa365b9c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java @@ -14,22 +14,16 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Savepoint; import java.sql.Statement; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; import java.util.Deque; import java.util.List; -import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.crypto.Crypto; import org.qortal.repository.ATRepository; import org.qortal.repository.AccountRepository; import org.qortal.repository.ArbitraryRepository; @@ -51,8 +45,6 @@ public class HSQLDBRepository implements Repository { private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class); - public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - protected Connection connection; protected Deque savepoints; protected boolean debugState = false; @@ -78,7 +70,7 @@ public class HSQLDBRepository implements Repository { try (ResultSet resultSet = stmt.getResultSet()) { if (resultSet == null || !resultSet.next()) - LOGGER.warn("Unable to fetch session ID from repository"); + throw new DataException("Unable to fetch session ID from repository"); this.sessionId = resultSet.getLong(1); } @@ -442,7 +434,7 @@ public class HSQLDBRepository implements Repository { long queryTime = System.currentTimeMillis() - beforeQuery; if (queryTime > this.slowQueryThreshold) { - LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); + LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); logStatements(); } @@ -517,7 +509,7 @@ public class HSQLDBRepository implements Repository { long queryTime = System.currentTimeMillis() - beforeQuery; if (queryTime > this.slowQueryThreshold) { - LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); + LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query")); logStatements(); } @@ -690,7 +682,7 @@ public class HSQLDBRepository implements Repository { if (this.sqlStatements == null) return; - LOGGER.info(String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId)); + LOGGER.info(() -> String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId)); for (String sql : this.sqlStatements) LOGGER.info(sql); @@ -752,42 +744,4 @@ public class HSQLDBRepository implements Repository { } } - // Utility methods - - public static byte[] ed25519PrivateToPublicKey(byte[] privateKey) { - if (privateKey == null) - return null; - - return PrivateKeyAccount.toPublicKey(privateKey); - } - - public static String ed25519PublicKeyToAddress(byte[] publicKey) { - if (publicKey == null) - return null; - - return Crypto.toAddress(publicKey); - } - - /** Converts milliseconds from epoch to OffsetDateTime needed for TIMESTAMP WITH TIME ZONE columns. */ - public static OffsetDateTime toOffsetDateTime(Long timestamp) { - if (timestamp == null) - return null; - - return OffsetDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC); - } - - /** Converts OffsetDateTime from TIMESTAMP WITH TIME ZONE column to milliseconds from epoch. */ - public static long fromOffsetDateTime(OffsetDateTime offsetDateTime) { - return offsetDateTime.toInstant().toEpochMilli(); - } - - /** Returns TIMESTAMP WITH TIME ZONE column value as milliseconds from epoch, or null. */ - public static Long getZonedTimestampMilli(ResultSet resultSet, int columnIndex) throws SQLException { - OffsetDateTime offsetDateTime = resultSet.getObject(columnIndex, OffsetDateTime.class); - if (offsetDateTime == null) - return null; - - return offsetDateTime.toInstant().toEpochMilli(); - } - } \ No newline at end of file diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java index a029a0f9..8384c22f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java @@ -7,6 +7,7 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import java.util.TimeZone; /** * Database helper for building, and executing, INSERT INTO ... ON DUPLICATE KEY UPDATE ... statements. @@ -20,6 +21,8 @@ import java.util.List; */ public class HSQLDBSaver { + private final Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + private String table; private List columns = new ArrayList<>(); @@ -137,8 +140,8 @@ public class HSQLDBSaver { preparedStatement.setBigDecimal(i + this.objects.size() + 1, (BigDecimal) object); } else if (object instanceof Timestamp) { // Special treatment for Timestamps so that they are stored as UTC - preparedStatement.setTimestamp(i + 1, (Timestamp) object, Calendar.getInstance(HSQLDBRepository.UTC)); - preparedStatement.setTimestamp(i + this.objects.size() + 1, (Timestamp) object, Calendar.getInstance(HSQLDBRepository.UTC)); + preparedStatement.setTimestamp(i + 1, (Timestamp) object, utcCalendar); + preparedStatement.setTimestamp(i + this.objects.size() + 1, (Timestamp) object, utcCalendar); } else { preparedStatement.setObject(i + 1, object); preparedStatement.setObject(i + this.objects.size() + 1, object); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java index f936b6f2..447fbe4c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java @@ -2,9 +2,7 @@ package org.qortal.repository.hsqldb; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.qortal.data.voting.PollData; @@ -25,7 +23,7 @@ public class HSQLDBVotingRepository implements VotingRepository { @Override public PollData fromPollName(String pollName) throws DataException { - String sql = "SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?"; + String sql = "SELECT description, creator, owner, published_when FROM Polls WHERE poll_name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, pollName)) { if (resultSet == null) @@ -34,7 +32,7 @@ public class HSQLDBVotingRepository implements VotingRepository { String description = resultSet.getString(1); byte[] creatorPublicKey = resultSet.getBytes(2); String owner = resultSet.getString(3); - long published = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long published = resultSet.getLong(4); String optionsSql = "SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC"; try (ResultSet optionsResultSet = this.repository.checkedExecute(optionsSql, pollName)) { @@ -71,7 +69,7 @@ public class HSQLDBVotingRepository implements VotingRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("Polls"); saveHelper.bind("poll_name", pollData.getPollName()).bind("description", pollData.getDescription()).bind("creator", pollData.getCreatorPublicKey()) - .bind("owner", pollData.getOwner()).bind("published", new Timestamp(pollData.getPublished())); + .bind("owner", pollData.getOwner()).bind("published_when", pollData.getPublished()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index a4ca30d1..adcc09c3 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -1,8 +1,5 @@ package org.qortal.repository.hsqldb.transaction; -import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; -import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; - import static org.qortal.transaction.Transaction.TransactionType.*; import java.lang.reflect.Constructor; @@ -124,7 +121,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { @Override public TransactionData fromSignature(byte[] signature) throws DataException { - String sql = "SELECT type, reference, creator, creation, fee, tx_group_id, block_height, approval_status, approval_height " + String sql = "SELECT type, reference, creator, created_when, fee, tx_group_id, block_height, approval_status, approval_height " + "FROM Transactions WHERE signature = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) { @@ -135,7 +132,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { byte[] reference = resultSet.getBytes(2); byte[] creatorPublicKey = resultSet.getBytes(3); - long timestamp = getZonedTimestampMilli(resultSet, 4); + long timestamp = resultSet.getLong(4); Long fee = resultSet.getLong(5); if (fee == 0 && resultSet.wasNull()) @@ -161,7 +158,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { @Override public TransactionData fromReference(byte[] reference) throws DataException { - String sql = "SELECT type, signature, creator, creation, fee, tx_group_id, block_height, approval_status, approval_height " + String sql = "SELECT type, signature, creator, created_when, fee, tx_group_id, block_height, approval_status, approval_height " + "FROM Transactions WHERE reference = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, reference)) { @@ -172,7 +169,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { byte[] signature = resultSet.getBytes(2); byte[] creatorPublicKey = resultSet.getBytes(3); - long timestamp = getZonedTimestampMilli(resultSet, 4); + long timestamp = resultSet.getLong(4); Long fee = resultSet.getLong(5); if (fee == 0 && resultSet.wasNull()) @@ -410,7 +407,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { if (hasAddress) { tables.append(" JOIN TransactionParticipants ON TransactionParticipants.signature = Transactions.signature"); - groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation"; + groupBy = " GROUP BY TransactionParticipants.signature, Transactions.created_when"; signatureColumn = "TransactionParticipants.signature"; } @@ -497,7 +494,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { if (groupBy != null) sql.append(groupBy); - sql.append(" ORDER BY Transactions.creation"); + sql.append(" ORDER BY Transactions.created_when"); sql.append((reverse == null || !reverse) ? " ASC" : " DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -553,7 +550,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { // Enum int value safe to use literally sql.append(ApprovalStatus.APPROVED.value); - sql.append(" ORDER BY creation DESC LIMIT 1"); + sql.append(" ORDER BY created_when DESC LIMIT 1"); try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) { if (resultSet == null) @@ -620,7 +617,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { sql.append(" OR AssetOrders.want_asset_id = "); sql.append(assetId); - sql.append(") GROUP BY Transactions.signature, Transactions.creation ORDER BY Transactions.creation"); + sql.append(") GROUP BY Transactions.signature, Transactions.created_when ORDER BY Transactions.created_when"); sql.append((reverse == null || !reverse) ? " ASC" : " DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -655,7 +652,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { List bindParams = new ArrayList<>(3); StringBuilder sql = new StringBuilder(1024); - sql.append("SELECT creation, tx_group_id, reference, fee, signature, sender, block_height, approval_status, approval_height, recipient, amount, asset_name " + sql.append("SELECT created_when, tx_group_id, reference, fee, signature, sender, block_height, approval_status, approval_height, recipient, amount, asset_name " + "FROM TransferAssetTransactions JOIN Transactions USING (signature) "); if (address != null) @@ -669,7 +666,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { bindParams.add(address); } - sql.append(" ORDER by creation "); + sql.append(" ORDER by created_when "); sql.append((reverse == null || !reverse) ? "ASC" : "DESC"); HSQLDBRepository.limitOffsetSql(sql, limit, offset); @@ -681,7 +678,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { return assetTransfers; do { - long timestamp = getZonedTimestampMilli(resultSet, 1); + long timestamp = resultSet.getLong(1); int txGroupId = resultSet.getInt(2); byte[] reference = resultSet.getBytes(3); long fee = resultSet.getLong(4); @@ -729,7 +726,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { bindParams = new Object[0]; } - sql.append(" ORDER BY creation"); + sql.append(" ORDER BY created_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -867,7 +864,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { String sql = "SELECT signature FROM GroupApprovalTransactions " + "NATURAL JOIN Transactions " + "WHERE pending_signature = ? AND admin = ? AND block_height IS NOT NULL " - + "ORDER BY creation DESC, signature DESC LIMIT 1"; + + "ORDER BY created_when DESC, signature DESC LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, pendingSignature, adminPublicKey)) { if (resultSet == null) @@ -888,7 +885,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { // Also make sure that GROUP_APPROVAL transaction's admin is still an admin of group // Sub-query SQL to find latest GroupApprovalTransaction relating to passed pending signature - String latestApprovalSql = "SELECT pending_signature, admin, approval, creation, signature FROM GroupApprovalTransactions " + String latestApprovalSql = "SELECT pending_signature, admin, approval, created_when, signature FROM GroupApprovalTransactions " + "NATURAL JOIN Transactions WHERE pending_signature = ? AND block_height IS NOT NULL"; StringBuilder sql = new StringBuilder(1024); @@ -896,7 +893,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { sql.append(latestApprovalSql); sql.append(") AS GAT LEFT OUTER JOIN ("); sql.append(latestApprovalSql); - sql.append(") AS NewerGAT ON NewerGAT.admin = GAT.admin AND (NewerGAT.creation > GAT.creation OR (NewerGAT.creation = GAT.creation AND NewerGat.signature > GAT.signature)) " + sql.append(") AS NewerGAT ON NewerGAT.admin = GAT.admin AND (NewerGAT.created_when > GAT.created_when OR (NewerGAT.created_when = GAT.created_when AND NewerGat.signature > GAT.signature)) " + "JOIN Transactions AS PendingTransactions ON PendingTransactions.signature = GAT.pending_signature " + "LEFT OUTER JOIN Accounts ON Accounts.public_key = GAT.admin " + "LEFT OUTER JOIN GroupAdmins ON GroupAdmins.admin = Accounts.account AND GroupAdmins.group_id = PendingTransactions.tx_group_id " @@ -935,7 +932,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { @Override public List getUnconfirmedTransactionSignatures() throws DataException { - String sql = "SELECT signature FROM UnconfirmedTransactions ORDER by creation DESC, signature DESC"; + String sql = "SELECT signature FROM UnconfirmedTransactions ORDER by created_when DESC, signature DESC"; List signatures = new ArrayList<>(); @@ -961,7 +958,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { StringBuilder sql = new StringBuilder(256); sql.append("SELECT signature FROM UnconfirmedTransactions "); - sql.append("ORDER BY creation"); + sql.append("ORDER BY created_when"); if (reverse != null && reverse) sql.append(" DESC"); @@ -1035,7 +1032,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { public void unconfirmTransaction(TransactionData transactionData) throws DataException { HSQLDBSaver saver = new HSQLDBSaver("UnconfirmedTransactions"); - saver.bind("signature", transactionData.getSignature()).bind("creation", toOffsetDateTime(transactionData.getTimestamp())); + saver.bind("signature", transactionData.getSignature()).bind("created_when", transactionData.getTimestamp()); try { saver.execute(repository); @@ -1052,8 +1049,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { saver.bind("signature", transactionData.getSignature()).bind("reference", transactionData.getReference()) .bind("type", transactionData.getType().value) - .bind("creator", transactionData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(transactionData.getTimestamp())) - .bind("fee", transactionData.getFee()).bind("milestone_block", null).bind("tx_group_id", transactionData.getTxGroupId()) + .bind("creator", transactionData.getCreatorPublicKey()).bind("created_when", transactionData.getTimestamp()) + .bind("fee", transactionData.getFee()).bind("tx_group_id", transactionData.getTxGroupId()) .bind("approval_status", transactionData.getApprovalStatus().value); try { diff --git a/src/test/java/org/qortal/test/api/AddressesApiTests.java b/src/test/java/org/qortal/test/api/AddressesApiTests.java index f508a970..c1d28cb6 100644 --- a/src/test/java/org/qortal/test/api/AddressesApiTests.java +++ b/src/test/java/org/qortal/test/api/AddressesApiTests.java @@ -23,6 +23,11 @@ public class AddressesApiTests extends ApiCommon { assertNotNull(this.addressesResource.getAccountInfo(aliceAddress)); } + @Test + public void testGetOnlineAccounts() { + assertNotNull(this.addressesResource.getOnlineAccounts()); + } + @Test public void testGetRewardShares() { assertNotNull(this.addressesResource.getRewardShares(Collections.singletonList(aliceAddress), null, null, null, null, null)); diff --git a/src/test/java/org/qortal/test/api/AdminApiTests.java b/src/test/java/org/qortal/test/api/AdminApiTests.java index c5620e50..4aa2ca3b 100644 --- a/src/test/java/org/qortal/test/api/AdminApiTests.java +++ b/src/test/java/org/qortal/test/api/AdminApiTests.java @@ -26,4 +26,9 @@ public class AdminApiTests extends ApiCommon { assertNotNull(this.adminResource.summary()); } + @Test + public void testGetMintingAccounts() { + assertNotNull(this.adminResource.getMintingAccounts()); + } + } diff --git a/src/test/java/org/qortal/test/api/ArbitraryApiTests.java b/src/test/java/org/qortal/test/api/ArbitraryApiTests.java new file mode 100644 index 00000000..2b80fb51 --- /dev/null +++ b/src/test/java/org/qortal/test/api/ArbitraryApiTests.java @@ -0,0 +1,43 @@ +package org.qortal.test.api; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.api.resource.ArbitraryResource; +import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; +import org.qortal.test.common.ApiCommon; + +public class ArbitraryApiTests extends ApiCommon { + + private ArbitraryResource arbitraryResource; + + @Before + public void buildResource() { + this.arbitraryResource = (ArbitraryResource) ApiCommon.buildResource(ArbitraryResource.class); + } + + @Test + public void testSearch() { + Integer[] startingBlocks = new Integer[] { null, 0, 1, 999999999 }; + Integer[] blockLimits = new Integer[] { null, 0, 1, 999999999 }; + Integer[] txGroupIds = new Integer[] { null, 0, 1, 999999999 }; + Integer[] services = new Integer[] { null, 0, 1, 999999999 }; + String[] addresses = new String[] { null, this.aliceAddress }; + ConfirmationStatus[] confirmationStatuses = new ConfirmationStatus[] { ConfirmationStatus.UNCONFIRMED, ConfirmationStatus.CONFIRMED, ConfirmationStatus.BOTH }; + + for (Integer startBlock : startingBlocks) + for (Integer blockLimit : blockLimits) + for (Integer txGroupId : txGroupIds) + for (Integer service : services) + for (String address : addresses) + for (ConfirmationStatus confirmationStatus : confirmationStatuses) { + if (confirmationStatus != ConfirmationStatus.CONFIRMED && (startBlock != null || blockLimit != null)) + continue; + + assertNotNull(this.arbitraryResource.searchTransactions(startBlock, blockLimit, txGroupId, service, address, confirmationStatus, 20, null, null)); + assertNotNull(this.arbitraryResource.searchTransactions(startBlock, blockLimit, txGroupId, service, address, confirmationStatus, 1, 1, true)); + } + } + +} diff --git a/src/test/java/org/qortal/test/api/AssetsApiTests.java b/src/test/java/org/qortal/test/api/AssetsApiTests.java index cf49ec6a..d678c1ce 100644 --- a/src/test/java/org/qortal/test/api/AssetsApiTests.java +++ b/src/test/java/org/qortal/test/api/AssetsApiTests.java @@ -12,8 +12,12 @@ import org.qortal.api.ApiError; import org.qortal.api.ApiException; import org.qortal.api.resource.AssetsResource; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; import org.qortal.repository.AccountRepository.BalanceOrdering; +import org.qortal.repository.DataException; import org.qortal.test.common.ApiCommon; +import org.qortal.test.common.AssetUtils; public class AssetsApiTests extends ApiCommon { @@ -22,12 +26,22 @@ public class AssetsApiTests extends ApiCommon { private AssetsResource assetsResource; @Before - public void buildResource() { + public void buildResource() throws DataException { this.assetsResource = (AssetsResource) ApiCommon.buildResource(AssetsResource.class); + + // Create some dummy data + try (final Repository repository = RepositoryManager.getRepository()) { + // Matching orders, to create a trade + AssetUtils.createOrder(repository, "alice", AssetUtils.goldAssetId, AssetUtils.otherAssetId, 1_00000000L, 1_00000000L); + AssetUtils.createOrder(repository, "bob", AssetUtils.otherAssetId, AssetUtils.goldAssetId, 1_00000000L, 1_00000000L); + + // Open order + AssetUtils.createOrder(repository, "bob", AssetUtils.otherAssetId, AssetUtils.testAssetId, 1_00000000L, 1_00000000L); + } } @Test - public void testGet() { + public void testResource() { assertNotNull(this.assetsResource); } @@ -70,8 +84,8 @@ public class AssetsApiTests extends ApiCommon { @Test public void testGetAssetBalances() { - List addresses = Arrays.asList(aliceAddress, aliceAddress); - List assetIds = Arrays.asList(1L, 2L, 3L); + List addresses = Arrays.asList(aliceAddress, bobAddress); + List assetIds = Arrays.asList(0L, 1L, 2L, 3L); for (BalanceOrdering balanceOrdering : BalanceOrdering.values()) { for (Boolean excludeZero : ALL_BOOLEAN_VALUES) { diff --git a/src/test/java/org/qortal/test/api/BlockApiTests.java b/src/test/java/org/qortal/test/api/BlockApiTests.java index 4d9d5320..09286a01 100644 --- a/src/test/java/org/qortal/test/api/BlockApiTests.java +++ b/src/test/java/org/qortal/test/api/BlockApiTests.java @@ -8,8 +8,16 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; import org.qortal.api.resource.BlocksResource; +import org.qortal.block.GenesisBlock; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; import org.qortal.test.common.ApiCommon; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.utils.Base58; public class BlockApiTests extends ApiCommon { @@ -21,24 +29,84 @@ public class BlockApiTests extends ApiCommon { } @Test - public void test() { + public void testResource() { assertNotNull(this.blocksResource); } @Test - public void testGetBlockMinters() { - List addresses = Arrays.asList(aliceAddress, aliceAddress); + public void testGetBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + byte[] signatureBytes = GenesisBlock.getInstance(repository).getSignature(); + String signature = Base58.encode(signatureBytes); - assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), null, null, null)); - assertNotNull(this.blocksResource.getBlockMinters(addresses, null, null, null)); - assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), 1, 1, true)); - assertNotNull(this.blocksResource.getBlockMinters(addresses, 1, 1, true)); + assertNotNull(this.blocksResource.getBlock(signature)); + } } @Test - public void testGetBlockSummariesByMinter() { - assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, null, null, null)); - assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, 1, 1, true)); + public void testGetBlockTransactions() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + byte[] signatureBytes = GenesisBlock.getInstance(repository).getSignature(); + String signature = Base58.encode(signatureBytes); + + assertNotNull(this.blocksResource.getBlockTransactions(signature, null, null, null)); + assertNotNull(this.blocksResource.getBlockTransactions(signature, 1, 1, true)); + } + } + + @Test + public void testGetHeight() { + assertNotNull(this.blocksResource.getHeight()); + } + + @Test + public void testGetBlockHeight() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + byte[] signatureBytes = GenesisBlock.getInstance(repository).getSignature(); + String signature = Base58.encode(signatureBytes); + + assertNotNull(this.blocksResource.getHeight(signature)); + } + } + + @Test + public void testGetBlockByHeight() { + assertNotNull(this.blocksResource.getByHeight(1)); + } + + @Test + public void testGetBlockByTimestamp() { + assertNotNull(this.blocksResource.getByTimestamp(System.currentTimeMillis())); + } + + @Test + public void testGetBlockRange() { + assertNotNull(this.blocksResource.getBlockRange(1, 1)); + } + + @Test + public void testGetBlockMinters() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share"); + BlockUtils.mintBlock(repository); + + List addresses = Arrays.asList(aliceAddress, mintingAccount.getAddress(), bobAddress); + + assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), null, null, null)); + assertNotNull(this.blocksResource.getBlockMinters(addresses, null, null, null)); + assertNotNull(this.blocksResource.getBlockMinters(Collections.emptyList(), 1, 1, true)); + assertNotNull(this.blocksResource.getBlockMinters(addresses, 1, 1, true)); + } + } + + @Test + public void testGetBlockSummariesByMinter() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + BlockUtils.mintBlock(repository); + + assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, null, null, null)); + assertNotNull(this.blocksResource.getBlockSummariesByMinter(aliceAddress, 1, 1, true)); + } } } diff --git a/src/test/java/org/qortal/test/api/NamesApiTests.java b/src/test/java/org/qortal/test/api/NamesApiTests.java new file mode 100644 index 00000000..ae7248b4 --- /dev/null +++ b/src/test/java/org/qortal/test/api/NamesApiTests.java @@ -0,0 +1,91 @@ +package org.qortal.test.api; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.api.resource.NamesResource; +import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.data.transaction.SellNameTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.ApiCommon; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; + +public class NamesApiTests extends ApiCommon { + + private NamesResource namesResource; + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + + this.namesResource = (NamesResource) ApiCommon.buildResource(NamesResource.class); + } + + @Test + public void testResource() { + assertNotNull(this.namesResource); + } + + @Test + public void testGetAllNames() { + assertNotNull(this.namesResource.getAllNames(null, null, null)); + assertNotNull(this.namesResource.getAllNames(1, 1, true)); + } + + @Test + public void testGetNamesByAddress() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null)); + assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), 1, 1, true)); + } + } + + @Test + public void testGetName() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + assertNotNull(this.namesResource.getName(name)); + } + } + + @Test + public void testGetAllAssets() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + long price = 1_23456789L; + + TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Sell-name + transactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, price); + TransactionUtils.signAndMint(repository, transactionData, alice); + + assertNotNull(this.namesResource.getNamesForSale(null, null, null)); + assertNotNull(this.namesResource.getNamesForSale(1, 1, true)); + } + } + +} diff --git a/src/test/java/org/qortal/test/common/ApiCommon.java b/src/test/java/org/qortal/test/common/ApiCommon.java index 2a6f0c37..25e6ec53 100644 --- a/src/test/java/org/qortal/test/common/ApiCommon.java +++ b/src/test/java/org/qortal/test/common/ApiCommon.java @@ -24,12 +24,14 @@ public class ApiCommon extends Common { private static final FakeRequest FAKE_REQUEST = new FakeRequest(); public String aliceAddress; + public String bobAddress; @Before public void beforeTests() throws DataException { Common.useDefaultSettings(); this.aliceAddress = Common.getTestAccount(null, "alice").getAddress(); + this.bobAddress = Common.getTestAccount(null, "bob").getAddress(); } public static Object buildResource(Class resourceClass) { diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java new file mode 100644 index 00000000..d9cbf6fc --- /dev/null +++ b/src/test/java/org/qortal/test/naming/MiscTests.java @@ -0,0 +1,42 @@ +package org.qortal.test.naming; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.data.transaction.RegisterNameTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; + +public class MiscTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testGetRecentNames() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Register-name + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String name = "test-name"; + + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), alice.getAddress(), name, "{}"); + TransactionUtils.signAndMint(repository, transactionData, alice); + + List recentNames = repository.getNameRepository().getRecentNames(0L); + + assertNotNull(recentNames); + assertFalse(recentNames.isEmpty()); + } + } + +}