Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/en/jpql-with-kotlin-jdsl/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ Kotlin JDSL provides a series of functions to support built-in functions in JPA.
* UPPER (upper)
* LENGTH (length)
* LOCATE (locate)
* CAST (cast) - *Added in JPA 3.2*
* LEFT (left) - *Added in JPA 3.2*
* RIGHT (right) - *Added in JPA 3.2*
* REPLACE (replace) - *Added in JPA 3.2*

```kotlin
concat(path(Book::title), literal(":"), path(Book::imageUrl))
Expand All @@ -215,6 +219,18 @@ upper(path(Book::title))
length(path(Book::title))

locate("Book", path(Book::title))

cast(path(Book::price), String::class)
cast<String>(path(Book::price))

left(path(Book::title), 3)
left(path(Book::title), literal(3))

right(path(Book::title), 3)
right(path(Book::title), literal(3))

replace(path(Book::title), "old", "new")
replace(path(Book::title), literal("old"), literal("new"))
```

### Arithmetic functions
Expand Down
16 changes: 16 additions & 0 deletions docs/ko/jpql-with-kotlin-jdsl/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ Kotlin JDSL은 JPA에서 제공하는 여러 함수들을 지원하기 위한
* UPPER (upper)
* LENGTH (length)
* LOCATE (locate)
* CAST (cast) - *JPA 3.2에 추가됨*
* LEFT (left) - *JPA 3.2에 추가됨*
* RIGHT (right) - *JPA 3.2에 추가됨*
* REPLACE (replace) - *JPA 3.2에 추가됨*

```kotlin
concat(path(Book::title), literal(":"), path(Book::imageUrl))
Expand All @@ -213,6 +217,18 @@ upper(path(Book::title))
length(path(Book::title))

locate("Book", path(Book::title))

cast(path(Book::price), String::class)
cast<String>(path(Book::price))

left(path(Book::title), 3)
left(path(Book::title), literal(3))

right(path(Book::title), 3)
right(path(Book::title), literal(3))

replace(path(Book::title), "old", "new")
replace(path(Book::title), literal("old"), literal("new"))
```

### Arithmetic functions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,78 @@ open class Jpql : JpqlDsl {
)
}

/**
* Creates an expression that represents the casting of a value to a different type.
*/
@SinceJdsl("3.6.0")
fun <T : Any> cast(value: Expressionable<*>, type: KClass<T>): Expression<T> {
return Expressions.cast(value.toExpression(), type)
}

/**
* Creates an expression that represents the casting of a value to a different type.
*/
@SinceJdsl("3.6.0")
inline fun <reified T : Any> cast(value: Expressionable<*>): Expression<T> {
return Expressions.cast(value.toExpression(), T::class)
}
Copy link
Member

Choose a reason for hiding this comment

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

https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#typecasts

string_cast_function::=
    CAST(scalar_expression AS STRING)
arithmetic_cast_function::=
    CAST(string_expression AS {INTEGER | LONG | FLOAT | DOUBLE})

Looking at the Jakarta specification, the range of castable types is limited depending on whether cast is a scalar type or a string type.

I believe this information should also be included in the function specification.

Copy link
Member

Choose a reason for hiding this comment

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

And I think it would be best to create a function form that is as similar as possible to the JPQL specification.

Therefore, I think it would be good to create CastStep, similar to TrimFromStep. Creating CastStep would also make it easier to implement type restrictions.

Copy link
Collaborator Author

@cj848 cj848 Jul 19, 2025

Choose a reason for hiding this comment

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

yeah. applied

Users can use the cast operator as follows:

cast(path(Book::price)).asString() // scalar expression as String
cast(path(Book::authorId)).asInt() // string expression as integer
cast(path(Book::authorId)).asLong() // string expression as long
cast(path(Book::authorId)).asDouble() // string expression as float
cast(path(Book::authorId)).asFloat() // string expression as double


/**
* Creates an expression that returns the leftmost count characters from a string.
*/
@SinceJdsl("3.6.0")
fun left(value: Expressionable<String>, count: Expressionable<Int>): Expression<String> {
return Expressions.left(value.toExpression(), count.toExpression())
}
Copy link
Member

Choose a reason for hiding this comment

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

https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#_criteriabuilder_

Expression<String> left(Expression<String> x, int len);
Expression<String> right(Expression<String> x, Expression<Integer> len);
Expression<String> replace(Expression<String> x, Expression<String> substring, Expression<String> replacement);

It would be best to follow the parameter naming conventions of CriteriaBuilder.
count should be length, and search should be substring.

The parameter name x is not ideal, so it would be best to use the value as is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

thank you. your reviews applied


/**
* Creates an expression that returns the leftmost count characters from a string.
*/
@SinceJdsl("3.6.0")
fun left(value: Expressionable<String>, count: Int): Expression<String> {
return Expressions.left(value.toExpression(), intLiteral(count))
}

/**
* Creates an expression that returns the rightmost count characters from a string.
*/
@SinceJdsl("3.6.0")
fun right(value: Expressionable<String>, count: Expressionable<Int>): Expression<String> {
return Expressions.right(value.toExpression(), count.toExpression())
}

/**
* Creates an expression that returns the rightmost count characters from a string.
*/
@SinceJdsl("3.6.0")
fun right(value: Expressionable<String>, count: Int): Expression<String> {
return Expressions.right(value.toExpression(), intLiteral(count))
}

/**
* Creates an expression that replaces all occurrences of a search string with a replacement string.
*/
@SinceJdsl("3.6.0")
fun replace(
value: Expressionable<String>,
search: Expressionable<String>,
replacement: Expressionable<String>,
): Expression<String> {
return Expressions.replace(value.toExpression(), search.toExpression(), replacement.toExpression())
}

/**
* Creates an expression that replaces all occurrences of a search string with a replacement string.
*/
@SinceJdsl("3.6.0")
fun replace(
value: Expressionable<String>,
search: String,
replacement: String,
): Expression<String> {
return Expressions.replace(value.toExpression(), stringLiteral(search), stringLiteral(replacement))
}

/**
* Creates an expression that represents predefined database functions and user-defined database functions.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,158 @@ class ExpressionDslTest : WithAssertions {

assertThat(actual).isEqualTo(expected)
}

@Test
fun `cast() with a expression and a class`() {
// when
val expression = queryPart {
cast(expression(String::class, alias1), Int::class)
}.toExpression()

val actual: Expression<Int> = expression // for type check

// then
val expected = Expressions.cast(
Expressions.expression(String::class, alias1),
Int::class,
)

assertThat(actual).isEqualTo(expected)
}

@Test
fun `inline cast() with a expression and a class`() {
// when
val expression = queryPart {
cast<Int>(expression(String::class, alias1))
}.toExpression()

val actual: Expression<Int> = expression // for type check

// then
val expected = Expressions.cast(
Expressions.expression(String::class, alias1),
Int::class,
)

assertThat(actual).isEqualTo(expected)
}

@Test
fun `left() with two expressions`() {
// when
val expression = queryPart {
left(expression(String::class, alias1), expression(Int::class, "alias2"))
}.toExpression()

val actual: Expression<String> = expression // for type check

// then
val expected = Expressions.left(
Expressions.expression(String::class, alias1),
Expressions.expression(Int::class, "alias2"),
)

assertThat(actual).isEqualTo(expected)
}

@Test
fun `left() literal with two expressions`() {
// when
val expression = queryPart {
left(expression(String::class, alias1), 1)
}.toExpression()

val actual: Expression<String> = expression // for type check

// then
val expected = Expressions.left(
Expressions.expression(String::class, alias1),
Expressions.intLiteral(1),
)

assertThat(actual).isEqualTo(expected)
}

@Test
fun `right() with two expressions`() {
// when
val expression = queryPart {
right(expression(String::class, alias1), expression(Int::class, "alias2"))
}.toExpression()

val actual: Expression<String> = expression // for type check

// then
val expected = Expressions.right(
Expressions.expression(String::class, alias1),
Expressions.expression(Int::class, "alias2"),
)

assertThat(actual).isEqualTo(expected)
}

@Test
fun `right() literal with two expressions`() {
// when
val expression = queryPart {
right(expression(String::class, alias1), 1)
}.toExpression()

val actual: Expression<String> = expression // for type check

// then
val expected = Expressions.right(
Expressions.expression(String::class, alias1),
Expressions.intLiteral(1),
)

assertThat(actual).isEqualTo(expected)
}

@Test
fun `replace() with three expressions`() {
// when
val expression = queryPart {
replace(
expression(String::class, alias1),
expression(String::class, "alias2"),
expression(String::class, "alias3"),
)
}.toExpression()

val actual: Expression<String> = expression // for type check

// then
val expected = Expressions.replace(
Expressions.expression(String::class, alias1),
Expressions.expression(String::class, "alias2"),
Expressions.expression(String::class, "alias3"),
)

assertThat(actual).isEqualTo(expected)
}

@Test
fun `replace() literal with three expressions`() {
// when
val expression = queryPart {
replace(
expression(String::class, alias1),
"string1",
"string2",
)
}.toExpression()

val actual: Expression<String> = expression // for type check

// then
val expected = Expressions.replace(
Expressions.expression(String::class, alias1),
Expressions.stringLiteral("string1"),
Expressions.stringLiteral("string2"),
)

assertThat(actual).isEqualTo(expected)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlAliasedExpres
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlAvg
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlCaseValue
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlCaseWhen
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlCast
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlCeiling
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlCoalesce
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlConcat
Expand All @@ -22,6 +23,7 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlExpression
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlExpressionParentheses
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFloor
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlFunctionExpression
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLeft
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLength
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLiteral
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlLn
Expand All @@ -41,6 +43,8 @@ import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlParam
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlPathType
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlPlus
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlPower
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlReplace
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlRight
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlRound
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlSign
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl.JpqlSize
Expand Down Expand Up @@ -723,6 +727,42 @@ object Expressions {
return JpqlLocate(substring, string, start)
}

/**
* Creates an expression that represents the casting of a value to a different type.
*/
@SinceJdsl("3.6.0")
fun <T : Any> cast(value: Expression<*>, type: KClass<T>): Expression<T> {
return JpqlCast(value, type)
}

/**
* Creates an expression that returns the leftmost count characters from a string.
*/
@SinceJdsl("3.6.0")
fun left(value: Expression<String>, count: Expression<Int>): Expression<String> {
return JpqlLeft(value, count)
}

/**
* Creates an expression that returns the rightmost count characters from a string.
*/
@SinceJdsl("3.6.0")
fun right(value: Expression<String>, count: Expression<Int>): Expression<String> {
return JpqlRight(value, count)
}

/**
* Creates an expression that replaces all occurrences of a search string with a replacement string.
*/
@SinceJdsl("3.6.0")
fun replace(
value: Expression<String>,
search: Expression<String>,
replacement: Expression<String>,
): Expression<String> {
return JpqlReplace(value, search, replacement)
}

/**
* Creates an expression that represents predefined database functions and user-defined database functions.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl

import com.linecorp.kotlinjdsl.SinceJdsl
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expression
import kotlin.reflect.KClass

@SinceJdsl("3.6.0")
data class JpqlCast<T : Any> internal constructor(
Copy link
Member

Choose a reason for hiding this comment

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

Since it is implementation code, it would be good to include the internal annotation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

thank you. your reviews applied

val value: Expression<*>,
val type: KClass<T>,
) : Expression<T>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.linecorp.kotlinjdsl.querymodel.jpql.expression.impl

import com.linecorp.kotlinjdsl.SinceJdsl
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expression

@SinceJdsl("3.6.0")
data class JpqlLeft internal constructor(
val value: Expression<String>,
val length: Expression<Int>,
) : Expression<String>
Loading
Loading