mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-02 23:38:34 +00:00
Update the groups tables to use foreign keys.
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user