From deff60605093dd105c7b218cec4f358e97a37264 Mon Sep 17 00:00:00 2001 From: Jocelyne <38375996+joc-a@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:30:20 +0200 Subject: [PATCH] feat: EXPOSED-78 Support database-generated values for columns (#1844) * feat: EXPOSED-78 Support DB-generated values for columns This feature will enable marking a column as databaseGenerated to be able to omit setting it when inserting a new record without getting an error. The value for the column can be set by creating a TRIGGER or with a DEFAULT clause, for example. --- exposed-core/api/exposed-core.api | 1 + .../org/jetbrains/exposed/sql/Column.kt | 2 + .../kotlin/org/jetbrains/exposed/sql/Table.kt | 5 ++ .../statements/BaseBatchInsertStatement.kt | 2 +- .../sql/tests/shared/entities/EntityTests.kt | 71 +++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index ed6910515b..d9ab88ba30 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -2218,6 +2218,7 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; public final fun customEnumeration (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Column; public static synthetic fun customEnumeration$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; + public final fun databaseGenerated (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Column; public final fun decimal (Ljava/lang/String;II)Lorg/jetbrains/exposed/sql/Column; public final fun default (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; public final fun default (Lorg/jetbrains/exposed/sql/CompositeColumn;Ljava/lang/Object;)Lorg/jetbrains/exposed/sql/CompositeColumn; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt index 810e5de52d..f6d4c22bf1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt @@ -39,6 +39,8 @@ class Column( fun defaultValueInDb() = dbDefaultValue + internal var isDatabaseGenerated: Boolean = false + /** Appends the SQL representation of this column to the specified [queryBuilder]. */ override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = TransactionManager.current().fullIdentity(this@Column, queryBuilder) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index 5503d8c44b..c861214ec7 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -772,6 +772,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { defaultValueFun = defaultValue } + // Potential names: readOnly, generatable, dbGeneratable, dbGenerated, generated, generatedDefault, generatedInDb + fun Column.databaseGenerated(): Column = apply { + isDatabaseGenerated = true + } + /** UUID column will auto generate its value on a client side just before an insert. */ fun Column.autoGenerate(): Column = clientDefault { UUID.randomUUID() } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt index e0f34017b8..bf30e70ec9 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BaseBatchInsertStatement.kt @@ -18,7 +18,7 @@ abstract class BaseBatchInsertStatement( internal val data = ArrayList, Any?>>() - private fun Column<*>.isDefaultable() = columnType.nullable || defaultValueFun != null + private fun Column<*>.isDefaultable() = columnType.nullable || defaultValueFun != null || isDatabaseGenerated override operator fun set(column: Column, value: S) { if (data.size > 1 && column !in data[data.size - 2] && !column.isDefaultable()) { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt index 81dab11b89..98a4f7ef46 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/entities/EntityTests.kt @@ -1423,4 +1423,75 @@ class EntityTests : DatabaseTestsBase() { assertEquals(1, count) } } + + object CreditCards : IntIdTable("CreditCards") { + val number = varchar("number", 16) + val spendingLimit = ulong("spendingLimit").databaseGenerated() + } + + class CreditCard(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(CreditCards) + + var number by CreditCards.number + var spendingLimit by CreditCards.spendingLimit + } + + @Test + fun testDatabaseGeneratedValues() { + withTables(excludeSettings = listOf(TestDB.SQLITE), CreditCards) { testDb -> + when (testDb) { + TestDB.POSTGRESQL, TestDB.POSTGRESQLNG -> { + // The value can also be set using a SQL trigger + exec( + """ + CREATE OR REPLACE FUNCTION set_spending_limit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS + $$ + BEGIN + NEW."spendingLimit" := 10000; + RETURN NEW; + END; + $$; + """.trimIndent() + ) + exec( + """ + CREATE TRIGGER set_spending_limit + BEFORE INSERT + ON CreditCards + FOR EACH ROW + EXECUTE PROCEDURE set_spending_limit(); + """.trimIndent() + ) + } + else -> { + // This table is only used to get the statement that adds the DEFAULT value, and use it with exec + val creditCards2 = object : IntIdTable("CreditCards") { + val spendingLimit = ulong("spendingLimit").default(10000uL) + } + val missingStatements = SchemaUtils.addMissingColumnsStatements(creditCards2) + missingStatements.forEach { + exec(it) + } + } + } + + val creditCardId = CreditCards.insertAndGetId { + it[number] = "0000111122223333" + }.value + assertEquals( + 10000uL, + CreditCards.select { CreditCards.id eq creditCardId }.single()[CreditCards.spendingLimit] + ) + + val creditCard = CreditCard.new { + number = "0000111122223333" + }.apply { + flush() + } + assertEquals(10000uL, creditCard.spendingLimit) + } + } }