Skip to content

Commit

Permalink
feat: EXPOSED-78 Support database-generated values for columns (#1844)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
joc-a authored Sep 18, 2023
1 parent 5ce2d5a commit deff606
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 1 deletion.
1 change: 1 addition & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class Column<T>(

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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
defaultValueFun = defaultValue
}

// Potential names: readOnly, generatable, dbGeneratable, dbGenerated, generated, generatedDefault, generatedInDb
fun <T> Column<T>.databaseGenerated(): Column<T> = apply {
isDatabaseGenerated = true
}

/** UUID column will auto generate its value on a client side just before an insert. */
fun Column<UUID>.autoGenerate(): Column<UUID> = clientDefault { UUID.randomUUID() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ abstract class BaseBatchInsertStatement(

internal val data = ArrayList<MutableMap<Column<*>, Any?>>()

private fun Column<*>.isDefaultable() = columnType.nullable || defaultValueFun != null
private fun Column<*>.isDefaultable() = columnType.nullable || defaultValueFun != null || isDatabaseGenerated

override operator fun <S> set(column: Column<S>, value: S) {
if (data.size > 1 && column !in data[data.size - 2] && !column.isDefaultable()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>) : IntEntity(id) {
companion object : IntEntityClass<CreditCard>(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)
}
}
}

0 comments on commit deff606

Please sign in to comment.