Update the groups tables to use foreign keys.

This commit is contained in:
Greyson Parrelli
2023-08-16 12:23:54 -04:00
committed by GitHub
parent 3be5d61ced
commit 442a66df2e
8 changed files with 151 additions and 45 deletions

View File

@@ -83,15 +83,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
const val AVATAR_ID = "avatar_id"
const val AVATAR_KEY = "avatar_key"
const val AVATAR_CONTENT_TYPE = "avatar_content_type"
const val AVATAR_RELAY = "avatar_relay"
const val AVATAR_DIGEST = "avatar_digest"
const val TIMESTAMP = "timestamp"
const val ACTIVE = "active"
const val MMS = "mms"
const val EXPECTED_V2_ID = "expected_v2_id"
const val UNMIGRATED_V1_MEMBERS = "former_v1_members"
const val UNMIGRATED_V1_MEMBERS = "unmigrated_v1_members"
const val DISTRIBUTION_ID = "distribution_id"
const val SHOW_AS_STORY_STATE = "display_as_story"
const val SHOW_AS_STORY_STATE = "show_as_story_state"
const val LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp"
/** 32 bytes serialized [GroupMasterKey] */
@@ -103,44 +102,33 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
/** Serialized [DecryptedGroup] protobuf */
const val V2_DECRYPTED_GROUP = "decrypted_group"
/** Was temporarily used for PNP accept by pni but is no longer needed/updated */
@Deprecated("")
private val AUTH_SERVICE_ID = "auth_service_id"
@JvmField
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$GROUP_ID TEXT,
$RECIPIENT_ID INTEGER,
$TITLE TEXT,
$AVATAR_ID INTEGER,
$AVATAR_KEY BLOB,
$AVATAR_CONTENT_TYPE TEXT,
$AVATAR_RELAY TEXT,
$TIMESTAMP INTEGER,
$GROUP_ID TEXT NOT NULL UNIQUE,
$RECIPIENT_ID INTEGER NOT NULL UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$TITLE TEXT DEFAULT NULL,
$AVATAR_ID INTEGER DEFAULT 0,
$AVATAR_KEY BLOB DEFAULT NULL,
$AVATAR_CONTENT_TYPE TEXT DEFAULT NULL,
$AVATAR_DIGEST BLOB DEFAULT NULL,
$TIMESTAMP INTEGER DEFAULT 0,
$ACTIVE INTEGER DEFAULT 1,
$AVATAR_DIGEST BLOB,
$MMS INTEGER DEFAULT 0,
$V2_MASTER_KEY BLOB,
$V2_REVISION BLOB,
$V2_DECRYPTED_GROUP BLOB,
$EXPECTED_V2_ID TEXT DEFAULT NULL,
$V2_MASTER_KEY BLOB DEFAULT NULL,
$V2_REVISION BLOB DEFAULT NULL,
$V2_DECRYPTED_GROUP BLOB DEFAULT NULL,
$EXPECTED_V2_ID TEXT UNIQUE DEFAULT NULL,
$UNMIGRATED_V1_MEMBERS TEXT DEFAULT NULL,
$DISTRIBUTION_ID TEXT DEFAULT NULL,
$SHOW_AS_STORY_STATE INTEGER DEFAULT 0,
$AUTH_SERVICE_ID TEXT DEFAULT NULL,
$DISTRIBUTION_ID TEXT UNIQUE DEFAULT NULL,
$SHOW_AS_STORY_STATE INTEGER DEFAULT ${ShowAsStoryState.IF_ACTIVE.code},
$LAST_FORCE_UPDATE_TIMESTAMP INTEGER DEFAULT 0
)
"""
@JvmField
val CREATE_INDEXS = arrayOf(
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON $TABLE_NAME ($GROUP_ID);",
"CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON $TABLE_NAME ($RECIPIENT_ID);",
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON $TABLE_NAME ($EXPECTED_V2_ID);",
"CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON $TABLE_NAME($DISTRIBUTION_ID);"
) + MembershipTable.CREATE_INDEXES
val CREATE_INDEXS = MembershipTable.CREATE_INDEXES
private val GROUP_PROJECTION = arrayOf(
GROUP_ID,
@@ -150,7 +138,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
AVATAR_ID,
AVATAR_KEY,
AVATAR_CONTENT_TYPE,
AVATAR_RELAY,
AVATAR_DIGEST,
TIMESTAMP,
ACTIVE,
@@ -194,7 +181,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$GROUP_ID TEXT NOT NULL,
$GROUP_ID TEXT NOT NULL REFERENCES ${GroupTable.TABLE_NAME} (${GroupTable.GROUP_ID}) ON DELETE CASCADE,
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
UNIQUE($GROUP_ID, $RECIPIENT_ID)
)
@@ -656,17 +643,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
@CheckReturnValue
fun create(groupId: GroupId.V1, title: String?, members: Collection<RecipientId>, avatar: SignalServiceAttachmentPointer?, relay: String?): Boolean {
fun create(groupId: GroupId.V1, title: String?, members: Collection<RecipientId>, avatar: SignalServiceAttachmentPointer?): Boolean {
if (groupExists(groupId.deriveV2MigrationGroupId())) {
throw LegacyGroupInsertException(groupId)
}
return create(groupId, title, members, avatar, relay, null, null)
return create(groupId, title, members, avatar, null, null)
}
@CheckReturnValue
fun create(groupId: GroupId.Mms, title: String?, members: Collection<RecipientId>): Boolean {
return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null, null)
return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null)
}
@JvmOverloads
@@ -680,7 +667,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
Log.w(TAG, "Forcing the creation of a group even though we already have a V1 ID!")
}
return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, relay = null, groupMasterKey = groupMasterKey, groupState = groupState)) {
return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, groupMasterKey = groupMasterKey, groupState = groupState)) {
groupId
} else {
null
@@ -731,7 +718,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
title: String?,
memberCollection: Collection<RecipientId>,
avatar: SignalServiceAttachmentPointer?,
relay: String?,
groupMasterKey: GroupMasterKey?,
groupState: DecryptedGroup?
): Boolean {
@@ -757,7 +743,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
values.put(AVATAR_ID, 0)
}
values.put(AVATAR_RELAY, relay)
values.put(TIMESTAMP, System.currentTimeMillis())
if (groupId.isV2) {
@@ -1176,7 +1161,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
avatarId = cursor.requireLong(AVATAR_ID),
avatarKey = cursor.requireBlob(AVATAR_KEY),
avatarContentType = cursor.requireString(AVATAR_CONTENT_TYPE),
relay = cursor.requireString(AVATAR_RELAY),
isActive = cursor.requireBoolean(ACTIVE),
avatarDigest = cursor.requireBlob(AVATAR_DIGEST),
isMms = cursor.requireBoolean(MMS),

View File

@@ -59,6 +59,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V200_ResetPniColumn
import org.thoughtcrime.securesms.database.helpers.migration.V201_RecipientTableValidations
import org.thoughtcrime.securesms.database.helpers.migration.V202_DropMessageTableThreadDateIndex
import org.thoughtcrime.securesms.database.helpers.migration.V203_PreKeyStaleTimestamp
import org.thoughtcrime.securesms.database.helpers.migration.V204_GroupForeignKeyMigration
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@@ -67,7 +68,7 @@ object SignalDatabaseMigrations {
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
const val DATABASE_VERSION = 203
const val DATABASE_VERSION = 204
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -290,6 +291,10 @@ object SignalDatabaseMigrations {
if (oldVersion < 203) {
V203_PreKeyStaleTimestamp.migrate(context, db, oldVersion, newVersion)
}
if (oldVersion < 204) {
V204_GroupForeignKeyMigration.migrate(context, db, oldVersion, newVersion)
}
}
@JvmStatic

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.signal.core.util.SqlUtil
import org.signal.core.util.Stopwatch
import org.signal.core.util.logging.Log
/**
* Back CallLinks with a Recipient to ease integration and ensure we can support
* different features which would require that relation in the future.
*/
@Suppress("ClassName")
object V204_GroupForeignKeyMigration : SignalDatabaseMigration {
private val TAG = Log.tag(V204_GroupForeignKeyMigration::class.java)
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
val stopwatch = Stopwatch("migration")
db.execSQL(
"""
CREATE TABLE groups_tmp (
_id INTEGER PRIMARY KEY,
group_id TEXT NOT NULL UNIQUE,
recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,
title TEXT DEFAULT NULL,
avatar_id INTEGER DEFAULT 0,
avatar_key BLOB DEFAULT NULL,
avatar_content_type TEXT DEFAULT NULL,
avatar_digest BLOB DEFAULT NULL,
timestamp INTEGER DEFAULT 0,
active INTEGER DEFAULT 1,
mms INTEGER DEFAULT 0,
master_key BLOB DEFAULT NULL,
revision BLOB DEFAULT NULL,
decrypted_group BLOB DEFAULT NULL,
expected_v2_id TEXT UNIQUE DEFAULT NULL,
unmigrated_v1_members TEXT DEFAULT NULL,
distribution_id TEXT UNIQUE DEFAULT NULL,
show_as_story_state INTEGER DEFAULT 0,
last_force_update_timestamp INTEGER DEFAULT 0
)
"""
)
val danglingRecipientCount = db.delete("groups", "recipient_id NOT IN (SELECT _id FROM recipient)", null)
Log.i(TAG, "There were $danglingRecipientCount groups that referenced non-existent recipients.")
db.execSQL(
"""
INSERT INTO groups_tmp SELECT
_id,
group_id,
recipient_id,
title,
avatar_id,
avatar_key,
avatar_content_Type,
avatar_digest,
timestamp,
active,
mms,
master_key,
revision,
decrypted_group,
expected_v2_id,
former_v1_members,
distribution_id,
display_as_story,
last_force_update_timestamp
FROM groups
"""
)
stopwatch.split("groups-table")
db.execSQL("DROP TABLE groups")
db.execSQL("ALTER TABLE groups_tmp RENAME TO groups")
db.execSQL(
"""
CREATE TABLE group_membership_tmp (
_id INTEGER PRIMARY KEY,
group_id TEXT NOT NULL REFERENCES groups (group_id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
UNIQUE(group_id, recipient_id)
)
"""
)
val danglingMemberCount = db.delete("group_membership", "group_id NOT IN (SELECT group_id FROM groups)", null)
Log.i(TAG, "There were $danglingMemberCount members that referenced non-existent groups.")
db.execSQL(
"""
INSERT INTO group_membership_tmp SELECT * FROM group_membership
"""
)
stopwatch.split("membership-table")
db.execSQL("DROP TABLE group_membership")
db.execSQL("ALTER TABLE group_membership_tmp RENAME TO group_membership")
db.execSQL("CREATE INDEX IF NOT EXISTS group_membership_recipient_id ON group_membership (recipient_id)")
stopwatch.split("membership-index")
val foreignKeyViolations: List<SqlUtil.ForeignKeyViolation> = SqlUtil.getForeignKeyViolations(db, "groups") + SqlUtil.getForeignKeyViolations(db, "group_membership")
if (foreignKeyViolations.isNotEmpty()) {
Log.w(TAG, "Foreign key violations!\n${foreignKeyViolations.joinToString(separator = "\n")}")
throw IllegalStateException("Foreign key violations!")
}
stopwatch.split("fk-check")
stopwatch.stop(TAG)
}
}

View File

@@ -24,7 +24,6 @@ class GroupRecord(
val avatarId: Long,
val avatarKey: ByteArray?,
val avatarContentType: String?,
val relay: String?,
val isActive: Boolean,
val avatarDigest: ByteArray?,
val isMms: Boolean,

View File

@@ -59,7 +59,7 @@ final class GroupManagerV1 {
if (groupId.isV1()) {
GroupId.V1 groupIdV1 = groupId.requireV1();
groupDatabase.create(groupIdV1, name, memberIds, null, null);
groupDatabase.create(groupIdV1, name, memberIds, null);
try {
AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);

View File

@@ -70,7 +70,6 @@ public final class AvatarGroupsV1DownloadJob extends BaseJob {
long avatarId = record.get().getAvatarId();
String contentType = record.get().getAvatarContentType();
byte[] key = record.get().getAvatarKey();
String relay = record.get().getRelay();
Optional<byte[]> digest = Optional.ofNullable(record.get().getAvatarDigest());
Optional<String> fileName = Optional.empty();