com.zeoflow.depot.processor.TableEntityProcessor.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of depot-compiler Show documentation
Show all versions of depot-compiler Show documentation
The Depot persistence library provides an abstraction layer over SQLite to allow for more robust database access while using the full power of SQLite.
The newest version!
/*
* Copyright (C) 2021 ZeoFlow SRL
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zeoflow.depot.processor
import com.zeoflow.depot.parser.SQLTypeAffinity
import com.zeoflow.depot.parser.SqlParser
import com.zeoflow.depot.compiler.processing.XType
import com.zeoflow.depot.compiler.processing.XTypeElement
import com.zeoflow.depot.ext.isNotNone
import com.zeoflow.depot.processor.EntityProcessor.Companion.createIndexName
import com.zeoflow.depot.processor.EntityProcessor.Companion.extractForeignKeys
import com.zeoflow.depot.processor.EntityProcessor.Companion.extractIndices
import com.zeoflow.depot.processor.EntityProcessor.Companion.extractTableName
import com.zeoflow.depot.processor.ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY
import com.zeoflow.depot.processor.ProcessorErrors.RELATION_IN_ENTITY
import com.zeoflow.depot.processor.cache.Cache
import com.zeoflow.depot.vo.EmbeddedField
import com.zeoflow.depot.vo.Entity
import com.zeoflow.depot.vo.Field
import com.zeoflow.depot.vo.Fields
import com.zeoflow.depot.vo.ForeignKey
import com.zeoflow.depot.vo.Index
import com.zeoflow.depot.vo.Pojo
import com.zeoflow.depot.vo.PrimaryKey
import com.zeoflow.depot.vo.Warning
import com.zeoflow.depot.vo.columnNames
import com.zeoflow.depot.vo.findFieldByColumnName
class TableEntityProcessor internal constructor(
baseContext: Context,
val element: XTypeElement,
private val referenceStack: LinkedHashSet = LinkedHashSet()
) : EntityProcessor {
val context = baseContext.fork(element)
override fun process(): Entity {
return context.cache.entities.get(Cache.EntityKey(element)) {
doProcess()
}
}
private fun doProcess(): Entity {
context.checker.hasAnnotation(
element, com.zeoflow.depot.Entity::class,
ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
)
val annotationBox = element.getAnnotation(com.zeoflow.depot.Entity::class)
val tableName: String
val entityIndices: List
val foreignKeyInputs: List
val inheritSuperIndices: Boolean
if (annotationBox != null) {
tableName = extractTableName(element, annotationBox.value)
entityIndices = extractIndices(annotationBox, tableName)
inheritSuperIndices = annotationBox.value.inheritSuperIndices
foreignKeyInputs = extractForeignKeys(annotationBox)
} else {
tableName = element.name
foreignKeyInputs = emptyList()
entityIndices = emptyList()
inheritSuperIndices = false
}
context.checker.notBlank(
tableName, element,
ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY
)
context.checker.check(
!tableName.startsWith("sqlite_", true), element,
ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_START_WITH_SQLITE
)
val pojo = PojoProcessor.createFor(
context = context,
element = element,
bindingScope = FieldProcessor.BindingScope.TWO_WAY,
parent = null,
referenceStack = referenceStack
).process()
context.checker.check(pojo.relations.isEmpty(), element, RELATION_IN_ENTITY)
val fieldIndices = pojo.fields
.filter { it.indexed }.mapNotNull {
if (it.parent != null) {
it.indexed = false
context.logger.w(
Warning.INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED, it.element,
ProcessorErrors.droppedEmbeddedFieldIndex(
it.getPath(), element.qualifiedName
)
)
null
} else if (it.element.enclosingElement != element && !inheritSuperIndices) {
it.indexed = false
context.logger.w(
Warning.INDEX_FROM_PARENT_FIELD_IS_DROPPED,
ProcessorErrors.droppedSuperClassFieldIndex(
it.columnName, element.qualifiedName,
it.element.enclosingElement.className.toString()
)
)
null
} else {
IndexInput(
name = createIndexName(listOf(it.columnName), tableName),
unique = false,
columnNames = listOf(it.columnName)
)
}
}
val superIndices = loadSuperIndices(element.superType, tableName, inheritSuperIndices)
val indexInputs = entityIndices + fieldIndices + superIndices
val indices = validateAndCreateIndices(indexInputs, pojo)
val primaryKey = findAndValidatePrimaryKey(pojo.fields, pojo.embeddedFields)
val affinity = primaryKey.fields.firstOrNull()?.affinity ?: SQLTypeAffinity.TEXT
context.checker.check(
!primaryKey.autoGenerateId || affinity == SQLTypeAffinity.INTEGER,
primaryKey.fields.firstOrNull()?.element ?: element,
ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT
)
val entityForeignKeys = validateAndCreateForeignKeyReferences(foreignKeyInputs, pojo)
checkIndicesForForeignKeys(entityForeignKeys, primaryKey, indices)
context.checker.check(
SqlParser.isValidIdentifier(tableName), element,
ProcessorErrors.INVALID_TABLE_NAME
)
pojo.fields.forEach {
context.checker.check(
SqlParser.isValidIdentifier(it.columnName), it.element,
ProcessorErrors.INVALID_COLUMN_NAME
)
}
val entity = Entity(
element = element,
tableName = tableName,
type = pojo.type,
fields = pojo.fields,
embeddedFields = pojo.embeddedFields,
indices = indices,
primaryKey = primaryKey,
foreignKeys = entityForeignKeys,
constructor = pojo.constructor,
shadowTableName = null
)
return entity
}
private fun checkIndicesForForeignKeys(
entityForeignKeys: List,
primaryKey: PrimaryKey,
indices: List
) {
fun covers(columnNames: List, fields: List): Boolean =
fields.size >= columnNames.size && columnNames.withIndex().all {
fields[it.index].columnName == it.value
}
entityForeignKeys.forEach { fKey ->
val columnNames = fKey.childFields.map { it.columnName }
val exists = covers(columnNames, primaryKey.fields) || indices.any { index ->
covers(columnNames, index.fields)
}
if (!exists) {
if (columnNames.size == 1) {
context.logger.w(
Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element,
ProcessorErrors.foreignKeyMissingIndexInChildColumn(columnNames[0])
)
} else {
context.logger.w(
Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element,
ProcessorErrors.foreignKeyMissingIndexInChildColumns(columnNames)
)
}
}
}
}
/**
* Does a validation on foreign keys except the parent table's columns.
*/
private fun validateAndCreateForeignKeyReferences(
foreignKeyInputs: List,
pojo: Pojo
): List {
return foreignKeyInputs.map {
if (it.onUpdate == null) {
context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
return@map null
}
if (it.onDelete == null) {
context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
return@map null
}
if (it.childColumns.isEmpty()) {
context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
return@map null
}
if (it.parentColumns.isEmpty()) {
context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
return@map null
}
if (it.childColumns.size != it.parentColumns.size) {
context.logger.e(
element,
ProcessorErrors.foreignKeyColumnNumberMismatch(
it.childColumns, it.parentColumns
)
)
return@map null
}
val parentElement = it.parent.typeElement
if (parentElement == null) {
context.logger.e(element, ProcessorErrors.FOREIGN_KEY_CANNOT_FIND_PARENT)
return@map null
}
val parentAnnotation = parentElement.getAnnotation(com.zeoflow.depot.Entity::class)
if (parentAnnotation == null) {
context.logger.e(
element,
ProcessorErrors.foreignKeyNotAnEntity(parentElement.qualifiedName)
)
return@map null
}
val tableName = extractTableName(parentElement, parentAnnotation.value)
val fields = it.childColumns.mapNotNull { columnName ->
val field = pojo.findFieldByColumnName(columnName)
if (field == null) {
context.logger.e(
pojo.element,
ProcessorErrors.foreignKeyChildColumnDoesNotExist(
columnName,
pojo.columnNames
)
)
}
field
}
if (fields.size != it.childColumns.size) {
return@map null
}
ForeignKey(
parentTable = tableName,
childFields = fields,
parentColumns = it.parentColumns,
onDelete = it.onDelete,
onUpdate = it.onUpdate,
deferred = it.deferred
)
}.filterNotNull()
}
private fun findAndValidatePrimaryKey(
fields: List,
embeddedFields: List
): PrimaryKey {
val candidates = collectPrimaryKeysFromEntityAnnotations(element, fields) +
collectPrimaryKeysFromPrimaryKeyAnnotations(fields) +
collectPrimaryKeysFromEmbeddedFields(embeddedFields)
context.checker.check(candidates.isNotEmpty(), element, ProcessorErrors.MISSING_PRIMARY_KEY)
// 1. If a key is not autogenerated, but is Primary key or is part of Primary key we
// force the @NonNull annotation. If the key is a single Primary Key, Integer or Long, we
// don't force the @NonNull annotation since SQLite will automatically generate IDs.
// 2. If a key is autogenerate, we generate NOT NULL in table spec, but we don't require
// @NonNull annotation on the field itself.
val verifiedFields = mutableSetOf() // track verified fields to not over report
candidates.filterNot { it.autoGenerateId }.forEach { candidate ->
candidate.fields.forEach { field ->
if (candidate.fields.size > 1 ||
(candidate.fields.size == 1 && field.affinity != SQLTypeAffinity.INTEGER)
) {
if (!verifiedFields.contains(field)) {
context.checker.check(
field.nonNull,
field.element,
ProcessorErrors.primaryKeyNull(field.getPath())
)
verifiedFields.add(field)
}
// Validate parents for nullability
var parent = field.parent
while (parent != null) {
val parentField = parent.field
if (!verifiedFields.contains(parentField)) {
context.checker.check(
parentField.nonNull,
parentField.element,
ProcessorErrors.primaryKeyNull(parentField.getPath())
)
verifiedFields.add(parentField)
}
parent = parentField.parent
}
}
}
}
if (candidates.size == 1) {
// easy :)
return candidates.first()
}
return choosePrimaryKey(candidates, element)
}
/**
* Check fields for @PrimaryKey.
*/
private fun collectPrimaryKeysFromPrimaryKeyAnnotations(fields: List): List {
return fields.mapNotNull { field ->
field.element.getAnnotation(com.zeoflow.depot.PrimaryKey::class)?.let {
if (field.parent != null) {
// the field in the entity that contains this error.
val grandParentField = field.parent.mRootParent.field.element
// bound for entity.
context.fork(grandParentField).logger.w(
Warning.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED,
grandParentField,
ProcessorErrors.embeddedPrimaryKeyIsDropped(
element.qualifiedName, field.name
)
)
null
} else {
PrimaryKey(
declaredIn = field.element.enclosingElement,
fields = Fields(field),
autoGenerateId = it.value.autoGenerate
)
}
}
}
}
/**
* Check classes for @Entity(primaryKeys = ?).
*/
private fun collectPrimaryKeysFromEntityAnnotations(
typeElement: XTypeElement,
availableFields: List
): List {
val myPkeys = typeElement.getAnnotation(com.zeoflow.depot.Entity::class)?.let {
val primaryKeyColumns = it.value.primaryKeys
if (primaryKeyColumns.isEmpty()) {
emptyList()
} else {
val fields = primaryKeyColumns.mapNotNull { pKeyColumnName ->
val field = availableFields.firstOrNull { it.columnName == pKeyColumnName }
context.checker.check(
field != null, typeElement,
ProcessorErrors.primaryKeyColumnDoesNotExist(
pKeyColumnName,
availableFields.map { it.columnName }
)
)
field
}
listOf(
PrimaryKey(
declaredIn = typeElement,
fields = Fields(fields),
autoGenerateId = false
)
)
}
} ?: emptyList()
// checks supers.
val mySuper = typeElement.superType
val superPKeys = if (mySuper != null && mySuper.isNotNone()) {
// my super cannot see my fields so remove them.
val remainingFields = availableFields.filterNot {
it.element.enclosingElement == typeElement
}
collectPrimaryKeysFromEntityAnnotations(mySuper.typeElement!!, remainingFields)
} else {
emptyList()
}
return superPKeys + myPkeys
}
private fun collectPrimaryKeysFromEmbeddedFields(
embeddedFields: List
): List {
return embeddedFields.mapNotNull { embeddedField ->
embeddedField.field.element.getAnnotation(com.zeoflow.depot.PrimaryKey::class)?.let {
context.checker.check(
!it.value.autoGenerate || embeddedField.pojo.fields.size == 1,
embeddedField.field.element,
ProcessorErrors.AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_FIELDS
)
PrimaryKey(
declaredIn = embeddedField.field.element.enclosingElement,
fields = embeddedField.pojo.fields,
autoGenerateId = it.value.autoGenerate
)
}
}
}
// start from my element and check if anywhere in the list we can find the only well defined
// pkey, if so, use it.
private fun choosePrimaryKey(
candidates: List,
typeElement: XTypeElement
): PrimaryKey {
// If 1 of these primary keys is declared in this class, then it is the winner. Just print
// a note for the others.
// If 0 is declared, check the parent.
// If more than 1 primary key is declared in this class, it is an error.
val myPKeys = candidates.filter { candidate ->
candidate.declaredIn == typeElement
}
return if (myPKeys.size == 1) {
// just note, this is not worth an error or warning
(candidates - myPKeys).forEach {
context.logger.d(
element,
"${it.toHumanReadableString()} is" +
" overridden by ${myPKeys.first().toHumanReadableString()}"
)
}
myPKeys.first()
} else if (myPKeys.isEmpty()) {
// i have not declared anything, delegate to super
val mySuper = typeElement.superType
if (mySuper != null && mySuper.isNotNone()) {
return choosePrimaryKey(candidates, mySuper.typeElement!!)
}
PrimaryKey.MISSING
} else {
context.logger.e(
element,
ProcessorErrors.multiplePrimaryKeyAnnotations(
myPKeys.map(PrimaryKey::toHumanReadableString)
)
)
PrimaryKey.MISSING
}
}
private fun validateAndCreateIndices(
inputs: List,
pojo: Pojo
): List {
// check for columns
val indices = inputs.mapNotNull { input ->
context.checker.check(
input.columnNames.isNotEmpty(), element,
INDEX_COLUMNS_CANNOT_BE_EMPTY
)
val fields = input.columnNames.mapNotNull { columnName ->
val field = pojo.findFieldByColumnName(columnName)
context.checker.check(
field != null, element,
ProcessorErrors.indexColumnDoesNotExist(columnName, pojo.columnNames)
)
field
}
if (fields.isEmpty()) {
null
} else {
Index(name = input.name, unique = input.unique, fields = fields)
}
}
// check for duplicate indices
indices
.groupBy { it.name }
.filter { it.value.size > 1 }
.forEach {
context.logger.e(element, ProcessorErrors.duplicateIndexInEntity(it.key))
}
// see if any embedded field is an entity with indices, if so, report a warning
pojo.embeddedFields.forEach { embedded ->
val embeddedElement = embedded.pojo.element
embeddedElement.getAnnotation(com.zeoflow.depot.Entity::class)?.let {
val subIndices = extractIndices(it, "")
if (subIndices.isNotEmpty()) {
context.logger.w(
Warning.INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED,
embedded.field.element,
ProcessorErrors.droppedEmbeddedIndex(
entityName = embedded.pojo.typeName.toString(),
fieldPath = embedded.field.getPath(),
grandParent = element.qualifiedName
)
)
}
}
}
return indices
}
// check if parent is an Entity, if so, report its annotation indices
private fun loadSuperIndices(
typeMirror: XType?,
tableName: String,
inherit: Boolean
): List {
if (typeMirror == null || typeMirror.isNone()) {
return emptyList()
}
val parentTypeElement = typeMirror.typeElement
@Suppress("FoldInitializerAndIfToElvis")
if (parentTypeElement == null) {
// this is coming from a parent, shouldn't happen so no reason to report an error
return emptyList()
}
val myIndices = parentTypeElement
.getAnnotation(com.zeoflow.depot.Entity::class)?.let { annotation ->
val indices = extractIndices(annotation, tableName = "super")
if (indices.isEmpty()) {
emptyList()
} else if (inherit) {
// rename them
indices.map {
IndexInput(
name = createIndexName(it.columnNames, tableName),
unique = it.unique,
columnNames = it.columnNames
)
}
} else {
context.logger.w(
Warning.INDEX_FROM_PARENT_IS_DROPPED,
parentTypeElement,
ProcessorErrors.droppedSuperClassIndex(
childEntity = element.qualifiedName,
superEntity = parentTypeElement.qualifiedName
)
)
emptyList()
}
} ?: emptyList()
return myIndices + loadSuperIndices(parentTypeElement.superType, tableName, inherit)
}
}