From 2c52178cc831778c61a505e801dd9f6414606086 Mon Sep 17 00:00:00 2001 From: PedroD Date: Sat, 9 Jan 2021 22:52:15 +0000 Subject: [PATCH 01/96] Adding returning to single insert or update query --- .../support/postgresql/InsertOrUpdate.kt | 109 ++++++++++++++++++ .../support/postgresql/PostgreSqlDialect.kt | 27 +++++ .../support/postgresql/PostgreSqlTest.kt | 68 +++++++++++ 3 files changed, 204 insertions(+) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index f81cacfd..6daeeb8f 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -16,6 +16,7 @@ package org.ktorm.support.postgresql +import org.ktorm.database.CachedRowSet import org.ktorm.database.Database import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl @@ -139,3 +140,111 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { conflictColumns += columns } } + + +/** + * Insert or update expression, represents an insert statement with an + * `on conflict (key) do update set (...) returning ...` clause in PostgreSQL, + * capable of retrieving columns. + * + * @property table the table to be inserted. + * @property assignments the inserted column assignments. + * @property conflictColumns the index columns on which the conflict may happens. + * @property updateAssignments the updated column assignments while any key conflict exists. + * @property returningColumns the columns to returning. + */ +public data class InsertOrUpdateAndReturningColumnsExpression( + val table: TableExpression, + val assignments: List>, + val conflictColumns: List> = emptyList(), + val updateAssignments: List> = emptyList(), + val returningColumns: List>, + override val isLeafNode: Boolean = false, + override val extraProperties: Map = emptyMap() +) : SqlExpression() + +/** + * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically + * performs an update if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.insertOrUpdate(Employees) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onDuplicateKey { + * set(it.salary, it.salary + 900) + * } + * returning { + * it.id, + * it.job + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job + * ``` + * + * @since 3.4 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @return the effected row count. + */ +public fun > Database.insertOrUpdateAndReturningColumns( + table: T, + block: InsertOrUpdateAndReturningColumnsStatementBuilder.(T) -> Unit +): Pair { + val builder = InsertOrUpdateAndReturningColumnsStatementBuilder().apply { block(table) } + + val primaryKeys = table.primaryKeys + if (primaryKeys.isEmpty() && builder.conflictColumns.isEmpty()) { + val msg = + "Table '$table' doesn't have a primary key, " + + "you must specify the conflict columns when calling onDuplicateKey(col) { .. }" + throw IllegalStateException(msg) + } + + val expression = InsertOrUpdateAndReturningColumnsExpression( + table = table.asExpression(), + assignments = builder.assignments, + conflictColumns = builder.conflictColumns.ifEmpty { primaryKeys }.map { it.asExpression() }, + updateAssignments = builder.updateAssignments, + returningColumns = builder.retrievingColumns.ifEmpty { primaryKeys }.map { it.asExpression() } + ) + + return executeUpdateAndRetrieveKeys(expression) +} + +/** + * DSL builder for insert or update statements that return columns. + */ +@KtormDsl +public class InsertOrUpdateAndReturningColumnsStatementBuilder : PostgreSqlAssignmentsBuilder() { + internal val updateAssignments = ArrayList>() + internal val conflictColumns = ArrayList>() + internal val retrievingColumns = ArrayList>() + + /** + * Specify the update assignments while any key conflict exists. + */ + public fun onDuplicateKey(vararg columns: Column<*>, block: AssignmentsBuilder.() -> Unit) { + val builder = PostgreSqlAssignmentsBuilder().apply(block) + updateAssignments += builder.assignments + conflictColumns += columns + } + + public fun returning(vararg retrievingColumns: Column<*>) { + this.retrievingColumns += retrievingColumns + } + +} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 7aae5851..4a10319e 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -48,6 +48,7 @@ public open class PostgreSqlFormatter( override fun visit(expr: SqlExpression): SqlExpression { val result = when (expr) { is InsertOrUpdateExpression -> visitInsertOrUpdate(expr) + is InsertOrUpdateAndReturningColumnsExpression -> visitInsertOrUpdateAndRetrieveColumns(expr) is BulkInsertExpression -> visitBulkInsert(expr) else -> super.visit(expr) } @@ -163,6 +164,32 @@ public open class PostgreSqlFormatter( return expr } + protected open fun visitInsertOrUpdateAndRetrieveColumns(expr: InsertOrUpdateAndReturningColumnsExpression): InsertOrUpdateAndReturningColumnsExpression { + writeKeyword("insert into ") + visitTable(expr.table) + writeInsertColumnNames(expr.assignments.map { it.column }) + writeKeyword("values ") + writeInsertValues(expr.assignments) + + if (expr.updateAssignments.isNotEmpty()) { + writeKeyword("on conflict ") + writeInsertColumnNames(expr.conflictColumns) + writeKeyword("do update set ") + visitColumnAssignments(expr.updateAssignments) + } + + if (expr.returningColumns.isNotEmpty()) { + writeKeyword(" returning ") + expr.returningColumns.forEachIndexed { i, column -> + if (i > 0) write(", ") + checkColumnName(column.name) + write(column.name.quoted) + } + } + + return expr + } + protected open fun visitBulkInsert(expr: BulkInsertExpression): BulkInsertExpression { writeKeyword("insert into ") visitTable(expr.table) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 2a026e7d..861a3ab2 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -161,6 +161,74 @@ class PostgreSqlTest : BaseTest() { assert(database.employees.count() == 6) } + @Test + fun testInsertOrUpdateAndRetrieveColumns() { + val t1 = database.insertOrUpdateAndReturningColumns(Employees) { + set(it.id, 1001) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + + onDuplicateKey { + set(it.salary, it.salary + 900) + } + + returning( + it.name, + it.id + ) + } + + assert(t1.first == 1) + t1.second.next() + assert(t1.second.getString("name") == "vince") + assert(t1.second.getInt("id") == 1001) + + val t2 = database.insertOrUpdateAndReturningColumns(Employees) { + set(it.id, 1001) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + + onDuplicateKey(it.id) { + set(it.salary, it.salary + 900) + } + + returning( + it.name, + it.id, + it.salary + ) + } + + assert(t2.first == 1) + t2.second.next() + assert(t2.second.getInt("salary") == 1900) + assert(t2.second.getString("name") == "vince") + assert(t2.second.getInt("id") == 1001) + + val t3 = database.insertOrUpdateAndReturningColumns(Employees) { + set(it.id, 1001) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + + onDuplicateKey(it.id) { + set(it.salary, it.salary + 900) + } + } + + assert(t3.first == 1) + t3.second.next() + assert(t3.second.getInt("id") == 1001) + } + @Test fun testBulkInsertOrUpdate() { database.bulkInsertOrUpdate(Employees) { From f1a887dd502e9f84e220b836f7b91dcba8ef58a2 Mon Sep 17 00:00:00 2001 From: PedroD Date: Sat, 9 Jan 2021 23:29:21 +0000 Subject: [PATCH 02/96] Adding respective bulk operations --- .../ktorm/support/postgresql/BulkInsert.kt | 211 +++++++++++++++++- .../support/postgresql/InsertOrUpdate.kt | 26 +-- .../support/postgresql/PostgreSqlDialect.kt | 38 +++- .../support/postgresql/PostgreSqlTest.kt | 81 ++++++- 4 files changed, 335 insertions(+), 21 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 5007829d..2fcaf1ae 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -16,6 +16,7 @@ package org.ktorm.support.postgresql +import org.ktorm.database.CachedRowSet import org.ktorm.database.Database import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl @@ -150,7 +151,7 @@ public fun > Database.bulkInsertOrUpdate( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } @@ -223,3 +224,211 @@ public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBu ) } } + +/** + * Bulk insert expression, represents a bulk insert statement in PostgreSQL. + * + * For example: + * + * ```sql + * insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * `on conflict (key) do update set (...) returning ... + * ``` + * + * @property table the table to be inserted. + * @property assignments column assignments of the bulk insert statement. + * @property conflictColumns the index columns on which the conflict may happens. + * @property updateAssignments the updated column assignments while key conflict exists. + * @property returningColumns the columns to returning. + */ +public data class BulkInsertReturningExpression( + val table: TableExpression, + val assignments: List>>, + val conflictColumns: List> = emptyList(), + val updateAssignments: List> = emptyList(), + val returningColumns: List>, + override val isLeafNode: Boolean = false, + override val extraProperties: Map = emptyMap() +) : SqlExpression() + +/** + * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. + * + * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL + * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance + * is much better than [batchInsert]. + * + * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * returning column1, column2, ...`. + * + * Usage: + * + * ```kotlin + * database.bulkInsert(Employees) { + * item { + * set(it.name, "jerry") + * set(it.job, "trainee") + * set(it.managerId, 1) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 50) + * set(it.departmentId, 1) + * } + * item { + * set(it.name, "linda") + * set(it.job, "assistant") + * set(it.managerId, 3) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 100) + * set(it.departmentId, 2) + * } + * + * returning ( + * it.id, + * it.job + * ) + * } + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. + * @return the effected row count. + * @see batchInsert + */ +public fun > Database.bulkInsertReturning( + table: T, block: BulkInsertReturningStatementBuilder.(T) -> Unit +): Pair { + val builder = BulkInsertReturningStatementBuilder(table).apply { block(table) } + val expression = BulkInsertReturningExpression( + table.asExpression(), + builder.assignments, + returningColumns = builder.returningColumns.ifEmpty { table.primaryKeys }.map { it.asExpression() } + ) + return executeUpdateAndRetrieveKeys(expression) +} + +/** + * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, + * and automatically performs updates if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.bulkInsertOrUpdate(Employees) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict { + * set(it.salary, it.salary + 900) + * } + * + * returning ( + * it.id, + * it.job + * ) + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, ... + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @return the effected row count. + * @see bulkInsert + */ +public fun > Database.bulkInsertOrUpdateReturning( + table: T, block: BulkInsertOrUpdateReturningStatementBuilder.(T) -> Unit +): Pair { + val builder = BulkInsertOrUpdateReturningStatementBuilder(table).apply { block(table) } + + val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } + if (conflictColumns.isEmpty()) { + val msg = + "Table '$table' doesn't have a primary key, " + + "you must specify the conflict columns when calling onConflict(col) { .. }" + throw IllegalStateException(msg) + } + + val expression = BulkInsertReturningExpression( + table = table.asExpression(), + assignments = builder.assignments, + conflictColumns = conflictColumns.map { it.asExpression() }, + updateAssignments = builder.updateAssignments, + returningColumns = builder.returningColumns.ifEmpty { table.primaryKeys }.map { it.asExpression() } + ) + + return executeUpdateAndRetrieveKeys(expression) +} + +/** + * DSL builder for bulk insert statements. + */ +@KtormDsl +public open class BulkInsertReturningStatementBuilder>(internal val table: T) { + internal val assignments = ArrayList>>() + internal val returningColumns = ArrayList>() + + /** + * Add the assignments of a new row to the bulk insert. + */ + public fun item(block: AssignmentsBuilder.() -> Unit) { + val builder = PostgreSqlAssignmentsBuilder().apply(block) + + if (assignments.isEmpty() + || assignments[0].map { it.column.name } == builder.assignments.map { it.column.name } + ) { + assignments += builder.assignments + } else { + throw IllegalArgumentException("Every item in a batch operation must be the same.") + } + } + + public fun returning(vararg returningColumns: Column<*>) { + this.returningColumns += returningColumns + } +} + +/** + * DSL builder for bulk insert or update statements. + */ +@KtormDsl +public class BulkInsertOrUpdateReturningStatementBuilder>(table: T) : + BulkInsertStatementBuilder(table) { + internal val updateAssignments = ArrayList>() + internal val conflictColumns = ArrayList>() + internal val returningColumns = ArrayList>() + + /** + * Specify the update assignments while any key conflict exists. + */ + public fun onConflict(vararg columns: Column<*>, block: BulkInsertOrUpdateOnConflictClauseBuilder.() -> Unit) { + val builder = BulkInsertOrUpdateOnConflictClauseBuilder().apply(block) + updateAssignments += builder.assignments + conflictColumns += columns + } + + public fun returning(vararg returningColumns: Column<*>) { + this.returningColumns += returningColumns + } +} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 6daeeb8f..77abd9a8 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -141,7 +141,6 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { } } - /** * Insert or update expression, represents an insert statement with an * `on conflict (key) do update set (...) returning ...` clause in PostgreSQL, @@ -153,7 +152,7 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { * @property updateAssignments the updated column assignments while any key conflict exists. * @property returningColumns the columns to returning. */ -public data class InsertOrUpdateAndReturningColumnsExpression( +public data class InsertOrUpdateReturningColumnsExpression( val table: TableExpression, val assignments: List>, val conflictColumns: List> = emptyList(), @@ -180,10 +179,10 @@ public data class InsertOrUpdateAndReturningColumnsExpression( * onDuplicateKey { * set(it.salary, it.salary + 900) * } - * returning { + * returning ( * it.id, * it.job - * } + * ) * } * ``` * @@ -200,11 +199,11 @@ public data class InsertOrUpdateAndReturningColumnsExpression( * @param block the DSL block used to construct the expression. * @return the effected row count. */ -public fun > Database.insertOrUpdateAndReturningColumns( +public fun > Database.insertOrUpdateReturningColumns( table: T, - block: InsertOrUpdateAndReturningColumnsStatementBuilder.(T) -> Unit + block: InsertOrUpdateReturningColumnsStatementBuilder.(T) -> Unit ): Pair { - val builder = InsertOrUpdateAndReturningColumnsStatementBuilder().apply { block(table) } + val builder = InsertOrUpdateReturningColumnsStatementBuilder().apply { block(table) } val primaryKeys = table.primaryKeys if (primaryKeys.isEmpty() && builder.conflictColumns.isEmpty()) { @@ -214,12 +213,12 @@ public fun > Database.insertOrUpdateAndReturningColumns( throw IllegalStateException(msg) } - val expression = InsertOrUpdateAndReturningColumnsExpression( + val expression = InsertOrUpdateReturningColumnsExpression( table = table.asExpression(), assignments = builder.assignments, conflictColumns = builder.conflictColumns.ifEmpty { primaryKeys }.map { it.asExpression() }, updateAssignments = builder.updateAssignments, - returningColumns = builder.retrievingColumns.ifEmpty { primaryKeys }.map { it.asExpression() } + returningColumns = builder.returningColumns.ifEmpty { primaryKeys }.map { it.asExpression() } ) return executeUpdateAndRetrieveKeys(expression) @@ -229,10 +228,10 @@ public fun > Database.insertOrUpdateAndReturningColumns( * DSL builder for insert or update statements that return columns. */ @KtormDsl -public class InsertOrUpdateAndReturningColumnsStatementBuilder : PostgreSqlAssignmentsBuilder() { +public class InsertOrUpdateReturningColumnsStatementBuilder : PostgreSqlAssignmentsBuilder() { internal val updateAssignments = ArrayList>() internal val conflictColumns = ArrayList>() - internal val retrievingColumns = ArrayList>() + internal val returningColumns = ArrayList>() /** * Specify the update assignments while any key conflict exists. @@ -243,8 +242,7 @@ public class InsertOrUpdateAndReturningColumnsStatementBuilder : PostgreSqlAssig conflictColumns += columns } - public fun returning(vararg retrievingColumns: Column<*>) { - this.retrievingColumns += retrievingColumns + public fun returning(vararg returningColumns: Column<*>) { + this.returningColumns += returningColumns } - } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 4a10319e..8588949c 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -48,7 +48,8 @@ public open class PostgreSqlFormatter( override fun visit(expr: SqlExpression): SqlExpression { val result = when (expr) { is InsertOrUpdateExpression -> visitInsertOrUpdate(expr) - is InsertOrUpdateAndReturningColumnsExpression -> visitInsertOrUpdateAndRetrieveColumns(expr) + is InsertOrUpdateReturningColumnsExpression -> visitInsertOrUpdateReturningColumns(expr) + is BulkInsertReturningExpression -> visitBulkInsertReturningColumns(expr) is BulkInsertExpression -> visitBulkInsert(expr) else -> super.visit(expr) } @@ -164,7 +165,7 @@ public open class PostgreSqlFormatter( return expr } - protected open fun visitInsertOrUpdateAndRetrieveColumns(expr: InsertOrUpdateAndReturningColumnsExpression): InsertOrUpdateAndReturningColumnsExpression { + protected open fun visitInsertOrUpdateReturningColumns(expr: InsertOrUpdateReturningColumnsExpression): InsertOrUpdateReturningColumnsExpression { writeKeyword("insert into ") visitTable(expr.table) writeInsertColumnNames(expr.assignments.map { it.column }) @@ -190,6 +191,39 @@ public open class PostgreSqlFormatter( return expr } + protected open fun visitBulkInsertReturningColumns(expr: BulkInsertReturningExpression): BulkInsertReturningExpression { + writeKeyword("insert into ") + visitTable(expr.table) + writeInsertColumnNames(expr.assignments[0].map { it.column }) + writeKeyword("values ") + + for ((i, assignments) in expr.assignments.withIndex()) { + if (i > 0) { + removeLastBlank() + write(", ") + } + writeInsertValues(assignments) + } + + if (expr.updateAssignments.isNotEmpty()) { + writeKeyword("on conflict ") + writeInsertColumnNames(expr.conflictColumns) + writeKeyword("do update set ") + visitColumnAssignments(expr.updateAssignments) + } + + if (expr.returningColumns.isNotEmpty()) { + writeKeyword(" returning ") + expr.returningColumns.forEachIndexed { i, column -> + if (i > 0) write(", ") + checkColumnName(column.name) + write(column.name.quoted) + } + } + + return expr + } + protected open fun visitBulkInsert(expr: BulkInsertExpression): BulkInsertExpression { writeKeyword("insert into ") visitTable(expr.table) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 861a3ab2..7c115a94 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -2,6 +2,7 @@ package org.ktorm.support.postgresql import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.nullValue +import org.junit.Assert.assertEquals import org.junit.Assert.assertThat import org.junit.ClassRule import org.junit.Test @@ -162,8 +163,8 @@ class PostgreSqlTest : BaseTest() { } @Test - fun testInsertOrUpdateAndRetrieveColumns() { - val t1 = database.insertOrUpdateAndReturningColumns(Employees) { + fun testInsertOrUpdateReturningColumns() { + val t1 = database.insertOrUpdateReturningColumns(Employees) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -186,7 +187,7 @@ class PostgreSqlTest : BaseTest() { assert(t1.second.getString("name") == "vince") assert(t1.second.getInt("id") == 1001) - val t2 = database.insertOrUpdateAndReturningColumns(Employees) { + val t2 = database.insertOrUpdateReturningColumns(Employees) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -211,7 +212,7 @@ class PostgreSqlTest : BaseTest() { assert(t2.second.getString("name") == "vince") assert(t2.second.getInt("id") == 1001) - val t3 = database.insertOrUpdateAndReturningColumns(Employees) { + val t3 = database.insertOrUpdateReturningColumns(Employees) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -268,6 +269,78 @@ class PostgreSqlTest : BaseTest() { } } + @Test + fun testBulkInsertReturningColumns() { + val rs = database.bulkInsertOrUpdateReturning(Employees) { + item { + set(it.id, 10001) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 50001) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + returning( + it.id, + it.job + ) + } + + assertEquals(rs.first, 2) + rs.second.next() + assertEquals(rs.second.getInt("id"), 10001) + assertEquals(rs.second.getString("job"), "trainee") + rs.second.next() + assertEquals(rs.second.getInt("id"), 50001) + assertEquals(rs.second.getString("job"), "engineer") + } + + @Test + fun testBulkInsertOrUpdateReturningColumns() { + val rs = database.bulkInsertOrUpdateReturning(Employees) { + item { + set(it.id, 1000) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 5000) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + onConflict(it.id) { + set(it.departmentId, excluded(it.departmentId)) + set(it.salary, it.salary + 1000) + } + returning( + it.id, + it.job + ) + } + + assertEquals(rs.first, 2) + rs.second.next() + assertEquals(rs.second.getInt("id"), 1000) + assertEquals(rs.second.getString("job"), "trainee") + rs.second.next() + assertEquals(rs.second.getInt("id"), 5000) + assertEquals(rs.second.getString("job"), "engineer") + } + @Test fun testInsertAndGenerateKey() { val id = database.insertAndGenerateKey(Employees) { From b3a220c361167799677e5bff243cbc638ebccc84 Mon Sep 17 00:00:00 2001 From: PedroD Date: Sat, 9 Jan 2021 23:43:51 +0000 Subject: [PATCH 03/96] Fixing code format --- .../kotlin/org/ktorm/support/postgresql/BulkInsert.kt | 6 ++++++ .../kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt | 3 +++ .../org/ktorm/support/postgresql/PostgreSqlDialect.kt | 8 ++++++-- .../kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt | 4 ++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 2fcaf1ae..a3a217dc 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -404,6 +404,9 @@ public open class BulkInsertReturningStatementBuilder>(internal } } + /** + * Specify the columns to return + */ public fun returning(vararg returningColumns: Column<*>) { this.returningColumns += returningColumns } @@ -428,6 +431,9 @@ public class BulkInsertOrUpdateReturningStatementBuilder>(table conflictColumns += columns } + /** + * Specify the columns to return + */ public fun returning(vararg returningColumns: Column<*>) { this.returningColumns += returningColumns } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 77abd9a8..5c5e5303 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -242,6 +242,9 @@ public class InsertOrUpdateReturningColumnsStatementBuilder : PostgreSqlAssignme conflictColumns += columns } + /** + * Specify the columns to return + */ public fun returning(vararg returningColumns: Column<*>) { this.returningColumns += returningColumns } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 8588949c..282978df 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -165,7 +165,9 @@ public open class PostgreSqlFormatter( return expr } - protected open fun visitInsertOrUpdateReturningColumns(expr: InsertOrUpdateReturningColumnsExpression): InsertOrUpdateReturningColumnsExpression { + protected open fun visitInsertOrUpdateReturningColumns( + expr: InsertOrUpdateReturningColumnsExpression + ): InsertOrUpdateReturningColumnsExpression { writeKeyword("insert into ") visitTable(expr.table) writeInsertColumnNames(expr.assignments.map { it.column }) @@ -191,7 +193,9 @@ public open class PostgreSqlFormatter( return expr } - protected open fun visitBulkInsertReturningColumns(expr: BulkInsertReturningExpression): BulkInsertReturningExpression { + protected open fun visitBulkInsertReturningColumns( + expr: BulkInsertReturningExpression + ): BulkInsertReturningExpression { writeKeyword("insert into ") visitTable(expr.table) writeInsertColumnNames(expr.assignments[0].map { it.column }) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 7c115a94..c146d68f 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -252,7 +252,7 @@ class PostgreSqlTest : BaseTest() { onConflict(it.id) { set(it.job, it.job) set(it.departmentId, excluded(it.departmentId)) - set(it.salary, it.salary + 1000) + set(it.salary, excluded(it.salary) + 1000) } } @@ -271,7 +271,7 @@ class PostgreSqlTest : BaseTest() { @Test fun testBulkInsertReturningColumns() { - val rs = database.bulkInsertOrUpdateReturning(Employees) { + val rs = database.bulkInsertReturning(Employees) { item { set(it.id, 10001) set(it.name, "vince") From 569b5b75529a5cccea35130765790e15c118b2e7 Mon Sep 17 00:00:00 2001 From: PedroD Date: Sat, 9 Jan 2021 23:53:13 +0000 Subject: [PATCH 04/96] Fixing end of sentence --- .../main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt | 4 ++-- .../kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index a3a217dc..6370deb9 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -405,7 +405,7 @@ public open class BulkInsertReturningStatementBuilder>(internal } /** - * Specify the columns to return + * Specify the columns to return. */ public fun returning(vararg returningColumns: Column<*>) { this.returningColumns += returningColumns @@ -432,7 +432,7 @@ public class BulkInsertOrUpdateReturningStatementBuilder>(table } /** - * Specify the columns to return + * Specify the columns to return. */ public fun returning(vararg returningColumns: Column<*>) { this.returningColumns += returningColumns diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 5c5e5303..b29d6242 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -243,7 +243,7 @@ public class InsertOrUpdateReturningColumnsStatementBuilder : PostgreSqlAssignme } /** - * Specify the columns to return + * Specify the columns to return. */ public fun returning(vararg returningColumns: Column<*>) { this.returningColumns += returningColumns From a224860d39373c8c676b42e32d89a62a2e305b50 Mon Sep 17 00:00:00 2001 From: PedroD Date: Sun, 10 Jan 2021 00:04:10 +0000 Subject: [PATCH 05/96] Fixing accidental change in test --- .../test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index c146d68f..3abbf4ed 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -252,7 +252,7 @@ class PostgreSqlTest : BaseTest() { onConflict(it.id) { set(it.job, it.job) set(it.departmentId, excluded(it.departmentId)) - set(it.salary, excluded(it.salary) + 1000) + set(it.salary, it.salary + 1000) } } From 157f581fbcffba3c88ae3962dbff9a406ee39962 Mon Sep 17 00:00:00 2001 From: PedroD Date: Sun, 10 Jan 2021 13:09:21 +0000 Subject: [PATCH 06/96] Making function names consistent --- .../org/ktorm/support/postgresql/BulkInsert.kt | 4 ++-- .../org/ktorm/support/postgresql/InsertOrUpdate.kt | 4 ++-- .../org/ktorm/support/postgresql/PostgreSqlTest.kt | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 6370deb9..30156065 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -264,7 +264,7 @@ public data class BulkInsertReturningExpression( * Usage: * * ```kotlin - * database.bulkInsert(Employees) { + * database.bulkInsertReturning(Employees) { * item { * set(it.name, "jerry") * set(it.job, "trainee") @@ -314,7 +314,7 @@ public fun > Database.bulkInsertReturning( * Usage: * * ```kotlin - * database.bulkInsertOrUpdate(Employees) { + * database.bulkInsertOrUpdateReturning(Employees) { * item { * set(it.id, 1) * set(it.name, "vince") diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index b29d6242..9c29dbae 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -169,7 +169,7 @@ public data class InsertOrUpdateReturningColumnsExpression( * Usage: * * ```kotlin - * database.insertOrUpdate(Employees) { + * database.insertOrUpdateReturning(Employees) { * set(it.id, 1) * set(it.name, "vince") * set(it.job, "engineer") @@ -199,7 +199,7 @@ public data class InsertOrUpdateReturningColumnsExpression( * @param block the DSL block used to construct the expression. * @return the effected row count. */ -public fun > Database.insertOrUpdateReturningColumns( +public fun > Database.insertOrUpdateReturning( table: T, block: InsertOrUpdateReturningColumnsStatementBuilder.(T) -> Unit ): Pair { diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 3abbf4ed..8ecea806 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -163,8 +163,8 @@ class PostgreSqlTest : BaseTest() { } @Test - fun testInsertOrUpdateReturningColumns() { - val t1 = database.insertOrUpdateReturningColumns(Employees) { + fun testInsertOrUpdateReturning() { + val t1 = database.insertOrUpdateReturning(Employees) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -187,7 +187,7 @@ class PostgreSqlTest : BaseTest() { assert(t1.second.getString("name") == "vince") assert(t1.second.getInt("id") == 1001) - val t2 = database.insertOrUpdateReturningColumns(Employees) { + val t2 = database.insertOrUpdateReturning(Employees) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -212,7 +212,7 @@ class PostgreSqlTest : BaseTest() { assert(t2.second.getString("name") == "vince") assert(t2.second.getInt("id") == 1001) - val t3 = database.insertOrUpdateReturningColumns(Employees) { + val t3 = database.insertOrUpdateReturning(Employees) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -270,7 +270,7 @@ class PostgreSqlTest : BaseTest() { } @Test - fun testBulkInsertReturningColumns() { + fun testBulkInsertReturning() { val rs = database.bulkInsertReturning(Employees) { item { set(it.id, 10001) @@ -304,7 +304,7 @@ class PostgreSqlTest : BaseTest() { } @Test - fun testBulkInsertOrUpdateReturningColumns() { + fun testBulkInsertOrUpdateReturning() { val rs = database.bulkInsertOrUpdateReturning(Employees) { item { set(it.id, 1000) From 7cfc55c5934d8536c5e49bafba542052e76ed31a Mon Sep 17 00:00:00 2001 From: PedroD Date: Wed, 20 Jan 2021 17:53:27 +0000 Subject: [PATCH 07/96] Removing redundant expressions --- .../ktorm/support/postgresql/BulkInsert.kt | 31 ++----------------- .../support/postgresql/InsertOrUpdate.kt | 26 ++-------------- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 30156065..590f2770 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -48,6 +48,7 @@ public data class BulkInsertExpression( val assignments: List>>, val conflictColumns: List> = emptyList(), val updateAssignments: List> = emptyList(), + val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() ) : SqlExpression() @@ -225,32 +226,6 @@ public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBu } } -/** - * Bulk insert expression, represents a bulk insert statement in PostgreSQL. - * - * For example: - * - * ```sql - * insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... - * `on conflict (key) do update set (...) returning ... - * ``` - * - * @property table the table to be inserted. - * @property assignments column assignments of the bulk insert statement. - * @property conflictColumns the index columns on which the conflict may happens. - * @property updateAssignments the updated column assignments while key conflict exists. - * @property returningColumns the columns to returning. - */ -public data class BulkInsertReturningExpression( - val table: TableExpression, - val assignments: List>>, - val conflictColumns: List> = emptyList(), - val updateAssignments: List> = emptyList(), - val returningColumns: List>, - override val isLeafNode: Boolean = false, - override val extraProperties: Map = emptyMap() -) : SqlExpression() - /** * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. * @@ -299,7 +274,7 @@ public fun > Database.bulkInsertReturning( table: T, block: BulkInsertReturningStatementBuilder.(T) -> Unit ): Pair { val builder = BulkInsertReturningStatementBuilder(table).apply { block(table) } - val expression = BulkInsertReturningExpression( + val expression = BulkInsertExpression( table.asExpression(), builder.assignments, returningColumns = builder.returningColumns.ifEmpty { table.primaryKeys }.map { it.asExpression() } @@ -370,7 +345,7 @@ public fun > Database.bulkInsertOrUpdateReturning( throw IllegalStateException(msg) } - val expression = BulkInsertReturningExpression( + val expression = BulkInsertExpression( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 9c29dbae..2ef18086 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -41,6 +41,7 @@ public data class InsertOrUpdateExpression( val assignments: List>, val conflictColumns: List> = emptyList(), val updateAssignments: List> = emptyList(), + val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() ) : SqlExpression() @@ -141,27 +142,6 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { } } -/** - * Insert or update expression, represents an insert statement with an - * `on conflict (key) do update set (...) returning ...` clause in PostgreSQL, - * capable of retrieving columns. - * - * @property table the table to be inserted. - * @property assignments the inserted column assignments. - * @property conflictColumns the index columns on which the conflict may happens. - * @property updateAssignments the updated column assignments while any key conflict exists. - * @property returningColumns the columns to returning. - */ -public data class InsertOrUpdateReturningColumnsExpression( - val table: TableExpression, - val assignments: List>, - val conflictColumns: List> = emptyList(), - val updateAssignments: List> = emptyList(), - val returningColumns: List>, - override val isLeafNode: Boolean = false, - override val extraProperties: Map = emptyMap() -) : SqlExpression() - /** * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically * performs an update if any conflict exists. @@ -194,7 +174,7 @@ public data class InsertOrUpdateReturningColumnsExpression( * returning id, job * ``` * - * @since 3.4 + * @since 3.4.0 * @param table the table to be inserted. * @param block the DSL block used to construct the expression. * @return the effected row count. @@ -213,7 +193,7 @@ public fun > Database.insertOrUpdateReturning( throw IllegalStateException(msg) } - val expression = InsertOrUpdateReturningColumnsExpression( + val expression = InsertOrUpdateExpression( table = table.asExpression(), assignments = builder.assignments, conflictColumns = builder.conflictColumns.ifEmpty { primaryKeys }.map { it.asExpression() }, From 25852c325e1532817f43d92a62e24d98b86b140c Mon Sep 17 00:00:00 2001 From: PedroD Date: Mon, 25 Jan 2021 17:25:06 +0000 Subject: [PATCH 08/96] Adding static access overloads to modified methods --- .../ktorm/support/postgresql/BulkInsert.kt | 88 +++++++--- .../support/postgresql/InsertOrUpdate.kt | 77 +++++++-- .../support/postgresql/PostgreSqlDialect.kt | 49 +----- .../support/postgresql/PostgreSqlTest.kt | 151 +++++++++++++----- 4 files changed, 234 insertions(+), 131 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 590f2770..acf44676 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -18,6 +18,7 @@ package org.ktorm.support.postgresql import org.ktorm.database.CachedRowSet import org.ktorm.database.Database +import org.ktorm.database.asIterable import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl import org.ktorm.dsl.batchInsert @@ -239,7 +240,7 @@ public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBu * Usage: * * ```kotlin - * database.bulkInsertReturning(Employees) { + * database.bulkInsertReturning(Employees, Pair(Employees.id, Employees.job)) { * item { * set(it.name, "jerry") * set(it.job, "trainee") @@ -256,11 +257,6 @@ public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBu * set(it.salary, 100) * set(it.departmentId, 2) * } - * - * returning ( - * it.id, - * it.job - * ) * } * ``` * @@ -270,15 +266,74 @@ public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBu * @return the effected row count. * @see batchInsert */ -public fun > Database.bulkInsertReturning( - table: T, block: BulkInsertReturningStatementBuilder.(T) -> Unit +public fun , R : Any> Database.bulkInsertReturning( + table: T, + returningColumn: Column, + block: BulkInsertReturningStatementBuilder.(T) -> Unit +): List { + val (_, rowSet) = this.bulkInsertReturningAux( + table, + listOf(returningColumn), + block + ) + + return rowSet.asIterable().map { row -> + returningColumn.sqlType.getResult(row, 1) + } +} + +public fun , R1 : Any, R2 : Any> Database.bulkInsertReturning( + table: T, + returningColumns: Pair, Column>, + block: BulkInsertReturningStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Pair( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2) + ) + } +} + +public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertReturning( + table: T, + returningColumns: Triple, Column, Column>, + block: BulkInsertReturningStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Triple( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2), + returningColumns.third.sqlType.getResult(row, 3) + ) + } +} + +private fun > Database.bulkInsertReturningAux( + table: T, + returningColumns: List>, + block: BulkInsertReturningStatementBuilder.(T) -> Unit ): Pair { val builder = BulkInsertReturningStatementBuilder(table).apply { block(table) } + val expression = BulkInsertExpression( table.asExpression(), builder.assignments, - returningColumns = builder.returningColumns.ifEmpty { table.primaryKeys }.map { it.asExpression() } + returningColumns = returningColumns.map { it.asExpression() } ) + return executeUpdateAndRetrieveKeys(expression) } @@ -289,7 +344,7 @@ public fun > Database.bulkInsertReturning( * Usage: * * ```kotlin - * database.bulkInsertOrUpdateReturning(Employees) { + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name)) { * item { * set(it.id, 1) * set(it.name, "vince") @@ -309,11 +364,6 @@ public fun > Database.bulkInsertReturning( * onConflict { * set(it.salary, it.salary + 900) * } - * - * returning ( - * it.id, - * it.job - * ) * } * ``` * @@ -362,7 +412,6 @@ public fun > Database.bulkInsertOrUpdateReturning( @KtormDsl public open class BulkInsertReturningStatementBuilder>(internal val table: T) { internal val assignments = ArrayList>>() - internal val returningColumns = ArrayList>() /** * Add the assignments of a new row to the bulk insert. @@ -378,13 +427,6 @@ public open class BulkInsertReturningStatementBuilder>(internal throw IllegalArgumentException("Every item in a batch operation must be the same.") } } - - /** - * Specify the columns to return. - */ - public fun returning(vararg returningColumns: Column<*>) { - this.returningColumns += returningColumns - } } /** diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 2ef18086..8f276d4e 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -18,6 +18,7 @@ package org.ktorm.support.postgresql import org.ktorm.database.CachedRowSet import org.ktorm.database.Database +import org.ktorm.database.asIterable import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl import org.ktorm.expression.ColumnAssignmentExpression @@ -87,7 +88,7 @@ public fun > Database.insertOrUpdate( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } @@ -149,7 +150,7 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { * Usage: * * ```kotlin - * database.insertOrUpdateReturning(Employees) { + * database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { * set(it.id, 1) * set(it.name, "vince") * set(it.job, "engineer") @@ -159,10 +160,6 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { * onDuplicateKey { * set(it.salary, it.salary + 900) * } - * returning ( - * it.id, - * it.job - * ) * } * ``` * @@ -179,8 +176,64 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { * @param block the DSL block used to construct the expression. * @return the effected row count. */ -public fun > Database.insertOrUpdateReturning( +public fun , R : Any> Database.insertOrUpdateReturning( table: T, + returningColumn: Column, + block: InsertOrUpdateReturningColumnsStatementBuilder.(T) -> Unit +): R? { + val (_, rowSet) = this.insertOrUpdateReturningAux( + table, + listOfNotNull(returningColumn), + block + ) + + return rowSet.asIterable().map { row -> + returningColumn.sqlType.getResult(row, 1) + }.first() +} + +public fun , R1 : Any, R2 : Any> Database.insertOrUpdateReturning( + table: T, + returningColumns: Pair, Column>, + block: InsertOrUpdateReturningColumnsStatementBuilder.(T) -> Unit +): Pair { + val (_, rowSet) = this.insertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Pair( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2) + ) + }.first() +} + +public fun , R1 : Any, R2 : Any, R3 : Any> Database.insertOrUpdateReturning( + table: T, + returningColumns: Triple, Column, Column>, + block: InsertOrUpdateReturningColumnsStatementBuilder.(T) -> Unit +): Triple { + val (_, rowSet) = this.insertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Triple( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2), + returningColumns.third.sqlType.getResult(row, 3) + ) + }.first() +} + +private fun > Database.insertOrUpdateReturningAux( + table: T, + returningColumns: List>, block: InsertOrUpdateReturningColumnsStatementBuilder.(T) -> Unit ): Pair { val builder = InsertOrUpdateReturningColumnsStatementBuilder().apply { block(table) } @@ -198,7 +251,7 @@ public fun > Database.insertOrUpdateReturning( assignments = builder.assignments, conflictColumns = builder.conflictColumns.ifEmpty { primaryKeys }.map { it.asExpression() }, updateAssignments = builder.updateAssignments, - returningColumns = builder.returningColumns.ifEmpty { primaryKeys }.map { it.asExpression() } + returningColumns = returningColumns.map { it.asExpression() } ) return executeUpdateAndRetrieveKeys(expression) @@ -211,7 +264,6 @@ public fun > Database.insertOrUpdateReturning( public class InsertOrUpdateReturningColumnsStatementBuilder : PostgreSqlAssignmentsBuilder() { internal val updateAssignments = ArrayList>() internal val conflictColumns = ArrayList>() - internal val returningColumns = ArrayList>() /** * Specify the update assignments while any key conflict exists. @@ -221,11 +273,4 @@ public class InsertOrUpdateReturningColumnsStatementBuilder : PostgreSqlAssignme updateAssignments += builder.assignments conflictColumns += columns } - - /** - * Specify the columns to return. - */ - public fun returning(vararg returningColumns: Column<*>) { - this.returningColumns += returningColumns - } } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 282978df..8fa6731b 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -48,8 +48,6 @@ public open class PostgreSqlFormatter( override fun visit(expr: SqlExpression): SqlExpression { val result = when (expr) { is InsertOrUpdateExpression -> visitInsertOrUpdate(expr) - is InsertOrUpdateReturningColumnsExpression -> visitInsertOrUpdateReturningColumns(expr) - is BulkInsertReturningExpression -> visitBulkInsertReturningColumns(expr) is BulkInsertExpression -> visitBulkInsert(expr) else -> super.visit(expr) } @@ -162,25 +160,6 @@ public open class PostgreSqlFormatter( visitColumnAssignments(expr.updateAssignments) } - return expr - } - - protected open fun visitInsertOrUpdateReturningColumns( - expr: InsertOrUpdateReturningColumnsExpression - ): InsertOrUpdateReturningColumnsExpression { - writeKeyword("insert into ") - visitTable(expr.table) - writeInsertColumnNames(expr.assignments.map { it.column }) - writeKeyword("values ") - writeInsertValues(expr.assignments) - - if (expr.updateAssignments.isNotEmpty()) { - writeKeyword("on conflict ") - writeInsertColumnNames(expr.conflictColumns) - writeKeyword("do update set ") - visitColumnAssignments(expr.updateAssignments) - } - if (expr.returningColumns.isNotEmpty()) { writeKeyword(" returning ") expr.returningColumns.forEachIndexed { i, column -> @@ -193,9 +172,7 @@ public open class PostgreSqlFormatter( return expr } - protected open fun visitBulkInsertReturningColumns( - expr: BulkInsertReturningExpression - ): BulkInsertReturningExpression { + protected open fun visitBulkInsert(expr: BulkInsertExpression): BulkInsertExpression { writeKeyword("insert into ") visitTable(expr.table) writeInsertColumnNames(expr.assignments[0].map { it.column }) @@ -227,30 +204,6 @@ public open class PostgreSqlFormatter( return expr } - - protected open fun visitBulkInsert(expr: BulkInsertExpression): BulkInsertExpression { - writeKeyword("insert into ") - visitTable(expr.table) - writeInsertColumnNames(expr.assignments[0].map { it.column }) - writeKeyword("values ") - - for ((i, assignments) in expr.assignments.withIndex()) { - if (i > 0) { - removeLastBlank() - write(", ") - } - writeInsertValues(assignments) - } - - if (expr.updateAssignments.isNotEmpty()) { - writeKeyword("on conflict ") - writeInsertColumnNames(expr.conflictColumns) - writeKeyword("do update set ") - visitColumnAssignments(expr.updateAssignments) - } - - return expr - } } /** diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 8ecea806..97bf9efd 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -164,30 +164,31 @@ class PostgreSqlTest : BaseTest() { @Test fun testInsertOrUpdateReturning() { - val t1 = database.insertOrUpdateReturning(Employees) { - set(it.id, 1001) - set(it.name, "vince") + database.insertOrUpdateReturning( + Employees, + Employees.id + ) { + set(it.id, 1009) + set(it.name, "pedro") set(it.job, "engineer") - set(it.salary, 1000) + set(it.salary, 1500) set(it.hireDate, LocalDate.now()) set(it.departmentId, 1) onDuplicateKey { set(it.salary, it.salary + 900) } - - returning( - it.name, - it.id - ) + }.let { createdId -> + assert(createdId == 1009) } - assert(t1.first == 1) - t1.second.next() - assert(t1.second.getString("name") == "vince") - assert(t1.second.getInt("id") == 1001) - - val t2 = database.insertOrUpdateReturning(Employees) { + database.insertOrUpdateReturning( + Employees, + Pair( + Employees.id, + Employees.name + ) + ) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -195,24 +196,22 @@ class PostgreSqlTest : BaseTest() { set(it.hireDate, LocalDate.now()) set(it.departmentId, 1) - onDuplicateKey(it.id) { + onDuplicateKey { set(it.salary, it.salary + 900) } - - returning( - it.name, - it.id, - it.salary - ) + }.let { (createdId, createdName) -> + assert(createdId == 1001) + assert(createdName == "vince") } - assert(t2.first == 1) - t2.second.next() - assert(t2.second.getInt("salary") == 1900) - assert(t2.second.getString("name") == "vince") - assert(t2.second.getInt("id") == 1001) - - val t3 = database.insertOrUpdateReturning(Employees) { + database.insertOrUpdateReturning( + Employees, + Triple( + Employees.id, + Employees.name, + Employees.salary + ) + ) { set(it.id, 1001) set(it.name, "vince") set(it.job, "engineer") @@ -223,11 +222,11 @@ class PostgreSqlTest : BaseTest() { onDuplicateKey(it.id) { set(it.salary, it.salary + 900) } + }.let { (createdId, createdName, createdSalary) -> + assert(createdId == 1001) + assert(createdName == "vince") + assert(createdSalary == 1900L) } - - assert(t3.first == 1) - t3.second.next() - assert(t3.second.getInt("id") == 1001) } @Test @@ -271,7 +270,10 @@ class PostgreSqlTest : BaseTest() { @Test fun testBulkInsertReturning() { - val rs = database.bulkInsertReturning(Employees) { + database.bulkInsertReturning( + Employees, + Employees.id + ) { item { set(it.id, 10001) set(it.name, "vince") @@ -288,19 +290,80 @@ class PostgreSqlTest : BaseTest() { set(it.hireDate, LocalDate.now()) set(it.departmentId, 2) } - returning( - it.id, - it.job + }.let { createdIds -> + assert(createdIds.size == 2) + assert( + listOf( + 10001, + 50001 + ) == createdIds ) } - assertEquals(rs.first, 2) - rs.second.next() - assertEquals(rs.second.getInt("id"), 10001) - assertEquals(rs.second.getString("job"), "trainee") - rs.second.next() - assertEquals(rs.second.getInt("id"), 50001) - assertEquals(rs.second.getString("job"), "engineer") + database.bulkInsertReturning( + Employees, + Pair( + Employees.id, + Employees.name + ) + ) { + item { + set(it.id, 10002) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 50002) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { created -> + assert( + listOf( + (10002 to "vince"), + (50002 to "vince") + ) == created + ) + } + + database.bulkInsertReturning( + Employees, + Triple( + Employees.id, + Employees.name, + Employees.job + ) + ) { + item { + set(it.id, 10003) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 50003) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { created -> + assert( + listOf( + Triple(10003,"vince","trainee"), + Triple(50003,"vince","engineer") + ) == created + ) + } } @Test From b556513d0e0942753446be05aa58bcdf8823fe8b Mon Sep 17 00:00:00 2001 From: PedroD Date: Mon, 25 Jan 2021 18:09:56 +0000 Subject: [PATCH 09/96] Adding documentation --- .../ktorm/support/postgresql/BulkInsert.kt | 307 ++++++++++++++---- .../support/postgresql/InsertOrUpdate.kt | 75 ++++- .../support/postgresql/PostgreSqlTest.kt | 26 +- 3 files changed, 329 insertions(+), 79 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index acf44676..7fc7a245 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -235,12 +235,12 @@ public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBu * is much better than [batchInsert]. * * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... - * returning column1, column2, ...`. + * returning id`. * * Usage: * * ```kotlin - * database.bulkInsertReturning(Employees, Pair(Employees.id, Employees.job)) { + * database.bulkInsertReturning(Employees, Employees.id) { * item { * set(it.name, "jerry") * set(it.job, "trainee") @@ -263,13 +263,14 @@ public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBu * @since 3.4.0 * @param table the table to be inserted. * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. - * @return the effected row count. + * @param returningColumn the column to return + * @return the returning column value. * @see batchInsert */ public fun , R : Any> Database.bulkInsertReturning( table: T, returningColumn: Column, - block: BulkInsertReturningStatementBuilder.(T) -> Unit + block: BulkInsertStatementBuilder.(T) -> Unit ): List { val (_, rowSet) = this.bulkInsertReturningAux( table, @@ -282,10 +283,50 @@ public fun , R : Any> Database.bulkInsertReturning( } } +/** + * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. + * + * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL + * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance + * is much better than [batchInsert]. + * + * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * returning id, job`. + * + * Usage: + * + * ```kotlin + * database.bulkInsertReturning(Employees, Pair(Employees.id, Employees.job)) { + * item { + * set(it.name, "jerry") + * set(it.job, "trainee") + * set(it.managerId, 1) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 50) + * set(it.departmentId, 1) + * } + * item { + * set(it.name, "linda") + * set(it.job, "assistant") + * set(it.managerId, 3) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 100) + * set(it.departmentId, 2) + * } + * } + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. + * @param returningColumns the columns to return + * @return the returning columns' values. + * @see batchInsert + */ public fun , R1 : Any, R2 : Any> Database.bulkInsertReturning( table: T, returningColumns: Pair, Column>, - block: BulkInsertReturningStatementBuilder.(T) -> Unit + block: BulkInsertStatementBuilder.(T) -> Unit ): List> { val (_, rowSet) = this.bulkInsertReturningAux( table, @@ -301,10 +342,50 @@ public fun , R1 : Any, R2 : Any> Database.bulkInsertReturning( } } +/** + * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. + * + * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL + * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance + * is much better than [batchInsert]. + * + * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * returning id, job, salary`. + * + * Usage: + * + * ```kotlin + * database.bulkInsertReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * item { + * set(it.name, "jerry") + * set(it.job, "trainee") + * set(it.managerId, 1) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 50) + * set(it.departmentId, 1) + * } + * item { + * set(it.name, "linda") + * set(it.job, "assistant") + * set(it.managerId, 3) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 100) + * set(it.departmentId, 2) + * } + * } + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. + * @param returningColumns the columns to return + * @return the returning columns' values. + * @see batchInsert + */ public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertReturning( table: T, returningColumns: Triple, Column, Column>, - block: BulkInsertReturningStatementBuilder.(T) -> Unit + block: BulkInsertStatementBuilder.(T) -> Unit ): List> { val (_, rowSet) = this.bulkInsertReturningAux( table, @@ -324,9 +405,9 @@ public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertR private fun > Database.bulkInsertReturningAux( table: T, returningColumns: List>, - block: BulkInsertReturningStatementBuilder.(T) -> Unit + block: BulkInsertStatementBuilder.(T) -> Unit ): Pair { - val builder = BulkInsertReturningStatementBuilder(table).apply { block(table) } + val builder = BulkInsertStatementBuilder(table).apply { block(table) } val expression = BulkInsertExpression( table.asExpression(), @@ -379,13 +460,163 @@ private fun > Database.bulkInsertReturningAux( * @since 3.4.0 * @param table the table to be inserted. * @param block the DSL block used to construct the expression. - * @return the effected row count. + * @param returningColumn the column to return + * @return the returning column value. * @see bulkInsert */ -public fun > Database.bulkInsertOrUpdateReturning( - table: T, block: BulkInsertOrUpdateReturningStatementBuilder.(T) -> Unit +public fun , R : Any> Database.bulkInsertOrUpdateReturning( + table: T, + returningColumn: Column, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List { + val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( + table, + listOf(returningColumn), + block + ) + + return rowSet.asIterable().map { row -> + returningColumn.sqlType.getResult(row, 1) + } +} + +/** + * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, + * and automatically performs updates if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, ... + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @param returningColumns the column to return + * @return the returning columns' values. + * @see bulkInsert + */ +public fun , R1 : Any, R2 : Any> Database.bulkInsertOrUpdateReturning( + table: T, + returningColumns: Pair, Column>, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Pair( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2) + ) + } +} + +/** + * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, + * and automatically performs updates if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name, Employees.salary)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, ... + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @param returningColumns the column to return + * @return the returning columns' values. + * @see bulkInsert + */ +public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertOrUpdateReturning( + table: T, + returningColumns: Triple, Column, Column>, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Triple( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2), + returningColumns.third.sqlType.getResult(row, 3) + ) + } +} + +private fun > Database.bulkInsertOrUpdateReturningAux( + table: T, + returningColumns: List>, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit ): Pair { - val builder = BulkInsertOrUpdateReturningStatementBuilder(table).apply { block(table) } + val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } if (conflictColumns.isEmpty()) { @@ -400,58 +631,8 @@ public fun > Database.bulkInsertOrUpdateReturning( assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, updateAssignments = builder.updateAssignments, - returningColumns = builder.returningColumns.ifEmpty { table.primaryKeys }.map { it.asExpression() } + returningColumns = returningColumns.map { it.asExpression() } ) return executeUpdateAndRetrieveKeys(expression) } - -/** - * DSL builder for bulk insert statements. - */ -@KtormDsl -public open class BulkInsertReturningStatementBuilder>(internal val table: T) { - internal val assignments = ArrayList>>() - - /** - * Add the assignments of a new row to the bulk insert. - */ - public fun item(block: AssignmentsBuilder.() -> Unit) { - val builder = PostgreSqlAssignmentsBuilder().apply(block) - - if (assignments.isEmpty() - || assignments[0].map { it.column.name } == builder.assignments.map { it.column.name } - ) { - assignments += builder.assignments - } else { - throw IllegalArgumentException("Every item in a batch operation must be the same.") - } - } -} - -/** - * DSL builder for bulk insert or update statements. - */ -@KtormDsl -public class BulkInsertOrUpdateReturningStatementBuilder>(table: T) : - BulkInsertStatementBuilder(table) { - internal val updateAssignments = ArrayList>() - internal val conflictColumns = ArrayList>() - internal val returningColumns = ArrayList>() - - /** - * Specify the update assignments while any key conflict exists. - */ - public fun onConflict(vararg columns: Column<*>, block: BulkInsertOrUpdateOnConflictClauseBuilder.() -> Unit) { - val builder = BulkInsertOrUpdateOnConflictClauseBuilder().apply(block) - updateAssignments += builder.assignments - conflictColumns += columns - } - - /** - * Specify the columns to return. - */ - public fun returning(vararg returningColumns: Column<*>) { - this.returningColumns += returningColumns - } -} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 8f276d4e..b74c6be7 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -150,7 +150,7 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { * Usage: * * ```kotlin - * database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * database.insertOrUpdateReturning(Employees, Employees.id) { * set(it.id, 1) * set(it.name, "vince") * set(it.job, "engineer") @@ -168,13 +168,14 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { * ```sql * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? - * returning id, job + * returning id * ``` * * @since 3.4.0 * @param table the table to be inserted. + * @param returningColumn the column to return * @param block the DSL block used to construct the expression. - * @return the effected row count. + * @return the returning column value. */ public fun , R : Any> Database.insertOrUpdateReturning( table: T, @@ -192,6 +193,40 @@ public fun , R : Any> Database.insertOrUpdateReturning( }.first() } +/** + * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically + * performs an update if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onDuplicateKey { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returningColumns the columns to return + * @param block the DSL block used to construct the expression. + * @return the returning columns' values. + */ public fun , R1 : Any, R2 : Any> Database.insertOrUpdateReturning( table: T, returningColumns: Pair, Column>, @@ -211,6 +246,40 @@ public fun , R1 : Any, R2 : Any> Database.insertOrUpdateReturni }.first() } +/** + * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically + * performs an update if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.insertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onDuplicateKey { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, salary + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returningColumns the columns to return + * @param block the DSL block used to construct the expression. + * @return the returning columns' values. + */ public fun , R1 : Any, R2 : Any, R3 : Any> Database.insertOrUpdateReturning( table: T, returningColumns: Triple, Column, Column>, diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 97bf9efd..ec222cf3 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -2,7 +2,6 @@ package org.ktorm.support.postgresql import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.nullValue -import org.junit.Assert.assertEquals import org.junit.Assert.assertThat import org.junit.ClassRule import org.junit.Test @@ -368,7 +367,13 @@ class PostgreSqlTest : BaseTest() { @Test fun testBulkInsertOrUpdateReturning() { - val rs = database.bulkInsertOrUpdateReturning(Employees) { + database.bulkInsertOrUpdateReturning( + Employees, + Pair( + Employees.id, + Employees.job + ) + ) { item { set(it.id, 1000) set(it.name, "vince") @@ -389,19 +394,14 @@ class PostgreSqlTest : BaseTest() { set(it.departmentId, excluded(it.departmentId)) set(it.salary, it.salary + 1000) } - returning( - it.id, - it.job + }.let { created -> + assert( + listOf( + Pair(1000, "trainee"), + Pair(5000, "engineer") + ) == created ) } - - assertEquals(rs.first, 2) - rs.second.next() - assertEquals(rs.second.getInt("id"), 1000) - assertEquals(rs.second.getString("job"), "trainee") - rs.second.next() - assertEquals(rs.second.getInt("id"), 5000) - assertEquals(rs.second.getString("job"), "engineer") } @Test From f25d2164a14f9d7cb66067167dcaef5620dd3afa Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Mon, 25 Jan 2021 17:19:05 -0500 Subject: [PATCH 10/96] use value semantics in Entity equals/hashCode --- .../org/ktorm/entity/EntityImplementation.kt | 6 ++-- .../kotlin/org/ktorm/entity/EntityTest.kt | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index c09823d0..f2ff967d 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -220,14 +220,14 @@ internal class EntityImplementation( override fun equals(other: Any?): Boolean { return when (other) { - is EntityImplementation -> this === other - is Entity<*> -> this === other.implementation + is EntityImplementation -> values == other.values && entityClass == other.entityClass + is Entity<*> -> values == other.implementation.values && entityClass == other.implementation.entityClass else -> false } } override fun hashCode(): Int { - return System.identityHashCode(this) + return values.hashCode() + (13 * entityClass.hashCode()) } override fun toString(): String { diff --git a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt index cc9ada0f..de15c8db 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt @@ -498,4 +498,39 @@ class EntityTest : BaseTest() { employee = database.employees.find { it.id eq employee.id } ?: throw AssertionError() assert(employee.job == "engineer") } + + @Test + fun testValueEquality() { + val now = LocalDate.now() + val employee1 = Employee { + id = 1 + name = "Eric" + job = "contributor" + hireDate = now + salary = 50 + } + + val employee2 = Employee { + id = 1 + name = "Eric" + job = "contributor" + hireDate = now + salary = 50 + } + + assert(employee1 == employee2) + } + + @Test + fun testDifferentClassesSameValuesNotEqual() { + val employee = Employee { + name = "name" + } + + val department = Department { + name = "name" + } + + assert(employee != department) + } } \ No newline at end of file From 96f6b48bbd212a40f9d5e50f0a9b62fdb2c9c55c Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Mon, 25 Jan 2021 17:30:09 -0500 Subject: [PATCH 11/96] added self to dev info --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index bdcb411b..dff43a29 100644 --- a/build.gradle +++ b/build.gradle @@ -162,6 +162,11 @@ subprojects { project -> name = "Pedro Domingues" email = "pedro.domingues.pt@gmail.com" } + developer { + id = "efenderbosch" + name = "Eric Fenderbosch" + email = "eric@fender.net" + } } scm { url = "https://github.com/kotlin-orm/ktorm.git" From 8bf327e7d929fde62ebe9ed795634e6472c97396 Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Mon, 25 Jan 2021 17:30:36 -0500 Subject: [PATCH 12/96] resolved detekt style issue --- .../src/main/kotlin/org/ktorm/entity/EntityImplementation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index f2ff967d..a12ce099 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -227,7 +227,7 @@ internal class EntityImplementation( } override fun hashCode(): Int { - return values.hashCode() + (13 * entityClass.hashCode()) + return values.hashCode() + 13 * entityClass.hashCode() } override fun toString(): String { From 2d50a56c8859fcb3bfc4d30bb9f841df4fae2042 Mon Sep 17 00:00:00 2001 From: PedroD Date: Tue, 26 Jan 2021 10:33:53 +0000 Subject: [PATCH 13/96] Hiding magic numbers --- .../org/ktorm/support/postgresql/BulkInsert.kt | 14 ++++++++------ .../org/ktorm/support/postgresql/InsertOrUpdate.kt | 7 ++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 7fc7a245..4ab996bb 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -394,10 +394,11 @@ public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertR ) return rowSet.asIterable().map { row -> + var i = 0 Triple( - returningColumns.first.sqlType.getResult(row, 1), - returningColumns.second.sqlType.getResult(row, 2), - returningColumns.third.sqlType.getResult(row, 3) + returningColumns.first.sqlType.getResult(row, ++i), + returningColumns.second.sqlType.getResult(row, ++i), + returningColumns.third.sqlType.getResult(row, ++i) ) } } @@ -603,10 +604,11 @@ public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertO ) return rowSet.asIterable().map { row -> + var i = 0 Triple( - returningColumns.first.sqlType.getResult(row, 1), - returningColumns.second.sqlType.getResult(row, 2), - returningColumns.third.sqlType.getResult(row, 3) + returningColumns.first.sqlType.getResult(row, ++i), + returningColumns.second.sqlType.getResult(row, ++i), + returningColumns.third.sqlType.getResult(row, ++i) ) } } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index b74c6be7..290776d4 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -292,10 +292,11 @@ public fun , R1 : Any, R2 : Any, R3 : Any> Database.insertOrUpd ) return rowSet.asIterable().map { row -> + var i = 0 Triple( - returningColumns.first.sqlType.getResult(row, 1), - returningColumns.second.sqlType.getResult(row, 2), - returningColumns.third.sqlType.getResult(row, 3) + returningColumns.first.sqlType.getResult(row, ++i), + returningColumns.second.sqlType.getResult(row, ++i), + returningColumns.third.sqlType.getResult(row, ++i) ) }.first() } From 343170dc7a5a2fe5707e79b39463f5564618a1ae Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 11:01:52 -0500 Subject: [PATCH 14/96] dialect specific for update options for mysql and postgres --- .../src/main/kotlin/org/ktorm/dsl/Query.kt | 6 +- .../kotlin/org/ktorm/entity/EntitySequence.kt | 8 ++- .../org/ktorm/expression/SqlExpressions.kt | 15 ++++- .../org/ktorm/expression/SqlFormatter.kt | 10 +++- .../test/kotlin/org/ktorm/dsl/QueryTest.kt | 27 --------- .../org/ktorm/support/mysql/MySqlDialect.kt | 58 +++++++++++++++++++ .../org/ktorm/support/mysql/MySqlTest.kt | 4 +- .../support/postgresql/PostgreSqlDialect.kt | 23 ++++++++ .../support/postgresql/PostgreSqlTest.kt | 2 +- 9 files changed, 114 insertions(+), 39 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt index 71dbc793..3b06ca2e 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt @@ -763,13 +763,13 @@ public fun Query.joinToString( } /** - * Indicate that this query should aquire the record-lock, the generated SQL would be `select ... for update`. + * Indicate that this query should acquire an update-lock, the generated SQL will depend on the SqlDialect. * * @since 3.1.0 */ -public fun Query.forUpdate(): Query { +public fun Query.forUpdate(forUpdateExpression: ForUpdateExpression?): Query { val expr = when (expression) { - is SelectExpression -> expression.copy(forUpdate = true) + is SelectExpression -> expression.copy(forUpdate = forUpdateExpression) is UnionExpression -> throw IllegalStateException("SELECT FOR UPDATE is not supported in a union expression.") } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index 2cc56c55..7f0f1722 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -19,6 +19,7 @@ package org.ktorm.entity import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.dsl.* +import org.ktorm.expression.ForUpdateExpression import org.ktorm.expression.OrderByExpression import org.ktorm.expression.SelectExpression import org.ktorm.schema.BaseTable @@ -1505,10 +1506,11 @@ public fun EntitySequence.joinToString( } /** - * Indicate that this query should aquire the record-lock, the generated SQL would be `select ... for update`. + * Indicate that this query should acquire an update-lock, the generated SQL will depend on the SqlDialect. * * @since 3.1.0 */ -public fun > EntitySequence.forUpdate(): EntitySequence { - return this.withExpression(expression.copy(forUpdate = true)) +public fun > EntitySequence.forUpdate( + forUpdateExpression: ForUpdateExpression?): EntitySequence { + return this.withExpression(expression.copy(forUpdate = forUpdateExpression)) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index e15fe706..570a4af4 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -19,6 +19,7 @@ package org.ktorm.expression import org.ktorm.schema.BooleanSqlType import org.ktorm.schema.ColumnDeclaring import org.ktorm.schema.SqlType +import org.ktorm.schema.VarcharSqlType /** * Root class of SQL expressions or statements. @@ -124,7 +125,7 @@ public data class SelectExpression( val groupBy: List> = emptyList(), val having: ScalarExpression? = null, val isDistinct: Boolean = false, - val forUpdate: Boolean = false, + val forUpdate: ForUpdateExpression? = null, override val orderBy: List = emptyList(), override val offset: Int? = null, override val limit: Int? = null, @@ -132,6 +133,18 @@ public data class SelectExpression( override val extraProperties: Map = emptyMap() ) : QueryExpression() +/** + * For Update expression, implementations are in the MySql and Postgres Dialects. + */ +public abstract class ForUpdateExpression : ScalarExpression() { + override val isLeafNode: Boolean + get() = true + override val extraProperties: Map + get() = emptyMap() + override val sqlType: SqlType + get() = VarcharSqlType +} + /** * Union expression, represents a `union` statement of SQL. * diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 854b81f0..e6f097f8 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -18,6 +18,7 @@ package org.ktorm.expression import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException +import org.ktorm.database.detectDialectImplementation /** * Subclass of [SqlExpressionVisitor], visiting SQL expression trees using visitor pattern. After the visit completes, @@ -388,12 +389,17 @@ public abstract class SqlFormatter( if (expr.offset != null || expr.limit != null) { writePagination(expr) } - if (expr.forUpdate) { - writeKeyword("for update ") + if (expr.forUpdate != null) { + visitForUpdate(expr.forUpdate) } return expr } + protected open fun visitForUpdate(expr: ForUpdateExpression): ForUpdateExpression = + throw DialectFeatureNotSupportedException( + "FOR UPDATE not supported in dialect ${detectDialectImplementation()::class.java.name}." + ) + override fun visitQuerySource(expr: QuerySourceExpression): QuerySourceExpression { when (expr) { is TableExpression -> { diff --git a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt index c58e0bc1..85c5086a 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt @@ -259,33 +259,6 @@ class QueryTest : BaseTest() { println(query.sql) } - @Test - fun testSelctForUpdate() { - database.useTransaction { - val employee = database - .sequenceOf(Employees, withReferences = false) - .filter { it.id eq 1 } - .forUpdate() - .first() - - val future = Executors.newSingleThreadExecutor().submit { - employee.name = "vince" - employee.flushChanges() - } - - try { - future.get(5, TimeUnit.SECONDS) - throw AssertionError() - } catch (e: ExecutionException) { - // Expected, the record is locked. - e.printStackTrace() - } catch (e: TimeoutException) { - // Expected, the record is locked. - e.printStackTrace() - } - } - } - @Test fun testFlatMap() { val names = database diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 3a6c8aa2..6b2eeac5 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -21,6 +21,11 @@ import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType import org.ktorm.schema.VarcharSqlType +import org.ktorm.support.mysql.MySqlForUpdateExpression.ForShare +import org.ktorm.support.mysql.MySqlForUpdateExpression.ForUpdate +import org.ktorm.support.mysql.Version.MySql5 +import org.ktorm.support.mysql.Version.MySql8 +import java.sql.DatabaseMetaData /** * [SqlDialect] implementation for MySQL database. @@ -32,12 +37,43 @@ public open class MySqlDialect : SqlDialect { } } +@Suppress("MagicNumber") +private enum class Version(val majorVersion: Int) { + MySql5(5), MySql8(8) +} + +/** + * Thrown to indicate that the MySql version is not supported by the current dialect. + * + * @param databaseMetaData used to format the exception's message. + */ +public class UnsupportedMySqlVersionException(databaseMetaData: DatabaseMetaData) : + UnsupportedOperationException( + "Unsupported SqlDialect for ${databaseMetaData.databaseProductName} v${databaseMetaData.databaseProductVersion}" + ) { + private companion object { + private const val serialVersionUID = 1L + } +} + /** * [SqlFormatter] implementation for MySQL, formatting SQL expressions as strings with their execution arguments. */ public open class MySqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { + private val version: Version + + init { + database.useConnection { + val metaData = it.metaData + version = when (metaData.databaseMajorVersion) { + MySql5.majorVersion -> MySql5 + MySql8.majorVersion -> MySql8 + else -> throw UnsupportedMySqlVersionException(metaData) + } + } + } override fun visit(expr: SqlExpression): SqlExpression { val result = when (expr) { @@ -129,6 +165,28 @@ public open class MySqlFormatter( write(") ") return expr } + + override fun visitForUpdate(expr: ForUpdateExpression): ForUpdateExpression { + when { + expr == ForUpdate -> writeKeyword("for update ") + expr == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") + expr == ForShare && version == MySql8 -> writeKeyword("for share ") + else -> { /* no-op */ } + } + return expr + } +} + +/** + * MySql Specific ForUpdateExpressions. + */ +public sealed class MySqlForUpdateExpression : ForUpdateExpression() { + /** + * The generated SQL would be `select ... lock in share mode` for MySql 5 and `select .. for share` for MySql 8. + **/ + public object ForShare : MySqlForUpdateExpression() + /** The generated SQL would be `select ... for update`. */ + public object ForUpdate : MySqlForUpdateExpression() } /** diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 4ca68ead..76feb6e9 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -417,12 +417,12 @@ class MySqlTest : BaseTest() { } @Test - fun testSelctForUpdate() { + fun testSelectForUpdate() { database.useTransaction { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate() + .forUpdate(MySqlForUpdateExpression.ForUpdate) .first() val future = Executors.newSingleThreadExecutor().submit { diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 7aae5851..7dd6ff4c 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -20,6 +20,9 @@ import org.ktorm.database.Database import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType +import org.ktorm.support.postgresql.PostgresForUpdateExpression.NoWait +import org.ktorm.support.postgresql.PostgresForUpdateExpression.SkipLocked +import org.ktorm.support.postgresql.PostgresForUpdateExpression.Wait /** * [SqlDialect] implementation for PostgreSQL database. @@ -31,12 +34,32 @@ public open class PostgreSqlDialect : SqlDialect { } } +/** + * Postgres Specific ForUpdateExpressions. + */ +public sealed class PostgresForUpdateExpression : ForUpdateExpression() { + /** The generated SQL would be `select ... for update skip locked`. */ + public object SkipLocked : PostgresForUpdateExpression() + /** The generated SQL would be `select ... for update nowait`. */ + public object NoWait : PostgresForUpdateExpression() + /** The generated SQL would be `select ... for update wait `. */ + public data class Wait(val seconds: Int) : PostgresForUpdateExpression() +} + /** * [SqlFormatter] implementation for PostgreSQL, formatting SQL expressions as strings with their execution arguments. */ public open class PostgreSqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { + override fun visitForUpdate(forUpdate: ForUpdateExpression): ForUpdateExpression { + when (forUpdate) { + SkipLocked -> writeKeyword("for update skip locked ") + NoWait -> writeKeyword("for update nowait ") + is Wait -> writeKeyword("for update wait ${forUpdate.seconds} ") + } + return forUpdate + } override fun checkColumnName(name: String) { val maxLength = database.maxColumnNameLength diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 2a026e7d..6b45988a 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -398,7 +398,7 @@ class PostgreSqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate() + .forUpdate(PostgresForUpdateExpression.NoWait) .first() val future = Executors.newSingleThreadExecutor().submit { From 92a0f88e61414b5df6ca4a6ca8fcf5e2ce465eac Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 11:07:55 -0500 Subject: [PATCH 15/96] cleaned up UnsupportedMySqlVersionException --- .../src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 6b2eeac5..e7c9fff5 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -43,13 +43,13 @@ private enum class Version(val majorVersion: Int) { } /** - * Thrown to indicate that the MySql version is not supported by the current dialect. + * Thrown to indicate that the MySql version is not supported by the dialect. * * @param databaseMetaData used to format the exception's message. */ public class UnsupportedMySqlVersionException(databaseMetaData: DatabaseMetaData) : UnsupportedOperationException( - "Unsupported SqlDialect for ${databaseMetaData.databaseProductName} v${databaseMetaData.databaseProductVersion}" + "Unsupported MySql version ${databaseMetaData.databaseProductVersion}." ) { private companion object { private const val serialVersionUID = 1L From fc670a28fc1fd29c806a03e16f06a30ddbc26187 Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 11:20:36 -0500 Subject: [PATCH 16/96] renamed visitForUpdate -> writeForUpdate to match writePagination --- .../src/main/kotlin/org/ktorm/expression/SqlFormatter.kt | 8 ++------ .../main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt | 3 +-- .../org/ktorm/support/postgresql/PostgreSqlDialect.kt | 8 ++++---- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index e6f097f8..4a696edb 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -18,7 +18,6 @@ package org.ktorm.expression import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException -import org.ktorm.database.detectDialectImplementation /** * Subclass of [SqlExpressionVisitor], visiting SQL expression trees using visitor pattern. After the visit completes, @@ -390,15 +389,12 @@ public abstract class SqlFormatter( writePagination(expr) } if (expr.forUpdate != null) { - visitForUpdate(expr.forUpdate) + writeForUpdate(expr.forUpdate) } return expr } - protected open fun visitForUpdate(expr: ForUpdateExpression): ForUpdateExpression = - throw DialectFeatureNotSupportedException( - "FOR UPDATE not supported in dialect ${detectDialectImplementation()::class.java.name}." - ) + protected abstract fun writeForUpdate(expr: ForUpdateExpression) override fun visitQuerySource(expr: QuerySourceExpression): QuerySourceExpression { when (expr) { diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index e7c9fff5..f07099a8 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -166,14 +166,13 @@ public open class MySqlFormatter( return expr } - override fun visitForUpdate(expr: ForUpdateExpression): ForUpdateExpression { + override fun writeForUpdate(expr: ForUpdateExpression) { when { expr == ForUpdate -> writeKeyword("for update ") expr == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") expr == ForShare && version == MySql8 -> writeKeyword("for share ") else -> { /* no-op */ } } - return expr } } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 7dd6ff4c..302c5994 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -52,13 +52,13 @@ public sealed class PostgresForUpdateExpression : ForUpdateExpression() { public open class PostgreSqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { - override fun visitForUpdate(forUpdate: ForUpdateExpression): ForUpdateExpression { - when (forUpdate) { + override fun writeForUpdate(expr: ForUpdateExpression) { + when (expr) { SkipLocked -> writeKeyword("for update skip locked ") NoWait -> writeKeyword("for update nowait ") - is Wait -> writeKeyword("for update wait ${forUpdate.seconds} ") + is Wait -> writeKeyword("for update wait ${expr.seconds} ") + else -> { /* no-op */ } } - return forUpdate } override fun checkColumnName(name: String) { From dfd9d58b45ab45d4aeb5a4b2a340a2b38dae81fd Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 11:25:33 -0500 Subject: [PATCH 17/96] renamed ForUpdateExpression to ForUpdateOption --- .../src/main/kotlin/org/ktorm/dsl/Query.kt | 4 ++-- .../kotlin/org/ktorm/entity/EntitySequence.kt | 6 +++--- .../org/ktorm/expression/SqlExpressions.kt | 16 ++++------------ .../kotlin/org/ktorm/expression/SqlFormatter.kt | 2 +- .../org/ktorm/support/mysql/MySqlDialect.kt | 12 ++++++------ .../kotlin/org/ktorm/support/mysql/MySqlTest.kt | 2 +- .../support/postgresql/PostgreSqlDialect.kt | 16 ++++++++-------- .../ktorm/support/postgresql/PostgreSqlTest.kt | 2 +- 8 files changed, 26 insertions(+), 34 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt index 3b06ca2e..ae0a8b2d 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt @@ -767,9 +767,9 @@ public fun Query.joinToString( * * @since 3.1.0 */ -public fun Query.forUpdate(forUpdateExpression: ForUpdateExpression?): Query { +public fun Query.forUpdate(forUpdate: ForUpdateOption?): Query { val expr = when (expression) { - is SelectExpression -> expression.copy(forUpdate = forUpdateExpression) + is SelectExpression -> expression.copy(forUpdate = forUpdate) is UnionExpression -> throw IllegalStateException("SELECT FOR UPDATE is not supported in a union expression.") } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index 7f0f1722..ef5cf1a9 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -19,7 +19,7 @@ package org.ktorm.entity import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.dsl.* -import org.ktorm.expression.ForUpdateExpression +import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.OrderByExpression import org.ktorm.expression.SelectExpression import org.ktorm.schema.BaseTable @@ -1511,6 +1511,6 @@ public fun EntitySequence.joinToString( * @since 3.1.0 */ public fun > EntitySequence.forUpdate( - forUpdateExpression: ForUpdateExpression?): EntitySequence { - return this.withExpression(expression.copy(forUpdate = forUpdateExpression)) + forUpdate: ForUpdateOption?): EntitySequence { + return this.withExpression(expression.copy(forUpdate = forUpdate)) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index 570a4af4..3abf4283 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -19,7 +19,6 @@ package org.ktorm.expression import org.ktorm.schema.BooleanSqlType import org.ktorm.schema.ColumnDeclaring import org.ktorm.schema.SqlType -import org.ktorm.schema.VarcharSqlType /** * Root class of SQL expressions or statements. @@ -125,7 +124,7 @@ public data class SelectExpression( val groupBy: List> = emptyList(), val having: ScalarExpression? = null, val isDistinct: Boolean = false, - val forUpdate: ForUpdateExpression? = null, + val forUpdate: ForUpdateOption? = null, override val orderBy: List = emptyList(), override val offset: Int? = null, override val limit: Int? = null, @@ -134,16 +133,9 @@ public data class SelectExpression( ) : QueryExpression() /** - * For Update expression, implementations are in the MySql and Postgres Dialects. - */ -public abstract class ForUpdateExpression : ScalarExpression() { - override val isLeafNode: Boolean - get() = true - override val extraProperties: Map - get() = emptyMap() - override val sqlType: SqlType - get() = VarcharSqlType -} + * ForUpdateOption, implementations are in the MySql and Postgres Dialects. + */ +public interface ForUpdateOption /** * Union expression, represents a `union` statement of SQL. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 4a696edb..7945a9b8 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -394,7 +394,7 @@ public abstract class SqlFormatter( return expr } - protected abstract fun writeForUpdate(expr: ForUpdateExpression) + protected abstract fun writeForUpdate(expr: ForUpdateOption) override fun visitQuerySource(expr: QuerySourceExpression): QuerySourceExpression { when (expr) { diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index f07099a8..378b2540 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -21,8 +21,8 @@ import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType import org.ktorm.schema.VarcharSqlType -import org.ktorm.support.mysql.MySqlForUpdateExpression.ForShare -import org.ktorm.support.mysql.MySqlForUpdateExpression.ForUpdate +import org.ktorm.support.mysql.MySqlForUpdateOption.ForShare +import org.ktorm.support.mysql.MySqlForUpdateOption.ForUpdate import org.ktorm.support.mysql.Version.MySql5 import org.ktorm.support.mysql.Version.MySql8 import java.sql.DatabaseMetaData @@ -166,7 +166,7 @@ public open class MySqlFormatter( return expr } - override fun writeForUpdate(expr: ForUpdateExpression) { + override fun writeForUpdate(expr: ForUpdateOption) { when { expr == ForUpdate -> writeKeyword("for update ") expr == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") @@ -179,13 +179,13 @@ public open class MySqlFormatter( /** * MySql Specific ForUpdateExpressions. */ -public sealed class MySqlForUpdateExpression : ForUpdateExpression() { +public sealed class MySqlForUpdateOption : ForUpdateOption { /** * The generated SQL would be `select ... lock in share mode` for MySql 5 and `select .. for share` for MySql 8. **/ - public object ForShare : MySqlForUpdateExpression() + public object ForShare : MySqlForUpdateOption() /** The generated SQL would be `select ... for update`. */ - public object ForUpdate : MySqlForUpdateExpression() + public object ForUpdate : MySqlForUpdateOption() } /** diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 76feb6e9..cf106007 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -422,7 +422,7 @@ class MySqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate(MySqlForUpdateExpression.ForUpdate) + .forUpdate(MySqlForUpdateOption.ForUpdate) .first() val future = Executors.newSingleThreadExecutor().submit { diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 302c5994..5b642bd3 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -20,9 +20,9 @@ import org.ktorm.database.Database import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType -import org.ktorm.support.postgresql.PostgresForUpdateExpression.NoWait -import org.ktorm.support.postgresql.PostgresForUpdateExpression.SkipLocked -import org.ktorm.support.postgresql.PostgresForUpdateExpression.Wait +import org.ktorm.support.postgresql.PostgresForUpdateOption.NoWait +import org.ktorm.support.postgresql.PostgresForUpdateOption.SkipLocked +import org.ktorm.support.postgresql.PostgresForUpdateOption.Wait /** * [SqlDialect] implementation for PostgreSQL database. @@ -37,13 +37,13 @@ public open class PostgreSqlDialect : SqlDialect { /** * Postgres Specific ForUpdateExpressions. */ -public sealed class PostgresForUpdateExpression : ForUpdateExpression() { +public sealed class PostgresForUpdateOption : ForUpdateOption { /** The generated SQL would be `select ... for update skip locked`. */ - public object SkipLocked : PostgresForUpdateExpression() + public object SkipLocked : PostgresForUpdateOption() /** The generated SQL would be `select ... for update nowait`. */ - public object NoWait : PostgresForUpdateExpression() + public object NoWait : PostgresForUpdateOption() /** The generated SQL would be `select ... for update wait `. */ - public data class Wait(val seconds: Int) : PostgresForUpdateExpression() + public data class Wait(val seconds: Int) : PostgresForUpdateOption() } /** @@ -52,7 +52,7 @@ public sealed class PostgresForUpdateExpression : ForUpdateExpression() { public open class PostgreSqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { - override fun writeForUpdate(expr: ForUpdateExpression) { + override fun writeForUpdate(expr: ForUpdateOption) { when (expr) { SkipLocked -> writeKeyword("for update skip locked ") NoWait -> writeKeyword("for update nowait ") diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 6b45988a..307d8819 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -398,7 +398,7 @@ class PostgreSqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate(PostgresForUpdateExpression.NoWait) + .forUpdate(PostgresForUpdateOption.NoWait) .first() val future = Executors.newSingleThreadExecutor().submit { From 58a1d8f0e0558580f3698cf90fdc669e586259d1 Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 11:30:32 -0500 Subject: [PATCH 18/96] throw DialectFeatureNotSupportedException instead of no-op when dialect does not support the ForUpdateOption --- .../main/kotlin/org/ktorm/expression/SqlFormatter.kt | 2 +- .../kotlin/org/ktorm/support/mysql/MySqlDialect.kt | 11 ++++++----- .../org/ktorm/support/postgresql/PostgreSqlDialect.kt | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 7945a9b8..06f9757b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -394,7 +394,7 @@ public abstract class SqlFormatter( return expr } - protected abstract fun writeForUpdate(expr: ForUpdateOption) + protected abstract fun writeForUpdate(forUpdate: ForUpdateOption) override fun visitQuerySource(expr: QuerySourceExpression): QuerySourceExpression { when (expr) { diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 378b2540..7e919926 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -17,6 +17,7 @@ package org.ktorm.support.mysql import org.ktorm.database.Database +import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType @@ -166,12 +167,12 @@ public open class MySqlFormatter( return expr } - override fun writeForUpdate(expr: ForUpdateOption) { + override fun writeForUpdate(forUpdate: ForUpdateOption) { when { - expr == ForUpdate -> writeKeyword("for update ") - expr == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") - expr == ForShare && version == MySql8 -> writeKeyword("for share ") - else -> { /* no-op */ } + forUpdate == ForUpdate -> writeKeyword("for update ") + forUpdate == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") + forUpdate == ForShare && version == MySql8 -> writeKeyword("for share ") + else -> throw DialectFeatureNotSupportedException("Unsupported ForUpdateOption ${forUpdate::class.java.name}.") } } } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 5b642bd3..4ac3bc82 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -17,6 +17,7 @@ package org.ktorm.support.postgresql import org.ktorm.database.Database +import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType @@ -52,12 +53,12 @@ public sealed class PostgresForUpdateOption : ForUpdateOption { public open class PostgreSqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { - override fun writeForUpdate(expr: ForUpdateOption) { - when (expr) { + override fun writeForUpdate(forUpdate: ForUpdateOption) { + when (forUpdate) { SkipLocked -> writeKeyword("for update skip locked ") NoWait -> writeKeyword("for update nowait ") - is Wait -> writeKeyword("for update wait ${expr.seconds} ") - else -> { /* no-op */ } + is Wait -> writeKeyword("for update wait ${forUpdate.seconds} ") + else -> throw DialectFeatureNotSupportedException("Unsupported ForUpdateOption ${forUpdate::class.java.name}.") } } From 5c5aa90ef4be3c20d6c3cefac29d8738c6689443 Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 12:56:17 -0500 Subject: [PATCH 19/96] basic for update options for Oracle and SqlServer; formatting --- .../org/ktorm/support/mysql/MySqlDialect.kt | 6 ++++-- .../org/ktorm/support/oracle/OracleDialect.kt | 20 ++++++++++++++++++- .../org/ktorm/support/oracle/OracleTest.kt | 2 +- .../support/postgresql/PostgreSqlDialect.kt | 6 ++++-- .../org/ktorm/support/sqlite/SQLiteDialect.kt | 4 ++++ .../support/sqlserver/SqlServerDialect.kt | 20 ++++++++++++++++++- 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 7e919926..6551c66f 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -172,13 +172,15 @@ public open class MySqlFormatter( forUpdate == ForUpdate -> writeKeyword("for update ") forUpdate == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") forUpdate == ForShare && version == MySql8 -> writeKeyword("for share ") - else -> throw DialectFeatureNotSupportedException("Unsupported ForUpdateOption ${forUpdate::class.java.name}.") + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) } } } /** - * MySql Specific ForUpdateExpressions. + * MySql Specific ForUpdateOptions. */ public sealed class MySqlForUpdateOption : ForUpdateOption { /** diff --git a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt index 834530d1..669b3802 100644 --- a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt +++ b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt @@ -21,6 +21,7 @@ import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType +import org.ktorm.support.oracle.OracleForUpdateOption.ForUpdate /** * [SqlDialect] implementation for Oracle database. @@ -32,6 +33,14 @@ public open class OracleDialect : SqlDialect { } } +/** + * Oracle Specific ForUpdateOptions. + */ +public sealed class OracleForUpdateOption : ForUpdateOption { + /** The generated SQL would be `select ... for update`. */ + public object ForUpdate : OracleForUpdateOption() +} + /** * [SqlFormatter] implementation for Oracle, formatting SQL expressions as strings with their execution arguments. */ @@ -51,11 +60,20 @@ public open class OracleFormatter( return identifier.startsWith('_') || super.shouldQuote(identifier) } + override fun writeForUpdate(forUpdate: ForUpdateOption) { + when (forUpdate) { + ForUpdate -> writeKeyword("for update ") + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) + } + } + override fun visitQuery(expr: QueryExpression): QueryExpression { if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate) { + if (expr is SelectExpression && expr.forUpdate != null) { throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") } diff --git a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt index beb868f1..6a465f2c 100644 --- a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt +++ b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt @@ -155,7 +155,7 @@ class OracleTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate() + .forUpdate(OracleForUpdateOption.ForUpdate) .single() val future = Executors.newSingleThreadExecutor().submit { diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 4ac3bc82..a4f63306 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -36,7 +36,7 @@ public open class PostgreSqlDialect : SqlDialect { } /** - * Postgres Specific ForUpdateExpressions. + * Postgres Specific ForUpdateOptions. */ public sealed class PostgresForUpdateOption : ForUpdateOption { /** The generated SQL would be `select ... for update skip locked`. */ @@ -58,7 +58,9 @@ public open class PostgreSqlFormatter( SkipLocked -> writeKeyword("for update skip locked ") NoWait -> writeKeyword("for update nowait ") is Wait -> writeKeyword("for update wait ${forUpdate.seconds} ") - else -> throw DialectFeatureNotSupportedException("Unsupported ForUpdateOption ${forUpdate::class.java.name}.") + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) } } diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt index d6d90cd9..8b3a51cb 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt @@ -18,6 +18,7 @@ package org.ktorm.support.sqlite import org.ktorm.database.* import org.ktorm.expression.ArgumentExpression +import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.QueryExpression import org.ktorm.expression.SqlFormatter import org.ktorm.schema.IntSqlType @@ -62,6 +63,9 @@ public open class SQLiteDialect : SqlDialect { public open class SQLiteFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { + override fun writeForUpdate(forUpdate: ForUpdateOption) { + throw DialectFeatureNotSupportedException("SQLite does not support SELECT ... FOR UPDATE.") + } override fun writePagination(expr: QueryExpression) { newLine(Indentation.SAME) diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt index 062cae3a..636d7cb4 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt @@ -20,6 +20,7 @@ import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* +import org.ktorm.support.sqlserver.SqlServerForUpdateOption.ForUpdate /** * [SqlDialect] implementation for Microsoft SqlServer database. @@ -31,6 +32,14 @@ public open class SqlServerDialect : SqlDialect { } } +/** + * SqlServer Specific ForUpdateOptions. + */ +public sealed class SqlServerForUpdateOption : ForUpdateOption { + /** The generated SQL would be `select ... for update`. */ + public object ForUpdate : SqlServerForUpdateOption() +} + /** * [SqlFormatter] implementation for SqlServer, formatting SQL expressions as strings with their execution arguments. */ @@ -45,11 +54,20 @@ public open class SqlServerFormatter( } } + override fun writeForUpdate(forUpdate: ForUpdateOption) { + when (forUpdate) { + ForUpdate -> writeKeyword("for update ") + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) + } + } + override fun visitQuery(expr: QueryExpression): QueryExpression { if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate) { + if (expr is SelectExpression && expr.forUpdate != null) { throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") } From 893803adc5764609680a63fe283dc4424bc2cedd Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 13:01:50 -0500 Subject: [PATCH 20/96] minor typo in comment --- .../src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 6551c66f..72d879fa 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -184,7 +184,7 @@ public open class MySqlFormatter( */ public sealed class MySqlForUpdateOption : ForUpdateOption { /** - * The generated SQL would be `select ... lock in share mode` for MySql 5 and `select .. for share` for MySql 8. + * The generated SQL would be `select ... lock in share mode` for MySql 5 and `select ... for share` for MySql 8. **/ public object ForShare : MySqlForUpdateOption() /** The generated SQL would be `select ... for update`. */ From db764ad907a0a5c0ec755d8e2a1e9c2b638fd58c Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 13:05:51 -0500 Subject: [PATCH 21/96] better comment on ForUpdateOption --- .../src/main/kotlin/org/ktorm/expression/SqlExpressions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index 3abf4283..d1247b42 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -133,7 +133,7 @@ public data class SelectExpression( ) : QueryExpression() /** - * ForUpdateOption, implementations are in the MySql and Postgres Dialects. + * ForUpdateOption, database-specific implementations are in support module for each database dialect. */ public interface ForUpdateOption From ed418ac7f646becc3eed73cd454a2c7be75b45bc Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 14:02:06 -0500 Subject: [PATCH 22/96] implemented Postgres for update as described in postgresql.org docs --- .../support/postgresql/PostgreSqlDialect.kt | 43 +++++++++++++------ .../support/postgresql/PostgreSqlTest.kt | 6 ++- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index a4f63306..59b7fb22 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -21,9 +21,7 @@ import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType -import org.ktorm.support.postgresql.PostgresForUpdateOption.NoWait -import org.ktorm.support.postgresql.PostgresForUpdateOption.SkipLocked -import org.ktorm.support.postgresql.PostgresForUpdateOption.Wait +import org.ktorm.schema.Table /** * [SqlDialect] implementation for PostgreSQL database. @@ -36,15 +34,34 @@ public open class PostgreSqlDialect : SqlDialect { } /** - * Postgres Specific ForUpdateOptions. + * Postgres Specific ForUpdateOption. */ -public sealed class PostgresForUpdateOption : ForUpdateOption { - /** The generated SQL would be `select ... for update skip locked`. */ - public object SkipLocked : PostgresForUpdateOption() - /** The generated SQL would be `select ... for update nowait`. */ - public object NoWait : PostgresForUpdateOption() - /** The generated SQL would be `select ... for update wait `. */ - public data class Wait(val seconds: Int) : PostgresForUpdateOption() +public class PostgresForUpdateOption( + private val lockStrength: LockStrength, + private val onLock: OnLock, + private vararg val tables: Table<*> = emptyArray() +) : ForUpdateOption { + public fun toLockingClause(): String { + val lockingClause = StringBuilder(lockStrength.keywords) + if (tables.isNotEmpty()) { + tables.joinTo(lockingClause, prefix = "of ", postfix = " ") { it.tableName } + } + onLock.keywords?.let { lockingClause.append(it) } + return lockingClause.toString() + } + + public enum class LockStrength(public val keywords: String) { + Update("for update "), + NoKeyUpdate("for no key update "), + Share("for share "), + KeyShare("for key share ") + } + + public enum class OnLock(public val keywords: String?) { + Wait(null), + NoWait("no wait "), + SkipLocked("skip locked ") + } } /** @@ -55,9 +72,7 @@ public open class PostgreSqlFormatter( ) : SqlFormatter(database, beautifySql, indentSize) { override fun writeForUpdate(forUpdate: ForUpdateOption) { when (forUpdate) { - SkipLocked -> writeKeyword("for update skip locked ") - NoWait -> writeKeyword("for update nowait ") - is Wait -> writeKeyword("for update wait ${forUpdate.seconds} ") + is PostgresForUpdateOption -> writeKeyword(forUpdate.toLockingClause()) else -> throw DialectFeatureNotSupportedException( "Unsupported ForUpdateOption ${forUpdate::class.java.name}." ) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 307d8819..cbf55f61 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -16,6 +16,8 @@ import org.ktorm.schema.ColumnDeclaring import org.ktorm.schema.Table import org.ktorm.schema.int import org.ktorm.schema.varchar +import org.ktorm.support.postgresql.PostgresForUpdateOption.LockStrength.Update +import org.ktorm.support.postgresql.PostgresForUpdateOption.OnLock.Wait import org.testcontainers.containers.PostgreSQLContainer import java.time.LocalDate import java.util.concurrent.ExecutionException @@ -393,12 +395,12 @@ class PostgreSqlTest : BaseTest() { } @Test - fun testSelctForUpdate() { + fun testSelectForUpdate() { database.useTransaction { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate(PostgresForUpdateOption.NoWait) + .forUpdate(PostgresForUpdateOption(lockStrength = Update, onLock = Wait)) .first() val future = Executors.newSingleThreadExecutor().submit { From 366399fdc441ae42aa63d87da70d86f164135ec4 Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 14:08:18 -0500 Subject: [PATCH 23/96] minor clean up --- ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt | 5 +++++ .../kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt index 584d83f2..2c619283 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt @@ -17,6 +17,7 @@ package org.ktorm.database import org.ktorm.expression.ArgumentExpression +import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.QueryExpression import org.ktorm.expression.SqlFormatter import java.sql.Statement @@ -50,6 +51,10 @@ public interface SqlDialect { */ public fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int): SqlFormatter { return object : SqlFormatter(database, beautifySql, indentSize) { + override fun writeForUpdate(forUpdate: ForUpdateOption) { + throw DialectFeatureNotSupportedException("ForUpdate is not supported in Standard SQL.") + } + override fun writePagination(expr: QueryExpression) { throw DialectFeatureNotSupportedException("Pagination is not supported in Standard SQL.") } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 59b7fb22..91252f94 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -39,7 +39,7 @@ public open class PostgreSqlDialect : SqlDialect { public class PostgresForUpdateOption( private val lockStrength: LockStrength, private val onLock: OnLock, - private vararg val tables: Table<*> = emptyArray() + private vararg val tables: Table<*> ) : ForUpdateOption { public fun toLockingClause(): String { val lockingClause = StringBuilder(lockStrength.keywords) From fc5368efc51249c39abd73ac70ab6a8eb901fa1d Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Fri, 5 Feb 2021 14:17:27 -0500 Subject: [PATCH 24/96] fix typo in comment and update to explain new forUpdate behavior --- .../src/main/kotlin/org/ktorm/expression/SqlExpressions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index d1247b42..a14beeac 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -115,7 +115,8 @@ public sealed class QueryExpression : QuerySourceExpression() { * @property groupBy the grouping conditions, represents the `group by` clause of SQL. * @property having the having condition, represents the `having` clause of SQL. * @property isDistinct mark if this query is distinct, true means the SQL is `select distinct ...`. - * @property forUpdate mark if this query should aquire the record-lock, true means the SQL is `select ... for update`. + * @property forUpdate mark if this query should acquire an update-lock, non-null will generate a dialect-specific + * `for update` clause. */ public data class SelectExpression( val columns: List> = emptyList(), From 0f17096e3182efc22e768165907e92e571f21b01 Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Mon, 8 Feb 2021 07:24:44 -0500 Subject: [PATCH 25/96] use database.productVersion instead of getting a new connection for metaData --- .../org/ktorm/support/mysql/MySqlDialect.kt | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 72d879fa..79113620 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -26,7 +26,6 @@ import org.ktorm.support.mysql.MySqlForUpdateOption.ForShare import org.ktorm.support.mysql.MySqlForUpdateOption.ForUpdate import org.ktorm.support.mysql.Version.MySql5 import org.ktorm.support.mysql.Version.MySql8 -import java.sql.DatabaseMetaData /** * [SqlDialect] implementation for MySQL database. @@ -38,9 +37,8 @@ public open class MySqlDialect : SqlDialect { } } -@Suppress("MagicNumber") -private enum class Version(val majorVersion: Int) { - MySql5(5), MySql8(8) +private enum class Version { + MySql5, MySql8 } /** @@ -48,10 +46,8 @@ private enum class Version(val majorVersion: Int) { * * @param databaseMetaData used to format the exception's message. */ -public class UnsupportedMySqlVersionException(databaseMetaData: DatabaseMetaData) : - UnsupportedOperationException( - "Unsupported MySql version ${databaseMetaData.databaseProductVersion}." - ) { +public class UnsupportedMySqlVersionException(productVersion: String) : + UnsupportedOperationException("Unsupported MySql version $productVersion.") { private companion object { private const val serialVersionUID = 1L } @@ -63,17 +59,10 @@ public class UnsupportedMySqlVersionException(databaseMetaData: DatabaseMetaData public open class MySqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { - private val version: Version - - init { - database.useConnection { - val metaData = it.metaData - version = when (metaData.databaseMajorVersion) { - MySql5.majorVersion -> MySql5 - MySql8.majorVersion -> MySql8 - else -> throw UnsupportedMySqlVersionException(metaData) - } - } + private val version: Version = when { + database.productVersion.startsWith("5") -> MySql5 + database.productVersion.startsWith("8") -> MySql8 + else -> throw UnsupportedMySqlVersionException(database.productVersion) } override fun visit(expr: SqlExpression): SqlExpression { @@ -187,6 +176,7 @@ public sealed class MySqlForUpdateOption : ForUpdateOption { * The generated SQL would be `select ... lock in share mode` for MySql 5 and `select ... for share` for MySql 8. **/ public object ForShare : MySqlForUpdateOption() + /** The generated SQL would be `select ... for update`. */ public object ForUpdate : MySqlForUpdateOption() } From 368b0db0306187a5617427171e42d0972b6248bb Mon Sep 17 00:00:00 2001 From: Eric Fenderbosch Date: Mon, 8 Feb 2021 08:57:06 -0500 Subject: [PATCH 26/96] made ForUpdateOption non-nullable; update testcontainers and detekt dependencies --- build.gradle | 2 +- ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt | 2 +- .../main/kotlin/org/ktorm/entity/EntitySequence.kt | 2 +- .../kotlin/org/ktorm/expression/SqlExpressions.kt | 8 ++++++-- .../main/kotlin/org/ktorm/expression/SqlFormatter.kt | 2 +- ktorm-support-mysql/ktorm-support-mysql.gradle | 2 +- .../test/kotlin/org/ktorm/support/mysql/MySqlTest.kt | 2 +- ktorm-support-oracle/ktorm-support-oracle.gradle | 2 +- .../kotlin/org/ktorm/support/oracle/OracleDialect.kt | 2 +- .../ktorm-support-postgresql.gradle | 2 +- .../ktorm/support/postgresql/PostgreSqlDialect.kt | 12 +++++++++++- .../org/ktorm/support/postgresql/PostgreSqlTest.kt | 2 +- ktorm-support-sqlite/ktorm-support-sqlite.gradle | 2 +- .../ktorm-support-sqlserver.gradle | 2 +- .../org/ktorm/support/sqlserver/SqlServerDialect.kt | 2 +- .../org/ktorm/support/sqlserver/SqlServerTest.kt | 2 +- 16 files changed, 31 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index dff43a29..55c9ce83 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlinVersion = "1.4.21" - detektVersion = "1.12.0-RC1" + detektVersion = "1.15.0" } repositories { jcenter() diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt index ae0a8b2d..e1325173 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt @@ -767,7 +767,7 @@ public fun Query.joinToString( * * @since 3.1.0 */ -public fun Query.forUpdate(forUpdate: ForUpdateOption?): Query { +public fun Query.forUpdate(forUpdate: ForUpdateOption): Query { val expr = when (expression) { is SelectExpression -> expression.copy(forUpdate = forUpdate) is UnionExpression -> throw IllegalStateException("SELECT FOR UPDATE is not supported in a union expression.") diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index ef5cf1a9..c163ca30 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -1511,6 +1511,6 @@ public fun EntitySequence.joinToString( * @since 3.1.0 */ public fun > EntitySequence.forUpdate( - forUpdate: ForUpdateOption?): EntitySequence { + forUpdate: ForUpdateOption): EntitySequence { return this.withExpression(expression.copy(forUpdate = forUpdate)) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index a14beeac..4d03eed6 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -125,7 +125,7 @@ public data class SelectExpression( val groupBy: List> = emptyList(), val having: ScalarExpression? = null, val isDistinct: Boolean = false, - val forUpdate: ForUpdateOption? = null, + val forUpdate: ForUpdateOption = ForUpdateOption.None, override val orderBy: List = emptyList(), override val offset: Int? = null, override val limit: Int? = null, @@ -136,7 +136,11 @@ public data class SelectExpression( /** * ForUpdateOption, database-specific implementations are in support module for each database dialect. */ -public interface ForUpdateOption +public interface ForUpdateOption { + public companion object { + public val None: ForUpdateOption = object : ForUpdateOption {} + } +} /** * Union expression, represents a `union` statement of SQL. diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 06f9757b..199e45ce 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -388,7 +388,7 @@ public abstract class SqlFormatter( if (expr.offset != null || expr.limit != null) { writePagination(expr) } - if (expr.forUpdate != null) { + if (expr.forUpdate != ForUpdateOption.None) { writeForUpdate(expr.forUpdate) } return expr diff --git a/ktorm-support-mysql/ktorm-support-mysql.gradle b/ktorm-support-mysql/ktorm-support-mysql.gradle index 3e2f1f00..6b401a15 100644 --- a/ktorm-support-mysql/ktorm-support-mysql.gradle +++ b/ktorm-support-mysql/ktorm-support-mysql.gradle @@ -5,5 +5,5 @@ dependencies { testImplementation project(path: ":ktorm-core", configuration: "testOutput") testImplementation project(":ktorm-jackson") testImplementation "mysql:mysql-connector-java:8.0.13" - testImplementation "org.testcontainers:mysql:1.11.3" + testImplementation "org.testcontainers:mysql:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index cf106007..125a240f 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -36,7 +36,7 @@ class MySqlTest : BaseTest() { const val ID_3 = 3 const val ID_4 = 4 - class KMySqlContainer : MySQLContainer() + class KMySqlContainer : MySQLContainer("mysql:8") @ClassRule @JvmField diff --git a/ktorm-support-oracle/ktorm-support-oracle.gradle b/ktorm-support-oracle/ktorm-support-oracle.gradle index 6231376e..0c4bdc8c 100644 --- a/ktorm-support-oracle/ktorm-support-oracle.gradle +++ b/ktorm-support-oracle/ktorm-support-oracle.gradle @@ -4,5 +4,5 @@ dependencies { testImplementation project(path: ":ktorm-core", configuration: "testOutput") testImplementation fileTree(dir: "lib", includes: ["*.jar"]) - testImplementation "org.testcontainers:oracle-xe:1.11.3" + testImplementation "org.testcontainers:oracle-xe:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt index 669b3802..8cc7e843 100644 --- a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt +++ b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt @@ -73,7 +73,7 @@ public open class OracleFormatter( if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate != null) { + if (expr is SelectExpression && expr.forUpdate != ForUpdateOption.None) { throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") } diff --git a/ktorm-support-postgresql/ktorm-support-postgresql.gradle b/ktorm-support-postgresql/ktorm-support-postgresql.gradle index c2917f96..a7c8e612 100644 --- a/ktorm-support-postgresql/ktorm-support-postgresql.gradle +++ b/ktorm-support-postgresql/ktorm-support-postgresql.gradle @@ -4,5 +4,5 @@ dependencies { testImplementation project(path: ":ktorm-core", configuration: "testOutput") testImplementation "org.postgresql:postgresql:42.2.5" - testImplementation "org.testcontainers:postgresql:1.11.3" + testImplementation "org.testcontainers:postgresql:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 91252f94..829c88da 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -34,13 +34,17 @@ public open class PostgreSqlDialect : SqlDialect { } /** - * Postgres Specific ForUpdateOption. + * Postgres Specific ForUpdateOption. See docs: https://www.postgresql.org/docs/13/sql-select.html#SQL-FOR-UPDATE-SHARE */ public class PostgresForUpdateOption( private val lockStrength: LockStrength, private val onLock: OnLock, private vararg val tables: Table<*> ) : ForUpdateOption { + + /** + * Generates SQL locking clause. + */ public fun toLockingClause(): String { val lockingClause = StringBuilder(lockStrength.keywords) if (tables.isNotEmpty()) { @@ -50,6 +54,9 @@ public class PostgresForUpdateOption( return lockingClause.toString() } + /** + * Lock strength. + */ public enum class LockStrength(public val keywords: String) { Update("for update "), NoKeyUpdate("for no key update "), @@ -57,6 +64,9 @@ public class PostgresForUpdateOption( KeyShare("for key share ") } + /** + * Behavior when a lock is detected. + */ public enum class OnLock(public val keywords: String?) { Wait(null), NoWait("no wait "), diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index cbf55f61..b3eed969 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -31,7 +31,7 @@ import java.util.concurrent.TimeoutException class PostgreSqlTest : BaseTest() { companion object { - class KPostgreSqlContainer : PostgreSQLContainer() + class KPostgreSqlContainer : PostgreSQLContainer("postgres:13-alpine") @ClassRule @JvmField diff --git a/ktorm-support-sqlite/ktorm-support-sqlite.gradle b/ktorm-support-sqlite/ktorm-support-sqlite.gradle index 03952d87..9c274ae2 100644 --- a/ktorm-support-sqlite/ktorm-support-sqlite.gradle +++ b/ktorm-support-sqlite/ktorm-support-sqlite.gradle @@ -3,5 +3,5 @@ dependencies { api project(":ktorm-core") testImplementation project(path: ":ktorm-core", configuration: "testOutput") - testImplementation "org.xerial:sqlite-jdbc:3.28.0" + testImplementation "org.xerial:sqlite-jdbc:3.34.0" } diff --git a/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle b/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle index 24c74768..bcdb531e 100644 --- a/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle +++ b/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle @@ -4,5 +4,5 @@ dependencies { api "com.microsoft.sqlserver:mssql-jdbc:7.2.2.jre8" testImplementation project(path: ":ktorm-core", configuration: "testOutput") - testImplementation "org.testcontainers:mssqlserver:1.11.3" + testImplementation "org.testcontainers:mssqlserver:1.15.1" } diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt index 636d7cb4..e7cd298e 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt @@ -67,7 +67,7 @@ public open class SqlServerFormatter( if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate != null) { + if (expr is SelectExpression && expr.forUpdate != ForUpdateOption.None) { throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") } diff --git a/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt b/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt index 93a5cb0d..b4205390 100644 --- a/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt +++ b/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt @@ -37,7 +37,7 @@ class SqlServerTest : BaseTest() { const val ID_3 = 3 const val ID_4 = 4 - class KSqlServerContainer : MSSQLServerContainer() + class KSqlServerContainer : MSSQLServerContainer("mcr.microsoft.com/mssql/server:2017-CU12") @ClassRule @JvmField From a7303e4a099541152c739905f82a2b5f20dc473c Mon Sep 17 00:00:00 2001 From: PedroD Date: Fri, 5 Mar 2021 11:08:16 +0000 Subject: [PATCH 27/96] Adding explicit do-nothing to on-conflict clauses --- .../ktorm/support/postgresql/BulkInsert.kt | 556 +++++++++++++++++- .../postgresql/CompositeCachedRowSet.kt | 70 +++ .../support/postgresql/InsertOrUpdate.kt | 230 +++++++- .../support/postgresql/PostgreSqlDialect.kt | 44 +- .../support/postgresql/PostgreSqlTest.kt | 251 ++++++++ 5 files changed, 1113 insertions(+), 38 deletions(-) create mode 100644 ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 5007829d..fb1ece26 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -26,6 +26,14 @@ import org.ktorm.expression.SqlExpression import org.ktorm.expression.TableExpression import org.ktorm.schema.BaseTable import org.ktorm.schema.Column +import java.util.* +import kotlin.collections.ArrayList + +// We leave some prepared statement parameters reserved for the query dialect building process +private const val RESERVED_SQL_EXPR_BATCH_SIZE = 100 + +// Max number of assignments we allow per batch in Postgresql (Max size as defined by Postgresql - reserved) +private const val MAX_SQL_EXPR_BATCH_SIZE = Short.MAX_VALUE - RESERVED_SQL_EXPR_BATCH_SIZE /** * Bulk insert expression, represents a bulk insert statement in PostgreSQL. @@ -45,8 +53,9 @@ import org.ktorm.schema.Column public data class BulkInsertExpression( val table: TableExpression, val assignments: List>>, - val conflictColumns: List> = emptyList(), + val conflictColumns: List>? = null, val updateAssignments: List> = emptyList(), + val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() ) : SqlExpression() @@ -92,9 +101,26 @@ public data class BulkInsertExpression( public fun > Database.bulkInsert( table: T, block: BulkInsertStatementBuilder.(T) -> Unit ): Int { + var affectedTotal = 0 + val builder = BulkInsertStatementBuilder(table).apply { block(table) } - val expression = BulkInsertExpression(table.asExpression(), builder.assignments) - return executeUpdate(expression) + + if (builder.assignments.isEmpty()) return 0 + + val execute: (List>>) -> Unit = { assignments -> + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = assignments + ) + + val total = executeUpdate(expression) + + affectedTotal += total + } + + executeQueryInBatches(builder, execute) + + return affectedTotal } /** @@ -144,24 +170,42 @@ public fun > Database.bulkInsert( public fun > Database.bulkInsertOrUpdate( table: T, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit ): Int { + var affectedTotal = 0 + val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } + if (builder.assignments.isEmpty()) return 0 + val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = builder.assignments, - conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = builder.updateAssignments - ) + if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { + val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + + " you must explicitly invoke `doNothing()`" + throw IllegalStateException(msg) + } + + val execute: (List>>) -> Unit = { assignments -> + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = assignments, + conflictColumns = conflictColumns.map { it.asExpression() }, + updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + ) + + val total = executeUpdate(expression) + + affectedTotal += total + } - return executeUpdate(expression) + executeQueryInBatches(builder, execute) + + return affectedTotal } /** @@ -195,31 +239,493 @@ public class BulkInsertOrUpdateStatementBuilder>(table: T) : Bu internal val updateAssignments = ArrayList>() internal val conflictColumns = ArrayList>() + internal var explicitlyDoNothing: Boolean = false + /** * Specify the update assignments while any key conflict exists. */ - public fun onConflict(vararg columns: Column<*>, block: BulkInsertOrUpdateOnConflictClauseBuilder.() -> Unit) { - val builder = BulkInsertOrUpdateOnConflictClauseBuilder().apply(block) + public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { + val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) + + explicitlyDoNothing = builder.explicitlyDoNothing + updateAssignments += builder.assignments + conflictColumns += columns } } /** - * DSL builder for bulk insert or update on conflict clause. + * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. + * + * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL + * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance + * is much better than [batchInsert]. + * + * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * returning id`. + * + * Usage: + * + * ```kotlin + * database.bulkInsertReturning(Employees, Employees.id) { + * item { + * set(it.name, "jerry") + * set(it.job, "trainee") + * set(it.managerId, 1) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 50) + * set(it.departmentId, 1) + * } + * item { + * set(it.name, "linda") + * set(it.job, "assistant") + * set(it.managerId, 3) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 100) + * set(it.departmentId, 2) + * } + * } + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. + * @param returningColumn the column to return + * @return the returning column value. + * @see batchInsert */ -@KtormDsl -public class BulkInsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBuilder() { +public fun , R : Any> Database.bulkInsertReturning( + table: T, + returningColumn: Column, + block: BulkInsertStatementBuilder.(T) -> Unit +): List { + val (_, rowSet) = this.bulkInsertReturningAux( + table, + listOf(returningColumn), + block + ) - /** - * Reference the 'EXCLUDED' table in a ON CONFLICT clause. - */ - public fun excluded(column: Column): ColumnExpression { - // excluded.name - return ColumnExpression( - table = TableExpression(name = "excluded"), - name = column.name, - sqlType = column.sqlType + return rowSet.asIterable().map { row -> + returningColumn.sqlType.getResult(row, 1) + } +} + +/** + * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. + * + * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL + * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance + * is much better than [batchInsert]. + * + * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * returning id, job`. + * + * Usage: + * + * ```kotlin + * database.bulkInsertReturning(Employees, Pair(Employees.id, Employees.job)) { + * item { + * set(it.name, "jerry") + * set(it.job, "trainee") + * set(it.managerId, 1) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 50) + * set(it.departmentId, 1) + * } + * item { + * set(it.name, "linda") + * set(it.job, "assistant") + * set(it.managerId, 3) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 100) + * set(it.departmentId, 2) + * } + * } + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. + * @param returningColumns the columns to return + * @return the returning columns' values. + * @see batchInsert + */ +public fun , R1 : Any, R2 : Any> Database.bulkInsertReturning( + table: T, + returningColumns: Pair, Column>, + block: BulkInsertStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Pair( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2) ) } } + +/** + * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. + * + * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL + * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance + * is much better than [batchInsert]. + * + * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * returning id, job, salary`. + * + * Usage: + * + * ```kotlin + * database.bulkInsertReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * item { + * set(it.name, "jerry") + * set(it.job, "trainee") + * set(it.managerId, 1) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 50) + * set(it.departmentId, 1) + * } + * item { + * set(it.name, "linda") + * set(it.job, "assistant") + * set(it.managerId, 3) + * set(it.hireDate, LocalDate.now()) + * set(it.salary, 100) + * set(it.departmentId, 2) + * } + * } + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. + * @param returningColumns the columns to return + * @return the returning columns' values. + * @see batchInsert + */ +public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertReturning( + table: T, + returningColumns: Triple, Column, Column>, + block: BulkInsertStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + var i = 0 + Triple( + returningColumns.first.sqlType.getResult(row, ++i), + returningColumns.second.sqlType.getResult(row, ++i), + returningColumns.third.sqlType.getResult(row, ++i) + ) + } +} + +private fun > Database.bulkInsertReturningAux( + table: T, + returningColumns: List>, + block: BulkInsertStatementBuilder.(T) -> Unit +): Pair { + var affectedTotal = 0 + val cachedRowSets = CompositeCachedRowSet() + + val builder = BulkInsertStatementBuilder(table).apply { block(table) } + + if (builder.assignments.isEmpty()) return Pair(0, CompositeCachedRowSet()) + + val execute: (List>>) -> Unit = { assignments -> + val expression = BulkInsertExpression( + table.asExpression(), + assignments, + returningColumns = returningColumns.map { it.asExpression() } + ) + + val (total, rows) = executeUpdateAndRetrieveKeys(expression) + + affectedTotal += total + cachedRowSets.add(rows) + } + + executeQueryInBatches(builder, execute) + + return Pair(affectedTotal, cachedRowSets) +} + +/** + * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, + * and automatically performs updates if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, ... + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @param returningColumn the column to return + * @return the returning column value. + * @see bulkInsert + */ +public fun , R : Any> Database.bulkInsertOrUpdateReturning( + table: T, + returningColumn: Column, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List { + val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( + table, + listOf(returningColumn), + block + ) + + return rowSet.asIterable().map { row -> + returningColumn.sqlType.getResult(row, 1) + } +} + +/** + * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, + * and automatically performs updates if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, ... + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @param returningColumns the column to return + * @return the returning columns' values. + * @see bulkInsert + */ +public fun , R1 : Any, R2 : Any> Database.bulkInsertOrUpdateReturning( + table: T, + returningColumns: Pair, Column>, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Pair( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2) + ) + } +} + +/** + * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, + * and automatically performs updates if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name, Employees.salary)) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, ... + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @param returningColumns the column to return + * @return the returning columns' values. + * @see bulkInsert + */ +public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertOrUpdateReturning( + table: T, + returningColumns: Triple, Column, Column>, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List> { + val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + var i = 0 + Triple( + returningColumns.first.sqlType.getResult(row, ++i), + returningColumns.second.sqlType.getResult(row, ++i), + returningColumns.third.sqlType.getResult(row, ++i) + ) + } +} + +private fun > Database.bulkInsertOrUpdateReturningAux( + table: T, + returningColumns: List>, + block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): Pair { + var affectedTotal = 0 + val cachedRowSets = CompositeCachedRowSet() + + val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } + + if (builder.assignments.isEmpty()) return Pair(0, CompositeCachedRowSet()) + + val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } + if (conflictColumns.isEmpty()) { + val msg = + "Table '$table' doesn't have a primary key, " + + "you must specify the conflict columns when calling onConflict(col) { .. }" + throw IllegalStateException(msg) + } + + if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { + val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + + " you must explicitly invoke `doNothing()`" + throw IllegalStateException(msg) + } + + val execute: (List>>) -> Unit = { assignments -> + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = assignments, + conflictColumns = conflictColumns.map { it.asExpression() }, + updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + returningColumns = returningColumns.map { it.asExpression() } + ) + + val (total, rows) = executeUpdateAndRetrieveKeys(expression) + + affectedTotal += total + cachedRowSets.add(rows) + } + + executeQueryInBatches(builder, execute) + + return Pair(affectedTotal, cachedRowSets) +} + +private fun > executeQueryInBatches( + builder: BulkInsertStatementBuilder, + execute: (List>>) -> Unit +) { + var batchAssignmentCount = 0 + val currentBatch = LinkedList>>() + builder.assignments.forEach { assignments -> + assignments.size.let { size -> + if (size > MAX_SQL_EXPR_BATCH_SIZE) { + throw IllegalArgumentException( + "The maximum number of assignments per item is $MAX_SQL_EXPR_BATCH_SIZE, but $size detected!" + ) + } + + currentBatch.add(assignments) + batchAssignmentCount += size + } + + if (batchAssignmentCount >= MAX_SQL_EXPR_BATCH_SIZE) { + execute(currentBatch) + currentBatch.clear() + batchAssignmentCount = 0 + } + } + if (currentBatch.isNotEmpty()) { + execute(currentBatch) + } +} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt new file mode 100644 index 00000000..bd9e7847 --- /dev/null +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ktorm.support.postgresql + +import org.ktorm.database.CachedRowSet +import java.util.* + +/** + * To document. + */ +public class CompositeCachedRowSet { + private val resultSets = LinkedList() + + /** + * To document. + * @param rs todo + * @return todo + */ + public fun add(rs: CachedRowSet) { + resultSets.add(rs) + } + + /** + * To document. + */ + @Suppress("IteratorHasNextCallsNextMethod") + public operator fun iterator(): Iterator = object : Iterator { + private var cursor = 0 + private var hasNext: Boolean? = null + + override fun hasNext(): Boolean { + val hasNext = (cursor < resultSets.size && resultSets[cursor].next()).also { hasNext = it } + + if (!hasNext) { + return ++cursor < resultSets.size && hasNext() + } + + return hasNext + } + + override fun next(): CachedRowSet { + return if (hasNext ?: hasNext()) { + resultSets[cursor].also { hasNext = null } + } else { + throw NoSuchElementException() + } + } + } + + /** + * To document. + */ + public fun asIterable(): Iterable { + return Iterable { iterator() } + } +} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index f81cacfd..29d06ed3 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -16,7 +16,9 @@ package org.ktorm.support.postgresql +import org.ktorm.database.CachedRowSet import org.ktorm.database.Database +import org.ktorm.database.asIterable import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl import org.ktorm.expression.ColumnAssignmentExpression @@ -38,8 +40,9 @@ import org.ktorm.schema.Column public data class InsertOrUpdateExpression( val table: TableExpression, val assignments: List>, - val conflictColumns: List> = emptyList(), + val conflictColumns: List>? = null, val updateAssignments: List> = emptyList(), + val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() ) : SqlExpression() @@ -85,7 +88,7 @@ public fun > Database.insertOrUpdate( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } @@ -93,7 +96,7 @@ public fun > Database.insertOrUpdate( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = builder.updateAssignments + updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, ) return executeUpdate(expression) @@ -119,6 +122,8 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { internal val updateAssignments = ArrayList>() internal val conflictColumns = ArrayList>() + internal var explicitlyDoNothing: Boolean = false + /** * Specify the update assignments while any key conflict exists. */ @@ -133,9 +138,224 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { /** * Specify the update assignments while any key conflict exists. */ - public fun onConflict(vararg columns: Column<*>, block: AssignmentsBuilder.() -> Unit) { - val builder = PostgreSqlAssignmentsBuilder().apply(block) + public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { + val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) + + explicitlyDoNothing = builder.explicitlyDoNothing + updateAssignments += builder.assignments + conflictColumns += columns } } + +/** + * DSL builder for insert or update on conflict clause. + */ +@KtormDsl +public class InsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBuilder() { + internal var explicitlyDoNothing: Boolean = false + + /** + * Explicitly tells ktorm to ignore any on-conflict errors and continue insertion. + */ + public fun doNothing() { + this.explicitlyDoNothing = true + } + + /** + * Reference the 'EXCLUDED' table in a ON CONFLICT clause. + */ + public fun excluded(column: Column): ColumnExpression { + // excluded.name + return ColumnExpression( + table = TableExpression(name = "excluded"), + name = column.name, + sqlType = column.sqlType + ) + } +} + +/** + * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically + * performs an update if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.insertOrUpdateReturning(Employees, Employees.id) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onDuplicateKey { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returningColumn the column to return + * @param block the DSL block used to construct the expression. + * @return the returning column value. + */ +public fun , R : Any> Database.insertOrUpdateReturning( + table: T, + returningColumn: Column, + block: InsertOrUpdateStatementBuilder.(T) -> Unit +): R? { + val (_, rowSet) = this.insertOrUpdateReturningAux( + table, + listOfNotNull(returningColumn), + block + ) + + return rowSet.asIterable().map { row -> + returningColumn.sqlType.getResult(row, 1) + }.first() +} + +/** + * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically + * performs an update if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onDuplicateKey { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returningColumns the columns to return + * @param block the DSL block used to construct the expression. + * @return the returning columns' values. + */ +public fun , R1 : Any, R2 : Any> Database.insertOrUpdateReturning( + table: T, + returningColumns: Pair, Column>, + block: InsertOrUpdateStatementBuilder.(T) -> Unit +): Pair { + val (_, rowSet) = this.insertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + Pair( + returningColumns.first.sqlType.getResult(row, 1), + returningColumns.second.sqlType.getResult(row, 2) + ) + }.first() +} + +/** + * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically + * performs an update if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.insertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * onDuplicateKey { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * returning id, job, salary + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returningColumns the columns to return + * @param block the DSL block used to construct the expression. + * @return the returning columns' values. + */ +public fun , R1 : Any, R2 : Any, R3 : Any> Database.insertOrUpdateReturning( + table: T, + returningColumns: Triple, Column, Column>, + block: InsertOrUpdateStatementBuilder.(T) -> Unit +): Triple { + val (_, rowSet) = this.insertOrUpdateReturningAux( + table, + returningColumns.toList(), + block + ) + + return rowSet.asIterable().map { row -> + var i = 0 + Triple( + returningColumns.first.sqlType.getResult(row, ++i), + returningColumns.second.sqlType.getResult(row, ++i), + returningColumns.third.sqlType.getResult(row, ++i) + ) + }.first() +} + +private fun > Database.insertOrUpdateReturningAux( + table: T, + returningColumns: List>, + block: InsertOrUpdateStatementBuilder.(T) -> Unit +): Pair { + val builder = InsertOrUpdateStatementBuilder().apply { block(table) } + + val primaryKeys = table.primaryKeys + if (primaryKeys.isEmpty() && builder.conflictColumns.isEmpty()) { + val msg = + "Table '$table' doesn't have a primary key, " + + "you must specify the conflict columns when calling onDuplicateKey(col) { .. }" + throw IllegalStateException(msg) + } + + val expression = InsertOrUpdateExpression( + table = table.asExpression(), + assignments = builder.assignments, + conflictColumns = builder.conflictColumns.ifEmpty { primaryKeys }.map { it.asExpression() }, + updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + returningColumns = returningColumns.map { it.asExpression() } + ) + + return executeUpdateAndRetrieveKeys(expression) +} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 7aae5851..358474ae 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -153,11 +153,25 @@ public open class PostgreSqlFormatter( writeKeyword("values ") writeInsertValues(expr.assignments) - if (expr.updateAssignments.isNotEmpty()) { + if (expr.conflictColumns != null) { writeKeyword("on conflict ") writeInsertColumnNames(expr.conflictColumns) - writeKeyword("do update set ") - visitColumnAssignments(expr.updateAssignments) + + if (expr.updateAssignments.isNotEmpty()) { + writeKeyword("do update set ") + visitColumnAssignments(expr.updateAssignments) + } else { + writeKeyword("do nothing ") + } + } + + if (expr.returningColumns.isNotEmpty()) { + writeKeyword(" returning ") + expr.returningColumns.forEachIndexed { i, column -> + if (i > 0) write(", ") + checkColumnName(column.name) + write(column.name.quoted) + } } return expr @@ -177,11 +191,25 @@ public open class PostgreSqlFormatter( writeInsertValues(assignments) } - if (expr.updateAssignments.isNotEmpty()) { + if (expr.conflictColumns != null) { writeKeyword("on conflict ") writeInsertColumnNames(expr.conflictColumns) - writeKeyword("do update set ") - visitColumnAssignments(expr.updateAssignments) + + if (expr.updateAssignments.isNotEmpty()) { + writeKeyword("do update set ") + visitColumnAssignments(expr.updateAssignments) + } else { + writeKeyword("do nothing ") + } + } + + if (expr.returningColumns.isNotEmpty()) { + writeKeyword(" returning ") + expr.returningColumns.forEachIndexed { i, column -> + if (i > 0) write(", ") + checkColumnName(column.name) + write(column.name.quoted) + } } return expr @@ -239,7 +267,7 @@ public open class PostgreSqlExpressionVisitor : SqlExpressionVisitor() { protected open fun visitInsertOrUpdate(expr: InsertOrUpdateExpression): InsertOrUpdateExpression { val table = visitTable(expr.table) val assignments = visitColumnAssignments(expr.assignments) - val conflictColumns = visitExpressionList(expr.conflictColumns) + val conflictColumns = if (expr.conflictColumns != null) visitExpressionList(expr.conflictColumns) else null val updateAssignments = visitColumnAssignments(expr.updateAssignments) @Suppress("ComplexCondition") @@ -262,7 +290,7 @@ public open class PostgreSqlExpressionVisitor : SqlExpressionVisitor() { protected open fun visitBulkInsert(expr: BulkInsertExpression): BulkInsertExpression { val table = expr.table val assignments = visitBulkInsertAssignments(expr.assignments) - val conflictColumns = visitExpressionList(expr.conflictColumns) + val conflictColumns = if (expr.conflictColumns != null) visitExpressionList(expr.conflictColumns) else null val updateAssignments = visitColumnAssignments(expr.updateAssignments) @Suppress("ComplexCondition") diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 2a026e7d..8e2f6bd5 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -22,6 +22,7 @@ import java.util.concurrent.ExecutionException import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException +import kotlin.math.roundToInt /** * Created by vince on Feb 13, 2019. @@ -161,6 +162,73 @@ class PostgreSqlTest : BaseTest() { assert(database.employees.count() == 6) } + @Test + fun testInsertOrUpdateReturning() { + database.insertOrUpdateReturning( + Employees, + Employees.id + ) { + set(it.id, 1009) + set(it.name, "pedro") + set(it.job, "engineer") + set(it.salary, 1500) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + + onDuplicateKey { + set(it.salary, it.salary + 900) + } + }.let { createdId -> + assert(createdId == 1009) + } + + database.insertOrUpdateReturning( + Employees, + Pair( + Employees.id, + Employees.name + ) + ) { + set(it.id, 1001) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + + onDuplicateKey { + set(it.salary, it.salary + 900) + } + }.let { (createdId, createdName) -> + assert(createdId == 1001) + assert(createdName == "vince") + } + + database.insertOrUpdateReturning( + Employees, + Triple( + Employees.id, + Employees.name, + Employees.salary + ) + ) { + set(it.id, 1001) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + + onDuplicateKey(it.id) { + set(it.salary, it.salary + 900) + } + }.let { (createdId, createdName, createdSalary) -> + assert(createdId == 1001) + assert(createdName == "vince") + assert(createdSalary == 1900L) + } + } + @Test fun testBulkInsertOrUpdate() { database.bulkInsertOrUpdate(Employees) { @@ -200,6 +268,189 @@ class PostgreSqlTest : BaseTest() { } } + @Test + fun testBulkInsertWithUpdate() { + // Make sure we are creating new entries in the table (avoid colliding with existing test data) + val id1 = (Math.random() * 10000).roundToInt() + val id2 = (Math.random() * 10000).roundToInt() + + val bulkInsertWithUpdate = { onDuplicateKeyDoNothing: Boolean -> + database.bulkInsertOrUpdate(Employees) { + item { + set(it.id, id1) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + } + item { + set(it.id, id2) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + } + onConflict(Employees.id) { + if (!onDuplicateKeyDoNothing) + set(it.salary, it.salary + 900) + else + doNothing() + } + } + } + + bulkInsertWithUpdate(false) + assert(database.employees.find { it.id eq id1 }!!.salary == 1000L) + assert(database.employees.find { it.id eq id2 }!!.salary == 1000L) + + bulkInsertWithUpdate(false) + assert(database.employees.find { it.id eq id1 }!!.salary == 1900L) + assert(database.employees.find { it.id eq id2 }!!.salary == 1900L) + + bulkInsertWithUpdate(true) + assert(database.employees.find { it.id eq id1 }!!.salary == 1900L) + assert(database.employees.find { it.id eq id2 }!!.salary == 1900L) + } + + @Test + fun testBulkInsertReturning() { + database.bulkInsertReturning( + Employees, + Employees.id + ) { + item { + set(it.id, 10001) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 50001) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { createdIds -> + assert(createdIds.size == 2) + assert( + listOf( + 10001, + 50001 + ) == createdIds + ) + } + + database.bulkInsertReturning( + Employees, + Pair( + Employees.id, + Employees.name + ) + ) { + item { + set(it.id, 10002) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 50002) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { created -> + assert( + listOf( + (10002 to "vince"), + (50002 to "vince") + ) == created + ) + } + + database.bulkInsertReturning( + Employees, + Triple( + Employees.id, + Employees.name, + Employees.job + ) + ) { + item { + set(it.id, 10003) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 50003) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { created -> + assert( + listOf( + Triple(10003, "vince", "trainee"), + Triple(50003, "vince", "engineer") + ) == created + ) + } + } + + @Test + fun testBulkInsertOrUpdateReturning() { + database.bulkInsertOrUpdateReturning( + Employees, + Pair( + Employees.id, + Employees.job + ) + ) { + item { + set(it.id, 1000) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 5000) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + onConflict(it.id) { + set(it.departmentId, excluded(it.departmentId)) + set(it.salary, it.salary + 1000) + } + }.let { created -> + assert( + listOf( + Pair(1000, "trainee"), + Pair(5000, "engineer") + ) == created + ) + } + } + @Test fun testInsertAndGenerateKey() { val id = database.insertAndGenerateKey(Employees) { From 21d00cdc0fc7702b68a113831f7ad317087af5db Mon Sep 17 00:00:00 2001 From: PedroD Date: Fri, 5 Mar 2021 11:17:31 +0000 Subject: [PATCH 28/96] Adding remaining documentation --- .../support/postgresql/CompositeCachedRowSet.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt index bd9e7847..77eeb4b8 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt @@ -20,22 +20,23 @@ import org.ktorm.database.CachedRowSet import java.util.* /** - * To document. + * Utility class that stores the resulting CachedRowSet from + * multiple queries, but abstract their iteration as if they were + * a single CachedRowSet. */ public class CompositeCachedRowSet { private val resultSets = LinkedList() /** - * To document. - * @param rs todo - * @return todo + * Adds a CachedRowSet to the composite group. + * @param rs the new CachedRowSet */ public fun add(rs: CachedRowSet) { resultSets.add(rs) } /** - * To document. + * Returns the iterator for this composite. */ @Suppress("IteratorHasNextCallsNextMethod") public operator fun iterator(): Iterator = object : Iterator { @@ -62,7 +63,7 @@ public class CompositeCachedRowSet { } /** - * To document. + * Returns the iterator for this composite. */ public fun asIterable(): Iterable { return Iterable { iterator() } From 0ee0f38d1fb1e28dbfa4832badc4f92ef8ad2802 Mon Sep 17 00:00:00 2001 From: PedroD Date: Fri, 5 Mar 2021 15:08:42 +0000 Subject: [PATCH 29/96] Adding forgotten error messages --- .../org/ktorm/support/postgresql/InsertOrUpdate.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 29d06ed3..74447c28 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -92,6 +92,12 @@ public fun > Database.insertOrUpdate( throw IllegalStateException(msg) } + if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { + val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + + " you must explicitly invoke `doNothing()`" + throw IllegalStateException(msg) + } + val expression = InsertOrUpdateExpression( table = table.asExpression(), assignments = builder.assignments, @@ -349,6 +355,12 @@ private fun > Database.insertOrUpdateReturningAux( throw IllegalStateException(msg) } + if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { + val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + + " you must explicitly invoke `doNothing()`" + throw IllegalStateException(msg) + } + val expression = InsertOrUpdateExpression( table = table.asExpression(), assignments = builder.assignments, From dd311a994702c15676bf63b3ea826799c0e3ae54 Mon Sep 17 00:00:00 2001 From: PedroD Date: Fri, 23 Apr 2021 10:26:20 +0100 Subject: [PATCH 30/96] Fixing limit scenario of queries with millions of parameters --- .../ktorm/support/postgresql/BulkInsert.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index a63b4de2..7a816e7e 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -180,14 +180,13 @@ public fun > Database.bulkInsertOrUpdate( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { - val msg = - "You cannot leave a on-conflict clause empty! If you desire no update action at all " + - "you must explicitly invoke doNothing()" + val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + + " you must explicitly invoke `doNothing()`" throw IllegalStateException(msg) } @@ -672,14 +671,13 @@ private fun > Database.bulkInsertOrUpdateReturningAux( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { - val msg = - "You cannot leave a on-conflict clause empty! If you desire no update action at all " + - "you must explicitly invoke `doNothing()`" + val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + + " you must explicitly invoke `doNothing()`" throw IllegalStateException(msg) } @@ -717,16 +715,17 @@ private fun > executeQueryInBatches( ) } + if (batchAssignmentCount + size >= MAX_SQL_EXPR_BATCH_SIZE) { + execute(currentBatch) + currentBatch.clear() + batchAssignmentCount = 0 + } + currentBatch.add(assignments) batchAssignmentCount += size } - - if (batchAssignmentCount >= MAX_SQL_EXPR_BATCH_SIZE) { - execute(currentBatch) - currentBatch.clear() - batchAssignmentCount = 0 - } } + // Flush the remaining if (currentBatch.isNotEmpty()) { execute(currentBatch) } From f1691b2265943845eafcaa3c701f52430503c1c1 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sat, 24 Apr 2021 21:38:39 +0800 Subject: [PATCH 31/96] rm forUpdate from core module --- .../main/kotlin/org/ktorm/database/SqlDialect.kt | 5 ----- ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt | 14 -------------- .../main/kotlin/org/ktorm/entity/EntitySequence.kt | 11 ----------- .../kotlin/org/ktorm/expression/SqlExpressions.kt | 12 ------------ .../kotlin/org/ktorm/expression/SqlFormatter.kt | 5 ----- 5 files changed, 47 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt index 2c619283..584d83f2 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt @@ -17,7 +17,6 @@ package org.ktorm.database import org.ktorm.expression.ArgumentExpression -import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.QueryExpression import org.ktorm.expression.SqlFormatter import java.sql.Statement @@ -51,10 +50,6 @@ public interface SqlDialect { */ public fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int): SqlFormatter { return object : SqlFormatter(database, beautifySql, indentSize) { - override fun writeForUpdate(forUpdate: ForUpdateOption) { - throw DialectFeatureNotSupportedException("ForUpdate is not supported in Standard SQL.") - } - override fun writePagination(expr: QueryExpression) { throw DialectFeatureNotSupportedException("Pagination is not supported in Standard SQL.") } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt index 71d5f359..7b0bbb3b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt @@ -761,17 +761,3 @@ public fun Query.joinToString( ): String { return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() } - -/** - * Indicate that this query should acquire the record-lock, the generated SQL will depend on the SqlDialect. - * - * @since 3.1.0 - */ -public fun Query.forUpdate(forUpdate: ForUpdateOption): Query { - val expr = when (expression) { - is SelectExpression -> expression.copy(forUpdate = forUpdate) - is UnionExpression -> throw IllegalStateException("SELECT FOR UPDATE is not supported in a union expression.") - } - - return this.withExpression(expr) -} diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index 1f926889..eccade8d 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -19,7 +19,6 @@ package org.ktorm.entity import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.dsl.* -import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.OrderByExpression import org.ktorm.expression.SelectExpression import org.ktorm.schema.BaseTable @@ -1504,13 +1503,3 @@ public fun EntitySequence.joinToString( ): String { return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() } - -/** - * Indicate that this query should acquire the record-lock, the generated SQL will depend on the SqlDialect. - * - * @since 3.1.0 - */ -public fun > EntitySequence.forUpdate( - forUpdate: ForUpdateOption): EntitySequence { - return this.withExpression(expression.copy(forUpdate = forUpdate)) -} diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index 096b17c1..8b933fc0 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -115,8 +115,6 @@ public sealed class QueryExpression : QuerySourceExpression() { * @property groupBy the grouping conditions, represents the `group by` clause of SQL. * @property having the having condition, represents the `having` clause of SQL. * @property isDistinct mark if this query is distinct, true means the SQL is `select distinct ...`. - * @property forUpdate mark if this query should acquire the record-lock, non-null will generate a dialect-specific - * `for update` clause. */ public data class SelectExpression( val columns: List> = emptyList(), @@ -125,7 +123,6 @@ public data class SelectExpression( val groupBy: List> = emptyList(), val having: ScalarExpression? = null, val isDistinct: Boolean = false, - val forUpdate: ForUpdateOption = ForUpdateOption.None, override val orderBy: List = emptyList(), override val offset: Int? = null, override val limit: Int? = null, @@ -133,15 +130,6 @@ public data class SelectExpression( override val extraProperties: Map = emptyMap() ) : QueryExpression() -/** - * ForUpdateOption, database-specific implementations are in support module for each database dialect. - */ -public interface ForUpdateOption { - public companion object { - public val None: ForUpdateOption = object : ForUpdateOption {} - } -} - /** * Union expression, represents a `union` statement of SQL. * diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 199e45ce..8b797cc8 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -388,14 +388,9 @@ public abstract class SqlFormatter( if (expr.offset != null || expr.limit != null) { writePagination(expr) } - if (expr.forUpdate != ForUpdateOption.None) { - writeForUpdate(expr.forUpdate) - } return expr } - protected abstract fun writeForUpdate(forUpdate: ForUpdateOption) - override fun visitQuerySource(expr: QuerySourceExpression): QuerySourceExpression { when (expr) { is TableExpression -> { From 4a91021f9ebd4cf3aabbb6bafac928f3b1140570 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sat, 24 Apr 2021 22:18:49 +0800 Subject: [PATCH 32/96] mysql select for update --- .../kotlin/org/ktorm/support/mysql/Lock.kt | 65 +++++++++++++++++++ .../org/ktorm/support/mysql/MySqlDialect.kt | 64 ++++-------------- .../org/ktorm/support/mysql/MySqlTest.kt | 2 +- 3 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt new file mode 100644 index 00000000..53f6ca0d --- /dev/null +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt @@ -0,0 +1,65 @@ +package org.ktorm.support.mysql + +import org.ktorm.dsl.Query +import org.ktorm.entity.EntitySequence +import org.ktorm.expression.SelectExpression +import org.ktorm.expression.UnionExpression +import org.ktorm.schema.BaseTable + +internal enum class LockMode { + FOR_UPDATE, FOR_SHARE +} + +/** + * Indicate that this query should acquire the record-lock, + * the generated SQL would be `select ... for update`. + * + * @since 3.4.0 + */ +public fun Query.forUpdate(): Query { + val expr = when (val e = this.expression) { + is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("lockMode", LockMode.FOR_UPDATE)) + is UnionExpression -> throw IllegalStateException("forUpdate() is not supported in a union expression.") + } + + return this.withExpression(expr) +} + +/** + * Indicate that this query should acquire the record-lock, + * the generated SQL would be `select ... for update`. + * + * @since 3.4.0 + */ +public fun > EntitySequence.forUpdate(): EntitySequence { + return this.withExpression( + expression.copy(extraProperties = expression.extraProperties + Pair("lockMode", LockMode.FOR_UPDATE)) + ) +} + +/** + * Indicate that this query should acquire the record-lock in share mode, + * the generated SQL would be `select ... lock in share mode`. + * + * @since 3.4.0 + */ +public fun Query.lockInShareMode(): Query { + val expr = when (val e = this.expression) { + is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("lockMode", LockMode.FOR_SHARE)) + is UnionExpression -> throw IllegalStateException("lockInShareMode() is not supported in a union expression.") + } + + return this.withExpression(expr) +} + +/** + * Indicate that this query should acquire the record-lock in share mode, + * the generated SQL would be `select ... lock in share mode`. + * + * @since 3.4.0 + */ +public fun > EntitySequence.lockInShareMode(): EntitySequence { + return this.withExpression( + expression.copy(extraProperties = expression.extraProperties + Pair("lockMode", LockMode.FOR_SHARE)) + ) +} diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 79113620..f7794b85 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -17,15 +17,10 @@ package org.ktorm.support.mysql import org.ktorm.database.Database -import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType import org.ktorm.schema.VarcharSqlType -import org.ktorm.support.mysql.MySqlForUpdateOption.ForShare -import org.ktorm.support.mysql.MySqlForUpdateOption.ForUpdate -import org.ktorm.support.mysql.Version.MySql5 -import org.ktorm.support.mysql.Version.MySql8 /** * [SqlDialect] implementation for MySQL database. @@ -37,33 +32,12 @@ public open class MySqlDialect : SqlDialect { } } -private enum class Version { - MySql5, MySql8 -} - -/** - * Thrown to indicate that the MySql version is not supported by the dialect. - * - * @param databaseMetaData used to format the exception's message. - */ -public class UnsupportedMySqlVersionException(productVersion: String) : - UnsupportedOperationException("Unsupported MySql version $productVersion.") { - private companion object { - private const val serialVersionUID = 1L - } -} - /** * [SqlFormatter] implementation for MySQL, formatting SQL expressions as strings with their execution arguments. */ public open class MySqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { - private val version: Version = when { - database.productVersion.startsWith("5") -> MySql5 - database.productVersion.startsWith("8") -> MySql8 - else -> throw UnsupportedMySqlVersionException(database.productVersion) - } override fun visit(expr: SqlExpression): SqlExpression { val result = when (expr) { @@ -93,6 +67,20 @@ public open class MySqlFormatter( } } + override fun visitSelect(expr: SelectExpression): SelectExpression { + super.visitSelect(expr) + + val lockMode = expr.extraProperties["lockMode"] as LockMode? + if (lockMode == LockMode.FOR_UPDATE) { + writeKeyword("for update ") + } + if (lockMode == LockMode.FOR_SHARE) { + writeKeyword("lock in share mode ") + } + + return expr + } + override fun writePagination(expr: QueryExpression) { newLine(Indentation.SAME) writeKeyword("limit ?, ? ") @@ -155,30 +143,6 @@ public open class MySqlFormatter( write(") ") return expr } - - override fun writeForUpdate(forUpdate: ForUpdateOption) { - when { - forUpdate == ForUpdate -> writeKeyword("for update ") - forUpdate == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") - forUpdate == ForShare && version == MySql8 -> writeKeyword("for share ") - else -> throw DialectFeatureNotSupportedException( - "Unsupported ForUpdateOption ${forUpdate::class.java.name}." - ) - } - } -} - -/** - * MySql Specific ForUpdateOptions. - */ -public sealed class MySqlForUpdateOption : ForUpdateOption { - /** - * The generated SQL would be `select ... lock in share mode` for MySql 5 and `select ... for share` for MySql 8. - **/ - public object ForShare : MySqlForUpdateOption() - - /** The generated SQL would be `select ... for update`. */ - public object ForUpdate : MySqlForUpdateOption() } /** diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 125a240f..431aea8d 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -422,7 +422,7 @@ class MySqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate(MySqlForUpdateOption.ForUpdate) + .forUpdate() .first() val future = Executors.newSingleThreadExecutor().submit { From 2f34363bed94cab7a493452ab1cd16a3b2f9521d Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sat, 24 Apr 2021 23:25:19 +0800 Subject: [PATCH 33/96] rm forUpdate() for oracle & sqlserver --- .../org/ktorm/support/oracle/OracleDialect.kt | 28 +++---------- .../org/ktorm/support/oracle/OracleTest.kt | 39 ++----------------- .../org/ktorm/support/sqlite/SQLiteDialect.kt | 4 -- .../support/sqlserver/SqlServerDialect.kt | 28 +++---------- 4 files changed, 16 insertions(+), 83 deletions(-) diff --git a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt index 8cc7e843..c55853c9 100644 --- a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt +++ b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt @@ -17,11 +17,9 @@ package org.ktorm.support.oracle import org.ktorm.database.Database -import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType -import org.ktorm.support.oracle.OracleForUpdateOption.ForUpdate /** * [SqlDialect] implementation for Oracle database. @@ -33,14 +31,6 @@ public open class OracleDialect : SqlDialect { } } -/** - * Oracle Specific ForUpdateOptions. - */ -public sealed class OracleForUpdateOption : ForUpdateOption { - /** The generated SQL would be `select ... for update`. */ - public object ForUpdate : OracleForUpdateOption() -} - /** * [SqlFormatter] implementation for Oracle, formatting SQL expressions as strings with their execution arguments. */ @@ -60,22 +50,16 @@ public open class OracleFormatter( return identifier.startsWith('_') || super.shouldQuote(identifier) } - override fun writeForUpdate(forUpdate: ForUpdateOption) { - when (forUpdate) { - ForUpdate -> writeKeyword("for update ") - else -> throw DialectFeatureNotSupportedException( - "Unsupported ForUpdateOption ${forUpdate::class.java.name}." - ) - } - } - override fun visitQuery(expr: QueryExpression): QueryExpression { if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate != ForUpdateOption.None) { - throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") - } + + // forUpdate() function in the core lib was removed, uncomment the following lines + // when we add this feature back in the Oracle dialect. + // if (expr is SelectExpression && expr.forUpdate) { + // throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") + // } val offset = expr.offset ?: 0 val minRowNum = offset + 1 diff --git a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt index 6a465f2c..f70789d6 100644 --- a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt +++ b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt @@ -1,24 +1,21 @@ package org.ktorm.support.oracle import org.junit.ClassRule -import org.junit.Ignore import org.junit.Test import org.ktorm.BaseTest import org.ktorm.database.Database -import org.ktorm.database.TransactionIsolation import org.ktorm.database.use import org.ktorm.dsl.* -import org.ktorm.entity.* +import org.ktorm.entity.count +import org.ktorm.entity.filter +import org.ktorm.entity.mapTo +import org.ktorm.entity.sequenceOf import org.ktorm.logging.ConsoleLogger import org.ktorm.logging.LogLevel import org.ktorm.schema.Table import org.ktorm.schema.int import org.ktorm.schema.varchar import org.testcontainers.containers.OracleContainer -import java.util.concurrent.ExecutionException -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException /** * Created by vince at Aug 01, 2020. @@ -148,34 +145,6 @@ class OracleTest : BaseTest() { } } - @Test - @Ignore - fun testSelectForUpdate() { - database.useTransaction(isolation = TransactionIsolation.SERIALIZABLE) { - val employee = database - .sequenceOf(Employees, withReferences = false) - .filter { it.id eq 1 } - .forUpdate(OracleForUpdateOption.ForUpdate) - .single() - - val future = Executors.newSingleThreadExecutor().submit { - employee.name = "vince" - employee.flushChanges() - } - - try { - future.get(5, TimeUnit.SECONDS) - throw AssertionError() - } catch (e: ExecutionException) { - // Expected, the record is locked. - e.printStackTrace() - } catch (e: TimeoutException) { - // Expected, the record is locked. - e.printStackTrace() - } - } - } - @Test fun testSchema() { val t = object : Table("t_department", schema = oracle.username.toUpperCase()) { diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt index 8b3a51cb..d6d90cd9 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt @@ -18,7 +18,6 @@ package org.ktorm.support.sqlite import org.ktorm.database.* import org.ktorm.expression.ArgumentExpression -import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.QueryExpression import org.ktorm.expression.SqlFormatter import org.ktorm.schema.IntSqlType @@ -63,9 +62,6 @@ public open class SQLiteDialect : SqlDialect { public open class SQLiteFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { - override fun writeForUpdate(forUpdate: ForUpdateOption) { - throw DialectFeatureNotSupportedException("SQLite does not support SELECT ... FOR UPDATE.") - } override fun writePagination(expr: QueryExpression) { newLine(Indentation.SAME) diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt index e7cd298e..2a118a27 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt @@ -17,10 +17,8 @@ package org.ktorm.support.sqlserver import org.ktorm.database.Database -import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* -import org.ktorm.support.sqlserver.SqlServerForUpdateOption.ForUpdate /** * [SqlDialect] implementation for Microsoft SqlServer database. @@ -32,14 +30,6 @@ public open class SqlServerDialect : SqlDialect { } } -/** - * SqlServer Specific ForUpdateOptions. - */ -public sealed class SqlServerForUpdateOption : ForUpdateOption { - /** The generated SQL would be `select ... for update`. */ - public object ForUpdate : SqlServerForUpdateOption() -} - /** * [SqlFormatter] implementation for SqlServer, formatting SQL expressions as strings with their execution arguments. */ @@ -54,22 +44,16 @@ public open class SqlServerFormatter( } } - override fun writeForUpdate(forUpdate: ForUpdateOption) { - when (forUpdate) { - ForUpdate -> writeKeyword("for update ") - else -> throw DialectFeatureNotSupportedException( - "Unsupported ForUpdateOption ${forUpdate::class.java.name}." - ) - } - } - override fun visitQuery(expr: QueryExpression): QueryExpression { if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate != ForUpdateOption.None) { - throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") - } + + // forUpdate() function in the core lib was removed, uncomment the following lines + // when we add this feature back in the SqlServer dialect. + // if (expr is SelectExpression && expr.forUpdate) { + // throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") + // } if (expr.orderBy.isEmpty()) { writePagingQuery(expr) From 4b99d0aa3cc90d914ef1f78198e02bd7f7780f3f Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 25 Apr 2021 23:44:45 +0800 Subject: [PATCH 34/96] postgresql locking clause --- .../org/ktorm/support/postgresql/Lock.kt | 86 +++++++++++++++++++ .../support/postgresql/PostgreSqlDialect.kt | 85 ++++++++---------- .../support/postgresql/PostgreSqlTest.kt | 4 +- 3 files changed, 121 insertions(+), 54 deletions(-) create mode 100644 ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt new file mode 100644 index 00000000..262009f2 --- /dev/null +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt @@ -0,0 +1,86 @@ +package org.ktorm.support.postgresql + +import org.ktorm.dsl.Query +import org.ktorm.entity.EntitySequence +import org.ktorm.expression.SelectExpression +import org.ktorm.expression.UnionExpression +import org.ktorm.schema.BaseTable +import org.ktorm.support.postgresql.LockingStrength.* +import org.ktorm.support.postgresql.LockingWait.* + +/** + * PostgreSQL lock strength. + * + * @since 3.4.0 + */ +public enum class LockingStrength { + FOR_UPDATE, + FOR_NO_KEY_UPDATE, + FOR_SHARE, + FOR_KEY_SHARE +} + +/** + * PostgreSQL waiting strategy for locked records. + * + * @since 3.4.0 + */ +public enum class LockingWait { + BLOCK, + NOWAIT, + SKIP_LOCKED +} + +/** + * PostgreSQL locking clause. + * + * @since 3.4.0 + */ +public data class LockingClause( + val strength: LockingStrength, + val tables: List, + val wait: LockingWait +) + +/** + * Specify the locking clause of this query, an example generated SQL could be: + * + * `select ... for update of table_name nowait` + * + * @param strength locking strength, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. + * @param tables specific the tables, then only rows coming from those tables would be locked. + * @param wait waiting strategy, one of [BLOCK], [NOWAIT], [SKIP_LOCKED]. + * @since 3.4.0 + */ +public fun Query.locking( + strength: LockingStrength, tables: List> = emptyList(), wait: LockingWait = BLOCK +): Query { + val locking = LockingClause(strength, tables.map { it.tableName }, wait) + + val expr = when (val e = this.expression) { + is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("locking", locking)) + is UnionExpression -> throw IllegalStateException("Locking clause is not supported for a union expression.") + } + + return this.withExpression(expr) +} + +/** + * Specify the locking clause of this query, an example generated SQL could be: + * + * `select ... for update of table_name nowait` + * + * @param strength locking strength, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. + * @param tables specific the tables, then only rows coming from those tables would be locked. + * @param wait waiting strategy, one of [BLOCK], [NOWAIT], [SKIP_LOCKED]. + * @since 3.4.0 + */ +public fun > EntitySequence.locking( + strength: LockingStrength, tables: List> = emptyList(), wait: LockingWait = BLOCK +): EntitySequence { + val locking = LockingClause(strength, tables.map { it.tableName }, wait) + + return this.withExpression( + expression.copy(extraProperties = expression.extraProperties + Pair("locking", locking)) + ) +} diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 1a9c57bf..b21a44d8 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -17,11 +17,9 @@ package org.ktorm.support.postgresql import org.ktorm.database.Database -import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType -import org.ktorm.schema.Table /** * [SqlDialect] implementation for PostgreSQL database. @@ -33,61 +31,12 @@ public open class PostgreSqlDialect : SqlDialect { } } -/** - * Postgres Specific ForUpdateOption. See docs: https://www.postgresql.org/docs/13/sql-select.html#SQL-FOR-UPDATE-SHARE - */ -public class PostgresForUpdateOption( - private val lockStrength: LockStrength, - private val onLock: OnLock, - private vararg val tables: Table<*> -) : ForUpdateOption { - - /** - * Generates SQL locking clause. - */ - public fun toLockingClause(): String { - val lockingClause = StringBuilder(lockStrength.keywords) - if (tables.isNotEmpty()) { - tables.joinTo(lockingClause, prefix = "of ", postfix = " ") { it.tableName } - } - onLock.keywords?.let { lockingClause.append(it) } - return lockingClause.toString() - } - - /** - * Lock strength. - */ - public enum class LockStrength(public val keywords: String) { - Update("for update "), - NoKeyUpdate("for no key update "), - Share("for share "), - KeyShare("for key share ") - } - - /** - * Behavior when a lock is detected. - */ - public enum class OnLock(public val keywords: String?) { - Wait(null), - NoWait("no wait "), - SkipLocked("skip locked ") - } -} - /** * [SqlFormatter] implementation for PostgreSQL, formatting SQL expressions as strings with their execution arguments. */ public open class PostgreSqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { - override fun writeForUpdate(forUpdate: ForUpdateOption) { - when (forUpdate) { - is PostgresForUpdateOption -> writeKeyword(forUpdate.toLockingClause()) - else -> throw DialectFeatureNotSupportedException( - "Unsupported ForUpdateOption ${forUpdate::class.java.name}." - ) - } - } override fun checkColumnName(name: String) { val maxLength = database.maxColumnNameLength @@ -136,6 +85,40 @@ public open class PostgreSqlFormatter( return expr } + override fun visitSelect(expr: SelectExpression): SelectExpression { + super.visitSelect(expr) + + val locking = expr.extraProperties["locking"] as LockingClause? + if (locking != null) { + when (locking.strength) { + LockingStrength.FOR_UPDATE -> writeKeyword("for update ") + LockingStrength.FOR_NO_KEY_UPDATE -> writeKeyword("for no key update ") + LockingStrength.FOR_SHARE -> writeKeyword("for share ") + LockingStrength.FOR_KEY_SHARE -> writeKeyword("for key share ") + } + + if (locking.tables.isNotEmpty()) { + writeKeyword("of ") + + for ((i, table) in locking.tables.withIndex()) { + if (i > 0) { + removeLastBlank() + write(", ") + } + write("${table.quoted} ") + } + } + + when (locking.wait) { + LockingWait.BLOCK -> { /* do nothing */ } + LockingWait.NOWAIT -> writeKeyword("nowait ") + LockingWait.SKIP_LOCKED -> writeKeyword("skip locked ") + } + } + + return expr + } + override fun writePagination(expr: QueryExpression) { newLine(Indentation.SAME) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 34eee2f1..0cca1a42 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -16,8 +16,6 @@ import org.ktorm.schema.ColumnDeclaring import org.ktorm.schema.Table import org.ktorm.schema.int import org.ktorm.schema.varchar -import org.ktorm.support.postgresql.PostgresForUpdateOption.LockStrength.Update -import org.ktorm.support.postgresql.PostgresForUpdateOption.OnLock.Wait import org.testcontainers.containers.PostgreSQLContainer import java.time.LocalDate import java.util.concurrent.ExecutionException @@ -651,7 +649,7 @@ class PostgreSqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate(PostgresForUpdateOption(lockStrength = Update, onLock = Wait)) + .locking(strength = LockingStrength.FOR_UPDATE) .first() val future = Executors.newSingleThreadExecutor().submit { From 14a6419eb647ce09036425f83b96a810b5317278 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 25 Apr 2021 23:50:03 +0800 Subject: [PATCH 35/96] locking strength => locking mode --- .../org/ktorm/support/postgresql/Lock.kt | 20 +++++++++---------- .../support/postgresql/PostgreSqlTest.kt | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt index 262009f2..fb1bfe67 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt @@ -5,15 +5,15 @@ import org.ktorm.entity.EntitySequence import org.ktorm.expression.SelectExpression import org.ktorm.expression.UnionExpression import org.ktorm.schema.BaseTable -import org.ktorm.support.postgresql.LockingStrength.* +import org.ktorm.support.postgresql.LockingMode.* import org.ktorm.support.postgresql.LockingWait.* /** - * PostgreSQL lock strength. + * PostgreSQL lock mode. * * @since 3.4.0 */ -public enum class LockingStrength { +public enum class LockingMode { FOR_UPDATE, FOR_NO_KEY_UPDATE, FOR_SHARE, @@ -37,7 +37,7 @@ public enum class LockingWait { * @since 3.4.0 */ public data class LockingClause( - val strength: LockingStrength, + val mode: LockingMode, val tables: List, val wait: LockingWait ) @@ -47,15 +47,15 @@ public data class LockingClause( * * `select ... for update of table_name nowait` * - * @param strength locking strength, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. + * @param mode locking mode, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. * @param tables specific the tables, then only rows coming from those tables would be locked. * @param wait waiting strategy, one of [BLOCK], [NOWAIT], [SKIP_LOCKED]. * @since 3.4.0 */ public fun Query.locking( - strength: LockingStrength, tables: List> = emptyList(), wait: LockingWait = BLOCK + mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = BLOCK ): Query { - val locking = LockingClause(strength, tables.map { it.tableName }, wait) + val locking = LockingClause(mode, tables.map { it.tableName }, wait) val expr = when (val e = this.expression) { is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("locking", locking)) @@ -70,15 +70,15 @@ public fun Query.locking( * * `select ... for update of table_name nowait` * - * @param strength locking strength, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. + * @param mode locking mode, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. * @param tables specific the tables, then only rows coming from those tables would be locked. * @param wait waiting strategy, one of [BLOCK], [NOWAIT], [SKIP_LOCKED]. * @since 3.4.0 */ public fun > EntitySequence.locking( - strength: LockingStrength, tables: List> = emptyList(), wait: LockingWait = BLOCK + mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = BLOCK ): EntitySequence { - val locking = LockingClause(strength, tables.map { it.tableName }, wait) + val locking = LockingClause(mode, tables.map { it.tableName }, wait) return this.withExpression( expression.copy(extraProperties = expression.extraProperties + Pair("locking", locking)) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 0cca1a42..72f52a03 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -649,7 +649,7 @@ class PostgreSqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .locking(strength = LockingStrength.FOR_UPDATE) + .locking(LockingMode.FOR_UPDATE) .first() val future = Executors.newSingleThreadExecutor().submit { From 314d729a24f7c519113fc5573611f4589bcf6e85 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 25 Apr 2021 23:56:15 +0800 Subject: [PATCH 36/96] fix compile error --- .../main/kotlin/org/ktorm/support/postgresql/Lock.kt | 2 +- .../org/ktorm/support/postgresql/PostgreSqlDialect.kt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt index fb1bfe67..4f801726 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt @@ -32,7 +32,7 @@ public enum class LockingWait { } /** - * PostgreSQL locking clause. + * PostgreSQL locking clause. See https://www.postgresql.org/docs/13/sql-select.html#SQL-FOR-UPDATE-SHARE * * @since 3.4.0 */ diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index b21a44d8..4e286c60 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -90,11 +90,11 @@ public open class PostgreSqlFormatter( val locking = expr.extraProperties["locking"] as LockingClause? if (locking != null) { - when (locking.strength) { - LockingStrength.FOR_UPDATE -> writeKeyword("for update ") - LockingStrength.FOR_NO_KEY_UPDATE -> writeKeyword("for no key update ") - LockingStrength.FOR_SHARE -> writeKeyword("for share ") - LockingStrength.FOR_KEY_SHARE -> writeKeyword("for key share ") + when (locking.mode) { + LockingMode.FOR_UPDATE -> writeKeyword("for update ") + LockingMode.FOR_NO_KEY_UPDATE -> writeKeyword("for no key update ") + LockingMode.FOR_SHARE -> writeKeyword("for share ") + LockingMode.FOR_KEY_SHARE -> writeKeyword("for key share ") } if (locking.tables.isNotEmpty()) { From 5877491411b7e61b39c16ed0d6213b834fb75c2b Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 00:50:26 +0800 Subject: [PATCH 37/96] mysql locking clause --- .../kotlin/org/ktorm/support/mysql/Lock.kt | 74 ++++++++++++------- .../org/ktorm/support/mysql/MySqlDialect.kt | 31 ++++++-- .../org/ktorm/support/mysql/MySqlTest.kt | 2 +- .../org/ktorm/support/postgresql/Lock.kt | 18 ++--- .../support/postgresql/PostgreSqlDialect.kt | 2 +- .../support/postgresql/PostgreSqlTest.kt | 2 +- 6 files changed, 84 insertions(+), 45 deletions(-) diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt index 53f6ca0d..9c0d018a 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt @@ -5,61 +5,81 @@ import org.ktorm.entity.EntitySequence import org.ktorm.expression.SelectExpression import org.ktorm.expression.UnionExpression import org.ktorm.schema.BaseTable +import org.ktorm.support.mysql.LockingMode.* +import org.ktorm.support.mysql.LockingWait.* -internal enum class LockMode { - FOR_UPDATE, FOR_SHARE +/** + * MySQL locking mode. + * + * @since 3.4.0 + */ +public enum class LockingMode { + FOR_UPDATE, + FOR_SHARE, + LOCK_IN_SHARE_MODE } /** - * Indicate that this query should acquire the record-lock, - * the generated SQL would be `select ... for update`. + * MySQL wait strategy for locked records. * * @since 3.4.0 */ -public fun Query.forUpdate(): Query { - val expr = when (val e = this.expression) { - is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("lockMode", LockMode.FOR_UPDATE)) - is UnionExpression -> throw IllegalStateException("forUpdate() is not supported in a union expression.") - } - - return this.withExpression(expr) +public enum class LockingWait { + WAIT, + NOWAIT, + SKIP_LOCKED } /** - * Indicate that this query should acquire the record-lock, - * the generated SQL would be `select ... for update`. + * MySQL locking clause, See https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html * * @since 3.4.0 */ -public fun > EntitySequence.forUpdate(): EntitySequence { - return this.withExpression( - expression.copy(extraProperties = expression.extraProperties + Pair("lockMode", LockMode.FOR_UPDATE)) - ) -} +public data class LockingClause( + val mode: LockingMode, + val tables: List, + val wait: LockingWait +) /** - * Indicate that this query should acquire the record-lock in share mode, - * the generated SQL would be `select ... lock in share mode`. + * Specify the locking clause of this query, an example generated SQL could be: + * + * `select ... for update of table_name nowait` * + * @param mode locking mode, one of [FOR_UPDATE], [FOR_SHARE], [LOCK_IN_SHARE_MODE]. + * @param tables specific the tables, only rows coming from those tables would be locked. + * @param wait waiting strategy, one of [WAIT], [NOWAIT], [SKIP_LOCKED]. * @since 3.4.0 */ -public fun Query.lockInShareMode(): Query { +public fun Query.locking( + mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT +): Query { + val locking = LockingClause(mode, tables.map { it.tableName }, wait) + val expr = when (val e = this.expression) { - is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("lockMode", LockMode.FOR_SHARE)) - is UnionExpression -> throw IllegalStateException("lockInShareMode() is not supported in a union expression.") + is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("locking", locking)) + is UnionExpression -> throw IllegalStateException("Locking clause is not supported for a union expression.") } return this.withExpression(expr) } /** - * Indicate that this query should acquire the record-lock in share mode, - * the generated SQL would be `select ... lock in share mode`. + * Specify the locking clause of this query, an example generated SQL could be: + * + * `select ... for update of table_name nowait` * + * @param mode locking mode, one of [FOR_UPDATE], [FOR_SHARE], [LOCK_IN_SHARE_MODE]. + * @param tables specific the tables, only rows coming from those tables would be locked. + * @param wait waiting strategy, one of [WAIT], [NOWAIT], [SKIP_LOCKED]. * @since 3.4.0 */ -public fun > EntitySequence.lockInShareMode(): EntitySequence { +public fun > EntitySequence.locking( + mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT +): EntitySequence { + val locking = LockingClause(mode, tables.map { it.tableName }, wait) + return this.withExpression( - expression.copy(extraProperties = expression.extraProperties + Pair("lockMode", LockMode.FOR_SHARE)) + expression.copy(extraProperties = expression.extraProperties + Pair("locking", locking)) ) } diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index f7794b85..f96dd33f 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -70,12 +70,31 @@ public open class MySqlFormatter( override fun visitSelect(expr: SelectExpression): SelectExpression { super.visitSelect(expr) - val lockMode = expr.extraProperties["lockMode"] as LockMode? - if (lockMode == LockMode.FOR_UPDATE) { - writeKeyword("for update ") - } - if (lockMode == LockMode.FOR_SHARE) { - writeKeyword("lock in share mode ") + val locking = expr.extraProperties["locking"] as LockingClause? + if (locking != null) { + when (locking.mode) { + LockingMode.FOR_UPDATE -> writeKeyword("for update ") + LockingMode.FOR_SHARE -> writeKeyword("for share ") + LockingMode.LOCK_IN_SHARE_MODE -> writeKeyword("lock in share mode ") + } + + if (locking.tables.isNotEmpty()) { + writeKeyword("of ") + + for ((i, table) in locking.tables.withIndex()) { + if (i > 0) { + removeLastBlank() + write(", ") + } + write("${table.quoted} ") + } + } + + when (locking.wait) { + LockingWait.WAIT -> { /* do nothing */ } + LockingWait.NOWAIT -> writeKeyword("nowait ") + LockingWait.SKIP_LOCKED -> writeKeyword("skip locked ") + } } return expr diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 431aea8d..55da194d 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -422,7 +422,7 @@ class MySqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate() + .locking(LockingMode.FOR_UPDATE, wait = LockingWait.SKIP_LOCKED) .first() val future = Executors.newSingleThreadExecutor().submit { diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt index 4f801726..8538d563 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt @@ -9,7 +9,7 @@ import org.ktorm.support.postgresql.LockingMode.* import org.ktorm.support.postgresql.LockingWait.* /** - * PostgreSQL lock mode. + * PostgreSQL locking mode. * * @since 3.4.0 */ @@ -21,12 +21,12 @@ public enum class LockingMode { } /** - * PostgreSQL waiting strategy for locked records. + * PostgreSQL wait strategy for locked records. * * @since 3.4.0 */ public enum class LockingWait { - BLOCK, + WAIT, NOWAIT, SKIP_LOCKED } @@ -48,12 +48,12 @@ public data class LockingClause( * `select ... for update of table_name nowait` * * @param mode locking mode, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. - * @param tables specific the tables, then only rows coming from those tables would be locked. - * @param wait waiting strategy, one of [BLOCK], [NOWAIT], [SKIP_LOCKED]. + * @param tables specific the tables, only rows coming from those tables would be locked. + * @param wait waiting strategy, one of [WAIT], [NOWAIT], [SKIP_LOCKED]. * @since 3.4.0 */ public fun Query.locking( - mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = BLOCK + mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT ): Query { val locking = LockingClause(mode, tables.map { it.tableName }, wait) @@ -71,12 +71,12 @@ public fun Query.locking( * `select ... for update of table_name nowait` * * @param mode locking mode, one of [FOR_UPDATE], [FOR_NO_KEY_UPDATE], [FOR_SHARE], [FOR_KEY_SHARE]. - * @param tables specific the tables, then only rows coming from those tables would be locked. - * @param wait waiting strategy, one of [BLOCK], [NOWAIT], [SKIP_LOCKED]. + * @param tables specific the tables, only rows coming from those tables would be locked. + * @param wait waiting strategy, one of [WAIT], [NOWAIT], [SKIP_LOCKED]. * @since 3.4.0 */ public fun > EntitySequence.locking( - mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = BLOCK + mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT ): EntitySequence { val locking = LockingClause(mode, tables.map { it.tableName }, wait) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 4e286c60..2ba8bd0f 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -110,7 +110,7 @@ public open class PostgreSqlFormatter( } when (locking.wait) { - LockingWait.BLOCK -> { /* do nothing */ } + LockingWait.WAIT -> { /* do nothing */ } LockingWait.NOWAIT -> writeKeyword("nowait ") LockingWait.SKIP_LOCKED -> writeKeyword("skip locked ") } diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 72f52a03..a3293855 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -649,7 +649,7 @@ class PostgreSqlTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .locking(LockingMode.FOR_UPDATE) + .locking(LockingMode.FOR_UPDATE, wait = LockingWait.SKIP_LOCKED) .first() val future = Executors.newSingleThreadExecutor().submit { From d765fe97c20dec78fff98721ca1c475b5385528a Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 10:58:00 +0800 Subject: [PATCH 38/96] fix tests --- .../src/test/kotlin/org/ktorm/dsl/QueryTest.kt | 8 -------- .../kotlin/org/ktorm/support/mysql/Lock.kt | 16 ++++++++++++++++ .../org/ktorm/support/oracle/OracleDialect.kt | 2 +- .../org/ktorm/support/postgresql/Lock.kt | 16 ++++++++++++++++ .../support/postgresql/PostgreSqlDialect.kt | 18 ------------------ .../support/sqlserver/SqlServerDialect.kt | 2 +- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt index 85c5086a..6042bd08 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt @@ -2,15 +2,7 @@ package org.ktorm.dsl import org.junit.Test import org.ktorm.BaseTest -import org.ktorm.entity.filter -import org.ktorm.entity.first -import org.ktorm.entity.forUpdate -import org.ktorm.entity.sequenceOf import org.ktorm.expression.ScalarExpression -import java.util.concurrent.ExecutionException -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException /** * Created by vince on Dec 07, 2018. diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt index 9c0d018a..e485f2bf 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.ktorm.support.mysql import org.ktorm.dsl.Query diff --git a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt index c55853c9..ab959bfa 100644 --- a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt +++ b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt @@ -58,7 +58,7 @@ public open class OracleFormatter( // forUpdate() function in the core lib was removed, uncomment the following lines // when we add this feature back in the Oracle dialect. // if (expr is SelectExpression && expr.forUpdate) { - // throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") + // throw DialectFeatureNotSupportedException("Locking is not supported when using offset/limit params.") // } val offset = expr.offset ?: 0 diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt index 8538d563..023678db 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.ktorm.support.postgresql import org.ktorm.dsl.Query diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 2ba8bd0f..d9bc2068 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -208,15 +208,6 @@ public open class PostgreSqlFormatter( } } - if (expr.returningColumns.isNotEmpty()) { - writeKeyword(" returning ") - expr.returningColumns.forEachIndexed { i, column -> - if (i > 0) write(", ") - checkColumnName(column.name) - write(column.name.quoted) - } - } - return expr } @@ -255,15 +246,6 @@ public open class PostgreSqlFormatter( } } - if (expr.returningColumns.isNotEmpty()) { - writeKeyword(" returning ") - expr.returningColumns.forEachIndexed { i, column -> - if (i > 0) write(", ") - checkColumnName(column.name) - write(column.name.quoted) - } - } - return expr } } diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt index 2a118a27..cfe4e4da 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt @@ -52,7 +52,7 @@ public open class SqlServerFormatter( // forUpdate() function in the core lib was removed, uncomment the following lines // when we add this feature back in the SqlServer dialect. // if (expr is SelectExpression && expr.forUpdate) { - // throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") + // throw DialectFeatureNotSupportedException("Locking is not supported when using offset/limit params.") // } if (expr.orderBy.isEmpty()) { From cc65de46a6c185265bfd80377ac783d5e32d500d Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 14:05:52 +0800 Subject: [PATCH 39/96] use postgresql table alias for locking --- .../src/main/kotlin/org/ktorm/support/postgresql/Lock.kt | 7 ++++--- .../org/ktorm/support/postgresql/PostgreSqlDialect.kt | 7 ++++++- .../kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt index 023678db..246d01ab 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/Lock.kt @@ -19,6 +19,7 @@ package org.ktorm.support.postgresql import org.ktorm.dsl.Query import org.ktorm.entity.EntitySequence import org.ktorm.expression.SelectExpression +import org.ktorm.expression.TableExpression import org.ktorm.expression.UnionExpression import org.ktorm.schema.BaseTable import org.ktorm.support.postgresql.LockingMode.* @@ -54,7 +55,7 @@ public enum class LockingWait { */ public data class LockingClause( val mode: LockingMode, - val tables: List, + val tables: List, val wait: LockingWait ) @@ -71,7 +72,7 @@ public data class LockingClause( public fun Query.locking( mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT ): Query { - val locking = LockingClause(mode, tables.map { it.tableName }, wait) + val locking = LockingClause(mode, tables.map { it.asExpression() }, wait) val expr = when (val e = this.expression) { is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("locking", locking)) @@ -94,7 +95,7 @@ public fun Query.locking( public fun > EntitySequence.locking( mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT ): EntitySequence { - val locking = LockingClause(mode, tables.map { it.tableName }, wait) + val locking = LockingClause(mode, tables.map { it.asExpression() }, wait) return this.withExpression( expression.copy(extraProperties = expression.extraProperties + Pair("locking", locking)) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index d9bc2068..c8f58fda 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -105,7 +105,12 @@ public open class PostgreSqlFormatter( removeLastBlank() write(", ") } - write("${table.quoted} ") + + if (table.tableAlias != null && table.tableAlias!!.isNotBlank()) { + write("${table.tableAlias!!.quoted} ") + } else { + write("${table.name.quoted} ") + } } } diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index a3293855..63a9e9a6 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -646,10 +646,12 @@ class PostgreSqlTest : BaseTest() { @Test fun testSelectForUpdate() { database.useTransaction { + val emp = Employees.aliased("emp") + val employee = database - .sequenceOf(Employees, withReferences = false) + .sequenceOf(emp, withReferences = false) .filter { it.id eq 1 } - .locking(LockingMode.FOR_UPDATE, wait = LockingWait.SKIP_LOCKED) + .locking(LockingMode.FOR_UPDATE, tables = listOf(emp), wait = LockingWait.SKIP_LOCKED) .first() val future = Executors.newSingleThreadExecutor().submit { From 57cb9f105a92940619663849eb8736466c22a1be Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 16:07:51 +0800 Subject: [PATCH 40/96] use mysql table alias for locking --- .../src/main/kotlin/org/ktorm/support/mysql/Lock.kt | 7 ++++--- .../main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt | 7 ++++++- .../src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt index e485f2bf..8edc356e 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/Lock.kt @@ -19,6 +19,7 @@ package org.ktorm.support.mysql import org.ktorm.dsl.Query import org.ktorm.entity.EntitySequence import org.ktorm.expression.SelectExpression +import org.ktorm.expression.TableExpression import org.ktorm.expression.UnionExpression import org.ktorm.schema.BaseTable import org.ktorm.support.mysql.LockingMode.* @@ -53,7 +54,7 @@ public enum class LockingWait { */ public data class LockingClause( val mode: LockingMode, - val tables: List, + val tables: List, val wait: LockingWait ) @@ -70,7 +71,7 @@ public data class LockingClause( public fun Query.locking( mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT ): Query { - val locking = LockingClause(mode, tables.map { it.tableName }, wait) + val locking = LockingClause(mode, tables.map { it.asExpression() }, wait) val expr = when (val e = this.expression) { is SelectExpression -> e.copy(extraProperties = e.extraProperties + Pair("locking", locking)) @@ -93,7 +94,7 @@ public fun Query.locking( public fun > EntitySequence.locking( mode: LockingMode, tables: List> = emptyList(), wait: LockingWait = WAIT ): EntitySequence { - val locking = LockingClause(mode, tables.map { it.tableName }, wait) + val locking = LockingClause(mode, tables.map { it.asExpression() }, wait) return this.withExpression( expression.copy(extraProperties = expression.extraProperties + Pair("locking", locking)) diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index f96dd33f..6baf51af 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -86,7 +86,12 @@ public open class MySqlFormatter( removeLastBlank() write(", ") } - write("${table.quoted} ") + + if (table.tableAlias != null && table.tableAlias!!.isNotBlank()) { + write("${table.tableAlias!!.quoted} ") + } else { + write("${table.name.quoted} ") + } } } diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 55da194d..ee2d94e3 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -419,10 +419,12 @@ class MySqlTest : BaseTest() { @Test fun testSelectForUpdate() { database.useTransaction { + val emp = Employees.aliased("emp") + val employee = database - .sequenceOf(Employees, withReferences = false) + .sequenceOf(emp, withReferences = false) .filter { it.id eq 1 } - .locking(LockingMode.FOR_UPDATE, wait = LockingWait.SKIP_LOCKED) + .locking(LockingMode.FOR_UPDATE, tables = listOf(emp), wait = LockingWait.SKIP_LOCKED) .first() val future = Executors.newSingleThreadExecutor().submit { From a440ecb72cd9bfc29b6b0eaa16ab0bc7dacdf4bb Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 20:18:01 +0800 Subject: [PATCH 41/96] deprecate forUpdate() --- .../src/main/kotlin/org/ktorm/dsl/Query.kt | 15 ++++++++ .../kotlin/org/ktorm/entity/EntitySequence.kt | 10 ++++++ .../org/ktorm/expression/SqlExpressions.kt | 3 ++ .../org/ktorm/expression/SqlFormatter.kt | 4 +++ .../test/kotlin/org/ktorm/dsl/QueryTest.kt | 35 +++++++++++++++++++ 5 files changed, 67 insertions(+) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt index 7b0bbb3b..05dfa037 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt @@ -761,3 +761,18 @@ public fun Query.joinToString( ): String { return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() } + +/** + * Indicate that this query should acquire the record-lock, the generated SQL would be `select ... for update`. + * + * @since 3.1.0 + */ +@Deprecated("Will remove in the future, locking clause should be implemented in dialects respectively.") +public fun Query.forUpdate(): Query { + val expr = when (expression) { + is SelectExpression -> expression.copy(forUpdate = true) + is UnionExpression -> throw IllegalStateException("SELECT FOR UPDATE is not supported in a union expression.") + } + + return this.withExpression(expr) +} diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index eccade8d..d26a4549 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -1503,3 +1503,13 @@ public fun EntitySequence.joinToString( ): String { return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() } + +/** + * Indicate that this query should aquire the record-lock, the generated SQL would be `select ... for update`. + * + * @since 3.1.0 + */ +@Deprecated("Will remove in the future, locking clause should be implemented in dialects respectively.") +public fun > EntitySequence.forUpdate(): EntitySequence { + return this.withExpression(expression.copy(forUpdate = true)) +} diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index 8b933fc0..2f6f8b5a 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -115,6 +115,7 @@ public sealed class QueryExpression : QuerySourceExpression() { * @property groupBy the grouping conditions, represents the `group by` clause of SQL. * @property having the having condition, represents the `having` clause of SQL. * @property isDistinct mark if this query is distinct, true means the SQL is `select distinct ...`. + * @property forUpdate mark if this query should acquire the record-lock, true means the SQL is `select ... for update`. */ public data class SelectExpression( val columns: List> = emptyList(), @@ -123,6 +124,8 @@ public data class SelectExpression( val groupBy: List> = emptyList(), val having: ScalarExpression? = null, val isDistinct: Boolean = false, + @Deprecated("Will remove in the future, locking clause should be implemented in dialects respectively.") + val forUpdate: Boolean = false, override val orderBy: List = emptyList(), override val offset: Int? = null, override val limit: Int? = null, diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 8b797cc8..a101af8c 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -388,6 +388,10 @@ public abstract class SqlFormatter( if (expr.offset != null || expr.limit != null) { writePagination(expr) } + @Suppress("DEPRECATION") + if (expr.forUpdate) { + writeKeyword("for update ") + } return expr } diff --git a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt index 6042bd08..af0f50a3 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt @@ -2,7 +2,15 @@ package org.ktorm.dsl import org.junit.Test import org.ktorm.BaseTest +import org.ktorm.entity.filter +import org.ktorm.entity.first +import org.ktorm.entity.forUpdate +import org.ktorm.entity.sequenceOf import org.ktorm.expression.ScalarExpression +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException /** * Created by vince on Dec 07, 2018. @@ -251,6 +259,33 @@ class QueryTest : BaseTest() { println(query.sql) } + @Test + fun testSelectForUpdate() { + database.useTransaction { + val employee = database + .sequenceOf(Employees, withReferences = false) + .filter { it.id eq 1 } + .forUpdate() + .first() + + val future = Executors.newSingleThreadExecutor().submit { + employee.name = "vince" + employee.flushChanges() + } + + try { + future.get(5, TimeUnit.SECONDS) + throw AssertionError() + } catch (e: ExecutionException) { + // Expected, the record is locked. + e.printStackTrace() + } catch (e: TimeoutException) { + // Expected, the record is locked. + e.printStackTrace() + } + } + } + @Test fun testFlatMap() { val names = database From a09c5e03fbbf776be1342d1de26cd91c50cff096 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 21:00:32 +0800 Subject: [PATCH 42/96] fix typo --- ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index d26a4549..3c6f0fe3 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -1505,7 +1505,7 @@ public fun EntitySequence.joinToString( } /** - * Indicate that this query should aquire the record-lock, the generated SQL would be `select ... for update`. + * Indicate that this query should acquire the record-lock, the generated SQL would be `select ... for update`. * * @since 3.1.0 */ From 7f65653b1f49131a0a5a54eec0aa8a3f35a3e009 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 23:04:18 +0800 Subject: [PATCH 43/96] entity equals & hashcode --- .../main/kotlin/org/ktorm/entity/EntityExtensions.kt | 3 ++- .../kotlin/org/ktorm/entity/EntityImplementation.kt | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt index 08edd5f9..d78469ec 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt @@ -17,6 +17,7 @@ package org.ktorm.entity import org.ktorm.schema.* +import java.lang.reflect.Proxy import java.util.* import kotlin.reflect.jvm.jvmErasure @@ -139,5 +140,5 @@ internal fun EntityImplementation.isPrimaryKey(name: String): Boolean { } internal val Entity<*>.implementation: EntityImplementation get() { - return java.lang.reflect.Proxy.getInvocationHandler(this) as EntityImplementation + return Proxy.getInvocationHandler(this) as EntityImplementation } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index a12ce099..c789c891 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -219,15 +219,20 @@ internal class EntityImplementation( } override fun equals(other: Any?): Boolean { + if (this === other) return true + return when (other) { - is EntityImplementation -> values == other.values && entityClass == other.entityClass - is Entity<*> -> values == other.implementation.values && entityClass == other.implementation.entityClass + is EntityImplementation -> entityClass == other.entityClass && values == other.values + is Entity<*> -> entityClass == other.implementation.entityClass && values == other.implementation.values else -> false } } override fun hashCode(): Int { - return values.hashCode() + 13 * entityClass.hashCode() + var result = 1 + result = 31 * result + entityClass.hashCode() + result = 31 * result + values.hashCode() + return result } override fun toString(): String { From d35501b118279ba34fb47d8c2bedf3b66e7ba530 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 26 Apr 2021 23:49:22 +0800 Subject: [PATCH 44/96] update code style --- .../org/ktorm/support/postgresql/BulkInsert.kt | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 7a816e7e..31319c3c 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -107,7 +107,7 @@ public fun > Database.bulkInsert( if (builder.assignments.isEmpty()) return 0 - val execute: (List>>) -> Unit = { assignments -> + executeQueryInBatches(builder) { assignments -> val expression = BulkInsertExpression( table = table.asExpression(), assignments = assignments @@ -118,8 +118,6 @@ public fun > Database.bulkInsert( affectedTotal += total } - executeQueryInBatches(builder, execute) - return affectedTotal } @@ -180,7 +178,7 @@ public fun > Database.bulkInsertOrUpdate( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } @@ -190,7 +188,7 @@ public fun > Database.bulkInsertOrUpdate( throw IllegalStateException(msg) } - val execute: (List>>) -> Unit = { assignments -> + executeQueryInBatches(builder) { assignments -> val expression = BulkInsertExpression( table = table.asExpression(), assignments = assignments, @@ -203,8 +201,6 @@ public fun > Database.bulkInsertOrUpdate( affectedTotal += total } - executeQueryInBatches(builder, execute) - return affectedTotal } @@ -443,7 +439,7 @@ private fun > Database.bulkInsertReturningAux( if (builder.assignments.isEmpty()) return Pair(0, CompositeCachedRowSet()) - val execute: (List>>) -> Unit = { assignments -> + executeQueryInBatches(builder) { assignments -> val expression = BulkInsertExpression( table.asExpression(), assignments, @@ -456,8 +452,6 @@ private fun > Database.bulkInsertReturningAux( cachedRowSets.add(rows) } - executeQueryInBatches(builder, execute) - return Pair(affectedTotal, cachedRowSets) } @@ -681,7 +675,7 @@ private fun > Database.bulkInsertOrUpdateReturningAux( throw IllegalStateException(msg) } - val execute: (List>>) -> Unit = { assignments -> + executeQueryInBatches(builder) { assignments -> val expression = BulkInsertExpression( table = table.asExpression(), assignments = assignments, @@ -696,8 +690,6 @@ private fun > Database.bulkInsertOrUpdateReturningAux( cachedRowSets.add(rows) } - executeQueryInBatches(builder, execute) - return Pair(affectedTotal, cachedRowSets) } From f03ab83b138c19abc4c3700893000dc4e21b32d3 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Tue, 27 Apr 2021 00:12:32 +0800 Subject: [PATCH 45/96] rm bulk chunking --- .../ktorm/support/postgresql/BulkInsert.kt | 139 ++++-------------- .../postgresql/CompositeCachedRowSet.kt | 71 --------- 2 files changed, 30 insertions(+), 180 deletions(-) delete mode 100644 ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 31319c3c..d6e9111b 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -16,7 +16,9 @@ package org.ktorm.support.postgresql +import org.ktorm.database.CachedRowSet import org.ktorm.database.Database +import org.ktorm.database.asIterable import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl import org.ktorm.dsl.batchInsert @@ -26,14 +28,6 @@ import org.ktorm.expression.SqlExpression import org.ktorm.expression.TableExpression import org.ktorm.schema.BaseTable import org.ktorm.schema.Column -import java.util.* -import kotlin.collections.ArrayList - -// We leave some prepared statement parameters reserved for the query dialect building process -private const val RESERVED_SQL_EXPR_BATCH_SIZE = 100 - -// Max number of assignments we allow per batch in Postgresql (Max size as defined by Postgresql - reserved) -private const val MAX_SQL_EXPR_BATCH_SIZE = Short.MAX_VALUE - RESERVED_SQL_EXPR_BATCH_SIZE /** * Bulk insert expression, represents a bulk insert statement in PostgreSQL. @@ -101,24 +95,14 @@ public data class BulkInsertExpression( public fun > Database.bulkInsert( table: T, block: BulkInsertStatementBuilder.(T) -> Unit ): Int { - var affectedTotal = 0 - val builder = BulkInsertStatementBuilder(table).apply { block(table) } - if (builder.assignments.isEmpty()) return 0 - - executeQueryInBatches(builder) { assignments -> - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = assignments - ) - - val total = executeUpdate(expression) - - affectedTotal += total - } + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = builder.assignments + ) - return affectedTotal + return executeUpdate(expression) } /** @@ -168,12 +152,8 @@ public fun > Database.bulkInsert( public fun > Database.bulkInsertOrUpdate( table: T, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit ): Int { - var affectedTotal = 0 - val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } - if (builder.assignments.isEmpty()) return 0 - val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } if (conflictColumns.isEmpty()) { val msg = @@ -188,20 +168,15 @@ public fun > Database.bulkInsertOrUpdate( throw IllegalStateException(msg) } - executeQueryInBatches(builder) { assignments -> - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = assignments, - conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, - ) - - val total = executeUpdate(expression) - affectedTotal += total - } + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = builder.assignments, + conflictColumns = conflictColumns.map { it.asExpression() }, + updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + ) - return affectedTotal + return executeUpdate(expression) } /** @@ -431,28 +406,16 @@ private fun > Database.bulkInsertReturningAux( table: T, returningColumns: List>, block: BulkInsertStatementBuilder.(T) -> Unit -): Pair { - var affectedTotal = 0 - val cachedRowSets = CompositeCachedRowSet() - +): Pair { val builder = BulkInsertStatementBuilder(table).apply { block(table) } - if (builder.assignments.isEmpty()) return Pair(0, CompositeCachedRowSet()) - - executeQueryInBatches(builder) { assignments -> - val expression = BulkInsertExpression( - table.asExpression(), - assignments, - returningColumns = returningColumns.map { it.asExpression() } - ) - - val (total, rows) = executeUpdateAndRetrieveKeys(expression) - - affectedTotal += total - cachedRowSets.add(rows) - } + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = builder.assignments, + returningColumns = returningColumns.map { it.asExpression() } + ) - return Pair(affectedTotal, cachedRowSets) + return executeUpdateAndRetrieveKeys(expression) } /** @@ -653,14 +616,9 @@ private fun > Database.bulkInsertOrUpdateReturningAux( table: T, returningColumns: List>, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit -): Pair { - var affectedTotal = 0 - val cachedRowSets = CompositeCachedRowSet() - +): Pair { val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } - if (builder.assignments.isEmpty()) return Pair(0, CompositeCachedRowSet()) - val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } if (conflictColumns.isEmpty()) { val msg = @@ -675,50 +633,13 @@ private fun > Database.bulkInsertOrUpdateReturningAux( throw IllegalStateException(msg) } - executeQueryInBatches(builder) { assignments -> - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = assignments, - conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, - returningColumns = returningColumns.map { it.asExpression() } - ) - - val (total, rows) = executeUpdateAndRetrieveKeys(expression) - - affectedTotal += total - cachedRowSets.add(rows) - } - - return Pair(affectedTotal, cachedRowSets) -} + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = builder.assignments, + conflictColumns = conflictColumns.map { it.asExpression() }, + updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + returningColumns = returningColumns.map { it.asExpression() } + ) -private fun > executeQueryInBatches( - builder: BulkInsertStatementBuilder, - execute: (List>>) -> Unit -) { - var batchAssignmentCount = 0 - val currentBatch = LinkedList>>() - builder.assignments.forEach { assignments -> - assignments.size.let { size -> - if (size > MAX_SQL_EXPR_BATCH_SIZE) { - throw IllegalArgumentException( - "The maximum number of assignments per item is $MAX_SQL_EXPR_BATCH_SIZE, but $size detected!" - ) - } - - if (batchAssignmentCount + size >= MAX_SQL_EXPR_BATCH_SIZE) { - execute(currentBatch) - currentBatch.clear() - batchAssignmentCount = 0 - } - - currentBatch.add(assignments) - batchAssignmentCount += size - } - } - // Flush the remaining - if (currentBatch.isNotEmpty()) { - execute(currentBatch) - } + return executeUpdateAndRetrieveKeys(expression) } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt deleted file mode 100644 index 77eeb4b8..00000000 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/CompositeCachedRowSet.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ktorm.support.postgresql - -import org.ktorm.database.CachedRowSet -import java.util.* - -/** - * Utility class that stores the resulting CachedRowSet from - * multiple queries, but abstract their iteration as if they were - * a single CachedRowSet. - */ -public class CompositeCachedRowSet { - private val resultSets = LinkedList() - - /** - * Adds a CachedRowSet to the composite group. - * @param rs the new CachedRowSet - */ - public fun add(rs: CachedRowSet) { - resultSets.add(rs) - } - - /** - * Returns the iterator for this composite. - */ - @Suppress("IteratorHasNextCallsNextMethod") - public operator fun iterator(): Iterator = object : Iterator { - private var cursor = 0 - private var hasNext: Boolean? = null - - override fun hasNext(): Boolean { - val hasNext = (cursor < resultSets.size && resultSets[cursor].next()).also { hasNext = it } - - if (!hasNext) { - return ++cursor < resultSets.size && hasNext() - } - - return hasNext - } - - override fun next(): CachedRowSet { - return if (hasNext ?: hasNext()) { - resultSets[cursor].also { hasNext = null } - } else { - throw NoSuchElementException() - } - } - } - - /** - * Returns the iterator for this composite. - */ - public fun asIterable(): Iterable { - return Iterable { iterator() } - } -} From d435f72b8c154bbcbeec6e6e6b7f23dae687a2aa Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Tue, 27 Apr 2021 09:59:55 +0800 Subject: [PATCH 46/96] refactor insert or update --- .../support/postgresql/InsertOrUpdate.kt | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 29fd7ab0..ef5c4142 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -34,14 +34,17 @@ import org.ktorm.schema.Column * * @property table the table to be inserted. * @property assignments the inserted column assignments. - * @property conflictColumns the index columns on which the conflict may happens. + * @property conflictColumns the index columns on which the conflict may happen. * @property updateAssignments the updated column assignments while any key conflict exists. + * @property doNothing whether we should ignore errors and do nothing when conflict happens. + * @property returningColumns the returning columns. */ public data class InsertOrUpdateExpression( val table: TableExpression, val assignments: List>, - val conflictColumns: List>? = null, + val conflictColumns: List> = emptyList(), val updateAssignments: List> = emptyList(), + val doNothing: Boolean = false, val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() @@ -92,10 +95,10 @@ public fun > Database.insertOrUpdate( throw IllegalStateException(msg) } - if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { + if (!builder.doNothing && builder.updateAssignments.isEmpty()) { val msg = - "You cannot leave a on-conflict clause empty! If you desire no update action at all " + - "you must explicitly invoke `doNothing()`" + "Cannot leave the onConflict clause empty! " + + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) } @@ -103,7 +106,8 @@ public fun > Database.insertOrUpdate( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + updateAssignments = builder.updateAssignments, + doNothing = builder.doNothing ) return executeUpdate(expression) @@ -126,10 +130,9 @@ public open class PostgreSqlAssignmentsBuilder : AssignmentsBuilder() { */ @KtormDsl public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { - internal val updateAssignments = ArrayList>() internal val conflictColumns = ArrayList>() - - internal var explicitlyDoNothing: Boolean = false + internal val updateAssignments = ArrayList>() + internal var doNothing = false /** * Specify the update assignments while any key conflict exists. @@ -147,29 +150,24 @@ public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { */ public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) - - explicitlyDoNothing = builder.explicitlyDoNothing - - updateAssignments += builder.assignments - - conflictColumns += columns + this.conflictColumns += columns + this.updateAssignments += builder.assignments + this.doNothing = builder.doNothing } } /** -<<<<<<< HEAD -======= * DSL builder for insert or update on conflict clause. */ @KtormDsl public class InsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBuilder() { - internal var explicitlyDoNothing: Boolean = false + internal var doNothing = false /** * Explicitly tells ktorm to ignore any on-conflict errors and continue insertion. */ public fun doNothing() { - this.explicitlyDoNothing = true + this.doNothing = true } /** @@ -186,7 +184,6 @@ public class InsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBuilde } /** ->>>>>>> 0ee0f38d1fb1e28dbfa4832badc4f92ef8ad2802 * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically * performs an update if any conflict exists. * @@ -359,7 +356,7 @@ private fun > Database.insertOrUpdateReturningAux( throw IllegalStateException(msg) } - if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { + if (!builder.doNothing && builder.updateAssignments.isEmpty()) { val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all " + "you must explicitly invoke `doNothing()`" @@ -370,7 +367,7 @@ private fun > Database.insertOrUpdateReturningAux( table = table.asExpression(), assignments = builder.assignments, conflictColumns = builder.conflictColumns.ifEmpty { primaryKeys }.map { it.asExpression() }, - updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + updateAssignments = if (builder.doNothing) emptyList() else builder.updateAssignments, returningColumns = returningColumns.map { it.asExpression() } ) From 971dbe071c73b559650752165c926bc940fd4c1e Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Tue, 27 Apr 2021 10:11:04 +0800 Subject: [PATCH 47/96] refactor insert or update returning --- .../support/postgresql/InsertOrUpdate.kt | 144 +++++++++--------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index ef5c4142..aa431bf2 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -113,76 +113,6 @@ public fun > Database.insertOrUpdate( return executeUpdate(expression) } -/** - * Base class of PostgreSQL DSL builders, provide basic functions used to build assignments for insert or update DSL. - */ -@KtormDsl -public open class PostgreSqlAssignmentsBuilder : AssignmentsBuilder() { - - /** - * A getter that returns the readonly view of the built assignments list. - */ - internal val assignments: List> get() = _assignments -} - -/** - * DSL builder for insert or update statements. - */ -@KtormDsl -public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { - internal val conflictColumns = ArrayList>() - internal val updateAssignments = ArrayList>() - internal var doNothing = false - - /** - * Specify the update assignments while any key conflict exists. - */ - @Deprecated( - message = "This function will be removed in the future, please use onConflict { } instead", - replaceWith = ReplaceWith("onConflict(columns, block)") - ) - public fun onDuplicateKey(vararg columns: Column<*>, block: AssignmentsBuilder.() -> Unit) { - onConflict(*columns, block = block) - } - - /** - * Specify the update assignments while any key conflict exists. - */ - public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { - val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) - this.conflictColumns += columns - this.updateAssignments += builder.assignments - this.doNothing = builder.doNothing - } -} - -/** - * DSL builder for insert or update on conflict clause. - */ -@KtormDsl -public class InsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBuilder() { - internal var doNothing = false - - /** - * Explicitly tells ktorm to ignore any on-conflict errors and continue insertion. - */ - public fun doNothing() { - this.doNothing = true - } - - /** - * Reference the 'EXCLUDED' table in a ON CONFLICT clause. - */ - public fun excluded(column: Column): ColumnExpression { - // excluded.name - return ColumnExpression( - table = TableExpression(name = "excluded"), - name = column.name, - sqlType = column.sqlType - ) - } -} - /** * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically * performs an update if any conflict exists. @@ -352,14 +282,14 @@ private fun > Database.insertOrUpdateReturningAux( if (primaryKeys.isEmpty() && builder.conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onDuplicateKey(col) { .. }" + "you must specify the conflict columns when calling onDuplicateKey(col) { .. }" throw IllegalStateException(msg) } if (!builder.doNothing && builder.updateAssignments.isEmpty()) { val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all " + - "you must explicitly invoke `doNothing()`" + "you must explicitly invoke `doNothing()`" throw IllegalStateException(msg) } @@ -373,3 +303,73 @@ private fun > Database.insertOrUpdateReturningAux( return executeUpdateAndRetrieveKeys(expression) } + +/** + * Base class of PostgreSQL DSL builders, provide basic functions used to build assignments for insert or update DSL. + */ +@KtormDsl +public open class PostgreSqlAssignmentsBuilder : AssignmentsBuilder() { + + /** + * A getter that returns the readonly view of the built assignments list. + */ + internal val assignments: List> get() = _assignments +} + +/** + * DSL builder for insert or update statements. + */ +@KtormDsl +public class InsertOrUpdateStatementBuilder : PostgreSqlAssignmentsBuilder() { + internal val conflictColumns = ArrayList>() + internal val updateAssignments = ArrayList>() + internal var doNothing = false + + /** + * Specify the update assignments while any key conflict exists. + */ + @Deprecated( + message = "This function will be removed in the future, please use onConflict { } instead", + replaceWith = ReplaceWith("onConflict(columns, block)") + ) + public fun onDuplicateKey(vararg columns: Column<*>, block: AssignmentsBuilder.() -> Unit) { + onConflict(*columns, block = block) + } + + /** + * Specify the update assignments while any key conflict exists. + */ + public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { + val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) + this.conflictColumns += columns + this.updateAssignments += builder.assignments + this.doNothing = builder.doNothing + } +} + +/** + * DSL builder for insert or update on conflict clause. + */ +@KtormDsl +public class InsertOrUpdateOnConflictClauseBuilder : PostgreSqlAssignmentsBuilder() { + internal var doNothing = false + + /** + * Explicitly tells ktorm to ignore any on-conflict errors and continue insertion. + */ + public fun doNothing() { + this.doNothing = true + } + + /** + * Reference the 'EXCLUDED' table in a ON CONFLICT clause. + */ + public fun excluded(column: Column): ColumnExpression { + // excluded.name + return ColumnExpression( + table = TableExpression(name = "excluded"), + name = column.name, + sqlType = column.sqlType + ) + } +} From 9ad9f7365594d52a30da2f9f4a3dd73be0b82706 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Tue, 27 Apr 2021 18:20:20 +0800 Subject: [PATCH 48/96] refactor insert or update returning --- .../support/postgresql/InsertOrUpdate.kt | 167 +++++++----------- 1 file changed, 67 insertions(+), 100 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index aa431bf2..0b649849 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -16,9 +16,7 @@ package org.ktorm.support.postgresql -import org.ktorm.database.CachedRowSet import org.ktorm.database.Database -import org.ktorm.database.asIterable import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl import org.ktorm.expression.ColumnAssignmentExpression @@ -85,42 +83,18 @@ public data class InsertOrUpdateExpression( public fun > Database.insertOrUpdate( table: T, block: InsertOrUpdateStatementBuilder.(T) -> Unit ): Int { - val builder = InsertOrUpdateStatementBuilder().apply { block(table) } - - val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } - if (conflictColumns.isEmpty()) { - val msg = - "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" - throw IllegalStateException(msg) - } - - if (!builder.doNothing && builder.updateAssignments.isEmpty()) { - val msg = - "Cannot leave the onConflict clause empty! " + - "If you desire no update action at all please explicitly call `doNothing()`" - throw IllegalStateException(msg) - } - - val expression = InsertOrUpdateExpression( - table = table.asExpression(), - assignments = builder.assignments, - conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = builder.updateAssignments, - doNothing = builder.doNothing - ) - + val expression = buildInsertOrUpdateExpression(table, returning = emptyList(), block = block) return executeUpdate(expression) } /** - * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically - * performs an update if any conflict exists. + * Insert a record to the table, determining if there is a key conflict while it's being inserted, automatically + * performs an update if any conflict exists, and finally returns the specific column. * * Usage: * * ```kotlin - * database.insertOrUpdateReturning(Employees, Employees.id) { + * val id = database.insertOrUpdateReturning(Employees, Employees.id) { * set(it.id, 1) * set(it.name, "vince") * set(it.job, "engineer") @@ -143,34 +117,33 @@ public fun > Database.insertOrUpdate( * * @since 3.4.0 * @param table the table to be inserted. - * @param returningColumn the column to return + * @param returning the column to return * @param block the DSL block used to construct the expression. * @return the returning column value. */ -public fun , R : Any> Database.insertOrUpdateReturning( - table: T, - returningColumn: Column, - block: InsertOrUpdateStatementBuilder.(T) -> Unit -): R? { - val (_, rowSet) = this.insertOrUpdateReturningAux( - table, - listOfNotNull(returningColumn), - block - ) +public fun , C : Any> Database.insertOrUpdateReturning( + table: T, returning: Column, block: InsertOrUpdateStatementBuilder.(T) -> Unit +): C? { + val expression = buildInsertOrUpdateExpression(table, listOf(returning), block) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - return rowSet.asIterable().map { row -> - returningColumn.sqlType.getResult(row, 1) - }.first() + if (rowSet.size() == 1) { + check(rowSet.next()) + return returning.sqlType.getResult(rowSet, 1) + } else { + val (sql, _) = formatExpression(expression, beautifySql = true) + throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") + } } /** - * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically - * performs an update if any conflict exists. + * Insert a record to the table, determining if there is a key conflict while it's being inserted, automatically + * performs an update if any conflict exists, and finally returns the specific columns. * * Usage: * * ```kotlin - * database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { + * val (id, job) = database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { * set(it.id, 1) * set(it.name, "vince") * set(it.job, "engineer") @@ -193,36 +166,34 @@ public fun , R : Any> Database.insertOrUpdateReturning( * * @since 3.4.0 * @param table the table to be inserted. - * @param returningColumns the columns to return + * @param returning the columns to return * @param block the DSL block used to construct the expression. * @return the returning columns' values. */ -public fun , R1 : Any, R2 : Any> Database.insertOrUpdateReturning( - table: T, - returningColumns: Pair, Column>, - block: InsertOrUpdateStatementBuilder.(T) -> Unit -): Pair { - val (_, rowSet) = this.insertOrUpdateReturningAux( - table, - returningColumns.toList(), - block - ) +public fun , C1 : Any, C2 : Any> Database.insertOrUpdateReturning( + table: T, returning: Pair, Column>, block: InsertOrUpdateStatementBuilder.(T) -> Unit +): Pair { + val (c1, c2) = returning + val expression = buildInsertOrUpdateExpression(table, listOf(c1, c2), block) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - return rowSet.asIterable().map { row -> - Pair( - returningColumns.first.sqlType.getResult(row, 1), - returningColumns.second.sqlType.getResult(row, 2) - ) - }.first() + if (rowSet.size() == 1) { + check(rowSet.next()) + return Pair(c1.sqlType.getResult(rowSet, 1), c2.sqlType.getResult(rowSet, 2)) + } else { + val (sql, _) = formatExpression(expression, beautifySql = true) + throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") + } } /** - * Insert a record to the table, determining if there is a key conflict while it's being inserted, and automatically - * performs an update if any conflict exists. + * Insert a record to the table, determining if there is a key conflict while it's being inserted, automatically + * performs an update if any conflict exists, and finally returns the specific columns. * * Usage: * * ```kotlin + * val (id, job, salary) = * database.insertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { * set(it.id, 1) * set(it.name, "vince") @@ -246,62 +217,58 @@ public fun , R1 : Any, R2 : Any> Database.insertOrUpdateReturni * * @since 3.4.0 * @param table the table to be inserted. - * @param returningColumns the columns to return + * @param returning the columns to return * @param block the DSL block used to construct the expression. * @return the returning columns' values. */ -public fun , R1 : Any, R2 : Any, R3 : Any> Database.insertOrUpdateReturning( - table: T, - returningColumns: Triple, Column, Column>, - block: InsertOrUpdateStatementBuilder.(T) -> Unit -): Triple { - val (_, rowSet) = this.insertOrUpdateReturningAux( - table, - returningColumns.toList(), - block - ) +public fun , C1 : Any, C2 : Any, C3 : Any> Database.insertOrUpdateReturning( + table: T, returning: Triple, Column, Column>, block: InsertOrUpdateStatementBuilder.(T) -> Unit +): Triple { + val (c1, c2, c3) = returning + val expression = buildInsertOrUpdateExpression(table, listOf(c1, c2, c3), block) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - return rowSet.asIterable().map { row -> - var i = 0 - Triple( - returningColumns.first.sqlType.getResult(row, ++i), - returningColumns.second.sqlType.getResult(row, ++i), - returningColumns.third.sqlType.getResult(row, ++i) + if (rowSet.size() == 1) { + check(rowSet.next()) + return Triple( + c1.sqlType.getResult(rowSet, 1), + c2.sqlType.getResult(rowSet, 2), + c3.sqlType.getResult(rowSet, 3) ) - }.first() + } else { + val (sql, _) = formatExpression(expression, beautifySql = true) + throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") + } } -private fun > Database.insertOrUpdateReturningAux( - table: T, - returningColumns: List>, - block: InsertOrUpdateStatementBuilder.(T) -> Unit -): Pair { +private fun > buildInsertOrUpdateExpression( + table: T, returning: List>, block: InsertOrUpdateStatementBuilder.(T) -> Unit +): InsertOrUpdateExpression { val builder = InsertOrUpdateStatementBuilder().apply { block(table) } - val primaryKeys = table.primaryKeys - if (primaryKeys.isEmpty() && builder.conflictColumns.isEmpty()) { + val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } + if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onDuplicateKey(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } if (!builder.doNothing && builder.updateAssignments.isEmpty()) { val msg = - "You cannot leave a on-conflict clause empty! If you desire no update action at all " + - "you must explicitly invoke `doNothing()`" + "Cannot leave the onConflict clause empty! " + + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) } - val expression = InsertOrUpdateExpression( + return InsertOrUpdateExpression( table = table.asExpression(), assignments = builder.assignments, - conflictColumns = builder.conflictColumns.ifEmpty { primaryKeys }.map { it.asExpression() }, - updateAssignments = if (builder.doNothing) emptyList() else builder.updateAssignments, - returningColumns = returningColumns.map { it.asExpression() } + conflictColumns = conflictColumns.map { it.asExpression() }, + updateAssignments = builder.updateAssignments, + doNothing = builder.doNothing, + returningColumns = returning.map { it.asExpression() } ) - - return executeUpdateAndRetrieveKeys(expression) } /** From e098bcd1f61dec1fe810f147b82898373c292b1f Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Tue, 27 Apr 2021 20:27:18 +0800 Subject: [PATCH 49/96] insert returning --- .../support/postgresql/InsertOrUpdate.kt | 169 +++++++++++++++++- 1 file changed, 166 insertions(+), 3 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 0b649849..ae1c503c 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -101,7 +101,7 @@ public fun > Database.insertOrUpdate( * set(it.salary, 1000) * set(it.hireDate, LocalDate.now()) * set(it.departmentId, 1) - * onDuplicateKey { + * onConflict { * set(it.salary, it.salary + 900) * } * } @@ -150,7 +150,7 @@ public fun , C : Any> Database.insertOrUpdateReturning( * set(it.salary, 1000) * set(it.hireDate, LocalDate.now()) * set(it.departmentId, 1) - * onDuplicateKey { + * onConflict { * set(it.salary, it.salary + 900) * } * } @@ -201,7 +201,7 @@ public fun , C1 : Any, C2 : Any> Database.insertOrUpdateReturni * set(it.salary, 1000) * set(it.hireDate, LocalDate.now()) * set(it.departmentId, 1) - * onDuplicateKey { + * onConflict { * set(it.salary, it.salary + 900) * } * } @@ -241,6 +241,9 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.insertOrUpd } } +/** + * Build an insert or update expression. + */ private fun > buildInsertOrUpdateExpression( table: T, returning: List>, block: InsertOrUpdateStatementBuilder.(T) -> Unit ): InsertOrUpdateExpression { @@ -271,6 +274,166 @@ private fun > buildInsertOrUpdateExpression( ) } +/** + * Insert a record to the table and return the specific column. + * + * Usage: + * + * ```kotlin + * val id = database.insertReturning(Employees, Employees.id) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * returning id + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returning the column to return + * @param block the DSL block used to construct the expression. + * @return the returning column's value. + */ +public fun , C : Any> Database.insertReturning( + table: T, returning: Column, block: AssignmentsBuilder.(T) -> Unit +): C? { + val builder = PostgreSqlAssignmentsBuilder().apply { block(table) } + + val expression = InsertOrUpdateExpression( + table = table.asExpression(), + assignments = builder.assignments, + returningColumns = listOf(returning.asExpression()) + ) + + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + + if (rowSet.size() == 1) { + check(rowSet.next()) + return returning.sqlType.getResult(rowSet, 1) + } else { + val (sql, _) = formatExpression(expression, beautifySql = true) + throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") + } +} + +/** + * Insert a record to the table and return the specific columns. + * + * Usage: + * + * ```kotlin + * val (id, job) = database.insertReturning(Employees, Pair(Employees.id, Employees.job)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * returning id, job + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returning the columns to return + * @param block the DSL block used to construct the expression. + * @return the returning columns' values. + */ +public fun , C1 : Any, C2 : Any> Database.insertReturning( + table: T, returning: Pair, Column>, block: AssignmentsBuilder.(T) -> Unit +): Pair { + val (c1, c2) = returning + val builder = PostgreSqlAssignmentsBuilder().apply { block(table) } + + val expression = InsertOrUpdateExpression( + table = table.asExpression(), + assignments = builder.assignments, + returningColumns = listOf(c1, c2).map { it.asExpression() } + ) + + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + + if (rowSet.size() == 1) { + check(rowSet.next()) + return Pair(c1.sqlType.getResult(rowSet, 1), c2.sqlType.getResult(rowSet, 2)) + } else { + val (sql, _) = formatExpression(expression, beautifySql = true) + throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") + } +} + +/** + * Insert a record to the table and return the specific columns. + * + * Usage: + * + * ```kotlin + * val (id, job, salary) = + * database.insertReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * returning id, job, salary + * ``` + * + * @since 3.4.0 + * @param table the table to be inserted. + * @param returning the columns to return + * @param block the DSL block used to construct the expression. + * @return the returning columns' values. + */ +public fun , C1 : Any, C2 : Any, C3 : Any> Database.insertReturning( + table: T, returning: Triple, Column, Column>, block: AssignmentsBuilder.(T) -> Unit +): Triple { + val (c1, c2, c3) = returning + val builder = PostgreSqlAssignmentsBuilder().apply { block(table) } + + val expression = InsertOrUpdateExpression( + table = table.asExpression(), + assignments = builder.assignments, + returningColumns = listOf(c1, c2, c3).map { it.asExpression() } + ) + + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + + if (rowSet.size() == 1) { + check(rowSet.next()) + return Triple( + c1.sqlType.getResult(rowSet, 1), + c2.sqlType.getResult(rowSet, 2), + c3.sqlType.getResult(rowSet, 3) + ) + } else { + val (sql, _) = formatExpression(expression, beautifySql = true) + throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") + } +} + /** * Base class of PostgreSQL DSL builders, provide basic functions used to build assignments for insert or update DSL. */ From d30975fe0af764c66d44e3c98cc50910bc5fee04 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Tue, 27 Apr 2021 22:24:41 +0800 Subject: [PATCH 50/96] fix typo --- .../main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index ae1c503c..5bea5a6e 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -119,7 +119,7 @@ public fun > Database.insertOrUpdate( * @param table the table to be inserted. * @param returning the column to return * @param block the DSL block used to construct the expression. - * @return the returning column value. + * @return the returning column's value. */ public fun , C : Any> Database.insertOrUpdateReturning( table: T, returning: Column, block: InsertOrUpdateStatementBuilder.(T) -> Unit From bda88aa3b69daa8f5089c46ce4674dac58ef9d88 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Tue, 27 Apr 2021 23:44:40 +0800 Subject: [PATCH 51/96] refactor bulk insert --- .../ktorm/support/postgresql/BulkInsert.kt | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index d6e9111b..7c22918a 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -41,14 +41,17 @@ import org.ktorm.schema.Column * * @property table the table to be inserted. * @property assignments column assignments of the bulk insert statement. - * @property conflictColumns the index columns on which the conflict may happens. + * @property conflictColumns the index columns on which the conflict may happen. * @property updateAssignments the updated column assignments while key conflict exists. + * @property doNothing whether we should ignore errors and do nothing when conflict happens. + * @property returningColumns the returning columns. */ public data class BulkInsertExpression( val table: TableExpression, val assignments: List>>, - val conflictColumns: List>? = null, + val conflictColumns: List> = emptyList(), val updateAssignments: List> = emptyList(), + val doNothing: Boolean = false, val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() @@ -96,12 +99,7 @@ public fun > Database.bulkInsert( table: T, block: BulkInsertStatementBuilder.(T) -> Unit ): Int { val builder = BulkInsertStatementBuilder(table).apply { block(table) } - - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = builder.assignments - ) - + val expression = BulkInsertExpression(table.asExpression(), builder.assignments) return executeUpdate(expression) } @@ -162,18 +160,19 @@ public fun > Database.bulkInsertOrUpdate( throw IllegalStateException(msg) } - if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { - val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + - " you must explicitly invoke `doNothing()`" + if (!builder.doNothing && builder.updateAssignments.isEmpty()) { + val msg = + "Cannot leave the onConflict clause empty! " + + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) } - val expression = BulkInsertExpression( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + updateAssignments = builder.updateAssignments, + doNothing = builder.doNothing ) return executeUpdate(expression) @@ -207,22 +206,18 @@ public open class BulkInsertStatementBuilder>(internal val tabl */ @KtormDsl public class BulkInsertOrUpdateStatementBuilder>(table: T) : BulkInsertStatementBuilder(table) { - internal val updateAssignments = ArrayList>() internal val conflictColumns = ArrayList>() - - internal var explicitlyDoNothing: Boolean = false + internal val updateAssignments = ArrayList>() + internal var doNothing: Boolean = false /** * Specify the update assignments while any key conflict exists. */ public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) - - explicitlyDoNothing = builder.explicitlyDoNothing - - updateAssignments += builder.assignments - - conflictColumns += columns + this.conflictColumns += columns + this.updateAssignments += builder.assignments + this.doNothing = builder.doNothing } } @@ -623,13 +618,14 @@ private fun > Database.bulkInsertOrUpdateReturningAux( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } - if (!builder.explicitlyDoNothing && builder.updateAssignments.isEmpty()) { - val msg = "You cannot leave a on-conflict clause empty! If you desire no update action at all" + - " you must explicitly invoke `doNothing()`" + if (!builder.doNothing && builder.updateAssignments.isEmpty()) { + val msg = + "Cannot leave the onConflict clause empty! " + + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) } @@ -637,7 +633,8 @@ private fun > Database.bulkInsertOrUpdateReturningAux( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = if (builder.explicitlyDoNothing) emptyList() else builder.updateAssignments, + updateAssignments = builder.updateAssignments, + doNothing = builder.doNothing, returningColumns = returningColumns.map { it.asExpression() } ) From 44247ecbad2b446bec5fd7daf65ff96f55c82913 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 00:20:48 +0800 Subject: [PATCH 52/96] refactor bulk insert returning --- .../ktorm/support/postgresql/BulkInsert.kt | 240 +++++++++--------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 7c22918a..85cb664e 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -103,124 +103,6 @@ public fun > Database.bulkInsert( return executeUpdate(expression) } -/** - * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, - * and automatically performs updates if any conflict exists. - * - * Usage: - * - * ```kotlin - * database.bulkInsertOrUpdate(Employees) { - * item { - * set(it.id, 1) - * set(it.name, "vince") - * set(it.job, "engineer") - * set(it.salary, 1000) - * set(it.hireDate, LocalDate.now()) - * set(it.departmentId, 1) - * } - * item { - * set(it.id, 5) - * set(it.name, "vince") - * set(it.job, "engineer") - * set(it.salary, 1000) - * set(it.hireDate, LocalDate.now()) - * set(it.departmentId, 1) - * } - * onConflict { - * set(it.salary, it.salary + 900) - * } - * } - * ``` - * - * Generated SQL: - * - * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) - * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) - * on conflict (id) do update set salary = t_employee.salary + ? - * ``` - * - * @since 3.3.0 - * @param table the table to be inserted. - * @param block the DSL block used to construct the expression. - * @return the effected row count. - * @see bulkInsert - */ -public fun > Database.bulkInsertOrUpdate( - table: T, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit -): Int { - val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } - - val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } - if (conflictColumns.isEmpty()) { - val msg = - "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" - throw IllegalStateException(msg) - } - - if (!builder.doNothing && builder.updateAssignments.isEmpty()) { - val msg = - "Cannot leave the onConflict clause empty! " + - "If you desire no update action at all please explicitly call `doNothing()`" - throw IllegalStateException(msg) - } - - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = builder.assignments, - conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = builder.updateAssignments, - doNothing = builder.doNothing - ) - - return executeUpdate(expression) -} - -/** - * DSL builder for bulk insert statements. - */ -@KtormDsl -public open class BulkInsertStatementBuilder>(internal val table: T) { - internal val assignments = ArrayList>>() - - /** - * Add the assignments of a new row to the bulk insert. - */ - public fun item(block: AssignmentsBuilder.() -> Unit) { - val builder = PostgreSqlAssignmentsBuilder().apply(block) - - if (assignments.isEmpty() - || assignments[0].map { it.column.name } == builder.assignments.map { it.column.name } - ) { - assignments += builder.assignments - } else { - throw IllegalArgumentException("Every item in a batch operation must be the same.") - } - } -} - -/** - * DSL builder for bulk insert or update statements. - */ -@KtormDsl -public class BulkInsertOrUpdateStatementBuilder>(table: T) : BulkInsertStatementBuilder(table) { - internal val conflictColumns = ArrayList>() - internal val updateAssignments = ArrayList>() - internal var doNothing: Boolean = false - - /** - * Specify the update assignments while any key conflict exists. - */ - public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { - val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) - this.conflictColumns += columns - this.updateAssignments += builder.assignments - this.doNothing = builder.doNothing - } -} - /** * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. * @@ -413,6 +295,81 @@ private fun > Database.bulkInsertReturningAux( return executeUpdateAndRetrieveKeys(expression) } +/** + * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, + * and automatically performs updates if any conflict exists. + * + * Usage: + * + * ```kotlin + * database.bulkInsertOrUpdate(Employees) { + * item { + * set(it.id, 1) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * item { + * set(it.id, 5) + * set(it.name, "vince") + * set(it.job, "engineer") + * set(it.salary, 1000) + * set(it.hireDate, LocalDate.now()) + * set(it.departmentId, 1) + * } + * onConflict { + * set(it.salary, it.salary + 900) + * } + * } + * ``` + * + * Generated SQL: + * + * ```sql + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) + * on conflict (id) do update set salary = t_employee.salary + ? + * ``` + * + * @since 3.3.0 + * @param table the table to be inserted. + * @param block the DSL block used to construct the expression. + * @return the effected row count. + * @see bulkInsert + */ +public fun > Database.bulkInsertOrUpdate( + table: T, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): Int { + val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } + + val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } + if (conflictColumns.isEmpty()) { + val msg = + "Table '$table' doesn't have a primary key, " + + "you must specify the conflict columns when calling onConflict(col) { .. }" + throw IllegalStateException(msg) + } + + if (!builder.doNothing && builder.updateAssignments.isEmpty()) { + val msg = + "Cannot leave the onConflict clause empty! " + + "If you desire no update action at all please explicitly call `doNothing()`" + throw IllegalStateException(msg) + } + + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = builder.assignments, + conflictColumns = conflictColumns.map { it.asExpression() }, + updateAssignments = builder.updateAssignments, + doNothing = builder.doNothing + ) + + return executeUpdate(expression) +} + /** * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, * and automatically performs updates if any conflict exists. @@ -618,14 +575,14 @@ private fun > Database.bulkInsertOrUpdateReturningAux( if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } if (!builder.doNothing && builder.updateAssignments.isEmpty()) { val msg = "Cannot leave the onConflict clause empty! " + - "If you desire no update action at all please explicitly call `doNothing()`" + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) } @@ -640,3 +597,46 @@ private fun > Database.bulkInsertOrUpdateReturningAux( return executeUpdateAndRetrieveKeys(expression) } + +/** + * DSL builder for bulk insert statements. + */ +@KtormDsl +public open class BulkInsertStatementBuilder>(internal val table: T) { + internal val assignments = ArrayList>>() + + /** + * Add the assignments of a new row to the bulk insert. + */ + public fun item(block: AssignmentsBuilder.() -> Unit) { + val builder = PostgreSqlAssignmentsBuilder().apply(block) + + if (assignments.isEmpty() + || assignments[0].map { it.column.name } == builder.assignments.map { it.column.name } + ) { + assignments += builder.assignments + } else { + throw IllegalArgumentException("Every item in a batch operation must be the same.") + } + } +} + +/** + * DSL builder for bulk insert or update statements. + */ +@KtormDsl +public class BulkInsertOrUpdateStatementBuilder>(table: T) : BulkInsertStatementBuilder(table) { + internal val conflictColumns = ArrayList>() + internal val updateAssignments = ArrayList>() + internal var doNothing: Boolean = false + + /** + * Specify the update assignments while any key conflict exists. + */ + public fun onConflict(vararg columns: Column<*>, block: InsertOrUpdateOnConflictClauseBuilder.() -> Unit) { + val builder = InsertOrUpdateOnConflictClauseBuilder().apply(block) + this.conflictColumns += columns + this.updateAssignments += builder.assignments + this.doNothing = builder.doNothing + } +} From f1777192ca3de0b0690bafd7e182b3651ccc5f5d Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 11:00:00 +0800 Subject: [PATCH 53/96] refactor bulk insert returning --- .../ktorm/support/postgresql/BulkInsert.kt | 159 ++++++++---------- .../support/postgresql/InsertOrUpdate.kt | 21 ++- 2 files changed, 87 insertions(+), 93 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 85cb664e..795cd496 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -35,7 +35,8 @@ import org.ktorm.schema.Column * For example: * * ```sql - * insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... + * insert into table (column1, column2) + * values (?, ?), (?, ?), (?, ?)... * on conflict (...) do update set ...` * ``` * @@ -58,7 +59,7 @@ public data class BulkInsertExpression( ) : SqlExpression() /** - * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. + * Bulk insert records to the table and return the effected row count. * * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance @@ -104,14 +105,7 @@ public fun > Database.bulkInsert( } /** - * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. - * - * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL - * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance - * is much better than [batchInsert]. - * - * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... - * returning id`. + * Bulk insert records to the table and return the specific column's values. * * Usage: * @@ -136,38 +130,37 @@ public fun > Database.bulkInsert( * } * ``` * + * Generated SQL: + * + * ```sql + * insert into table (name, job, manager_id, hire_date, salary, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)... + * returning id + * ``` + * * @since 3.4.0 * @param table the table to be inserted. + * @param returning the column to return * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. - * @param returningColumn the column to return - * @return the returning column value. - * @see batchInsert + * @return the returning column's values. */ -public fun , R : Any> Database.bulkInsertReturning( - table: T, - returningColumn: Column, - block: BulkInsertStatementBuilder.(T) -> Unit -): List { - val (_, rowSet) = this.bulkInsertReturningAux( - table, - listOf(returningColumn), - block +public fun , C : Any> Database.bulkInsertReturning( + table: T, returning: Column, block: BulkInsertStatementBuilder.(T) -> Unit +): List { + val builder = BulkInsertStatementBuilder(table).apply { block(table) } + + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = builder.assignments, + returningColumns = listOf(returning.asExpression()) ) - return rowSet.asIterable().map { row -> - returningColumn.sqlType.getResult(row, 1) - } + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + return rowSet.asIterable().map { row -> returning.sqlType.getResult(row, 1) } } /** - * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. - * - * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL - * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance - * is much better than [batchInsert]. - * - * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... - * returning id, job`. + * Bulk insert records to the table and return the specific columns' values. * * Usage: * @@ -192,41 +185,43 @@ public fun , R : Any> Database.bulkInsertReturning( * } * ``` * + * Generated SQL: + * + * ```sql + * insert into table (name, job, manager_id, hire_date, salary, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)... + * returning id, job + * ``` + * * @since 3.4.0 * @param table the table to be inserted. + * @param returning the column to return * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. - * @param returningColumns the columns to return - * @return the returning columns' values. - * @see batchInsert + * @return the returning column's values. */ -public fun , R1 : Any, R2 : Any> Database.bulkInsertReturning( - table: T, - returningColumns: Pair, Column>, - block: BulkInsertStatementBuilder.(T) -> Unit -): List> { - val (_, rowSet) = this.bulkInsertReturningAux( - table, - returningColumns.toList(), - block +public fun , C1 : Any, C2 : Any> Database.bulkInsertReturning( + table: T, returning: Pair, Column>, block: BulkInsertStatementBuilder.(T) -> Unit +): List> { + val (c1, c2) = returning + val builder = BulkInsertStatementBuilder(table).apply { block(table) } + + val expression = BulkInsertExpression( + table = table.asExpression(), + assignments = builder.assignments, + returningColumns = listOf(c1, c2).map { it.asExpression() } ) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) return rowSet.asIterable().map { row -> Pair( - returningColumns.first.sqlType.getResult(row, 1), - returningColumns.second.sqlType.getResult(row, 2) + c1.sqlType.getResult(row, 1), + c2.sqlType.getResult(row, 2) ) } } /** - * Construct a bulk insert expression in the given closure, then execute it and return the effected row count. - * - * The usage is almost the same as [batchInsert], but this function is implemented by generating a special SQL - * using PostgreSQL's bulk insert syntax, instead of based on JDBC batch operations. For this reason, its performance - * is much better than [batchInsert]. - * - * The generated SQL is like: `insert into table (column1, column2) values (?, ?), (?, ?), (?, ?)... - * returning id, job, salary`. + * Bulk insert records to the table and return the specific columns' values. * * Usage: * @@ -251,48 +246,40 @@ public fun , R1 : Any, R2 : Any> Database.bulkInsertReturning( * } * ``` * + * Generated SQL: + * + * ```sql + * insert into table (name, job, manager_id, hire_date, salary, department_id) + * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)... + * returning id, job, salary + * ``` + * * @since 3.4.0 * @param table the table to be inserted. + * @param returning the column to return * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. - * @param returningColumns the columns to return - * @return the returning columns' values. - * @see batchInsert + * @return the returning column's values. */ -public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertReturning( - table: T, - returningColumns: Triple, Column, Column>, - block: BulkInsertStatementBuilder.(T) -> Unit -): List> { - val (_, rowSet) = this.bulkInsertReturningAux( - table, - returningColumns.toList(), - block - ) - - return rowSet.asIterable().map { row -> - var i = 0 - Triple( - returningColumns.first.sqlType.getResult(row, ++i), - returningColumns.second.sqlType.getResult(row, ++i), - returningColumns.third.sqlType.getResult(row, ++i) - ) - } -} - -private fun > Database.bulkInsertReturningAux( - table: T, - returningColumns: List>, - block: BulkInsertStatementBuilder.(T) -> Unit -): Pair { +public fun , C1 : Any, C2 : Any, C3 : Any> Database.bulkInsertReturning( + table: T, returning: Triple, Column, Column>, block: BulkInsertStatementBuilder.(T) -> Unit +): List> { + val (c1, c2, c3) = returning val builder = BulkInsertStatementBuilder(table).apply { block(table) } val expression = BulkInsertExpression( table = table.asExpression(), assignments = builder.assignments, - returningColumns = returningColumns.map { it.asExpression() } + returningColumns = listOf(c1, c2, c3).map { it.asExpression() } ) - return executeUpdateAndRetrieveKeys(expression) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + return rowSet.asIterable().map { row -> + Triple( + c1.sqlType.getResult(row, 1), + c2.sqlType.getResult(row, 2), + c3.sqlType.getResult(row, 3) + ) + } } /** diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 5bea5a6e..615a1200 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -71,7 +71,8 @@ public data class InsertOrUpdateExpression( * Generated SQL: * * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? * ``` * @@ -110,7 +111,8 @@ public fun > Database.insertOrUpdate( * Generated SQL: * * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? * returning id * ``` @@ -159,7 +161,8 @@ public fun , C : Any> Database.insertOrUpdateReturning( * Generated SQL: * * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? * returning id, job * ``` @@ -210,7 +213,8 @@ public fun , C1 : Any, C2 : Any> Database.insertOrUpdateReturni * Generated SQL: * * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? * returning id, job, salary * ``` @@ -293,7 +297,8 @@ private fun > buildInsertOrUpdateExpression( * Generated SQL: * * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) * returning id * ``` * @@ -344,7 +349,8 @@ public fun , C : Any> Database.insertReturning( * Generated SQL: * * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) * returning id, job * ``` * @@ -397,7 +403,8 @@ public fun , C1 : Any, C2 : Any> Database.insertReturning( * Generated SQL: * * ```sql - * insert into t_employee (id, name, job, salary, hire_date, department_id) values (?, ?, ?, ?, ?, ?) + * insert into t_employee (id, name, job, salary, hire_date, department_id) + * values (?, ?, ?, ?, ?, ?) * returning id, job, salary * ``` * From 17235540621fb028e08892f482ee81318569fbf8 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 11:03:20 +0800 Subject: [PATCH 54/96] fix typo --- .../kotlin/org/ktorm/support/postgresql/BulkInsert.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 795cd496..dae67abc 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -195,9 +195,9 @@ public fun , C : Any> Database.bulkInsertReturning( * * @since 3.4.0 * @param table the table to be inserted. - * @param returning the column to return + * @param returning the columns to return * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. - * @return the returning column's values. + * @return the returning columns' values. */ public fun , C1 : Any, C2 : Any> Database.bulkInsertReturning( table: T, returning: Pair, Column>, block: BulkInsertStatementBuilder.(T) -> Unit @@ -256,9 +256,9 @@ public fun , C1 : Any, C2 : Any> Database.bulkInsertReturning( * * @since 3.4.0 * @param table the table to be inserted. - * @param returning the column to return + * @param returning the columns to return * @param block the DSL block, extension function of [BulkInsertStatementBuilder], used to construct the expression. - * @return the returning column's values. + * @return the returning columns' values. */ public fun , C1 : Any, C2 : Any, C3 : Any> Database.bulkInsertReturning( table: T, returning: Triple, Column, Column>, block: BulkInsertStatementBuilder.(T) -> Unit From f1720b320becc80708d23abbcf6b922a6479f377 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 17:40:28 +0800 Subject: [PATCH 55/96] refactor bulk insert or update returning --- .../ktorm/support/postgresql/BulkInsert.kt | 141 ++++++------------ 1 file changed, 47 insertions(+), 94 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index dae67abc..564135f7 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -16,7 +16,6 @@ package org.ktorm.support.postgresql -import org.ktorm.database.CachedRowSet import org.ktorm.database.Database import org.ktorm.database.asIterable import org.ktorm.dsl.AssignmentsBuilder @@ -324,47 +323,22 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.bulkInsertR * @param table the table to be inserted. * @param block the DSL block used to construct the expression. * @return the effected row count. - * @see bulkInsert */ public fun > Database.bulkInsertOrUpdate( table: T, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit ): Int { - val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } - - val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } - if (conflictColumns.isEmpty()) { - val msg = - "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" - throw IllegalStateException(msg) - } - - if (!builder.doNothing && builder.updateAssignments.isEmpty()) { - val msg = - "Cannot leave the onConflict clause empty! " + - "If you desire no update action at all please explicitly call `doNothing()`" - throw IllegalStateException(msg) - } - - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = builder.assignments, - conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = builder.updateAssignments, - doNothing = builder.doNothing - ) - + val expression = buildBulkInsertOrUpdateExpression(table, returning = emptyList(), block = block) return executeUpdate(expression) } /** * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, - * and automatically performs updates if any conflict exists. + * automatically performs updates if any conflict exists, and finally returns the specific column. * * Usage: * * ```kotlin - * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name)) { + * database.bulkInsertOrUpdateReturning(Employees, Employees.id) { * item { * set(it.id, 1) * set(it.name, "vince") @@ -393,40 +367,31 @@ public fun > Database.bulkInsertOrUpdate( * insert into t_employee (id, name, job, salary, hire_date, department_id) * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? - * returning id, job, ... + * returning id * ``` * * @since 3.4.0 * @param table the table to be inserted. + * @param returning the column to return * @param block the DSL block used to construct the expression. - * @param returningColumn the column to return - * @return the returning column value. - * @see bulkInsert + * @return the returning column's values. */ -public fun , R : Any> Database.bulkInsertOrUpdateReturning( - table: T, - returningColumn: Column, - block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit -): List { - val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( - table, - listOf(returningColumn), - block - ) - - return rowSet.asIterable().map { row -> - returningColumn.sqlType.getResult(row, 1) - } +public fun , C : Any> Database.bulkInsertOrUpdateReturning( + table: T, returning: Column, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List { + val expression = buildBulkInsertOrUpdateExpression(table, listOf(returning), block) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + return rowSet.asIterable().map { row -> returning.sqlType.getResult(row, 1) } } /** * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, - * and automatically performs updates if any conflict exists. + * automatically performs updates if any conflict exists, and finally returns the specific columns. * * Usage: * * ```kotlin - * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name)) { + * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { * item { * set(it.id, 1) * set(it.name, "vince") @@ -455,43 +420,37 @@ public fun , R : Any> Database.bulkInsertOrUpdateReturning( * insert into t_employee (id, name, job, salary, hire_date, department_id) * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? - * returning id, job, ... + * returning id, job * ``` * * @since 3.4.0 * @param table the table to be inserted. + * @param returning the columns to return * @param block the DSL block used to construct the expression. - * @param returningColumns the column to return * @return the returning columns' values. - * @see bulkInsert */ -public fun , R1 : Any, R2 : Any> Database.bulkInsertOrUpdateReturning( - table: T, - returningColumns: Pair, Column>, - block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit -): List> { - val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( - table, - returningColumns.toList(), - block - ) - +public fun , C1 : Any, C2 : Any> Database.bulkInsertOrUpdateReturning( + table: T, returning: Pair, Column>, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): List> { + val (c1, c2) = returning + val expression = buildBulkInsertOrUpdateExpression(table, listOf(c1, c2), block) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) return rowSet.asIterable().map { row -> Pair( - returningColumns.first.sqlType.getResult(row, 1), - returningColumns.second.sqlType.getResult(row, 2) + c1.sqlType.getResult(row, 1), + c2.sqlType.getResult(row, 2) ) } } /** * Bulk insert records to the table, determining if there is a key conflict while inserting each of them, - * and automatically performs updates if any conflict exists. + * automatically performs updates if any conflict exists, and finally returns the specific columns. * * Usage: * * ```kotlin - * database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.name, Employees.salary)) { + * database.bulkInsertOrUpdateReturning(Employees, Triple(Employees.id, Employees.job, Employees.salary)) { * item { * set(it.id, 1) * set(it.name, "vince") @@ -520,69 +479,63 @@ public fun , R1 : Any, R2 : Any> Database.bulkInsertOrUpdateRet * insert into t_employee (id, name, job, salary, hire_date, department_id) * values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?) * on conflict (id) do update set salary = t_employee.salary + ? - * returning id, job, ... + * returning id, job, salary * ``` * * @since 3.4.0 * @param table the table to be inserted. + * @param returning the columns to return * @param block the DSL block used to construct the expression. - * @param returningColumns the column to return * @return the returning columns' values. - * @see bulkInsert */ -public fun , R1 : Any, R2 : Any, R3 : Any> Database.bulkInsertOrUpdateReturning( +public fun , C1 : Any, C2 : Any, C3 : Any> Database.bulkInsertOrUpdateReturning( table: T, - returningColumns: Triple, Column, Column>, + returning: Triple, Column, Column>, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit -): List> { - val (_, rowSet) = this.bulkInsertOrUpdateReturningAux( - table, - returningColumns.toList(), - block - ) - +): List> { + val (c1, c2, c3) = returning + val expression = buildBulkInsertOrUpdateExpression(table, listOf(c1, c2, c3), block) + val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) return rowSet.asIterable().map { row -> - var i = 0 Triple( - returningColumns.first.sqlType.getResult(row, ++i), - returningColumns.second.sqlType.getResult(row, ++i), - returningColumns.third.sqlType.getResult(row, ++i) + c1.sqlType.getResult(row, 1), + c2.sqlType.getResult(row, 2), + c3.sqlType.getResult(row, 3) ) } } -private fun > Database.bulkInsertOrUpdateReturningAux( - table: T, - returningColumns: List>, - block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit -): Pair { +/** + * Build a bulk insert or update expression. + */ +private fun > buildBulkInsertOrUpdateExpression( + table: T, returning: List>, block: BulkInsertOrUpdateStatementBuilder.(T) -> Unit +): BulkInsertExpression { val builder = BulkInsertOrUpdateStatementBuilder(table).apply { block(table) } val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } if (conflictColumns.isEmpty()) { val msg = "Table '$table' doesn't have a primary key, " + - "you must specify the conflict columns when calling onConflict(col) { .. }" + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } if (!builder.doNothing && builder.updateAssignments.isEmpty()) { val msg = "Cannot leave the onConflict clause empty! " + - "If you desire no update action at all please explicitly call `doNothing()`" + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) } - val expression = BulkInsertExpression( + return BulkInsertExpression( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, updateAssignments = builder.updateAssignments, doNothing = builder.doNothing, - returningColumns = returningColumns.map { it.asExpression() } + returningColumns = returning.map { it.asExpression() } ) - - return executeUpdateAndRetrieveKeys(expression) } /** From e3a7a14410cd9e979a1f8d204ee30e26a84493d3 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 18:06:00 +0800 Subject: [PATCH 56/96] insert or update returning row --- .../support/postgresql/InsertOrUpdate.kt | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 615a1200..91a9ec55 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -16,6 +16,7 @@ package org.ktorm.support.postgresql +import org.ktorm.database.CachedRowSet import org.ktorm.database.Database import org.ktorm.dsl.AssignmentsBuilder import org.ktorm.dsl.KtormDsl @@ -126,16 +127,8 @@ public fun > Database.insertOrUpdate( public fun , C : Any> Database.insertOrUpdateReturning( table: T, returning: Column, block: InsertOrUpdateStatementBuilder.(T) -> Unit ): C? { - val expression = buildInsertOrUpdateExpression(table, listOf(returning), block) - val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - - if (rowSet.size() == 1) { - check(rowSet.next()) - return returning.sqlType.getResult(rowSet, 1) - } else { - val (sql, _) = formatExpression(expression, beautifySql = true) - throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") - } + val row = insertOrUpdateReturningRow(table, listOf(returning), block) + return returning.sqlType.getResult(row, 1) } /** @@ -177,16 +170,8 @@ public fun , C1 : Any, C2 : Any> Database.insertOrUpdateReturni table: T, returning: Pair, Column>, block: InsertOrUpdateStatementBuilder.(T) -> Unit ): Pair { val (c1, c2) = returning - val expression = buildInsertOrUpdateExpression(table, listOf(c1, c2), block) - val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - - if (rowSet.size() == 1) { - check(rowSet.next()) - return Pair(c1.sqlType.getResult(rowSet, 1), c2.sqlType.getResult(rowSet, 2)) - } else { - val (sql, _) = formatExpression(expression, beautifySql = true) - throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") - } + val row = insertOrUpdateReturningRow(table, listOf(c1, c2), block) + return Pair(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2)) } /** @@ -229,16 +214,22 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.insertOrUpd table: T, returning: Triple, Column, Column>, block: InsertOrUpdateStatementBuilder.(T) -> Unit ): Triple { val (c1, c2, c3) = returning - val expression = buildInsertOrUpdateExpression(table, listOf(c1, c2, c3), block) + val row = insertOrUpdateReturningRow(table, listOf(c1, c2, c3), block) + return Triple(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2), c3.sqlType.getResult(row, 3)) +} + +/** + * Insert or update, returning one row. + */ +private fun > Database.insertOrUpdateReturningRow( + table: T, returning: List>, block: InsertOrUpdateStatementBuilder.(T) -> Unit +): CachedRowSet { + val expression = buildInsertOrUpdateExpression(table, returning, block) val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) if (rowSet.size() == 1) { check(rowSet.next()) - return Triple( - c1.sqlType.getResult(rowSet, 1), - c2.sqlType.getResult(rowSet, 2), - c3.sqlType.getResult(rowSet, 3) - ) + return rowSet } else { val (sql, _) = formatExpression(expression, beautifySql = true) throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") From e490a35c3aa3761e8703a34d78c1d8c2c860fb6d Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 18:13:29 +0800 Subject: [PATCH 57/96] insert returning row --- .../support/postgresql/InsertOrUpdate.kt | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 91a9ec55..991f0de0 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -302,23 +302,8 @@ private fun > buildInsertOrUpdateExpression( public fun , C : Any> Database.insertReturning( table: T, returning: Column, block: AssignmentsBuilder.(T) -> Unit ): C? { - val builder = PostgreSqlAssignmentsBuilder().apply { block(table) } - - val expression = InsertOrUpdateExpression( - table = table.asExpression(), - assignments = builder.assignments, - returningColumns = listOf(returning.asExpression()) - ) - - val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - - if (rowSet.size() == 1) { - check(rowSet.next()) - return returning.sqlType.getResult(rowSet, 1) - } else { - val (sql, _) = formatExpression(expression, beautifySql = true) - throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") - } + val row = insertReturningRow(table, listOf(returning), block) + return returning.sqlType.getResult(row, 1) } /** @@ -355,23 +340,8 @@ public fun , C1 : Any, C2 : Any> Database.insertReturning( table: T, returning: Pair, Column>, block: AssignmentsBuilder.(T) -> Unit ): Pair { val (c1, c2) = returning - val builder = PostgreSqlAssignmentsBuilder().apply { block(table) } - - val expression = InsertOrUpdateExpression( - table = table.asExpression(), - assignments = builder.assignments, - returningColumns = listOf(c1, c2).map { it.asExpression() } - ) - - val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - - if (rowSet.size() == 1) { - check(rowSet.next()) - return Pair(c1.sqlType.getResult(rowSet, 1), c2.sqlType.getResult(rowSet, 2)) - } else { - val (sql, _) = formatExpression(expression, beautifySql = true) - throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") - } + val row = insertReturningRow(table, listOf(c1, c2), block) + return Pair(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2)) } /** @@ -409,23 +379,29 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.insertRetur table: T, returning: Triple, Column, Column>, block: AssignmentsBuilder.(T) -> Unit ): Triple { val (c1, c2, c3) = returning + val row = insertReturningRow(table, listOf(c1, c2, c3), block) + return Triple(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2), c3.sqlType.getResult(row, 3)) +} + +/** + * Insert and returning one row. + */ +private fun > Database.insertReturningRow( + table: T, returning: List>, block: AssignmentsBuilder.(T) -> Unit +): CachedRowSet { val builder = PostgreSqlAssignmentsBuilder().apply { block(table) } val expression = InsertOrUpdateExpression( table = table.asExpression(), assignments = builder.assignments, - returningColumns = listOf(c1, c2, c3).map { it.asExpression() } + returningColumns = returning.map { it.asExpression() } ) val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) if (rowSet.size() == 1) { check(rowSet.next()) - return Triple( - c1.sqlType.getResult(rowSet, 1), - c2.sqlType.getResult(rowSet, 2), - c3.sqlType.getResult(rowSet, 3) - ) + return rowSet } else { val (sql, _) = formatExpression(expression, beautifySql = true) throw IllegalStateException("Expected 1 row but ${rowSet.size()} returned from sql: \n\n$sql") From ddf868090231aeab867e6270c53092d6efce4f9d Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 22:54:07 +0800 Subject: [PATCH 58/96] bulk insert returning row set --- .../ktorm/support/postgresql/BulkInsert.kt | 50 +++++++------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 564135f7..68c7cf05 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -16,6 +16,7 @@ package org.ktorm.support.postgresql +import org.ktorm.database.CachedRowSet import org.ktorm.database.Database import org.ktorm.database.asIterable import org.ktorm.dsl.AssignmentsBuilder @@ -146,15 +147,7 @@ public fun > Database.bulkInsert( public fun , C : Any> Database.bulkInsertReturning( table: T, returning: Column, block: BulkInsertStatementBuilder.(T) -> Unit ): List { - val builder = BulkInsertStatementBuilder(table).apply { block(table) } - - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = builder.assignments, - returningColumns = listOf(returning.asExpression()) - ) - - val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + val rowSet = bulkInsertReturningRowSet(table, listOf(returning), block) return rowSet.asIterable().map { row -> returning.sqlType.getResult(row, 1) } } @@ -202,21 +195,8 @@ public fun , C1 : Any, C2 : Any> Database.bulkInsertReturning( table: T, returning: Pair, Column>, block: BulkInsertStatementBuilder.(T) -> Unit ): List> { val (c1, c2) = returning - val builder = BulkInsertStatementBuilder(table).apply { block(table) } - - val expression = BulkInsertExpression( - table = table.asExpression(), - assignments = builder.assignments, - returningColumns = listOf(c1, c2).map { it.asExpression() } - ) - - val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - return rowSet.asIterable().map { row -> - Pair( - c1.sqlType.getResult(row, 1), - c2.sqlType.getResult(row, 2) - ) - } + val rowSet = bulkInsertReturningRowSet(table, listOf(c1, c2), block) + return rowSet.asIterable().map { row -> Pair(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2)) } } /** @@ -263,22 +243,28 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.bulkInsertR table: T, returning: Triple, Column, Column>, block: BulkInsertStatementBuilder.(T) -> Unit ): List> { val (c1, c2, c3) = returning + val rowSet = bulkInsertReturningRowSet(table, listOf(c1, c2, c3), block) + return rowSet.asIterable().map { row -> + Triple(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2), c3.sqlType.getResult(row, 3)) + } +} + +/** + * Bulk insert records to the table, returning row set. + */ +private fun > Database.bulkInsertReturningRowSet( + table: T, returning: List>, block: BulkInsertStatementBuilder.(T) -> Unit +): CachedRowSet { val builder = BulkInsertStatementBuilder(table).apply { block(table) } val expression = BulkInsertExpression( table = table.asExpression(), assignments = builder.assignments, - returningColumns = listOf(c1, c2, c3).map { it.asExpression() } + returningColumns = returning.map { it.asExpression() } ) val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - return rowSet.asIterable().map { row -> - Triple( - c1.sqlType.getResult(row, 1), - c2.sqlType.getResult(row, 2), - c3.sqlType.getResult(row, 3) - ) - } + return rowSet } /** From cbee8890c1a18e472f9827d2931dd1dc5491a6b5 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 23:06:52 +0800 Subject: [PATCH 59/96] update code style --- .../org/ktorm/support/postgresql/BulkInsert.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 68c7cf05..5c151dd4 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -421,12 +421,7 @@ public fun , C1 : Any, C2 : Any> Database.bulkInsertOrUpdateRet val (c1, c2) = returning val expression = buildBulkInsertOrUpdateExpression(table, listOf(c1, c2), block) val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) - return rowSet.asIterable().map { row -> - Pair( - c1.sqlType.getResult(row, 1), - c2.sqlType.getResult(row, 2) - ) - } + return rowSet.asIterable().map { row -> Pair(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2)) } } /** @@ -483,11 +478,7 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.bulkInsertO val expression = buildBulkInsertOrUpdateExpression(table, listOf(c1, c2, c3), block) val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) return rowSet.asIterable().map { row -> - Triple( - c1.sqlType.getResult(row, 1), - c2.sqlType.getResult(row, 2), - c3.sqlType.getResult(row, 3) - ) + Triple(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2), c3.sqlType.getResult(row, 3)) } } From fddce19e18c9be26be568efe0420e8e6e9883fb8 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 28 Apr 2021 23:48:54 +0800 Subject: [PATCH 60/96] rm doNothing --- .../main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt | 5 +---- .../kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index 5c151dd4..a8881b0f 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -44,7 +44,6 @@ import org.ktorm.schema.Column * @property assignments column assignments of the bulk insert statement. * @property conflictColumns the index columns on which the conflict may happen. * @property updateAssignments the updated column assignments while key conflict exists. - * @property doNothing whether we should ignore errors and do nothing when conflict happens. * @property returningColumns the returning columns. */ public data class BulkInsertExpression( @@ -52,7 +51,6 @@ public data class BulkInsertExpression( val assignments: List>>, val conflictColumns: List> = emptyList(), val updateAssignments: List> = emptyList(), - val doNothing: Boolean = false, val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() @@ -509,8 +507,7 @@ private fun > buildBulkInsertOrUpdateExpression( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = builder.updateAssignments, - doNothing = builder.doNothing, + updateAssignments = if (builder.doNothing) emptyList() else builder.updateAssignments, returningColumns = returning.map { it.asExpression() } ) } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 991f0de0..8251acfe 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -35,7 +35,6 @@ import org.ktorm.schema.Column * @property assignments the inserted column assignments. * @property conflictColumns the index columns on which the conflict may happen. * @property updateAssignments the updated column assignments while any key conflict exists. - * @property doNothing whether we should ignore errors and do nothing when conflict happens. * @property returningColumns the returning columns. */ public data class InsertOrUpdateExpression( @@ -43,7 +42,6 @@ public data class InsertOrUpdateExpression( val assignments: List>, val conflictColumns: List> = emptyList(), val updateAssignments: List> = emptyList(), - val doNothing: Boolean = false, val returningColumns: List> = emptyList(), override val isLeafNode: Boolean = false, override val extraProperties: Map = emptyMap() @@ -263,8 +261,7 @@ private fun > buildInsertOrUpdateExpression( table = table.asExpression(), assignments = builder.assignments, conflictColumns = conflictColumns.map { it.asExpression() }, - updateAssignments = builder.updateAssignments, - doNothing = builder.doNothing, + updateAssignments = if (builder.doNothing) emptyList() else builder.updateAssignments, returningColumns = returning.map { it.asExpression() } ) } From c2a40201f020d73a7bd46464d4d4a22fa6fb0262 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 01:13:50 +0800 Subject: [PATCH 61/96] update postgresql formatter --- .../support/postgresql/PostgreSqlDialect.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index c8f58fda..5033717f 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -192,7 +192,7 @@ public open class PostgreSqlFormatter( writeKeyword("values ") writeInsertValues(expr.assignments) - if (expr.conflictColumns != null) { + if (expr.conflictColumns.isNotEmpty()) { writeKeyword("on conflict ") writeInsertColumnNames(expr.conflictColumns) @@ -205,8 +205,9 @@ public open class PostgreSqlFormatter( } if (expr.returningColumns.isNotEmpty()) { - writeKeyword(" returning ") - expr.returningColumns.forEachIndexed { i, column -> + writeKeyword("returning ") + + for ((i, column) in expr.returningColumns.withIndex()) { if (i > 0) write(", ") checkColumnName(column.name) write(column.name.quoted) @@ -230,7 +231,7 @@ public open class PostgreSqlFormatter( writeInsertValues(assignments) } - if (expr.conflictColumns != null) { + if (expr.conflictColumns.isNotEmpty()) { writeKeyword("on conflict ") writeInsertColumnNames(expr.conflictColumns) @@ -243,8 +244,9 @@ public open class PostgreSqlFormatter( } if (expr.returningColumns.isNotEmpty()) { - writeKeyword(" returning ") - expr.returningColumns.forEachIndexed { i, column -> + writeKeyword("returning ") + + for ((i, column) in expr.returningColumns.withIndex()) { if (i > 0) write(", ") checkColumnName(column.name) write(column.name.quoted) @@ -306,14 +308,16 @@ public open class PostgreSqlExpressionVisitor : SqlExpressionVisitor() { protected open fun visitInsertOrUpdate(expr: InsertOrUpdateExpression): InsertOrUpdateExpression { val table = visitTable(expr.table) val assignments = visitColumnAssignments(expr.assignments) - val conflictColumns = if (expr.conflictColumns != null) visitExpressionList(expr.conflictColumns) else null + val conflictColumns = visitExpressionList(expr.conflictColumns) val updateAssignments = visitColumnAssignments(expr.updateAssignments) + val returningColumns = visitExpressionList(expr.returningColumns) @Suppress("ComplexCondition") if (table === expr.table && assignments === expr.assignments && conflictColumns === expr.conflictColumns && updateAssignments === expr.updateAssignments + && returningColumns === expr.returningColumns ) { return expr } else { @@ -329,14 +333,16 @@ public open class PostgreSqlExpressionVisitor : SqlExpressionVisitor() { protected open fun visitBulkInsert(expr: BulkInsertExpression): BulkInsertExpression { val table = expr.table val assignments = visitBulkInsertAssignments(expr.assignments) - val conflictColumns = if (expr.conflictColumns != null) visitExpressionList(expr.conflictColumns) else null + val conflictColumns = visitExpressionList(expr.conflictColumns) val updateAssignments = visitColumnAssignments(expr.updateAssignments) + val returningColumns = visitExpressionList(expr.returningColumns) @Suppress("ComplexCondition") if (table === expr.table && assignments === expr.assignments && conflictColumns === expr.conflictColumns && updateAssignments === expr.updateAssignments + && returningColumns === expr.returningColumns ) { return expr } else { From d8a79c92ac59ef30f4f71e70fae8c90292cb687b Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 11:51:11 +0800 Subject: [PATCH 62/96] fix tests --- .../support/postgresql/PostgreSqlTest.kt | 333 +++++++++--------- 1 file changed, 171 insertions(+), 162 deletions(-) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 63a9e9a6..ba781300 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -140,92 +140,172 @@ class PostgreSqlTest : BaseTest() { assert(database.employees.find { it.id eq 5 }!!.salary == 1000L) } - @Test - fun testBulkInsert() { - database.bulkInsert(Employees) { - item { - set(it.name, "vince") - set(it.job, "engineer") - set(it.salary, 1000) - set(it.hireDate, LocalDate.now()) - set(it.departmentId, 1) - } - item { - set(it.name, "vince") - set(it.job, "engineer") - set(it.salary, 1000) - set(it.hireDate, LocalDate.now()) - set(it.departmentId, 1) - } - } - - assert(database.employees.count() == 6) - } - @Test fun testInsertOrUpdateReturning() { - database.insertOrUpdateReturning( - Employees, - Employees.id - ) { - set(it.id, 1009) + database.insertOrUpdateReturning(Employees, Employees.id) { set(it.name, "pedro") set(it.job, "engineer") set(it.salary, 1500) set(it.hireDate, LocalDate.now()) set(it.departmentId, 1) - onConflict { set(it.salary, it.salary + 900) } - }.let { createdId -> - assert(createdId == 1009) + }.let { id -> + assert(id == 5) } - database.insertOrUpdateReturning( - Employees, - Pair( - Employees.id, - Employees.name - ) - ) { - set(it.id, 1001) + database.insertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) set(it.hireDate, LocalDate.now()) set(it.departmentId, 1) - onConflict { set(it.salary, it.salary + 900) } - }.let { (createdId, createdName) -> - assert(createdId == 1001) - assert(createdName == "vince") + }.let { (id, job) -> + assert(id == 6) + assert(job == "engineer") } - database.insertOrUpdateReturning( - Employees, - Triple( - Employees.id, - Employees.name, - Employees.salary - ) - ) { - set(it.id, 1001) + val t = Employees.aliased("t") + database.insertOrUpdateReturning(t, Triple(t.id, t.job, t.salary)) { set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) set(it.hireDate, LocalDate.now()) set(it.departmentId, 1) - onConflict(it.id) { set(it.salary, it.salary + 900) } - }.let { (createdId, createdName, createdSalary) -> - assert(createdId == 1001) - assert(createdName == "vince") - assert(createdSalary == 1900L) + }.let { (id, job, salary) -> + assert(id == 7) + assert(job == "engineer") + assert(salary == 1000L) + } + } + + @Test + fun testInsertReturning() { + database.insertReturning(Employees, Employees.id) { + set(it.name, "pedro") + set(it.job, "engineer") + set(it.salary, 1500) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + }.let { id -> + assert(id == 5) + } + + database.insertReturning(Employees, Pair(Employees.id, Employees.job)) { + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + }.let { (id, job) -> + assert(id == 6) + assert(job == "engineer") + } + + val t = Employees.aliased("t") + database.insertReturning(t, Triple(t.id, t.job, t.salary)) { + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + }.let { (id, job, salary) -> + assert(id == 7) + assert(job == "engineer") + assert(salary == 1000L) + } + } + + @Test + fun testBulkInsert() { + database.bulkInsert(Employees) { + item { + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + } + item { + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + } + } + + assert(database.employees.count() == 6) + } + + @Test + fun testBulkInsertReturning() { + database.bulkInsertReturning(Employees, Employees.id) { + item { + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { results -> + assert(results.size == 2) + assert(results == listOf(5, 6)) + } + + database.bulkInsertReturning(Employees, Pair(Employees.id, Employees.job)) { + item { + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { results -> + assert(results.size == 2) + assert(results == listOf(Pair(7, "trainee"), Pair(8, "engineer"))) + } + + val t = Employees.aliased("t") + database.bulkInsertReturning(t, Triple(t.id, t.job, t.salary)) { + item { + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + }.let { results -> + assert(results.size == 2) + assert(results == listOf(Triple(9, "trainee", 1000L), Triple(10, "engineer", 1000L))) } } @@ -269,15 +349,11 @@ class PostgreSqlTest : BaseTest() { } @Test - fun testBulkInsertWithUpdate() { - // Make sure we are creating new entries in the table (avoid colliding with existing test data) - val id1 = (Math.random() * 10000).roundToInt() - val id2 = (Math.random() * 10000).roundToInt() - - val bulkInsertWithUpdate = { onDuplicateKeyDoNothing: Boolean -> + fun testBulkInsertOrUpdate1() { + val bulkInsertWithUpdate = { ignoreErrors: Boolean -> database.bulkInsertOrUpdate(Employees) { item { - set(it.id, id1) + set(it.id, 5) set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) @@ -285,43 +361,36 @@ class PostgreSqlTest : BaseTest() { set(it.departmentId, 1) } item { - set(it.id, id2) + set(it.id, 6) set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) set(it.hireDate, LocalDate.now()) set(it.departmentId, 1) } - onConflict(Employees.id) { - if (!onDuplicateKeyDoNothing) - set(it.salary, it.salary + 900) - else - doNothing() + onConflict { + if (ignoreErrors) doNothing() else set(it.salary, it.salary + 900) } } } bulkInsertWithUpdate(false) - assert(database.employees.find { it.id eq id1 }!!.salary == 1000L) - assert(database.employees.find { it.id eq id2 }!!.salary == 1000L) + assert(database.employees.find { it.id eq 5 }!!.salary == 1000L) + assert(database.employees.find { it.id eq 6 }!!.salary == 1000L) bulkInsertWithUpdate(false) - assert(database.employees.find { it.id eq id1 }!!.salary == 1900L) - assert(database.employees.find { it.id eq id2 }!!.salary == 1900L) + assert(database.employees.find { it.id eq 5 }!!.salary == 1900L) + assert(database.employees.find { it.id eq 6 }!!.salary == 1900L) bulkInsertWithUpdate(true) - assert(database.employees.find { it.id eq id1 }!!.salary == 1900L) - assert(database.employees.find { it.id eq id2 }!!.salary == 1900L) + assert(database.employees.find { it.id eq 5 }!!.salary == 1900L) + assert(database.employees.find { it.id eq 6 }!!.salary == 1900L) } @Test - fun testBulkInsertReturning() { - database.bulkInsertReturning( - Employees, - Employees.id - ) { + fun testBulkInsertOrUpdateReturning() { + database.bulkInsertOrUpdateReturning(Employees, Employees.id) { item { - set(it.id, 10001) set(it.name, "vince") set(it.job, "trainee") set(it.salary, 1000) @@ -329,65 +398,22 @@ class PostgreSqlTest : BaseTest() { set(it.departmentId, 2) } item { - set(it.id, 50001) set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) set(it.hireDate, LocalDate.now()) set(it.departmentId, 2) } - }.let { createdIds -> - assert(createdIds.size == 2) - assert( - listOf( - 10001, - 50001 - ) == createdIds - ) - } - - database.bulkInsertReturning( - Employees, - Pair( - Employees.id, - Employees.name - ) - ) { - item { - set(it.id, 10002) - set(it.name, "vince") - set(it.job, "trainee") - set(it.salary, 1000) - set(it.hireDate, LocalDate.now()) - set(it.departmentId, 2) - } - item { - set(it.id, 50002) - set(it.name, "vince") - set(it.job, "engineer") - set(it.salary, 1000) - set(it.hireDate, LocalDate.now()) - set(it.departmentId, 2) + onConflict { + doNothing() } - }.let { created -> - assert( - listOf( - (10002 to "vince"), - (50002 to "vince") - ) == created - ) - } - - database.bulkInsertReturning( - Employees, - Triple( - Employees.id, - Employees.name, - Employees.job - ) - ) { + }.let { results -> + assert(results.size == 2) + assert(results == listOf(5, 6)) + } + + database.bulkInsertOrUpdateReturning(Employees, Pair(Employees.id, Employees.job)) { item { - set(it.id, 10003) set(it.name, "vince") set(it.job, "trainee") set(it.salary, 1000) @@ -395,34 +421,23 @@ class PostgreSqlTest : BaseTest() { set(it.departmentId, 2) } item { - set(it.id, 50003) set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) set(it.hireDate, LocalDate.now()) set(it.departmentId, 2) } - }.let { created -> - assert( - listOf( - Triple(10003, "vince", "trainee"), - Triple(50003, "vince", "engineer") - ) == created - ) + onConflict { + doNothing() + } + }.let { results -> + assert(results.size == 2) + assert(results == listOf(Pair(7, "trainee"), Pair(8, "engineer"))) } - } - @Test - fun testBulkInsertOrUpdateReturning() { - database.bulkInsertOrUpdateReturning( - Employees, - Pair( - Employees.id, - Employees.job - ) - ) { + val t = Employees.aliased("t") + database.bulkInsertOrUpdateReturning(t, Triple(t.id, t.job, t.salary)) { item { - set(it.id, 1000) set(it.name, "vince") set(it.job, "trainee") set(it.salary, 1000) @@ -430,24 +445,18 @@ class PostgreSqlTest : BaseTest() { set(it.departmentId, 2) } item { - set(it.id, 5000) set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) set(it.hireDate, LocalDate.now()) set(it.departmentId, 2) } - onConflict(it.id) { - set(it.departmentId, excluded(it.departmentId)) - set(it.salary, it.salary + 1000) + onConflict { + doNothing() } - }.let { created -> - assert( - listOf( - Pair(1000, "trainee"), - Pair(5000, "engineer") - ) == created - ) + }.let { results -> + assert(results.size == 2) + assert(results == listOf(Triple(9, "trainee", 1000L), Triple(10, "engineer", 1000L))) } } From f2189da85157ed7edca127a0e44c59861ec4d17c Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 12:12:59 +0800 Subject: [PATCH 63/96] fix insert or update returning row --- .../support/postgresql/InsertOrUpdate.kt | 25 ++++++++-- .../support/postgresql/PostgreSqlTest.kt | 47 +++++++++++++++++-- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index 8251acfe..f8884899 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -126,7 +126,11 @@ public fun , C : Any> Database.insertOrUpdateReturning( table: T, returning: Column, block: InsertOrUpdateStatementBuilder.(T) -> Unit ): C? { val row = insertOrUpdateReturningRow(table, listOf(returning), block) - return returning.sqlType.getResult(row, 1) + if (row == null) { + return null + } else { + return returning.sqlType.getResult(row, 1) + } } /** @@ -169,7 +173,11 @@ public fun , C1 : Any, C2 : Any> Database.insertOrUpdateReturni ): Pair { val (c1, c2) = returning val row = insertOrUpdateReturningRow(table, listOf(c1, c2), block) - return Pair(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2)) + if (row == null) { + return Pair(null, null) + } else { + return Pair(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2)) + } } /** @@ -213,7 +221,11 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.insertOrUpd ): Triple { val (c1, c2, c3) = returning val row = insertOrUpdateReturningRow(table, listOf(c1, c2, c3), block) - return Triple(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2), c3.sqlType.getResult(row, 3)) + if (row == null) { + return Triple(null, null, null) + } else { + return Triple(c1.sqlType.getResult(row, 1), c2.sqlType.getResult(row, 2), c3.sqlType.getResult(row, 3)) + } } /** @@ -221,10 +233,15 @@ public fun , C1 : Any, C2 : Any, C3 : Any> Database.insertOrUpd */ private fun > Database.insertOrUpdateReturningRow( table: T, returning: List>, block: InsertOrUpdateStatementBuilder.(T) -> Unit -): CachedRowSet { +): CachedRowSet? { val expression = buildInsertOrUpdateExpression(table, returning, block) val (_, rowSet) = executeUpdateAndRetrieveKeys(expression) + if (rowSet.size() == 0) { + // Possible when using onConflict { doNothing() } + return null + } + if (rowSet.size() == 1) { check(rowSet.next()) return rowSet diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index ba781300..641956ee 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -22,7 +22,6 @@ import java.util.concurrent.ExecutionException import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException -import kotlin.math.roundToInt /** * Created by vince on Feb 13, 2019. @@ -171,6 +170,7 @@ class PostgreSqlTest : BaseTest() { val t = Employees.aliased("t") database.insertOrUpdateReturning(t, Triple(t.id, t.job, t.salary)) { + set(it.id, 6) set(it.name, "vince") set(it.job, "engineer") set(it.salary, 1000) @@ -180,9 +180,25 @@ class PostgreSqlTest : BaseTest() { set(it.salary, it.salary + 900) } }.let { (id, job, salary) -> - assert(id == 7) + assert(id == 6) assert(job == "engineer") - assert(salary == 1000L) + assert(salary == 1900L) + } + + database.insertOrUpdateReturning(t, Triple(t.id, t.job, t.salary)) { + set(it.id, 6) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 1) + onConflict(it.id) { + doNothing() + } + }.let { (id, job, salary) -> + assert(id == null) + assert(job == null) + assert(salary == null) } } @@ -458,6 +474,31 @@ class PostgreSqlTest : BaseTest() { assert(results.size == 2) assert(results == listOf(Triple(9, "trainee", 1000L), Triple(10, "engineer", 1000L))) } + + database.bulkInsertOrUpdateReturning(t, Triple(t.id, t.job, t.salary)) { + item { + set(it.id, 10) + set(it.name, "vince") + set(it.job, "trainee") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + item { + set(it.id, 11) + set(it.name, "vince") + set(it.job, "engineer") + set(it.salary, 1000) + set(it.hireDate, LocalDate.now()) + set(it.departmentId, 2) + } + onConflict { + doNothing() + } + }.let { results -> + assert(results.size == 1) + assert(results == listOf(Triple(11, "engineer", 1000L))) + } } @Test From dd9809ad7498bc34683be0e46a1888560cbeee04 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 17:14:29 +0800 Subject: [PATCH 64/96] fix detekt rule --- detekt.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detekt.yml b/detekt.yml index 2913b510..4f2de32f 100644 --- a/detekt.yml +++ b/detekt.yml @@ -28,9 +28,9 @@ console-reports: comments: active: true CommentOverPrivateFunction: - active: true + active: false CommentOverPrivateProperty: - active: true + active: false EndOfSentenceFormat: active: true endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) @@ -411,7 +411,7 @@ style: maxJumpCount: 2 MagicNumber: active: true - ignoreNumbers: '-1,0,1,2' + ignoreNumbers: '-1,0,1,2,3' ignoreHashCodeFunction: true ignorePropertyDeclaration: false ignoreConstantDeclaration: true From 5c661d7398bb9678e593abbf94bd5b3e7baab363 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 18:17:26 +0800 Subject: [PATCH 65/96] fix detekt rule --- detekt.yml | 4 +--- .../kotlin/org/ktorm/database/Database.kt | 4 ++-- .../kotlin/org/ktorm/database/SqlDialect.kt | 5 ++-- .../org/ktorm/entity/EntityImplementation.kt | 2 +- .../ktorm/expression/SqlExpressionVisitor.kt | 3 ++- .../org/ktorm/expression/SqlFormatter.kt | 4 +++- .../main/kotlin/org/ktorm/schema/BaseTable.kt | 24 ++++++++++--------- .../ktorm/support/postgresql/BulkInsert.kt | 4 ++-- .../support/postgresql/InsertOrUpdate.kt | 4 ++-- 9 files changed, 29 insertions(+), 25 deletions(-) diff --git a/detekt.yml b/detekt.yml index 4f2de32f..a9b7f1fb 100644 --- a/detekt.yml +++ b/detekt.yml @@ -192,8 +192,7 @@ formatting: ImportOrdering: active: false Indentation: - # Temporarily disable the indentation rule as it doesn't work after upgrading detekt. https://github.com/detekt/detekt/issues/2970 - active: false + active: true autoCorrect: false indentSize: 4 continuationIndentSize: 4 @@ -242,7 +241,6 @@ formatting: active: true autoCorrect: false ParameterListWrapping: - # Temporarily disable this rule as it doesn't work after upgrading detekt. https://github.com/detekt/detekt/issues/2970 active: false autoCorrect: false indentSize: 4 diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt index 654edf92..0aad560a 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/Database.kt @@ -269,8 +269,8 @@ public class Database( } if (logger.isInfoEnabled()) { - logger.info("Connected to $url, productName: $productName, " + - "productVersion: $productVersion, logger: $logger, dialect: $dialect") + val msg = "Connected to %s, productName: %s, productVersion: %s, logger: %s, dialect: %s" + logger.info(msg.format(url, productName, productVersion, logger, dialect)) } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt index 584d83f2..45684ed3 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt @@ -107,7 +107,8 @@ public fun detectDialectImplementation(): SqlDialect { return when (dialects.size) { 0 -> object : SqlDialect { } 1 -> dialects[0] - else -> error("More than one dialect implementations found in the classpath, " + - "please choose one manually, they are: $dialects") + else -> error( + "More than one dialect implementations found in the classpath, please choose one manually: $dialects" + ) } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index c789c891..dcb0789f 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -110,7 +110,7 @@ internal class EntityImplementation( try { return returnType.jvmErasure.defaultValue } catch (e: Throwable) { - val msg = + val msg = "" + "The value of non-null property [$this] doesn't exist, " + "an error occurred while trying to create a default one. " + "Please ensure its value exists, or you can mark the return type nullable [${this.returnType}?]" diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt index a295bcf8..4f696b8e 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressionVisitor.kt @@ -206,7 +206,8 @@ public open class SqlExpressionVisitor { && where === expr.where && orderBy === expr.orderBy && groupBy === expr.groupBy - && having === expr.having) { + && having === expr.having + ) { return expr } else { return expr.copy( diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index a101af8c..67cd6ce2 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -104,7 +104,9 @@ public abstract class SqlFormatter( return true } if (identifier.isMixedCase - && !database.supportsMixedCaseIdentifiers && database.supportsMixedCaseQuotedIdentifiers) { + && !database.supportsMixedCaseIdentifiers + && database.supportsMixedCaseQuotedIdentifiers + ) { return true } return false diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt index 4a17f266..6fecd1d4 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/BaseTable.kt @@ -233,17 +233,19 @@ public abstract class BaseTable( private fun Column.checkConflictBinding(binding: ColumnBinding) { for (column in _columns.values) { val hasConflict = when (binding) { - is NestedBinding -> column.allBindings - .filterIsInstance() - .filter { it.properties == binding.properties } - .any() - is ReferenceBinding -> column.allBindings - .filterIsInstance() - .filter { it.referenceTable.tableName == binding.referenceTable.tableName } - .filter { it.referenceTable.catalog == binding.referenceTable.catalog } - .filter { it.referenceTable.schema == binding.referenceTable.schema } - .filter { it.onProperty == binding.onProperty } - .any() + is NestedBinding -> + column.allBindings + .filterIsInstance() + .filter { it.properties == binding.properties } + .any() + is ReferenceBinding -> + column.allBindings + .filterIsInstance() + .filter { it.referenceTable.tableName == binding.referenceTable.tableName } + .filter { it.referenceTable.catalog == binding.referenceTable.catalog } + .filter { it.referenceTable.schema == binding.referenceTable.schema } + .filter { it.onProperty == binding.onProperty } + .any() } if (hasConflict) { diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt index a8881b0f..e8d1f641 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/BulkInsert.kt @@ -490,14 +490,14 @@ private fun > buildBulkInsertOrUpdateExpression( val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } if (conflictColumns.isEmpty()) { - val msg = + val msg = "" + "Table '$table' doesn't have a primary key, " + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } if (!builder.doNothing && builder.updateAssignments.isEmpty()) { - val msg = + val msg = "" + "Cannot leave the onConflict clause empty! " + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt index f8884899..adbc67bf 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/InsertOrUpdate.kt @@ -261,14 +261,14 @@ private fun > buildInsertOrUpdateExpression( val conflictColumns = builder.conflictColumns.ifEmpty { table.primaryKeys } if (conflictColumns.isEmpty()) { - val msg = + val msg = "" + "Table '$table' doesn't have a primary key, " + "you must specify the conflict columns when calling onConflict(col) { .. }" throw IllegalStateException(msg) } if (!builder.doNothing && builder.updateAssignments.isEmpty()) { - val msg = + val msg = "" + "Cannot leave the onConflict clause empty! " + "If you desire no update action at all please explicitly call `doNothing()`" throw IllegalStateException(msg) From fc8c9390b570ac0bd5569997bf19e477cc063113 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 18:31:00 +0800 Subject: [PATCH 66/96] rm unused constants --- .../org/ktorm/support/mysql/MySqlTest.kt | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index ee2d94e3..6ba72bfb 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -26,16 +26,6 @@ import java.util.concurrent.TimeoutException class MySqlTest : BaseTest() { companion object { - const val TOTAL_RECORDS = 4 - const val MINUS_ONE = -1 - const val ZERO = 0 - const val ONE = 1 - const val TWO = 2 - const val ID_1 = 1 - const val ID_2 = 2 - const val ID_3 = 3 - const val ID_4 = 4 - class KMySqlContainer : MySQLContainer("mysql:8") @ClassRule @@ -99,11 +89,11 @@ class MySqlTest : BaseTest() { */ @Test fun testBothLimitAndOffsetAreNotPositive() { - val query = database.from(Employees).select().orderBy(Employees.id.desc()).limit(ZERO, MINUS_ONE) - assert(query.totalRecords == TOTAL_RECORDS) + val query = database.from(Employees).select().orderBy(Employees.id.desc()).limit(0, -1) + assert(query.totalRecords == 4) val ids = query.map { it[Employees.id] } - assert(ids == listOf(ID_4, ID_3, ID_2, ID_1)) + assert(ids == listOf(4, 3, 2, 1)) } /** @@ -111,11 +101,11 @@ class MySqlTest : BaseTest() { */ @Test fun testLimitWithoutOffset() { - val query = database.from(Employees).select().orderBy(Employees.id.desc()).limit(TWO) - assert(query.totalRecords == TOTAL_RECORDS) + val query = database.from(Employees).select().orderBy(Employees.id.desc()).limit(2) + assert(query.totalRecords == 4) val ids = query.map { it[Employees.id] } - assert(ids == listOf(ID_4, ID_3)) + assert(ids == listOf(4, 3)) } /** @@ -123,11 +113,11 @@ class MySqlTest : BaseTest() { */ @Test fun testOffsetWithoutLimit() { - val query = database.from(Employees).select().orderBy(Employees.id.desc()).offset(TWO) - assert(query.totalRecords == TOTAL_RECORDS) + val query = database.from(Employees).select().orderBy(Employees.id.desc()).offset(2) + assert(query.totalRecords == 4) val ids = query.map { it[Employees.id] } - assert(ids == listOf(ID_2, ID_1)) + assert(ids == listOf(2, 1)) } /** @@ -135,11 +125,11 @@ class MySqlTest : BaseTest() { */ @Test fun testOffsetWithLimit() { - val query = database.from(Employees).select().orderBy(Employees.id.desc()).offset(TWO).limit(ONE) - assert(query.totalRecords == TOTAL_RECORDS) + val query = database.from(Employees).select().orderBy(Employees.id.desc()).offset(2).limit(1) + assert(query.totalRecords == 4) val ids = query.map { it[Employees.id] } - assert(ids == listOf(ID_2)) + assert(ids == listOf(2)) } @Test From 23ca9ea0a55b678df9ccb6acde9b93dc139fc4a7 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 21:12:00 +0800 Subject: [PATCH 67/96] reproduce #252 --- .../ktorm-support-mysql.gradle | 2 +- .../org/ktorm/support/mysql/MySqlTest.kt | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ktorm-support-mysql/ktorm-support-mysql.gradle b/ktorm-support-mysql/ktorm-support-mysql.gradle index 6b401a15..6c5ad2b4 100644 --- a/ktorm-support-mysql/ktorm-support-mysql.gradle +++ b/ktorm-support-mysql/ktorm-support-mysql.gradle @@ -4,6 +4,6 @@ dependencies { testImplementation project(path: ":ktorm-core", configuration: "testOutput") testImplementation project(":ktorm-jackson") - testImplementation "mysql:mysql-connector-java:8.0.13" + testImplementation "mysql:mysql-connector-java:8.0.23" testImplementation "org.testcontainers:mysql:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 6ba72bfb..9f994d93 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -11,10 +11,12 @@ import org.ktorm.jackson.json import org.ktorm.logging.ConsoleLogger import org.ktorm.logging.LogLevel import org.ktorm.schema.Table +import org.ktorm.schema.datetime import org.ktorm.schema.int import org.ktorm.schema.varchar import org.testcontainers.containers.MySQLContainer import java.time.LocalDate +import java.time.LocalDateTime import java.util.concurrent.ExecutionException import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -473,4 +475,29 @@ class MySqlTest : BaseTest() { val name = database.from(t).select(t.col).map { it[t.col] }.first() assert(name == "test") } + + @Test + fun testDateTime() { + val t = object : Table("t_test_datetime") { + val id = int("id").primaryKey() + val d = datetime("d") + } + + database.useConnection { conn -> + conn.createStatement().use { statement -> + val sql = """create table t_test_datetime(id int not null primary key auto_increment, d datetime not null)""" + statement.executeUpdate(sql) + } + } + + val now = LocalDateTime.now().withNano(0) + + database.insert(t) { + set(it.d, now) + } + + val d = database.sequenceOf(t).mapColumns { it.d }.first() + println(d) + assert(d == now) + } } From 473f5c6182aa69df6352da356bbe6d1fcd2d0278 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 29 Apr 2021 22:39:01 +0800 Subject: [PATCH 68/96] full compatible with jsr310 #252 --- build.gradle | 8 +- .../kotlin/org/ktorm/database/CachedRowSet.kt | 122 +++++++++++++++--- 2 files changed, 113 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index 55c9ce83..eb316fa4 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,9 @@ buildscript { detektVersion = "1.15.0" } repositories { - jcenter() + maven { + url 'https://maven.aliyun.com/repository/public/' + } gradlePluginPortal() } dependencies { @@ -28,7 +30,9 @@ subprojects { project -> apply from: "${project.rootDir}/check-source-header.gradle" repositories { - jcenter() + maven { + url 'https://maven.aliyun.com/repository/public/' + } } dependencies { diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt index 553a063b..917fd088 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/CachedRowSet.kt @@ -24,11 +24,7 @@ import java.net.URL import java.sql.* import java.sql.Date import java.sql.ResultSet.* -import java.text.DateFormat -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime +import java.time.* import java.util.* import javax.sql.rowset.serial.* @@ -71,7 +67,28 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { * as a [java.time.LocalDate] object in the Java programming language. */ public fun getLocalDate(columnIndex: Int): LocalDate? { - return getDate(columnIndex)?.toLocalDate() + return when (val value = getColumnValue(columnIndex)) { + null -> null + is Date -> value.toLocalDate() + is java.util.Date -> Date(value.time).toLocalDate() + is LocalDate -> value + is LocalDateTime -> value.toLocalDate() + is ZonedDateTime -> value.toLocalDate() + is OffsetDateTime -> value.toLocalDate() + is Instant -> Date(value.toEpochMilli()).toLocalDate() + is Number -> Date(value.toLong()).toLocalDate() + is String -> { + val number = value.toLongOrNull() + if (number != null) { + Date(number).toLocalDate() + } else { + Date.valueOf(value).toLocalDate() + } + } + else -> { + throw SQLException("Cannot convert ${value.javaClass.name} value to LocalDate.") + } + } } /** @@ -87,7 +104,28 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { * as a [java.time.LocalTime] object in the Java programming language. */ public fun getLocalTime(columnIndex: Int): LocalTime? { - return getTime(columnIndex)?.toLocalTime() + return when (val value = getColumnValue(columnIndex)) { + null -> null + is Time -> value.toLocalTime() + is java.util.Date -> Time(value.time).toLocalTime() + is LocalTime -> value + is LocalDateTime -> value.toLocalTime() + is ZonedDateTime -> value.toLocalTime() + is OffsetDateTime -> value.toLocalTime() + is Instant -> Time(value.toEpochMilli()).toLocalTime() + is Number -> Time(value.toLong()).toLocalTime() + is String -> { + val number = value.toLongOrNull() + if (number != null) { + Time(number).toLocalTime() + } else { + Time.valueOf(value).toLocalTime() + } + } + else -> { + throw SQLException("Cannot convert ${value.javaClass.name} value to LocalTime.") + } + } } /** @@ -103,7 +141,28 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { * as a [java.time.LocalDateTime] object in the Java programming language. */ public fun getLocalDateTime(columnIndex: Int): LocalDateTime? { - return getTimestamp(columnIndex)?.toLocalDateTime() + return when (val value = getColumnValue(columnIndex)) { + null -> null + is Timestamp -> value.toLocalDateTime() + is java.util.Date -> Timestamp(value.time).toLocalDateTime() + is LocalDate -> value.atStartOfDay() + is LocalDateTime -> value + is ZonedDateTime -> value.toLocalDateTime() + is OffsetDateTime -> value.toLocalDateTime() + is Instant -> Timestamp.from(value).toLocalDateTime() + is Number -> Timestamp(value.toLong()).toLocalDateTime() + is String -> { + val number = value.toLongOrNull() + if (number != null) { + Timestamp(number).toLocalDateTime() + } else { + Timestamp.valueOf(value).toLocalDateTime() + } + } + else -> { + throw SQLException("Cannot convert ${value.javaClass.name} value to LocalDateTime.") + } + } } /** @@ -119,7 +178,28 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { * as a [java.time.Instant] object in the Java programming language. */ public fun getInstant(columnIndex: Int): Instant? { - return getTimestamp(columnIndex)?.toInstant() + return when (val value = getColumnValue(columnIndex)) { + null -> null + is Timestamp -> value.toInstant() + is java.util.Date -> value.toInstant() + is Instant -> value + is LocalDate -> Timestamp.valueOf(value.atStartOfDay()).toInstant() + is LocalDateTime -> Timestamp.valueOf(value).toInstant() + is ZonedDateTime -> value.toInstant() + is OffsetDateTime -> value.toInstant() + is Number -> Instant.ofEpochMilli(value.toLong()) + is String -> { + val number = value.toLongOrNull() + if (number != null) { + Instant.ofEpochMilli(number) + } else { + Timestamp.valueOf(value).toInstant() + } + } + else -> { + throw SQLException("Cannot convert ${value.javaClass.name} value to LocalDateTime.") + } + } } /** @@ -326,14 +406,18 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { null -> null is Date -> value.clone() as Date is java.util.Date -> Date(value.time) + is Instant -> Date(value.toEpochMilli()) + is LocalDate -> Date.valueOf(value) + is LocalDateTime -> Date.valueOf(value.toLocalDate()) + is ZonedDateTime -> Date.valueOf(value.toLocalDate()) + is OffsetDateTime -> Date.valueOf(value.toLocalDate()) is Number -> Date(value.toLong()) is String -> { val number = value.toLongOrNull() if (number != null) { Date(number) } else { - val date = DateFormat.getDateInstance().parse(value) - Date(date.time) + Date.valueOf(value) } } else -> { @@ -347,14 +431,18 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { null -> null is Time -> value.clone() as Time is java.util.Date -> Time(value.time) + is Instant -> Time(value.toEpochMilli()) + is LocalTime -> Time.valueOf(value) + is LocalDateTime -> Time.valueOf(value.toLocalTime()) + is ZonedDateTime -> Time.valueOf(value.toLocalTime()) + is OffsetDateTime -> Time.valueOf(value.toLocalTime()) is Number -> Time(value.toLong()) is String -> { val number = value.toLongOrNull() if (number != null) { Time(number) } else { - val date = DateFormat.getTimeInstance().parse(value) - Time(date.time) + Time.valueOf(value) } } else -> { @@ -368,14 +456,18 @@ public open class CachedRowSet(rs: ResultSet) : ResultSet { null -> null is Timestamp -> value.clone() as Timestamp is java.util.Date -> Timestamp(value.time) + is Instant -> Timestamp.from(value) + is LocalDate -> Timestamp.valueOf(value.atStartOfDay()) + is LocalDateTime -> Timestamp.valueOf(value) + is ZonedDateTime -> Timestamp.from(value.toInstant()) + is OffsetDateTime -> Timestamp.from(value.toInstant()) is Number -> Timestamp(value.toLong()) is String -> { val number = value.toLongOrNull() if (number != null) { Timestamp(number) } else { - val date = DateFormat.getDateTimeInstance().parse(value) - Timestamp(date.time) + Timestamp.valueOf(value) } } else -> { From 8fdb330c1bd356b8c76f0c85f9131e85232521ef Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Fri, 30 Apr 2021 01:24:04 +0800 Subject: [PATCH 69/96] compatible with unsigned types --- .../org/ktorm/schema/ColumnBindingHandler.kt | 6 +++ .../kotlin/org/ktorm/database/DatabaseTest.kt | 39 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt index b7d9b230..d67864df 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt @@ -32,6 +32,7 @@ import kotlin.reflect.jvm.javaSetter import kotlin.reflect.jvm.jvmErasure @PublishedApi +@OptIn(ExperimentalUnsignedTypes::class) internal class ColumnBindingHandler(val properties: MutableList>) : InvocationHandler { override fun invoke(proxy: Any, method: Method, args: Array?): Any? { @@ -54,6 +55,10 @@ internal class ColumnBindingHandler(val properties: MutableList return when { returnType.isSubclassOf(Entity::class) -> createProxy(returnType, properties) returnType.java.isPrimitive -> returnType.defaultValue + returnType == UByte::class -> 0.toByte() + returnType == UShort::class -> 0.toShort() + returnType == UInt::class -> 0 + returnType == ULong::class -> 0L else -> null } } @@ -85,6 +90,7 @@ internal val Method.kotlinProperty: Pair, Boolean>? get() { return null } +// should use java Class instead of KClass to avoid inline class issues. internal val KClass<*>.defaultValue: Any get() { val value = when { this == Boolean::class -> false diff --git a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt index 695a1c41..3979487c 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt @@ -3,14 +3,19 @@ package org.ktorm.database import org.junit.Test import org.ktorm.BaseTest import org.ktorm.dsl.* +import org.ktorm.entity.Entity import org.ktorm.entity.count +import org.ktorm.entity.mapColumns import org.ktorm.entity.sequenceOf -import org.ktorm.schema.Table -import org.ktorm.schema.varchar +import org.ktorm.schema.* +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.Types /** * Created by vince on Dec 02, 2018. */ +@ExperimentalUnsignedTypes class DatabaseTest : BaseTest() { @Test @@ -90,4 +95,34 @@ class DatabaseTest : BaseTest() { assert(names[0] == "vince") assert(names[1] == "marry") } + + fun BaseTable<*>.ulong(name: String): Column { + return registerColumn(name, object : SqlType(Types.BIGINT, "bigint unsigned") { + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: ULong) { + ps.setLong(index, parameter.toLong()) + } + + override fun doGetResult(rs: ResultSet, index: Int): ULong? { + return rs.getLong(index).toULong() + } + }) + } + + interface Emp : Entity { + companion object : Entity.Factory() + val id: ULong + } + + @Test + fun testULong() { + val t = object : Table("t_employee") { + val id = ulong("id").primaryKey().bindTo { it.id } + } + + val ids = database.sequenceOf(t).mapColumns { it.id } + println(ids) + assert(ids == listOf(1.toULong(), 2.toULong(), 3.toULong(), 4.toULong())) + + assert(Emp().id == 0.toULong()) + } } \ No newline at end of file From 67bf7968cf06f461e513f3cf7744a6e393c71e2c Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 2 May 2021 00:31:52 +0800 Subject: [PATCH 70/96] fix #253 --- build.gradle | 2 +- .../src/main/kotlin/org/ktorm/dsl/Dml.kt | 2 +- .../main/kotlin/org/ktorm/entity/EntityDml.kt | 8 +- .../org/ktorm/entity/EntityExtensions.kt | 16 ++-- .../org/ktorm/entity/EntityImplementation.kt | 86 +++++++++++++---- .../org/ktorm/schema/ColumnBindingHandler.kt | 71 +++++++------- .../kotlin/org/ktorm/database/DatabaseTest.kt | 92 ++++++++++++++++--- 7 files changed, 194 insertions(+), 83 deletions(-) diff --git a/build.gradle b/build.gradle index eb316fa4..154fcd24 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - kotlinVersion = "1.4.21" + kotlinVersion = "1.4.32" detektVersion = "1.15.0" } repositories { diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt index 21d6d622..ad226c97 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Dml.kt @@ -352,7 +352,7 @@ public open class AssignmentsBuilder { if (method.returnType == Void.TYPE || !method.returnType.isPrimitive) { null } else { - method.returnType.kotlin.defaultValue + method.returnType.defaultValue } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt index 4c6e619b..33e08c1c 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt @@ -246,7 +246,7 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map { if (binding.onProperty.name in changedProperties) { - val child = this.getProperty(binding.onProperty.name) as Entity<*>? + val child = this.getProperty(binding.onProperty) as Entity<*>? assignments[column] = child?.implementation?.getPrimaryKeyValue(binding.referenceTable as Table<*>) } } @@ -265,7 +265,7 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map) } } - curr = curr.getProperty(prop.name) + curr = curr.getProperty(prop) } } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt index d78469ec..c4a4620b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt @@ -33,18 +33,18 @@ internal fun EntityImplementation.getPrimaryKeyValue(fromTable: Table<*>): Any? internal fun EntityImplementation.getColumnValue(binding: ColumnBinding): Any? { when (binding) { is ReferenceBinding -> { - val child = this.getProperty(binding.onProperty.name) as Entity<*>? + val child = this.getProperty(binding.onProperty) as Entity<*>? return child?.implementation?.getPrimaryKeyValue(binding.referenceTable as Table<*>) } is NestedBinding -> { var curr: EntityImplementation? = this for ((i, prop) in binding.properties.withIndex()) { if (i != binding.properties.lastIndex) { - val child = curr?.getProperty(prop.name) as Entity<*>? + val child = curr?.getProperty(prop) as Entity<*>? curr = child?.implementation } } - return curr?.getProperty(binding.properties.last().name) + return curr?.getProperty(binding.properties.last()) } } } @@ -72,14 +72,14 @@ internal fun EntityImplementation.setPrimaryKeyValue( internal fun EntityImplementation.setColumnValue(binding: ColumnBinding, value: Any?, forceSet: Boolean = false) { when (binding) { is ReferenceBinding -> { - var child = this.getProperty(binding.onProperty.name) as Entity<*>? + var child = this.getProperty(binding.onProperty) as Entity<*>? if (child == null) { child = Entity.create( entityClass = binding.onProperty.returnType.jvmErasure, fromDatabase = this.fromDatabase, fromTable = binding.referenceTable as Table<*> ) - this.setProperty(binding.onProperty.name, child, forceSet) + this.setProperty(binding.onProperty, child, forceSet) } val refTable = binding.referenceTable as Table<*> @@ -89,17 +89,17 @@ internal fun EntityImplementation.setColumnValue(binding: ColumnBinding, value: var curr: EntityImplementation = this for ((i, prop) in binding.properties.withIndex()) { if (i != binding.properties.lastIndex) { - var child = curr.getProperty(prop.name) as Entity<*>? + var child = curr.getProperty(prop) as Entity<*>? if (child == null) { child = Entity.create(prop.returnType.jvmErasure, parent = curr) - curr.setProperty(prop.name, child, forceSet) + curr.setProperty(prop, child, forceSet) } curr = child.implementation } } - curr.setProperty(binding.properties.last().name, value, forceSet) + curr.setProperty(binding.properties.last(), value, forceSet) } } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index dcb0789f..9feb7f4b 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -29,6 +29,7 @@ import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashSet import kotlin.reflect.KClass import kotlin.reflect.KProperty1 +import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmName import kotlin.reflect.jvm.kotlinFunction @@ -65,8 +66,8 @@ internal class EntityImplementation( "flushChanges" -> this.doFlushChanges() "discardChanges" -> this.doDiscardChanges() "delete" -> this.doDelete() - "get" -> this.getProperty(args!![0] as String) - "set" -> this.setProperty(args!![0] as String, args[1]) + "get" -> this.doGetProperty(args!![0] as String) + "set" -> this.doSetProperty(args!![0] as String, args[1]) "copy" -> this.copy() else -> throw IllegalStateException("Unrecognized method: $method") } @@ -83,14 +84,14 @@ internal class EntityImplementation( val (prop, isGetter) = ktProp if (prop.isAbstract) { if (isGetter) { - val result = this.getProperty(prop.name) + val result = this.getProperty(prop, unboxInlineValues = true) if (result != null || prop.returnType.isMarkedNullable) { return result } else { - return prop.defaultValue.also { cacheDefaultValue(prop, it) } + return method.defaultReturnValue.also { cacheDefaultValue(prop, it) } } } else { - this.setProperty(prop.name, args!![0]) + this.setProperty(prop, args!![0]) return null } } else { @@ -106,9 +107,9 @@ internal class EntityImplementation( } } - private val KProperty1<*, *>.defaultValue: Any get() { + private val Method.defaultReturnValue: Any get() { try { - return returnType.jvmErasure.defaultValue + return returnType.defaultValue } catch (e: Throwable) { val msg = "" + "The value of non-null property [$this] doesn't exist, " + @@ -119,19 +120,19 @@ internal class EntityImplementation( } private fun cacheDefaultValue(prop: KProperty1<*, *>, value: Any) { - val type = prop.returnType.jvmErasure + val type = prop.javaGetter!!.returnType // Skip for primitive types, enums and string, because their default values always share the same instance. - if (type == Boolean::class) return - if (type == Char::class) return - if (type == Byte::class) return - if (type == Short::class) return - if (type == Int::class) return - if (type == Long::class) return - if (type == String::class) return - if (type.java.isEnum) return + if (type == Boolean::class.javaPrimitiveType) return + if (type == Char::class.javaPrimitiveType) return + if (type == Byte::class.javaPrimitiveType) return + if (type == Short::class.javaPrimitiveType) return + if (type == Int::class.javaPrimitiveType) return + if (type == Long::class.javaPrimitiveType) return + if (type == String::class.java) return + if (type.isEnum) return - setProperty(prop.name, value) + setProperty(prop, value) } @Suppress("SwallowedException") @@ -152,11 +153,58 @@ internal class EntityImplementation( } } - fun getProperty(name: String): Any? { + @OptIn(ExperimentalUnsignedTypes::class) + fun getProperty(prop: KProperty1<*, *>, unboxInlineValues: Boolean = false): Any? { + if (!unboxInlineValues) { + return doGetProperty(prop.name) + } + + val returnType = prop.javaGetter!!.returnType + val value = doGetProperty(prop.name) + + // Unbox inline class values if necessary. + // In principle, we need to check for all inline classes, but kotlin-reflect is still unable to determine + // whether a class is inline, so as a workaround, we have to enumerate some common-used types here. + return when { + value is UByte && returnType == Byte::class.javaPrimitiveType -> value.toByte() + value is UShort && returnType == Short::class.javaPrimitiveType -> value.toShort() + value is UInt && returnType == Int::class.javaPrimitiveType -> value.toInt() + value is ULong && returnType == Long::class.javaPrimitiveType -> value.toLong() + value is UByteArray && returnType == ByteArray::class.java -> value.toByteArray() + value is UShortArray && returnType == ShortArray::class.java -> value.toShortArray() + value is UIntArray && returnType == IntArray::class.java -> value.toIntArray() + value is ULongArray && returnType == LongArray::class.java -> value.toLongArray() + else -> value + } + } + + private fun doGetProperty(name: String): Any? { return values[name] } - fun setProperty(name: String, value: Any?, forceSet: Boolean = false) { + @OptIn(ExperimentalUnsignedTypes::class) + fun setProperty(prop: KProperty1<*, *>, value: Any?, forceSet: Boolean = false) { + val propType = prop.returnType.jvmErasure + + // For inline classes, always box the underlying values as wrapper types. + // In principle, we need to check for all inline classes, but kotlin-reflect is still unable to determine + // whether a class is inline, so as a workaround, we have to enumerate some common-used types here. + val boxedValue = when { + propType == UByte::class && value is Byte -> value.toUByte() + propType == UShort::class && value is Short -> value.toUShort() + propType == UInt::class && value is Int -> value.toUInt() + propType == ULong::class && value is Long -> value.toULong() + propType == UByteArray::class && value is ByteArray -> value.toUByteArray() + propType == UShortArray::class && value is ShortArray -> value.toUShortArray() + propType == UIntArray::class && value is IntArray -> value.toUIntArray() + propType == ULongArray::class && value is LongArray -> value.toULongArray() + else -> value + } + + doSetProperty(prop.name, boxedValue, forceSet) + } + + private fun doSetProperty(name: String, value: Any?, forceSet: Boolean = false) { if (!forceSet && isPrimaryKey(name) && name in values) { val msg = "Cannot modify the primary key `$name` because it's already set to ${values[name]}" throw UnsupportedOperationException(msg) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt index d67864df..bf507565 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt @@ -24,15 +24,12 @@ import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty1 -import kotlin.reflect.full.createInstance import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.isSubclassOf import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.javaSetter -import kotlin.reflect.jvm.jvmErasure @PublishedApi -@OptIn(ExperimentalUnsignedTypes::class) internal class ColumnBindingHandler(val properties: MutableList>) : InvocationHandler { override fun invoke(proxy: Any, method: Method, args: Array?): Any? { @@ -51,14 +48,10 @@ internal class ColumnBindingHandler(val properties: MutableList properties += prop - val returnType = prop.returnType.jvmErasure + val returnType = method.returnType return when { - returnType.isSubclassOf(Entity::class) -> createProxy(returnType, properties) - returnType.java.isPrimitive -> returnType.defaultValue - returnType == UByte::class -> 0.toByte() - returnType == UShort::class -> 0.toShort() - returnType == UInt::class -> 0 - returnType == ULong::class -> 0L + returnType.kotlin.isSubclassOf(Entity::class) -> createProxy(returnType.kotlin, properties) + returnType.isPrimitive -> returnType.defaultValue else -> null } } @@ -90,39 +83,43 @@ internal val Method.kotlinProperty: Pair, Boolean>? get() { return null } -// should use java Class instead of KClass to avoid inline class issues. -internal val KClass<*>.defaultValue: Any get() { +@OptIn(ExperimentalUnsignedTypes::class) +internal val Class<*>.defaultValue: Any get() { val value = when { - this == Boolean::class -> false - this == Char::class -> 0.toChar() - this == Byte::class -> 0.toByte() - this == Short::class -> 0.toShort() - this == Int::class -> 0 - this == Long::class -> 0L - this == Float::class -> 0.0F - this == Double::class -> 0.0 - this == String::class -> "" - this.isSubclassOf(Entity::class) -> Entity.create(this) - this.java.isEnum -> this.java.enumConstants[0] - this.java.isArray -> this.java.componentType.createArray(0) - this == Set::class || this == MutableSet::class -> LinkedHashSet() - this == List::class || this == MutableList::class -> ArrayList() - this == Collection::class || this == MutableCollection::class -> ArrayList() - this == Map::class || this == MutableMap::class -> LinkedHashMap() - this == Queue::class || this == Deque::class -> LinkedList() - this == SortedSet::class || this == NavigableSet::class -> TreeSet() - this == SortedMap::class || this == NavigableMap::class -> TreeMap() - else -> this.createInstance() + this == Boolean::class.javaPrimitiveType -> false + this == Char::class.javaPrimitiveType -> 0.toChar() + this == Byte::class.javaPrimitiveType -> 0.toByte() + this == Short::class.javaPrimitiveType -> 0.toShort() + this == Int::class.javaPrimitiveType -> 0 + this == Long::class.javaPrimitiveType -> 0L + this == Float::class.javaPrimitiveType -> 0.0F + this == Double::class.javaPrimitiveType -> 0.0 + this == String::class.java -> "" + this == UByte::class.java -> 0.toUByte() + this == UShort::class.java -> 0.toUShort() + this == UInt::class.java -> 0U + this == ULong::class.java -> 0UL + this == UByteArray::class.java -> ubyteArrayOf() + this == UShortArray::class.java -> ushortArrayOf() + this == UIntArray::class.java -> uintArrayOf() + this == ULongArray::class.java -> ulongArrayOf() + this == Set::class.java -> LinkedHashSet() + this == List::class.java -> ArrayList() + this == Collection::class.java -> ArrayList() + this == Map::class.java -> LinkedHashMap() + this == Queue::class.java || this == Deque::class.java -> LinkedList() + this == SortedSet::class.java || this == NavigableSet::class.java -> TreeSet() + this == SortedMap::class.java || this == NavigableMap::class.java -> TreeMap() + this.isEnum -> this.enumConstants[0] + this.isArray -> java.lang.reflect.Array.newInstance(this.componentType, 0) + this.kotlin.isSubclassOf(Entity::class) -> Entity.create(this.kotlin) + else -> this.newInstance() } - if (this.isInstance(value)) { + if (this.kotlin.isInstance(value)) { return value } else { // never happens... throw AssertionError("$value must be instance of $this") } } - -private fun Class<*>.createArray(length: Int): Any { - return java.lang.reflect.Array.newInstance(this, length) -} diff --git a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt index 3979487c..27d373da 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/database/DatabaseTest.kt @@ -3,10 +3,7 @@ package org.ktorm.database import org.junit.Test import org.ktorm.BaseTest import org.ktorm.dsl.* -import org.ktorm.entity.Entity -import org.ktorm.entity.count -import org.ktorm.entity.mapColumns -import org.ktorm.entity.sequenceOf +import org.ktorm.entity.* import org.ktorm.schema.* import java.sql.PreparedStatement import java.sql.ResultSet @@ -108,21 +105,90 @@ class DatabaseTest : BaseTest() { }) } - interface Emp : Entity { - companion object : Entity.Factory() - val id: ULong + interface TestUnsigned : Entity { + companion object : Entity.Factory() + var id: ULong } @Test - fun testULong() { - val t = object : Table("t_employee") { - val id = ulong("id").primaryKey().bindTo { it.id } + fun testUnsigned() { + val t = object : Table("T_TEST_UNSIGNED") { + val id = ulong("ID").primaryKey().bindTo { it.id } } - val ids = database.sequenceOf(t).mapColumns { it.id } + database.useConnection { conn -> + conn.createStatement().use { statement -> + val sql = """CREATE TABLE T_TEST_UNSIGNED(ID BIGINT UNSIGNED NOT NULL PRIMARY KEY)""" + statement.executeUpdate(sql) + } + } + + val unsigned = TestUnsigned { id = 5UL } + assert(unsigned.id == 5UL) + database.sequenceOf(t).add(unsigned) + + val ids = database.sequenceOf(t).toList().map { it.id } println(ids) - assert(ids == listOf(1.toULong(), 2.toULong(), 3.toULong(), 4.toULong())) + assert(ids == listOf(5UL)) - assert(Emp().id == 0.toULong()) + database.insert(t) { + set(it.id, 6UL) + } + + val ids2 = database.from(t).select(t.id).map { row -> row[t.id] } + println(ids2) + assert(ids2 == listOf(5UL, 6UL)) + + assert(TestUnsigned().id == 0UL) + } + + interface TestUnsignedNullable : Entity { + companion object : Entity.Factory() + var id: ULong? + } + + @Test + fun testUnsignedNullable() { + val t = object : Table("T_TEST_UNSIGNED_NULLABLE") { + val id = ulong("ID").primaryKey().bindTo { it.id } + } + + database.useConnection { conn -> + conn.createStatement().use { statement -> + val sql = """CREATE TABLE T_TEST_UNSIGNED_NULLABLE(ID BIGINT UNSIGNED NOT NULL PRIMARY KEY)""" + statement.executeUpdate(sql) + } + } + + val unsigned = TestUnsignedNullable { id = 5UL } + assert(unsigned.id == 5UL) + database.sequenceOf(t).add(unsigned) + + val ids = database.sequenceOf(t).toList().map { it.id } + println(ids) + assert(ids == listOf(5UL)) + + assert(TestUnsignedNullable().id == null) + } + + @Test + fun testDefaultValueReferenceEquality() { + assert(Boolean::class.javaPrimitiveType!!.defaultValue === Boolean::class.javaPrimitiveType!!.defaultValue) + assert(Char::class.javaPrimitiveType!!.defaultValue === Char::class.javaPrimitiveType!!.defaultValue) + assert(Byte::class.javaPrimitiveType!!.defaultValue === Byte::class.javaPrimitiveType!!.defaultValue) + assert(Short::class.javaPrimitiveType!!.defaultValue === Short::class.javaPrimitiveType!!.defaultValue) + assert(Int::class.javaPrimitiveType!!.defaultValue === Int::class.javaPrimitiveType!!.defaultValue) + assert(Long::class.javaPrimitiveType!!.defaultValue === Long::class.javaPrimitiveType!!.defaultValue) + assert(Float::class.javaPrimitiveType!!.defaultValue !== Float::class.javaPrimitiveType!!.defaultValue) + assert(Double::class.javaPrimitiveType!!.defaultValue !== Double::class.javaPrimitiveType!!.defaultValue) + assert(String::class.java.defaultValue === String::class.java.defaultValue) + assert(UByte::class.java.defaultValue !== UByte::class.java.defaultValue) + assert(UShort::class.java.defaultValue !== UShort::class.java.defaultValue) + assert(UInt::class.java.defaultValue !== UInt::class.java.defaultValue) + assert(ULong::class.java.defaultValue !== ULong::class.java.defaultValue) + assert(UByteArray::class.java.defaultValue !== UByteArray::class.java.defaultValue) + assert(UShortArray::class.java.defaultValue !== UShortArray::class.java.defaultValue) + assert(UIntArray::class.java.defaultValue !== UIntArray::class.java.defaultValue) + assert(ULongArray::class.java.defaultValue !== ULongArray::class.java.defaultValue) } } \ No newline at end of file From 56a296c95953ee8561b27f3a86caecfed09c7715 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 2 May 2021 14:30:55 +0800 Subject: [PATCH 71/96] support inline class json serialization #253 --- .../org/ktorm/entity/EntityImplementation.kt | 10 +++------- ktorm-jackson/ktorm-jackson.gradle | 6 +++--- .../org/ktorm/jackson/EntityDeserializers.kt | 3 +-- .../org/ktorm/jackson/EntitySerializers.kt | 5 +++-- .../org/ktorm/jackson/JacksonExtensions.kt | 19 +++++++++++++++--- .../kotlin/org/ktorm/jackson/JacksonTest.kt | 20 +++++++++++++++---- 6 files changed, 42 insertions(+), 21 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index 9feb7f4b..91600ecd 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -66,7 +66,7 @@ internal class EntityImplementation( "flushChanges" -> this.doFlushChanges() "discardChanges" -> this.doDiscardChanges() "delete" -> this.doDelete() - "get" -> this.doGetProperty(args!![0] as String) + "get" -> this.values[args!![0] as String] "set" -> this.doSetProperty(args!![0] as String, args[1]) "copy" -> this.copy() else -> throw IllegalStateException("Unrecognized method: $method") @@ -156,11 +156,11 @@ internal class EntityImplementation( @OptIn(ExperimentalUnsignedTypes::class) fun getProperty(prop: KProperty1<*, *>, unboxInlineValues: Boolean = false): Any? { if (!unboxInlineValues) { - return doGetProperty(prop.name) + return values[prop.name] } val returnType = prop.javaGetter!!.returnType - val value = doGetProperty(prop.name) + val value = values[prop.name] // Unbox inline class values if necessary. // In principle, we need to check for all inline classes, but kotlin-reflect is still unable to determine @@ -178,10 +178,6 @@ internal class EntityImplementation( } } - private fun doGetProperty(name: String): Any? { - return values[name] - } - @OptIn(ExperimentalUnsignedTypes::class) fun setProperty(prop: KProperty1<*, *>, value: Any?, forceSet: Boolean = false) { val propType = prop.returnType.jvmErasure diff --git a/ktorm-jackson/ktorm-jackson.gradle b/ktorm-jackson/ktorm-jackson.gradle index fad943bb..59de4c52 100644 --- a/ktorm-jackson/ktorm-jackson.gradle +++ b/ktorm-jackson/ktorm-jackson.gradle @@ -17,7 +17,7 @@ compileKotlin.dependsOn(generatePackageVersion) dependencies { api project(":ktorm-core") - api "com.fasterxml.jackson.core:jackson-databind:2.9.7" - api "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7" - api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.7" + api "com.fasterxml.jackson.core:jackson-databind:2.12.3" + api "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3" + api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3" } \ No newline at end of file diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt index 75bc4d6f..5ab6bd9d 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntityDeserializers.kt @@ -29,7 +29,6 @@ import kotlin.reflect.KClass import kotlin.reflect.KProperty1 import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.javaGetter /** * Created by vince on Aug 13, 2018. @@ -97,7 +96,7 @@ internal class EntityDeserializers : SimpleDeserializers() { parser.nextToken() // skip to field value if (prop != null) { - val propType = ctxt.constructType(prop.javaGetter!!.genericReturnType) + val propType = ctxt.constructType(prop.getPropertyType()) intoValue[prop.name] = parser.codec.readValue(parser, propType) } else { if (parser.currentToken.isStructStart) { diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt index dd90354b..09f085dd 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt @@ -27,7 +27,6 @@ import com.fasterxml.jackson.databind.module.SimpleSerializers import org.ktorm.entity.Entity import kotlin.reflect.KProperty1 import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.javaGetter /** * Created by vince on Aug 13, 2018. @@ -92,13 +91,15 @@ internal class EntitySerializers : SimpleSerializers() { if (value == null) { gen.writeNull() } else { - val propType = serializers.constructType(prop.javaGetter!!.genericReturnType) + val propType = serializers.constructType(prop.getPropertyType()) val ser = serializers.findTypedValueSerializer(propType, true, null) ser.serialize(value, gen, serializers) } } } + + override fun serializeWithType( entity: Entity<*>, gen: JsonGenerator, diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt index f987741a..8f66bb08 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JacksonExtensions.kt @@ -26,9 +26,7 @@ import com.fasterxml.jackson.databind.cfg.MapperConfig import com.fasterxml.jackson.databind.introspect.AnnotatedMethod import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty1 -import kotlin.reflect.jvm.javaField -import kotlin.reflect.jvm.javaGetter -import kotlin.reflect.jvm.javaSetter +import kotlin.reflect.jvm.* internal fun JsonGenerator.configureIndentOutputIfEnabled() { val codec = this.codec @@ -112,3 +110,18 @@ internal inline fun KProperty1<*, *>.findAnnotationForD return annotation } + +@OptIn(ExperimentalUnsignedTypes::class) +internal fun KProperty1<*, *>.getPropertyType(): java.lang.reflect.Type { + return when (returnType.jvmErasure) { + UByte::class -> UByte::class.java + UShort::class -> UShort::class.java + UInt::class -> UInt::class.java + ULong::class -> ULong::class.java + UByteArray::class -> UByteArray::class.java + UShortArray::class -> UShortArray::class.java + UIntArray::class -> UIntArray::class.java + ULongArray::class -> ULongArray::class.java + else -> returnType.javaType + } +} diff --git a/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonTest.kt b/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonTest.kt index 658be099..c5b97a83 100644 --- a/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonTest.kt +++ b/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonTest.kt @@ -11,16 +11,18 @@ import java.math.BigInteger /** * Created by vince on Dec 09, 2018. */ +@ExperimentalUnsignedTypes class JacksonTest { private val objectMapper = ObjectMapper() .configure(SerializationFeature.INDENT_OUTPUT, true) .findAndRegisterModules() - private val typedObjectMapper = ObjectMapper() - .enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY) - .configure(SerializationFeature.INDENT_OUTPUT, true) - .findAndRegisterModules() + private val typedObjectMapper = ObjectMapper().apply { + activateDefaultTyping(polymorphicTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY) + configure(SerializationFeature.INDENT_OUTPUT, true) + findAndRegisterModules() + } private val separator = "$" @@ -30,6 +32,8 @@ class JacksonTest { short = 2 int = 3 long = 4 + ulong = 123U + ulong0 = 1230U float = 5.0F double = 6.0 bigInteger = BigInteger("7") @@ -93,6 +97,8 @@ class JacksonTest { assert(f.short == foo.short) assert(f.int == foo.int) assert(f.long == foo.long) + assert(f.ulong == foo.ulong) + assert(f.ulong0 == foo.ulong0) assert(f.float == foo.float) assert(f.double == foo.double) assert(f.bigInteger == foo.bigInteger) @@ -135,6 +141,8 @@ class JacksonTest { "short" : 2, "int" : 3, "long" : 4, + "ulong" : 123, + "ulong0" : 1230, "float" : 5.0, "double" : 6.0, "bigInteger" : 7, @@ -199,6 +207,8 @@ class JacksonTest { "short" : 2, "int" : 3, "long" : 4, + "ulong" : 123, + "ulong0" : 1230, "float" : 5.0, "double" : 6.0, "bigInteger" : [ "java.math.BigInteger", 7 ], @@ -333,6 +343,8 @@ class JacksonTest { var short: Short var int: Int var long: Long + var ulong: ULong + var ulong0: ULong? var float: Float var double: Double var bigInteger: BigInteger From 3913e22f62b8b93793a4e03c85490edce27cb24f Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 2 May 2021 14:37:49 +0800 Subject: [PATCH 72/96] rm unused lines --- .../src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt index 09f085dd..a44ed933 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/EntitySerializers.kt @@ -98,8 +98,6 @@ internal class EntitySerializers : SimpleSerializers() { } } - - override fun serializeWithType( entity: Entity<*>, gen: JsonGenerator, From a7e80da2f733956d0d94e89d10f293ae1ba73192 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 2 May 2021 15:58:45 +0800 Subject: [PATCH 73/96] fix postgresql json bug #268 --- ktorm-jackson/ktorm-jackson.gradle | 1 + .../kotlin/org/ktorm/jackson/JsonSqlType.kt | 15 ++++++++- .../ktorm-support-postgresql.gradle | 1 + .../support/postgresql/PostgreSqlTest.kt | 31 ++++++++++++++++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/ktorm-jackson/ktorm-jackson.gradle b/ktorm-jackson/ktorm-jackson.gradle index 59de4c52..28f2a8b4 100644 --- a/ktorm-jackson/ktorm-jackson.gradle +++ b/ktorm-jackson/ktorm-jackson.gradle @@ -20,4 +20,5 @@ dependencies { api "com.fasterxml.jackson.core:jackson-databind:2.12.3" api "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3" api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3" + compileOnly "org.postgresql:postgresql:42.2.5" } \ No newline at end of file diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt index 99aa3155..a0ed8e98 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt @@ -21,6 +21,8 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import org.ktorm.schema.* +import org.postgresql.PGStatement +import org.postgresql.util.PGobject import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.Types @@ -78,8 +80,19 @@ public class JsonSqlType( public val javaType: JavaType ) : SqlType(Types.VARCHAR, "json") { + private val hasPostgresqlDriver by lazy { + runCatching { Class.forName("org.postgresql.Driver") }.isSuccess + } + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: T) { - ps.setString(index, objectMapper.writeValueAsString(parameter)) + if (hasPostgresqlDriver && ps is PGStatement) { + val obj = PGobject() + obj.type = "json" + obj.value = objectMapper.writeValueAsString(parameter) + ps.setObject(index, obj) + } else { + ps.setString(index, objectMapper.writeValueAsString(parameter)) + } } override fun doGetResult(rs: ResultSet, index: Int): T? { diff --git a/ktorm-support-postgresql/ktorm-support-postgresql.gradle b/ktorm-support-postgresql/ktorm-support-postgresql.gradle index a7c8e612..839f2ec1 100644 --- a/ktorm-support-postgresql/ktorm-support-postgresql.gradle +++ b/ktorm-support-postgresql/ktorm-support-postgresql.gradle @@ -3,6 +3,7 @@ dependencies { api project(":ktorm-core") testImplementation project(path: ":ktorm-core", configuration: "testOutput") + testImplementation project(":ktorm-jackson") testImplementation "org.postgresql:postgresql:42.2.5" testImplementation "org.testcontainers:postgresql:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 641956ee..ca6f0f8f 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -2,7 +2,7 @@ package org.ktorm.support.postgresql import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.nullValue -import org.junit.Assert.assertThat +import org.hamcrest.MatcherAssert.assertThat import org.junit.ClassRule import org.junit.Test import org.ktorm.BaseTest @@ -10,6 +10,7 @@ import org.ktorm.database.Database import org.ktorm.database.use import org.ktorm.dsl.* import org.ktorm.entity.* +import org.ktorm.jackson.json import org.ktorm.logging.ConsoleLogger import org.ktorm.logging.LogLevel import org.ktorm.schema.ColumnDeclaring @@ -796,4 +797,32 @@ class PostgreSqlTest : BaseTest() { assertThat(count, equalTo(1)) } + + + @Test + fun testJson() { + val t = object : Table("t_json") { + val obj = json("obj") + val arr = json>("arr") + } + + database.useConnection { conn -> + conn.createStatement().use { statement -> + val sql = """create table t_json (obj json, arr json)""" + statement.executeUpdate(sql) + } + } + + database.insert(t) { + set(it.obj, Employee { name = "vince"; salary = 100 }) + set(it.arr, listOf(1, 2, 3)) + } + + database + .from(t) + .select(t.obj, t.arr) + .forEach { row -> + println("${row.getString(1)}:${row.getString(2)}") + } + } } \ No newline at end of file From f9b7e5b3a97e783c450e643e43cb246590627f48 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 2 May 2021 16:30:54 +0800 Subject: [PATCH 74/96] deprecate pgEnum --- ktorm-core/ktorm-core.gradle | 1 + .../main/kotlin/org/ktorm/schema/SqlTypes.kt | 21 +++++++++++++---- .../org/ktorm/support/postgresql/SqlTypes.kt | 9 ++++++++ .../support/postgresql/PostgreSqlTest.kt | 23 +++++++------------ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/ktorm-core/ktorm-core.gradle b/ktorm-core/ktorm-core.gradle index bf8be8a5..b8c815c4 100644 --- a/ktorm-core/ktorm-core.gradle +++ b/ktorm-core/ktorm-core.gradle @@ -7,6 +7,7 @@ dependencies { compileOnly "com.google.android:android:1.5_r4" compileOnly "org.springframework:spring-jdbc:5.0.10.RELEASE" compileOnly "org.springframework:spring-tx:5.0.10.RELEASE" + compileOnly "org.postgresql:postgresql:42.2.5" testImplementation "com.h2database:h2:1.4.197" } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt index 2dcbc9eb..7dcd8d55 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt @@ -16,6 +16,7 @@ package org.ktorm.schema +import org.postgresql.PGStatement import java.math.BigDecimal import java.sql.* import java.sql.Date @@ -500,15 +501,27 @@ public inline fun > BaseTable<*>.enum(name: String): Column< * * @property enumClass the enum class. */ -public class EnumSqlType>(public val enumClass: Class) : SqlType(Types.VARCHAR, "varchar") { - private val valueOf = enumClass.getDeclaredMethod("valueOf", String::class.java) +public class EnumSqlType>(public val enumClass: Class) : SqlType(Types.VARCHAR, "enum") { + + private val hasPostgresqlDriver by lazy { + runCatching { Class.forName("org.postgresql.Driver") }.isSuccess + } override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: C) { - ps.setString(index, parameter.name) + if (hasPostgresqlDriver && ps is PGStatement) { + ps.setObject(index, parameter.name, Types.OTHER) + } else { + ps.setString(index, parameter.name) + } } override fun doGetResult(rs: ResultSet, index: Int): C? { - return rs.getString(index)?.takeIf { it.isNotBlank() }?.let { enumClass.cast(valueOf(null, it)) } + val value = rs.getString(index) + if (value.isNullOrBlank()) { + return null + } else { + return java.lang.Enum.valueOf(enumClass, value) + } } } diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt index aad6c9b3..411a982e 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/SqlTypes.kt @@ -89,6 +89,11 @@ public object TextArraySqlType : SqlType(Types.ARRAY, "text[]") { * @param name the column's name. * @return the registered column. */ +@Suppress("DEPRECATION") +@Deprecated( + message = "Will remove in the future, please use `enum` instead", + replaceWith = ReplaceWith(expression = "enum(name)", imports = ["org.ktorm.schema.enum"]) +) public inline fun > BaseTable<*>.pgEnum(name: String): Column { return registerColumn(name, PgEnumType(C::class.java)) } @@ -97,6 +102,10 @@ public inline fun > BaseTable<*>.pgEnum(name: String): Colum * [SqlType] implementation represents PostgreSQL `enum` type. * @see datatype-enum */ +@Deprecated( + message = "Will remove in the future, please use `EnumSqlType` instead", + replaceWith = ReplaceWith(expression = "EnumSqlType", imports = ["org.ktorm.schema.EnumSqlType"]) +) public class PgEnumType>(private val enumClass: Class) : SqlType(Types.OTHER, enumClass.name) { private val valueOf = enumClass.getDeclaredMethod("valueOf", String::class.java) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index ca6f0f8f..990df762 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -13,10 +13,7 @@ import org.ktorm.entity.* import org.ktorm.jackson.json import org.ktorm.logging.ConsoleLogger import org.ktorm.logging.LogLevel -import org.ktorm.schema.ColumnDeclaring -import org.ktorm.schema.Table -import org.ktorm.schema.int -import org.ktorm.schema.varchar +import org.ktorm.schema.* import org.testcontainers.containers.PostgreSQLContainer import java.time.LocalDate import java.util.concurrent.ExecutionException @@ -776,19 +773,11 @@ class PostgreSqlTest : BaseTest() { object TableWithEnum : Table("t_enum") { val id = int("id").primaryKey() - val current_mood = pgEnum("current_mood") + val current_mood = enum("current_mood") } @Test - fun testCanParseEnum() { - val currentMood = - database.sequenceOf(TableWithEnum).filter { it.id eq 1 }.mapColumns { it.current_mood }.first() - - assertThat(currentMood, equalTo(Mood.HAPPY)) - } - - @Test - fun testCanSetEnum() { + fun testEnum() { database.insert(TableWithEnum) { set(it.current_mood, Mood.SAD) } @@ -796,8 +785,12 @@ class PostgreSqlTest : BaseTest() { val count = database.sequenceOf(TableWithEnum).count { it.current_mood eq Mood.SAD } assertThat(count, equalTo(1)) - } + val currentMood = + database.sequenceOf(TableWithEnum).filter { it.id eq 1 }.mapColumns { it.current_mood }.first() + + assertThat(currentMood, equalTo(Mood.HAPPY)) + } @Test fun testJson() { From 01f96f4a5c3aa7aae338dfe8fdf6253020070f9f Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Sun, 2 May 2021 16:37:16 +0800 Subject: [PATCH 75/96] mysql test enum --- .../org/ktorm/support/mysql/MySqlTest.kt | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 9f994d93..b6975075 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -1,5 +1,7 @@ package org.ktorm.support.mysql +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert import org.junit.ClassRule import org.junit.Test import org.ktorm.BaseTest @@ -10,10 +12,7 @@ import org.ktorm.entity.* import org.ktorm.jackson.json import org.ktorm.logging.ConsoleLogger import org.ktorm.logging.LogLevel -import org.ktorm.schema.Table -import org.ktorm.schema.datetime -import org.ktorm.schema.int -import org.ktorm.schema.varchar +import org.ktorm.schema.* import org.testcontainers.containers.MySQLContainer import java.time.LocalDate import java.time.LocalDateTime @@ -500,4 +499,37 @@ class MySqlTest : BaseTest() { println(d) assert(d == now) } + + enum class Mood { + HAPPY, + SAD + } + + object TableWithEnum : Table("t_enum") { + val id = int("id").primaryKey() + val current_mood = enum("current_mood") + } + + @Test + fun testEnum() { + database.useConnection { conn -> + conn.createStatement().use { statement -> + val sql = """create table t_enum(id int not null primary key auto_increment, current_mood text)""" + statement.executeUpdate(sql) + } + } + + database.insert(TableWithEnum) { + set(it.current_mood, Mood.SAD) + } + + val count = database.sequenceOf(TableWithEnum).count { it.current_mood eq Mood.SAD } + + MatcherAssert.assertThat(count, CoreMatchers.equalTo(1)) + + val currentMood = + database.sequenceOf(TableWithEnum).filter { it.id eq 1 }.mapColumns { it.current_mood }.first() + + MatcherAssert.assertThat(currentMood, CoreMatchers.equalTo(Mood.SAD)) + } } From 21876d6b5d12729a6dec0f952a2d4f45e3168a97 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 3 May 2021 01:30:22 +0800 Subject: [PATCH 76/96] fix null-skipped bug for insertion #273 --- .../main/kotlin/org/ktorm/entity/EntityDml.kt | 21 ++---- .../org/ktorm/entity/EntityExtensions.kt | 48 ++++++++++++ .../org/ktorm/entity/EntityImplementation.kt | 4 + .../test/kotlin/org/ktorm/dsl/QueryTest.kt | 1 + .../kotlin/org/ktorm/entity/EntityTest.kt | 74 +++++++++++++++++-- .../org/ktorm/global/GlobalEntityTest.kt | 2 +- .../ktorm/jackson/JacksonAnnotationTest.kt | 7 +- 7 files changed, 129 insertions(+), 28 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt index 33e08c1c..183060d9 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt @@ -22,8 +22,7 @@ import org.ktorm.expression.* import org.ktorm.schema.* /** - * Insert the given entity into this sequence and return the affected record number. Only non-null properties - * are inserted. + * Insert the given entity into this sequence and return the affected record number. * * If we use an auto-increment key in our table, we need to tell Ktorm which is the primary key by calling * [Table.primaryKey] while registering columns, then this function will obtain the generated key from the @@ -60,7 +59,7 @@ public fun , T : Table> EntitySequence.add(entity: E): In val ignoreGeneratedKeys = primaryKeys.size != 1 || primaryKeys[0].binding == null - || entity.implementation.getColumnValue(primaryKeys[0].binding!!) != null + || entity.implementation.hasColumnValue(primaryKeys[0].binding!!) if (ignoreGeneratedKeys) { val effects = database.executeUpdate(expression) @@ -90,7 +89,7 @@ public fun , T : Table> EntitySequence.add(entity: E): In } /** - * Update the non-null properties of the given entity to the database and return the affected record number. + * Update properties of the given entity to the database and return the affected record number. * * Note that after calling this function, the [entity] will **be associated with the current table**. * @@ -211,11 +210,8 @@ private fun Entity<*>.findInsertColumns(table: Table<*>): Map, Any?> { val assignments = LinkedHashMap, Any?>() for (column in table.columns) { - if (column.binding != null) { - val value = implementation.getColumnValue(column.binding) - if (value != null) { - assignments[column] = value - } + if (column.binding != null && implementation.hasColumnValue(column.binding)) { + assignments[column] = implementation.getColumnValue(column.binding) } } @@ -226,11 +222,8 @@ private fun Entity<*>.findUpdateColumns(table: Table<*>): Map, Any?> { val assignments = LinkedHashMap, Any?>() for (column in table.columns - table.primaryKeys) { - if (column.binding != null) { - val value = implementation.getColumnValue(column.binding) - if (value != null) { - assignments[column] = value - } + if (column.binding != null && implementation.hasColumnValue(column.binding)) { + assignments[column] = implementation.getColumnValue(column.binding) } } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt index c4a4620b..9088e377 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityExtensions.kt @@ -21,6 +21,54 @@ import java.lang.reflect.Proxy import java.util.* import kotlin.reflect.jvm.jvmErasure +internal fun EntityImplementation.hasPrimaryKeyValue(fromTable: Table<*>): Boolean { + val pk = fromTable.singlePrimaryKey { "Table '$fromTable' has compound primary keys." } + if (pk.binding == null) { + error("Primary column $pk has no bindings to any entity field.") + } else { + return hasColumnValue(pk.binding) + } +} + +internal fun EntityImplementation.hasColumnValue(binding: ColumnBinding): Boolean { + when (binding) { + is ReferenceBinding -> { + if (!this.hasProperty(binding.onProperty)) { + return false + } + + val child = this.getProperty(binding.onProperty) as Entity<*>? + if (child == null) { + // null is also a legal column value. + return true + } + + return child.implementation.hasPrimaryKeyValue(binding.referenceTable as Table<*>) + } + is NestedBinding -> { + var curr: EntityImplementation = this + + for ((i, prop) in binding.properties.withIndex()) { + if (i != binding.properties.lastIndex) { + if (!curr.hasProperty(prop)) { + return false + } + + val child = curr.getProperty(prop) as Entity<*>? + if (child == null) { + // null is also a legal column value. + return true + } + + curr = child.implementation + } + } + + return curr.hasProperty(binding.properties.last()) + } + } +} + internal fun EntityImplementation.getPrimaryKeyValue(fromTable: Table<*>): Any? { val pk = fromTable.singlePrimaryKey { "Table '$fromTable' has compound primary keys." } if (pk.binding == null) { diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index 91600ecd..f049bc72 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -153,6 +153,10 @@ internal class EntityImplementation( } } + fun hasProperty(prop: KProperty1<*, *>): Boolean { + return prop.name in values + } + @OptIn(ExperimentalUnsignedTypes::class) fun getProperty(prop: KProperty1<*, *>, unboxInlineValues: Boolean = false): Any? { if (!unboxInlineValues) { diff --git a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt index af0f50a3..671f03ec 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt @@ -260,6 +260,7 @@ class QueryTest : BaseTest() { } @Test + @Suppress("DEPRECATION") fun testSelectForUpdate() { database.useTransaction { val employee = database diff --git a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt index de15c8db..0fa10782 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt @@ -153,7 +153,7 @@ class EntityTest : BaseTest() { employee.id = 2 employee.job = "engineer" employee.salary = 100 - employee.manager = null + // employee.manager = null database.employees.update(employee) employee = database.employees.find { it.id eq 2 } ?: throw AssertionError() @@ -190,7 +190,7 @@ class EntityTest : BaseTest() { var employee = Employee { name = "jerry" job = "trainee" - manager = database.employees.find { it.name eq "vince" } + manager = null hireDate = LocalDate.now() salary = 50 department = database.departments.find { it.name eq "tech" } ?: throw AssertionError() @@ -224,28 +224,86 @@ class EntityTest : BaseTest() { } interface Parent : Entity { - var child: Child + companion object : Entity.Factory() + var child: Child? } interface Child : Entity { - var grandChild: GrandChild + companion object : Entity.Factory() + var grandChild: GrandChild? } interface GrandChild : Entity { - var id: Int + companion object : Entity.Factory() + var id: Int? } object Parents : Table("t_employee") { - val id = int("id").primaryKey().bindTo { it.child.grandChild.id } + val id = int("id").primaryKey().bindTo { it.child?.grandChild?.id } + } + + @Test + fun testHasColumnValue() { + val p1 = Parent() + assert(!p1.implementation.hasColumnValue(Parents.id.binding!!)) + assert(p1.implementation.getColumnValue(Parents.id.binding!!) == null) + + val p2 = Parent { + child = null + } + assert(p2.implementation.hasColumnValue(Parents.id.binding!!)) + assert(p2.implementation.getColumnValue(Parents.id.binding!!) == null) + + val p3 = Parent { + child = Child() + } + assert(!p3.implementation.hasColumnValue(Parents.id.binding!!)) + assert(p3.implementation.getColumnValue(Parents.id.binding!!) == null) + + val p4 = Parent { + child = Child { + grandChild = null + } + } + assert(p4.implementation.hasColumnValue(Parents.id.binding!!)) + assert(p4.implementation.getColumnValue(Parents.id.binding!!) == null) + + val p5 = Parent { + child = Child { + grandChild = GrandChild() + } + } + assert(!p5.implementation.hasColumnValue(Parents.id.binding!!)) + assert(p5.implementation.getColumnValue(Parents.id.binding!!) == null) + + val p6 = Parent { + child = Child { + grandChild = GrandChild { + id = null + } + } + } + assert(p6.implementation.hasColumnValue(Parents.id.binding!!)) + assert(p6.implementation.getColumnValue(Parents.id.binding!!) == null) + + val p7 = Parent { + child = Child { + grandChild = GrandChild { + id = 6 + } + } + } + assert(p7.implementation.hasColumnValue(Parents.id.binding!!)) + assert(p7.implementation.getColumnValue(Parents.id.binding!!) == 6) } @Test fun testUpdatePrimaryKey() { try { val parent = database.sequenceOf(Parents).find { it.id eq 1 } ?: throw AssertionError() - assert(parent.child.grandChild.id == 1) + assert(parent.child?.grandChild?.id == 1) - parent.child.grandChild.id = 2 + parent.child?.grandChild?.id = 2 throw AssertionError() } catch (e: UnsupportedOperationException) { diff --git a/ktorm-global/src/test/kotlin/org/ktorm/global/GlobalEntityTest.kt b/ktorm-global/src/test/kotlin/org/ktorm/global/GlobalEntityTest.kt index 179c6519..69bda281 100644 --- a/ktorm-global/src/test/kotlin/org/ktorm/global/GlobalEntityTest.kt +++ b/ktorm-global/src/test/kotlin/org/ktorm/global/GlobalEntityTest.kt @@ -58,7 +58,7 @@ class GlobalEntityTest : BaseGlobalTest() { employee.id = 2 employee.job = "engineer" employee.salary = 100 - employee.manager = null + // employee.manager = null Employees.updateEntity(employee) employee = Employees.findOne { it.id eq 2 } ?: throw AssertionError() diff --git a/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt b/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt index 6ccd3669..b60f6549 100644 --- a/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt +++ b/ktorm-jackson/src/test/kotlin/org/ktorm/jackson/JacksonAnnotationTest.kt @@ -18,10 +18,7 @@ package org.ktorm.jackson import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.PropertyNamingStrategy -import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.module.kotlin.readValue import org.junit.Test import org.ktorm.entity.Entity @@ -534,7 +531,7 @@ class JacksonAnnotationTest { fun testDeserializeAliasName2() { val mapper = ObjectMapper().findAndRegisterModules() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .setPropertyNamingStrategy(PropertyNamingStrategy.SnakeCaseStrategy()) + .setPropertyNamingStrategy(PropertyNamingStrategies.SnakeCaseStrategy()) val json = """ { "name" : "alias", From 9879cf395510792c9e05ff0cb3753417b5ea061a Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 3 May 2021 01:54:04 +0800 Subject: [PATCH 77/96] fix warning --- .../src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt index bf507565..d7d1fb07 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/ColumnBindingHandler.kt @@ -24,6 +24,7 @@ import java.util.* import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty1 +import kotlin.reflect.full.createInstance import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.isSubclassOf import kotlin.reflect.jvm.javaGetter @@ -113,7 +114,7 @@ internal val Class<*>.defaultValue: Any get() { this.isEnum -> this.enumConstants[0] this.isArray -> java.lang.reflect.Array.newInstance(this.componentType, 0) this.kotlin.isSubclassOf(Entity::class) -> Entity.create(this.kotlin) - else -> this.newInstance() + else -> this.kotlin.createInstance() } if (this.kotlin.isInstance(value)) { From 0e217fe0293d1d096226858dd4dff8bbcc93cb4a Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 3 May 2021 02:03:09 +0800 Subject: [PATCH 78/96] fix error text --- .../main/kotlin/org/ktorm/entity/EntityImplementation.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt index f049bc72..1c167d59 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityImplementation.kt @@ -88,7 +88,7 @@ internal class EntityImplementation( if (result != null || prop.returnType.isMarkedNullable) { return result } else { - return method.defaultReturnValue.also { cacheDefaultValue(prop, it) } + return prop.defaultValue.also { cacheDefaultValue(prop, it) } } } else { this.setProperty(prop, args!![0]) @@ -107,9 +107,9 @@ internal class EntityImplementation( } } - private val Method.defaultReturnValue: Any get() { + private val KProperty1<*, *>.defaultValue: Any get() { try { - return returnType.defaultValue + return javaGetter!!.returnType.defaultValue } catch (e: Throwable) { val msg = "" + "The value of non-null property [$this] doesn't exist, " + From 6bcc17cd326b4358799fbe2ba321f0aaec89f503 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Mon, 3 May 2021 20:25:19 +0800 Subject: [PATCH 79/96] refactor pom structure --- build.gradle | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 154fcd24..96c9eacb 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ subprojects { project -> ] } - task generateSourcesJar(type: Jar) { + task generateSources(type: Jar) { archiveClassifier = "sources" from sourceSets.main.allSource } @@ -73,23 +73,28 @@ subprojects { project -> publications { bintray(MavenPublication) { from components.java - artifact generateSourcesJar + artifact generateSources artifact generateJavadoc - groupId project.group - artifactId project.name - version project.version + groupId = project.group + artifactId = project.name + version = project.version pom { - name = project.name + name = "${project.group}:${project.name}" description = "A lightweight ORM Framework for Kotlin with strong typed SQL DSL and sequence APIs." - url = "https://github.com/kotlin-orm/ktorm" + url = "https://www.ktorm.org" licenses { license { name = "The Apache Software License, Version 2.0" url = "http://www.apache.org/licenses/LICENSE-2.0.txt" } } + scm { + url = "https://github.com/kotlin-orm/ktorm" + connection = "scm:git:https://github.com/kotlin-orm/ktorm.git" + developerConnection = "scm:git:ssh://git@github.com/kotlin-orm/ktorm.git" + } developers { developer { id = "vincentlauvlwj" @@ -172,9 +177,6 @@ subprojects { project -> email = "eric@fender.net" } } - scm { - url = "https://github.com/kotlin-orm/ktorm.git" - } } } } From 1a2995775052c8e13935a5739872ba55ebedfb19 Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 4 May 2021 14:50:11 +0800 Subject: [PATCH 80/96] test ci --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index efec7952..eed9d3e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,9 @@ env: # - mysql -e "create database ktorm;" # - psql -c "create database ktorm;" -U postgres +before_install: + - echo $TEST_MULTI_LINES + after_success: - chmod +x auto-upload.sh - ./auto-upload.sh From c323aead820cf90ca31321e51c041bd5662f8f4a Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 4 May 2021 20:42:04 +0800 Subject: [PATCH 81/96] migrate from jcenter to maven central --- build.gradle | 71 +++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/build.gradle b/build.gradle index 96c9eacb..0044dd87 100644 --- a/build.gradle +++ b/build.gradle @@ -2,37 +2,32 @@ buildscript { ext { kotlinVersion = "1.4.32" - detektVersion = "1.15.0" + detektVersion = "1.17.0-RC2" } repositories { - maven { - url 'https://maven.aliyun.com/repository/public/' - } + mavenCentral() gradlePluginPortal() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" - classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${detektVersion}" } } allprojects { group = "org.ktorm" - version = "3.3.0" + version = "3.4.0-SNAPSHOT" } subprojects { project -> apply plugin: "kotlin" + apply plugin: "signing" apply plugin: "maven-publish" - apply plugin: "com.jfrog.bintray" apply plugin: "io.gitlab.arturbosch.detekt" apply from: "${project.rootDir}/check-source-header.gradle" repositories { - maven { - url 'https://maven.aliyun.com/repository/public/' - } + mavenCentral() } dependencies { @@ -62,16 +57,13 @@ subprojects { project -> detekt { toolVersion = detektVersion + input = files("src/main/kotlin") config = files("${project.rootDir}/detekt.yml") - reports { - xml.enabled = false - html.enabled = false - } } publishing { publications { - bintray(MavenPublication) { + dist(MavenPublication) { from components.java artifact generateSources artifact generateJavadoc @@ -180,34 +172,39 @@ subprojects { project -> } } } - } - bintray { - user = System.getenv("BINTRAY_USER") - key = System.getenv("BINTRAY_KEY") - publications = ["bintray"] - publish = true - - pkg { - repo = "ktorm" - name = project.name - licenses = ["Apache-2.0"] - vcsUrl = "https://github.com/kotlin-orm/ktorm.git" - labels = ["Kotlin", "ORM", "SQL"] - - version { - name = project.version - released = new Date() - vcsTag = project.version - - mavenCentralSync { - sync = false - user = System.getenv("OSSRH_USER") + repositories { + maven { + name = "central" + url = "https://oss.sonatype.org/service/local/staging/deploy/maven2" + credentials { + username = System.getenv("OSSRH_USER") + password = System.getenv("OSSRH_PASSWORD") + } + } + maven { + name = "snapshot" + url = "https://oss.sonatype.org/content/repositories/snapshots" + credentials { + username = System.getenv("OSSRH_USER") password = System.getenv("OSSRH_PASSWORD") } } } } + + signing { + def keyId = System.getenv("GPG_KEY_ID") + def secretKey = System.getenv("GPG_SECRET_KEY") + def password = System.getenv("GPG_PASSWORD") + + required { + !project.version.endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publishDistPublicationToCentralRepository") + } + + useInMemoryPgpKeys(keyId, secretKey, password) + sign publishing.publications.dist + } } task printClasspath() { From 144e082269cb117c4a0b9b248075feaea3abe6bd Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 4 May 2021 20:47:08 +0800 Subject: [PATCH 82/96] auto upload snapshots --- .travis.yml | 6 +----- auto-upload.sh | 10 ---------- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100755 auto-upload.sh diff --git a/.travis.yml b/.travis.yml index eed9d3e7..dedbda68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,12 +12,8 @@ env: # - mysql -e "create database ktorm;" # - psql -c "create database ktorm;" -U postgres -before_install: - - echo $TEST_MULTI_LINES - after_success: - - chmod +x auto-upload.sh - - ./auto-upload.sh + - ./gradlew publishDistPublicationToSnapshotRepository before_cache: - rm -f "${HOME}/.gradle/caches/modules-2/modules-2.lock" diff --git a/auto-upload.sh b/auto-upload.sh deleted file mode 100755 index 3d2528f0..00000000 --- a/auto-upload.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -last_commit=$(git log --pretty=format:'%d' | grep HEAD) - -if [[ ${last_commit} =~ "tag: " ]] -then - echo "New version found, auto uploading archives to bintray..." - ./gradlew bintrayUpload --stacktrace -else - echo "New version not found, exiting..." -fi From 84e7e034acef4828811f07c709d8423f21738431 Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 4 May 2021 23:33:28 +0800 Subject: [PATCH 83/96] update version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0044dd87..7ab407ac 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { allprojects { group = "org.ktorm" - version = "3.4.0-SNAPSHOT" + version = "3.4.0-RC1" } subprojects { project -> From 5369da9c1714e75444b94aca7c5c97fbb192ce94 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Wed, 5 May 2021 00:01:42 +0800 Subject: [PATCH 84/96] update signing requirement --- build.gradle | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 7ab407ac..34cb286a 100644 --- a/build.gradle +++ b/build.gradle @@ -198,10 +198,7 @@ subprojects { project -> def secretKey = System.getenv("GPG_SECRET_KEY") def password = System.getenv("GPG_PASSWORD") - required { - !project.version.endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publishDistPublicationToCentralRepository") - } - + required { !project.version.endsWith("SNAPSHOT") } useInMemoryPgpKeys(keyId, secretKey, password) sign publishing.publications.dist } From 53d8422122a7ce800f38a9ef568b6a68a8c917a9 Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 5 May 2021 00:39:39 +0800 Subject: [PATCH 85/96] update version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 34cb286a..e6f6b8e7 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { allprojects { group = "org.ktorm" - version = "3.4.0-RC1" + version = "3.4.0-SNAPSHOT" } subprojects { project -> From 16fd9758b60c237926bee5bdf04906884c90c728 Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 5 May 2021 16:02:53 +0800 Subject: [PATCH 86/96] add comments for equals & hashCode --- .../main/kotlin/org/ktorm/entity/Entity.kt | 27 ++++++++++++++++--- .../kotlin/org/ktorm/entity/EntityTest.kt | 3 ++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt index 01224f98..cc064633 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt @@ -44,11 +44,11 @@ import kotlin.reflect.jvm.jvmErasure * * ### Creating Entity Objects * - * As everyone knows, interfaces cannot be instantiated, so Ktorm provides [Entity.create] functions for us to - * create entity objects. Those functions generate implementations for entity interfaces via JDK dynamic proxy + * As everyone knows, interfaces cannot be instantiated, so Ktorm provides a [Entity.create] function for us to + * create entity objects. This function generate implementations for entity interfaces via JDK dynamic proxy * and create their instances. * - * If you don't like creating objects by [Entity.create] functions, Ktorm also provides an abstract factory class + * In case you don't like creating objects by [Entity.create], Ktorm also provides an abstract factory class * [Entity.Factory]. This class overloads the `invoke` operator of Kotlin, so we just need to add a companion * object to our entity class extending from [Entity.Factory], then entity objects can be created just like there * is a constructor: `val department = Department()`. @@ -209,6 +209,27 @@ public interface Entity> : Serializable { */ public fun copy(): E + /** + * Indicate whether some other object is "equal to" this entity. + * Two entities are equal only if they have the same [entityClass] and [properties]. + * + * @since 3.4.0 + */ + public override fun equals(other: Any?): Boolean + + /** + * Return a hash code value for this entity. + * + * @since 3.4.0 + */ + public override fun hashCode(): Int + + /** + * Return a string representation of this table. + * The format is like `Employee{id=1, name=Eric, job=contributor, hireDate=2021-05-05, salary=50}`. + */ + public override fun toString(): String + /** * Companion object provides functions to create entity instances. */ diff --git a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt index 0fa10782..25d4e27b 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt @@ -520,7 +520,8 @@ class EntityTest : BaseTest() { val employee = database.employees.find { it.id eq 2 } ?: return val copy = employee.copy() - assert(employee.hireDate == copy.hireDate) + assert(employee == copy) + assert(employee !== copy) assert(employee.hireDate !== copy.hireDate) // should not be the same instance because of deep copy. assert(copy.manager?.implementation?.parent === copy.implementation) // should keep the parent relationship. } From d2547bd0492f17e64fdd911a17b4d3c5c8f98674 Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 5 May 2021 16:14:20 +0800 Subject: [PATCH 87/96] fix comment --- ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt index cc064633..962abe8c 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt @@ -71,7 +71,7 @@ import kotlin.reflect.jvm.jvmErasure * - For [Boolean] type, the default value is `false`. * - For [Char] type, the default value is `\u0000`. * - For number types (such as [Int], [Long], [Double], etc), the default value is zero. - * - For the [String] type, the default value is the empty string. + * - For [String] type, the default value is an empty string. * - For entity types, the default value is a new-created entity object which is empty. * - For enum types, the default value is the first value of the enum, whose ordinal is 0. * - For array types, the default value is a new-created empty array. From c2b34ce18ea9703ab55fc1ac2429e01c1384132f Mon Sep 17 00:00:00 2001 From: vince Date: Wed, 5 May 2021 22:46:41 +0800 Subject: [PATCH 88/96] update comments for entity attachment --- .../main/kotlin/org/ktorm/entity/Entity.kt | 17 +++++------ .../main/kotlin/org/ktorm/entity/EntityDml.kt | 20 ++++++------- .../kotlin/org/ktorm/global/EntitySequence.kt | 30 +++++++++---------- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt index 962abe8c..b3832e4c 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt @@ -150,15 +150,14 @@ public interface Entity> : Serializable { * Using this function, we need to note that: * * 1. This function requires a primary key specified in the table object via [Table.primaryKey], - * otherwise Ktorm doesn’t know how to identify entity objects, then throws an exception. + * otherwise Ktorm doesn’t know how to identify entity objects and will throw an exception. * - * 2. The entity object calling this function must **be associated with a table** first. In Ktorm’s implementation, - * every entity object holds a reference `fromTable`, that means this object is associated with the table or was - * obtained from it. For entity objects obtained by sequence APIs, their `fromTable` references point to the current - * table object they are obtained from. But for entity objects created by [Entity.create] or [Entity.Factory], their - * `fromTable` references are `null` initially, so we can not call [flushChanges] on them. But once we use them with - * [add] or [update] function of entity sequences, Ktorm will modify their `fromTable` to the current table object, - * then we can call [flushChanges] on them afterwards. + * 2. The entity object calling this function must be ATTACHED to the database first. In Ktorm’s implementation, + * every entity object holds a reference `fromDatabase`. For entity objects obtained by sequence APIs, their + * `fromDatabase` references point to the database they are obtained from. For entity objects created by + * [Entity.create] or [Entity.Factory], their `fromDatabase` references are `null` initially, so we can not call + * [flushChanges] on them. But once we use them with [add] or [update] function, `fromDatabase` will be modified + * to the current database, so we will be able to call [flushChanges] on them afterwards. * * @see add * @see update @@ -182,7 +181,7 @@ public interface Entity> : Serializable { * 1. The function requires a primary key specified in the table object via [Table.primaryKey], * otherwise, Ktorm doesn’t know how to identify entity objects. * - * 2. The entity object calling this function must **be associated with a table** first. + * 2. The entity object calling this function must be ATTACHED to the database first. * * @see add * @see update diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt index 183060d9..467157a1 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntityDml.kt @@ -30,7 +30,7 @@ import org.ktorm.schema.* * not to set the primary key’s value beforehand, otherwise, if you do that, the given value will be inserted * into the database, and no keys generated. * - * Note that after calling this function, the [entity] will **be associated with the current table**. + * Note that after calling this function, the [entity] will be ATTACHED to the current database. * * @see Entity.flushChanges * @see Entity.delete @@ -91,7 +91,7 @@ public fun , T : Table> EntitySequence.add(entity: E): In /** * Update properties of the given entity to the database and return the affected record number. * - * Note that after calling this function, the [entity] will **be associated with the current table**. + * Note that after calling this function, the [entity] will be ATTACHED to the current database. * * @see Entity.flushChanges * @see Entity.delete @@ -148,10 +148,10 @@ public fun > EntitySequence.clear(): Int { @Suppress("UNCHECKED_CAST") internal fun EntityImplementation.doFlushChanges(): Int { - check(parent == null) { "The entity is not associated with any database yet." } + check(parent == null) { "The entity is not attached to any database yet." } - val fromDatabase = fromDatabase ?: error("The entity is not associated with any database yet.") - val fromTable = fromTable ?: error("The entity is not associated with any table yet.") + val fromDatabase = fromDatabase ?: error("The entity is not attached to any database yet.") + val fromTable = fromTable ?: error("The entity is not attached to any database yet.") checkUnexpectedDiscarding(fromTable) val assignments = findChangedColumns(fromTable).takeIf { it.isNotEmpty() } ?: return 0 @@ -174,10 +174,10 @@ internal fun EntityImplementation.doFlushChanges(): Int { @Suppress("UNCHECKED_CAST") internal fun EntityImplementation.doDelete(): Int { - check(parent == null) { "The entity is not associated with any database yet." } + check(parent == null) { "The entity is not attached to any database yet." } - val fromDatabase = fromDatabase ?: error("The entity is not associated with any database yet.") - val fromTable = fromTable ?: error("The entity is not associated with any table yet.") + val fromDatabase = fromDatabase ?: error("The entity is not attached to any database yet.") + val fromTable = fromTable ?: error("The entity is not attached to any database yet.") val expression = AliasRemover.visit( expr = DeleteExpression( @@ -272,8 +272,8 @@ private fun EntityImplementation.findChangedColumns(fromTable: Table<*>): Map> T.asSequence(withReferences: Boolean = tr } /** - * Insert the given entity into this table and return the affected record number. Only non-null properties - * are inserted. + * Insert the given entity into this sequence and return the affected record number. * * If we use an auto-increment key in our table, we need to tell Ktorm which is the primary key by calling - * [Table.primaryKey] function while registering columns, then this function will obtain the generated key - * from the database and fill it into the corresponding property after the insertion completes. But this - * requires us not to set the primary key’s value beforehand, otherwise, if you do that, the given value - * will be inserted into the database, and no keys generated. + * [Table.primaryKey] while registering columns, then this function will obtain the generated key from the + * database and fill it into the corresponding property after the insertion completes. But this requires us + * not to set the primary key’s value beforehand, otherwise, if you do that, the given value will be inserted + * into the database, and no keys generated. * - * Note that after calling this function, the [entity] will **be associated with the current table**. + * Note that after calling this function, the [entity] will be ATTACHED to the current database. * * @see Entity.flushChanges * @see Entity.delete @@ -53,16 +52,15 @@ public fun > Table.add(entity: E): Int { } /** - * Insert the given entity into this table and return the affected record number. Only non-null properties - * are inserted. + * Insert the given entity into this sequence and return the affected record number. * * If we use an auto-increment key in our table, we need to tell Ktorm which is the primary key by calling - * [Table.primaryKey] function while registering columns, then this function will obtain the generated key - * from the database and fill it into the corresponding property after the insertion completes. But this - * requires us not to set the primary key’s value beforehand, otherwise, if you do that, the given value - * will be inserted into the database, and no keys generated. + * [Table.primaryKey] while registering columns, then this function will obtain the generated key from the + * database and fill it into the corresponding property after the insertion completes. But this requires us + * not to set the primary key’s value beforehand, otherwise, if you do that, the given value will be inserted + * into the database, and no keys generated. * - * Note that after calling this function, the [entity] will **be associated with the current table**. + * Note that after calling this function, the [entity] will be ATTACHED to the current database. * * @see Entity.flushChanges * @see Entity.delete @@ -72,9 +70,9 @@ public fun > Table.addEntity(entity: E): Int { } /** - * Update the non-null properties of the given entity to the database and return the affected record number. + * Update properties of the given entity to the database and return the affected record number. * - * Note that after calling this function, the [entity] will **be associated with the current table**. + * Note that after calling this function, the [entity] will be ATTACHED to the current database. * * @see Entity.flushChanges * @see Entity.delete From c649e1475f6e947f06bfed5549f9565c808943b6 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 6 May 2021 00:43:43 +0800 Subject: [PATCH 89/96] update detekt rule --- detekt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detekt.yml b/detekt.yml index a9b7f1fb..4b32c185 100644 --- a/detekt.yml +++ b/detekt.yml @@ -50,7 +50,7 @@ complexity: threshold: 4 ComplexInterface: active: true - threshold: 11 + threshold: 12 includeStaticDeclarations: false ComplexMethod: active: true From c4efbc81189102887364a878d2a109d746e35009 Mon Sep 17 00:00:00 2001 From: "vincent.liu" Date: Thu, 6 May 2021 13:02:44 +0800 Subject: [PATCH 90/96] add test for jvm-default --- ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt | 3 +++ ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt index 70f59ca2..232b2840 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/BaseTest.kt @@ -71,6 +71,9 @@ open class BaseTest { var hireDate: LocalDate var salary: Long var department: Department + + val upperName get() = name.toUpperCase() + fun upperName() = name.toUpperCase() } interface Customer : Entity { diff --git a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt index 0fa10782..36a3450f 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/entity/EntityTest.kt @@ -43,6 +43,8 @@ class EntityTest : BaseTest() { assert(employee["name"] == "vince") assert(employee.name == "vince") + assert(employee.upperName == "VINCE") + assert(employee.upperName() == "VINCE") assert(employee["job"] == null) assert(employee.job == "") From 9dc39ac23874b315a6ac472370e90175e8dc5af2 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 6 May 2021 23:53:12 +0800 Subject: [PATCH 91/96] fix comment syntax --- ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt index b3832e4c..137743d8 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/Entity.kt @@ -45,7 +45,7 @@ import kotlin.reflect.jvm.jvmErasure * ### Creating Entity Objects * * As everyone knows, interfaces cannot be instantiated, so Ktorm provides a [Entity.create] function for us to - * create entity objects. This function generate implementations for entity interfaces via JDK dynamic proxy + * create entity objects. This function will generate implementations for entity interfaces via JDK dynamic proxy * and create their instances. * * In case you don't like creating objects by [Entity.create], Ktorm also provides an abstract factory class From a7c287cf46d7b8e2f0258c2459aaa15bd7d194fb Mon Sep 17 00:00:00 2001 From: vince Date: Sun, 9 May 2021 13:56:50 +0800 Subject: [PATCH 92/96] fix null enum bug --- .../src/main/kotlin/org/ktorm/schema/SqlTypes.kt | 14 +++++++++++++- .../kotlin/org/ktorm/support/mysql/MySqlTest.kt | 16 ++++++++++------ .../ktorm/support/postgresql/PostgreSqlTest.kt | 12 ++++++++---- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt index 7dcd8d55..51497a91 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/schema/SqlTypes.kt @@ -501,12 +501,24 @@ public inline fun > BaseTable<*>.enum(name: String): Column< * * @property enumClass the enum class. */ -public class EnumSqlType>(public val enumClass: Class) : SqlType(Types.VARCHAR, "enum") { +public class EnumSqlType>(public val enumClass: Class) : SqlType(Types.OTHER, "enum") { private val hasPostgresqlDriver by lazy { runCatching { Class.forName("org.postgresql.Driver") }.isSuccess } + override fun setParameter(ps: PreparedStatement, index: Int, parameter: C?) { + if (parameter != null) { + doSetParameter(ps, index, parameter) + } else { + if (hasPostgresqlDriver && ps is PGStatement) { + ps.setNull(index, Types.OTHER) + } else { + ps.setNull(index, Types.VARCHAR) + } + } + } + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: C) { if (hasPostgresqlDriver && ps is PGStatement) { ps.setObject(index, parameter.name, Types.OTHER) diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index b6975075..45e0566f 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -1,7 +1,7 @@ package org.ktorm.support.mysql -import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat import org.junit.ClassRule import org.junit.Test import org.ktorm.BaseTest @@ -524,12 +524,16 @@ class MySqlTest : BaseTest() { } val count = database.sequenceOf(TableWithEnum).count { it.current_mood eq Mood.SAD } + assertThat(count, equalTo(1)) - MatcherAssert.assertThat(count, CoreMatchers.equalTo(1)) + val mood = database.sequenceOf(TableWithEnum).filter { it.id eq 1 }.mapColumns { it.current_mood }.first() + assertThat(mood, equalTo(Mood.SAD)) - val currentMood = - database.sequenceOf(TableWithEnum).filter { it.id eq 1 }.mapColumns { it.current_mood }.first() + database.insert(TableWithEnum) { + set(it.current_mood, null) + } - MatcherAssert.assertThat(currentMood, CoreMatchers.equalTo(Mood.SAD)) + val mood1 = database.sequenceOf(TableWithEnum).filter { it.id eq 2 }.mapColumns { it.current_mood }.first() + assertThat(mood1, equalTo(null)) } } diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 990df762..7b260e9a 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -783,13 +783,17 @@ class PostgreSqlTest : BaseTest() { } val count = database.sequenceOf(TableWithEnum).count { it.current_mood eq Mood.SAD } - assertThat(count, equalTo(1)) - val currentMood = - database.sequenceOf(TableWithEnum).filter { it.id eq 1 }.mapColumns { it.current_mood }.first() + val mood = database.sequenceOf(TableWithEnum).filter { it.id eq 1 }.mapColumns { it.current_mood }.first() + assertThat(mood, equalTo(Mood.HAPPY)) + + database.insert(TableWithEnum) { + set(it.current_mood, null) + } - assertThat(currentMood, equalTo(Mood.HAPPY)) + val mood1 = database.sequenceOf(TableWithEnum).filter { it.id eq 3 }.mapColumns { it.current_mood }.first() + assertThat(mood1, equalTo(null)) } @Test From 35113b3ca34800d88fd49b3b4cff1adaed0e0b4a Mon Sep 17 00:00:00 2001 From: vince Date: Sun, 9 May 2021 14:08:27 +0800 Subject: [PATCH 93/96] fix null json bug --- .../main/kotlin/org/ktorm/jackson/JsonSqlType.kt | 14 +++++++++++++- .../kotlin/org/ktorm/support/mysql/MySqlTest.kt | 5 +++++ .../org/ktorm/support/postgresql/PostgreSqlTest.kt | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt index a0ed8e98..70e9d31e 100644 --- a/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt +++ b/ktorm-jackson/src/main/kotlin/org/ktorm/jackson/JsonSqlType.kt @@ -78,12 +78,24 @@ public inline fun BaseTable<*>.json( public class JsonSqlType( public val objectMapper: ObjectMapper, public val javaType: JavaType -) : SqlType(Types.VARCHAR, "json") { +) : SqlType(Types.OTHER, "json") { private val hasPostgresqlDriver by lazy { runCatching { Class.forName("org.postgresql.Driver") }.isSuccess } + override fun setParameter(ps: PreparedStatement, index: Int, parameter: T?) { + if (parameter != null) { + doSetParameter(ps, index, parameter) + } else { + if (hasPostgresqlDriver && ps is PGStatement) { + ps.setNull(index, Types.OTHER) + } else { + ps.setNull(index, Types.VARCHAR) + } + } + } + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: T) { if (hasPostgresqlDriver && ps is PGStatement) { val obj = PGobject() diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 45e0566f..00df266a 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -392,6 +392,11 @@ class MySqlTest : BaseTest() { set(it.arr, listOf(1, 2, 3)) } + database.insert(t) { + set(it.obj, null) + set(it.arr, null) + } + database .from(t) .select(t.obj, t.arr) diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 7b260e9a..b9fe1677 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -815,6 +815,11 @@ class PostgreSqlTest : BaseTest() { set(it.arr, listOf(1, 2, 3)) } + database.insert(t) { + set(it.obj, null) + set(it.arr, null) + } + database .from(t) .select(t.obj, t.arr) From c1723e4ef6a380632ab0b01b8f250c66d30ad0f5 Mon Sep 17 00:00:00 2001 From: vince Date: Sun, 9 May 2021 14:38:48 +0800 Subject: [PATCH 94/96] rm jcenter words --- README.md | 2 +- README_cn.md | 2 +- README_jp.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b53bbea6..661b2488 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ For more documentation, go to our site: [https://www.ktorm.org](https://www.ktor # Quick Start -Ktorm was deployed to maven central and jcenter, so you just need to add a dependency to your `pom.xml` file if you are using maven: +Ktorm was deployed to maven central, so you just need to add a dependency to your `pom.xml` file if you are using maven: ```xml diff --git a/README_cn.md b/README_cn.md index e566c5b3..5b78d1fd 100644 --- a/README_cn.md +++ b/README_cn.md @@ -42,7 +42,7 @@ Ktorm 是直接基于纯 JDBC 编写的高效简洁的轻量级 Kotlin ORM 框 # 快速开始 -Ktorm 已经发布到 maven 中央仓库和 jcenter,因此,如果你使用 maven 的话,只需要在 `pom.xml` 文件里面添加一个依赖: +Ktorm 已经发布到 maven 中央仓库,因此,如果你使用 maven 的话,只需要在 `pom.xml` 文件里面添加一个依赖: ```xml diff --git a/README_jp.md b/README_jp.md index edd1f7c7..5e82e6e4 100644 --- a/README_jp.md +++ b/README_jp.md @@ -42,7 +42,7 @@ Ktormは純粋なJDBCをベースにしたKotlin用の軽量で効率的なORM # クイックスタート -Ktormはmaven centralとjcenterにデプロイされているので、mavenを使っている場合は `pom.xml` ファイルに依存関係を追加するだけです。 +Ktormはmaven centralにデプロイされているので、mavenを使っている場合は `pom.xml` ファイルに依存関係を追加するだけです。 ```xml From 5393d6dc01cd4d2857d0ac7d6ad59eec336128d8 Mon Sep 17 00:00:00 2001 From: vince Date: Sun, 9 May 2021 16:31:55 +0800 Subject: [PATCH 95/96] fix expression visitor --- .../org/ktorm/support/postgresql/PostgreSqlDialect.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 5033717f..f5b7f523 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -325,7 +325,8 @@ public open class PostgreSqlExpressionVisitor : SqlExpressionVisitor() { table = table, assignments = assignments, conflictColumns = conflictColumns, - updateAssignments = updateAssignments + updateAssignments = updateAssignments, + returningColumns = returningColumns ) } } @@ -350,7 +351,8 @@ public open class PostgreSqlExpressionVisitor : SqlExpressionVisitor() { table = table, assignments = assignments, conflictColumns = conflictColumns, - updateAssignments = updateAssignments + updateAssignments = updateAssignments, + returningColumns = returningColumns ) } } From 19e425ac291be207e2ebf988585ef56b29e6f901 Mon Sep 17 00:00:00 2001 From: vince Date: Sun, 9 May 2021 16:41:32 +0800 Subject: [PATCH 96/96] release v3.4.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e6f6b8e7..e9325c4b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { allprojects { group = "org.ktorm" - version = "3.4.0-SNAPSHOT" + version = "3.4.0" } subprojects { project ->