1 /* <lambda>null2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.room.processor 18 19 import androidx.room.ext.getAsBoolean 20 import androidx.room.ext.getAsInt 21 import androidx.room.ext.getAsString 22 import androidx.room.ext.getAsStringList 23 import androidx.room.ext.toType 24 import androidx.room.parser.SQLTypeAffinity 25 import androidx.room.parser.SqlParser 26 import androidx.room.processor.ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY 27 import androidx.room.processor.ProcessorErrors.RELATION_IN_ENTITY 28 import androidx.room.processor.cache.Cache 29 import androidx.room.vo.EmbeddedField 30 import androidx.room.vo.Entity 31 import androidx.room.vo.Field 32 import androidx.room.vo.ForeignKey 33 import androidx.room.vo.ForeignKeyAction 34 import androidx.room.vo.Index 35 import androidx.room.vo.Pojo 36 import androidx.room.vo.PrimaryKey 37 import androidx.room.vo.Warning 38 import com.google.auto.common.AnnotationMirrors 39 import com.google.auto.common.AnnotationMirrors.getAnnotationValue 40 import com.google.auto.common.MoreElements 41 import com.google.auto.common.MoreTypes 42 import javax.lang.model.element.AnnotationMirror 43 import javax.lang.model.element.AnnotationValue 44 import javax.lang.model.element.Name 45 import javax.lang.model.element.TypeElement 46 import javax.lang.model.type.TypeKind 47 import javax.lang.model.type.TypeMirror 48 import javax.lang.model.util.SimpleAnnotationValueVisitor6 49 50 class EntityProcessor(baseContext: Context, 51 val element: TypeElement, 52 private val referenceStack: LinkedHashSet<Name> = LinkedHashSet()) { 53 val context = baseContext.fork(element) 54 55 fun process(): Entity { 56 return context.cache.entities.get(Cache.EntityKey(element), { 57 doProcess() 58 }) 59 } 60 private fun doProcess(): Entity { 61 context.checker.hasAnnotation(element, androidx.room.Entity::class, 62 ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY) 63 val pojo = PojoProcessor( 64 baseContext = context, 65 element = element, 66 bindingScope = FieldProcessor.BindingScope.TWO_WAY, 67 parent = null, 68 referenceStack = referenceStack).process() 69 context.checker.check(pojo.relations.isEmpty(), element, RELATION_IN_ENTITY) 70 val annotation = MoreElements.getAnnotationMirror(element, 71 androidx.room.Entity::class.java).orNull() 72 val tableName: String 73 val entityIndices: List<IndexInput> 74 val foreignKeyInputs: List<ForeignKeyInput> 75 val inheritSuperIndices: Boolean 76 if (annotation != null) { 77 tableName = extractTableName(element, annotation) 78 entityIndices = extractIndices(annotation, tableName) 79 inheritSuperIndices = AnnotationMirrors 80 .getAnnotationValue(annotation, "inheritSuperIndices").getAsBoolean(false) 81 foreignKeyInputs = extractForeignKeys(annotation) 82 } else { 83 tableName = element.simpleName.toString() 84 foreignKeyInputs = emptyList() 85 entityIndices = emptyList() 86 inheritSuperIndices = false 87 } 88 context.checker.notBlank(tableName, element, 89 ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY) 90 91 val fieldIndices = pojo.fields 92 .filter { it.indexed }.mapNotNull { 93 if (it.parent != null) { 94 it.indexed = false 95 context.logger.w(Warning.INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED, it.element, 96 ProcessorErrors.droppedEmbeddedFieldIndex( 97 it.getPath(), element.qualifiedName.toString())) 98 null 99 } else if (it.element.enclosingElement != element && !inheritSuperIndices) { 100 it.indexed = false 101 context.logger.w(Warning.INDEX_FROM_PARENT_FIELD_IS_DROPPED, 102 ProcessorErrors.droppedSuperClassFieldIndex( 103 it.columnName, element.toString(), 104 it.element.enclosingElement.toString() 105 )) 106 null 107 } else { 108 IndexInput( 109 name = createIndexName(listOf(it.columnName), tableName), 110 unique = false, 111 columnNames = listOf(it.columnName) 112 ) 113 } 114 } 115 val superIndices = loadSuperIndices(element.superclass, tableName, inheritSuperIndices) 116 val indexInputs = entityIndices + fieldIndices + superIndices 117 val indices = validateAndCreateIndices(indexInputs, pojo) 118 119 val primaryKey = findAndValidatePrimaryKey(pojo.fields, pojo.embeddedFields) 120 val affinity = primaryKey.fields.firstOrNull()?.affinity ?: SQLTypeAffinity.TEXT 121 context.checker.check( 122 !primaryKey.autoGenerateId || affinity == SQLTypeAffinity.INTEGER, 123 primaryKey.fields.firstOrNull()?.element ?: element, 124 ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT 125 ) 126 127 val entityForeignKeys = validateAndCreateForeignKeyReferences(foreignKeyInputs, pojo) 128 checkIndicesForForeignKeys(entityForeignKeys, primaryKey, indices) 129 130 context.checker.check(SqlParser.isValidIdentifier(tableName), element, 131 ProcessorErrors.INVALID_TABLE_NAME) 132 pojo.fields.forEach { 133 context.checker.check(SqlParser.isValidIdentifier(it.columnName), it.element, 134 ProcessorErrors.INVALID_COLUMN_NAME) 135 } 136 137 val entity = Entity(element = element, 138 tableName = tableName, 139 type = pojo.type, 140 fields = pojo.fields, 141 embeddedFields = pojo.embeddedFields, 142 indices = indices, 143 primaryKey = primaryKey, 144 foreignKeys = entityForeignKeys, 145 constructor = pojo.constructor) 146 147 return entity 148 } 149 150 private fun checkIndicesForForeignKeys(entityForeignKeys: List<ForeignKey>, 151 primaryKey: PrimaryKey, 152 indices: List<Index>) { 153 fun covers(columnNames: List<String>, fields: List<Field>): Boolean = 154 fields.size >= columnNames.size && columnNames.withIndex().all { 155 fields[it.index].columnName == it.value 156 } 157 158 entityForeignKeys.forEach { fKey -> 159 val columnNames = fKey.childFields.map { it.columnName } 160 val exists = covers(columnNames, primaryKey.fields) || indices.any { index -> 161 covers(columnNames, index.fields) 162 } 163 if (!exists) { 164 if (columnNames.size == 1) { 165 context.logger.w(Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element, 166 ProcessorErrors.foreignKeyMissingIndexInChildColumn(columnNames[0])) 167 } else { 168 context.logger.w(Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element, 169 ProcessorErrors.foreignKeyMissingIndexInChildColumns(columnNames)) 170 } 171 } 172 } 173 } 174 175 /** 176 * Does a validation on foreign keys except the parent table's columns. 177 */ 178 private fun validateAndCreateForeignKeyReferences(foreignKeyInputs: List<ForeignKeyInput>, 179 pojo: Pojo): List<ForeignKey> { 180 return foreignKeyInputs.map { 181 if (it.onUpdate == null) { 182 context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION) 183 return@map null 184 } 185 if (it.onDelete == null) { 186 context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION) 187 return@map null 188 } 189 if (it.childColumns.isEmpty()) { 190 context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST) 191 return@map null 192 } 193 if (it.parentColumns.isEmpty()) { 194 context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST) 195 return@map null 196 } 197 if (it.childColumns.size != it.parentColumns.size) { 198 context.logger.e(element, ProcessorErrors.foreignKeyColumnNumberMismatch( 199 it.childColumns, it.parentColumns 200 )) 201 return@map null 202 } 203 val parentElement = try { 204 MoreTypes.asElement(it.parent) as TypeElement 205 } catch (noClass: IllegalArgumentException) { 206 context.logger.e(element, ProcessorErrors.FOREIGN_KEY_CANNOT_FIND_PARENT) 207 return@map null 208 } 209 val parentAnnotation = MoreElements.getAnnotationMirror(parentElement, 210 androidx.room.Entity::class.java).orNull() 211 if (parentAnnotation == null) { 212 context.logger.e(element, 213 ProcessorErrors.foreignKeyNotAnEntity(parentElement.toString())) 214 return@map null 215 } 216 val tableName = extractTableName(parentElement, parentAnnotation) 217 val fields = it.childColumns.mapNotNull { columnName -> 218 val field = pojo.fields.find { it.columnName == columnName } 219 if (field == null) { 220 context.logger.e(pojo.element, 221 ProcessorErrors.foreignKeyChildColumnDoesNotExist(columnName, 222 pojo.fields.map { it.columnName })) 223 } 224 field 225 } 226 if (fields.size != it.childColumns.size) { 227 return@map null 228 } 229 ForeignKey( 230 parentTable = tableName, 231 childFields = fields, 232 parentColumns = it.parentColumns, 233 onDelete = it.onDelete, 234 onUpdate = it.onUpdate, 235 deferred = it.deferred 236 ) 237 }.filterNotNull() 238 } 239 240 private fun findAndValidatePrimaryKey( 241 fields: List<Field>, embeddedFields: List<EmbeddedField>): PrimaryKey { 242 val candidates = collectPrimaryKeysFromEntityAnnotations(element, fields) + 243 collectPrimaryKeysFromPrimaryKeyAnnotations(fields) + 244 collectPrimaryKeysFromEmbeddedFields(embeddedFields) 245 246 context.checker.check(candidates.isNotEmpty(), element, ProcessorErrors.MISSING_PRIMARY_KEY) 247 248 // 1. If a key is not autogenerated, but is Primary key or is part of Primary key we 249 // force the @NonNull annotation. If the key is a single Primary Key, Integer or Long, we 250 // don't force the @NonNull annotation since SQLite will automatically generate IDs. 251 // 2. If a key is autogenerate, we generate NOT NULL in table spec, but we don't require 252 // @NonNull annotation on the field itself. 253 candidates.filter { candidate -> !candidate.autoGenerateId } 254 .map { candidate -> 255 candidate.fields.map { field -> 256 if (candidate.fields.size > 1 || 257 (candidate.fields.size == 1 258 && field.affinity != SQLTypeAffinity.INTEGER)) { 259 context.checker.check(field.nonNull, field.element, 260 ProcessorErrors.primaryKeyNull(field.getPath())) 261 // Validate parents for nullability 262 var parent = field.parent 263 while (parent != null) { 264 val parentField = parent.field 265 context.checker.check(parentField.nonNull, 266 parentField.element, 267 ProcessorErrors.primaryKeyNull(parentField.getPath())) 268 parent = parentField.parent 269 } 270 } 271 } 272 } 273 274 if (candidates.size == 1) { 275 // easy :) 276 return candidates.first() 277 } 278 279 return choosePrimaryKey(candidates, element) 280 } 281 282 /** 283 * Check fields for @PrimaryKey. 284 */ 285 private fun collectPrimaryKeysFromPrimaryKeyAnnotations(fields: List<Field>): List<PrimaryKey> { 286 return fields.mapNotNull { field -> 287 MoreElements.getAnnotationMirror(field.element, 288 androidx.room.PrimaryKey::class.java).orNull()?.let { 289 if (field.parent != null) { 290 // the field in the entity that contains this error. 291 val grandParentField = field.parent.mRootParent.field.element 292 // bound for entity. 293 context.fork(grandParentField).logger.w( 294 Warning.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED, 295 grandParentField, 296 ProcessorErrors.embeddedPrimaryKeyIsDropped( 297 element.qualifiedName.toString(), field.name)) 298 null 299 } else { 300 PrimaryKey(declaredIn = field.element.enclosingElement, 301 fields = listOf(field), 302 autoGenerateId = AnnotationMirrors 303 .getAnnotationValue(it, "autoGenerate") 304 .getAsBoolean(false)) 305 } 306 } 307 } 308 } 309 310 /** 311 * Check classes for @Entity(primaryKeys = ?). 312 */ 313 private fun collectPrimaryKeysFromEntityAnnotations( 314 typeElement: TypeElement, availableFields: List<Field>): List<PrimaryKey> { 315 val myPkeys = MoreElements.getAnnotationMirror(typeElement, 316 androidx.room.Entity::class.java).orNull()?.let { 317 val primaryKeyColumns = AnnotationMirrors.getAnnotationValue(it, "primaryKeys") 318 .getAsStringList() 319 if (primaryKeyColumns.isEmpty()) { 320 emptyList() 321 } else { 322 val fields = primaryKeyColumns.mapNotNull { pKeyColumnName -> 323 val field = availableFields.firstOrNull { it.columnName == pKeyColumnName } 324 context.checker.check(field != null, typeElement, 325 ProcessorErrors.primaryKeyColumnDoesNotExist(pKeyColumnName, 326 availableFields.map { it.columnName })) 327 field 328 } 329 listOf(PrimaryKey(declaredIn = typeElement, 330 fields = fields, 331 autoGenerateId = false)) 332 } 333 } ?: emptyList() 334 // checks supers. 335 val mySuper = typeElement.superclass 336 val superPKeys = if (mySuper != null && mySuper.kind != TypeKind.NONE) { 337 // my super cannot see my fields so remove them. 338 val remainingFields = availableFields.filterNot { 339 it.element.enclosingElement == typeElement 340 } 341 collectPrimaryKeysFromEntityAnnotations( 342 MoreTypes.asTypeElement(mySuper), remainingFields) 343 } else { 344 emptyList() 345 } 346 return superPKeys + myPkeys 347 } 348 349 private fun collectPrimaryKeysFromEmbeddedFields( 350 embeddedFields: List<EmbeddedField>): List<PrimaryKey> { 351 return embeddedFields.mapNotNull { embeddedField -> 352 MoreElements.getAnnotationMirror(embeddedField.field.element, 353 androidx.room.PrimaryKey::class.java).orNull()?.let { 354 val autoGenerate = AnnotationMirrors 355 .getAnnotationValue(it, "autoGenerate").getAsBoolean(false) 356 context.checker.check(!autoGenerate || embeddedField.pojo.fields.size == 1, 357 embeddedField.field.element, 358 ProcessorErrors.AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_FIELDS) 359 PrimaryKey(declaredIn = embeddedField.field.element.enclosingElement, 360 fields = embeddedField.pojo.fields, 361 autoGenerateId = autoGenerate) 362 } 363 } 364 } 365 366 // start from my element and check if anywhere in the list we can find the only well defined 367 // pkey, if so, use it. 368 private fun choosePrimaryKey( 369 candidates: List<PrimaryKey>, typeElement: TypeElement): PrimaryKey { 370 // If 1 of these primary keys is declared in this class, then it is the winner. Just print 371 // a note for the others. 372 // If 0 is declared, check the parent. 373 // If more than 1 primary key is declared in this class, it is an error. 374 val myPKeys = candidates.filter { candidate -> 375 candidate.declaredIn == typeElement 376 } 377 return if (myPKeys.size == 1) { 378 // just note, this is not worth an error or warning 379 (candidates - myPKeys).forEach { 380 context.logger.d(element, 381 "${it.toHumanReadableString()} is" + 382 " overridden by ${myPKeys.first().toHumanReadableString()}") 383 } 384 myPKeys.first() 385 } else if (myPKeys.isEmpty()) { 386 // i have not declared anything, delegate to super 387 val mySuper = typeElement.superclass 388 if (mySuper != null && mySuper.kind != TypeKind.NONE) { 389 return choosePrimaryKey(candidates, MoreTypes.asTypeElement(mySuper)) 390 } 391 PrimaryKey.MISSING 392 } else { 393 context.logger.e(element, ProcessorErrors.multiplePrimaryKeyAnnotations( 394 myPKeys.map(PrimaryKey::toHumanReadableString))) 395 PrimaryKey.MISSING 396 } 397 } 398 399 private fun validateAndCreateIndices( 400 inputs: List<IndexInput>, pojo: Pojo): List<Index> { 401 // check for columns 402 val indices = inputs.mapNotNull { input -> 403 context.checker.check(input.columnNames.isNotEmpty(), element, 404 INDEX_COLUMNS_CANNOT_BE_EMPTY) 405 val fields = input.columnNames.mapNotNull { columnName -> 406 val field = pojo.fields.firstOrNull { 407 it.columnName == columnName 408 } 409 context.checker.check(field != null, element, 410 ProcessorErrors.indexColumnDoesNotExist( 411 columnName, pojo.fields.map { it.columnName } 412 )) 413 field 414 } 415 if (fields.isEmpty()) { 416 null 417 } else { 418 Index(name = input.name, unique = input.unique, fields = fields) 419 } 420 } 421 422 // check for duplicate indices 423 indices 424 .groupBy { it.name } 425 .filter { it.value.size > 1 } 426 .forEach { 427 context.logger.e(element, ProcessorErrors.duplicateIndexInEntity(it.key)) 428 } 429 430 // see if any embedded field is an entity with indices, if so, report a warning 431 pojo.embeddedFields.forEach { embedded -> 432 val embeddedElement = embedded.pojo.element 433 val subEntityAnnotation = MoreElements.getAnnotationMirror(embeddedElement, 434 androidx.room.Entity::class.java).orNull() 435 subEntityAnnotation?.let { 436 val subIndices = extractIndices(subEntityAnnotation, "") 437 if (subIndices.isNotEmpty()) { 438 context.logger.w(Warning.INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED, 439 embedded.field.element, ProcessorErrors.droppedEmbeddedIndex( 440 entityName = embedded.pojo.typeName.toString(), 441 fieldPath = embedded.field.getPath(), 442 grandParent = element.qualifiedName.toString())) 443 } 444 } 445 } 446 return indices 447 } 448 449 // check if parent is an Entity, if so, report its annotation indices 450 private fun loadSuperIndices( 451 typeMirror: TypeMirror?, tableName: String, inherit: Boolean): List<IndexInput> { 452 if (typeMirror == null || typeMirror.kind == TypeKind.NONE) { 453 return emptyList() 454 } 455 val parentElement = MoreTypes.asTypeElement(typeMirror) 456 val myIndices = MoreElements.getAnnotationMirror(parentElement, 457 androidx.room.Entity::class.java).orNull()?.let { annotation -> 458 val indices = extractIndices(annotation, tableName = "super") 459 if (indices.isEmpty()) { 460 emptyList() 461 } else if (inherit) { 462 // rename them 463 indices.map { 464 IndexInput( 465 name = createIndexName(it.columnNames, tableName), 466 unique = it.unique, 467 columnNames = it.columnNames) 468 } 469 } else { 470 context.logger.w(Warning.INDEX_FROM_PARENT_IS_DROPPED, 471 parentElement, 472 ProcessorErrors.droppedSuperClassIndex( 473 childEntity = element.qualifiedName.toString(), 474 superEntity = parentElement.qualifiedName.toString())) 475 emptyList() 476 } 477 } ?: emptyList() 478 return myIndices + loadSuperIndices(parentElement.superclass, tableName, inherit) 479 } 480 481 companion object { 482 fun extractTableName(element: TypeElement, annotation: AnnotationMirror): String { 483 val annotationValue = AnnotationMirrors 484 .getAnnotationValue(annotation, "tableName").value.toString() 485 return if (annotationValue == "") { 486 element.simpleName.toString() 487 } else { 488 annotationValue 489 } 490 } 491 492 private fun extractIndices( 493 annotation: AnnotationMirror, tableName: String): List<IndexInput> { 494 val arrayOfIndexAnnotations = AnnotationMirrors.getAnnotationValue(annotation, 495 "indices") 496 return INDEX_LIST_VISITOR.visit(arrayOfIndexAnnotations, tableName) 497 } 498 499 private val INDEX_LIST_VISITOR = object 500 : SimpleAnnotationValueVisitor6<List<IndexInput>, String>() { 501 override fun visitArray( 502 values: MutableList<out AnnotationValue>?, 503 tableName: String 504 ): List<IndexInput> { 505 return values?.mapNotNull { 506 INDEX_VISITOR.visit(it, tableName) 507 } ?: emptyList() 508 } 509 } 510 511 private val INDEX_VISITOR = object : SimpleAnnotationValueVisitor6<IndexInput?, String>() { 512 override fun visitAnnotation(a: AnnotationMirror?, tableName: String): IndexInput? { 513 val fieldInput = getAnnotationValue(a, "value").getAsStringList() 514 val unique = getAnnotationValue(a, "unique").getAsBoolean(false) 515 val nameValue = getAnnotationValue(a, "name") 516 .getAsString("") 517 val name = if (nameValue == null || nameValue == "") { 518 createIndexName(fieldInput, tableName) 519 } else { 520 nameValue 521 } 522 return IndexInput(name, unique, fieldInput) 523 } 524 } 525 526 private fun createIndexName(columnNames: List<String>, tableName: String): String { 527 return Index.DEFAULT_PREFIX + tableName + "_" + columnNames.joinToString("_") 528 } 529 530 private fun extractForeignKeys(annotation: AnnotationMirror): List<ForeignKeyInput> { 531 val arrayOfForeignKeyAnnotations = getAnnotationValue(annotation, "foreignKeys") 532 return FOREIGN_KEY_LIST_VISITOR.visit(arrayOfForeignKeyAnnotations) 533 } 534 535 private val FOREIGN_KEY_LIST_VISITOR = object 536 : SimpleAnnotationValueVisitor6<List<ForeignKeyInput>, Void?>() { 537 override fun visitArray( 538 values: MutableList<out AnnotationValue>?, 539 void: Void? 540 ): List<ForeignKeyInput> { 541 return values?.mapNotNull { 542 FOREIGN_KEY_VISITOR.visit(it) 543 } ?: emptyList() 544 } 545 } 546 547 private val FOREIGN_KEY_VISITOR = object : SimpleAnnotationValueVisitor6<ForeignKeyInput?, 548 Void?>() { 549 override fun visitAnnotation(a: AnnotationMirror?, void: Void?): ForeignKeyInput? { 550 val entityClass = try { 551 getAnnotationValue(a, "entity").toType() 552 } catch (notPresent: TypeNotPresentException) { 553 return null 554 } 555 val parentColumns = getAnnotationValue(a, "parentColumns").getAsStringList() 556 val childColumns = getAnnotationValue(a, "childColumns").getAsStringList() 557 val onDeleteInput = getAnnotationValue(a, "onDelete").getAsInt() 558 val onUpdateInput = getAnnotationValue(a, "onUpdate").getAsInt() 559 val deferred = getAnnotationValue(a, "deferred").getAsBoolean(true) 560 val onDelete = ForeignKeyAction.fromAnnotationValue(onDeleteInput) 561 val onUpdate = ForeignKeyAction.fromAnnotationValue(onUpdateInput) 562 return ForeignKeyInput( 563 parent = entityClass, 564 parentColumns = parentColumns, 565 childColumns = childColumns, 566 onDelete = onDelete, 567 onUpdate = onUpdate, 568 deferred = deferred) 569 } 570 } 571 } 572 573 /** 574 * processed Index annotation output 575 */ 576 data class IndexInput(val name: String, val unique: Boolean, val columnNames: List<String>) 577 578 /** 579 * ForeignKey, before it is processed in the context of a database. 580 */ 581 data class ForeignKeyInput( 582 val parent: TypeMirror, 583 val parentColumns: List<String>, 584 val childColumns: List<String>, 585 val onDelete: ForeignKeyAction?, 586 val onUpdate: ForeignKeyAction?, 587 val deferred: Boolean) 588 } 589