diff --git a/exposed-migration/api/exposed-migration.api b/exposed-migration/api/exposed-migration.api index e6cd36bf18..3428f6fe2e 100644 --- a/exposed-migration/api/exposed-migration.api +++ b/exposed-migration/api/exposed-migration.api @@ -16,3 +16,35 @@ public final class org/jetbrains/exposed/v1/migration/MigrationUtils : org/jetbr public static synthetic fun statementsRequiredForDatabaseMigration$default (Lorg/jetbrains/exposed/v1/migration/MigrationUtils;[Lorg/jetbrains/exposed/v1/core/Table;ZILjava/lang/Object;)Ljava/util/List; } +public final class org/jetbrains/exposed/v1/migration/SchemaValidationException : java/lang/Exception { + public fun (Ljava/lang/String;Ljava/util/List;)V + public final fun getMigrationStatements ()Ljava/util/List; +} + +public final class org/jetbrains/exposed/v1/migration/SchemaValidationKt { + public static final fun assertSchemaIsCorrect ([Lorg/jetbrains/exposed/v1/core/Table;Z)V + public static synthetic fun assertSchemaIsCorrect$default ([Lorg/jetbrains/exposed/v1/core/Table;ZILjava/lang/Object;)V + public static final fun validateSchema ([Lorg/jetbrains/exposed/v1/core/Table;Z)Lorg/jetbrains/exposed/v1/migration/SchemaValidationResult; + public static synthetic fun validateSchema$default ([Lorg/jetbrains/exposed/v1/core/Table;ZILjava/lang/Object;)Lorg/jetbrains/exposed/v1/migration/SchemaValidationResult; +} + +public abstract class org/jetbrains/exposed/v1/migration/SchemaValidationResult { + public final fun getMigrationStatements ()Ljava/util/List; + public final fun isValid ()Z +} + +public final class org/jetbrains/exposed/v1/migration/SchemaValidationResult$Invalid : org/jetbrains/exposed/v1/migration/SchemaValidationResult { + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lorg/jetbrains/exposed/v1/migration/SchemaValidationResult$Invalid; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/v1/migration/SchemaValidationResult$Invalid;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/exposed/v1/migration/SchemaValidationResult$Invalid; + public fun equals (Ljava/lang/Object;)Z + public final fun getStatements ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/exposed/v1/migration/SchemaValidationResult$Valid : org/jetbrains/exposed/v1/migration/SchemaValidationResult { + public static final field INSTANCE Lorg/jetbrains/exposed/v1/migration/SchemaValidationResult$Valid; +} + diff --git a/exposed-migration/src/main/kotlin/org/jetbrains/exposed/v1/migration/SchemaValidation.kt b/exposed-migration/src/main/kotlin/org/jetbrains/exposed/v1/migration/SchemaValidation.kt new file mode 100644 index 0000000000..5948a0dcea --- /dev/null +++ b/exposed-migration/src/main/kotlin/org/jetbrains/exposed/v1/migration/SchemaValidation.kt @@ -0,0 +1,92 @@ +package org.jetbrains.exposed.v1.migration + +import org.jetbrains.exposed.v1.core.Table + +/** + * Asserts that the database schema is aligned with the Exposed table definitions. + * + * This function uses MigrationUtils.statementsRequiredForDatabaseMigration() to + * determine if there are any differences between the database schema and the code definitions. + * + * @param tables The tables to verify + * @param inBatch If true, performs the verification in batch for improved performance + * @throws SchemaValidationException if the schema is not correct + */ +fun assertSchemaIsCorrect(vararg tables: Table, inBatch: Boolean = false) { + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(*tables, withLogs = inBatch) + + if (statements.isNotEmpty()) { + val errorMessage = buildString { + appendLine("Schema validation failed. Database schema is not aligned with code definitions.") + appendLine("Required migration statements:") + statements.forEachIndexed { index, statement -> + appendLine("${index + 1}. $statement") + } + } + throw SchemaValidationException(errorMessage, statements) + } +} + +/** + * Verifies that the database schema is aligned with the Exposed table definitions. + * + * Alternative version that returns a result instead of throwing an exception. + * + * @param tables The tables to verify + * @param inBatch If true, performs the verification in batch for improved performance + * @return SchemaValidationResult with information about the validation result + */ +fun validateSchema(vararg tables: Table, inBatch: Boolean = false): SchemaValidationResult { + val statements = MigrationUtils.statementsRequiredForDatabaseMigration(*tables, withLogs = inBatch) + + return if (statements.isEmpty()) { + SchemaValidationResult.Valid + } else { + SchemaValidationResult.Invalid(statements) + } +} + +/** + * Exception thrown when schema validation fails. + * + * @param message The error message describing the validation failure + * @param migrationStatements The SQL statements required to fix the schema + */ +class SchemaValidationException( + message: String, + val migrationStatements: List +) : Exception(message) + +/** + * Result of schema validation. + */ +sealed class SchemaValidationResult { + /** + * The schema is valid and aligned. + */ + object Valid : SchemaValidationResult() + + /** + * The schema is invalid and requires migrations. + * + * @param statements The SQL statements required to align the schema + */ + data class Invalid(val statements: List) : SchemaValidationResult() + + /** + * Checks if the schema is valid. + * + * @return true if the schema is valid, false otherwise + */ + fun isValid(): Boolean = this is Valid + + /** + * Gets the migration statements required to fix the schema. + * + * @return List of SQL statements, empty if schema is valid + */ + fun getMigrationStatements(): List = when (this) { + is Valid -> emptyList() + is Invalid -> statements + } +} diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/v1/tests/shared/ddl/SchemaValidationTest.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/v1/tests/shared/ddl/SchemaValidationTest.kt new file mode 100644 index 0000000000..06f8d359d8 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/v1/tests/shared/ddl/SchemaValidationTest.kt @@ -0,0 +1,116 @@ +package org.jetbrains.exposed.v1.tests.shared.ddl + +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.migration.SchemaValidationException +import org.jetbrains.exposed.v1.migration.assertSchemaIsCorrect +import org.jetbrains.exposed.v1.migration.validateSchema +import org.jetbrains.exposed.v1.tests.DatabaseTestsBase +import org.jetbrains.exposed.v1.tests.shared.assertFalse +import org.jetbrains.exposed.v1.tests.shared.assertTrue +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class SchemaValidationTest : DatabaseTestsBase() { + + // Simple test table + object TestTable : Table("test_table") { + val id = integer("id").autoIncrement() + val name = varchar("name", 50) + + override val primaryKey = PrimaryKey(id) + } + + // Table for mismatched schema test + object MismatchedTable : Table("mismatched_table") { + val id = integer("id").autoIncrement() + + override val primaryKey = PrimaryKey(id) + } + + @Test + fun testAssertSchemaIsCorrectWithValidSchema() { + withTables(TestTable) { + // Schema should be correct after creation + assertSchemaIsCorrect(TestTable) + } + } + + @Test + fun testAssertSchemaIsCorrectWithInvalidSchema() { + withTables(TestTable) { + // Create a situation where the schema is not aligned + // by testing with a table that was not created + assertFailsWith { + assertSchemaIsCorrect(MismatchedTable) + } + } + } + + @Test + fun testAssertSchemaIsCorrectWithBatchMode() { + withTables(TestTable) { + // Test with batch mode + assertSchemaIsCorrect(TestTable, inBatch = true) + } + } + + @Test + fun testAssertSchemaIsCorrectMultipleTables() { + withTables(TestTable, MismatchedTable) { + // Schema should be correct for both tables + assertSchemaIsCorrect(TestTable, MismatchedTable) + } + } + + @Test + fun testValidateSchemaWithValidSchema() { + withTables(TestTable) { + val result = validateSchema(TestTable) + assertTrue(result.isValid()) + assertTrue(result.getMigrationStatements().isEmpty()) + } + } + + @Test + fun testValidateSchemaWithInvalidSchema() { + withTables(TestTable) { + // Test with a table that was not created + val result = validateSchema(MismatchedTable) + assertFalse(result.isValid()) + assertFalse(result.getMigrationStatements().isEmpty()) + } + } + + @Test + fun testValidateSchemaWithBatchMode() { + withTables(TestTable) { + val result = validateSchema(TestTable, inBatch = true) + assertTrue(result.isValid()) + } + } + + @Test + fun testSchemaValidationExceptionProperties() { + withTables(TestTable) { + try { + assertSchemaIsCorrect(MismatchedTable) + } catch (e: SchemaValidationException) { + assertFalse(e.migrationStatements.isEmpty()) + assertTrue(e.message!!.contains("Schema validation failed")) + } + } + } + + @Test + fun testValidateSchemaResultTypes() { + withTables(TestTable) { + // Test with valid schema + val validResult = validateSchema(TestTable) + assertTrue(validResult.isValid()) + + // Test with invalid schema + val invalidResult = validateSchema(MismatchedTable) + assertFalse(invalidResult.isValid()) + } + } +}