Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constraint foreign key exception when having conflicting operations inside one transaction #9

Closed
antailyaqwer opened this issue Apr 1, 2024 · 5 comments · Fixed by #13
Assignees
Labels
enhancement New feature or request

Comments

@antailyaqwer
Copy link
Contributor

Problem

If we have several tables with foreign key relations, then inside one transaction do some operations influences on each other and get exception android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY).

Unlikely, in Room library the same tables and relations such transaction is considered valid.

Environment: kabin: 0.1.0-alpha04;
SQLite version (according to documentation): 3.39 (API 34).

Example

We have 2 tables and a dao.

@Entity
data class UserEntity(
    @PrimaryKey
    val id: Int,
    val name: String
)

@Entity(
    primaryKeys = ["id", "userId"],
    foreignKeys = [
        ForeignKey(
            entity = UserEntity::class,
            parentColumns = ["id"],
            childColumns = ["userId"],
            onDelete = ForeignKey.Action.CASCADE,
            onUpdate = ForeignKey.Action.CASCADE
        )
    ],
)
data class UserAvatarEntity(
    val userId: Int,
    val id: String,
    val url: String
)

@Dao
interface IUserDaoSpecial {

    @Transaction
    suspend fun doSomeOperations() {
        val userEntity = UserEntitySpecial(
            id = 0,
            name = "John"
        )
        val userAvatarEntity = UserAvatarEntitySpec(
            userId = 0,
            id = "d64c2cd8-fd5f-4f06-844e-b4006b9464bb",
            url = "https://icon.jpg"
        )

        insertAvatar(userAvatarEntity)
        insertUser(userEntity)
    }

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: UserEntitySpecial)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAvatar(avatarEntitySpec: UserAvatarEntitySpec)
}

If we will invoke dao method, we will catch an exception.

android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)

Expected behavior: successful operations inside one transaction.
I guess this error is connected with deferred flag in ForeignKey.

@tamimattafi tamimattafi self-assigned this Apr 1, 2024
@tamimattafi tamimattafi added the enhancement New feature or request label Apr 1, 2024
@tamimattafi
Copy link
Owner

tamimattafi commented Apr 18, 2024

According to this answer and many others, we need to make these changes:

  1. Make all constraints DEFERRED inside one transaction
  2. Adjust insert order for compounds and lists and call notifyQuery only after all SQL operations
  3. Wrap database operations such as create, migrate, clear and drop inside a transaction and turn off foreign keys using PRAGMA

@tamimattafi
Copy link
Owner

tamimattafi commented Apr 18, 2024

  1. Make all constraints DEFERRED inside one transaction

In order to make the first point work correctly, all foreign keys should be created as DEFERRABLE INITIALLY IMMEDIATE by default, and DEFERRABLE INITIALLY DEFERRED

Changed here: 022636f

This means that projects adopting these changes, should update their database version

@tamimattafi
Copy link
Owner

tamimattafi commented Apr 18, 2024

  1. Make all constraints DEFERRED inside one transaction

Some deeper research showed that room uses PRAGMA syntax to make foreign keys DEFERRED inside a transaction, which seems like a better option than SET CONSTRAINTS ALL DEFERRED since it's rolled back once the transaction ends, and you don't have to do it manually.

More info: https://sqlite.org/pragma.html#pragma_defer_foreign_keys

@tamimattafi
Copy link
Owner

tamimattafi commented Apr 18, 2024

1- Make all constraints DEFERRED inside one transaction
3- Wrap database operations such as create, migrate, clear and drop inside a transaction and turn off foreign keys using PRAGMA

In order to achieve this type of control over foreign keys, KabinDatabaseConfiguration had to be changed to include more flags. configuration is now passed to Database, Scheme and Dao, which means this commit might introduce breaking changes for some projects.

Changed here: 132af15

@tamimattafi
Copy link
Owner

  1. Adjust insert order for compounds and lists and call notifyQuery only after all SQL operations

Further research showed that notifyQuery inside one transaction delays triggers until the transaction is successful, therefore, these changes were postponed until future refactoring of the compiler and processor modules.

tamimattafi added a commit that referenced this issue Apr 18, 2024
fix: foreign key behaviour inside a transaction

Close #9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants