Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add partial index support (Postgres only) #1748

Merged
merged 14 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ data class CheckConstraint(
}
}

typealias FilterCondition = (SqlExpressionBuilder.() -> Op<Boolean>)?

/**
* Represents an index.
*/
Expand All @@ -204,7 +206,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<Boolean>? = null
) : DdlAware {
/** Table where the index is defined. */
val table: Table
Expand All @@ -229,7 +233,7 @@ data class Index(

override fun createStatement(): List<String> = listOf(currentDialect.createIndex(this))
override fun modifyStatement(): List<String> = dropStatement() + createStatement()
override fun dropStatement(): List<String> = listOf(currentDialect.dropIndex(table.nameInDatabaseCase(), indexName))
override fun dropStatement(): List<String> = 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
Expand Down
35 changes: 26 additions & 9 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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("'", "")

Expand Down Expand Up @@ -478,25 +484,25 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
fun byte(name: String): Column<Byte> = registerColumn(name, ByteColumnType())

/** Creates a numeric column, with the specified [name], for storing 1-byte unsigned integers. */
fun ubyte(name: String): Column<UByte> = registerColumn(name, UByteColumnType())
fun ubyte(name: String): Column<UByte> = registerColumn(name, UByteColumnType())

/** Creates a numeric column, with the specified [name], for storing 2-byte integers. */
fun short(name: String): Column<Short> = registerColumn(name, ShortColumnType())

/** Creates a numeric column, with the specified [name], for storing 2-byte unsigned integers. */
fun ushort(name: String): Column<UShort> = registerColumn(name, UShortColumnType())
fun ushort(name: String): Column<UShort> = registerColumn(name, UShortColumnType())

/** Creates a numeric column, with the specified [name], for storing 4-byte integers. */
fun integer(name: String): Column<Int> = registerColumn(name, IntegerColumnType())

/** Creates a numeric column, with the specified [name], for storing 4-byte unsigned integers. */
fun uinteger(name: String): Column<UInt> = registerColumn(name, UIntegerColumnType())
fun uinteger(name: String): Column<UInt> = registerColumn(name, UIntegerColumnType())

/** Creates a numeric column, with the specified [name], for storing 8-byte integers. */
fun long(name: String): Column<Long> = registerColumn(name, LongColumnType())

/** Creates a numeric column, with the specified [name], for storing 8-byte unsigned integers. */
fun ulong(name: String): Column<ULong> = registerColumn(name, ULongColumnType())
fun ulong(name: String): Column<ULong> = registerColumn(name, ULongColumnType())

/** Creates a numeric column, with the specified [name], for storing 4-byte (single precision) floating-point numbers. */
fun float(name: String): Column<Float> = registerColumn(name, FloatColumnType())
Expand Down Expand Up @@ -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 A custom index type (e.g., "BTREE" or "HASH").
lure marked this conversation as resolved.
Show resolved Hide resolved
*/
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)))
}

/**
Expand All @@ -962,22 +975,25 @@ open class Table(name: String = "") : ColumnSet(), DdlAware {
*
* @param customIndexName Name of the index.
*/
fun <T> Column<T>.uniqueIndex(customIndexName: String? = null): Column<T> = index(customIndexName, true)
fun <T> Column<T>.uniqueIndex(customIndexName: String? = null): Column<T> =
index(customIndexName, true)

/**
* Creates a unique index.
*
* @param columns Columns that compose the index.
*/
fun uniqueIndex(vararg columns: Column<*>): Unit = index(null, true, *columns)
fun uniqueIndex(vararg columns: Column<*>, filterCondition: FilterCondition = null): Unit =
bog-walk marked this conversation as resolved.
Show resolved Hide resolved
index(null, true, *columns, filterCondition = filterCondition)

/**
* Creates a unique index.
*
* @param customIndexName Name of the index.
* @param columns Columns that compose the index.
*/
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 =
bog-walk marked this conversation as resolved.
Show resolved Hide resolved
index(customIndexName, true, *columns, filterCondition = filterCondition)

/**
* Creates a composite foreign key.
Expand Down Expand Up @@ -1074,6 +1090,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")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,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<String>
Expand Down Expand Up @@ -879,7 +879,6 @@ sealed class ForUpdateOption(open val querySuffix: String) {

class ForUpdate(mode: MODE? = null, vararg ofTables: Table) : ForUpdateBase("FOR UPDATE", mode, *ofTables)


open class ForNoKeyUpdate(mode: MODE? = null, vararg ofTables: Table) : ForUpdateBase("FOR NO KEY UPDATE", mode, *ofTables) {
companion object : ForNoKeyUpdate()
}
Expand Down Expand Up @@ -910,6 +909,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 */
Expand Down Expand Up @@ -1013,30 +1015,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
}
}

/**
* The uniquieness might be required for foreign constraints
lure marked this conversation as resolved.
Show resolved Hide resolved
*
* In postgresq (https://www.postgresql.org/docs/current/indexes-unique.html) Uniq means btree only.
lure marked this conversation as resolved.
Show resolved Hide resolved
* 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 -> {
// uniq and no filter -> constraint, the type is not supported
lure marked this conversation as resolved.
Show resolved Hide resolved
index.unique && maybeFilterCondition.isEmpty() -> {
"ALTER TABLE $quotedTableName ADD CONSTRAINT $quotedIndexName UNIQUE $columnsList"
}
// uniq and filter -> index only, the type is not supported
lure marked this conversation as resolved.
Show resolved Hide resolved
index.unique -> {
"CREATE UNIQUE INDEX $quotedIndexName ON $quotedTableName $columnsList$maybeFilterCondition"
}
// type -> can't be uniq or constraint
lure marked this conversation as resolved.
Show resolved Hide resolved
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)}"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,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)}"
joc-a marked this conversation as resolved.
Show resolved Hide resolved

override fun setSchema(schema: Schema): String = "USE ${schema.identifier}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() {
}
toString()
}

override fun upsert(
table: Table,
data: List<Pair<Column<*>, Any?>>,
Expand Down Expand Up @@ -256,8 +256,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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,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"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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('\'')
}
}
Expand All @@ -203,21 +201,22 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData)
}
val rs = metadata.getIndexInfo(databaseName, currentScheme, tableName, false, false)

val tmpIndices = hashMapOf<Pair<String, Boolean>, MutableList<String>>()
val tmpIndices = hashMapOf<Triple<String, Boolean, Op.TRUE?>, MutableList<String>>()

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()
val tColumns = table.columns.associateBy { transaction.identity(it) }
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) }
}
}
}
Expand Down
Loading