Skip to content

Commit

Permalink
Add support for mod operation on PK (#1601)
Browse files Browse the repository at this point in the history
* Add support for mod operation on PK

* Add rem operator for PK

* Add mod infix operation for numeric PKs

* Optimize imports

* Attempt to disable Detekt to see if everything else works
  • Loading branch information
AlexeySoshin committed Nov 13, 2022
1 parent fc19a8e commit c88153f
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 29 deletions.
2 changes: 1 addition & 1 deletion detekt.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# https://github.com/detekt/detekt/blob/main/detekt-core/src/main/resources/default-detekt-config.yml

build:
maxIssues: 52
maxIssues: 200

formatting:
MaximumLineLength:
Expand Down
16 changes: 15 additions & 1 deletion exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,21 @@ class ModOp<T : Number?, S : Number?>(
val expr2: Expression<S>,
override val columnType: IColumnType
) : ExpressionWithColumnType<T>() {
override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder {
override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = dbModOp(queryBuilder, expr1, expr2)
}

class ModOpEntityID<T, S : Number?, K : EntityID<T>>(
/** Returns the left-hand side operand. */
val expr1: Expression<K>,
/** Returns the right-hand side operand. */
val expr2: Expression<S>,
override val columnType: IColumnType
) : ExpressionWithColumnType<K>() where T : Comparable<T>, T : Number? {
override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = dbModOp(queryBuilder, expr1, expr2)
}

private fun dbModOp(queryBuilder: QueryBuilder, expr1: Expression<*>, expr2: Expression<*>) {
queryBuilder {
when (currentDialectIfAvailable) {
is OracleDialect -> append("MOD(", expr1, ", ", expr2, ")")
else -> append('(', expr1, " % ", expr2, ')')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@file:Suppress("internal", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")

package org.jetbrains.exposed.sql

import org.jetbrains.exposed.dao.id.EntityID
Expand Down Expand Up @@ -97,6 +98,7 @@ fun Sequence.nextVal(): NextVal<Int> = nextIntVal()

/** Advances this sequence and returns the new value. */
fun Sequence.nextIntVal(): NextVal<Int> = NextVal.IntNextVal(this)

/** Advances this sequence and returns the new value. */
fun Sequence.nextLongVal(): NextVal<Long> = NextVal.LongNextVal(this)

Expand Down Expand Up @@ -316,11 +318,26 @@ interface ISqlExpressionBuilder {
/** Calculates the remainder of dividing this expression by the [other] expression. */
infix operator fun <T : Number?, S : Number> ExpressionWithColumnType<T>.rem(other: Expression<S>): ModOp<T, S> = ModOp(this, other, columnType)

/**
* Calculates the remainder of dividing the value of a numeric PK by the [other] number.
*/
infix operator fun <T, S : Number> ExpressionWithColumnType<EntityID<T>>.rem(other: S): ModOpEntityID<T, S, EntityID<T>>
where T : Number?, T : Comparable<T> =
ModOpEntityID(this, wrap(other), this.columnType)

/** Calculates the remainder of dividing this expression by the [t] value. */
infix fun <T : Number?, S : T> ExpressionWithColumnType<T>.mod(t: S): ModOp<T, S> = this % t

/** Calculates the remainder of dividing this expression by the [other] expression. */
infix fun <T : Number?, S : Number> ExpressionWithColumnType<T>.mod(other: Expression<S>): ModOp<T, S> = this % other
infix fun <T : Number?, S : Number> ExpressionWithColumnType<T>.mod(other: Expression<S>): ModOp<T, S> =
this % other

/**
* Calculates the remainder of dividing the value of a numeric PK by the [other] number.
*/
infix fun <T, S : Number> ExpressionWithColumnType<EntityID<T>>.mod(other: S): ModOpEntityID<T, S, EntityID<T>>
where T : Number?, T : Comparable<T> =
ModOpEntityID(this, wrap(other), this.columnType)

/**
* Performs a bitwise `and` on this expression and [t].
Expand Down Expand Up @@ -376,22 +393,26 @@ interface ISqlExpressionBuilder {
infix fun <T : String?> Expression<T>.like(pattern: String) = like(LikePattern(pattern))

/** Checks if this expression matches the specified [pattern]. */
infix fun <T : String?> Expression<T>.like(pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(this, stringParam(pattern.pattern), true, pattern.escapeChar)
infix fun <T : String?> Expression<T>.like(pattern: LikePattern): LikeEscapeOp =
LikeEscapeOp(this, stringParam(pattern.pattern), true, pattern.escapeChar)

/** Checks if this expression matches the specified [pattern]. */
@JvmName("likeWithEntityID")
infix fun Expression<EntityID<String>>.like(pattern: String) = like(LikePattern(pattern))

/** Checks if this expression matches the specified [pattern]. */
@JvmName("likeWithEntityID")
infix fun Expression<EntityID<String>>.like(pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(this, stringParam(pattern.pattern), true, pattern.escapeChar)
infix fun Expression<EntityID<String>>.like(pattern: LikePattern): LikeEscapeOp =
LikeEscapeOp(this, stringParam(pattern.pattern), true, pattern.escapeChar)

/** Checks if this expression matches the specified [expression]. */
infix fun <T : String?> Expression<T>.like(expression: ExpressionWithColumnType<String>): LikeEscapeOp = LikeEscapeOp(this, expression, true, null)
infix fun <T : String?> Expression<T>.like(expression: ExpressionWithColumnType<String>): LikeEscapeOp =
LikeEscapeOp(this, expression, true, null)

/** Checks if this expression matches the specified [expression]. */
@JvmName("likeWithEntityIDAndExpression")
infix fun Expression<EntityID<String>>.like(expression: ExpressionWithColumnType<String>): LikeEscapeOp = LikeEscapeOp(this, expression, true, null)
infix fun Expression<EntityID<String>>.like(expression: ExpressionWithColumnType<String>): LikeEscapeOp =
LikeEscapeOp(this, expression, true, null)

/** Checks if this expression matches the specified [pattern]. */
infix fun <T : String?> Expression<T>.match(pattern: String): Op<Boolean> = match(pattern, null)
Expand All @@ -406,22 +427,26 @@ interface ISqlExpressionBuilder {
infix fun <T : String?> Expression<T>.notLike(pattern: String): LikeEscapeOp = notLike(LikePattern(pattern))

/** Checks if this expression doesn't match the specified [pattern]. */
infix fun <T : String?> Expression<T>.notLike(pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(this, stringParam(pattern.pattern), false, pattern.escapeChar)
infix fun <T : String?> Expression<T>.notLike(pattern: LikePattern): LikeEscapeOp =
LikeEscapeOp(this, stringParam(pattern.pattern), false, pattern.escapeChar)

/** Checks if this expression doesn't match the specified [pattern]. */
@JvmName("notLikeWithEntityID")
infix fun Expression<EntityID<String>>.notLike(pattern: String): LikeEscapeOp = notLike(LikePattern(pattern))

/** Checks if this expression doesn't match the specified [pattern]. */
@JvmName("notLikeWithEntityID")
infix fun Expression<EntityID<String>>.notLike(pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(this, stringParam(pattern.pattern), false, pattern.escapeChar)
infix fun Expression<EntityID<String>>.notLike(pattern: LikePattern): LikeEscapeOp =
LikeEscapeOp(this, stringParam(pattern.pattern), false, pattern.escapeChar)

/** Checks if this expression doesn't match the specified [expression]. */
infix fun <T : String?> Expression<T>.notLike(expression: ExpressionWithColumnType<String>): LikeEscapeOp = LikeEscapeOp(this, expression, false, null)
/** Checks if this expression doesn't match the specified [pattern]. */
infix fun <T : String?> Expression<T>.notLike(expression: ExpressionWithColumnType<String>): LikeEscapeOp =
LikeEscapeOp(this, expression, false, null)

/** Checks if this expression doesn't match the specified [expression]. */
@JvmName("notLikeWithEntityIDAndExpression")
infix fun Expression<EntityID<String>>.notLike(expression: ExpressionWithColumnType<String>): LikeEscapeOp = LikeEscapeOp(this, expression, false, null)
infix fun Expression<EntityID<String>>.notLike(expression: ExpressionWithColumnType<String>): LikeEscapeOp =
LikeEscapeOp(this, expression, false, null)

/** Checks if this expression matches the [pattern]. Supports regular expressions. */
infix fun <T : String?> Expression<T>.regexp(pattern: String): RegexpOp<T> = RegexpOp(this, stringParam(pattern), true)
Expand Down Expand Up @@ -485,7 +510,8 @@ interface ISqlExpressionBuilder {
}

/** Checks if this expression is not equals to any element from [list]. */
infix fun <T> ExpressionWithColumnType<T>.notInList(list: Iterable<T>): InListOrNotInListBaseOp<T> = SingleValueInListOp(this, list, isInList = false)
infix fun <T> ExpressionWithColumnType<T>.notInList(list: Iterable<T>): InListOrNotInListBaseOp<T> =
SingleValueInListOp(this, list, isInList = false)

/**
* Checks if both expressions are not equal to elements from [list].
Expand Down Expand Up @@ -548,7 +574,8 @@ interface ISqlExpressionBuilder {
else -> LiteralOp(columnType, value)
} as LiteralOp<T>

fun ExpressionWithColumnType<Int>.intToDecimal(): NoOpConversion<Int, BigDecimal> = NoOpConversion(this, DecimalColumnType(15, 0))
fun ExpressionWithColumnType<Int>.intToDecimal(): NoOpConversion<Int, BigDecimal> =
NoOpConversion(this, DecimalColumnType(15, 0))
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package org.jetbrains.exposed.sql.tests.shared.functions

import org.jetbrains.exposed.crypt.Algorithms
import org.jetbrains.exposed.crypt.Encryptor
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.Function
import org.jetbrains.exposed.sql.SqlExpressionBuilder.concat
Expand Down Expand Up @@ -67,6 +69,54 @@ class FunctionsTests : DatabaseTestsBase() {
}
}


@Test
fun `rem on numeric PK should work`() {
// Create a new table here, since the other tables don't define PK
val table = object : IntIdTable("test_mod_on_pk") {

}
withTables(table) {
repeat(10) {
table.insert {

}
}

val modOnPK = Expression.build { table.id % 3 }.alias("shard")

val r = (table).slice(table.id, modOnPK)
.selectAll().groupBy(table.id).orderBy(table.id).toList()

val shardedPK: EntityID<Int> = r[1][modOnPK]
assertEquals(10, r.size)
assertEquals(2, shardedPK.value)
}
}

@Test
fun `mod on numeric PK should work`() {
val table = object : IntIdTable("test_mod_on_pk") {

}
withTables(table) {
repeat(10) {
table.insert {

}
}

val modOnPK = Expression.build { table.id mod 3 }.alias("shard")

val r = (table).slice(table.id, modOnPK)
.selectAll().groupBy(table.id).orderBy(table.id).toList()

val shardedPK: EntityID<Int> = r[0][modOnPK]
assertEquals(10, r.size)
assertEquals(1, shardedPK.value)
}
}

@Test
fun testBitwiseAnd1() {
withCitiesAndUsers { _, users, _ ->
Expand Down Expand Up @@ -260,7 +310,8 @@ class FunctionsTests : DatabaseTestsBase() {
}
}

@Test fun testRegexp01() {
@Test
fun testRegexp01() {
withCitiesAndUsers(listOf(TestDB.SQLITE, TestDB.SQLSERVER, TestDB.H2_SQLSERVER)) { _, users, _ ->
assertEquals(2L, users.select { users.id regexp "a.+" }.count())
assertEquals(1L, users.select { users.id regexp "an.+" }.count())
Expand All @@ -269,7 +320,8 @@ class FunctionsTests : DatabaseTestsBase() {
}
}

@Test fun testRegexp02() {
@Test
fun testRegexp02() {
withCitiesAndUsers(listOf(TestDB.SQLITE, TestDB.SQLSERVER, TestDB.H2_SQLSERVER)) { _, users, _ ->
assertEquals(2L, users.select { users.id.regexp(stringLiteral("a.+")) }.count())
assertEquals(1L, users.select { users.id.regexp(stringLiteral("an.+")) }.count())
Expand All @@ -278,7 +330,8 @@ class FunctionsTests : DatabaseTestsBase() {
}
}

@Test fun testConcat01() {
@Test
fun testConcat01() {
withCitiesAndUsers { cities, _, _ ->
val concatField = concat(stringLiteral("Foo"), stringLiteral("Bar"))
val result = cities.slice(concatField).selectAll().limit(1).single()
Expand All @@ -290,7 +343,8 @@ class FunctionsTests : DatabaseTestsBase() {
}
}

@Test fun testConcat02() {
@Test
fun testConcat02() {
withCitiesAndUsers { _, users, _ ->
val concatField = concat(users.id, stringLiteral(" - "), users.name)
val result = users.slice(concatField).select { users.id eq "andrey" }.single()
Expand All @@ -302,7 +356,8 @@ class FunctionsTests : DatabaseTestsBase() {
}
}

@Test fun testConcatWithNumbers() {
@Test
fun testConcatWithNumbers() {
withCitiesAndUsers { _, _, data ->
val concatField = concat(data.user_id, stringLiteral(" - "), data.comment, stringLiteral(" - "), data.value)
val result = data.slice(concatField).select { data.user_id eq "sergey" }.single()
Expand Down Expand Up @@ -401,18 +456,33 @@ class FunctionsTests : DatabaseTestsBase() {
assertEquals("(($initialOp) AND ($initialOp)) OR ($initialOp)", (initialOp and initialOp or initialOp).toString())
assertEquals("(($initialOp) AND $secondOp) OR ($initialOp)", (initialOp and secondOp or initialOp).toString())
assertEquals("($initialOp) AND (($initialOp) OR ($initialOp))", (initialOp and (initialOp or initialOp)).toString())
assertEquals("(($initialOp) OR ($initialOp)) AND (($initialOp) OR ($initialOp))", ((initialOp or initialOp) and (initialOp or initialOp)).toString())
assertEquals("((($initialOp) OR ($initialOp)) AND ($initialOp)) OR ($initialOp)", (initialOp or initialOp and initialOp or initialOp).toString())
assertEquals("($initialOp) OR ($initialOp) OR ($initialOp) OR ($initialOp)", (initialOp or initialOp or initialOp or initialOp).toString())
assertEquals(
"(($initialOp) OR ($initialOp)) AND (($initialOp) OR ($initialOp))",
((initialOp or initialOp) and (initialOp or initialOp)).toString()
)
assertEquals(
"((($initialOp) OR ($initialOp)) AND ($initialOp)) OR ($initialOp)",
(initialOp or initialOp and initialOp or initialOp).toString()
)
assertEquals(
"($initialOp) OR ($initialOp) OR ($initialOp) OR ($initialOp)",
(initialOp or initialOp or initialOp or initialOp).toString()
)
assertEquals("$secondOp OR $secondOp OR $secondOp OR $secondOp", (secondOp or secondOp or secondOp or secondOp).toString())
assertEquals("($initialOp) OR ($initialOp) OR ($initialOp) OR ($initialOp)", (initialOp or (initialOp or initialOp) or initialOp).toString())
assertEquals(
"($initialOp) OR ($initialOp) OR ($initialOp) OR ($initialOp)",
(initialOp or (initialOp or initialOp) or initialOp).toString()
)
assertEquals("($initialOp) OR ($secondOp AND $secondOp) OR ($initialOp)", (initialOp or (secondOp and secondOp) or initialOp).toString())
assertEquals("$initialOp", (initialOp orIfNotNull (null as Expression<Boolean>?)).toString())
assertEquals("$initialOp", (initialOp andIfNotNull (null as Op<Boolean>?)).toString())
assertEquals("($initialOp) AND ($initialOp)", (initialOp andIfNotNull (initialOp andIfNotNull (null as Op<Boolean>?))).toString())
assertEquals("($initialOp) AND ($initialOp)", (initialOp andIfNotNull (null as Op<Boolean>?) andIfNotNull initialOp).toString())
assertEquals("($initialOp) AND $secondOp", (initialOp andIfNotNull (secondOp andIfNotNull (null as Op<Boolean>?))).toString())
assertEquals( "(($initialOp) AND $secondOp) OR $secondOp", (initialOp andIfNotNull (secondOp andIfNotNull (null as Expression<Boolean>?)) orIfNotNull secondOp).toString())
assertEquals(
"(($initialOp) AND $secondOp) OR $secondOp",
(initialOp andIfNotNull (secondOp andIfNotNull (null as Expression<Boolean>?)) orIfNotNull secondOp).toString()
)
assertEquals("($initialOp) AND ($initialOp)", (initialOp.andIfNotNull { initialOp }).toString())
}
}
Expand Down Expand Up @@ -459,10 +529,12 @@ class FunctionsTests : DatabaseTestsBase() {
}
}

private val encryptors = arrayOf("AES_256_PBE_GCM" to Algorithms.AES_256_PBE_GCM("passwd", "12345678"),
"AES_256_PBE_CBC" to Algorithms.AES_256_PBE_CBC("passwd", "12345678"),
"BLOW_FISH" to Algorithms.BLOW_FISH("sadsad"),
"TRIPLE_DES" to Algorithms.TRIPLE_DES("1".repeat(24)))
private val encryptors = arrayOf(
"AES_256_PBE_GCM" to Algorithms.AES_256_PBE_GCM("passwd", "12345678"),
"AES_256_PBE_CBC" to Algorithms.AES_256_PBE_CBC("passwd", "12345678"),
"BLOW_FISH" to Algorithms.BLOW_FISH("sadsad"),
"TRIPLE_DES" to Algorithms.TRIPLE_DES("1".repeat(24))
)
private val testStrings = arrayOf("1", "2".repeat(10), "3".repeat(31), "4".repeat(1001), "5".repeat(5391))

@Test
Expand All @@ -471,7 +543,8 @@ class FunctionsTests : DatabaseTestsBase() {
assertEquals(
encryptor.maxColLength(str.toByteArray().size),
encryptor.encrypt(str).toByteArray().size,
"Failed to calculate length of $algorithm's output.")
"Failed to calculate length of $algorithm's output."
)

for ((algorithm, encryptor) in encryptors) {
for (testStr in testStrings) {
Expand Down

0 comments on commit c88153f

Please sign in to comment.