diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..5cb71ef --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/main/kotlin/dev/mduchene/bolts/entity/EntityMigrationService.kt b/src/main/kotlin/dev/mduchene/bolts/entity/EntityMigrationService.kt index 9304e03..354bbc0 100644 --- a/src/main/kotlin/dev/mduchene/bolts/entity/EntityMigrationService.kt +++ b/src/main/kotlin/dev/mduchene/bolts/entity/EntityMigrationService.kt @@ -180,6 +180,41 @@ class EntityMigrationService( } } + definitions.forEach { parent -> + parent.fields.filter { it.type == FieldType.RELATIONSHIP }.forEach { field -> + val child = definitions.first { it.id == field.targetEntityId } + val childTable = existingTables[child.identifier.lowercase()] ?: child.identifier + val parentTable = existingTables[parent.identifier.lowercase()] ?: parent.identifier + val column = parentColumn(field.id) + val childColumns = if (child.identifier.lowercase() in existingTables) { + columnTypes(connection, child.identifier) + } else { + emptyMap() + } + if (column.lowercase() !in childColumns) { + statements += MigrationStatement( + "ALTER TABLE ${quote(childTable)} ADD COLUMN ${quote(column)} BIGINT", + "Add internal parent link for ${parent.identifier}.${field.identifier}.", + ) + } + val foreignKey = "em_parent_fk_${field.id}" + if (!constraintExists(connection, foreignKey)) { + statements += MigrationStatement( + "ALTER TABLE ${quote(childTable)} ADD CONSTRAINT ${quote(foreignKey)} " + + "FOREIGN KEY (${quote(column)}) REFERENCES ${quote(parentTable)} (\"id\")", + "Link ${child.identifier} children to ${parent.identifier} parents.", + ) + } + val uniqueName = "em_parent_uq_${field.id}" + if (field.relationshipType == RelationshipType.ONE_TO_ONE && !constraintExists(connection, uniqueName)) { + statements += MigrationStatement( + "ALTER TABLE ${quote(childTable)} ADD CONSTRAINT ${quote(uniqueName)} UNIQUE (${quote(column)})", + "Limit ${parent.identifier}.${field.identifier} to one child.", + ) + } + } + } + return migrationPlan(errors, warnings, statements) } @@ -298,6 +333,8 @@ class EntityMigrationService( private fun quote(identifier: String) = "\"${identifier.replace("\"", "\"\"")}\"" + private fun parentColumn(fieldId: Long) = "__parent_$fieldId" + private fun migrationPlan( errors: List, warnings: List, diff --git a/src/main/kotlin/dev/mduchene/bolts/entity/EntityRecordRepository.kt b/src/main/kotlin/dev/mduchene/bolts/entity/EntityRecordRepository.kt index 5b22e7b..149425b 100644 --- a/src/main/kotlin/dev/mduchene/bolts/entity/EntityRecordRepository.kt +++ b/src/main/kotlin/dev/mduchene/bolts/entity/EntityRecordRepository.kt @@ -127,11 +127,10 @@ class EntityRecordRepository( } val childId = insertRecord(child, userId, submittedValues) database.executeUpdate( - "INSERT INTO entity_record_relationships (field_id, source_record_id, target_record_id) VALUES (?, ?, ?)", + "UPDATE ${quote(child.identifier)} SET ${quote(parentColumn(relationship.id))} = ? WHERE \"id\" = ?", ) { - setLong(1, relationship.id) - setLong(2, parentRecordId) - setLong(3, childId) + setLong(1, parentRecordId) + setLong(2, childId) } if (relationship.relationshipType == RelationshipType.ONE_TO_ONE) { database.executeUpdate( @@ -160,13 +159,15 @@ class EntityRecordRepository( } private fun outboundChildIds(fieldId: Long, sourceRecordId: Long): List = - database.queryList( - "SELECT target_record_id FROM entity_record_relationships WHERE field_id = ? AND source_record_id = ? ORDER BY target_record_id", - bind = { - setLong(1, fieldId) - setLong(2, sourceRecordId) - }, - ) { getLong("target_record_id") } + entities.findAll().firstNotNullOfOrNull { parent -> + val relationship = parent.fields.firstOrNull { it.id == fieldId } ?: return@firstNotNullOfOrNull null + val child = entities.findAll().firstOrNull { it.id == relationship.targetEntityId } + ?: return@firstNotNullOfOrNull emptyList() + database.queryList( + "SELECT \"id\" FROM ${quote(child.identifier)} WHERE ${quote(parentColumn(fieldId))} = ? ORDER BY \"id\"", + bind = { setLong(1, sourceRecordId) }, + ) { getLong("id") } + }.orEmpty() private fun insertRecord(entity: EntityDefinition, userId: Long, submittedValues: Map): Long { val fields = entity.fields @@ -219,13 +220,10 @@ class EntityRecordRepository( .filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId == entity.id } .any { field -> database.queryList( - "SELECT source_record_id FROM entity_record_relationships " + - "WHERE field_id = ? AND target_record_id = ?", - bind = { - setLong(1, field.id) - setLong(2, recordId) - }, - ) { getLong("source_record_id") }.any { parentId -> + "SELECT ${quote(parentColumn(field.id))} AS parent_id FROM ${quote(entity.identifier)} " + + "WHERE \"id\" = ? AND ${quote(parentColumn(field.id))} IS NOT NULL", + bind = { setLong(1, recordId) }, + ) { getLong("parent_id") }.any { parentId -> val parentValues = findValues(parent, parentId) ?: return@any false isAccessibleToUser(parent, parentValues, userId, definitions, visited) } @@ -268,4 +266,6 @@ class EntityRecordRepository( } private fun quote(identifier: String) = "\"${identifier.replace("\"", "\"\"")}\"" + + private fun parentColumn(fieldId: Long) = "__parent_$fieldId" } diff --git a/src/main/resources/db/migrations/20260731192000_create_entity_record_relationships.sql b/src/main/resources/db/migrations/20260731192000_create_entity_record_relationships.sql deleted file mode 100644 index 4e9f4e1..0000000 --- a/src/main/resources/db/migrations/20260731192000_create_entity_record_relationships.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE entity_record_relationships ( - field_id BIGINT NOT NULL REFERENCES entity_fields(id) ON DELETE CASCADE, - source_record_id BIGINT NOT NULL, - target_record_id BIGINT NOT NULL, - PRIMARY KEY (field_id, source_record_id, target_record_id) -); - -CREATE INDEX entity_record_relationships_target_idx - ON entity_record_relationships(field_id, target_record_id); diff --git a/src/main/resources/db/reset.sql b/src/main/resources/db/reset.sql index 4172148..b6ca9cd 100644 --- a/src/main/resources/db/reset.sql +++ b/src/main/resources/db/reset.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS entity_record_relationships; DROP TABLE IF EXISTS entity_fields; DROP TABLE IF EXISTS entity_definitions; DROP TABLE IF EXISTS sessions;