diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 9b854f77de..518d5b4e2a 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -1016,19 +1016,21 @@ 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;)V - public synthetic fun (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + 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 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 copy (Ljava/util/List;ZLjava/lang/String;Ljava/lang/String;)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;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Index; + 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 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 getIndexName ()Ljava/lang/String; public final fun getIndexType ()Ljava/lang/String; public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; @@ -2039,10 +2041,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;)V + 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 (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;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/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; @@ -2085,11 +2087,12 @@ 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;)V + public final fun uniqueIndex (Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;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;)V - public static synthetic fun uniqueIndex$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;[Lorg/jetbrains/exposed/sql/Column;ILjava/lang/Object;)V + 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;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; public final fun uuid (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; public final fun varchar (Ljava/lang/String;ILjava/lang/String;)Lorg/jetbrains/exposed/sql/Column; @@ -2932,7 +2935,7 @@ public abstract interface class org/jetbrains/exposed/sql/vendors/DatabaseDialec public abstract fun createIndex (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; public abstract fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; public abstract fun dropDatabase (Ljava/lang/String;)Ljava/lang/String; - public abstract fun dropIndex (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public abstract fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; public abstract fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; public abstract fun existingIndices ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; public abstract fun getDataTypeProvider ()Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider; @@ -3202,7 +3205,7 @@ public class org/jetbrains/exposed/sql/vendors/MysqlDialect : org/jetbrains/expo public static final field Companion Lorg/jetbrains/exposed/sql/vendors/MysqlDialect$Companion; public fun ()V public fun createSchema (Lorg/jetbrains/exposed/sql/Schema;)Ljava/lang/String; - public fun dropIndex (Ljava/lang/String;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; protected fun fillConstraintCacheForTables (Ljava/util/List;)V public fun getSupportsCreateSequence ()Z @@ -3244,8 +3247,9 @@ public class org/jetbrains/exposed/sql/vendors/PostgreSQLDialect : org/jetbrains public static final field Companion Lorg/jetbrains/exposed/sql/vendors/PostgreSQLDialect$Companion; public fun ()V public fun createDatabase (Ljava/lang/String;)Ljava/lang/String; - protected fun createIndexWithType (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)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 dropDatabase (Ljava/lang/String;)Ljava/lang/String; + public fun dropIndex (Ljava/lang/String;Ljava/lang/String;ZZ)Ljava/lang/String; public fun getRequiresAutoCommitOnCreateDrop ()Z public fun getSupportsOrderByNullsFirstLast ()Z public fun isAllowedAsColumnDefault (Lorg/jetbrains/exposed/sql/Expression;)Z @@ -3269,7 +3273,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; - protected fun createIndexWithType (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)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; public fun dropSchema (Lorg/jetbrains/exposed/sql/Schema;Z)Ljava/lang/String; @@ -3311,19 +3315,21 @@ public abstract class org/jetbrains/exposed/sql/vendors/VendorDialect : org/jetb public fun columnConstraints ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; 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; + 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; - public fun dropIndex (Ljava/lang/String;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 existingIndices ([Lorg/jetbrains/exposed/sql/Table;)Ljava/util/Map; protected fun fillConstraintCacheForTables (Ljava/util/List;)V + public final fun filterCondition (Lorg/jetbrains/exposed/sql/Index;)Ljava/lang/String; public final fun getAllTablesNames ()Ljava/util/List; protected final fun getColumnConstraintsCache ()Ljava/util/Map; public fun getDataTypeProvider ()Lorg/jetbrains/exposed/sql/vendors/DataTypeProvider; public fun getDatabase ()Ljava/lang/String; public fun getDefaultReferenceOption ()Lorg/jetbrains/exposed/sql/ReferenceOption; public fun getFunctionProvider ()Lorg/jetbrains/exposed/sql/vendors/FunctionProvider; + protected final fun getIdentifierManager ()Lorg/jetbrains/exposed/sql/statements/api/IdentifierManagerApi; public fun getLikePatternSpecialChars ()Ljava/util/Map; public fun getName ()Ljava/lang/String; public fun getNeedsQuotesWhenSymbolsInNames ()Z 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 adf6495979..aaf30cac21 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 @@ -227,6 +227,8 @@ data class CheckConstraint( } } +typealias FilterCondition = (SqlExpressionBuilder.() -> Op)? + /** * Represents an index. */ @@ -238,7 +240,9 @@ data class Index( /** Optional custom name for the index. */ val customName: String? = null, /** Optional custom index type (e.g, BTREE or HASH) */ - val indexType: String? = null + val indexType: String? = null, + /** Partial index filter condition */ + val filterCondition: Op? = null ) : DdlAware { /** Table where the index is defined. */ val table: Table @@ -263,7 +267,7 @@ data class Index( override fun createStatement(): List = listOf(currentDialect.createIndex(this)) override fun modifyStatement(): List = dropStatement() + createStatement() - override fun dropStatement(): List = listOf(currentDialect.dropIndex(table.nameInDatabaseCase(), indexName)) + override fun dropStatement(): List = listOf(currentDialect.dropIndex(table.nameInDatabaseCase(), indexName, unique, filterCondition != null)) /** Returns `true` if the [other] index has the same columns and uniqueness as this index, but a different name, `false` otherwise */ fun onlyNameDiffer(other: Index): Boolean = indexName != other.indexName && columns == other.columns && unique == other.unique 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 b8462d089f..2d1471fa0d 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 @@ -196,12 +196,15 @@ class Join( onColumn != null && otherColumn != null -> { join(otherTable, joinType, onColumn, otherColumn, additionalConstraint) } + onColumn != null || otherColumn != null -> { error("Can't prepare join on $table and $otherTable when only column from a one side provided.") } + additionalConstraint != null -> { join(otherTable, joinType, emptyList(), additionalConstraint) } + else -> { implicitJoin(otherTable, joinType) } @@ -250,10 +253,12 @@ class Join( joinType != JoinType.CROSS && fkKeys.isEmpty() -> { error("Cannot join with $otherTable as there is no matching primary key/foreign key pair and constraint missing") } + fkKeys.any { it.second.size > 1 } -> { val references = fkKeys.joinToString(" & ") { "${it.first} -> ${it.second.joinToString()}" } error("Cannot join with $otherTable as there is multiple primary key <-> foreign key references.\n$references") } + else -> { val cond = fkKeys.filter { it.second.size == 1 }.map { it.first to it.second.single() } join(otherTable, joinType, cond, null) @@ -337,6 +342,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { } internal val tableNameWithoutScheme: String get() = tableName.substringAfter(".") + // Table name may contain quotes, remove those before appending internal val tableNameWithoutSchemeSanitized: String get() = tableNameWithoutScheme.replace("\"", "").replace("'", "") @@ -943,9 +949,16 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * @param columns Columns that compose the index. * @param isUnique Whether the index is unique or not. * @param indexType A custom index type (e.g., "BTREE" or "HASH"). + * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun index(customIndexName: String? = null, isUnique: Boolean = false, vararg columns: Column<*>, indexType: String? = null) { - _indices.add(Index(columns.toList(), isUnique, customIndexName, indexType = indexType)) + fun index( + customIndexName: String? = null, + isUnique: Boolean = false, + vararg columns: Column<*>, + indexType: String? = null, + filterCondition: FilterCondition = null + ) { + _indices.add(Index(columns.toList(), isUnique, customIndexName, indexType = indexType, filterCondition?.invoke(SqlExpressionBuilder))) } /** @@ -962,22 +975,27 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { * * @param customIndexName Name of the index. */ - fun Column.uniqueIndex(customIndexName: String? = null): Column = index(customIndexName, true) + fun Column.uniqueIndex(customIndexName: String? = null): Column = + index(customIndexName, true) /** * Creates a unique index. * * @param columns Columns that compose the index. + * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun uniqueIndex(vararg columns: Column<*>): Unit = index(null, true, *columns) + fun uniqueIndex(vararg columns: Column<*>, filterCondition: FilterCondition = null): Unit = + index(null, true, *columns, filterCondition = filterCondition) /** * Creates a unique index. * * @param customIndexName Name of the index. * @param columns Columns that compose the index. + * @param filterCondition Index filtering conditions (also known as "partial index") declaration. */ - fun uniqueIndex(customIndexName: String? = null, vararg columns: Column<*>): Unit = index(customIndexName, true, *columns) + fun uniqueIndex(customIndexName: String? = null, vararg columns: Column<*>, filterCondition: FilterCondition = null): Unit = + index(customIndexName, true, *columns, filterCondition = filterCondition) /** * Creates a composite foreign key. @@ -1074,6 +1092,7 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { is ColumnType -> { this.withColumnType(AutoIncColumnType(columnType, idSeqName, "${tableName}_${name}_seq")) } + else -> error("Unsupported column type for auto-increment $columnType") } 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 9eeccc4e59..9c155db86b 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 @@ -856,7 +856,7 @@ interface DatabaseDialect { fun createIndex(index: Index): String /** Returns the SQL command that drops the specified [indexName] from the specified [tableName]. */ - fun dropIndex(tableName: String, indexName: String): String + fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartial: Boolean): String /** Returns the SQL command that modifies the specified [column]. */ fun modifyColumn(column: Column<*>, columnDiff: ColumnDiff): List @@ -959,6 +959,9 @@ abstract class VendorDialect( override val functionProvider: FunctionProvider ) : DatabaseDialect { + protected val identifierManager + get() = TransactionManager.current().db.identifierManager + abstract class DialectNameProvider(val dialectName: String) /* Cached values */ @@ -1062,30 +1065,61 @@ abstract class VendorDialect( resetCaches() } + fun filterCondition(index: Index): String? { + return if (currentDialect is PostgreSQLDialect) { + index.filterCondition?.let { + QueryBuilder(false) + .append(" WHERE ").append(it) + .toString() + } + } else { + null + } + } + + /** + * Uniqueness might be required for foreign key constraints. + * + * In PostgreSQL (https://www.postgresql.org/docs/current/indexes-unique.html), UNIQUE means B-tree only. + * Unique constraints can not be partial + * Unique indexes can be partial + */ override fun createIndex(index: Index): String { 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 maybeFilterCondition = filterCondition(index) ?: "" + return when { - index.unique -> { + // unique and no filter -> constraint, the type is not supported + index.unique && maybeFilterCondition.isEmpty() -> { "ALTER TABLE $quotedTableName ADD CONSTRAINT $quotedIndexName UNIQUE $columnsList" } + // unique and filter -> index only, the type is not supported + index.unique -> { + "CREATE UNIQUE INDEX $quotedIndexName ON $quotedTableName $columnsList$maybeFilterCondition" + } + // type -> can't be unique or constraint index.indexType != null -> { - createIndexWithType(name = quotedIndexName, table = quotedTableName, columns = columnsList, type = index.indexType) + createIndexWithType( + name = quotedIndexName, table = quotedTableName, + columns = columnsList, type = index.indexType, filterCondition = maybeFilterCondition + ) } + // any other indexes. May be can be merged with `createIndexWithType` else -> { - "CREATE INDEX $quotedIndexName ON $quotedTableName $columnsList" + "CREATE INDEX $quotedIndexName ON $quotedTableName $columnsList$maybeFilterCondition" } } } - protected open fun createIndexWithType(name: String, table: String, columns: String, type: String): String { - return "CREATE INDEX $name ON $table $columns USING $type" + protected open fun createIndexWithType(name: String, table: String, columns: String, type: String, filterCondition: String): String { + return "CREATE INDEX $name ON $table $columns USING $type$filterCondition" } - override fun dropIndex(tableName: String, indexName: String): String { - val identifierManager = TransactionManager.current().db.identifierManager + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartial: Boolean): String { return "ALTER TABLE ${identifierManager.quoteIfNecessary(tableName)} DROP CONSTRAINT ${identifierManager.quoteIfNecessary(indexName)}" } 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 1cf2d381a9..68c4b0bbac 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 @@ -279,7 +279,8 @@ open class MysqlDialect : VendorDialect(dialectName, MysqlDataTypeProvider, Mysq } } - override fun dropIndex(tableName: String, indexName: String): String = "ALTER TABLE $tableName DROP INDEX $indexName" + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartial: Boolean): String = + "ALTER TABLE ${identifierManager.quoteIfNecessary(tableName)} DROP INDEX ${identifierManager.quoteIfNecessary(indexName)}" override fun setSchema(schema: Schema): String = "USE ${schema.identifier}" diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index a8fe39ae3e..55734df175 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -258,8 +258,16 @@ open class PostgreSQLDialect : VendorDialect(dialectName, PostgreSQLDataTypeProv override fun setSchema(schema: Schema): String = "SET search_path TO ${schema.identifier}" - override fun createIndexWithType(name: String, table: String, columns: String, type: String): String { - return "CREATE INDEX $name ON $table USING $type $columns" + override fun createIndexWithType(name: String, table: String, columns: String, type: String, filterCondition: String): String { + return "CREATE INDEX $name ON $table USING $type $columns$filterCondition" + } + + override fun dropIndex(tableName: String, indexName: String, isUnique: Boolean, isPartial: Boolean): String { + return if (isUnique && !isPartial) { + "ALTER TABLE IF EXISTS ${identifierManager.quoteIfNecessary(tableName)} DROP CONSTRAINT IF EXISTS ${identifierManager.quoteIfNecessary(indexName)}" + } else { + "DROP INDEX IF EXISTS ${identifierManager.quoteIfNecessary(indexName)}" + } } companion object : DialectNameProvider("postgresql") 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 a0b5101917..07a09008a0 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 @@ -243,7 +243,7 @@ open class SQLServerDialect : VendorDialect(dialectName, SQLServerDataTypeProvid } } - override fun createIndexWithType(name: String, table: String, columns: String, type: String): String { + override fun createIndexWithType(name: String, table: String, columns: String, type: String, filterCondition: String): String { return "CREATE $type INDEX $name ON $table $columns" } diff --git a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt index fa5371d800..bda2ed1170 100644 --- a/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt +++ b/exposed-jdbc/src/main/kotlin/org/jetbrains/exposed/sql/statements/jdbc/JdbcDatabaseMetadataImpl.kt @@ -1,11 +1,6 @@ package org.jetbrains.exposed.sql.statements.jdbc -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.ForeignKeyConstraint -import org.jetbrains.exposed.sql.Index -import org.jetbrains.exposed.sql.ReferenceOption -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.api.ExposedDatabaseMetadata import org.jetbrains.exposed.sql.statements.api.IdentifierManagerApi import org.jetbrains.exposed.sql.transactions.TransactionManager @@ -25,6 +20,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) "MySQL-AB JDBC Driver", "MySQL Connector/J", "MySQL Connector Java" -> MysqlDialect.dialectName + "MariaDB Connector/J" -> MariaDBDialect.dialectName "SQLite JDBC" -> SQLiteDialect.dialectName "H2 JDBC Driver" -> H2Dialect.dialectName @@ -177,10 +173,12 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) dialect is OracleDialect || h2Mode == H2CompatibilityMode.Oracle -> defaultValue.trim().trim('\'') dialect is MysqlDialect || h2Mode == H2CompatibilityMode.MySQL || h2Mode == H2CompatibilityMode.MariaDB -> defaultValue.substringAfter("b'").trim('\'') + dialect is PostgreSQLDialect || h2Mode == H2CompatibilityMode.PostgreSQL -> when { defaultValue.startsWith('\'') && defaultValue.endsWith('\'') -> defaultValue.trim('\'') else -> defaultValue } + else -> defaultValue.trim('\'') } } @@ -203,13 +201,14 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) } val rs = metadata.getIndexInfo(databaseName, currentScheme, tableName, false, false) - val tmpIndices = hashMapOf, MutableList>() + val tmpIndices = hashMapOf, MutableList>() while (rs.next()) { rs.getString("INDEX_NAME")?.let { val column = transaction.db.identifierManager.quoteIdentifierWhenWrongCaseOrNecessary(rs.getString("COLUMN_NAME")!!) val isUnique = !rs.getBoolean("NON_UNIQUE") - tmpIndices.getOrPut(it to isUnique) { arrayListOf() }.add(column) + val isPartial = if (rs.getString("FILTER_CONDITION").isNullOrEmpty()) null else Op.TRUE + tmpIndices.getOrPut(Triple(it, isUnique, isPartial)) { arrayListOf() }.add(column) } } rs.close() @@ -217,7 +216,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData) tmpIndices.filterNot { it.key.first in pkNames } .mapNotNull { (index, columns) -> columns.distinct().mapNotNull { cn -> tColumns[cn] }.takeIf { c -> c.size == columns.size } - ?.let { c -> Index(c, index.second, index.first) } + ?.let { c -> Index(c, index.second, index.first, filterCondition = index.third) } } } } 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 d8a58bdaed..7a09fc4242 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 @@ -1,13 +1,12 @@ package org.jetbrains.exposed.sql.tests.shared.ddl -import org.jetbrains.exposed.sql.Schema -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.exists +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.tests.DatabaseTestsBase import org.jetbrains.exposed.sql.tests.TestDB import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.assertTrue +import org.jetbrains.exposed.sql.vendors.currentDialect import org.junit.Test class CreateIndexTests : DatabaseTestsBase() { @@ -83,4 +82,68 @@ class CreateIndexTests : DatabaseTestsBase() { assertEquals(true, TestTable.exists()) } } + + + @Test + fun `test partial index`() { + val partialIndexTable = object : IntIdTable("PartialIndexTableTest") { + val name = varchar("name", 50) + val value = integer("value") + val anotherName = integer("anotherName") + val anotherValue = integer("anotherValue") + val flag = bool("flag") + init { + index("flag_index", columns = arrayOf(flag, name)) { + flag eq true + } + index(columns = arrayOf(value, name)) { + (name eq "aaa") and (value greaterEq 6) + } + uniqueIndex(columns = arrayOf(anotherValue)) + } + } + + withDb(listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG)) { + SchemaUtils.createMissingTablesAndColumns(partialIndexTable) + assertTrue(partialIndexTable.exists()) + + // check that indexes are created and contain the proper filtering conditions + exec( + """SELECT indexname AS INDEX_NAME, + substring(indexdef, strpos(indexdef, ' WHERE ') + 7) AS FILTER_CONDITION + FROM pg_indexes + WHERE tablename='partialindextabletest' AND indexname != 'partialindextabletest_pkey' + """.trimIndent() + ) { + var totalIndexCount = 0 + while (it.next()) { + totalIndexCount += 1 + val filter = it.getString("FILTER_CONDITION") + + when (it.getString("INDEX_NAME")) { + "partialindextabletest_value_name" -> assertEquals(filter, "(((name)::text = 'aaa'::text) AND (value >= 6))") + "flag_index" -> assertEquals(filter, "(flag = true)") + "partialindextabletest_anothervalue_unique" -> assertTrue(filter.startsWith(" UNIQUE INDEX ")) + } + } + kotlin.test.assertEquals(totalIndexCount, 3, "Indexes expected to be created") + } + + fun List>.names(): Set { return map { identity(it) }.toSet() } + fun getIndexes(): List { + db.dialect.resetCaches() + return currentDialect.existingIndices(partialIndexTable)[partialIndexTable].orEmpty() + } + + val dropIndex = Index(columns = listOf(partialIndexTable.value, partialIndexTable.name), unique = false).dropStatement().first() + kotlin.test.assertTrue(dropIndex.startsWith("DROP INDEX "), "Unique partial index must be created and dropped as index") + val dropUniqueConstraint = Index(columns = listOf(partialIndexTable.anotherValue), unique = true).dropStatement().first() + kotlin.test.assertTrue(dropUniqueConstraint.startsWith("ALTER TABLE "), "Unique index must be created and dropped as constraint") + + execInBatch(listOf(dropUniqueConstraint, dropIndex)) + + assertEquals(getIndexes().size, 1) + SchemaUtils.drop(partialIndexTable) + } + } }