mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-19 14:19:53 +01:00
Merge branch 'migration-30-31' into 'maintenance-0.16'
Beta: Migrate DB schema from version 30 to 31 See merge request akwizgran/briar!690
This commit is contained in:
@@ -71,7 +71,7 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
|
|||||||
abstract class JdbcDatabase implements Database<Connection> {
|
abstract class JdbcDatabase implements Database<Connection> {
|
||||||
|
|
||||||
// Package access for testing
|
// Package access for testing
|
||||||
static final int CODE_SCHEMA_VERSION = 30;
|
static final int CODE_SCHEMA_VERSION = 31;
|
||||||
|
|
||||||
private static final String CREATE_SETTINGS =
|
private static final String CREATE_SETTINGS =
|
||||||
"CREATE TABLE settings"
|
"CREATE TABLE settings"
|
||||||
@@ -150,11 +150,16 @@ 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,"
|
||||||
|
+ " groupId HASH NOT NULL," // Denormalised
|
||||||
|
+ " state INT NOT NULL," // Denormalised
|
||||||
+ " key VARCHAR NOT NULL,"
|
+ " key VARCHAR NOT NULL,"
|
||||||
+ " value BINARY NOT NULL,"
|
+ " value BINARY NOT NULL,"
|
||||||
+ " PRIMARY KEY (messageId, key),"
|
+ " PRIMARY KEY (messageId, key),"
|
||||||
+ " FOREIGN KEY (messageId)"
|
+ " FOREIGN KEY (messageId)"
|
||||||
+ " REFERENCES messages (messageId)"
|
+ " REFERENCES messages (messageId)"
|
||||||
|
+ " ON DELETE CASCADE,"
|
||||||
|
+ " FOREIGN KEY (groupId)"
|
||||||
|
+ " REFERENCES groups (groupId)"
|
||||||
+ " ON DELETE CASCADE)";
|
+ " ON DELETE CASCADE)";
|
||||||
|
|
||||||
private static final String CREATE_MESSAGE_DEPENDENCIES =
|
private static final String CREATE_MESSAGE_DEPENDENCIES =
|
||||||
@@ -242,6 +247,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
"CREATE INDEX IF NOT EXISTS groupsByClientId"
|
"CREATE INDEX IF NOT EXISTS groupsByClientId"
|
||||||
+ " ON groups (clientId)";
|
+ " ON groups (clientId)";
|
||||||
|
|
||||||
|
private static final String INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE =
|
||||||
|
"CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
|
||||||
|
+ " ON messageMetadata (groupId, state)";
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(JdbcDatabase.class.getName());
|
Logger.getLogger(JdbcDatabase.class.getName());
|
||||||
|
|
||||||
@@ -330,7 +339,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
|
|
||||||
// Package access for testing
|
// Package access for testing
|
||||||
List<Migration<Connection>> getMigrations() {
|
List<Migration<Connection>> getMigrations() {
|
||||||
return Collections.emptyList();
|
return Collections.singletonList(new Migration30_31());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storeSchemaVersion(Connection txn, int version)
|
private void storeSchemaVersion(Connection txn, int version)
|
||||||
@@ -387,6 +396,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
s = txn.createStatement();
|
s = txn.createStatement();
|
||||||
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
|
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
|
||||||
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
|
s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
|
||||||
|
s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
|
||||||
s.close();
|
s.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(s);
|
tryToClose(s);
|
||||||
@@ -512,7 +522,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());
|
||||||
@@ -1342,16 +1353,13 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
try {
|
try {
|
||||||
// Retrieve the message IDs for each query term and intersect
|
// Retrieve the message IDs for each query term and intersect
|
||||||
Set<MessageId> intersection = null;
|
Set<MessageId> intersection = null;
|
||||||
String sql = "SELECT m.messageId"
|
String sql = "SELECT messageId FROM messageMetadata"
|
||||||
+ " FROM messages AS m"
|
+ " WHERE groupId = ? AND state = ?"
|
||||||
+ " JOIN messageMetadata AS md"
|
|
||||||
+ " ON m.messageId = md.messageId"
|
|
||||||
+ " WHERE state = ? AND groupId = ?"
|
|
||||||
+ " AND key = ? AND value = ?";
|
+ " AND key = ? 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.setBytes(1, g.getBytes());
|
||||||
ps.setBytes(2, g.getBytes());
|
ps.setInt(2, DELIVERED.getValue());
|
||||||
ps.setString(3, e.getKey());
|
ps.setString(3, e.getKey());
|
||||||
ps.setBytes(4, e.getValue());
|
ps.setBytes(4, e.getValue());
|
||||||
rs = ps.executeQuery();
|
rs = ps.executeQuery();
|
||||||
@@ -1379,25 +1387,20 @@ 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 messageId, key, value"
|
||||||
+ " FROM messages AS m"
|
+ " FROM messageMetadata"
|
||||||
+ " JOIN messageMetadata AS md"
|
+ " WHERE groupId = ? AND state = ?";
|
||||||
+ " ON m.messageId = md.messageId"
|
|
||||||
+ " WHERE state = ? AND groupId = ?"
|
|
||||||
+ " ORDER BY m.messageId";
|
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setInt(1, DELIVERED.getValue());
|
ps.setBytes(1, g.getBytes());
|
||||||
ps.setBytes(2, g.getBytes());
|
ps.setInt(2, DELIVERED.getValue());
|
||||||
rs = ps.executeQuery();
|
rs = ps.executeQuery();
|
||||||
Map<MessageId, Metadata> all = new HashMap<>();
|
Map<MessageId, Metadata> all = new HashMap<>();
|
||||||
Metadata metadata = null;
|
|
||||||
MessageId lastMessageId = null;
|
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
MessageId messageId = new MessageId(rs.getBytes(1));
|
MessageId messageId = new MessageId(rs.getBytes(1));
|
||||||
if (lastMessageId == null || !messageId.equals(lastMessageId)) {
|
Metadata metadata = all.get(messageId);
|
||||||
|
if (metadata == null) {
|
||||||
metadata = new Metadata();
|
metadata = new Metadata();
|
||||||
all.put(messageId, metadata);
|
all.put(messageId, metadata);
|
||||||
lastMessageId = messageId;
|
|
||||||
}
|
}
|
||||||
metadata.put(rs.getString(2), rs.getBytes(3));
|
metadata.put(rs.getString(2), rs.getBytes(3));
|
||||||
}
|
}
|
||||||
@@ -1452,10 +1455,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 messageMetadata AS md"
|
String sql = "SELECT key, value FROM messageMetadata"
|
||||||
+ " JOIN messages AS m"
|
+ " WHERE state = ? AND messageId = ?";
|
||||||
+ " ON m.messageId = md.messageId"
|
|
||||||
+ " WHERE m.state = ? AND md.messageId = ?";
|
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setInt(1, DELIVERED.getValue());
|
ps.setInt(1, DELIVERED.getValue());
|
||||||
ps.setBytes(2, m.getBytes());
|
ps.setBytes(2, m.getBytes());
|
||||||
@@ -1478,11 +1479,9 @@ 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 key, value FROM messageMetadata"
|
||||||
+ " JOIN messages AS m"
|
+ " WHERE (state = ? OR state = ?)"
|
||||||
+ " ON m.messageId = md.messageId"
|
+ " AND messageId = ?";
|
||||||
+ " WHERE (m.state = ? OR m.state = ?)"
|
|
||||||
+ " AND md.messageId = ?";
|
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setInt(1, DELIVERED.getValue());
|
ps.setInt(1, DELIVERED.getValue());
|
||||||
ps.setInt(2, PENDING.getValue());
|
ps.setInt(2, PENDING.getValue());
|
||||||
@@ -2056,7 +2055,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
int[] batchAffected = ps.executeBatch();
|
int[] batchAffected = ps.executeBatch();
|
||||||
if (batchAffected.length != requested.size())
|
if (batchAffected.length != requested.size())
|
||||||
throw new DbStateException();
|
throw new DbStateException();
|
||||||
for (int rows: batchAffected) {
|
for (int rows : batchAffected) {
|
||||||
if (rows < 0) throw new DbStateException();
|
if (rows < 0) throw new DbStateException();
|
||||||
if (rows > 1) throw new DbStateException();
|
if (rows > 1) throw new DbStateException();
|
||||||
}
|
}
|
||||||
@@ -2070,25 +2069,92 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
@Override
|
@Override
|
||||||
public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta)
|
public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
mergeMetadata(txn, g.getBytes(), meta, "groupMetadata", "groupId");
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
Map<String, byte[]> added = removeOrUpdateMetadata(txn,
|
||||||
|
g.getBytes(), meta, "groupMetadata", "groupId");
|
||||||
|
if (added.isEmpty()) return;
|
||||||
|
// Insert any keys that don't already exist
|
||||||
|
String sql = "INSERT INTO groupMetadata (groupId, key, value)"
|
||||||
|
+ " VALUES (?, ?, ?)";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, g.getBytes());
|
||||||
|
for (Entry<String, byte[]> e : added.entrySet()) {
|
||||||
|
ps.setString(2, e.getKey());
|
||||||
|
ps.setBytes(3, e.getValue());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
int[] batchAffected = ps.executeBatch();
|
||||||
|
if (batchAffected.length != added.size())
|
||||||
|
throw new DbStateException();
|
||||||
|
for (int rows : batchAffected)
|
||||||
|
if (rows != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(ps);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta)
|
public void mergeMessageMetadata(Connection txn, MessageId m,
|
||||||
throws DbException {
|
Metadata meta) throws DbException {
|
||||||
mergeMetadata(txn, m.getBytes(), meta, "messageMetadata", "messageId");
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
Map<String, byte[]> added = removeOrUpdateMetadata(txn,
|
||||||
|
m.getBytes(), meta, "messageMetadata", "messageId");
|
||||||
|
if (added.isEmpty()) return;
|
||||||
|
// Get the group ID and message state for the denormalised columns
|
||||||
|
String sql = "SELECT groupId, state FROM messages"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
GroupId g = new GroupId(rs.getBytes(1));
|
||||||
|
State state = State.fromValue(rs.getInt(2));
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
// Insert any keys that don't already exist
|
||||||
|
sql = "INSERT INTO messageMetadata"
|
||||||
|
+ " (messageId, groupId, state, key, value)"
|
||||||
|
+ " VALUES (?, ?, ?, ?, ?)";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
ps.setBytes(2, g.getBytes());
|
||||||
|
ps.setInt(3, state.getValue());
|
||||||
|
for (Entry<String, byte[]> e : added.entrySet()) {
|
||||||
|
ps.setString(4, e.getKey());
|
||||||
|
ps.setBytes(5, e.getValue());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
int[] batchAffected = ps.executeBatch();
|
||||||
|
if (batchAffected.length != added.size())
|
||||||
|
throw new DbStateException();
|
||||||
|
for (int rows : batchAffected)
|
||||||
|
if (rows != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs);
|
||||||
|
tryToClose(ps);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeMetadata(Connection txn, byte[] id, Metadata meta,
|
// Removes or updates any existing entries, returns any entries that
|
||||||
String tableName, String columnName) throws DbException {
|
// need to be added
|
||||||
|
private Map<String, byte[]> removeOrUpdateMetadata(Connection txn,
|
||||||
|
byte[] id, Metadata meta, String tableName, String columnName)
|
||||||
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
// Determine which keys are being removed
|
// Determine which keys are being removed
|
||||||
List<String> removed = new ArrayList<>();
|
List<String> removed = new ArrayList<>();
|
||||||
Map<String, byte[]> retained = new HashMap<>();
|
Map<String, byte[]> notRemoved = new HashMap<>();
|
||||||
for (Entry<String, byte[]> e : meta.entrySet()) {
|
for (Entry<String, byte[]> e : meta.entrySet()) {
|
||||||
if (e.getValue() == REMOVE) removed.add(e.getKey());
|
if (e.getValue() == REMOVE) removed.add(e.getKey());
|
||||||
else retained.put(e.getKey(), e.getValue());
|
else notRemoved.put(e.getKey(), e.getValue());
|
||||||
}
|
}
|
||||||
// Delete any keys that are being removed
|
// Delete any keys that are being removed
|
||||||
if (!removed.isEmpty()) {
|
if (!removed.isEmpty()) {
|
||||||
@@ -2109,45 +2175,33 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
ps.close();
|
ps.close();
|
||||||
}
|
}
|
||||||
if (retained.isEmpty()) return;
|
if (notRemoved.isEmpty()) return Collections.emptyMap();
|
||||||
// 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 key = ?";
|
||||||
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 : notRemoved.entrySet()) {
|
||||||
ps.setBytes(1, e.getValue());
|
ps.setBytes(1, e.getValue());
|
||||||
ps.setString(3, e.getKey());
|
ps.setString(3, e.getKey());
|
||||||
ps.addBatch();
|
ps.addBatch();
|
||||||
}
|
}
|
||||||
int[] batchAffected = ps.executeBatch();
|
int[] batchAffected = ps.executeBatch();
|
||||||
if (batchAffected.length != retained.size())
|
if (batchAffected.length != notRemoved.size())
|
||||||
throw new DbStateException();
|
throw new DbStateException();
|
||||||
for (int rows : batchAffected) {
|
for (int rows : batchAffected) {
|
||||||
if (rows < 0) throw new DbStateException();
|
if (rows < 0) throw new DbStateException();
|
||||||
if (rows > 1) throw new DbStateException();
|
if (rows > 1) throw new DbStateException();
|
||||||
}
|
}
|
||||||
// Insert any keys that don't already exist
|
|
||||||
sql = "INSERT INTO " + tableName
|
|
||||||
+ " (" + columnName + ", key, value)"
|
|
||||||
+ " VALUES (?, ?, ?)";
|
|
||||||
ps = txn.prepareStatement(sql);
|
|
||||||
ps.setBytes(1, id);
|
|
||||||
int updateIndex = 0, inserted = 0;
|
|
||||||
for (Entry<String, byte[]> e : retained.entrySet()) {
|
|
||||||
if (batchAffected[updateIndex] == 0) {
|
|
||||||
ps.setString(2, e.getKey());
|
|
||||||
ps.setBytes(3, e.getValue());
|
|
||||||
ps.addBatch();
|
|
||||||
inserted++;
|
|
||||||
}
|
|
||||||
updateIndex++;
|
|
||||||
}
|
|
||||||
batchAffected = ps.executeBatch();
|
|
||||||
if (batchAffected.length != inserted) throw new DbStateException();
|
|
||||||
for (int rows : batchAffected)
|
|
||||||
if (rows != 1) throw new DbStateException();
|
|
||||||
ps.close();
|
ps.close();
|
||||||
|
// Are there any keys that don't already exist?
|
||||||
|
Map<String, byte[]> added = new HashMap<>();
|
||||||
|
int updateIndex = 0;
|
||||||
|
for (Entry<String, byte[]> e : notRemoved.entrySet()) {
|
||||||
|
if (batchAffected[updateIndex++] == 0)
|
||||||
|
added.put(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
return added;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(ps);
|
tryToClose(ps);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
@@ -2500,7 +2554,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMessageShared(Connection txn, MessageId m) throws DbException {
|
public void setMessageShared(Connection txn, MessageId m)
|
||||||
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
String sql = "UPDATE messages SET shared = TRUE"
|
String sql = "UPDATE messages SET shared = TRUE"
|
||||||
@@ -2528,6 +2583,14 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
|
// Update denormalised column in messageMetadata
|
||||||
|
sql = "UPDATE messageMetadata SET state = ? WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setInt(1, state.getValue());
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
|
affected = ps.executeUpdate();
|
||||||
|
if (affected < 0) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(ps);
|
tryToClose(ps);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
|
||||||
|
class Migration30_31 implements Migration<Connection> {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
Logger.getLogger(Migration30_31.class.getName());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStartVersion() {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getEndVersion() {
|
||||||
|
return 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(Connection txn) throws DbException {
|
||||||
|
Statement s = null;
|
||||||
|
try {
|
||||||
|
s = txn.createStatement();
|
||||||
|
// Add groupId column
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ADD COLUMN groupId BINARY(32) AFTER messageId");
|
||||||
|
// Populate groupId column
|
||||||
|
s.execute("UPDATE messageMetadata AS mm SET groupId ="
|
||||||
|
+ " (SELECT groupId FROM messages AS m"
|
||||||
|
+ " WHERE mm.messageId = m.messageId)");
|
||||||
|
// Add not null constraint now column has been populated
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ALTER COLUMN groupId"
|
||||||
|
+ " SET NOT NULL");
|
||||||
|
// Add foreign key constraint
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ADD CONSTRAINT groupIdForeignKey"
|
||||||
|
+ " FOREIGN KEY (groupId)"
|
||||||
|
+ " REFERENCES groups (groupId)"
|
||||||
|
+ " ON DELETE CASCADE");
|
||||||
|
// Add state column
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ADD COLUMN state INT AFTER groupId");
|
||||||
|
// Populate state column
|
||||||
|
s.execute("UPDATE messageMetadata AS mm SET state ="
|
||||||
|
+ " (SELECT state FROM messages AS m"
|
||||||
|
+ " WHERE mm.messageId = m.messageId)");
|
||||||
|
// Add not null constraint now column has been populated
|
||||||
|
s.execute("ALTER TABLE messageMetadata"
|
||||||
|
+ " ALTER COLUMN state"
|
||||||
|
+ " SET NOT NULL");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(s);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryToClose(@Nullable Statement s) {
|
||||||
|
try {
|
||||||
|
if (s != null) s.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.Metadata;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.Message;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.bramble.api.sync.ValidationManager.State;
|
||||||
|
import org.briarproject.bramble.test.BrambleTestCase;
|
||||||
|
import org.briarproject.bramble.test.TestUtils;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import static junit.framework.TestCase.assertNotNull;
|
||||||
|
import static junit.framework.TestCase.assertTrue;
|
||||||
|
import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
|
||||||
|
import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
|
||||||
|
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||||
|
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class Migration30_31Test extends BrambleTestCase {
|
||||||
|
|
||||||
|
private static final String CREATE_GROUPS_STUB =
|
||||||
|
"CREATE TABLE groups"
|
||||||
|
+ " (groupID BINARY(32) NOT NULL,"
|
||||||
|
+ " PRIMARY KEY (groupId))";
|
||||||
|
|
||||||
|
private static final String CREATE_MESSAGES =
|
||||||
|
"CREATE TABLE messages"
|
||||||
|
+ " (messageId BINARY(32) NOT NULL,"
|
||||||
|
+ " groupId BINARY(32) NOT NULL,"
|
||||||
|
+ " timestamp BIGINT NOT NULL,"
|
||||||
|
+ " state INT NOT NULL,"
|
||||||
|
+ " shared BOOLEAN NOT NULL,"
|
||||||
|
+ " length INT NOT NULL,"
|
||||||
|
+ " raw BLOB," // Null if message has been deleted
|
||||||
|
+ " PRIMARY KEY (messageId),"
|
||||||
|
+ " FOREIGN KEY (groupId)"
|
||||||
|
+ " REFERENCES groups (groupId)"
|
||||||
|
+ " ON DELETE CASCADE)";
|
||||||
|
|
||||||
|
private static final String CREATE_MESSAGE_METADATA_30 =
|
||||||
|
"CREATE TABLE messageMetadata"
|
||||||
|
+ " (messageId BINARY(32) NOT NULL,"
|
||||||
|
+ " key VARCHAR NOT NULL,"
|
||||||
|
+ " value BINARY NOT NULL,"
|
||||||
|
+ " PRIMARY KEY (messageId, key),"
|
||||||
|
+ " FOREIGN KEY (messageId)"
|
||||||
|
+ " REFERENCES messages (messageId)"
|
||||||
|
+ " ON DELETE CASCADE)";
|
||||||
|
|
||||||
|
private final File testDir = TestUtils.getTestDirectory();
|
||||||
|
private final File db = new File(testDir, "db");
|
||||||
|
private final String url = "jdbc:h2:" + db.getAbsolutePath();
|
||||||
|
private final GroupId groupId = new GroupId(getRandomId());
|
||||||
|
private final GroupId groupId1 = new GroupId(getRandomId());
|
||||||
|
private final Message message = TestUtils.getMessage(groupId);
|
||||||
|
private final Message message1 = TestUtils.getMessage(groupId1);
|
||||||
|
private final Metadata meta = new Metadata(), meta1 = new Metadata();
|
||||||
|
|
||||||
|
private Connection connection = null;
|
||||||
|
|
||||||
|
public Migration30_31Test() {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
meta.put(getRandomString(123 + i), getRandomBytes(123 + i));
|
||||||
|
meta1.put(getRandomString(123 + i), getRandomBytes(123 + i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
assertTrue(testDir.mkdirs());
|
||||||
|
Class.forName("org.h2.Driver");
|
||||||
|
connection = DriverManager.getConnection(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
if (connection != null) connection.close();
|
||||||
|
TestUtils.deleteTestDirectory(testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMigration() throws Exception {
|
||||||
|
try {
|
||||||
|
Statement s = connection.createStatement();
|
||||||
|
s.execute(CREATE_GROUPS_STUB);
|
||||||
|
s.execute(CREATE_MESSAGES);
|
||||||
|
s.execute(CREATE_MESSAGE_METADATA_30);
|
||||||
|
s.close();
|
||||||
|
|
||||||
|
addGroup(groupId);
|
||||||
|
addMessage(message, DELIVERED, true);
|
||||||
|
addMessageMetadata30(message, meta);
|
||||||
|
assertMetadataEquals(meta, getMessageMetadata(message.getId()));
|
||||||
|
|
||||||
|
addGroup(groupId1);
|
||||||
|
addMessage(message1, UNKNOWN, false);
|
||||||
|
addMessageMetadata30(message1, meta1);
|
||||||
|
assertMetadataEquals(meta1, getMessageMetadata(message1.getId()));
|
||||||
|
|
||||||
|
new Migration30_31().migrate(connection);
|
||||||
|
|
||||||
|
assertMetadataEquals(meta, getMessageMetadata(message.getId()));
|
||||||
|
for (String key : meta.keySet()) {
|
||||||
|
GroupId g = getMessageMetadataGroupId31(message.getId(), key);
|
||||||
|
assertEquals(groupId, g);
|
||||||
|
State state = getMessageMetadataState31(message.getId(), key);
|
||||||
|
assertEquals(DELIVERED, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertMetadataEquals(meta1, getMessageMetadata(message1.getId()));
|
||||||
|
for (String key : meta1.keySet()) {
|
||||||
|
GroupId g = getMessageMetadataGroupId31(message1.getId(), key);
|
||||||
|
assertEquals(groupId1, g);
|
||||||
|
State state = getMessageMetadataState31(message1.getId(), key);
|
||||||
|
assertEquals(UNKNOWN, state);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
connection.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addGroup(GroupId g) throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "INSERT INTO groups (groupId) VALUES (?)";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, g.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMessage(Message m, State state, boolean shared)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
|
||||||
|
+ " state, shared, length, raw)"
|
||||||
|
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getId().getBytes());
|
||||||
|
ps.setBytes(2, m.getGroupId().getBytes());
|
||||||
|
ps.setLong(3, m.getTimestamp());
|
||||||
|
ps.setInt(4, state.getValue());
|
||||||
|
ps.setBoolean(5, shared);
|
||||||
|
byte[] raw = m.getRaw();
|
||||||
|
ps.setInt(6, raw.length);
|
||||||
|
ps.setBytes(7, raw);
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMessageMetadata30(Message m, Metadata meta)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "INSERT INTO messageMetadata"
|
||||||
|
+ " (messageId, key, value)"
|
||||||
|
+ " VALUES (?, ?, ?)";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getId().getBytes());
|
||||||
|
for (Entry<String, byte[]> e : meta.entrySet()) {
|
||||||
|
ps.setString(2, e.getKey());
|
||||||
|
ps.setBytes(3, e.getValue());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
int[] batchAffected = ps.executeBatch();
|
||||||
|
if (batchAffected.length != meta.size())
|
||||||
|
throw new DbStateException();
|
||||||
|
for (int rows : batchAffected)
|
||||||
|
if (rows != 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Metadata getMessageMetadata(MessageId m) throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT key, value FROM messageMetadata"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
Metadata meta = new Metadata();
|
||||||
|
while (rs.next()) meta.put(rs.getString(1), rs.getBytes(2));
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return meta;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (rs != null) rs.close();
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupId getMessageMetadataGroupId31(MessageId m, String key)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT groupId FROM messageMetadata"
|
||||||
|
+ " WHERE messageId = ? AND key = ?";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
ps.setString(2, key);
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
GroupId g = new GroupId(rs.getBytes(1));
|
||||||
|
if (rs.next()) throw new DbStateException();
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return g;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (rs != null) rs.close();
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private State getMessageMetadataState31(MessageId m, String key)
|
||||||
|
throws SQLException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT state FROM messageMetadata"
|
||||||
|
+ " WHERE messageId = ? AND key = ?";
|
||||||
|
ps = connection.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
ps.setString(2, key);
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
State state = State.fromValue(rs.getInt(1));
|
||||||
|
if (rs.next()) throw new DbStateException();
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return state;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
if (rs != null) rs.close();
|
||||||
|
if (ps != null) ps.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMetadataEquals(Metadata expected, Metadata actual) {
|
||||||
|
assertEquals(expected.size(), actual.size());
|
||||||
|
for (Entry<String, byte[]> e : expected.entrySet()) {
|
||||||
|
byte[] value = actual.get(e.getKey());
|
||||||
|
assertNotNull(value);
|
||||||
|
assertArrayEquals(e.getValue(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user