Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions exposed-migration/api/exposed-migration.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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 <init> (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;
}

Original file line number Diff line number Diff line change
@@ -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<String>
) : 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<String>) : 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<String> = when (this) {
is Valid -> emptyList()
is Invalid -> statements
}
}
Original file line number Diff line number Diff line change
@@ -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<SchemaValidationException> {
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())
}
}
}