diff --git a/src/repository/VotingRepository.java b/src/repository/VotingRepository.java index 64ce8256..4433a446 100644 --- a/src/repository/VotingRepository.java +++ b/src/repository/VotingRepository.java @@ -1,5 +1,7 @@ package repository; +import java.util.List; + import data.voting.PollData; import data.voting.VoteOnPollData; @@ -17,6 +19,8 @@ public interface VotingRepository { // Votes + public List getVotes(String pollName) throws DataException; + public VoteOnPollData getVote(String pollName, byte[] voterPublicKey) throws DataException; public void save(VoteOnPollData voteOnPollData) throws DataException; diff --git a/src/repository/hsqldb/HSQLDBAccountRepository.java b/src/repository/hsqldb/HSQLDBAccountRepository.java index bb52a491..b75ca7f1 100644 --- a/src/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/repository/hsqldb/HSQLDBAccountRepository.java @@ -68,7 +68,7 @@ public class HSQLDBAccountRepository implements AccountRepository { public void delete(String address, long assetId) throws DataException { try { - this.repository.checkedExecute("DELETE FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId); + this.repository.delete("AccountBalances", "account = ? and asset_id = ?", address, assetId); } catch (SQLException e) { throw new DataException("Unable to delete account balance from repository", e); } diff --git a/src/repository/hsqldb/HSQLDBAssetRepository.java b/src/repository/hsqldb/HSQLDBAssetRepository.java index 55751439..64498a0c 100644 --- a/src/repository/hsqldb/HSQLDBAssetRepository.java +++ b/src/repository/hsqldb/HSQLDBAssetRepository.java @@ -73,7 +73,7 @@ public class HSQLDBAssetRepository implements AssetRepository { public void delete(long assetId) throws DataException { try { - this.repository.checkedExecute("DELETE FROM Assets WHERE assetId = ?", assetId); + this.repository.delete("Assets", "assetId = ?", assetId); } catch (SQLException e) { throw new DataException("Unable to delete asset from repository", e); } @@ -119,7 +119,7 @@ public class HSQLDBAssetRepository implements AssetRepository { public void delete(byte[] orderId) throws DataException { try { - this.repository.checkedExecute("DELETE FROM AssetOrders WHERE orderId = ?", orderId); + this.repository.delete("AssetOrders", "orderId = ?", orderId); } catch (SQLException e) { throw new DataException("Unable to delete asset order from repository", e); } diff --git a/src/repository/hsqldb/HSQLDBBlockRepository.java b/src/repository/hsqldb/HSQLDBBlockRepository.java index 24a1cdec..7fdccc69 100644 --- a/src/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/repository/hsqldb/HSQLDBBlockRepository.java @@ -146,7 +146,7 @@ public class HSQLDBBlockRepository implements BlockRepository { public void delete(BlockData blockData) throws DataException { try { - this.repository.checkedExecute("DELETE FROM Blocks WHERE signature = ?", blockData.getSignature()); + this.repository.delete("Blocks", "signature = ?", blockData.getSignature()); } catch (SQLException e) { throw new DataException("Unable to delete Block from repository", e); } @@ -166,7 +166,7 @@ public class HSQLDBBlockRepository implements BlockRepository { public void delete(BlockTransactionData blockTransactionData) throws DataException { try { - this.repository.checkedExecute("DELETE FROM BlockTransactions WHERE block_signature = ? AND sequence = ? AND transaction_signature = ?", + this.repository.delete("BlockTransactions", "block_signature = ? AND sequence = ? AND transaction_signature = ?", blockTransactionData.getBlockSignature(), blockTransactionData.getSequence(), blockTransactionData.getTransactionSignature()); } catch (SQLException e) { throw new DataException("Unable to delete BlockTransaction from repository", e); diff --git a/src/repository/hsqldb/HSQLDBRepository.java b/src/repository/hsqldb/HSQLDBRepository.java index e9097173..3a49ae83 100644 --- a/src/repository/hsqldb/HSQLDBRepository.java +++ b/src/repository/hsqldb/HSQLDBRepository.java @@ -112,7 +112,26 @@ public class HSQLDBRepository implements Repository { public ResultSet checkedExecute(String sql, Object... objects) throws SQLException { PreparedStatement preparedStatement = this.connection.prepareStatement(sql); - return this.checkedExecute(preparedStatement, objects); + return this.checkedExecuteResultSet(preparedStatement, objects); + } + + /** + * Bind objects to placeholders in prepared statement. + *

+ * Special treatment for BigDecimals so that they retain their "scale". + * + * @param preparedStatement + * @param objects + * @throws SQLException + */ + private void prepareExecute(PreparedStatement preparedStatement, Object... objects) throws SQLException { + for (int i = 0; i < objects.length; ++i) + // Special treatment for BigDecimals so that they retain their "scale", + // which would otherwise be assumed as 0. + if (objects[i] instanceof BigDecimal) + preparedStatement.setBigDecimal(i + 1, (BigDecimal) objects[i]); + else + preparedStatement.setObject(i + 1, objects[i]); } /** @@ -125,14 +144,8 @@ public class HSQLDBRepository implements Repository { * @return ResultSet, or null if there are no found rows * @throws SQLException */ - public ResultSet checkedExecute(PreparedStatement preparedStatement, Object... objects) throws SQLException { - for (int i = 0; i < objects.length; ++i) - // Special treatment for BigDecimals so that they retain their "scale", - // which would otherwise be assumed as 0. - if (objects[i] instanceof BigDecimal) - preparedStatement.setBigDecimal(i + 1, (BigDecimal) objects[i]); - else - preparedStatement.setObject(i + 1, objects[i]); + private ResultSet checkedExecuteResultSet(PreparedStatement preparedStatement, Object... objects) throws SQLException { + prepareExecute(preparedStatement, objects); if (!preparedStatement.execute()) throw new SQLException("Fetching from database produced no results"); @@ -147,6 +160,27 @@ public class HSQLDBRepository implements Repository { return resultSet; } + /** + * Execute PreparedStatement and return changed row count. + * + * @param preparedStatement + * @param objects + * @return number of changed rows + * @throws SQLException + */ + private int checkedExecuteUpdateCount(PreparedStatement preparedStatement, Object... objects) throws SQLException { + prepareExecute(preparedStatement, objects); + + if (preparedStatement.execute()) + throw new SQLException("Database produced results, not row count"); + + int rowCount = preparedStatement.getUpdateCount(); + if (rowCount == -1) + throw new SQLException("Database returned invalid row count"); + + return rowCount; + } + /** * Fetch last value of IDENTITY column after an INSERT statement. *

@@ -159,7 +193,7 @@ public class HSQLDBRepository implements Repository { */ public Long callIdentity() throws SQLException { PreparedStatement preparedStatement = this.connection.prepareStatement("CALL IDENTITY()"); - ResultSet resultSet = this.checkedExecute(preparedStatement); + ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement); if (resultSet == null) return null; @@ -185,11 +219,24 @@ public class HSQLDBRepository implements Repository { */ public boolean exists(String tableName, String whereClause, Object... objects) throws SQLException { PreparedStatement preparedStatement = this.connection.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " LIMIT 1"); - ResultSet resultSet = this.checkedExecute(preparedStatement, objects); + ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects); if (resultSet == null) return false; return true; } + /** + * Delete rows from database table. + * + * @param tableName + * @param whereClause + * @param objects + * @throws SQLException + */ + public void delete(String tableName, String whereClause, Object... objects) throws SQLException { + PreparedStatement preparedStatement = this.connection.prepareStatement("DELETE FROM " + tableName + " WHERE " + whereClause); + this.checkedExecuteUpdateCount(preparedStatement, objects); + } + } diff --git a/src/repository/hsqldb/HSQLDBVotingRepository.java b/src/repository/hsqldb/HSQLDBVotingRepository.java index efd8759d..de929878 100644 --- a/src/repository/hsqldb/HSQLDBVotingRepository.java +++ b/src/repository/hsqldb/HSQLDBVotingRepository.java @@ -20,7 +20,7 @@ public class HSQLDBVotingRepository implements VotingRepository { this.repository = repository; } - // Votes + // Polls public PollData fromPollName(String pollName) throws DataException { try { @@ -93,7 +93,7 @@ public class HSQLDBVotingRepository implements VotingRepository { // NOTE: The corresponding rows in PollOptions are deleted automatically by the database thanks to "ON DELETE CASCADE" in the PollOptions' FOREIGN KEY // definition. try { - this.repository.checkedExecute("DELETE FROM Polls WHERE poll_name = ?", pollName); + this.repository.delete("Polls", "poll_name = ?", pollName); } catch (SQLException e) { throw new DataException("Unable to delete poll from repository", e); } @@ -101,6 +101,28 @@ public class HSQLDBVotingRepository implements VotingRepository { // Votes + public List getVotes(String pollName) throws DataException { + List votes = new ArrayList(); + + try { + ResultSet resultSet = this.repository.checkedExecute("SELECT voter, option_index FROM PollVotes WHERE poll_name = ?", pollName); + if (resultSet == null) + return votes; + + // NOTE: do-while because checkedExecute() above has already called rs.next() for us + do { + byte[] voterPublicKey = resultSet.getBytes(1); + int optionIndex = resultSet.getInt(2); + + votes.add(new VoteOnPollData(pollName, voterPublicKey, optionIndex)); + } while (resultSet.next()); + + return votes; + } catch (SQLException e) { + throw new DataException("Unable to fetch poll votes from repository", e); + } + } + public VoteOnPollData getVote(String pollName, byte[] voterPublicKey) throws DataException { try { ResultSet resultSet = this.repository.checkedExecute("SELECT option_index FROM PollVotes WHERE poll_name = ? AND voter = ?", pollName, @@ -131,7 +153,7 @@ public class HSQLDBVotingRepository implements VotingRepository { public void delete(String pollName, byte[] voterPublicKey) throws DataException { try { - this.repository.checkedExecute("DELETE FROM PollVotes WHERE poll_name = ? AND voter = ?", pollName, voterPublicKey); + this.repository.delete("PollVotes", "poll_name = ? AND voter = ?", pollName, voterPublicKey); } catch (SQLException e) { throw new DataException("Unable to delete poll vote from repository", e); } diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 5b0ef74d..e7aa1468 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -262,7 +262,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { // NOTE: The corresponding row in sub-table is deleted automatically by the database thanks to "ON DELETE CASCADE" in the sub-table's FOREIGN KEY // definition. try { - this.repository.checkedExecute("DELETE FROM Transactions WHERE signature = ?", transactionData.getSignature()); + this.repository.delete("Transactions", "signature = ?", transactionData.getSignature()); } catch (SQLException e) { throw new DataException("Unable to delete transaction from repository", e); } diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index 23fd88a7..eac05879 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -20,6 +20,7 @@ import data.transaction.PaymentTransactionData; import data.transaction.VoteOnPollTransactionData; import data.voting.PollData; import data.voting.PollOptionData; +import data.voting.VoteOnPollData; import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; @@ -245,11 +246,40 @@ public class TransactionTests { block.process(); repository.saveChanges(); + // Check vote was registered properly + VoteOnPollData actualVoteOnPollData = repository.getVotingRepository().getVote(pollName, sender.getPublicKey()); + assertNotNull(actualVoteOnPollData); + assertEquals(optionIndex, actualVoteOnPollData.getOptionIndex()); + // update variables for next round previousBlockData = block.getBlockData(); timestamp += 1_000; reference = voteOnPollTransaction.getTransactionData().getSignature(); } + + // Check poll's votes + List votes = repository.getVotingRepository().getVotes(pollName); + assertNotNull(votes); + + assertEquals("Only one vote expected", 1, votes.size()); + + assertEquals("Wrong vote option index", pollOptionsSize - 1, votes.get(0).getOptionIndex()); + assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey())); + + // Orphan last block + BlockData lastBlockData = repository.getBlockRepository().getLastBlock(); + Block lastBlock = new Block(repository, lastBlockData); + lastBlock.orphan(); + repository.saveChanges(); + + // Recheck poll's votes + votes = repository.getVotingRepository().getVotes(pollName); + assertNotNull(votes); + + assertEquals("Only one vote expected", 1, votes.size()); + + assertEquals("Wrong vote option index", pollOptionsSize - 1 - 1, votes.get(0).getOptionIndex()); + assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey())); } } \ No newline at end of file