Merge branch '545-hyper-sql' into 'master'

Add HyperSQL as an alternative DB library for testing

See merge request !619
This commit is contained in:
akwizgran
2017-12-05 16:05:42 +00:00
11 changed files with 2360 additions and 2083 deletions

View File

@@ -9,13 +9,14 @@ apply plugin: 'witness'
dependencies { dependencies {
implementation project(path: ':bramble-api', configuration: 'default') implementation project(path: ':bramble-api', configuration: 'default')
implementation 'com.madgag.spongycastle:core:1.58.0.0' implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.h2database:h2:1.4.192' // This is the last version that supports Java 1.6 implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6
implementation 'org.bitlet:weupnp:0.1.4' implementation 'org.bitlet:weupnp:0.1.4'
implementation 'net.i2p.crypto:eddsa:0.2.0' implementation 'net.i2p.crypto:eddsa:0.2.0'
apt 'com.google.dagger:dagger-compiler:2.0.2' apt 'com.google.dagger:dagger-compiler:2.0.2'
testImplementation project(path: ':bramble-api', configuration: 'testOutput') testImplementation project(path: ':bramble-api', configuration: 'testOutput')
testImplementation 'org.hsqldb:hsqldb:2.3.5' // The last version that supports Java 1.6
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation "org.jmock:jmock:2.8.2" testImplementation "org.jmock:jmock:2.8.2"
testImplementation "org.jmock:jmock-junit4:2.8.2" testImplementation "org.jmock:jmock-junit4:2.8.2"
@@ -45,6 +46,7 @@ dependencyVerification {
'org.bitlet:weupnp:0.1.4:weupnp-0.1.4.jar:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb', 'org.bitlet:weupnp:0.1.4:weupnp-0.1.4.jar:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb',
'org.hamcrest:hamcrest-core:1.3:hamcrest-core-1.3.jar:66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9', 'org.hamcrest:hamcrest-core:1.3:hamcrest-core-1.3.jar:66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9',
'org.hamcrest:hamcrest-library:1.3:hamcrest-library-1.3.jar:711d64522f9ec410983bd310934296da134be4254a125080a0416ec178dfad1c', 'org.hamcrest:hamcrest-library:1.3:hamcrest-library-1.3.jar:711d64522f9ec410983bd310934296da134be4254a125080a0416ec178dfad1c',
'org.hsqldb:hsqldb:2.3.5:hsqldb-2.3.5.jar:6676a6977ac98997a80f827ddbd3fe8ca1e0853dad1492512135fd1a222ccfad',
'org.jmock:jmock-junit4:2.8.2:jmock-junit4-2.8.2.jar:f7ee4df4f7bd7b7f1cafad3b99eb74d579f109d5992ff625347352edb55e674c', 'org.jmock:jmock-junit4:2.8.2:jmock-junit4-2.8.2.jar:f7ee4df4f7bd7b7f1cafad3b99eb74d579f109d5992ff625347352edb55e674c',
'org.jmock:jmock-legacy:2.8.2:jmock-legacy-2.8.2.jar:f2b985a5c08a9edb7f37612330c058809da3f6a6d63ce792426ebf8ff0d6d31b', 'org.jmock:jmock-legacy:2.8.2:jmock-legacy-2.8.2.jar:f2b985a5c08a9edb7f37612330c058809da3f6a6d63ce792426ebf8ff0d6d31b',
'org.jmock:jmock-testjar:2.8.2:jmock-testjar-2.8.2.jar:8900860f72c474e027cf97fe78dcbf154a1aa7fc62b6845c5fb4e4f3c7bc8760', 'org.jmock:jmock-testjar:2.8.2:jmock-testjar-2.8.2.jar:8900860f72c474e027cf97fe78dcbf154a1aa7fc62b6845c5fb4e4f3c7bc8760',

View File

@@ -22,21 +22,23 @@ import javax.inject.Inject;
class H2Database extends JdbcDatabase { class H2Database extends JdbcDatabase {
private static final String HASH_TYPE = "BINARY(32)"; private static final String HASH_TYPE = "BINARY(32)";
private static final String SECRET_TYPE = "BINARY(32)";
private static final String BINARY_TYPE = "BINARY"; private static final String BINARY_TYPE = "BINARY";
private static final String COUNTER_TYPE = "INT NOT NULL AUTO_INCREMENT"; private static final String COUNTER_TYPE = "INT NOT NULL AUTO_INCREMENT";
private static final String SECRET_TYPE = "BINARY(32)"; private static final String STRING_TYPE = "VARCHAR";
private final DatabaseConfig config; private final DatabaseConfig config;
private final String url; private final String url;
@Inject @Inject
H2Database(DatabaseConfig config, Clock clock) { H2Database(DatabaseConfig config, Clock clock) {
super(HASH_TYPE, BINARY_TYPE, COUNTER_TYPE, SECRET_TYPE, clock); super(HASH_TYPE, SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE,
clock);
this.config = config; this.config = config;
File dir = config.getDatabaseDirectory(); File dir = config.getDatabaseDirectory();
String path = new File(dir, "db").getAbsolutePath(); String path = new File(dir, "db").getAbsolutePath();
url = "jdbc:h2:split:" + path + ";CIPHER=AES;MULTI_THREADED=1" url = "jdbc:h2:split:" + path + ";CIPHER=AES;MULTI_THREADED=1"
+ ";WRITE_DELAY=0;DB_CLOSE_ON_EXIT=false"; + ";WRITE_DELAY=0";
} }
@Override @Override

View File

@@ -0,0 +1,99 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import javax.inject.Inject;
/**
* Contains all the HSQLDB-specific code for the database.
*/
@NotNullByDefault
class HyperSqlDatabase extends JdbcDatabase {
private static final String HASH_TYPE = "BINARY(32)";
private static final String SECRET_TYPE = "BINARY(32)";
private static final String BINARY_TYPE = "BINARY";
private static final String COUNTER_TYPE =
"INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY(START WITH 1)";
private static final String STRING_TYPE = "VARCHAR";
private final DatabaseConfig config;
private final String url;
@Inject
HyperSqlDatabase(DatabaseConfig config, Clock clock) {
super(HASH_TYPE, SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE,
clock);
this.config = config;
File dir = config.getDatabaseDirectory();
String path = new File(dir, "db").getAbsolutePath();
url = "jdbc:hsqldb:file:" + path
+ ";sql.enforce_size=false;allow_empty_batch=true"
+ ";encrypt_lobs=true;crypt_type=AES";
}
@Override
public boolean open() throws DbException {
boolean reopen = config.databaseExists();
if (!reopen) config.getDatabaseDirectory().mkdirs();
super.open("org.hsqldb.jdbc.JDBCDriver", reopen);
return reopen;
}
@Override
public void close() throws DbException {
try {
super.closeAllConnections();
Connection c = createConnection();
Statement s = c.createStatement();
s.executeQuery("SHUTDOWN");
s.close();
c.close();
} catch (SQLException e) {
throw new DbException(e);
}
}
@Override
public long getFreeSpace() throws DbException {
File dir = config.getDatabaseDirectory();
long maxSize = config.getMaxSize();
long free = dir.getFreeSpace();
long used = getDiskSpace(dir);
long quota = maxSize - used;
return Math.min(free, quota);
}
private long getDiskSpace(File f) {
if (f.isDirectory()) {
long total = 0;
File[] children = f.listFiles();
if (children != null)
for (File child : children) total += getDiskSpace(child);
return total;
} else if (f.isFile()) {
return f.length();
} else {
return 0;
}
}
@Override
protected Connection createConnection() throws SQLException {
SecretKey key = config.getEncryptionKey();
if (key == null) throw new IllegalStateException();
String hex = StringUtils.toHexString(key.getBytes());
return DriverManager.getConnection(url + ";crypt_key=" + hex);
}
}

View File

@@ -68,32 +68,32 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
@NotNullByDefault @NotNullByDefault
abstract class JdbcDatabase implements Database<Connection> { abstract class JdbcDatabase implements Database<Connection> {
private static final int SCHEMA_VERSION = 30; private static final int SCHEMA_VERSION = 31;
private static final int MIN_SCHEMA_VERSION = 30; private static final int MIN_SCHEMA_VERSION = 31;
private static final String CREATE_SETTINGS = private static final String CREATE_SETTINGS =
"CREATE TABLE settings" "CREATE TABLE settings"
+ " (namespace VARCHAR NOT NULL," + " (namespace _STRING NOT NULL,"
+ " key VARCHAR NOT NULL," + " settingKey _STRING NOT NULL,"
+ " value VARCHAR NOT NULL," + " value _STRING NOT NULL,"
+ " PRIMARY KEY (namespace, key))"; + " PRIMARY KEY (namespace, settingKey))";
private static final String CREATE_LOCAL_AUTHORS = private static final String CREATE_LOCAL_AUTHORS =
"CREATE TABLE localAuthors" "CREATE TABLE localAuthors"
+ " (authorId HASH NOT NULL," + " (authorId _HASH NOT NULL,"
+ " name VARCHAR NOT NULL," + " name _STRING NOT NULL,"
+ " publicKey BINARY NOT NULL," + " publicKey _BINARY NOT NULL,"
+ " privateKey BINARY NOT NULL," + " privateKey _BINARY NOT NULL,"
+ " created BIGINT NOT NULL," + " created BIGINT NOT NULL,"
+ " PRIMARY KEY (authorId))"; + " PRIMARY KEY (authorId))";
private static final String CREATE_CONTACTS = private static final String CREATE_CONTACTS =
"CREATE TABLE contacts" "CREATE TABLE contacts"
+ " (contactId COUNTER," + " (contactId _COUNTER,"
+ " authorId HASH NOT NULL," + " authorId _HASH NOT NULL,"
+ " name VARCHAR NOT NULL," + " name _STRING NOT NULL,"
+ " publicKey BINARY NOT NULL," + " publicKey _BINARY NOT NULL,"
+ " localAuthorId HASH NOT NULL," + " localAuthorId _HASH NOT NULL,"
+ " verified BOOLEAN NOT NULL," + " verified BOOLEAN NOT NULL,"
+ " active BOOLEAN NOT NULL," + " active BOOLEAN NOT NULL,"
+ " PRIMARY KEY (contactId)," + " PRIMARY KEY (contactId),"
@@ -103,17 +103,17 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_GROUPS = private static final String CREATE_GROUPS =
"CREATE TABLE groups" "CREATE TABLE groups"
+ " (groupId HASH NOT NULL," + " (groupId _HASH NOT NULL,"
+ " clientId VARCHAR NOT NULL," + " clientId _STRING NOT NULL,"
+ " descriptor BINARY NOT NULL," + " descriptor _BINARY NOT NULL,"
+ " PRIMARY KEY (groupId))"; + " PRIMARY KEY (groupId))";
private static final String CREATE_GROUP_METADATA = private static final String CREATE_GROUP_METADATA =
"CREATE TABLE groupMetadata" "CREATE TABLE groupMetadata"
+ " (groupId HASH NOT NULL," + " (groupId _HASH NOT NULL,"
+ " key VARCHAR NOT NULL," + " metaKey _STRING NOT NULL,"
+ " value BINARY NOT NULL," + " value _BINARY NOT NULL,"
+ " PRIMARY KEY (groupId, key)," + " PRIMARY KEY (groupId, metaKey),"
+ " FOREIGN KEY (groupId)" + " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)" + " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
@@ -121,7 +121,7 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_GROUP_VISIBILITIES = private static final String CREATE_GROUP_VISIBILITIES =
"CREATE TABLE groupVisibilities" "CREATE TABLE groupVisibilities"
+ " (contactId INT NOT NULL," + " (contactId INT NOT NULL,"
+ " groupId HASH NOT NULL," + " groupId _HASH NOT NULL,"
+ " shared BOOLEAN NOT NULL," + " shared BOOLEAN NOT NULL,"
+ " PRIMARY KEY (contactId, groupId)," + " PRIMARY KEY (contactId, groupId),"
+ " FOREIGN KEY (contactId)" + " FOREIGN KEY (contactId)"
@@ -133,8 +133,8 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_MESSAGES = private static final String CREATE_MESSAGES =
"CREATE TABLE messages" "CREATE TABLE messages"
+ " (messageId HASH NOT NULL," + " (messageId _HASH NOT NULL,"
+ " groupId HASH NOT NULL," + " groupId _HASH NOT NULL,"
+ " timestamp BIGINT NOT NULL," + " timestamp BIGINT NOT NULL,"
+ " state INT NOT NULL," + " state INT NOT NULL,"
+ " shared BOOLEAN NOT NULL," + " shared BOOLEAN NOT NULL,"
@@ -147,19 +147,19 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_MESSAGE_METADATA = private static final String CREATE_MESSAGE_METADATA =
"CREATE TABLE messageMetadata" "CREATE TABLE messageMetadata"
+ " (messageId HASH NOT NULL," + " (messageId _HASH NOT NULL,"
+ " key VARCHAR NOT NULL," + " metaKey _STRING NOT NULL,"
+ " value BINARY NOT NULL," + " value _BINARY NOT NULL,"
+ " PRIMARY KEY (messageId, key)," + " PRIMARY KEY (messageId, metaKey),"
+ " FOREIGN KEY (messageId)" + " FOREIGN KEY (messageId)"
+ " REFERENCES messages (messageId)" + " REFERENCES messages (messageId)"
+ " ON DELETE CASCADE)"; + " ON DELETE CASCADE)";
private static final String CREATE_MESSAGE_DEPENDENCIES = private static final String CREATE_MESSAGE_DEPENDENCIES =
"CREATE TABLE messageDependencies" "CREATE TABLE messageDependencies"
+ " (groupId HASH NOT NULL," + " (groupId _HASH NOT NULL,"
+ " messageId HASH NOT NULL," + " messageId _HASH NOT NULL,"
+ " dependencyId HASH NOT NULL," // Not a foreign key + " dependencyId _HASH NOT NULL," // Not a foreign key
+ " FOREIGN KEY (groupId)" + " FOREIGN KEY (groupId)"
+ " REFERENCES groups (groupId)" + " REFERENCES groups (groupId)"
+ " ON DELETE CASCADE," + " ON DELETE CASCADE,"
@@ -169,7 +169,7 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_OFFERS = private static final String CREATE_OFFERS =
"CREATE TABLE offers" "CREATE TABLE offers"
+ " (messageId HASH NOT NULL," // Not a foreign key + " (messageId _HASH NOT NULL," // Not a foreign key
+ " contactId INT NOT NULL," + " contactId INT NOT NULL,"
+ " PRIMARY KEY (messageId, contactId)," + " PRIMARY KEY (messageId, contactId),"
+ " FOREIGN KEY (contactId)" + " FOREIGN KEY (contactId)"
@@ -178,7 +178,7 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_STATUSES = private static final String CREATE_STATUSES =
"CREATE TABLE statuses" "CREATE TABLE statuses"
+ " (messageId HASH NOT NULL," + " (messageId _HASH NOT NULL,"
+ " contactId INT NOT NULL," + " contactId INT NOT NULL,"
+ " ack BOOLEAN NOT NULL," + " ack BOOLEAN NOT NULL,"
+ " seen BOOLEAN NOT NULL," + " seen BOOLEAN NOT NULL,"
@@ -195,20 +195,20 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_TRANSPORTS = private static final String CREATE_TRANSPORTS =
"CREATE TABLE transports" "CREATE TABLE transports"
+ " (transportId VARCHAR NOT NULL," + " (transportId _STRING NOT NULL,"
+ " maxLatency INT NOT NULL," + " maxLatency INT NOT NULL,"
+ " PRIMARY KEY (transportId))"; + " PRIMARY KEY (transportId))";
private static final String CREATE_INCOMING_KEYS = private static final String CREATE_INCOMING_KEYS =
"CREATE TABLE incomingKeys" "CREATE TABLE incomingKeys"
+ " (contactId INT NOT NULL," + " (contactId INT NOT NULL,"
+ " transportId VARCHAR NOT NULL," + " transportId _STRING NOT NULL,"
+ " period BIGINT NOT NULL," + " rotationPeriod BIGINT NOT NULL,"
+ " tagKey SECRET NOT NULL," + " tagKey _SECRET NOT NULL,"
+ " headerKey SECRET NOT NULL," + " headerKey _SECRET NOT NULL,"
+ " base BIGINT NOT NULL," + " base BIGINT NOT NULL,"
+ " bitmap BINARY NOT NULL," + " bitmap _BINARY NOT NULL,"
+ " PRIMARY KEY (contactId, transportId, period)," + " PRIMARY KEY (contactId, transportId, rotationPeriod),"
+ " FOREIGN KEY (contactId)" + " FOREIGN KEY (contactId)"
+ " REFERENCES contacts (contactId)" + " REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE," + " ON DELETE CASCADE,"
@@ -219,10 +219,10 @@ abstract class JdbcDatabase implements Database<Connection> {
private static final String CREATE_OUTGOING_KEYS = private static final String CREATE_OUTGOING_KEYS =
"CREATE TABLE outgoingKeys" "CREATE TABLE outgoingKeys"
+ " (contactId INT NOT NULL," + " (contactId INT NOT NULL,"
+ " transportId VARCHAR NOT NULL," + " transportId _STRING NOT NULL,"
+ " period BIGINT NOT NULL," + " rotationPeriod BIGINT NOT NULL,"
+ " tagKey SECRET NOT NULL," + " tagKey _SECRET NOT NULL,"
+ " headerKey SECRET NOT NULL," + " headerKey _SECRET NOT NULL,"
+ " stream BIGINT NOT NULL," + " stream BIGINT NOT NULL,"
+ " PRIMARY KEY (contactId, transportId)," + " PRIMARY KEY (contactId, transportId),"
+ " FOREIGN KEY (contactId)" + " FOREIGN KEY (contactId)"
@@ -260,7 +260,8 @@ abstract class JdbcDatabase implements Database<Connection> {
Logger.getLogger(JdbcDatabase.class.getName()); Logger.getLogger(JdbcDatabase.class.getName());
// Different database libraries use different names for certain types // Different database libraries use different names for certain types
private final String hashType, binaryType, counterType, secretType; private final String hashType, secretType, binaryType;
private final String counterType, stringType;
private final Clock clock; private final Clock clock;
// Locking: connectionsLock // Locking: connectionsLock
@@ -275,12 +276,13 @@ abstract class JdbcDatabase implements Database<Connection> {
private final Lock connectionsLock = new ReentrantLock(); private final Lock connectionsLock = new ReentrantLock();
private final Condition connectionsChanged = connectionsLock.newCondition(); private final Condition connectionsChanged = connectionsLock.newCondition();
JdbcDatabase(String hashType, String binaryType, String counterType, JdbcDatabase(String hashType, String secretType, String binaryType,
String secretType, Clock clock) { String counterType, String stringType, Clock clock) {
this.hashType = hashType; this.hashType = hashType;
this.secretType = secretType;
this.binaryType = binaryType; this.binaryType = binaryType;
this.counterType = counterType; this.counterType = counterType;
this.secretType = secretType; this.stringType = stringType;
this.clock = clock; this.clock = clock;
} }
@@ -383,16 +385,17 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
private String insertTypeNames(String s) { private String insertTypeNames(String s) {
s = s.replaceAll("HASH", hashType); s = s.replaceAll("_HASH", hashType);
s = s.replaceAll("BINARY", binaryType); s = s.replaceAll("_SECRET", secretType);
s = s.replaceAll("COUNTER", counterType); s = s.replaceAll("_BINARY", binaryType);
s = s.replaceAll("SECRET", secretType); s = s.replaceAll("_COUNTER", counterType);
s = s.replaceAll("_STRING", stringType);
return s; return s;
} }
@Override @Override
public Connection startTransaction() throws DbException { public Connection startTransaction() throws DbException {
Connection txn = null; Connection txn;
connectionsLock.lock(); connectionsLock.lock();
try { try {
if (closed) throw new DbClosedException(); if (closed) throw new DbClosedException();
@@ -500,7 +503,8 @@ abstract class JdbcDatabase implements Database<Connection> {
try { try {
// Create a contact row // Create a contact row
String sql = "INSERT INTO contacts" String sql = "INSERT INTO contacts"
+ " (authorId, name, publicKey, localAuthorId, verified, active)" + " (authorId, name, publicKey, localAuthorId,"
+ " verified, active)"
+ " VALUES (?, ?, ?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, remote.getId().getBytes()); ps.setBytes(1, remote.getId().getBytes());
@@ -719,7 +723,7 @@ abstract class JdbcDatabase implements Database<Connection> {
try { try {
// Store the incoming keys // Store the incoming keys
String sql = "INSERT INTO incomingKeys (contactId, transportId," String sql = "INSERT INTO incomingKeys (contactId, transportId,"
+ " period, tagKey, headerKey, base, bitmap)" + " rotationPeriod, tagKey, headerKey, base, bitmap)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
@@ -754,8 +758,8 @@ abstract class JdbcDatabase implements Database<Connection> {
if (rows != 1) throw new DbStateException(); if (rows != 1) throw new DbStateException();
ps.close(); ps.close();
// Store the outgoing keys // Store the outgoing keys
sql = "INSERT INTO outgoingKeys (contactId, transportId, period," sql = "INSERT INTO outgoingKeys (contactId, transportId,"
+ " tagKey, headerKey, stream)" + " rotationPeriod, tagKey, headerKey, stream)"
+ " VALUES (?, ?, ?, ?, ?, ?)"; + " VALUES (?, ?, ?, ?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
@@ -1335,7 +1339,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " JOIN messageMetadata AS md" + " JOIN messageMetadata AS md"
+ " ON m.messageId = md.messageId" + " ON m.messageId = md.messageId"
+ " WHERE state = ? AND groupId = ?" + " WHERE state = ? AND groupId = ?"
+ " AND key = ? AND value = ?"; + " AND metaKey = ? AND value = ?";
for (Entry<String, byte[]> e : query.entrySet()) { for (Entry<String, byte[]> e : query.entrySet()) {
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, DELIVERED.getValue()); ps.setInt(1, DELIVERED.getValue());
@@ -1367,7 +1371,7 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT m.messageId, key, value" String sql = "SELECT m.messageId, metaKey, value"
+ " FROM messages AS m" + " FROM messages AS m"
+ " JOIN messageMetadata AS md" + " JOIN messageMetadata AS md"
+ " ON m.messageId = md.messageId" + " ON m.messageId = md.messageId"
@@ -1417,7 +1421,7 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT key, value FROM groupMetadata" String sql = "SELECT metaKey, value FROM groupMetadata"
+ " WHERE groupId = ?"; + " WHERE groupId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes()); ps.setBytes(1, g.getBytes());
@@ -1440,7 +1444,7 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT key, value FROM messageMetadata AS md" String sql = "SELECT metaKey, value FROM messageMetadata AS md"
+ " JOIN messages AS m" + " JOIN messages AS m"
+ " ON m.messageId = md.messageId" + " ON m.messageId = md.messageId"
+ " WHERE m.state = ? AND md.messageId = ?"; + " WHERE m.state = ? AND md.messageId = ?";
@@ -1466,7 +1470,7 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT key, value FROM messageMetadata AS md" String sql = "SELECT metaKey, value FROM messageMetadata AS md"
+ " JOIN messages AS m" + " JOIN messages AS m"
+ " ON m.messageId = md.messageId" + " ON m.messageId = md.messageId"
+ " WHERE (m.state = ? OR m.state = ?)" + " WHERE (m.state = ? OR m.state = ?)"
@@ -1904,7 +1908,8 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT key, value FROM settings WHERE namespace = ?"; String sql = "SELECT settingKey, value FROM settings"
+ " WHERE namespace = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setString(1, namespace); ps.setString(1, namespace);
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -1927,10 +1932,11 @@ abstract class JdbcDatabase implements Database<Connection> {
ResultSet rs = null; ResultSet rs = null;
try { try {
// Retrieve the incoming keys // Retrieve the incoming keys
String sql = "SELECT period, tagKey, headerKey, base, bitmap" String sql = "SELECT rotationPeriod, tagKey, headerKey,"
+ " base, bitmap"
+ " FROM incomingKeys" + " FROM incomingKeys"
+ " WHERE transportId = ?" + " WHERE transportId = ?"
+ " ORDER BY contactId, period"; + " ORDER BY contactId, rotationPeriod";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setString(1, t.getString()); ps.setString(1, t.getString());
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -1947,10 +1953,10 @@ abstract class JdbcDatabase implements Database<Connection> {
rs.close(); rs.close();
ps.close(); ps.close();
// Retrieve the outgoing keys in the same order // Retrieve the outgoing keys in the same order
sql = "SELECT contactId, period, tagKey, headerKey, stream" sql = "SELECT contactId, rotationPeriod, tagKey, headerKey, stream"
+ " FROM outgoingKeys" + " FROM outgoingKeys"
+ " WHERE transportId = ?" + " WHERE transportId = ?"
+ " ORDER BY contactId, period"; + " ORDER BY contactId, rotationPeriod";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setString(1, t.getString()); ps.setString(1, t.getString());
rs = ps.executeQuery(); rs = ps.executeQuery();
@@ -1987,7 +1993,8 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE outgoingKeys SET stream = stream + 1" String sql = "UPDATE outgoingKeys SET stream = stream + 1"
+ " WHERE contactId = ? AND transportId = ? AND period = ?"; + " WHERE contactId = ? AND transportId = ?"
+ " AND rotationPeriod = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
ps.setString(2, t.getString()); ps.setString(2, t.getString());
@@ -2081,7 +2088,7 @@ abstract class JdbcDatabase implements Database<Connection> {
// Delete any keys that are being removed // Delete any keys that are being removed
if (!removed.isEmpty()) { if (!removed.isEmpty()) {
String sql = "DELETE FROM " + tableName String sql = "DELETE FROM " + tableName
+ " WHERE " + columnName + " = ? AND key = ?"; + " WHERE " + columnName + " = ? AND metaKey = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, id); ps.setBytes(1, id);
for (String key : removed) { for (String key : removed) {
@@ -2100,7 +2107,7 @@ abstract class JdbcDatabase implements Database<Connection> {
if (retained.isEmpty()) return; if (retained.isEmpty()) return;
// Update any keys that already exist // Update any keys that already exist
String sql = "UPDATE " + tableName + " SET value = ?" String sql = "UPDATE " + tableName + " SET value = ?"
+ " WHERE " + columnName + " = ? AND key = ?"; + " WHERE " + columnName + " = ? AND metaKey = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(2, id); ps.setBytes(2, id);
for (Entry<String, byte[]> e : retained.entrySet()) { for (Entry<String, byte[]> e : retained.entrySet()) {
@@ -2117,7 +2124,7 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
// Insert any keys that don't already exist // Insert any keys that don't already exist
sql = "INSERT INTO " + tableName sql = "INSERT INTO " + tableName
+ " (" + columnName + ", key, value)" + " (" + columnName + ", metaKey, value)"
+ " VALUES (?, ?, ?)"; + " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, id); ps.setBytes(1, id);
@@ -2149,7 +2156,7 @@ abstract class JdbcDatabase implements Database<Connection> {
try { try {
// Update any settings that already exist // Update any settings that already exist
String sql = "UPDATE settings SET value = ?" String sql = "UPDATE settings SET value = ?"
+ " WHERE namespace = ? AND key = ?"; + " WHERE namespace = ? AND settingKey = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
for (Entry<String, String> e : s.entrySet()) { for (Entry<String, String> e : s.entrySet()) {
ps.setString(1, e.getValue()); ps.setString(1, e.getValue());
@@ -2164,7 +2171,7 @@ abstract class JdbcDatabase implements Database<Connection> {
if (rows > 1) throw new DbStateException(); if (rows > 1) throw new DbStateException();
} }
// Insert any settings that don't already exist // Insert any settings that don't already exist
sql = "INSERT INTO settings (namespace, key, value)" sql = "INSERT INTO settings (namespace, settingKey, value)"
+ " VALUES (?, ?, ?)"; + " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
int updateIndex = 0, inserted = 0; int updateIndex = 0, inserted = 0;
@@ -2528,7 +2535,8 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
try { try {
String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?" String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?"
+ " WHERE contactId = ? AND transportId = ? AND period = ?"; + " WHERE contactId = ? AND transportId = ?"
+ " AND rotationPeriod = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setLong(1, base); ps.setLong(1, base);
ps.setBytes(2, bitmap); ps.setBytes(2, bitmap);

View File

@@ -0,0 +1,380 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.StringUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import static java.sql.Types.BINARY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public abstract class BasicDatabaseTest extends BrambleTestCase {
private static final int BATCH_SIZE = 100;
private final File testDir = TestUtils.getTestDirectory();
private final File db = new File(testDir, "db");
protected abstract String getBinaryType();
protected abstract String getDriverName();
protected abstract Connection openConnection(File db, boolean encrypt)
throws SQLException;
protected abstract void shutdownDatabase(File db, boolean encrypt)
throws SQLException;
@Before
public void setUp() throws Exception {
testDir.mkdirs();
Class.forName(getDriverName());
}
@Test
public void testInsertUpdateAndDelete() throws Exception {
Connection connection = openConnection(db, false);
try {
// Create the table
createTable(connection);
// Generate an ID and two names
byte[] id = TestUtils.getRandomId();
String oldName = StringUtils.getRandomString(50);
String newName = StringUtils.getRandomString(50);
// Insert the ID and old name into the table
insertRow(connection, id, oldName);
// Check that the old name can be retrieved using the ID
assertTrue(rowExists(connection, id));
assertEquals(oldName, getName(connection, id));
// Update the name
updateRow(connection, id, newName);
// Check that the new name can be retrieved using the ID
assertTrue(rowExists(connection, id));
assertEquals(newName, getName(connection, id));
// Delete the row from the table
assertTrue(deleteRow(connection, id));
// Check that the row no longer exists
assertFalse(rowExists(connection, id));
// Deleting the row again should have no effect
assertFalse(deleteRow(connection, id));
} finally {
connection.close();
shutdownDatabase(db, false);
}
}
@Test
public void testBatchInsertUpdateAndDelete() throws Exception {
Connection connection = openConnection(db, false);
try {
// Create the table
createTable(connection);
// Generate some IDs and two sets of names
byte[][] ids = new byte[BATCH_SIZE][];
String[] oldNames = new String[BATCH_SIZE];
String[] newNames = new String[BATCH_SIZE];
for (int i = 0; i < BATCH_SIZE; i++) {
ids[i] = TestUtils.getRandomId();
oldNames[i] = StringUtils.getRandomString(50);
newNames[i] = StringUtils.getRandomString(50);
}
// Insert the IDs and old names into the table as a batch
insertBatch(connection, ids, oldNames);
// Update the names as a batch
updateBatch(connection, ids, newNames);
// Check that the new names can be retrieved using the IDs
for (int i = 0; i < BATCH_SIZE; i++) {
assertTrue(rowExists(connection, ids[i]));
assertEquals(newNames[i], getName(connection, ids[i]));
}
// Delete the rows as a batch
boolean[] deleted = deleteBatch(connection, ids);
// Check that the rows no longer exist
for (int i = 0; i < BATCH_SIZE; i++) {
assertTrue(deleted[i]);
assertFalse(rowExists(connection, ids[i]));
}
// Deleting the rows again should have no effect
deleted = deleteBatch(connection, ids);
for (int i = 0; i < BATCH_SIZE; i++) assertFalse(deleted[i]);
} finally {
connection.close();
shutdownDatabase(db, false);
}
}
@Test
public void testSortOrder() throws Exception {
byte[] first = new byte[] {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0
};
byte[] second = new byte[] {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 127
};
byte[] third = new byte[] {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, (byte) 255
};
Connection connection = openConnection(db, false);
try {
// Create the table
createTable(connection);
// Insert the rows
insertRow(connection, first, "first");
insertRow(connection, second, "second");
insertRow(connection, third, "third");
insertRow(connection, null, "null");
// Check the ordering of the < operator: null is not comparable
assertNull(getPredecessor(connection, first));
assertEquals("first", getPredecessor(connection, second));
assertEquals("second", getPredecessor(connection, third));
assertNull(getPredecessor(connection, null));
// Check the ordering of ORDER BY: nulls come first
List<String> names = getNames(connection);
assertEquals(4, names.size());
assertEquals("null", names.get(0));
assertEquals("first", names.get(1));
assertEquals("second", names.get(2));
assertEquals("third", names.get(3));
} finally {
connection.close();
shutdownDatabase(db, false);
}
}
@Test
public void testDataIsFoundWithoutEncryption() throws Exception {
testEncryption(false);
}
@Test
public void testDataIsNotFoundWithEncryption() throws Exception {
testEncryption(true);
}
private void testEncryption(boolean encrypt) throws Exception {
byte[] sequence = new byte[] {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
Connection connection = openConnection(db, encrypt);
try {
createTable(connection);
insertRow(connection, sequence, "abcdefg");
} finally {
connection.close();
shutdownDatabase(db, encrypt);
}
try {
if (findSequence(testDir, sequence) == encrypt) fail();
} finally {
shutdownDatabase(db, encrypt);
}
}
private void createTable(Connection connection) throws SQLException {
Statement s = connection.createStatement();
String sql = "CREATE TABLE foo (uniqueId " + getBinaryType() + ","
+ " name VARCHAR(100) NOT NULL)";
s.executeUpdate(sql);
s.close();
}
private void insertRow(Connection connection, byte[] id, String name)
throws SQLException {
String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
if (id == null) ps.setNull(1, BINARY);
else ps.setBytes(1, id);
ps.setString(2, name);
int affected = ps.executeUpdate();
assertEquals(1, affected);
ps.close();
}
private boolean rowExists(Connection connection, byte[] id)
throws SQLException {
assertNotNull(id);
String sql = "SELECT * FROM foo WHERE uniqueID = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setBytes(1, id);
ResultSet rs = ps.executeQuery();
boolean found = rs.next();
assertFalse(rs.next());
rs.close();
ps.close();
return found;
}
private String getName(Connection connection, byte[] id)
throws SQLException {
assertNotNull(id);
String sql = "SELECT name FROM foo WHERE uniqueID = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setBytes(1, id);
ResultSet rs = ps.executeQuery();
assertTrue(rs.next());
String name = rs.getString(1);
assertFalse(rs.next());
rs.close();
ps.close();
return name;
}
private void updateRow(Connection connection, byte[] id, String name)
throws SQLException {
String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
PreparedStatement ps = connection.prepareStatement(sql);
if (id == null) ps.setNull(2, BINARY);
else ps.setBytes(2, id);
ps.setString(1, name);
assertEquals(1, ps.executeUpdate());
ps.close();
}
private boolean deleteRow(Connection connection, byte[] id)
throws SQLException {
String sql = "DELETE FROM foo WHERE uniqueId = ?";
PreparedStatement ps = connection.prepareStatement(sql);
if (id == null) ps.setNull(1, BINARY);
else ps.setBytes(1, id);
int affected = ps.executeUpdate();
ps.close();
return affected == 1;
}
private void insertBatch(Connection connection, byte[][] ids,
String[] names) throws SQLException {
assertEquals(ids.length, names.length);
String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 0; i < ids.length; i++) {
if (ids[i] == null) ps.setNull(1, BINARY);
else ps.setBytes(1, ids[i]);
ps.setString(2, names[i]);
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
assertEquals(ids.length, batchAffected.length);
for (int affected : batchAffected) assertEquals(1, affected);
ps.close();
}
private void updateBatch(Connection connection, byte[][] ids,
String[] names) throws SQLException {
assertEquals(ids.length, names.length);
String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 0; i < ids.length; i++) {
if (ids[i] == null) ps.setNull(2, BINARY);
else ps.setBytes(2, ids[i]);
ps.setString(1, names[i]);
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
assertEquals(ids.length, batchAffected.length);
for (int affected : batchAffected) assertEquals(1, affected);
ps.close();
}
private boolean[] deleteBatch(Connection connection, byte[][] ids)
throws SQLException {
String sql = "DELETE FROM foo WHERE uniqueId = ?";
PreparedStatement ps = connection.prepareStatement(sql);
for (byte[] id : ids) {
if (id == null) ps.setNull(1, BINARY);
else ps.setBytes(1, id);
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
assertEquals(ids.length, batchAffected.length);
boolean[] ret = new boolean[ids.length];
for (int i = 0; i < batchAffected.length; i++)
ret[i] = batchAffected[i] == 1;
ps.close();
return ret;
}
private String getPredecessor(Connection connection, byte[] id)
throws SQLException {
String sql = "SELECT name FROM foo WHERE uniqueId < ?"
+ " ORDER BY uniqueId DESC";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setBytes(1, id);
ps.setMaxRows(1);
ResultSet rs = ps.executeQuery();
String name = rs.next() ? rs.getString(1) : null;
assertFalse(rs.next());
rs.close();
ps.close();
return name;
}
private List<String> getNames(Connection connection) throws SQLException {
String sql = "SELECT name FROM foo ORDER BY uniqueId NULLS FIRST";
List<String> names = new ArrayList<>();
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
while (rs.next()) names.add(rs.getString(1));
rs.close();
ps.close();
return names;
}
private boolean findSequence(File f, byte[] sequence) throws IOException {
if (f.isDirectory()) {
File[] children = f.listFiles();
if (children != null)
for (File child : children)
if (findSequence(child, sequence)) return true;
return false;
} else if (f.isFile()) {
FileInputStream in = new FileInputStream(f);
try {
int offset = 0;
while (true) {
int read = in.read();
if (read == -1) return false;
if (((byte) read) == sequence[offset]) {
offset++;
if (offset == sequence.length) return true;
} else {
offset = 0;
}
}
} finally {
in.close();
}
} else {
return false;
}
}
@After
public void tearDown() throws Exception {
TestUtils.deleteTestDirectory(testDir);
}
}

View File

@@ -1,345 +1,46 @@
package org.briarproject.bramble.db; package org.briarproject.bramble.db;
import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.test.TestUtils; import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File; import java.io.File;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.util.Properties;
import java.util.ArrayList;
import java.util.List;
import static java.sql.Types.BINARY; public class BasicH2Test extends BasicDatabaseTest {
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class BasicH2Test extends BrambleTestCase { private final SecretKey key = TestUtils.getSecretKey();
private static final String CREATE_TABLE = @Override
"CREATE TABLE foo (uniqueId BINARY(32), name VARCHAR NOT NULL)"; protected String getBinaryType() {
private static final int BATCH_SIZE = 100; return "BINARY(32)";
private final File testDir = TestUtils.getTestDirectory();
private final File db = new File(testDir, "db");
private final String url = "jdbc:h2:" + db.getAbsolutePath();
private Connection connection = null;
@Before
public void setUp() throws Exception {
testDir.mkdirs();
Class.forName("org.h2.Driver");
connection = DriverManager.getConnection(url);
} }
@Test @Override
public void testInsertUpdateAndDelete() throws Exception { protected String getDriverName() {
// Create the table return "org.h2.Driver";
createTable(connection);
// Generate an ID and two names
byte[] id = TestUtils.getRandomId();
String oldName = StringUtils.getRandomString(50);
String newName = StringUtils.getRandomString(50);
// Insert the ID and old name into the table
insertRow(id, oldName);
// Check that the old name can be retrieved using the ID
assertTrue(rowExists(id));
assertEquals(oldName, getName(id));
// Update the name
updateRow(id, newName);
// Check that the new name can be retrieved using the ID
assertTrue(rowExists(id));
assertEquals(newName, getName(id));
// Delete the row from the table
assertTrue(deleteRow(id));
// Check that the row no longer exists
assertFalse(rowExists(id));
// Deleting the row again should have no effect
assertFalse(deleteRow(id));
} }
@Test @Override
public void testBatchInsertUpdateAndDelete() throws Exception { protected Connection openConnection(File db, boolean encrypt)
// Create the table throws SQLException {
createTable(connection); String url = "jdbc:h2:split:" + db.getAbsolutePath();
// Generate some IDs and two sets of names Properties props = new Properties();
byte[][] ids = new byte[BATCH_SIZE][]; props.setProperty("user", "user");
String[] oldNames = new String[BATCH_SIZE]; if (encrypt) {
String[] newNames = new String[BATCH_SIZE]; url += ";CIPHER=AES";
for (int i = 0; i < BATCH_SIZE; i++) { String hex = StringUtils.toHexString(key.getBytes());
ids[i] = TestUtils.getRandomId(); props.setProperty("password", hex + " password");
oldNames[i] = StringUtils.getRandomString(50);
newNames[i] = StringUtils.getRandomString(50);
} }
// Insert the IDs and old names into the table as a batch return DriverManager.getConnection(url, props);
insertBatch(ids, oldNames);
// Update the names as a batch
updateBatch(ids, newNames);
// Check that the new names can be retrieved using the IDs
for (int i = 0; i < BATCH_SIZE; i++) {
assertTrue(rowExists(ids[i]));
assertEquals(newNames[i], getName(ids[i]));
}
// Delete the rows as a batch
boolean[] deleted = deleteBatch(ids);
// Check that the rows no longer exist
for (int i = 0; i < BATCH_SIZE; i++) {
assertTrue(deleted[i]);
assertFalse(rowExists(ids[i]));
}
// Deleting the rows again should have no effect
deleted = deleteBatch(ids);
for (int i = 0; i < BATCH_SIZE; i++) assertFalse(deleted[i]);
} }
@Test @Override
public void testSortOrder() throws Exception { protected void shutdownDatabase(File db, boolean encrypt)
byte[] first = new byte[] { throws SQLException {
0, 0, 0, 0, 0, 0, 0, 0, // The DB is closed automatically when the connection is closed
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0
};
byte[] second = new byte[] {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 127
};
byte[] third = new byte[] {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, (byte) 255
};
// Create the table
createTable(connection);
// Insert the rows
insertRow(first, "first");
insertRow(second, "second");
insertRow(third, "third");
insertRow(null, "null");
// Check the ordering of the < operator: the null ID is not comparable
assertNull(getPredecessor(first));
assertEquals("first", getPredecessor(second));
assertEquals("second", getPredecessor(third));
assertNull(getPredecessor(null));
// Check the ordering of ORDER BY: nulls come first
List<String> names = getNames();
assertEquals(4, names.size());
assertEquals("null", names.get(0));
assertEquals("first", names.get(1));
assertEquals("second", names.get(2));
assertEquals("third", names.get(3));
}
private void createTable(Connection connection) throws SQLException {
try {
Statement s = connection.createStatement();
s.executeUpdate(CREATE_TABLE);
s.close();
} catch (SQLException e) {
connection.close();
throw e;
}
}
private void insertRow(byte[] id, String name) throws SQLException {
String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
try {
PreparedStatement ps = connection.prepareStatement(sql);
if (id == null) ps.setNull(1, BINARY);
else ps.setBytes(1, id);
ps.setString(2, name);
int affected = ps.executeUpdate();
assertEquals(1, affected);
ps.close();
} catch (SQLException e) {
connection.close();
throw e;
}
}
private boolean rowExists(byte[] id) throws SQLException {
assertNotNull(id);
String sql = "SELECT NULL FROM foo WHERE uniqueID = ?";
try {
PreparedStatement ps = connection.prepareStatement(sql);
ps.setBytes(1, id);
ResultSet rs = ps.executeQuery();
boolean found = rs.next();
assertFalse(rs.next());
rs.close();
ps.close();
return found;
} catch (SQLException e) {
connection.close();
throw e;
}
}
private String getName(byte[] id) throws SQLException {
assertNotNull(id);
String sql = "SELECT name FROM foo WHERE uniqueID = ?";
try {
PreparedStatement ps = connection.prepareStatement(sql);
ps.setBytes(1, id);
ResultSet rs = ps.executeQuery();
assertTrue(rs.next());
String name = rs.getString(1);
assertFalse(rs.next());
rs.close();
ps.close();
return name;
} catch (SQLException e) {
connection.close();
throw e;
}
}
private void updateRow(byte[] id, String name) throws SQLException {
String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
try {
PreparedStatement ps = connection.prepareStatement(sql);
if (id == null) ps.setNull(2, BINARY);
else ps.setBytes(2, id);
ps.setString(1, name);
assertEquals(1, ps.executeUpdate());
ps.close();
} catch (SQLException e) {
connection.close();
throw e;
}
}
private boolean deleteRow(byte[] id) throws SQLException {
String sql = "DELETE FROM foo WHERE uniqueId = ?";
try {
PreparedStatement ps = connection.prepareStatement(sql);
if (id == null) ps.setNull(1, BINARY);
else ps.setBytes(1, id);
int affected = ps.executeUpdate();
ps.close();
return affected == 1;
} catch (SQLException e) {
connection.close();
throw e;
}
}
private void insertBatch(byte[][] ids, String[] names) throws SQLException {
assertEquals(ids.length, names.length);
String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)";
try {
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 0; i < ids.length; i++) {
if (ids[i] == null) ps.setNull(1, BINARY);
else ps.setBytes(1, ids[i]);
ps.setString(2, names[i]);
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
assertEquals(ids.length, batchAffected.length);
for (int affected : batchAffected) assertEquals(1, affected);
ps.close();
} catch (SQLException e) {
connection.close();
throw e;
}
}
private void updateBatch(byte[][] ids, String[] names) throws SQLException {
assertEquals(ids.length, names.length);
String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?";
try {
PreparedStatement ps = connection.prepareStatement(sql);
for (int i = 0; i < ids.length; i++) {
if (ids[i] == null) ps.setNull(2, BINARY);
else ps.setBytes(2, ids[i]);
ps.setString(1, names[i]);
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
assertEquals(ids.length, batchAffected.length);
for (int affected : batchAffected) assertEquals(1, affected);
ps.close();
} catch (SQLException e) {
connection.close();
throw e;
}
}
private boolean[] deleteBatch(byte[][] ids) throws SQLException {
String sql = "DELETE FROM foo WHERE uniqueId = ?";
try {
PreparedStatement ps = connection.prepareStatement(sql);
for (byte[] id : ids) {
if (id == null) ps.setNull(1, BINARY);
else ps.setBytes(1, id);
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
assertEquals(ids.length, batchAffected.length);
boolean[] ret = new boolean[ids.length];
for (int i = 0; i < batchAffected.length; i++)
ret[i] = batchAffected[i] == 1;
ps.close();
return ret;
} catch (SQLException e) {
connection.close();
throw e;
}
}
private String getPredecessor(byte[] id) throws SQLException {
String sql = "SELECT name FROM foo WHERE uniqueId < ?"
+ " ORDER BY uniqueId DESC LIMIT ?";
try {
PreparedStatement ps = connection.prepareStatement(sql);
ps.setBytes(1, id);
ps.setInt(2, 1);
ResultSet rs = ps.executeQuery();
String name = rs.next() ? rs.getString(1) : null;
assertFalse(rs.next());
rs.close();
ps.close();
return name;
} catch (SQLException e) {
connection.close();
throw e;
}
}
private List<String> getNames() throws SQLException {
String sql = "SELECT name FROM foo ORDER BY uniqueId";
List<String> names = new ArrayList<>();
try {
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
while (rs.next()) names.add(rs.getString(1));
rs.close();
ps.close();
return names;
} catch (SQLException e) {
connection.close();
throw e;
}
}
@After
public void tearDown() throws Exception {
if (connection != null) connection.close();
TestUtils.deleteTestDirectory(testDir);
} }
} }

View File

@@ -0,0 +1,48 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.bramble.util.StringUtils;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class BasicHyperSqlTest extends BasicDatabaseTest {
private final SecretKey key = TestUtils.getSecretKey();
@Override
protected String getBinaryType() {
return "BINARY(32)";
}
@Override
protected String getDriverName() {
return "org.hsqldb.jdbc.JDBCDriver";
}
@Override
protected Connection openConnection(File db, boolean encrypt)
throws SQLException {
String url = "jdbc:hsqldb:file:" + db.getAbsolutePath() +
";sql.enforce_size=false;allow_empty_batch=true";
if (encrypt) {
String hex = StringUtils.toHexString(key.getBytes());
url += ";encrypt_lobs=true;crypt_type=AES;crypt_key=" + hex;
}
return DriverManager.getConnection(url);
}
@Override
protected void shutdownDatabase(File db, boolean encrypt)
throws SQLException {
Connection c = openConnection(db, encrypt);
Statement s = c.createStatement();
s.executeQuery("SHUTDOWN");
s.close();
c.close();
}
}

View File

@@ -19,7 +19,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
public class TransactionIsolationTest extends BrambleTestCase { public class H2TransactionIsolationTest extends BrambleTestCase {
private static final String DROP_TABLE = "DROP TABLE foo IF EXISTS"; private static final String DROP_TABLE = "DROP TABLE foo IF EXISTS";
private static final String CREATE_TABLE = "CREATE TABLE foo" private static final String CREATE_TABLE = "CREATE TABLE foo"
@@ -34,9 +34,9 @@ public class TransactionIsolationTest extends BrambleTestCase {
private final File testDir = TestUtils.getTestDirectory(); private final File testDir = TestUtils.getTestDirectory();
private final File db = new File(testDir, "db"); private final File db = new File(testDir, "db");
private final String withMvcc = "jdbc:h2:" + db.getAbsolutePath() private final String withMvcc = "jdbc:h2:split:" + db.getAbsolutePath()
+ ";MV_STORE=TRUE;MVCC=TRUE"; + ";MV_STORE=TRUE;MVCC=TRUE";
private final String withoutMvcc = "jdbc:h2:" + db.getAbsolutePath() private final String withoutMvcc = "jdbc:h2:split:" + db.getAbsolutePath()
+ ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1"; + ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1";
@Before @Before

View File

@@ -0,0 +1,16 @@
package org.briarproject.bramble.db;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.system.Clock;
public class HyperSqlDatabaseTest extends JdbcDatabaseTest {
public HyperSqlDatabaseTest() throws Exception {
super();
}
@Override
protected JdbcDatabase createDatabase(DatabaseConfig config, Clock clock) {
return new HyperSqlDatabase(config, clock);
}
}

File diff suppressed because it is too large Load Diff