Coverage Summary for Class: OracleSupport (com.kotlinorm.database.oracle)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| OracleSupport |
3.8%
(1/26)
|
0%
(0/200)
|
0.5%
(1/205)
|
0.4%
(9/2102)
|
| OracleSupport$WhenMappings |
|
| Total |
3.8%
(1/26)
|
0%
(0/200)
|
0.5%
(1/205)
|
0.4%
(9/2102)
|
/**
* Copyright 2022-2025 kronos-orm
*
* 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 com.kotlinorm.database.oracle
import com.kotlinorm.beans.dsl.Field
import com.kotlinorm.beans.dsl.FunctionField
import com.kotlinorm.beans.dsl.KTableIndex
import com.kotlinorm.beans.task.KronosAtomicQueryTask
import com.kotlinorm.database.ConflictResolver
import com.kotlinorm.database.SqlManager.getDBNameFrom
import com.kotlinorm.database.SqlManager.getKotlinColumnType
import com.kotlinorm.database.mssql.MssqlSupport
import com.kotlinorm.enums.DBType
import com.kotlinorm.enums.KColumnType
import com.kotlinorm.enums.KColumnType.*
import com.kotlinorm.enums.PessimisticLock
import com.kotlinorm.enums.PrimaryKeyType
import com.kotlinorm.exceptions.UnsupportedDatabaseTypeException
import com.kotlinorm.functions.FunctionManager.getBuiltFunctionField
import com.kotlinorm.interfaces.DatabasesSupport
import com.kotlinorm.interfaces.KronosDataSourceWrapper
import com.kotlinorm.orm.ddl.TableColumnDiff
import com.kotlinorm.orm.ddl.TableIndexDiff
import com.kotlinorm.orm.join.JoinClauseInfo
import com.kotlinorm.orm.select.SelectClauseInfo
import com.kotlinorm.utils.trimWhitespace
object OracleSupport : DatabasesSupport {
override var quotes = Pair("\"", "\"")
override fun getDBNameFromUrl(wrapper: KronosDataSourceWrapper) = wrapper.userName
override fun getColumnType(type: KColumnType, length: Int, scale: Int): String {
return when (type) {
// 数值类型处理(Oracle 主要使用 NUMBER)
BIT -> "NUMBER(1)"
TINYINT -> "NUMBER(3)" // -128 到 127
SMALLINT -> "NUMBER(5)" // -32,768 到 32,767
MEDIUMINT -> "NUMBER(7)" // -8,388,608 到 8,388,607
INT -> "NUMBER(${length.takeIf { it > 0 } ?: 10})" // 默认 10 位精度
BIGINT -> "NUMBER(19)" // -2^63 到 2^63-1
SERIAL -> "NUMBER GENERATED ALWAYS AS IDENTITY" // Oracle 12c+ 自增方式
// 浮点类型
REAL -> "BINARY_FLOAT" // Oracle 32位浮点
FLOAT -> if (length > 0) "FLOAT($length)" else "BINARY_DOUBLE" // 默认 64位
DOUBLE -> "BINARY_DOUBLE" // 明确双精度
// 精确数值(必须处理精度)
DECIMAL -> when {
length > 0 && scale > 0 -> "NUMBER($length,$scale)"
length > 0 -> "NUMBER($length,0)"
else -> "NUMBER(10,0)" // Oracle 常用默认值
}
NUMERIC -> when {
length > 0 && scale > 0 -> "NUMERIC($length,$scale)"
else -> "NUMERIC(10,0)"
}
// 字符类型
CHAR -> "CHAR(${length.takeIf { it > 0 } ?: 255})"
VARCHAR -> "VARCHAR2(${length.takeIf { it > 0 } ?: 255})" // 推荐 VARCHAR2
NVARCHAR -> "NVARCHAR2(${length.takeIf { it > 0 } ?: 255})"
NCHAR -> "NCHAR(${length.takeIf { it > 0 } ?: 255})"
// 大对象类型
TEXT, MEDIUMTEXT, LONGTEXT -> "CLOB"
CLOB -> "CLOB"
NCLOB -> "NCLOB"
BINARY, VARBINARY -> "RAW(${length.takeIf { it > 0 } ?: 2000})" // RAW 默认长度
BLOB, MEDIUMBLOB, LONGBLOB, LONGVARBINARY -> "BLOB"
// 时间类型
DATE -> "DATE" // 包含日期和时间
TIME -> "TIMESTAMP(0)" // 单独时间用 TIMESTAMP
DATETIME -> "TIMESTAMP(6)" // 高精度时间戳
TIMESTAMP -> "TIMESTAMP(${scale.coerceIn(0, 9)})" // 允许指定小数秒精度
// 其他类型
JSON -> "JSON" // Oracle 12c+
XML -> "XMLType" // Oracle 专用类型
UUID -> "CHAR(36)"
ENUM -> "VARCHAR2(255)" // Oracle 无 ENUM,用字符串替代
SET -> "VARCHAR2(1000)" // 集合类型用长字符串
GEOMETRY -> "SDO_GEOMETRY" // Oracle 空间类型
POINT -> "SDO_GEOMETRY"
LINESTRING -> "SDO_GEOMETRY"
YEAR -> "NUMBER(4)" // 单独年份存储
else -> "VARCHAR2(255)"
}
}
override fun getKColumnType(type: String, length: Int, scale: Int): KColumnType {
return when (type) {
"NUMBER" -> when (length) {
1 -> BIT
3 -> TINYINT
5 -> SMALLINT
7 -> MEDIUMINT
11 -> INT
19 -> BIGINT
else -> INT
}
"VARCHAR2" -> VARCHAR
else -> super.getKColumnType(type, length, scale)
}
}
override fun getColumnCreateSql(dbType: DBType, column: Field): String {
return "${
quote(column.columnName)
}${
" ${getColumnType(column.type, column.length, column.scale)}"
}${
if (column.primaryKey == PrimaryKeyType.IDENTITY) " GENERATED ALWAYS AS IDENTITY" else ""
}${
if (column.nullable) "" else " NOT NULL"
}${
if (column.primaryKey != PrimaryKeyType.NOT) " PRIMARY KEY" else ""
}${
if (column.defaultValue != null) " DEFAULT ${column.defaultValue.ifEmpty { "\"\"" }}" else ""
}"
}
override fun getIndexCreateSql(dbType: DBType, tableName: String, index: KTableIndex) =
"CREATE ${index.type.uppercase()} INDEX ${index.name} ON ${quote(tableName)} (${
index.columns.joinToString(
", "
) { quote(it) }
})"
override fun getTableCreateSqlList(
dbType: DBType, tableName: String, tableComment: String?, columns: List<Field>, indexes: List<KTableIndex>
): List<String> {
val columnsSql = columns.joinToString(",") { getColumnCreateSql(dbType, it) }
val indexesSql = indexes.map { getIndexCreateSql(dbType, tableName, it) }
return listOfNotNull(
"CREATE TABLE ${quote(tableName.uppercase())} ($columnsSql)",
*indexesSql.toTypedArray(),
*columns.asSequence().filter { !it.kDoc.isNullOrEmpty() }.map {
"COMMENT ON COLUMN ${quote(tableName)}.${quote(it.columnName)} IS '${it.kDoc}'"
}.toList().toTypedArray(),
if (tableComment.isNullOrEmpty()) null else "COMMENT ON TABLE ${quote(tableName)} IS '$tableComment'"
)
}
override fun getTableExistenceSql(dbType: DBType) =
"select count(1) from all_objects where object_type in ('TABLE','VIEW') and object_name = :tableName and owner = :dbName"
override fun getTableTruncateSql(dbType: DBType, tableName: String, restartIdentity: Boolean) =
"TRUNCATE TABLE ${quote(tableName)}"
override fun getTableDropSql(dbType: DBType, tableName: String) = """
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE ${quote(tableName)}';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
END;
""".trimIndent()
override fun getTableCommentSql(dbType: DBType): String =
"SELECT comments FROM all_tab_comments WHERE table_name = :tableName AND owner = :dbName"
override fun getTableColumns(dataSource: KronosDataSourceWrapper, tableName: String): List<Field> {
return dataSource.forList(
KronosAtomicQueryTask(
"""
WITH RankedColumns AS (
SELECT
cols.column_name AS COLUMN_NAME,
cols.data_type AS DATE_TYPE,
cols.data_length AS LENGTH,
cols.data_precision AS PRECISION,
cols.nullable AS IS_NULLABLE,
cols.data_default AS COLUMN_DEFAULT,
CASE WHEN cons.constraint_type = 'P' THEN '1' ELSE '0' END AS PRIMARY_KEY,
col_comments.comments AS COLUMN_COMMENT,
ROW_NUMBER() OVER (PARTITION BY cols.column_name ORDER BY CASE WHEN cons.constraint_type = 'P' THEN 0 ELSE 1 END, cons.constraint_type) AS rn
FROM
all_tab_columns cols
LEFT JOIN
all_cons_columns cons_cols
ON cols.owner = cons_cols.owner AND cols.table_name = cons_cols.table_name AND cols.column_name = cons_cols.column_name
LEFT JOIN
all_constraints cons
ON cols.owner = cons.owner AND cons_cols.constraint_name = cons.constraint_name AND cons_cols.table_name = cons.table_name
LEFT JOIN
all_col_comments col_comments
ON cols.owner = col_comments.owner AND cols.table_name = col_comments.table_name AND cols.column_name = col_comments.column_name
WHERE
cols.table_name = :tableName AND cols.OWNER = :dbName
)
SELECT
COLUMN_NAME, DATE_TYPE, LENGTH, PRECISION, IS_NULLABLE, COLUMN_DEFAULT, PRIMARY_KEY, COLUMN_COMMENT
FROM
RankedColumns
WHERE
rn = 1
""".trimWhitespace(),
mapOf("tableName" to tableName.uppercase(), "dbName" to getDBNameFrom(dataSource).uppercase())
)
).map {
val dataType = it["DATE_TYPE"].toString()
val length = it["LENGTH"]?.toString()?.toIntOrNull() ?: 0
val precision = it["PRECISION"]?.toString()?.toIntOrNull() ?: 0
Field(
columnName = it["COLUMN_NAME"].toString(),
type = getKotlinColumnType(DBType.Oracle, dataType, length, precision),
length = length,
scale = precision,
tableName = tableName.uppercase(),
nullable = it["IS_NULLABLE"] == "Y",
primaryKey = when{
it["PRIMARY_KEY"] == 0 -> PrimaryKeyType.NOT
it["COLUMN_DEFAULT"]?.toString()?.endsWith(".nextval") == true -> PrimaryKeyType.IDENTITY
else -> PrimaryKeyType.DEFAULT
},
defaultValue = if (it["COLUMN_DEFAULT"]?.toString()?.endsWith(".nextval") == true) {
null
} else {
it["COLUMN_DEFAULT"] as String?
},
kDoc = it["COLUMN_COMMENT"] as String?
)
}
}
override fun getTableIndexes(
dataSource: KronosDataSourceWrapper, tableName: String
): List<KTableIndex> {
return dataSource.forList(
KronosAtomicQueryTask(
"""
SELECT DISTINCT i.INDEX_NAME AS NAME
FROM ALL_INDEXES i
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME
WHERE i.TABLE_NAME = UPPER(:tableName)
AND i.OWNER = :dbName
AND i.INDEX_NAME NOT LIKE UPPER('SYS_%')
""".trimWhitespace(),
mapOf(
"tableName" to tableName.uppercase(), "dbName" to getDBNameFrom(dataSource).uppercase()
)
)
).map {
KTableIndex(it["NAME"] as String, arrayOf(), "", "")
}
}
override fun getTableSyncSqlList(
dataSource: KronosDataSourceWrapper,
tableName: String,
originalTableComment: String?,
tableComment: String?,
columns: TableColumnDiff,
indexes: TableIndexDiff
): List<String> {
val syncSqlList = mutableListOf<String>()
if (originalTableComment.orEmpty() != tableComment.orEmpty()) {
syncSqlList.add("COMMENT ON TABLE ${quote(tableName)} IS '$tableComment'")
}
val dbType = dataSource.dbType
val dbName = getDBNameFrom(dataSource)
syncSqlList.addAll(
indexes.toDelete.map {
"DROP INDEX ${quote(dbName)}.\"${it.name}\""
} + columns.toDelete.map {
"ALTER TABLE ${quote(tableName)} DROP COLUMN \"${it.columnName}\""
} + columns.toModified.map {
"ALTER TABLE ${quote(tableName)} MODIFY(${getColumnCreateSql(dbType, it.first)})"
} + columns.toAdd.map {
"ALTER TABLE ${quote(tableName)} ADD ${getColumnCreateSql(dbType, it.first)}"
} + columns.toModified.map {
if (it.first.kDoc.isNullOrEmpty()) {
"COMMENT ON COLUMN ${quote(dbName)}.${quote(tableName)}.${quote(it.first.columnName)} IS NULL"
} else {
"COMMENT ON COLUMN ${quote(dbName)}.${quote(tableName)}.${quote(it.first.columnName)} IS '${it.first.kDoc}'"
}
} + indexes.toAdd.map {
"CREATE ${it.type} INDEX ${it.name} ON ${quote(dbName)}.${quote(tableName)} (${
it.columns.joinToString(",") { col ->
quote(col.uppercase())
}
})"
}
)
return syncSqlList
}
override fun getOnConflictSql(conflictResolver: ConflictResolver): String {
val (tableName, onFields, toUpdateFields, toInsertFields) = conflictResolver
return """
BEGIN
INSERT INTO ${quote(tableName)}
(${toInsertFields.joinToString { quote(it) }})
VALUES
(${toInsertFields.joinToString(", ") { ":$it" }})
EXCEPTION
WHEN
DUP_VAL_ON_INDEX
THEN
UPDATE ${quote(tableName)}
SET
${toUpdateFields.joinToString(", ") { equation(it) }}
WHERE
${onFields.joinToString(" AND ") { equation(it) }};
END;
""".trimWhitespace()
}
override fun getInsertSql(dataSource: KronosDataSourceWrapper, tableName: String, columns: List<Field>) =
"INSERT INTO ${quote(tableName.uppercase())} (${
columns.joinToString {
quote(it.columnName.uppercase())
}
}) VALUES (${columns.joinToString { ":$it" }})"
override fun getDeleteSql(dataSource: KronosDataSourceWrapper, tableName: String, whereClauseSql: String?) =
"DELETE FROM ${quote(tableName.uppercase())}${whereClauseSql.orEmpty()}"
override fun getUpdateSql(
dataSource: KronosDataSourceWrapper,
tableName: String,
toUpdateFields: List<Field>,
whereClauseSql: String?,
plusAssigns: MutableList<Pair<Field, String>>,
minusAssigns: MutableList<Pair<Field, String>>
) =
"UPDATE ${quote(tableName.uppercase())} SET ${toUpdateFields.joinToString { equation(it + "New") }}" +
plusAssigns.joinToString { ", ${quote(it.first)} = ${quote(it.first)} + :${it.second}" } +
minusAssigns.joinToString { ", ${quote(it.first)} = ${quote(it.first)} - :${it.second}" } +
whereClauseSql.orEmpty()
override fun getSelectSql(dataSource: KronosDataSourceWrapper, selectClause: SelectClauseInfo): String {
val (databaseName, tableName, selectFields, distinct, pagination, pi, ps, limit, lock, whereClauseSql, groupByClauseSql, orderByClauseSql, havingClauseSql) = selectClause
if (!databaseName.isNullOrEmpty()) throw UnsupportedDatabaseTypeException(
DBType.Oracle,
"Oracle does not support databaseName in select clause because of its dblink-liked configuration mode"
)
val selectSql = selectFields.joinToString(", ") {
when {
it is FunctionField -> getBuiltFunctionField(it, dataSource)
it.type == CUSTOM_CRITERIA_SQL -> it.toString()
it.name != it.columnName -> "${quote(it.columnName.uppercase())} AS ${quote(it.name)}"
else -> quote(it)
}
}
val paginationSql = if (pagination) " OFFSET $pi ROWS FETCH NEXT $ps ROWS ONLY" else null
val limitSql =
if (paginationSql == null && limit != null && limit > 0) " FETCH FIRST $limit ROWS ONLY" else null
val distinctSql = if (distinct) " DISTINCT" else null
val lockSql = when (lock) {
PessimisticLock.X -> " FOR UPDATE(NOWAIT)"
PessimisticLock.S -> " LOCK IN SHARE MODE"
else -> null
}
return "SELECT${distinctSql.orEmpty()} $selectSql FROM ${
quote(tableName.uppercase())
}${
whereClauseSql.orEmpty()
}${
groupByClauseSql.orEmpty()
}${
havingClauseSql.orEmpty()
}${
orderByClauseSql.orEmpty()
}${
paginationSql ?: limitSql ?: ""
}${
lockSql.orEmpty()
}"
}
override fun getJoinSql(dataSource: KronosDataSourceWrapper, joinClause: JoinClauseInfo): String {
val (tableName, selectFields, distinct, pagination, pi, ps, limit, databaseOfTable, whereClauseSql, groupByClauseSql, orderByClauseSql, havingClauseSql, joinSql) = joinClause
if (databaseOfTable.isNotEmpty()) throw UnsupportedDatabaseTypeException(
DBType.Oracle,
"Oracle does not support databaseName in select clause because of its dblink-liked configuration mode"
)
val selectSql = selectFields.joinToString(", ") {
val field = it.second
when {
field is FunctionField -> getBuiltFunctionField(field, dataSource, true)
field.type == CUSTOM_CRITERIA_SQL -> field.toString()
field.name != field.columnName -> "${quote(field, true)} AS ${quote(field.name)}"
else -> "${quote(field, true)} AS ${MssqlSupport.quote(it.first)}"
}
}
val paginationSql = if (pagination) " OFFSET $pi ROWS FETCH NEXT $ps ROWS ONLY" else null
val limitSql =
if (paginationSql == null && limit != null && limit > 0) " FETCH FIRST $limit ROWS ONLY" else null
val distinctSql = if (distinct) " DISTINCT" else null
return "SELECT${distinctSql.orEmpty()} $selectSql FROM ${
quote(tableName.uppercase())
}${
joinSql.orEmpty()
}${
whereClauseSql.orEmpty()
}${
groupByClauseSql.orEmpty()
}${
havingClauseSql.orEmpty()
}${
orderByClauseSql.orEmpty()
}${
paginationSql ?: limitSql ?: ""
}"
}
}