From 1b2441811b141474323259539662b90fc3e1581c Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Sat, 19 Jun 2021 15:39:42 +0300 Subject: [PATCH 1/7] Update .editorconfig so IDEs configure indent and trimwhitespace appropriately --- .editorconfig | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.editorconfig b/.editorconfig index c0b5eb66ca..89ffa5ead1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,21 @@ +root = true + +[*] +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +indent_style = space +max_line_length = 300 + +[{*.sh,gradlew}] +end_of_line = lf + +[{*.bat,*.cmd}] +end_of_line = crlf + +[*.md] +# Trailing space means "paragraph continues" in markdown +trim_trailing_whitespace = false + [*.{kt,kts}] disabled_rules=filename,no-wildcard-imports From c2df46de0b70ea027237e2fc8628993ca358f7bb Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Sat, 19 Jun 2021 15:40:20 +0300 Subject: [PATCH 2/7] Add .gitattributes to avoid LF vs CRLF in merge conflicts --- .gitattributes | 10 +++ gradlew.bat | 178 ++++++++++++++++++++++++------------------------- 2 files changed, 99 insertions(+), 89 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..266ebaa13a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto +*.java text +*.md text +*.sql text +*.sh text eol=lf +*.xml text +*.yaml text +*.yml text +*.bat text eol=crlf +/docs/ChangeLog.md merge=union diff --git a/gradlew.bat b/gradlew.bat index ac1b06f938..107acd32c4 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 2c616ec9842231fc84bd09171d85889b8c9e634c Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Sat, 19 Jun 2021 15:51:55 +0300 Subject: [PATCH 3/7] Add EntityClass#testCache(id: ID) fixes #1276 --- .../org/jetbrains/exposed/dao/EntityClass.kt | 37 +++++++++++++++++++ .../sql/tests/shared/entities/EntityTests.kt | 4 ++ 2 files changed, 41 insertions(+) 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 1dabc600ad..174190139e 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 @@ -17,8 +17,22 @@ abstract class EntityClass, out T : Entity>(val table: I internal val klass: Class<*> = entityType ?: javaClass.enclosingClass as Class private val ctor = klass.kotlin.primaryConstructor!! + /** + * Get an entity by its [id] or throws [EntityNotFoundException] if entity is not found. + * + * @param id The id of the entity + * + * @return The entity that has this id or [EntityNotFoundException] entity was found. + */ operator fun get(id: EntityID): T = findById(id) ?: throw EntityNotFoundException(id, this) + /** + * Get an entity by its [id] or throws [EntityNotFoundException] if entity is not found. + * + * @param id The id of the entity + * + * @return The entity that has this id or [EntityNotFoundException] entity was found. + */ operator fun get(id: ID): T = get(DaoEntityID(id, table)) protected open fun warmCache(): EntityCache = TransactionManager.current().entityCache @@ -70,8 +84,31 @@ abstract class EntityClass, out T : Entity>(val table: I } } + /** + * Get an entity by its [id] from cache or returns null if cache does not contain it. + * + * @param id The id of the entity + * + * @return The entity that has this id or null if no entity was found in cache. + */ fun testCache(id: EntityID): T? = warmCache().find(this, id) + /** + * Get an entity by its [id] from cache or returns null if cache does not contain it. + * + * @param id The id of the entity + * + * @return The entity that has this id or null if no entity was found in cache. + */ + fun testCache(id: ID): T? = testCache(DaoEntityID(id, table)) + + /** + * Get a sequence of entities matching given [cacheCheckCondition] from the cache. + * + * @param cacheCheckCondition The condition for selecting the entries from the cache. + * + * @return The sequence of entities from the cache matching the given predicate. + */ fun testCache(cacheCheckCondition: T.() -> Boolean): Sequence = warmCache().findAll(this).asSequence().filter { it.cacheCheckCondition() } fun removeFromCache(entity: Entity) { 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 9ad92b5630..60716bfa0f 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 @@ -346,13 +346,17 @@ class EntityTests : DatabaseTestsBase() { withTables(Boards) { val board1 = Board.new { name = "irrelevant" } assertNotNull(Board.testCache(board1.id)) + assertNotNull(Board.testCache(board1.id.value)) board1.delete() assertNull(Board.testCache(board1.id)) + assertNull(Board.testCache(board1.id.value)) val board2 = Board.new { name = "irrelevant" } assertNotNull(Board.testCache(board2.id)) + assertNotNull(Board.testCache(board2.id.value)) Boards.deleteWhere { Boards.id eq board2.id } assertNull(Board.testCache(board2.id)) + assertNull(Board.testCache(board2.id.value)) } } From 24c731a89d42401888a4d5a22b7eb9e884fac91e Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Sat, 19 Jun 2021 17:46:39 +0300 Subject: [PATCH 4/7] Support update { it[column] = nullableValue } Key change is UpdateBuilder#setWithEntityIdValue, which now accepts column: Column?>, value: S? previous signature was column: Column>, value: S? A downside is that "[optionalReferenceColumn] = null" can't decide between "null as ColumnType?" and "null as EntityID?" update { // it[optionalReferenceColumn] = null // <- does not work // Workaround: specify null type explicitly to call the proper overload it[optionalReferenceColumn] = null as EntityID? // The following works as well: // it[optionalReferenceColumn] = null as ColumnType? } fixes #1275 --- .../sql/statements/BatchUpdateStatement.kt | 2 +- .../exposed/sql/statements/UpdateBuilder.kt | 10 ++--- .../exposed/sql/tests/shared/DDLTests.kt | 12 +++++- .../sql/tests/shared/dml/InsertTests.kt | 40 +++++++++++-------- .../sql/tests/shared/entities/EntityTests.kt | 20 ++++++++++ 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt index 8dbcf285bd..3efc91457c 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/BatchUpdateStatement.kt @@ -33,7 +33,7 @@ open class BatchUpdateStatement(val table: IdTable<*>) : UpdateStatement(table, data.add(id to values) } - override fun update(column: Column, value: Expression) = error("Expressions unsupported in batch update") + override fun update(column: Column, value: Expression) = error("Expressions unsupported in batch update") override fun prepareSQL(transaction: Transaction): String = "${super.prepareSQL(transaction)} WHERE ${transaction.identity(table.id)} = ?" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt index 5577b01274..3e21b4d611 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt @@ -24,32 +24,32 @@ abstract class UpdateBuilder(type: StatementType, targets: List) : } @JvmName("setWithEntityIdExpression") - operator fun , E : Expression> set(column: Column, value: E) { + operator fun > set(column: Column?>, value: Expression) { require(!values.containsKey(column)) { "$column is already initialized" } column.columnType.validateValueBeforeUpdate(value) values[column] = value } @JvmName("setWithEntityIdValue") - operator fun , ID : EntityID, E : S?> set(column: Column, value: E) { + operator fun > set(column: Column?>, value: S?) { require(!values.containsKey(column)) { "$column is already initialized" } column.columnType.validateValueBeforeUpdate(value) values[column] = value } - open operator fun > set(column: Column, value: E) = update(column, value) + open operator fun set(column: Column, value: Expression) = update(column, value) open operator fun set(column: CompositeColumn, value: S) { column.getRealColumnsWithValues(value).forEach { (realColumn, itsValue) -> set(realColumn as Column, itsValue) } } - open fun update(column: Column, value: Expression) { + open fun update(column: Column, value: Expression) { require(!values.containsKey(column)) { "$column is already initialized" } column.columnType.validateValueBeforeUpdate(value) values[column] = value } - open fun update(column: Column, value: SqlExpressionBuilder.() -> Expression) { + open fun update(column: Column, value: SqlExpressionBuilder.() -> Expression) { require(!values.containsKey(column)) { "$column is already initialized" } val expression = SqlExpressionBuilder.value() column.columnType.validateValueBeforeUpdate(expression) diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt index f10a7c24b1..6442e644f9 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt @@ -501,7 +501,17 @@ class DDLTests : DatabaseTestsBase() { assertEquals(2L, Table2.selectAll().count()) Table2.update { - it[table1] = null + } + + Table2.update { + // = null can't decide between "null as Int?" and "null as EntityID" + // it[table1] = null + it[table1] = null as Int? + } + Table2.update { + // = null can't decide between "null as Int?" and "null as EntityID?" + // it[table1] = null + it[table1] = null as EntityID? } Table1.deleteAll() diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt index 56ecb66b97..ddbb6515be 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt @@ -6,10 +6,14 @@ import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.UpdateBuilder import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest -import org.jetbrains.exposed.sql.tests.shared.* +import org.jetbrains.exposed.sql.tests.shared.assertEqualLists +import org.jetbrains.exposed.sql.tests.shared.assertEquals +import org.jetbrains.exposed.sql.tests.shared.assertFailAndRollback +import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.junit.Test import java.math.BigDecimal @@ -230,33 +234,37 @@ class InsertTests : DatabaseTestsBase() { val string = varchar("stringCol", 20) } - fun expression(value: String) = stringLiteral(value).trim().substring(2, 4) + fun T.verifyInsert(expectedIntValue: Int?, insertClause: T.(UpdateBuilder) -> Unit) { + fun expression(value: String) = stringLiteral(value).trim().substring(2, 4) - fun verify(value: String) { - val row = tbl.select { tbl.string eq value }.single() - assertEquals(row[tbl.string], value) + deleteAll() + insert { + it[tbl.string] = expression(" _test_ ") + insertClause(it) + } + val rows = selectAll().adjustSlice { slice(tbl.string, tbl.nullableInt) } + .map { mapOf("string" to it[tbl.string], "nullableInt" to it[tbl.nullableInt]) } + assertEquals( + listOf(mapOf("string" to "test", "nullableInt" to expectedIntValue)).toString(), + rows.toString() + ) } withTables(tbl) { - tbl.insert { - it[string] = expression(" _exp1_ ") + tbl.verifyInsert(null) { } - verify("exp1") - - tbl.insert { - it[string] = expression(" _exp2_ ") + tbl.verifyInsert(5) { it[nullableInt] = 5 } - verify("exp2") - - tbl.insert { - it[string] = expression(" _exp3_ ") + tbl.verifyInsert(null) { it[nullableInt] = null } - verify("exp3") + tbl.verifyInsert(null) { + it[nullableInt] = LiteralOp(nullableInt.columnType, null) + } } } 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 60716bfa0f..24a7580aa6 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 @@ -208,6 +208,26 @@ class EntityTests : DatabaseTestsBase() { } } + @Test + fun testOptionalReference() { + withTables(EntityTestsData.XTable, EntityTestsData.YTable) { + val y = EntityTestsData.YEntity.new { } + EntityTestsData.XTable.insert { + it[y1] = y.id + } + EntityTestsData.XTable.insert { + // = null can't decide between "null as String?" and "null as EntityId?" + // it[EntityTestsData.XTable.y1] = null + it[y1] = null as String? + } + EntityTestsData.XTable.insert { + // = null can't decide between "null as String?" and "null as EntityID?" + // it[EntityTestsData.XTable.y1] = null + it[y1] = null as EntityID? + } + } + } + object Boards : IntIdTable(name = "board") { val name = varchar("name", 255).index(isUnique = true) } From 97fc4812b868b0d2a7bd56357bb2e8cd1ccff2a2 Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Sat, 19 Jun 2021 18:26:50 +0300 Subject: [PATCH 5/7] Add UpdateBuilder#setNull(T) to simplify usages like "it[column] = null as Int?" --- .../org/jetbrains/exposed/sql/statements/UpdateBuilder.kt | 7 +++++++ .../jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt | 4 ++++ .../exposed/sql/tests/shared/entities/EntityTests.kt | 3 +++ 3 files changed, 14 insertions(+) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt index 3e21b4d611..8a1e34b2a3 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt @@ -37,6 +37,13 @@ abstract class UpdateBuilder(type: StatementType, targets: List
) : values[column] = value } + /** + * Sets column value to null. + * This method is helpful for "optional references" since compiler can't decide between + * "null as T?" and "null as EntityID?". + */ + fun setNull(column: Column) = set(column, null as T?) + open operator fun set(column: Column, value: Expression) = update(column, value) open operator fun set(column: CompositeColumn, value: S) { diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt index ddbb6515be..192aaa6fac 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt @@ -265,6 +265,10 @@ class InsertTests : DatabaseTestsBase() { tbl.verifyInsert(null) { it[nullableInt] = LiteralOp(nullableInt.columnType, null) } + + tbl.verifyInsert(null) { + it.setNull(nullableInt) + } } } 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 24a7580aa6..ec017d456f 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 @@ -225,6 +225,9 @@ class EntityTests : DatabaseTestsBase() { // it[EntityTestsData.XTable.y1] = null it[y1] = null as EntityID? } + EntityTestsData.XTable.insert { + it.setNull(y1) + } } } From c9c87c3fc046da414b041809c5834659f670ff24 Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Sat, 19 Jun 2021 21:55:37 +0300 Subject: [PATCH 6/7] Improve error reporting when user inserts NULL into non-nullable column * Exception message would include the relevant table and column name * LiteralOp(...) and QueryParameter(...) would fail if user creates NULL value for non-nullable type * Single-value QueryBuilder#registerArgument is slightly faster as it no longer creates list fixes #1268 --- .../org/jetbrains/exposed/sql/ColumnType.kt | 2 +- .../org/jetbrains/exposed/sql/Expression.kt | 70 ++++++++++++++----- .../kotlin/org/jetbrains/exposed/sql/Op.kt | 49 +++++-------- .../jetbrains/exposed/sql/ops/InListOps.kt | 4 +- .../exposed/sql/statements/UpdateBuilder.kt | 45 ++++++------ .../jetbrains/exposed/sql/vendors/Mysql.kt | 2 +- .../sql/tests/shared/dml/InsertTests.kt | 59 ++++++++++++++++ 7 files changed, 156 insertions(+), 75 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt index f3c35ac1f4..ac473dfdd1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ColumnType.kt @@ -46,7 +46,7 @@ interface IColumnType { */ fun valueToString(value: Any?): String = when (value) { null -> { - check(nullable) { "NULL in non-nullable column" } + check(nullable) { "NULL in non-nullable column with type ${sqlType()}" } "NULL" } DefaultValueMarker -> "DEFAULT" 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 ba715c178f..3ab1cb1851 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 @@ -51,34 +51,66 @@ class QueryBuilder( operator fun Expression<*>.unaryPlus(): QueryBuilder = append(this) /** Adds the specified [argument] as a value of the specified [column]. */ - fun registerArgument(column: Column<*>, argument: T) { + fun registerArgument(column: ExpressionWithColumnType<*>, argument: Any?) { when (argument) { - is Expression<*> -> append(argument) - DefaultValueMarker -> append(TransactionManager.current().db.dialect.dataTypeProvider.processForDefaultValue(column.dbDefaultValue!!)) - else -> registerArgument(column.columnType, argument) + is Expression<*> -> { + require(column.columnType.nullable || argument !is LiteralOp<*> || argument.value != null) { + "Column ${column.identity} does not support NULLs, so cannot register null literal $argument" + } + append(argument) + } + DefaultValueMarker -> { + require(column is Column<*>) { + "DefaultValueMarker is supported only for Column<*>, given argument is $column" + } + append(TransactionManager.current().db.dialect.dataTypeProvider.processForDefaultValue(column.dbDefaultValue!!)) + } + else -> { + require(column.columnType.nullable || argument != null) { + "Column ${column.identity} does not support NULLs" + } + @Suppress("DEPRECATION") + registerArgument(column.columnType, argument) + } } } + private val ExpressionWithColumnType<*>.identity: String + get() = if (this is Column<*>) "${table.tableName}.$name" else toString() + /** Adds the specified [argument] as a value of the specified [sqlType]. */ - fun registerArgument(sqlType: IColumnType, argument: T): Unit = registerArguments(sqlType, listOf(argument)) + @Deprecated( + level = DeprecationLevel.WARNING, + message = "Prefer registerArgument(Column, ...) and registerArgument(ExpressionWithColumnType, ...) since they have better error reporting" + ) + @Suppress("DeprecatedCallableAddReplaceWith") + fun registerArgument(sqlType: IColumnType, argument: Any?) { + if (!prepared) { + +sqlType.valueToString(argument) + return + } + require(argument != null || sqlType.nullable) { + "Can't register NULL value for non-nullable type $sqlType" + } + +"?" + _args += sqlType to argument + } /** Adds the specified sequence of [arguments] as values of the specified [sqlType]. */ + @Deprecated( + message = "Replace with [SingleValueInListOp]", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("org.jetbrains.exposed.sql.ops.SingleValueInListOp") + ) fun registerArguments(sqlType: IColumnType, arguments: Iterable) { - fun toString(value: T) = when { - prepared && value is String -> value - else -> sqlType.valueToString(value) + if (!prepared) { + arguments.appendTo { +sqlType.valueToString(it) } + return + } + arguments.appendTo { + +"?" + _args += sqlType to it } - - arguments.map { it to toString(it) } - .sortedBy { it.second } - .appendTo { - if (prepared) { - _args.add(sqlType to it.first) - append("?") - } else { - append(it.second) - } - } } override fun toString(): String = internalBuilder.toString() diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt index 0972fc4c61..3279dbb428 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt @@ -1,6 +1,7 @@ package org.jetbrains.exposed.sql import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.ops.SingleValueInListOp import org.jetbrains.exposed.sql.vendors.OracleDialect import org.jetbrains.exposed.sql.vendors.SQLServerDialect import org.jetbrains.exposed.sql.vendors.currentDialect @@ -408,35 +409,10 @@ class InListOrNotInListOp( /** Returns `true` if the check is inverted, `false` otherwise. */ val isInList: Boolean = true ) : Op(), ComplexExpression { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { - list.iterator().let { i -> - if (!i.hasNext()) { - if (isInList) { - +FALSE - } else { - +TRUE - } - } else { - val first = i.next() - if (!i.hasNext()) { - append(expr) - when { - isInList -> append(" = ") - else -> append(" != ") - } - registerArgument(expr.columnType, first) - } else { - append(expr) - when { - isInList -> append(" IN (") - else -> append(" NOT IN (") - } - registerArguments(expr.columnType, list) - append(")") - } - } - } - } + private val impl = SingleValueInListOp(expr, list, isInList) + + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = + impl.toQueryBuilder(queryBuilder) } // Literals @@ -449,6 +425,11 @@ class LiteralOp( /** Returns the value being used as a literal. */ val value: T ) : ExpressionWithColumnType() { + init { + require(value != null || columnType.nullable) { + "Can't create NULL literal for non-nullable type $columnType" + } + } override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { +columnType.valueToString(value) } } @@ -506,7 +487,15 @@ class QueryParameter( /** Returns the column type of this expression. */ val sqlType: IColumnType ) : Expression() { - override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { registerArgument(sqlType, value) } + init { + require(value != null || sqlType.nullable) { + "Can't create NULL query parameter for non-nullable type $sqlType" + } + } + override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { + @Suppress("DEPRECATION") + registerArgument(sqlType, value) + } } /** Returns the specified [value] as a query parameter with the same type as [column]. */ diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/InListOps.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/InListOps.kt index dd2e17c0e5..32af7cf623 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/InListOps.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/ops/InListOps.kt @@ -59,12 +59,12 @@ abstract class InListOrNotInListBaseOp ( private fun QueryBuilder.registerValues(values: List) { val singleColumn = columnTypes.singleOrNull() if (singleColumn != null) - registerArgument(singleColumn.columnType, values.single()) + registerArgument(singleColumn as ExpressionWithColumnType, values.single()) else { append("(") columnTypes.forEachIndexed { index, columnExpression -> if (index != 0) append(", ") - registerArgument(columnExpression.columnType, values[index]) + registerArgument(columnExpression as ExpressionWithColumnType, values[index]) } append(")") } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt index 8a1e34b2a3..fafacb3abb 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/UpdateBuilder.kt @@ -3,7 +3,6 @@ package org.jetbrains.exposed.sql.statements import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.sql.* -import java.util.* /** * @author max @@ -13,28 +12,28 @@ abstract class UpdateBuilder(type: StatementType, targets: List
) : protected val values: MutableMap, Any?> = LinkedHashMap() open operator fun set(column: Column, value: S) { - when { - values.containsKey(column) -> error("$column is already initialized") - !column.columnType.nullable && value == null -> error("Trying to set null to not nullable column $column") - else -> { - column.columnType.validateValueBeforeUpdate(value) - values[column] = value - } + require(column !in values) { "$column is already initialized" } + column.validateValueBeforeUpdate(value) + values[column] = value + } + + private fun Column<*>.validateValueBeforeUpdate(value: Any?) { + require(columnType.nullable || value != null && !(value is LiteralOp<*> && value.value == null)) { + "Can't set NULL into non-nullable column ${table.tableName}.$name, column type is $columnType" } + columnType.validateValueBeforeUpdate(value) } @JvmName("setWithEntityIdExpression") operator fun > set(column: Column?>, value: Expression) { - require(!values.containsKey(column)) { "$column is already initialized" } - column.columnType.validateValueBeforeUpdate(value) - values[column] = value + @Suppress("UNCHECKED_CAST") + set(column as Column, value as Any?) } @JvmName("setWithEntityIdValue") operator fun > set(column: Column?>, value: S?) { - require(!values.containsKey(column)) { "$column is already initialized" } - column.columnType.validateValueBeforeUpdate(value) - values[column] = value + @Suppress("UNCHECKED_CAST") + set(column as Column, value as Any?) } /** @@ -47,19 +46,21 @@ abstract class UpdateBuilder(type: StatementType, targets: List
) : open operator fun set(column: Column, value: Expression) = update(column, value) open operator fun set(column: CompositeColumn, value: S) { - column.getRealColumnsWithValues(value).forEach { (realColumn, itsValue) -> set(realColumn as Column, itsValue) } + column.getRealColumnsWithValues(value).forEach { (realColumn, itsValue) -> + @Suppress("UNCHECKED_CAST") + set(realColumn as Column, itsValue) + } } open fun update(column: Column, value: Expression) { - require(!values.containsKey(column)) { "$column is already initialized" } - column.columnType.validateValueBeforeUpdate(value) - values[column] = value + @Suppress("UNCHECKED_CAST") + set(column as Column, value as Any?) } open fun update(column: Column, value: SqlExpressionBuilder.() -> Expression) { - require(!values.containsKey(column)) { "$column is already initialized" } - val expression = SqlExpressionBuilder.value() - column.columnType.validateValueBeforeUpdate(expression) - values[column] = expression + // Note: the implementation builds value before it verifies if the column is already initialized + // however it makes the implementation easier and the optimization is not that important since + // the exceptional case should be rare. + set(column, SqlExpressionBuilder.value()) } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt index 4d211be92c..9d5c0d94f9 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt @@ -62,7 +62,7 @@ internal open class MysqlFunctionProvider : FunctionProvider() { override fun replace(table: Table, data: List, Any?>>, transaction: Transaction): String { val builder = QueryBuilder(true) val columns = data.joinToString { transaction.identity(it.first) } - val values = builder.apply { data.appendTo { registerArgument(it.first.columnType, it.second) } }.toString() + val values = builder.apply { data.appendTo { registerArgument(it.first, it.second) } }.toString() return "REPLACE INTO ${transaction.identity(table)} ($columns) VALUES ($values)" } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt index 192aaa6fac..b7568396a6 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/InsertTests.kt @@ -17,7 +17,9 @@ import org.jetbrains.exposed.sql.tests.shared.expectException import org.jetbrains.exposed.sql.vendors.MysqlDialect import org.junit.Test import java.math.BigDecimal +import kotlin.test.assertContains import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull class InsertTests : DatabaseTestsBase() { @@ -201,6 +203,63 @@ class InsertTests : DatabaseTestsBase() { } } + @Test + fun testInsertNullIntoNonNullableColumn() { + val cities = object : IntIdTable("cities") { + } + val users = object : IntIdTable("users") { + val cityId = reference("city_id", cities) + } + + withTables(users, cities) { + // This is needed so valid inserts to users to succeed + cities.insert { + it[id] = 42 + } + users.insert { + // The assertion would try inserting null, and it ensures the insert would fail before the statement is even generated + it.assertInsertNullFails(cityId) + // This is needed for insert statement to succeed + it[cityId] = 42 + } + } + } + + private fun > UpdateBuilder.assertInsertNullFails(column: Column>) { + fun assertInsertNullFails(column: Column>, block: () -> Unit) { + val e = assertFailsWith( + """ + Unfortunately, type system can't protect from inserting null here + since the setter is declared as set(column: Column?>, value: S?), + and there's no way to tell that nullness of both arguments should match, so expecting it[${column.name}] = null + to fail at runtime + """.trimIndent() + ) { + block() + } + val message = e.toString() + assertContains( + message, + "${column.table.tableName}.${column.name}", ignoreCase = true, + "Exception message should contain table and column name" + ) + assertContains(message, column.columnType.toString(), ignoreCase = true, "Exception message should contain column type") + } + + require(!column.columnType.nullable) { + "Assertion works for non-nullable columns only. Given column ${column.table.tableName}.${column.name} is nullable ${column.columnType}" + } + assertInsertNullFails(column) { + // This is written explicitly to demonstrate that the code compiles, yet it fails in the runtime + // This call resolves to set(column: Column?>, value: S?) + this[column] = null + } + val nullableType = EntityIDColumnType(column).apply { nullable = true } + assertInsertNullFails(column) { + this[column] = LiteralOp(nullableType, null) + } + } + @Test fun testInsertWithPredefinedId() { val stringTable = object : IdTable("stringTable") { override val id = varchar("id", 15).entityId() From f0029de4bf6a103eae925d70f16ff0d568b7e759 Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Sun, 20 Jun 2021 01:45:51 +0300 Subject: [PATCH 7/7] Refine BiCompositeColumn generics --- .../jetbrains/exposed/sql/CompositeColumn.kt | 13 +++++---- .../org/jetbrains/exposed/sql/ResultRow.kt | 12 +++++++-- .../exposed/sql/money/CompositeMoneyColumn.kt | 16 +++++------ .../sql/money/CompositeMoneyColumnType.kt | 3 +++ .../exposed/sql/money/MoneyDefaultsTest.kt | 27 +++++++++++++++---- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt index 96f07229ba..5390cf3d25 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/CompositeColumn.kt @@ -35,8 +35,8 @@ abstract class CompositeColumn : Expression() { abstract class BiCompositeColumn( protected val column1: Column, protected val column2: Column, - val transformFromValue: (T) -> Pair, - val transformToValue: (Any?, Any?) -> T + val transformToValue: (C1, C2) -> T, + val transformFromValue: (T) -> Pair ) : CompositeColumn() { override fun getRealColumns(): List> = listOf(column1, column2) @@ -50,12 +50,11 @@ abstract class BiCompositeColumn( } override fun restoreValueFromParts(parts: Map, Any?>): T { - val v1 = parts[column1] - val v2 = parts[column2] - val result = transformToValue(v1, v2) - check(result != null || nullable) { + @Suppress("UNCHECKED_CAST") + val result = transformToValue(parts[column1] as C1, parts[column2] as C2) + require(result != null || nullable) { "Null value received from DB for non-nullable ${this::class.simpleName} column" } - return result as T + return result } } 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 f1ad570acb..b4f2fc5b8f 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 @@ -50,17 +50,24 @@ class ResultRow(val fieldIndex: Map, Int>) { return when { raw == null -> null raw == NotInitializedValue -> error("$c is not initialized yet") + c is CompositeColumn -> c.restoreValueFromParts( + (raw as Map, Any?>).mapValues { (column, rawValue) -> + rawToColumnValue(rawValue, column as Column) + } + ) c is ExpressionAlias && c.delegate is ExpressionWithColumnType -> c.delegate.columnType.valueFromDB(raw) c is ExpressionWithColumnType -> c.columnType.valueFromDB(raw) else -> raw } as T } - @Suppress("UNCHECKED_CAST") private fun getRaw(c: Expression): T? { if (c is CompositeColumn) { val rawParts = c.getRealColumns().associateWith { getRaw(it) } - return c.restoreValueFromParts(rawParts) + // Result row for composite is not really defined, and we return Map for now + // See https://github.com/JetBrains/Exposed/issues/1278 + @Suppress("UNCHECKED_CAST") + return rawParts as T? } val index = fieldIndex[c] @@ -70,6 +77,7 @@ class ResultRow(val fieldIndex: Map, Int>) { }?.let { fieldIndex[it] } ?: error("$c is not in record set") + @Suppress("UNCHECKED_CAST") return data[index] as T? } diff --git a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt index ccc7315533..67416d0910 100644 --- a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt +++ b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumn.kt @@ -20,22 +20,18 @@ class CompositeMoneyColumn - val amountValue = money?.number?.numberValue(BigDecimal::class.java) as? T1 - val currencyValue = money?.currency as? T2 + val amountValue = money?.number?.numberValue(BigDecimal::class.java) as T1 + val currencyValue = money?.currency as T2 amountValue to currencyValue }, transformToValue = { amountVal, currencyVal -> if (amountVal == null || currencyVal == null) { null as R } else { - val result = Monetary.getDefaultAmountFactory().setNumber(amountVal as Number) - - when (currencyVal) { - is CurrencyUnit -> result.setCurrency(currencyVal) - is String -> result.setCurrency(currencyVal) - } - - result.create() as R + Monetary.getDefaultAmountFactory().run { + setNumber(amountVal) + setCurrency(currencyVal) + }.create() as R } } ) diff --git a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt index fef945653a..afa765e15e 100644 --- a/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt +++ b/exposed-money/src/main/kotlin/org/jetbrains/exposed/sql/money/CompositeMoneyColumnType.kt @@ -1,6 +1,7 @@ package org.jetbrains.exposed.sql.money import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.CompositeColumn import org.jetbrains.exposed.sql.Table import java.math.BigDecimal import javax.money.CurrencyUnit @@ -23,5 +24,7 @@ fun Table.compositeMoney(amountColumn: Column, currencyColumn: Colu if (amountColumn !in columns && currencyColumn !in columns) { registerCompositeColumn(it) } + // Set CompositeColumn.nullable = true + (it as CompositeColumn).nullable() } } diff --git a/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyDefaultsTest.kt b/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyDefaultsTest.kt index 2682e5f1c8..47c1d225a6 100644 --- a/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyDefaultsTest.kt +++ b/exposed-money/src/test/kotlin/org/jetbrains/exposed/sql/money/MoneyDefaultsTest.kt @@ -22,6 +22,11 @@ class MoneyDefaultsTest : DatabaseTestsBase() { val field = varchar("field", 100) val t1 = compositeMoney(10, 0, "t1").default(defaultValue) val t2 = compositeMoney(10, 0, "t2").nullable() + + val price_amount = decimal("price_amount", 10, 0).nullable() + val price_currency = currency("price_currency").nullable() + val price = compositeMoney(price_amount, price_currency) /* it is implicitly nullable since price_amount is nullable */ + val clientDefault = integer("clientDefault").clientDefault { cIndex++ } } @@ -29,6 +34,7 @@ class MoneyDefaultsTest : DatabaseTestsBase() { var field by TableWithDBDefault.field var t1 by TableWithDBDefault.t1 var t2 by TableWithDBDefault.t2 + var price by TableWithDBDefault.price val clientDefault by TableWithDBDefault.clientDefault override fun hashCode(): Int = id.value.hashCode() @@ -36,11 +42,7 @@ class MoneyDefaultsTest : DatabaseTestsBase() { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is DBDefault) return false - if (other.t1 != other.t1) return false - if (other.t2 != other.t2) return false - if (other.clientDefault != other.clientDefault) return false - - return true + return id.value == other.id.value } companion object : IntEntityClass(TableWithDBDefault) @@ -94,4 +96,19 @@ class MoneyDefaultsTest : DatabaseTestsBase() { assertEquals(TableWithDBDefault.defaultValue, db1.t1) } } + + @Test + fun testImplicitlyNullableCompositeColumnType() { + withTables(TableWithDBDefault) { + TableWithDBDefault.cIndex = 0 + val db1 = DBDefault.new { field = "1" } + flushCache() + assertNull(db1.price, "db1.price should be null since it was not set when calling new") + val money = Money.of(BigDecimal.ONE, "USD") + db1.price = money + db1.refresh(flush = true) + assertEquals(money, db1.t1) + assertEquals(TableWithDBDefault.defaultValue, db1.t1) + } + } }