Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import com.electronwill.nightconfig.core.Config
import com.electronwill.nightconfig.core.conversion.ObjectConverter
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Test
import java.util.EnumSet
import java.util.LinkedHashSet
import java.util.LinkedList
import java.util.Queue
import java.util.SortedMap
import java.util.SortedSet
import java.util.TreeMap
import java.util.TreeSet

class TestObjectConverterCollections {

@Test
fun `preserves declared collection types and converts nested elements`() {
val root = Config.inMemory()
root.set<Any>("names", listOf("alpha", "beta", "alpha"))
root.set<Any>("queue", listOf("first", "second"))
root.set<Any>("numbers", listOf(1L, 2L))
root.set<Any>("sorted", listOf("beta", "alpha"))
root.set<Any>("modes", listOf("first", "SECOND"))
root.set<Any>("groups", listOf(listOf(itemConfig("one"), itemConfig("two"))))

val indexed = Config.inMemory()
indexed.set<Any>("primary", itemConfig("indexed"))
root.set<Any>("indexed", indexed)

val sortedIndex = Config.inMemory()
sortedIndex.set<Any>("second", 2)
sortedIndex.set<Any>("first", 1)
root.set<Any>("sortedIndex", sortedIndex)

val result = CollectionHolder()
ObjectConverter().toObject(root, result)

assertInstanceOf(LinkedHashSet::class.java, result.names)
assertEquals(linkedSetOf("alpha", "beta"), result.names)
assertInstanceOf(LinkedList::class.java, result.queue)
assertEquals(listOf("first", "second"), result.queue.toList())
assertInstanceOf(LinkedList::class.java, result.numbers)
assertEquals(listOf(1, 2), result.numbers)
assertInstanceOf(TreeSet::class.java, result.sorted)
assertEquals(listOf("alpha", "beta"), result.sorted.toList())
assertEquals(EnumSet.of(Mode.FIRST, Mode.SECOND), result.modes)
assertInstanceOf(LinkedHashSet::class.java, result.groups.single())
assertEquals(listOf("one", "two"), result.groups.single().map { it.name })
assertEquals("indexed", result.indexed.getValue("primary").name)
assertInstanceOf(TreeMap::class.java, result.sortedIndex)
assertEquals(listOf("first", "second"), result.sortedIndex.keys.toList())
assertEquals(listOf(1L, 2L), result.sortedIndex.values.toList())
}

private fun itemConfig(name: String): Config {
return Config.inMemory().also { it.set<Any>("name", name) }
}

class CollectionHolder {

var names: Set<String> = emptySet()
var queue: Queue<String> = LinkedList()
var numbers: LinkedList<Int> = LinkedList()
var sorted: SortedSet<String> = sortedSetOf()
var modes: EnumSet<Mode> = EnumSet.noneOf(Mode::class.java)
var groups: List<Set<Item>> = emptyList()
var indexed: Map<String, Item> = emptyMap()
var sortedIndex: SortedMap<String, Long> = sortedMapOf()
}

class Item {

var name: String = ""
}

enum class Mode {
FIRST,
SECOND,
}
}
1 change: 1 addition & 0 deletions module/database/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dependencies {
compileOnly(project(":common-platform-api"))
compileOnly(project(":common-util"))
compileOnly(project(":module:basic:basic-configuration"))
testImplementation(project(":common-util"))
testImplementation("com.zaxxer:HikariCP:4.0.3")
testImplementation("org.xerial:sqlite-jdbc:3.42.0.0")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,32 @@ class ActionInsert(val table: String, val keys: Array<String>) : Action {
/** 重复时更新 */
private var duplicateUpdate = ArrayList<UpdateOperation>()

/** 重复键方言 */
private var duplicateKeyDialect = DuplicateKeyDialect.MYSQL

/** 冲突目标 */
private var conflictKeys: Array<String>? = null

/** 语句 */
override val query: String
get() = Statement("INSERT INTO")
.addSegment(table.asFormattedColumnName())
.addSegmentIfTrue(keys.isNotEmpty()) {
addKeys(keys)
}
.addSegmentIfTrue(values.isNotEmpty()) {
addSegment("VALUES")
addValues(values)
get() {
require(table.isNotBlank()) { "Insert table must not be blank" }
require(keys.none { it.isBlank() }) { "Insert keys must not contain blank names" }
require(values.isNotEmpty()) { "Insert values must not be empty" }
if (keys.isNotEmpty()) {
require(values.all { it.size == keys.size }) { "Insert value count must match key count" }
}
.addSegmentIfTrue(duplicateUpdate.isNotEmpty()) {
addSegment("ON DUPLICATE KEY UPDATE")
addOperations(duplicateUpdate)
}.build()
return Statement("INSERT INTO")
.addSegment(table.asFormattedColumnName())
.addSegmentIfTrue(keys.isNotEmpty()) {
addKeys(keys)
}
.addSegment("VALUES")
.addValues(values)
.addSegmentIfTrue(duplicateUpdate.isNotEmpty()) {
addDuplicateUpdate()
}.build()
}

/** 元素 */
override val elements: List<Any?>
Expand All @@ -60,9 +71,28 @@ class ActionInsert(val table: String, val keys: Array<String>) : Action {
values.add(args.toTypedArray())
}

/** 重复时更新 */
/**
* 重复时更新。
* PostgreSQL 无法从插入字段可靠推断唯一约束,需使用带冲突字段的重载。
*/
fun onDuplicateKeyUpdate(func: DuplicateUpdateBehavior.() -> Unit) {
duplicateUpdate = DuplicateUpdateBehavior().also(func).updateOperations
setupDuplicateUpdate(null, func)
}

/**
* 重复时更新,并显式指定 PostgreSQL/SQLite 的冲突字段。
* MySQL 会忽略冲突字段并继续使用 ON DUPLICATE KEY UPDATE。
*/
fun onDuplicateKeyUpdate(conflictKeys: Collection<String>, func: DuplicateUpdateBehavior.() -> Unit) {
setupDuplicateUpdate(conflictKeys.toTypedArray(), func)
}

internal fun setupDialect(host: Host<*>) {
duplicateKeyDialect = when (host) {
is HostPostgreSQL -> DuplicateKeyDialect.POSTGRESQL
is HostSQLite -> DuplicateKeyDialect.SQLITE
else -> DuplicateKeyDialect.MYSQL
}
}

override fun onFinally(onFinally: PreparedStatement.(Connection) -> Unit) {
Expand All @@ -73,16 +103,64 @@ class ActionInsert(val table: String, val keys: Array<String>) : Action {
this.finallyCallback?.invoke(preparedStatement, connection)
}

private fun setupDuplicateUpdate(conflictKeys: Array<String>?, func: DuplicateUpdateBehavior.() -> Unit) {
val behavior = DuplicateUpdateBehavior().also(func)
duplicateUpdate = behavior.updateOperations
this.conflictKeys = conflictKeys
}

private fun Statement.addDuplicateUpdate() {
when (duplicateKeyDialect) {
DuplicateKeyDialect.MYSQL -> {
addSegment("ON DUPLICATE KEY UPDATE")
addOperations(duplicateUpdate)
}
DuplicateKeyDialect.POSTGRESQL -> {
val targetKeys = conflictKeys
require(!targetKeys.isNullOrEmpty()) {
"PostgreSQL duplicate update requires explicit conflict keys"
}
require(targetKeys.none { it.isBlank() }) {
"PostgreSQL conflict keys must not contain blank names"
}
addSegment("ON CONFLICT")
addKeys(targetKeys)
addSegment("DO UPDATE SET")
addOperations(duplicateUpdate)
}
DuplicateKeyDialect.SQLITE -> {
addSegment("ON CONFLICT")
conflictKeys?.also { targetKeys ->
require(targetKeys.none { it.isBlank() }) {
"SQLite conflict keys must not contain blank names"
}
if (targetKeys.isNotEmpty()) {
addKeys(targetKeys)
}
}
addSegment("DO UPDATE SET")
addOperations(duplicateUpdate)
}
}
}

class DuplicateUpdateBehavior {

val updateOperations = ArrayList<UpdateOperation>()

fun update(key: String, value: Any) {
require(key.isNotBlank()) { "Duplicate update key must not be blank" }
updateOperations += if (value is PreValue) {
UpdateOperation("${key.asFormattedColumnName()} = ${value.asFormattedColumnName()}")
} else {
UpdateOperation("${key.asFormattedColumnName()} = ?", value)
}
}
}
}

private enum class DuplicateKeyDialect {
MYSQL,
POSTGRESQL,
SQLITE,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,20 @@ open class ExecutableSource(val table: Table<*, *>, var dataSource: DataSource,
/** 插入数据 */
open fun insert(vararg keys: String, func: ActionInsert.() -> Unit = {}): ResultProcessor {
setupQuoter()
val action = ActionInsert(table.name, arrayOf(*keys)).also(func)
val action = ActionInsert(table.name, arrayOf(*keys)).also {
it.setupDialect(table.host)
func(it)
}
return executeUpdate(action.query, action)
}

/** 插入数据 */
open fun insert(keys: List<String>, func: ActionInsert.() -> Unit = {}): ResultProcessor {
setupQuoter()
val action = ActionInsert(table.name, keys.toTypedArray()).also(func)
val action = ActionInsert(table.name, keys.toTypedArray()).also {
it.setupDialect(table.host)
func(it)
}
return executeUpdate(action.query, action)
}

Expand Down Expand Up @@ -289,9 +295,9 @@ open class ExecutableSource(val table: Table<*, *>, var dataSource: DataSource,
.addSegmentIfTrue(index.checkExists) {
addSegment("IF NOT EXISTS")
}
.addSegment(index.name)
.addSegment(index.name.asFormattedColumnName())
.addSegment("ON")
.addSegment(table.name)
.addSegment(table.name.asFormattedColumnName())
.addSegment("(")
.addSegment(index.columns.joinToString(",", transform = { it.asFormattedColumnName() }))
.addSegment(")")
Expand Down
Loading
Loading