diff --git a/documentation-website/Writerside/topics/Breaking-Changes.md b/documentation-website/Writerside/topics/Breaking-Changes.md index b7e737d407..75ca282e22 100644 --- a/documentation-website/Writerside/topics/Breaking-Changes.md +++ b/documentation-website/Writerside/topics/Breaking-Changes.md @@ -24,6 +24,20 @@ -- Starting from version 0.57.0 INSERT INTO TEST DEFAULT VALUES ``` + +* The fields `defaultValueFun`, `dbDefaultValue`, and `isDatabaseGenerated` have been consolidated into a single field, + `default`, of type `ColumnDefault`. The previous setup with the three separate fields often led to inconsistencies + and confusion regarding their various combinations. + + Here’s how you can migrate the common combinations: + - `defaultValueFun != null && dbDefaultValue != null` (knows as `default()`) becomes `DatabaseColumnDefaultExpressionWithValue` + - `dbDefaultValue != null` (knows as `defaultExpression()`) becomes `DatabaseColumnDefaultExpression` + - `defaultValueFun != null` (knows as `clientDefault()`) becomes `ClientColumnDefaultValue` + - `isDatabaseGenerated == true` (knows as `databaseGenerated`) becomes `DatabaseGeneratedColumnDefault` + + If it is anticipated that a column will be transformed using `transform()`, + the default value for that column should implement `TransformableColumnDefault`. However, not all defaults need to + implement this interface; it is primarily necessary for columns with in-memory Kotlin values. ## 0.56.0 * If the `distinct` parameter of `groupConcat()` is set to `true`, when using Oracle or SQL Server, this will now fail early with an diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 26a7a383e4..29a96ac5c1 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -413,6 +413,15 @@ public final class org/jetbrains/exposed/sql/CheckConstraint : org/jetbrains/exp public final class org/jetbrains/exposed/sql/CheckConstraint$Companion { } +public abstract interface class org/jetbrains/exposed/sql/ClientColumnDefault : org/jetbrains/exposed/sql/ColumnDefault { +} + +public final class org/jetbrains/exposed/sql/ClientColumnDefaultValue : org/jetbrains/exposed/sql/ClientColumnDefault, org/jetbrains/exposed/sql/ColumnDefaultValue, org/jetbrains/exposed/sql/TransformableColumnDefault { + public fun (Lkotlin/jvm/functions/Function0;)V + public fun getValue ()Lkotlin/jvm/functions/Function0; + public fun transform (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/ColumnDefault; +} + public final class org/jetbrains/exposed/sql/Coalesce : org/jetbrains/exposed/sql/Function { public fun (Lorg/jetbrains/exposed/sql/ExpressionWithColumnType;Lorg/jetbrains/exposed/sql/Expression;[Lorg/jetbrains/exposed/sql/Expression;)V public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V @@ -423,32 +432,54 @@ public final class org/jetbrains/exposed/sql/Column : org/jetbrains/exposed/sql/ public synthetic fun compareTo (Ljava/lang/Object;)I public fun compareTo (Lorg/jetbrains/exposed/sql/Column;)I public fun createStatement ()Ljava/util/List; - public final fun defaultValueInDb ()Lorg/jetbrains/exposed/sql/Expression; public final fun descriptionDdl (Z)Ljava/lang/String; public static synthetic fun descriptionDdl$default (Lorg/jetbrains/exposed/sql/Column;ZILjava/lang/Object;)Ljava/lang/String; public fun dropStatement ()Ljava/util/List; public fun equals (Ljava/lang/Object;)Z public fun getColumnType ()Lorg/jetbrains/exposed/sql/IColumnType; public fun getDdl ()Ljava/util/List; - public final fun getDefaultValueFun ()Lkotlin/jvm/functions/Function0; + public final fun getDefault ()Lorg/jetbrains/exposed/sql/ColumnDefault; public final fun getForeignKey ()Lorg/jetbrains/exposed/sql/ForeignKeyConstraint; public final fun getName ()Ljava/lang/String; public final fun getReferee ()Lorg/jetbrains/exposed/sql/Column; public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; public fun hashCode ()I - public final fun isDatabaseGenerated ()Z public fun modifyStatement ()Ljava/util/List; public final fun modifyStatements (Lorg/jetbrains/exposed/sql/ColumnDiff;)Ljava/util/List; public final fun nameInDatabaseCase ()Ljava/lang/String; public final fun nameUnquoted ()Ljava/lang/String; public final fun referee ()Lorg/jetbrains/exposed/sql/Column; - public final fun setDefaultValueFun (Lkotlin/jvm/functions/Function0;)V + public final fun setDefault (Lorg/jetbrains/exposed/sql/ColumnDefault;)V public final fun setForeignKey (Lorg/jetbrains/exposed/sql/ForeignKeyConstraint;)V public fun toQueryBuilder (Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun toString ()Ljava/lang/String; public final fun withColumnType (Lorg/jetbrains/exposed/sql/IColumnType;)Lorg/jetbrains/exposed/sql/Column; } +public abstract interface class org/jetbrains/exposed/sql/ColumnDefault { +} + +public abstract interface class org/jetbrains/exposed/sql/ColumnDefaultExpression : org/jetbrains/exposed/sql/ColumnDefault { + public abstract fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/ColumnDefaultKt { + public static final fun clientDefaultValue (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/Object; + public static final fun clientDefaultValueOrExpression (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/Object; + public static final fun databaseDefaultExpression (Lorg/jetbrains/exposed/sql/Column;)Lorg/jetbrains/exposed/sql/Expression; + public static final fun defaultValue (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/Object; + public static final fun hasClientDefault (Lorg/jetbrains/exposed/sql/Column;)Z + public static final fun hasClientDefaultValue (Lorg/jetbrains/exposed/sql/Column;)Z + public static final fun hasDatabaseDefault (Lorg/jetbrains/exposed/sql/Column;)Z + public static final fun hasDefaultValue (Lorg/jetbrains/exposed/sql/Column;)Z + public static final fun isDefaultable (Lorg/jetbrains/exposed/sql/Column;)Z + public static final fun transform (Lorg/jetbrains/exposed/sql/ColumnDefault;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/ColumnDefault; +} + +public abstract interface class org/jetbrains/exposed/sql/ColumnDefaultValue : org/jetbrains/exposed/sql/ColumnDefault { + public abstract fun getValue ()Lkotlin/jvm/functions/Function0; +} + public final class org/jetbrains/exposed/sql/ColumnDiff { public static final field Companion Lorg/jetbrains/exposed/sql/ColumnDiff$Companion; public fun (ZZZZZ)V @@ -688,6 +719,21 @@ public final class org/jetbrains/exposed/sql/Database$Companion { public final fun registerJdbcDriver (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V } +public abstract interface class org/jetbrains/exposed/sql/DatabaseColumnDefault : org/jetbrains/exposed/sql/ColumnDefault { +} + +public final class org/jetbrains/exposed/sql/DatabaseColumnDefaultExpression : org/jetbrains/exposed/sql/ColumnDefaultExpression, org/jetbrains/exposed/sql/DatabaseColumnDefault { + public fun (Lorg/jetbrains/exposed/sql/Expression;)V + public fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; +} + +public final class org/jetbrains/exposed/sql/DatabaseColumnDefaultExpressionWithValue : org/jetbrains/exposed/sql/ColumnDefaultExpression, org/jetbrains/exposed/sql/ColumnDefaultValue, org/jetbrains/exposed/sql/DatabaseColumnDefault, org/jetbrains/exposed/sql/TransformableColumnDefault { + public fun (Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function0;)V + public fun getExpression ()Lorg/jetbrains/exposed/sql/Expression; + public fun getValue ()Lkotlin/jvm/functions/Function0; + public fun transform (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/ColumnDefault; +} + public final class org/jetbrains/exposed/sql/DatabaseConfig { public static final field Companion Lorg/jetbrains/exposed/sql/DatabaseConfig$Companion; public synthetic fun (Lorg/jetbrains/exposed/sql/SqlLogger;ZLjava/lang/Integer;IIJJIJJZLjava/lang/Long;IZLorg/jetbrains/exposed/sql/vendors/DatabaseDialect;Lorg/jetbrains/exposed/sql/Schema;IZLkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -761,6 +807,10 @@ public final class org/jetbrains/exposed/sql/DatabaseConfig$Companion { public abstract interface class org/jetbrains/exposed/sql/DatabaseConnectionAutoRegistration : kotlin/jvm/functions/Function1 { } +public final class org/jetbrains/exposed/sql/DatabaseGeneratedColumnDefault : org/jetbrains/exposed/sql/DatabaseColumnDefault { + public fun ()V +} + public final class org/jetbrains/exposed/sql/DatabaseKt { public static final fun getName (Lorg/jetbrains/exposed/sql/Database;)Ljava/lang/String; } @@ -2712,6 +2762,10 @@ public class org/jetbrains/exposed/sql/Transaction : org/jetbrains/exposed/sql/U public final class org/jetbrains/exposed/sql/Transaction$Companion { } +public abstract interface class org/jetbrains/exposed/sql/TransformableColumnDefault : org/jetbrains/exposed/sql/ColumnDefault { + public abstract fun transform (Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/ColumnDefault; +} + public final class org/jetbrains/exposed/sql/Trim : org/jetbrains/exposed/sql/Function { public fun (Lorg/jetbrains/exposed/sql/Expression;)V public final fun getExpr ()Lorg/jetbrains/exposed/sql/Expression; @@ -3210,7 +3264,7 @@ public class org/jetbrains/exposed/sql/statements/InsertStatement : org/jetbrain public final fun getResultedValues ()Ljava/util/List; public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; protected fun isColumnValuePreferredFromResultSet (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/Object;)Z - protected final fun isEntityIdClientSideGeneratedUUID (Lorg/jetbrains/exposed/sql/Column;)Z + protected final fun isEntityIdClientSideGeneratedUUID (Lorg/jetbrains/exposed/sql/Column;)Ljava/lang/Boolean; public final fun isIgnore ()Z public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; public fun prepared (Lorg/jetbrains/exposed/sql/Transaction;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt index 84cb9cbdd9..7a73b25c50 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/dao/id/IdTable.kt @@ -140,7 +140,7 @@ open class CompositeIdTable(name: String = "") : IdTable(name) { } ) return Column(this, "composite_id", EntityIDColumnType(placeholder)).apply { - defaultValueFun = null + default = null } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt index 2b547f808c..c47824855f 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt @@ -18,9 +18,7 @@ class Alias(val delegate: T, val alias: String) : Table() { val tableNameWithAlias: String = "${delegate.tableName} $alias" private fun Column.clone() = Column(this@Alias, name, columnType).also { - it.defaultValueFun = defaultValueFun - it.dbDefaultValue = dbDefaultValue - it.isDatabaseGenerated = isDatabaseGenerated + it.default = default it.foreignKey = foreignKey it.extraDefinitions = extraDefinitions } 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 cdbb2b8025..cc2d4305bf 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 @@ -28,17 +28,10 @@ class Column( @Suppress("UNCHECKED_CAST") fun referee(): Column? = referee as? Column - /** Returns the function that calculates the default value for this column. */ - var defaultValueFun: (() -> T)? = null - internal var dbDefaultValue: Expression? = null - - /** Returns the default value for this column on the database-side. */ - fun defaultValueInDb() = dbDefaultValue - - internal var isDatabaseGenerated: Boolean = false - - /** Returns whether this column's value will be generated in the database. */ - fun isDatabaseGenerated() = isDatabaseGenerated + /** + * The default value for this column. + */ + var default: ColumnDefault? = null internal var extraDefinitions = mutableListOf() @@ -121,12 +114,13 @@ class Column( else -> append(columnType.sqlType()) } - val defaultValue = dbDefaultValue - if (defaultValue != null) { - val expressionSQL = currentDialect.dataTypeProvider.processForDefaultValue(defaultValue) - if (!currentDialect.isAllowedAsColumnDefault(defaultValue)) { + val defaultDatabaseValue = databaseDefaultExpression() + + if (defaultDatabaseValue != null) { + val expressionSQL = currentDialect.dataTypeProvider.processForDefaultValue(defaultDatabaseValue) + if (!currentDialect.isAllowedAsColumnDefault(defaultDatabaseValue)) { val clientDefault = when { - defaultValueFun != null && dbDefaultValue == null -> " Expression will be evaluated on the client." + hasClientDefault() -> " Expression will be evaluated on the client." !columnType.nullable -> " Column will be created with NULL marker." else -> "" } @@ -148,7 +142,7 @@ class Column( append(extraDefinitions.joinToString(separator = " ", prefix = " ") { "$it" }) } - if (columnType.nullable || (defaultValue != null && defaultValueFun == null && !currentDialect.isAllowedAsColumnDefault(defaultValue))) { + if (columnType.nullable || (defaultDatabaseValue != null && !currentDialect.isAllowedAsColumnDefault(defaultDatabaseValue))) { append(" NULL") } else if (!isPKColumn || (currentDialect is SQLiteDialect && !isSQLiteAutoIncColumn)) { append(" NOT NULL") @@ -163,14 +157,10 @@ class Column( val newColumn: Column = Column(table, name, columnType) newColumn.foreignKey = foreignKey @Suppress("UNCHECKED_CAST") - newColumn.dbDefaultValue = dbDefaultValue as Expression? - newColumn.isDatabaseGenerated = isDatabaseGenerated + newColumn.default = default as ColumnDefault? newColumn.extraDefinitions = extraDefinitions body?.let { newColumn.it() } - if (defaultValueFun != null) { - require(newColumn.defaultValueFun != null) { "defaultValueFun was lost on cloning the column" } - } return newColumn } @@ -183,9 +173,7 @@ class Column( columnType = columnType ).also { it.foreignKey = this.foreignKey - it.defaultValueFun = this.defaultValueFun - it.dbDefaultValue = this.dbDefaultValue - it.isDatabaseGenerated = this.isDatabaseGenerated + it.default = this.default it.extraDefinitions = this.extraDefinitions } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnDefault.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnDefault.kt new file mode 100644 index 0000000000..d3459285cf --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnDefault.kt @@ -0,0 +1,163 @@ +package org.jetbrains.exposed.sql + +/** + * Represents a default value for a column. + * + * Commonly it should be implementation of [ClientColumnDefault] or [DatabaseColumnDefault] interfaces. + */ +interface ColumnDefault + +/** + * Represents a client-side default value for a column. + * + * The column with [ClientColumnDefault] default value will not affect DDL, + * and will be automatically added to all the insert/upsert statements + */ +interface ClientColumnDefault : ColumnDefault + +/** + * Represents a database-side default value for a column. + * + * The [DatabaseColumnDefault] default value will be used for DDL generation, + * and will not be added to insert/upsert statements automatically. + */ +interface DatabaseColumnDefault : ColumnDefault + +/** + * Represents a default value for a column generated by an SQL expression. + */ +interface ColumnDefaultExpression : ColumnDefault { + val expression: Expression +} + +/** + * Represents a default value for a column generated by a function. + */ +interface ColumnDefaultValue : ColumnDefault { + val value: () -> T +} + +/** + * A column default value that can be transformed. + */ +interface TransformableColumnDefault : ColumnDefault { + /** + * Creates new default value based on the transformation function + * + * @param fn The transformation function. + * @return The transformed column default value. + */ + fun transform(fn: (Unwrapped) -> Wrapped): ColumnDefault +} + +/** + * Represents a client-side default value for a column, implemented as a function. + */ +class ClientColumnDefaultValue(override val value: () -> T) : + ColumnDefaultValue, ClientColumnDefault, TransformableColumnDefault { + + override fun transform(fn: (T) -> Wrapped): ColumnDefault = + ClientColumnDefaultValue { fn(value()) } +} + +/** + * Represents a database-side generated column default value. + */ +class DatabaseGeneratedColumnDefault : DatabaseColumnDefault + +/** + * Represents a database-side column default value generated by an SQL expression. + */ +class DatabaseColumnDefaultExpression(override val expression: Expression) : + ColumnDefaultExpression, DatabaseColumnDefault + +/** + * Represents a database-side column default value generated by expression + * that also contains in-memory kotlin value. + * + * It's still database default value, so Exposed will expect that the value will be + * generated on the database side. But DAO layer could use in-memory value to + * give the user possibility to access data before actual commit. + */ +class DatabaseColumnDefaultExpressionWithValue( + override val expression: Expression, + override val value: () -> T +) : ColumnDefaultValue, ColumnDefaultExpression, DatabaseColumnDefault, TransformableColumnDefault { + + @Suppress("UNCHECKED_CAST") + override fun transform(fn: (T) -> Wrapped): ColumnDefault = + DatabaseColumnDefaultExpressionWithValue(expression as Expression) { fn(value()) } +} + +/** + * Checks if the column has a database-side default value. + */ +fun Column<*>.hasDatabaseDefault() = default is DatabaseColumnDefault<*> + +/** + * Checks if the column has a client-side default value. + */ +fun Column<*>.hasClientDefault() = default is ClientColumnDefault<*> + +/** + * Checks if the column has a default value. + */ +fun Column<*>.hasDefaultValue() = default is ColumnDefaultValue<*> + +/** + * Gets the default value of the column if it exists. + * + * @return The default value, or null if none exists. + */ +fun Column.defaultValue() = (default as? ColumnDefaultValue)?.value?.invoke() + +/** + * Checks if the column has a client-side default value. + */ +fun Column<*>.hasClientDefaultValue() = default is ClientColumnDefaultValue<*> + +/** + * Gets the client-side default value of the column if it exists. + * + * @return The client-side default value, or null if none exists. + */ +fun Column.clientDefaultValue() = (default as? ClientColumnDefaultValue)?.value?.invoke() + +/** + * Checks if the column is defaultable. It should have either default value or be nullable. + */ +fun Column<*>.isDefaultable() = columnType.nullable || default != null + +/** + * Gets the database-side default SQL expression if it exists. + * + * @return The SQL expression, or null if none exists. + */ +fun Column.databaseDefaultExpression() = if (default is DatabaseColumnDefault) (default as? ColumnDefaultExpression)?.expression else null + +/** + * Gets the client-side default value or expression if it exists. + * + * @return The client-side default value or SQL expression, or null if none exists. + */ +fun Column<*>.clientDefaultValueOrExpression() = (default as? ClientColumnDefault)?.let { + when (it) { + is ColumnDefaultValue<*> -> it.value() + is ColumnDefaultExpression<*> -> it.expression + else -> null + } +} + +/** + * Transforms the column default value using the provided function. + * + * @param fn The transformation function. + * @return The transformed column default value. + */ +fun ColumnDefault.transform(fn: (Unwrapped) -> Wrapped): ColumnDefault { + @Suppress("UNCHECKED_CAST") + return when (this) { + is TransformableColumnDefault -> transform(fn) + else -> this as ColumnDefault + } +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Expression.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Expression.kt index fa1f1deb51..4505e14e4b 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Expression.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Expression.kt @@ -70,7 +70,7 @@ class QueryBuilder( fun registerArgument(column: Column<*>, argument: T) { when (argument) { is Expression<*> -> append(argument) - DefaultValueMarker -> append(TransactionManager.current().db.dialect.dataTypeProvider.processForDefaultValue(column.dbDefaultValue!!)) + DefaultValueMarker -> append(TransactionManager.current().db.dialect.dataTypeProvider.processForDefaultValue(column.databaseDefaultExpression()!!)) else -> registerArgument(column.columnType, argument) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt index e2c38aca4c..0fc4b7a1b5 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ResultRow.kt @@ -72,7 +72,7 @@ class ResultRow( val rawValue = getRaw(expression) if (checkNullability) { - if (rawValue == null && expression is Column<*> && expression.dbDefaultValue != null && !expression.columnType.nullable) { + if (rawValue == null && expression is Column<*> && expression.hasDatabaseDefault() && !expression.columnType.nullable) { exposedLogger.warn( "Column ${TransactionManager.current().fullIdentity(expression)} is marked as not null, " + "has default db value, but returns null. Possible have to re-read it from DB." @@ -172,11 +172,11 @@ class ResultRow( private fun Column.defaultValueOrNotInitialized(): Any? { return when { - defaultValueFun != null -> when { + hasClientDefault() -> when { columnType is ColumnWithTransform<*, *> -> { - (columnType as ColumnWithTransform).unwrapRecursive(defaultValueFun!!()) + (columnType as ColumnWithTransform).unwrapRecursive(clientDefaultValue()) } - else -> defaultValueFun!!() + else -> clientDefaultValue() } columnType.nullable -> null else -> NotInitializedValue diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt index a4d838bd1b..35e218610e 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/SchemaUtils.kt @@ -358,7 +358,7 @@ object SchemaUtils { val dataTypeProvider = currentDialect.dataTypeProvider val redoColumns = existingTableColumns.mapValues { (col, existingCol) -> val columnType = col.columnType - val colNullable = if (col.dbDefaultValue?.let { currentDialect.isAllowedAsColumnDefault(it) } == false) { + val colNullable = if (col.databaseDefaultExpression()?.let { currentDialect.isAllowedAsColumnDefault(it) } == false) { true // Treat a disallowed default value as null because that is what Exposed does with it } else { columnType.nullable @@ -411,8 +411,10 @@ object SchemaUtils { */ private fun isIncorrectDefault(dataTypeProvider: DataTypeProvider, columnMeta: ColumnMetadata, column: Column<*>): Boolean { val isExistingColumnDefaultNull = columnMeta.defaultDbValue == null - val isDefinedColumnDefaultNull = column.dbDefaultValue?.takeIf { currentDialect.isAllowedAsColumnDefault(it) } == null || - (column.dbDefaultValue is LiteralOp<*> && (column.dbDefaultValue as? LiteralOp<*>)?.value == null) + val dbDefaultValue = column.databaseDefaultExpression() + + val isDefinedColumnDefaultNull = dbDefaultValue?.takeIf { currentDialect.isAllowedAsColumnDefault(it) } == null || + (dbDefaultValue is LiteralOp<*> && (dbDefaultValue as? LiteralOp<*>)?.value == null) return when { // Both values are null-like, no DDL update is needed @@ -421,7 +423,7 @@ object SchemaUtils { isExistingColumnDefaultNull != isDefinedColumnDefaultNull -> true else -> { - val columnDefaultValue = column.dbDefaultValue?.let { + val columnDefaultValue = dbDefaultValue?.let { dataTypeProvider.dbDefaultToString(column, it) } columnMeta.defaultDbValue != columnDefaultValue 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 11171dac14..ecfbdddf0a 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 @@ -654,9 +654,8 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** Converts the @receiver column to an [EntityID] column. */ @Suppress("UNCHECKED_CAST") fun Column.entityId(): Column> { - val newColumn = Column>(table, name, EntityIDColumnType(this)).also { - it.defaultValueFun = defaultValueFun?.let { { EntityIDFunctionProvider.createEntityID(it(), table as IdTable) } } - it.dbDefaultValue = dbDefaultValue?.let { default -> default as Expression> } + val newColumn = Column(table, name, EntityIDColumnType(this)).also { + it.default = default?.transform { EntityIDFunctionProvider.createEntityID(it, table as IdTable) } it.extraDefinitions = extraDefinitions } (table as IdTable).addIdColumnInternal(newColumn) @@ -1023,8 +1022,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** Sets the default value for this column in the database side. */ fun Column.default(defaultValue: T): Column = apply { - dbDefaultValue = with(SqlExpressionBuilder) { asLiteral(defaultValue) } - defaultValueFun = { defaultValue } + default = DatabaseColumnDefaultExpressionWithValue(with(SqlExpressionBuilder) { asLiteral(defaultValue) }) { defaultValue } } /** Sets the default value for this column in the database side. */ @@ -1038,14 +1036,12 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** Sets the default value for this column in the database side. */ fun Column.defaultExpression(defaultValue: Expression): Column = apply { - dbDefaultValue = defaultValue - defaultValueFun = null + default = DatabaseColumnDefaultExpression(defaultValue) } /** Sets the default value for this column in the client side. */ fun Column.clientDefault(defaultValue: () -> T): Column = apply { - dbDefaultValue = null - defaultValueFun = defaultValue + default = ClientColumnDefaultValue(defaultValue) } /** @@ -1056,7 +1052,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * by using GENERATED ALWAYS AS via [Column.withDefinition], for example. */ fun Column.databaseGenerated(): Column = apply { - isDatabaseGenerated = true + default = DatabaseGeneratedColumnDefault() } /** UUID column will auto generate its value on a client side just before an insert. */ @@ -1290,10 +1286,8 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { fun Column.nullable(): Column { val newColumn = Column(table, name, columnType) newColumn.foreignKey = foreignKey - newColumn.defaultValueFun = defaultValueFun @Suppress("UNCHECKED_CAST") - newColumn.dbDefaultValue = dbDefaultValue as Expression? - newColumn.isDatabaseGenerated = isDatabaseGenerated + newColumn.default = default?.let { it as ColumnDefault } newColumn.columnType.nullable = true newColumn.extraDefinitions = extraDefinitions return replaceColumn(this, newColumn) @@ -1368,7 +1362,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { transformer: ColumnTransformer ): Column { val newColumn = copyWithAnotherColumnType(ColumnWithTransform(this.columnType, transformer)) { - defaultValueFun = this@transform.defaultValueFun?.let { { transformer.wrap(it()) } } + default = this@transform.default?.transform(transformer::wrap) } return replaceColumn(this, newColumn) } @@ -1423,7 +1417,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { transformer: ColumnTransformer ): Column { val newColumn = copyWithAnotherColumnType(NullableColumnWithTransform(this.columnType, transformer)) { - defaultValueFun = this@transform.defaultValueFun?.let { { it()?.let { value -> transformer.wrap(value) } } } + default = this@transform.default?.transform(transformer::wrap) } return replaceColumn(this, newColumn) } @@ -1468,7 +1462,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { transformer: ColumnTransformer ): Column { val newColumn = copyWithAnotherColumnType(NullableColumnWithTransform(this.columnType, transformer)) { - defaultValueFun = this@nullTransform.defaultValueFun?.let { { it().let { value -> transformer.wrap(value) } } } + default = this@nullTransform.default?.transform(transformer::wrap) } return replaceColumn(this, newColumn) } 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 d7abc30399..0833cea29c 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 @@ -19,8 +19,6 @@ abstract class BaseBatchInsertStatement( internal val data = ArrayList, Any?>>() - 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()) { val fullIdentity = TransactionManager.current().fullIdentity(column) @@ -67,7 +65,7 @@ abstract class BaseBatchInsertStatement( ) } val requiredInTargets = (targets.flatMap { it.columns } - values.keys).filter { - !it.isDefaultable() && !it.columnType.isAutoInc && it.dbDefaultValue == null && it.columnType !is EntityIDColumnType<*> + !it.isDefaultable() && !it.columnType.isAutoInc && !it.hasDatabaseDefault() && it.columnType !is EntityIDColumnType<*> } if (requiredInTargets.any()) { val columnList = requiredInTargets.joinToString { tr.fullIdentity(it) } @@ -91,7 +89,7 @@ abstract class BaseBatchInsertStatement( columnsToInsert.map { column -> column to when { values.contains(column) -> values[column] - column.dbDefaultValue != null || column.isDatabaseGenerated -> DefaultValueMarker + column.hasDatabaseDefault() -> DefaultValueMarker else -> { require(column.columnType.nullable) { "The value for the column ${column.name} was not provided. " + diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt index 9eeed48353..e3f9e6a6fb 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/InsertStatement.kt @@ -74,9 +74,8 @@ open class InsertStatement( return table.columns .filter { column -> !exceptColumns.contains(column) } .mapNotNull { column -> - val defaultFn = column.defaultValueFun when { - defaultFn != null -> column to defaultFn() + column.hasDefaultValue() -> column to column.defaultValue() column.columnType.nullable -> column to null else -> null } @@ -172,9 +171,9 @@ open class InsertStatement( val result = values.toMutableMap() targets.forEach { table -> table.columns.forEach { column -> - if ((column.dbDefaultValue != null || column.defaultValueFun != null) && column !in values.keys) { + if (column.default != null && column !in values.keys) { val value = when { - column.defaultValueFun != null -> column.defaultValueFun!!() + column.hasClientDefault() -> column.clientDefaultValueOrExpression() else -> DefaultValueMarker } result[column] = value @@ -194,13 +193,12 @@ open class InsertStatement( } protected fun clientDefaultColumns() = targets - // The current check for existing client side without db side default value - .flatMap { it.columns.filter { column -> column.dbDefaultValue == null && column.defaultValueFun != null } } + .flatMap { it.columns.filter { column -> column.hasClientDefault() } } protected fun valuesAndClientDefaults(values: Map, Any?> = this.values): Map, Any?> { val clientDefaultValues = clientDefaultColumns() .filter { column -> column !in values.keys } - .map { column -> column to column.defaultValueFun!!() } + .map { column -> column to column.clientDefaultValueOrExpression() } return clientDefaultValues.toMap() + values } @@ -295,13 +293,13 @@ open class InsertStatement( (column.columnType as? EntityIDColumnType<*>) ?.idColumn ?.takeIf { it.columnType is UUIDColumnType } - ?.defaultValueFun != null + ?.hasClientDefault() /** * Returns the list of columns with default values that can not be taken locally. * It is the columns defined with `defaultExpression()`, `databaseGenerated()` */ - private fun columnsWithDatabaseDefaults() = targets.flatMap { it.columns }.filter { it.defaultValueFun == null && it.dbDefaultValue != null } + private fun columnsWithDatabaseDefaults() = targets.flatMap { it.columns }.filter { it.hasDatabaseDefault() } /** * Returns all the columns for which value can not be derived without actual request. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index 22b3ad0876..0bb0471966 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -19,6 +19,7 @@ internal object PostgreSQLDataTypeProvider : DataTypeProvider() { exposedLogger.warn("The length of the binary column is not required.") return binaryType() } + override fun blobType(): String = "bytea" override fun uuidToDB(value: UUID): Any = value override fun dateTimeType(): String = "TIMESTAMP" @@ -422,7 +423,7 @@ open class PostgreSQLDialect(override val name: String = dialectName) : VendorDi append("NOT NULL") } if (columnDiff.defaults) { - column.dbDefaultValue?.let { + column.databaseDefaultExpression()?.let { append(", ALTER COLUMN $colName SET DEFAULT ${PostgreSQLDataTypeProvider.processForDefaultValue(it)}") } ?: run { append(", ALTER COLUMN $colName DROP DEFAULT") diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index 26067ada78..fbb98258c1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -376,11 +376,11 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid } if (columnDiff.nullability) { - val defaultValue = column.dbDefaultValue + val defaultValue = column.databaseDefaultExpression() val isPKColumn = column.table.primaryKey?.columns?.contains(column) == true if (column.columnType.nullable || - (defaultValue != null && column.defaultValueFun == null && !currentDialect.isAllowedAsColumnDefault(defaultValue)) + (defaultValue != null && !currentDialect.isAllowedAsColumnDefault(defaultValue)) ) { append(" NULL") } else if (!isPKColumn) { @@ -399,7 +399,7 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid statements.add( buildString { - column.dbDefaultValue?.let { + column.databaseDefaultExpression()?.let { append(alterTablePart + dropConstraint) append("; ") append( diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt index b03747a2d4..a1d601a6a4 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/Entity.kt @@ -273,7 +273,7 @@ open class Entity(val id: EntityID) { @Suppress("UNCHECKED_CAST", "USELESS_CAST") fun Column.lookup(): T = when { writeValues.containsKey(this as Column) -> writeValues[this as Column] as T - id._value == null && _readValues?.hasValue(this)?.not() ?: true -> defaultValueFun?.invoke() as T + id._value == null && _readValues?.hasValue(this)?.not() ?: true -> defaultValue() as T columnType.nullable -> readValues[this] else -> readValues[this]!! } @@ -443,7 +443,7 @@ open class Entity(val id: EntityID) { private fun writeCompositeIdColumnValue(table: IdTable<*>, id: CompositeID) { table.idColumns.forEach { column -> val wrappedIdColumnType = (column.columnType as EntityIDColumnType<*>).idColumn.columnType - if (wrappedIdColumnType !is AutoIncColumnType<*> && column.defaultValueFun == null && column !in id) { + if (wrappedIdColumnType !is AutoIncColumnType<*> && !column.hasClientDefault() && column !in id) { error("Required column $column is not set to composite id") } if (column in id) { // so we skip autoincrement columns and autogenerated columns diff --git a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt index f5a53cbd4b..865f817d6d 100644 --- a/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt +++ b/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt @@ -368,8 +368,8 @@ abstract class EntityClass>( * @sample org.jetbrains.exposed.sql.tests.shared.entities.EntityTests.testNewIdWithGet */ open fun new(id: ID?, init: T.() -> Unit): T { - val entityId = if (id == null && table.id.defaultValueFun != null) { - table.id.defaultValueFun!!() + val entityId = if (id == null && table.id.hasDefaultValue()) { + table.id.defaultValue()!! } else { DaoEntityID(id, table) } @@ -391,7 +391,7 @@ abstract class EntityClass>( val readValues = prototype._readValues!! val writeValues = prototype.writeValues table.columns.filter { col -> - col.defaultValueFun != null && col !in writeValues && readValues.hasValue(col) + col.hasClientDefaultValue() && col !in writeValues && readValues.hasValue(col) }.forEach { col -> writeValues[col as Column] = readValues[col] } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt index 91e8f4875d..c2d60fbdf9 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/AliasesTests.kt @@ -183,8 +183,8 @@ class AliasesTests : DatabaseTestsBase() { val aliasTester = tester.alias("alias_tester") - val default = tester.columns.find { it.name == "text" }?.defaultValueFun - val aliasDefault = aliasTester.columns.find { it.name == "text" }?.defaultValueFun + val default = tester.columns.find { it.name == "text" }?.default + val aliasDefault = aliasTester.columns.find { it.name == "text" }?.default assertEquals(default, aliasDefault) } @@ -199,8 +199,8 @@ class AliasesTests : DatabaseTestsBase() { val testerAlias = tester.alias("alias_tester") - val default = tester.columns.find { it.name == "text" }?.defaultValueInDb() - val aliasDefault = testerAlias.columns.find { it.name == "text" }?.defaultValueInDb() + val default = tester.columns.find { it.name == "text" }?.databaseDefaultExpression() + val aliasDefault = testerAlias.columns.find { it.name == "text" }?.databaseDefaultExpression() assertEquals(default, aliasDefault) } @@ -213,8 +213,8 @@ class AliasesTests : DatabaseTestsBase() { val testerAlias = tester.alias("alias_tester") - val default = tester.columns.find { it.name == "text" }?.isDatabaseGenerated() - val aliasDefault = testerAlias.columns.find { it.name == "text" }?.isDatabaseGenerated() + val default = tester.columns.find { it.name == "text" }?.hasDatabaseDefault() + val aliasDefault = testerAlias.columns.find { it.name == "text" }?.hasDatabaseDefault() assertEquals(default, aliasDefault) }