use latest effective model instead of current config

This commit is contained in:
Maxime Duchêne-Savard 2026-08-01 23:18:07 -04:00
parent 140b195111
commit 5693657b54
5 changed files with 111 additions and 8 deletions

View File

@ -45,6 +45,25 @@ class EntityDefinitionRepository(private val database: Database) {
} }
} }
/** The last model whose physical database migration completed successfully. */
fun findEffectiveAll(): List<EntityDefinition> {
val fields = database.queryList(
"""
SELECT fields.id, fields.entity_id, fields.name, fields.identifier,
fields.field_type, fields.target_entity_id, fields.relationship_type,
targets.name AS target_entity_name
FROM effective_entity_fields fields
LEFT JOIN effective_entity_definitions targets ON targets.id = fields.target_entity_id
ORDER BY fields.id
""".trimIndent(),
) { FieldRow(getLong("entity_id"), toEntityField()) }.groupBy(FieldRow::entityId)
return database.queryList("SELECT id, name, identifier FROM effective_entity_definitions ORDER BY id") {
val id = getLong("id")
EntityDefinition(id, getString("name"), getString("identifier"), fields[id].orEmpty().map(FieldRow::field))
}
}
fun create(name: String, identifier: String): EntityDefinition { fun create(name: String, identifier: String): EntityDefinition {
val sql = "INSERT INTO entity_definitions (name, identifier) VALUES (?, ?) RETURNING id, name, identifier" val sql = "INSERT INTO entity_definitions (name, identifier) VALUES (?, ?) RETURNING id, name, identifier"
return checkNotNull(database.queryOne(sql, bind = { return checkNotNull(database.queryOne(sql, bind = {

View File

@ -27,7 +27,8 @@ class EntityMigrationService(
fun migrate(): EntityMigrationResult = fun migrate(): EntityMigrationResult =
try { try {
database.transaction { connection -> database.transaction { connection ->
val plan = plan(connection, loadEntities(connection)) val definitions = loadEntities(connection)
val plan = plan(connection, definitions)
if (!plan.validation.valid) { if (!plan.validation.valid) {
return@transaction EntityMigrationResult( return@transaction EntityMigrationResult(
success = false, success = false,
@ -40,6 +41,7 @@ class EntityMigrationService(
connection.createStatement().use { statement -> connection.createStatement().use { statement ->
plan.statements.forEach { statement.execute(it.sql) } plan.statements.forEach { statement.execute(it.sql) }
} }
saveEffectiveModel(connection, definitions)
EntityMigrationResult( EntityMigrationResult(
success = true, success = true,
message = "Entity migration completed successfully.", message = "Entity migration completed successfully.",
@ -55,6 +57,43 @@ class EntityMigrationService(
) )
} }
private fun saveEffectiveModel(connection: Connection, definitions: List<EntityDefinition>) {
connection.createStatement().use { statement ->
statement.executeUpdate("DELETE FROM effective_entity_fields")
statement.executeUpdate("DELETE FROM effective_entity_definitions")
}
connection.prepareStatement(
"INSERT INTO effective_entity_definitions (id, name, identifier) VALUES (?, ?, ?)",
).use { statement ->
definitions.forEach { entity ->
statement.setLong(1, entity.id)
statement.setString(2, entity.name)
statement.setString(3, entity.identifier)
statement.addBatch()
}
statement.executeBatch()
}
connection.prepareStatement(
"""INSERT INTO effective_entity_fields
(id, entity_id, name, identifier, field_type, target_entity_id, relationship_type)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
).use { statement ->
definitions.forEach { entity ->
entity.fields.forEach { field ->
statement.setLong(1, field.id)
statement.setLong(2, entity.id)
statement.setString(3, field.name)
statement.setString(4, field.identifier)
statement.setString(5, field.type.name)
statement.setObject(6, field.targetEntityId)
statement.setString(7, field.relationshipType?.name)
statement.addBatch()
}
}
statement.executeBatch()
}
}
private fun plan(connection: Connection, definitions: List<EntityDefinition>): MigrationPlan { private fun plan(connection: Connection, definitions: List<EntityDefinition>): MigrationPlan {
val errors = mutableListOf<String>() val errors = mutableListOf<String>()
val warnings = mutableListOf<String>() val warnings = mutableListOf<String>()
@ -62,6 +101,7 @@ class EntityMigrationService(
val identifierPattern = Regex("[A-Za-z_][A-Za-z0-9_]*") val identifierPattern = Regex("[A-Za-z_][A-Za-z0-9_]*")
val reservedTables = setOf( val reservedTables = setOf(
"entity_definitions", "entity_fields", "entity_indexes", "entity_index_fields", "entity_definitions", "entity_fields", "entity_indexes", "entity_index_fields",
"effective_entity_definitions", "effective_entity_fields",
"schema_migrations", "sessions", "users", "schema_migrations", "sessions", "users",
) )
val duplicateEntities = definitions.groupBy { it.identifier.lowercase() }.filterValues { it.size > 1 } val duplicateEntities = definitions.groupBy { it.identifier.lowercase() }.filterValues { it.size > 1 }

View File

@ -34,7 +34,7 @@ class EntityRecordRepository(
buildSet { while (result.next()) add(result.getString("TABLE_NAME").lowercase()) } buildSet { while (result.next()) add(result.getString("TABLE_NAME").lowercase()) }
} }
entities.findAll() entities.findEffectiveAll()
.filter { entity -> .filter { entity ->
entity.identifier.lowercase() in tables && entity.fields.any { it.type == FieldType.USER } entity.identifier.lowercase() in tables && entity.fields.any { it.type == FieldType.USER }
} }
@ -58,7 +58,7 @@ class EntityRecordRepository(
userFieldId: Long, userFieldId: Long,
submittedValues: Map<String, String>, submittedValues: Map<String, String>,
): EntityRecord? { ): EntityRecord? {
val child = entities.findAll().firstOrNull { it.name == childEntityName } ?: return null val child = entities.findEffectiveAll().firstOrNull { it.name == childEntityName } ?: return null
val userField = child.fields.firstOrNull { val userField = child.fields.firstOrNull {
it.id == userFieldId && it.type == FieldType.USER it.id == userFieldId && it.type == FieldType.USER
} ?: return null } ?: return null
@ -75,7 +75,7 @@ class EntityRecordRepository(
} }
fun findRecordLinkedToUser(userId: Long, entityName: String, recordId: Long): EntityRecord? { fun findRecordLinkedToUser(userId: Long, entityName: String, recordId: Long): EntityRecord? {
val definitions = entities.findAll() val definitions = entities.findEffectiveAll()
val entity = definitions.firstOrNull { it.name == entityName } ?: return null val entity = definitions.firstOrNull { it.name == entityName } ?: return null
val values = findValues(entity, recordId) ?: return null val values = findValues(entity, recordId) ?: return null
if (!isAccessibleToUser(entity, values, userId, definitions, mutableSetOf())) return null if (!isAccessibleToUser(entity, values, userId, definitions, mutableSetOf())) return null
@ -111,7 +111,7 @@ class EntityRecordRepository(
relationshipFieldId: Long, relationshipFieldId: Long,
submittedValues: Map<String, String>, submittedValues: Map<String, String>,
): EntityRecord? { ): EntityRecord? {
val definitions = entities.findAll() val definitions = entities.findEffectiveAll()
val parent = definitions.firstOrNull { it.name == parentEntityName } ?: return null val parent = definitions.firstOrNull { it.name == parentEntityName } ?: return null
if (findRecordLinkedToUser(userId, parentEntityName, parentRecordId) == null) return null if (findRecordLinkedToUser(userId, parentEntityName, parentRecordId) == null) return null
val child = definitions.firstOrNull { it.name == childEntityName } ?: return null val child = definitions.firstOrNull { it.name == childEntityName } ?: return null
@ -144,7 +144,7 @@ class EntityRecordRepository(
} }
private fun findChildren(parentEntityId: Long, parentRecordId: Long): List<ChildEntityRecords> { private fun findChildren(parentEntityId: Long, parentRecordId: Long): List<ChildEntityRecords> {
val definitions = entities.findAll() val definitions = entities.findEffectiveAll()
val parent = definitions.firstOrNull { it.id == parentEntityId } ?: return emptyList() val parent = definitions.firstOrNull { it.id == parentEntityId } ?: return emptyList()
return parent.fields return parent.fields
.filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId != null } .filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId != null }
@ -159,9 +159,9 @@ class EntityRecordRepository(
} }
private fun outboundChildIds(fieldId: Long, sourceRecordId: Long): List<Long> = private fun outboundChildIds(fieldId: Long, sourceRecordId: Long): List<Long> =
entities.findAll().firstNotNullOfOrNull { parent -> entities.findEffectiveAll().firstNotNullOfOrNull { parent ->
val relationship = parent.fields.firstOrNull { it.id == fieldId } ?: return@firstNotNullOfOrNull null val relationship = parent.fields.firstOrNull { it.id == fieldId } ?: return@firstNotNullOfOrNull null
val child = entities.findAll().firstOrNull { it.id == relationship.targetEntityId } val child = entities.findEffectiveAll().firstOrNull { it.id == relationship.targetEntityId }
?: return@firstNotNullOfOrNull emptyList() ?: return@firstNotNullOfOrNull emptyList()
database.queryList( database.queryList(
"SELECT \"id\" FROM ${quote(child.identifier)} WHERE ${quote(parentColumn(fieldId))} = ? ORDER BY \"id\"", "SELECT \"id\" FROM ${quote(child.identifier)} WHERE ${quote(parentColumn(fieldId))} = ? ORDER BY \"id\"",

View File

@ -0,0 +1,42 @@
CREATE TABLE IF NOT EXISTS effective_entity_definitions (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
identifier VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS effective_entity_fields (
id BIGINT PRIMARY KEY,
entity_id BIGINT NOT NULL REFERENCES effective_entity_definitions(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
identifier VARCHAR(255) NOT NULL,
field_type VARCHAR(32) NOT NULL,
target_entity_id BIGINT,
relationship_type VARCHAR(32)
);
-- Seed upgrades from only the parts of the configured model that are already
-- present in the physical database. Future snapshots are written after a
-- successful dynamic-entity migration.
INSERT INTO effective_entity_definitions (id, name, identifier)
SELECT d.id, d.name, d.identifier
FROM entity_definitions d
WHERE EXISTS (
SELECT 1 FROM information_schema.tables t
WHERE t.table_schema = current_schema() AND lower(t.table_name) = lower(d.identifier)
);
INSERT INTO effective_entity_fields (
id, entity_id, name, identifier, field_type, target_entity_id, relationship_type
)
SELECT f.id, f.entity_id, f.name, f.identifier, f.field_type, f.target_entity_id, f.relationship_type
FROM entity_fields f
JOIN effective_entity_definitions d ON d.id = f.entity_id
WHERE EXISTS (
SELECT 1 FROM information_schema.columns c
WHERE c.table_schema = current_schema()
AND lower(c.table_name) = lower(d.identifier)
AND lower(c.column_name) = lower(f.identifier)
)
AND (f.target_entity_id IS NULL OR EXISTS (
SELECT 1 FROM effective_entity_definitions target WHERE target.id = f.target_entity_id
));

View File

@ -1,5 +1,7 @@
DROP TABLE IF EXISTS entity_index_fields; DROP TABLE IF EXISTS entity_index_fields;
DROP TABLE IF EXISTS entity_indexes; DROP TABLE IF EXISTS entity_indexes;
DROP TABLE IF EXISTS effective_entity_fields;
DROP TABLE IF EXISTS effective_entity_definitions;
DROP TABLE IF EXISTS entity_fields; DROP TABLE IF EXISTS entity_fields;
DROP TABLE IF EXISTS entity_definitions; DROP TABLE IF EXISTS entity_definitions;
DROP TABLE IF EXISTS sessions; DROP TABLE IF EXISTS sessions;