Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add EntityClass#test(id) and support update { it[nullable] = nullable } #1277

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ abstract class CompositeColumn<T> : Expression<T>() {
abstract class BiCompositeColumn<C1, C2, T>(
protected val column1: Column<C1>,
protected val column2: Column<C2>,
val transformFromValue: (T) -> Pair<C1?, C2?>,
val transformToValue: (Any?, Any?) -> T
val transformToValue: (C1, C2) -> T,
val transformFromValue: (T) -> Pair<C1, C2>
) : CompositeColumn<T>() {

override fun getRealColumns(): List<Column<*>> = listOf(column1, column2)
Expand All @@ -50,12 +50,11 @@ abstract class BiCompositeColumn<C1, C2, T>(
}

override fun restoreValueFromParts(parts: Map<Column<*>, 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> 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 <T> 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 <T> registerArguments(sqlType: IColumnType, arguments: Iterable<T>) {
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()
Expand Down
49 changes: 19 additions & 30 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -408,35 +409,10 @@ class InListOrNotInListOp<T>(
/** Returns `true` if the check is inverted, `false` otherwise. */
val isInList: Boolean = true
) : Op<Boolean>(), 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
Expand All @@ -449,6 +425,11 @@ class LiteralOp<T>(
/** Returns the value being used as a literal. */
val value: T
) : ExpressionWithColumnType<T>() {
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) }
}

Expand Down Expand Up @@ -506,7 +487,15 @@ class QueryParameter<T>(
/** Returns the column type of this expression. */
val sqlType: IColumnType
) : Expression<T>() {
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]. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,24 @@ class ResultRow(val fieldIndex: Map<Expression<*>, Int>) {
return when {
raw == null -> null
raw == NotInitializedValue -> error("$c is not initialized yet")
c is CompositeColumn<T> -> c.restoreValueFromParts(
(raw as Map<Column<*>, Any?>).mapValues { (column, rawValue) ->
rawToColumnValue(rawValue, column as Column<Any?>)
}
)
c is ExpressionAlias<T> && c.delegate is ExpressionWithColumnType<T> -> c.delegate.columnType.valueFromDB(raw)
c is ExpressionWithColumnType<T> -> c.columnType.valueFromDB(raw)
else -> raw
} as T
}

@Suppress("UNCHECKED_CAST")
private fun <T> getRaw(c: Expression<T>): T? {
if (c is CompositeColumn<T>) {
val rawParts = c.getRealColumns().associateWith { getRaw(it) }
return c.restoreValueFromParts(rawParts)
// Result row for composite is not really defined, and we return Map<Column, ItsRawValue> for now
// See https://github.com/JetBrains/Exposed/issues/1278
@Suppress("UNCHECKED_CAST")
return rawParts as T?
}

val index = fieldIndex[c]
Expand All @@ -70,6 +77,7 @@ class ResultRow(val fieldIndex: Map<Expression<*>, Int>) {
}?.let { fieldIndex[it] }
?: error("$c is not in record set")

@Suppress("UNCHECKED_CAST")
return data[index] as T?
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ abstract class InListOrNotInListBaseOp<V> (
private fun QueryBuilder.registerValues(values: List<Any?>) {
val singleColumn = columnTypes.singleOrNull()
if (singleColumn != null)
registerArgument(singleColumn.columnType, values.single())
registerArgument(singleColumn as ExpressionWithColumnType<Any?>, values.single())
else {
append("(")
columnTypes.forEachIndexed { index, columnExpression ->
if (index != 0) append(", ")
registerArgument(columnExpression.columnType, values[index])
registerArgument(columnExpression as ExpressionWithColumnType<Any?>, values[index])
}
append(")")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ open class BatchUpdateStatement(val table: IdTable<*>) : UpdateStatement(table,
data.add(id to values)
}

override fun <T, S : T?> update(column: Column<T>, value: Expression<S>) = error("Expressions unsupported in batch update")
override fun <T> update(column: Column<T>, value: Expression<out T>) = error("Expressions unsupported in batch update")

override fun prepareSQL(transaction: Transaction): String =
"${super.prepareSQL(transaction)} WHERE ${transaction.identity(table.id)} = ?"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,46 +12,55 @@ abstract class UpdateBuilder<out T>(type: StatementType, targets: List<Table>) :
protected val values: MutableMap<Column<*>, Any?> = LinkedHashMap()

open operator fun <S> set(column: Column<S>, 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 <S, ID : EntityID<S>, E : Expression<S>> set(column: Column<ID>, value: E) {
require(!values.containsKey(column)) { "$column is already initialized" }
column.columnType.validateValueBeforeUpdate(value)
values[column] = value
operator fun <S : Comparable<S>> set(column: Column<out EntityID<S>?>, value: Expression<out S?>) {
@Suppress("UNCHECKED_CAST")
set(column as Column<Any?>, value as Any?)
}

@JvmName("setWithEntityIdValue")
operator fun <S : Comparable<S>, ID : EntityID<S>, E : S?> set(column: Column<ID>, value: E) {
require(!values.containsKey(column)) { "$column is already initialized" }
column.columnType.validateValueBeforeUpdate(value)
values[column] = value
operator fun <S : Comparable<S>> set(column: Column<out EntityID<S>?>, value: S?) {
@Suppress("UNCHECKED_CAST")
set(column as Column<Any?>, value as Any?)
}

open operator fun <T, S : T, E : Expression<S>> set(column: Column<T>, value: E) = update(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<T>?".
*/
fun <T> setNull(column: Column<T?>) = set(column, null as T?)

open operator fun <S> set(column: Column<S>, value: Expression<out S>) = update(column, value)

open operator fun <S> set(column: CompositeColumn<S>, value: S) {
column.getRealColumnsWithValues(value).forEach { (realColumn, itsValue) -> set(realColumn as Column<Any?>, itsValue) }
column.getRealColumnsWithValues(value).forEach { (realColumn, itsValue) ->
@Suppress("UNCHECKED_CAST")
set(realColumn as Column<Any?>, itsValue)
}
}

open fun <T, S : T?> update(column: Column<T>, value: Expression<S>) {
require(!values.containsKey(column)) { "$column is already initialized" }
column.columnType.validateValueBeforeUpdate(value)
values[column] = value
open fun <S> update(column: Column<S>, value: Expression<out S>) {
@Suppress("UNCHECKED_CAST")
set(column as Column<Any?>, value as Any?)
}

open fun <T, S : T?> update(column: Column<T>, value: SqlExpressionBuilder.() -> Expression<S>) {
require(!values.containsKey(column)) { "$column is already initialized" }
val expression = SqlExpressionBuilder.value()
column.columnType.validateValueBeforeUpdate(expression)
values[column] = expression
open fun <S> update(column: Column<S>, value: SqlExpressionBuilder.() -> Expression<out S>) {
// 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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal open class MysqlFunctionProvider : FunctionProvider() {
override fun replace(table: Table, data: List<Pair<Column<*>, 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)"
}

Expand Down
Loading