From 86823ae35c5a60bf7308567b17947f73096326f4 Mon Sep 17 00:00:00 2001 From: bog-walk <82039410+bog-walk@users.noreply.github.com> Date: Wed, 12 Jul 2023 05:29:53 -0400 Subject: [PATCH] feat: EXPOSED-89 Support functions in Create Index (#1788) Add support for functional indices. Note: - Add dropIndex() override for Oracle as default was incorrect. - Adjust all Table index functions to not use = operator with Unit return type. --- exposed-core/api/exposed-core.api | 24 ++++++---- .../org/jetbrains/exposed/sql/Constraints.kt | 24 ++++++++-- .../kotlin/org/jetbrains/exposed/sql/Table.kt | 30 +++++++++--- .../jetbrains/exposed/sql/vendors/Default.kt | 35 ++++++++++---- .../org/jetbrains/exposed/sql/vendors/H2.kt | 6 +++ .../exposed/sql/vendors/MariaDBDialect.kt | 12 +++++ .../jetbrains/exposed/sql/vendors/Mysql.kt | 10 ++++ .../exposed/sql/vendors/OracleDialect.kt | 4 ++ .../exposed/sql/vendors/SQLServerDialect.kt | 10 ++++ .../exposed/sql/tests/shared/DDLTests.kt | 47 +++++++++++++++++++ .../sql/tests/shared/ddl/CreateIndexTests.kt | 28 +++++++++++ 11 files changed, 202 insertions(+), 28 deletions(-) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index e1dcf59fe1..ec09a5b132 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -1106,21 +1106,25 @@ public final class org/jetbrains/exposed/sql/InSubQueryOp : org/jetbrains/expose } public final class org/jetbrains/exposed/sql/Index : org/jetbrains/exposed/sql/DdlAware { - public fun (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;)V - public synthetic fun (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;)V + public synthetic fun (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/util/List; public final fun component2 ()Z public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Lorg/jetbrains/exposed/sql/Op; - public final fun copy (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;)Lorg/jetbrains/exposed/sql/Index; - public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/Index;Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Index; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Lorg/jetbrains/exposed/sql/Table; + public final fun copy (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;)Lorg/jetbrains/exposed/sql/Index; + public static synthetic fun copy$default (Lorg/jetbrains/exposed/sql/Index;Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Op;Ljava/util/List;Lorg/jetbrains/exposed/sql/Table;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Index; public fun createStatement ()Ljava/util/List; public fun dropStatement ()Ljava/util/List; public fun equals (Ljava/lang/Object;)Z public final fun getColumns ()Ljava/util/List; public final fun getCustomName ()Ljava/lang/String; public final fun getFilterCondition ()Lorg/jetbrains/exposed/sql/Op; + public final fun getFunctions ()Ljava/util/List; + public final fun getFunctionsTable ()Lorg/jetbrains/exposed/sql/Table; public final fun getIndexName ()Ljava/lang/String; public final fun getIndexType ()Ljava/lang/String; public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; @@ -2291,10 +2295,10 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public fun getPrimaryKey ()Lorg/jetbrains/exposed/sql/Table$PrimaryKey; public fun getTableName ()Ljava/lang/String; public fun hashCode ()I - public final fun index (Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public final fun index (Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public final fun index (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/Column; public final fun index (Z[Lorg/jetbrains/exposed/sql/Column;)V - public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Z[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Z[Lorg/jetbrains/exposed/sql/Column;ILjava/lang/Object;)V public fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; @@ -2339,10 +2343,10 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public final fun ubyte (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; public final fun uinteger (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; public final fun ulong (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; - public final fun uniqueIndex (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;)V + public final fun uniqueIndex (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V public final fun uniqueIndex (Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; public final fun uniqueIndex ([Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/Column;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun ushort (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; @@ -3524,6 +3528,7 @@ public final class org/jetbrains/exposed/sql/vendors/KeywordsKt { public final class org/jetbrains/exposed/sql/vendors/MariaDBDialect : org/jetbrains/exposed/sql/vendors/MysqlDialect { public static final field Companion Lorg/jetbrains/exposed/sql/vendors/MariaDBDialect$Companion; public fun ()V + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; public fun getFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; public fun getName ()Ljava/lang/String; public fun getSupportsOnlyIdentifiersInGeneratedKeys ()Z @@ -3535,6 +3540,7 @@ public final class org/jetbrains/exposed/sql/vendors/MariaDBDialect$Companion : public class org/jetbrains/exposed/sql/vendors/MysqlDialect : org/jetbrains/exposed/sql/vendors/VendorDialect { public static final field Companion Lorg/jetbrains/exposed/sql/vendors/MysqlDialect$Companion; public fun ()V + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; public fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; @@ -3557,6 +3563,7 @@ public class org/jetbrains/exposed/sql/vendors/OracleDialect : org/jetbrains/exp public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; public fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; public fun getDefaultReferenceOption ()Lorg/jetbrains/exposed/sql/ReferenceOption; public fun getNeedsQuotesWhenSymbolsInNames ()Z @@ -3605,6 +3612,7 @@ public class org/jetbrains/exposed/sql/vendors/SQLServerDialect : org/jetbrains/ public static final field Companion Lorg/jetbrains/exposed/sql/vendors/SQLServerDialect$Companion; public fun ()V public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; protected fun createIndexWithType (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; public fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt index aaf30cac21..6dd02b15cd 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Constraints.kt @@ -242,7 +242,11 @@ data class Index( /** Optional custom index type (e.g, BTREE or HASH) */ val indexType: String? = null, /** Partial index filter condition */ - val filterCondition: Op? = null + val filterCondition: Op? = null, + /** Functions that are part of the index. */ + val functions: List>? = null, + /** Table where the functional index should be defined. */ + val functionsTable: Table? = null ) : DdlAware { /** Table where the index is defined. */ val table: Table @@ -253,16 +257,26 @@ data class Index( append(table.nameInDatabaseCase()) append('_') append(columns.joinToString("_") { it.name }.inProperCase()) + functions?.let { f -> + if (columns.isNotEmpty()) append('_') + append(f.joinToString("_") { it.toString().substringBefore("(").lowercase() }.inProperCase()) + } if (unique) { append("_unique".inProperCase()) } } init { - require(columns.isNotEmpty()) { "At least one column is required to create an index" } - val table = columns.distinctBy { it.table }.singleOrNull()?.table - requireNotNull(table) { "Columns from different tables can't persist in one index" } - this.table = table + require(columns.isNotEmpty() || functions?.isNotEmpty() == true) { "At least one column or function is required to create an index" } + val columnsTable = if (columns.isNotEmpty()) { + val table = columns.distinctBy { it.table }.singleOrNull()?.table + requireNotNull(table) { "Columns from different tables can't persist in one index" } + table + } else null + if (functions?.isNotEmpty() == true) { + requireNotNull(functionsTable) { "functionsTable argument must also be provided if functions are defined to create an index" } + } + this.table = columnsTable ?: functionsTable!! } override fun createStatement(): List = listOf(currentDialect.createIndex(this)) diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index 58f571f166..cbc3d3a203 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -986,17 +986,18 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { /** * Creates an index. * - * @param columns Columns that compose the index. * @param isUnique Whether the index is unique or not. + * @param columns Columns that compose the index. */ - fun index(isUnique: Boolean = false, vararg columns: Column<*>): Unit = index(null, isUnique, *columns) + fun index(isUnique: Boolean = false, vararg columns: Column<*>) { index(null, isUnique, *columns) } /** * Creates an index. * * @param customIndexName Name of the index. - * @param columns Columns that compose the index. * @param isUnique Whether the index is unique or not. + * @param columns Columns that compose the index. + * @param functions Functions that compose the index. * @param indexType A custom index type (e.g., "BTREE" or "HASH"). * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ @@ -1004,10 +1005,17 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { customIndexName: String? = null, isUnique: Boolean = false, vararg columns: Column<*>, + functions: List>? = null, indexType: String? = null, filterCondition: FilterCondition = null ) { - _indices.add(Index(columns.toList(), isUnique, customIndexName, indexType = indexType, filterCondition?.invoke(SqlExpressionBuilder))) + _indices.add( + Index( + columns.toList(), isUnique, customIndexName, indexType, + filterCondition?.invoke(SqlExpressionBuilder), + functions, functions?.let { this } + ) + ) } /** @@ -1033,18 +1041,26 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * @param columns Columns that compose the index. * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun uniqueIndex(vararg columns: Column<*>, filterCondition: FilterCondition = null): Unit = + fun uniqueIndex(vararg columns: Column<*>, filterCondition: FilterCondition = null) { index(null, true, *columns, filterCondition = filterCondition) + } /** * Creates a unique index. * * @param customIndexName Name of the index. * @param columns Columns that compose the index. + * @param functions Functions that compose the index. * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun uniqueIndex(customIndexName: String? = null, vararg columns: Column<*>, filterCondition: FilterCondition = null): Unit = - index(customIndexName, true, *columns, filterCondition = filterCondition) + fun uniqueIndex( + customIndexName: String? = null, + vararg columns: Column<*>, + functions: List>? = null, + filterCondition: FilterCondition = null + ) { + index(customIndexName, true, *columns, functions = functions, filterCondition = filterCondition) + } /** * Creates a composite foreign key. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt index 7b5fadd4c8..53dacd490c 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Default.kt @@ -1161,6 +1161,16 @@ abstract class VendorDialect( } ?: "" } + private fun indexFunctionToString(function: Function<*>): String { + val baseString = function.toString() + return when (currentDialect) { + // SQLite & Oracle do not support "." operator (with table prefix) in index expressions + is SQLiteDialect, is OracleDialect -> baseString.replace(Regex("""^*[^( ]*\."""), "") + is MysqlDialect -> if (baseString.first() != '(') "($baseString)" else baseString + else -> baseString + } + } + /** * Uniqueness might be required for foreign key constraints. * @@ -1172,29 +1182,38 @@ abstract class VendorDialect( val t = TransactionManager.current() val quotedTableName = t.identity(index.table) val quotedIndexName = t.db.identifierManager.cutIfNecessaryAndQuote(index.indexName) - val columnsList = index.columns.joinToString(prefix = "(", postfix = ")") { t.identity(it) } - + val keyFields = index.columns.plus(index.functions ?: emptyList()) + val fieldsList = keyFields.joinToString(prefix = "(", postfix = ")") { + when (it) { + is Column<*> -> t.identity(it) + is Function<*> -> indexFunctionToString(it) + else -> { + exposedLogger.warn("Unexpected defining key field will be passed as String: $it") + it.toString() + } + } + } + val includesOnlyColumns = index.functions?.isEmpty() != false val maybeFilterCondition = filterCondition(index) ?: return "" return when { // unique and no filter -> constraint, the type is not supported - index.unique && maybeFilterCondition.isEmpty() -> { - "ALTER TABLE $quotedTableName ADD CONSTRAINT $quotedIndexName UNIQUE $columnsList" + index.unique && maybeFilterCondition.isEmpty() && includesOnlyColumns -> { + "ALTER TABLE $quotedTableName ADD CONSTRAINT $quotedIndexName UNIQUE $fieldsList" } // unique and filter -> index only, the type is not supported index.unique -> { - "CREATE UNIQUE INDEX $quotedIndexName ON $quotedTableName $columnsList$maybeFilterCondition" + "CREATE UNIQUE INDEX $quotedIndexName ON $quotedTableName $fieldsList$maybeFilterCondition" } // type -> can't be unique or constraint index.indexType != null -> { createIndexWithType( name = quotedIndexName, table = quotedTableName, - columns = columnsList, type = index.indexType, filterCondition = maybeFilterCondition + columns = fieldsList, type = index.indexType, filterCondition = maybeFilterCondition ) } - // any other indexes. May be can be merged with `createIndexWithType` else -> { - "CREATE INDEX $quotedIndexName ON $quotedTableName $columnsList$maybeFilterCondition" + "CREATE INDEX $quotedIndexName ON $quotedTableName $fieldsList$maybeFilterCondition" } } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt index 58f5281784..6e959d4b60 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/H2.kt @@ -244,6 +244,12 @@ open class H2Dialect : VendorDialect(dialectName, H2DataTypeProvider, H2Function ) return "" } + if (index.functions != null) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in H2" + ) + return "" + } return super.createIndex(index) } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt index 9d65c28d7b..48bf58e08d 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/MariaDBDialect.kt @@ -1,9 +1,11 @@ package org.jetbrains.exposed.sql.vendors import org.jetbrains.exposed.sql.Expression +import org.jetbrains.exposed.sql.Index import org.jetbrains.exposed.sql.QueryBuilder import org.jetbrains.exposed.sql.Sequence import org.jetbrains.exposed.sql.append +import org.jetbrains.exposed.sql.exposedLogger internal object MariaDBFunctionProvider : MysqlFunctionProvider() { override fun nextVal(seq: Sequence, builder: QueryBuilder) = builder { @@ -36,5 +38,15 @@ class MariaDBDialect : MysqlDialect() { override val functionProvider: FunctionProvider = MariaDBFunctionProvider override val supportsOnlyIdentifiersInGeneratedKeys: Boolean = true + override fun createIndex(index: Index): String { + if (index.functions != null) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in MariaDB" + ) + return "" + } + return super.createIndex(index) + } + companion object : DialectNameProvider("mariadb") } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt index d983dab2c9..b7aad27db1 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/Mysql.kt @@ -327,6 +327,16 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq } } + override fun createIndex(index: Index): String { + if (index.functions != null && !isMysql8) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in MySQL prior to 8.0" + ) + return "" + } + return super.createIndex(index) + } + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartial: Boolean): String = "ALTER TABLE ${identifierManager.quoteIfNecessary(tableName)} DROP INDEX ${identifierManager.quoteIfNecessary(indexName)}" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt index 1fb7b689fb..afa5ae4209 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/OracleDialect.kt @@ -296,6 +296,10 @@ open class OracleDialect : VendorDialect(dialectName, OracleDataTypeProvider, Or override fun isAllowedAsColumnDefault(e: Expression<*>): Boolean = true + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartial: Boolean): String { + return "DROP INDEX ${identifierManager.quoteIfNecessary(indexName)}" + } + override fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List { val result = super.modifyColumn(column, columnDiff).map { it.replace("MODIFY COLUMN", "MODIFY") diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt index 7ed30cf5e8..e662eaeb40 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLServerDialect.kt @@ -262,6 +262,16 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid } } + override fun createIndex(index: Index): String { + if (index.functions != null) { + exposedLogger.warn( + "Functional index on ${index.table.tableName} using ${index.functions.joinToString { it.toString() }} can't be created in SQLServer" + ) + return "" + } + return super.createIndex(index) + } + override fun createIndexWithType(name: String, table: String, columns: String, type: String, filterCondition: String): String { return "CREATE $type INDEX $name ON $table $columns$filterCondition" } diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt index 603d536575..fcc4f988d3 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/DDLTests.kt @@ -9,6 +9,7 @@ import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.exceptions.UnsupportedByDialectException import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus import org.jetbrains.exposed.sql.statements.api.ExposedBlob import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB @@ -395,6 +396,52 @@ class DDLTests : DatabaseTestsBase() { } } + @Test + fun testIndexWithFunctions() { + val tester = object : Table("tester") { + val amount = integer("amount") + val price = integer("price") + val item = varchar("item", 32).nullable() + + init { + index(customIndexName = "tester_plus_index", isUnique = false, functions = listOf(amount.plus(price))) + index(isUnique = false, functions = listOf(item.lowerCase())) + uniqueIndex(columns = arrayOf(price), functions = listOf(Coalesce(item, stringLiteral("*")))) + } + } + + withDb { testDb -> + val tableProperName = tester.tableName.inProperCase() + val priceColumnName = tester.price.nameInDatabaseCase() + val uniqueIndexName = "tester_price_coalesce${if (testDb == TestDB.SQLITE) "" else "_unique"}".inProperCase() + val (p1, p2) = if (testDb == TestDB.MYSQL) "(" to ")" else "" to "" + val functionStrings = when (testDb) { + TestDB.SQLITE, TestDB.ORACLE -> listOf("(amount + price)", "LOWER(item)", "COALESCE(item, '*')").map(String::inProperCase) + else -> listOf( + tester.amount.plus(tester.price).toString(), + "$p1${tester.item.lowerCase()}$p2", + "$p1${Coalesce(tester.item, stringLiteral("*"))}$p2" + ) + } + + val functionsNotSupported = testDb in (TestDB.allH2TestDB + TestDB.SQLSERVER + TestDB.MARIADB) || isOldMySql() + val expectedStatements = if (functionsNotSupported) { + List(3) { "" } + } else { + listOf( + "CREATE INDEX tester_plus_index ON $tableProperName (${functionStrings[0]})", + "CREATE INDEX ${"tester_lower".inProperCase()} ON $tableProperName (${functionStrings[1]})", + "CREATE UNIQUE INDEX $uniqueIndexName ON $tableProperName ($priceColumnName, ${functionStrings[2]})" + ) + } + + repeat(3) { i -> + val actualStatement = SchemaUtils.createIndex(tester.indices[i]) + assertEquals(expectedStatements[i], actualStatement) + } + } + } + @Test fun testBlob() { val t = object : Table("t1") { val id = integer("id").autoIncrement() diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt index 9aa16f1405..3dc0af13b3 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/ddl/CreateIndexTests.kt @@ -3,6 +3,7 @@ package org.jetbrains.exposed.sql.tests.shared.ddl import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.currentDialectTest @@ -234,4 +235,31 @@ class CreateIndexTests : DatabaseTestsBase() { assertEquals(expectedIndexCount, actualIndexCount) } } + + @Test + fun testCreateAndDropFunctionalIndex() { + val tester = object : IntIdTable("tester") { + val amount = integer("amount") + val price = integer("price") + val item = varchar("item", 32).nullable() + + init { + index(customIndexName = "tester_plus_index", isUnique = false, functions = listOf(amount.plus(price))) + index(isUnique = false, functions = listOf(item.lowerCase())) + uniqueIndex(columns = arrayOf(price), functions = listOf(Coalesce(item, stringLiteral("*")))) + } + } + + val functionsNotSupported = TestDB.allH2TestDB + TestDB.SQLSERVER + TestDB.MARIADB + withTables(excludeSettings = functionsNotSupported, tester) { + if (!isOldMySql()) { + SchemaUtils.createMissingTablesAndColumns() + assertTrue(tester.exists()) + assertEquals(3, tester.indices.size) + + val dropStatements = tester.indices.map { it.dropStatement().first() } + expect(Unit) { execInBatch(dropStatements) } + } + } + } }