-
-
Notifications
You must be signed in to change notification settings - Fork 22
Description
Error log
android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: Stroke.id (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)
at android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java:-2)
at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:709)
at android.database.sqlite.SQLiteSession.execute(SQLiteSession.java:621)
at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:47)
at androidx.sqlite.db.framework.FrameworkSQLiteStatement.execute(FrameworkSQLiteStatement.android.kt:30)
at androidx.sqlite.driver.SupportSQLiteStatement$OtherSQLiteStatement.step(SupportSQLiteStatement.android.kt:588)
at androidx.room.EntityInsertAdapter.insert(EntityInsertAdapter.kt:91)
at com.ethran.notable.data.db.StrokeDao_Impl.create$lambda$1(StrokeDao_Impl.kt:120)
at com.ethran.notable.data.db.StrokeDao_Impl.$r8$lambda$F6YajNQPaF1WozUveMfpQA__9JU(:0)
at com.ethran.notable.data.db.StrokeDao_Impl$$ExternalSyntheticLambda3.invoke(D8$$SyntheticClass:0)
at androidx.room.util.DBUtil__DBUtil_androidKt$performBlocking$1$1$invokeSuspend$$inlined$internalPerform$1$1.invokeSuspend(DBUtil.kt:61)
[...]
at androidx.room.util.DBUtil__DBUtil_androidKt.performBlocking(DBUtil.android.kt:71)
at androidx.room.util.DBUtil.performBlocking(:1)
at com.ethran.notable.data.db.StrokeDao_Impl.create(StrokeDao_Impl.kt:118)
at com.ethran.notable.editor.PageView.saveStrokesToPersistLayer(PageView.kt:315)
at com.ethran.notable.editor.PageView.addStrokes(PageView.kt:295)
at com.ethran.notable.editor.state.History.treatOperation(history.kt:103)
at com.ethran.notable.editor.state.History.undoRedo(history.kt:146)
at com.ethran.notable.editor.state.History.access$undoRedo(history.kt:40)
at com.ethran.notable.editor.state.History$1$1.emit(history.kt:70)
at com.ethran.notable.editor.state.History$1$1.emit(history.kt:61)
at kotlinx.coroutines.flow.SharedFlowImpl.collect$suspendImpl(SharedFlow.kt:392)
at kotlinx.coroutines.flow.SharedFlowImpl$collect$1.invokeSuspend(:14)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
[...]
Possible cause
The log shows a UNIQUE constraint violation on the primary key of the Stroke table (Stroke.id). The code attempted to insert a stroke record with an id value that already exists in the database. This can happen if the code tries to insert a duplicate stroke, or if the mechanism to generate unique stroke IDs failed or produced a collision.
No confirmed reproduction case is known. Only the error log and code context are available.
Referenced code:
Stroke entity (primary key definition):
notable/app/src/main/java/com/ethran/notable/data/db/Stroke.kt
Lines 10 to 98 in e6c2af5
| @kotlinx.serialization.Serializable | |
| data class StrokePoint( | |
| val x: Float, // with scroll | |
| var y: Float, // with scroll | |
| val pressure: Float? = null, // relative pressure values 1 to 4096, usually whole number | |
| val tiltX: Int? = null, // tilt values in degrees, -90 to 90 | |
| val tiltY: Int? = null, | |
| val dt: UShort? = null, // delta time in milliseconds, from first point in stroke, not used yet. | |
| @SerialName("timestamp") private val legacyTimestamp: Long? = null, | |
| @SerialName("size") private val legacySize: Float? = null, | |
| ) | |
| @Entity( | |
| foreignKeys = [ForeignKey( | |
| entity = Page::class, | |
| parentColumns = arrayOf("id"), | |
| childColumns = arrayOf("pageId"), | |
| onDelete = ForeignKey.CASCADE | |
| )] | |
| ) | |
| data class Stroke( | |
| @PrimaryKey | |
| val id: String = UUID.randomUUID().toString(), | |
| val size: Float, | |
| val pen: Pen, | |
| @ColumnInfo(defaultValue = "0xFF000000") | |
| val color: Int = 0xFF000000.toInt(), | |
| @ColumnInfo(defaultValue = "4096") | |
| val maxPressure: Int = 4096, // might be useful for synchronization between devices | |
| var top: Float, | |
| var bottom: Float, | |
| var left: Float, | |
| var right: Float, | |
| val points: List<StrokePoint>, | |
| @ColumnInfo(index = true) | |
| val pageId: String, | |
| val createdAt: Date = Date(), | |
| val updatedAt: Date = Date() | |
| ) | |
| // DAO | |
| @Dao | |
| interface StrokeDao { | |
| @Insert | |
| fun create(stroke: Stroke): Long | |
| @Insert | |
| fun create(strokes: List<Stroke>) | |
| @Update | |
| fun update(stroke: Stroke) | |
| @Query("DELETE FROM stroke WHERE id IN (:ids)") | |
| fun deleteAll(ids: List<String>) | |
| @Transaction | |
| @Query("SELECT * FROM stroke WHERE id =:strokeId") | |
| fun getById(strokeId: String): Stroke | |
| } | |
| class StrokeRepository(context: Context) { | |
| var db = AppDatabase.getDatabase(context).strokeDao() | |
| fun create(stroke: Stroke): Long { | |
| return db.create(stroke) | |
| } | |
| fun create(strokes: List<Stroke>) { | |
| return db.create(strokes) | |
| } | |
| fun update(stroke: Stroke) { | |
| return db.update(stroke) | |
| } | |
| fun deleteAll(ids: List<String>) { | |
| return db.deleteAll(ids) | |
| } | |
| fun getStrokeWithPointsById(strokeId: String): Stroke { | |
| return db.getById(strokeId) | |
| } | |
| } |
notable/app/src/main/java/com/ethran/notable/editor/PageView.kt
Lines 332 to 334 in e6c2af5
| private fun saveStrokesToPersistLayer(strokes: List<Stroke>) { | |
| dbStrokes.create(strokes) | |
| } |
-
PageView methods (call sites in the stack trace):
- addStrokes (calls saveStrokesToPersistLayer):
} catch (_: CancellationException) { - saveStrokesToPersistLayer (delegates to DAO insert):
- addStrokes (calls saveStrokesToPersistLayer):
-
History functions referenced in the stack trace:
- treatOperation (invokes pageModel.addStrokes / removeStrokes):
notable/app/src/main/java/com/ethran/notable/editor/state/history.kt
Lines 60 to 110 in e6c2af5
duration = 3000, ) ) } } is HistoryBusActions.RegisterHistoryOperationBlock -> { addOperationsToHistory(actions.operationBlock) } } } fun cleanHistory() { undoList.clear() redoList.clear() } private fun treatOperation(operation: Operation): Pair<Operation, Rect> { when (operation) { is Operation.AddStroke -> { pageModel.addStrokes(operation.strokes) return Operation.DeleteStroke(strokeIds = operation.strokes.map { it.id }) to strokeBounds( operation.strokes ) } is Operation.DeleteStroke -> { val strokes = pageModel.getStrokes(operation.strokeIds).filterNotNull() pageModel.removeStrokes(operation.strokeIds) return Operation.AddStroke(strokes = strokes) to strokeBounds(strokes) } is Operation.AddImage -> { pageModel.addImage(operation.images) return Operation.DeleteImage(imageIds = operation.images.map { it.id }) to imageBoundsInt( operation.images ) } is Operation.DeleteImage -> { val images = pageModel.getImages(operation.imageIds).filterNotNull() pageModel.removeImages(operation.imageIds) return Operation.AddImage(images = images) to imageBoundsInt(images) } } } private fun undoRedo(type: UndoRedoType): Rect? { val originList = - undoRedo (moves across history and calls treatOperation internally):
https://github.com/Ethran/notable/blob/e6c2af53eee3399ae4b203f2baa8547982fb8019/app/src/main/java/com/ethran/notable/editor/state/history.kt#L110-L200
- treatOperation (invokes pageModel.addStrokes / removeStrokes):