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;