Skip to content

Commit

Permalink
Review issues: fix binary colum default values and ddl generation
Browse files Browse the repository at this point in the history
  • Loading branch information
obabichevjb committed Jul 3, 2024
1 parent ecc8a94 commit 519f098
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 78 deletions.
1 change: 1 addition & 0 deletions exposed-core/api/exposed-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,7 @@ public final class org/jetbrains/exposed/sql/OpKt {
public static final fun andIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op;
public static final fun andIfNotNull (Lorg/jetbrains/exposed/sql/Op;Lorg/jetbrains/exposed/sql/Expression;)Lorg/jetbrains/exposed/sql/Op;
public static final fun andNot (Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Op;
public static final fun binaryLiteral ([B)Lorg/jetbrains/exposed/sql/LiteralOp;
public static final fun blobParam (Lorg/jetbrains/exposed/sql/statements/api/ExposedBlob;Z)Lorg/jetbrains/exposed/sql/Expression;
public static synthetic fun blobParam$default (Lorg/jetbrains/exposed/sql/statements/api/ExposedBlob;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Expression;
public static final fun booleanLiteral (Z)Lorg/jetbrains/exposed/sql/LiteralOp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,21 @@ open class BasicBinaryColumnType : ColumnType<ByteArray>() {
else -> error("Unexpected value $value of type ${value::class.qualifiedName}")
}

override fun nonNullValueToString(value: ByteArray): String = value.toString(Charsets.UTF_8)
override fun nonNullValueToString(value: ByteArray): String {
return when {
currentDialect is H2Dialect && currentDialect.h2Mode == null -> "X'${value.toHexString().lowercase()}'"
currentDialect is MariaDBDialect -> toTextString(value)
currentDialect is OracleDialect -> "HEXTORAW('${value.toHexString()}')"
currentDialect is SQLServerDialect || currentDialect is MysqlDialect -> "0x${value.toHexString()}"
currentDialect is PostgreSQLDialect -> "'\\x${value.toHexString().lowercase()}'::bytea"
else -> toTextString(value)
}
}

private fun toTextString(value: ByteArray) = TextColumnType().nonNullValueToString(value.toString(Charsets.UTF_8))

@Suppress("MagicNumber")
private fun ByteArray.toHexString(): String = joinToString("") { it.toString(16).uppercase().padStart(2, '0') }
}

/**
Expand Down
3 changes: 3 additions & 0 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,9 @@ fun stringLiteral(value: String): LiteralOp<String> = LiteralOp(TextColumnType()
/** Returns the specified [value] as a decimal literal. */
fun decimalLiteral(value: BigDecimal): LiteralOp<BigDecimal> = LiteralOp(DecimalColumnType(value.precision(), value.scale()), value)

/** Returns the specified [value] as a binary literal. */
fun binaryLiteral(value: ByteArray): LiteralOp<ByteArray> = LiteralOp(BasicBinaryColumnType(), value)

/**
* Returns the specified [value] as an array literal, with elements parsed by the [delegateType] if provided.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ interface ISqlExpressionBuilder {
/** Returns the specified [value] as a literal of type [T]. */
@Suppress("UNCHECKED_CAST", "ComplexMethod")
fun <T, S : T?> ExpressionWithColumnType<S>.asLiteral(value: T): LiteralOp<T> = when {
value is ByteArray && columnType is BasicBinaryColumnType -> stringLiteral(value.toString(Charsets.UTF_8))
value is ByteArray && columnType is BasicBinaryColumnType -> binaryLiteral(value)
else -> LiteralOp(columnType as IColumnType<T & Any>, value)
} as LiteralOp<T>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,17 @@ object SchemaUtils {
else -> processForDefaultValue(exp)
}

is ByteArray -> when {
dialect is MariaDBDialect -> value.toString(Charsets.UTF_8)
dialect is MysqlDialect && !dialect.isMysql8 -> value.toString(Charsets.UTF_8)
dialect is MysqlDialect ||
dialect is SQLServerDialect ||
dialect is OracleDialect ||
dialect is PostgreSQLDialect -> BasicBinaryColumnType().nonNullValueToString(value)
dialect is H2Dialect && dialect.h2Mode == null -> BasicBinaryColumnType().nonNullValueToString(value)
else -> value.toString(Charsets.UTF_8)
}

else -> {
when {
column.columnType is JsonColumnMarker -> {
Expand Down Expand Up @@ -354,7 +365,7 @@ object SchemaUtils {
(column.dbDefaultValue is LiteralOp<*> && (column.dbDefaultValue as? LiteralOp<*>)?.value == null)

return when {
// Bot values are null-like, no DDL update is needed
// Both values are null-like, no DDL update is needed
isExistingColumnDefaultNull && isDefinedColumnDefaultNull -> false
// Only one of the values is null-like, DDL update is needed
isExistingColumnDefaultNull != isDefinedColumnDefaultNull -> true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.jetbrains.exposed.sql.statements.api.ExposedDatabaseMetadata
import org.jetbrains.exposed.sql.statements.api.IdentifierManagerApi
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.*
import org.jetbrains.exposed.sql.vendors.H2Dialect.H2CompatibilityMode
import java.math.BigDecimal
import java.sql.DatabaseMetaData
import java.sql.ResultSet
Expand Down Expand Up @@ -213,14 +214,21 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData)
*/
private fun sanitizedDefault(defaultValue: String): String? {
val dialect = currentDialect
val h2Mode = dialect.h2Mode
return when {
dialect is SQLServerDialect -> defaultValue.trim('(', ')').extractNullAndStringFromDefaultValue()
dialect is MariaDBDialect -> when (defaultValue) {
"NULL" -> null
// Check for MariaDB must be before MySql because MariaDBDialect as a class inherits MysqlDialect
dialect is MariaDBDialect || h2Mode == H2CompatibilityMode.MariaDB -> when {
defaultValue.startsWith("b'") -> defaultValue.substringAfter("b'").trim('\'')
else -> defaultValue.extractNullAndStringFromDefaultValue()
}
// It's the special case, because MySql returns default string "NULL" as string "NULL", but other DBs return it as "'NULL'"
dialect is MysqlDialect && defaultValue == "NULL" -> defaultValue
dialect is MysqlDialect || h2Mode == H2CompatibilityMode.MySQL -> when {
defaultValue.startsWith("b'") -> defaultValue.substringAfter("b'").trim('\'')
else -> defaultValue.extractNullAndStringFromDefaultValue()
}
dialect is SQLServerDialect -> defaultValue.trim('(', ')').extractNullAndStringFromDefaultValue()
dialect is OracleDialect -> defaultValue.trim().extractNullAndStringFromDefaultValue()
dialect is MysqlDialect -> defaultValue.substringAfter("b'").trim('\'')
else -> defaultValue.extractNullAndStringFromDefaultValue()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,75 +186,4 @@ class ColumnDefinitionTests : DatabaseTestsBase() {
result2.close()
}
}

@Test
fun testNoChangesOnCreateMissingNullableColumns() {
val testerWithDefaults = object : Table("tester") {
val defaultNullNumber = integer("default_null_number").nullable().default(null)
val defaultNullWord = varchar("default_null_word", 8).nullable().default(null)
val nullNumber = integer("null_number").nullable()
val nullWord = varchar("null_word", 8).nullable()
val defaultNumber = integer("default_number").default(999).nullable()
val defaultWord = varchar("default_word", 8).default("Hello").nullable()
}

val testerWithoutDefaults = object : Table("tester") {
val defaultNullNumber = integer("default_null_number").nullable()
val defaultNullWord = varchar("default_null_word", 8).nullable()
val nullNumber = integer("null_number").nullable()
val nullWord = varchar("null_word", 8).nullable()
val defaultNumber = integer("default_number").default(999).nullable()
val defaultWord = varchar("default_word", 8).default("Hello").nullable()
}

listOf(
testerWithDefaults to testerWithDefaults,
testerWithDefaults to testerWithoutDefaults,
testerWithoutDefaults to testerWithDefaults,
testerWithoutDefaults to testerWithoutDefaults
).forEach { (existingTable, definedTable) ->
withTables(existingTable) {
SchemaUtils.statementsRequiredToActualizeScheme(definedTable).also {
assertTrue(it.isEmpty())
}
}
}
}

@Test
fun testChangesOnCreateMissingNullableColumns() {
val testerWithDefaults = object : Table("tester") {
val defaultNullString = varchar("default_null_string", 8).nullable().default("NULL")
val defaultNumber = integer("default_number").default(999).nullable()
val defaultWord = varchar("default_word", 8).default("Hello").nullable()
}

val testerWithoutDefaults = object : Table("tester") {
val defaultNullString = varchar("default_null_string", 8).nullable()
val defaultNumber = integer("default_number").nullable()
val defaultWord = varchar("default_word", 8).nullable()
}

listOf(
testerWithDefaults to testerWithoutDefaults,
testerWithoutDefaults to testerWithDefaults,
).forEach { (existingTable, definedTable) ->
withTables(excludeSettings = listOf(TestDB.SQLITE), existingTable) {
SchemaUtils.statementsRequiredToActualizeScheme(definedTable).also {
assertTrue(it.isNotEmpty())
}
}
}

listOf(
testerWithDefaults to testerWithDefaults,
testerWithoutDefaults to testerWithoutDefaults
).forEach { (existingTable, definedTable) ->
withTables(excludeSettings = listOf(TestDB.SQLITE), existingTable) {
SchemaUtils.statementsRequiredToActualizeScheme(definedTable).also {
assertTrue(it.isEmpty())
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -726,4 +726,79 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() {
}
}
}

@Test
fun testNoChangesOnCreateMissingNullableColumns() {
val testerWithDefaults = object : Table("tester") {
val defaultNullNumber = integer("default_null_number").nullable().default(null)
val defaultNullWord = varchar("default_null_word", 8).nullable().default(null)
val nullNumber = integer("null_number").nullable()
val nullWord = varchar("null_word", 8).nullable()
val defaultNumber = integer("default_number").default(999).nullable()
val defaultWord = varchar("default_word", 8).default("Hello").nullable()
val defaultBinary = binary("default_binary", 256).nullable().default(null)
}

val testerWithoutDefaults = object : Table("tester") {
val defaultNullNumber = integer("default_null_number").nullable()
val defaultNullWord = varchar("default_null_word", 8).nullable()
val nullNumber = integer("null_number").nullable()
val nullWord = varchar("null_word", 8).nullable()
val defaultNumber = integer("default_number").default(999).nullable()
val defaultWord = varchar("default_word", 8).default("Hello").nullable()
val defaultBinary = binary("default_binary", 256).nullable()
}

listOf(
testerWithDefaults to testerWithDefaults,
testerWithDefaults to testerWithoutDefaults,
testerWithoutDefaults to testerWithDefaults,
testerWithoutDefaults to testerWithoutDefaults
).forEach { (existingTable, definedTable) ->
withTables(existingTable) {
SchemaUtils.statementsRequiredToActualizeScheme(definedTable).also {
assertTrue(it.isEmpty())
}
}
}
}

@Test
fun testChangesOnCreateMissingNullableColumns() {
val testerWithDefaults = object : Table("tester") {
val defaultNullString = varchar("default_null_string", 8).nullable().default("NULL")
val defaultNumber = integer("default_number").default(999).nullable()
val defaultWord = varchar("default_word", 8).default("Hello").nullable()
val defaultBinary = binary("default_binary", 256).nullable().default("default-binary".toByteArray())
}

val testerWithoutDefaults = object : Table("tester") {
val defaultNullString = varchar("default_null_string", 8).nullable()
val defaultNumber = integer("default_number").nullable()
val defaultWord = varchar("default_word", 8).nullable()
val defaultBinary = binary("default_binary", 256).nullable()
}

listOf(
testerWithDefaults to testerWithoutDefaults,
testerWithoutDefaults to testerWithDefaults,
).forEach { (existingTable, definedTable) ->
withTables(excludeSettings = listOf(TestDB.SQLITE), existingTable) {
SchemaUtils.statementsRequiredToActualizeScheme(definedTable).also {
assertTrue(it.isNotEmpty())
}
}
}

listOf(
testerWithDefaults to testerWithDefaults,
testerWithoutDefaults to testerWithoutDefaults
).forEach { (existingTable, definedTable) ->
withTables(excludeSettings = listOf(TestDB.SQLITE), existingTable) {
SchemaUtils.statementsRequiredToActualizeScheme(definedTable).also {
assertTrue(it.isEmpty())
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.jetbrains.exposed.sql.tests.shared.types

import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.binaryLiteral
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
import org.jetbrains.exposed.sql.tests.shared.assertEqualLists
import org.junit.Test

class BinaryColumnTypeTests : DatabaseTestsBase() {
@Test
fun testBinaryColumn() {
val defaultValue = "default-value".toByteArray()
val tester = object : IntIdTable("testBinaryColumn") {
val bin = binary("bin", 256).default(defaultValue)
}

withTables(tester) {
assertEqualLists(emptyList(), SchemaUtils.statementsRequiredToActualizeScheme(tester))

val id1 = tester.insertAndGetId { }
val row1 = tester.selectAll().where { tester.id eq id1 }.first()
val binValue1 = row1[tester.bin]
assertEqualLists(defaultValue.toList(), binValue1.toList())

val literalValue = "literal-value".toByteArray()
val id2 = tester.insertAndGetId { it[tester.bin] = binaryLiteral(literalValue) }
val row2 = tester.selectAll().where { tester.id eq id2 }.first()
val binValue2 = row2[tester.bin]
assertEqualLists(literalValue.toList(), binValue2.toList())

val nonLiteralValue = "non-literal-value".toByteArray()
val id3 = tester.insertAndGetId { it[tester.bin] = nonLiteralValue }
val row3 = tester.selectAll().where { tester.id eq id3 }.first()
val binValue3 = row3[tester.bin]
assertEqualLists(nonLiteralValue.toList(), binValue3.toList())
}
}
}

0 comments on commit 519f098

Please sign in to comment.