Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: EXPOSED-78 Support database-generated values for columns #1844

Merged
merged 2 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -2215,6 +2215,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 dbGenerated (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 isGeneratedInDb: Boolean = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we check what names are used in other frameworks for that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is what I found:

I didn't find this feature in Ktorm and jOOQ.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

databaseGenerated lgtm


/** 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 @@ -725,6 +725,11 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
defaultValueFun = defaultValue
}

// Potential names: readOnly, generatable, dbGeneratable, dbGenerated, generated, generatedDefault, generatedInDb
fun <T> Column<T>.dbGenerated(): Column<T> = apply {
isGeneratedInDb = 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 || isGeneratedInDb

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 @@ -1400,4 +1400,76 @@ class EntityTests : DatabaseTestsBase() {
assertEquals(1, count)
}
}

object CreditCards : IntIdTable("CreditCards") {
val number = varchar("number", 16)
val spendingLimit = ulong("spendingLimit").dbGenerated()
}

class CreditCard(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<CreditCard>(CreditCards)

var number by CreditCards.number
var spendingLimit by CreditCards.spendingLimit
}

@Test
fun testDbGeneratedDefault() {
withTables(excludeSettings = listOf(TestDB.SQLITE), CreditCards) { testDb ->
addLogger(StdOutSqlLogger)
joc-a marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
}