Skip to content
Merged
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
6 changes: 6 additions & 0 deletions pkg/ddl/create_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,12 @@ func BuildTableInfo(
return nil, err
}
isSingleIntPK := isSingleIntPKFromCol(constr, lastCol)
if constr.Option != nil && constr.Option.Condition != nil {
// Theoretically, if the index is not a clustered index and also not PKIsHandle, it can be a partial index
// because it'll have no difference compared to a normal index. However, for simplicity, this branch blocks
// all partial primary key.
return nil, dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs("create an primary key with partial index is not supported")
}
if ShouldBuildClusteredIndex(ctx.GetClusteredIndexDefMode(), constr.Option, isSingleIntPK) {
if isSingleIntPK {
tbInfo.PKIsHandle = true
Expand Down
8 changes: 8 additions & 0 deletions pkg/ddl/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4975,6 +4975,13 @@ func (e *executor) createIndex(ctx sessionctx.Context, ti ast.Ident, keyType ast
return errors.Trace(err)
}

var conditionString string
if indexOption != nil {
conditionString, err = CheckAndBuildIndexConditionString(tblInfo, indexOption.Condition)
if err != nil {
return errors.Trace(err)
}
}
args := &model.ModifyIndexArgs{
IndexArgs: []*model.IndexArg{{
Unique: unique,
Expand All @@ -4983,6 +4990,7 @@ func (e *executor) createIndex(ctx sessionctx.Context, ti ast.Ident, keyType ast
IndexOption: indexOption,
HiddenCols: hiddenCols,
Global: global,
ConditionString: conditionString,
}},
OpType: model.OpAddIndex,
}
Expand Down
201 changes: 201 additions & 0 deletions pkg/ddl/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ import (
"github.com/pingcap/tidb/pkg/metrics"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/parser/charset"
"github.com/pingcap/tidb/pkg/parser/format"
pmodel "github.com/pingcap/tidb/pkg/parser/model"
"github.com/pingcap/tidb/pkg/parser/mysql"
"github.com/pingcap/tidb/pkg/parser/opcode"
"github.com/pingcap/tidb/pkg/parser/terror"
"github.com/pingcap/tidb/pkg/sessionctx"
"github.com/pingcap/tidb/pkg/sessionctx/variable"
Expand Down Expand Up @@ -418,6 +420,12 @@ func BuildIndexInfo(
}
idxInfo.Global = indexOption.Global
setGlobalIndexVersion(tblInfo, idxInfo)

conditionString, err := CheckAndBuildIndexConditionString(tblInfo, indexOption.Condition)
if err != nil {
return nil, errors.Trace(err)
}
idxInfo.ConditionExprString = conditionString
} else {
// Use btree as default index type.
idxInfo.Tp = pmodel.IndexTypeBtree
Expand Down Expand Up @@ -1019,6 +1027,10 @@ func (w *worker) onCreateIndex(jobCtx *jobContext, job *model.Job, isPK bool) (v
return ver, errors.Trace(err)
}
allIndexInfos = append(allIndexInfos, indexInfo)
// The condition in the index option is not marshaled, so we need to set it here.
if len(arg.ConditionString) > 0 {
indexInfo.ConditionExprString = arg.ConditionString
}
}

originalState := allIndexInfos[0].State
Expand Down Expand Up @@ -3699,3 +3711,192 @@ func renameHiddenColumns(tblInfo *model.TableInfo, from, to pmodel.CIStr) {
}
}
}

// CheckAndBuildIndexConditionString validates whether the given expression is compatible with
// the table schema and returns a string representation of the expression.
func CheckAndBuildIndexConditionString(tblInfo *model.TableInfo, indexConditionExpr ast.ExprNode) (string, error) {
if indexConditionExpr == nil {
return "", nil
}

// check partial index condition expression
err := checkIndexCondition(tblInfo, indexConditionExpr)
if err != nil {
return "", errors.Trace(err)
}

var sb strings.Builder
restoreFlags := format.RestoreStringSingleQuotes | format.RestoreKeyWordLowercase | format.RestoreNameBackQuotes |
format.RestoreSpacesAroundBinaryOperation | format.RestoreWithoutSchemaName | format.RestoreWithoutTableName
restoreCtx := format.NewRestoreCtx(restoreFlags, &sb)
sb.Reset()
err = indexConditionExpr.Restore(restoreCtx)
if err != nil {
return "", errors.Trace(err)
}

return sb.String(), nil
}

func checkIndexCondition(tblInfo *model.TableInfo, indexCondition ast.ExprNode) error {
// Only the following expressions are supported:
// 1. column IS NULL
// 2. column IS NOT NULL
// 3. column = / != / > / < / >= / <= const
// The column must be a visible column in the table, and the const must be a literal value with
// the same type as the column.
// The column must **NOT** be a generated column. We can loosen this restriction in the future.
//
// TODO: support more expressions in the future.
if indexCondition == nil {
return nil
}

switch cond := indexCondition.(type) {
case *ast.IsNullExpr:
// `IS NULL` and `IS NOT NULL` are both in this branch.
columnName, ok := cond.Expr.(*ast.ColumnNameExpr)
if !ok {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
"partial index condition must include a column name in the IS NULL expression")
}
columnInfo := model.FindColumnInfo(tblInfo.Columns, columnName.Name.Name.L)
if columnInfo == nil {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("column name %s referenced in partial index condition is not found in table",
columnName.Name.Name.L))
}
if columnInfo.IsGenerated() {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("generated column %s cannot be used in partial index condition", columnName.Name.Name.L))
}

return nil
case *ast.BinaryOperationExpr:
if cond.Op != opcode.EQ && cond.Op != opcode.NE && cond.Op != opcode.GT &&
cond.Op != opcode.LT && cond.Op != opcode.GE && cond.Op != opcode.LE {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("binary operation %s is not supported", cond.Op.String()))
}

var columnName *ast.ColumnNameExpr
var anotherSide ast.ExprNode
columnName, ok := cond.L.(*ast.ColumnNameExpr)
if !ok {
// maybe the right side is a column name
columnName, ok = cond.R.(*ast.ColumnNameExpr)
if !ok {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
"partial index condition must include a column name in the binary operation")
}

anotherSide = cond.L
} else {
anotherSide = cond.R
}
columnInfo := model.FindColumnInfo(tblInfo.Columns, columnName.Name.Name.L)
if columnInfo == nil {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("column name `%s` referenced in partial index condition is not found in table",
columnName.Name.Name.L))
}
if columnInfo.IsGenerated() {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("generated column %s cannot be used in partial index condition", columnName.Name.Name.L))
}

// The another side must be a literal value, and it must have the same type as the column.
constantExpr, ok := anotherSide.(ast.ValueExpr)
if !ok {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
"partial index condition must include a literal value on the other side of the binary operation")
}
// Reference `types.DefaultTypeForValue`, they are all possible types for literal values.
Comment on lines +3808 to +3814
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact code region
sed -n '3780,3845p' pkg/ddl/index.go | cat -n

# Find all occurrences of the specific error message to locate full predicate parsing logic
rg -n "partial index condition must include a literal value" -S pkg/ddl/index.go

# Search nearby for any unary +/- unwrapping or similar helpers
rg -n "UnaryOperationExpr|unwrap|signed|DefaultTypeForValue" -S pkg/ddl/index.go

# Broader search: where partial index conditions are parsed/validated with literal requirements
rg -n "partial index condition" -S pkg/ddl pkg/parser pkg/executor pkg/planner

# Look for partial-index tests involving negative literals
rg -n "partial index" -S . | head -n 50
rg -n "<\\s*-1|<=\\s*\\+?1|>\\s*-1|>=\\s*\\+?1|\\(-1\\)|\\+1" -S . | head -n 50

Repository: pingcap/tidb

Length of output: 16114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '3680,3925p' pkg/ddl/index.go | cat -n

# Narrow search for unary operation handling within ddl/index.go
rg -n "UnaryOperationExpr" pkg/ddl/index.go
rg -n "ValueExpr" pkg/ddl/index.go | head -n 50
rg -n "partial index condition must include a literal value on the other side of the binary operation" -S .

# Look for partial index tests and any usage with negative literals
rg -n "add partial index|partial index" tests -S | head -n 200

# Specifically check for negative numeric literals in partial index related tests/DDL statements
rg -n "partial index[^\\n]*-[0-9]|-[0-9][^\\n]*partial index" -S tests | head -n 200

# Search for helper unwrapping/evaluating constants/unary ops near partial index logic
rg -n "unwrap.*unary|Unwrap.*Unary|Signed|Unary|constant.*fold|Eval|GetExpr.*Value|AsValueExpr|ValueExpr\\(" -S pkg/ddl/index.go pkg/ddl | head -n 200

Repository: pingcap/tidb

Length of output: 11410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '3680,3925p' pkg/ddl/index.go | cat -n

rg -n "UnaryOperationExpr" pkg/ddl/index.go
rg -n "ValueExpr" pkg/ddl/index.go | head -n 50
rg -n "partial index condition must include a literal value on the other side of the binary operation" -S .

rg -n "add partial index|partial index" tests -S | head -n 200
rg -n "partial index[^\n]*-[0-9]|-[0-9][^\n]*partial index" -S tests | head -n 200

rg -n "unwrap.*unary|Unwrap.*Unary|Signed|Unary|constant.*fold|Eval|AsValueExpr|ValueExpr\\(" -S pkg/ddl/index.go pkg/ddl | head -n 200

Repository: pingcap/tidb

Length of output: 11410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Look for any partial-index DDL tests/fixtures (exact phrase + variants)
rg -n "partial index|add partial index|ADD PARTIAL INDEX|partial_index" -S . | head -n 200

# 2) Look for parser/AST tests involving negative numeric literals and unary expressions
rg -n "UnaryOperationExpr" -S pkg/parser | head -n 200
rg -n "col\\s*[<>!=]=?\\s*-1|\\b-1\\b.*(parse|ast|expr)" -S pkg/parser pkg/executor pkg/planner | head -n 200
rg -n "(-[0-9]+)" -S pkg/parser/test | head -n 200 || true

# 3) If parser exposes node types in tests, search for "GetInt64" or ValueExpr construction around negative literals
rg -n "ValueExpr|NewValueExpr|driver\\.ValueExpr|ast\\.ValueExpr" -S pkg/parser | head -n 200

Repository: pingcap/tidb

Length of output: 50442


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find callers of CheckAndBuildIndexConditionString / checkIndexCondition
rg -n "CheckAndBuildIndexConditionString\\(" -S pkg/ddl | head -n 50
rg -n "checkIndexCondition\\(" -S pkg/ddl | head -n 50

# 2) Inspect the referenced partial index test file if it exists
ls -la tests/realtikvtest/addindextest4 || true
if [ -f tests/realtikvtest/addindextest4/partial_index_test.go ]; then
  sed -n '1,200p' tests/realtikvtest/addindextest4/partial_index_test.go | cat -n
  rg -n "partial index|add partial index|WHERE" tests/realtikvtest/addindextest4/partial_index_test.go
  rg -n "-[0-9]+|\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true
fi

# 3) Inspect surrounding code around the call site in pkg/ddl/index.go
# (Use the line of checkIndexCondition definition we already saw.)
sed -n '3600,3725p' pkg/ddl/index.go | cat -n

Repository: pingcap/tidb

Length of output: 6755


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for negative/positive literals in the partial index test file
rg -n -- "-[0-9]+|\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true
rg -n -- "where .*-[0-9]+|where .*\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Inspect surrounding code in pkg/ddl/index.go to see how indexOption.Condition is parsed/constructed
sed -n '390,520p' pkg/ddl/index.go | cat -n
sed -n '420,470p' pkg/ddl/index.go | cat -n

# Also inspect executor side call site if it does any expression preprocessing
sed -n '4920,5040p' pkg/ddl/executor.go | cat -n

Repository: pingcap/tidb

Length of output: 13135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check entire partial index test file for negative literals
rg -n -- "-[0-9]+|\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true
rg -n -- "where .*-[0-9]+|where .*\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Find other uses of indexOption.Condition within pkg/ddl
rg -n "indexOption\\.Condition" -S pkg/ddl | head -n 200

# Search for constant folding / expression simplification helpers near partial index condition building
rg -n "Fold|ConstantFold|Simplif|Simplify|normalize|unwrap|UnaryOperationExpr" -S pkg/ddl/index.go pkg/ddl | head -n 200

# Parser-side: find existing tests that assert unary minus structure around ValueExpr
rg -n "opcode\\.Minus|opcode\\.Plus" -S pkg/parser/parser_test.go pkg/parser/ast -S | head -n 200
rg -n -- "\"-1\"|\\b-1\\b" -S pkg/parser/parser_test.go | head -n 200

Repository: pingcap/tidb

Length of output: 5921


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check entire partial index test file for negative literals
rg -n -- "-[0-9]+|\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true
rg -n -- "where .*-[0-9]+|where .*\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Find other uses of indexOption.Condition within pkg/ddl
rg -n "indexOption\\.Condition" -S pkg/ddl | head -n 200

# Search for constant folding / expression simplification helpers near partial index condition building
rg -n "Fold|ConstantFold|Simplif|Simplify|normalize|unwrap|UnaryOperationExpr" -S pkg/ddl/index.go pkg/ddl | head -n 200

# Parser-side: find existing tests that assert unary +/- structure around ValueExpr
rg -n "opcode\\.Minus|opcode\\.Plus" -S pkg/parser/parser_test.go pkg/parser/ast -S | head -n 200
rg -n -- "\"-1\"|\\b-1\\b" -S pkg/parser/parser_test.go | head -n 200

Repository: pingcap/tidb

Length of output: 5921


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Negative literal usage in partial index test
rg -n -- "-[0-9]+|\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Show any '-' occurrences near WHERE clauses (broader)
rg -n -- "where .*-[0-9]+|where .*\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Inspect parser grammar around unary +/- rules
sed -n '4130,4205p' pkg/parser/parser.y | cat -n

# Parser tests for -1 or unary operation parsing
rg -n -- "\\b-1\\b" pkg/parser/parser_test.go | head -n 50
rg -n -- "UnaryOperationExpr" pkg/parser/parser_test.go | head -n 50
rg -n -- "opcode\\.Minus|opcode\\.Plus" pkg/parser/parser_test.go | head -n 50

# Search for expr simplification/constant folding in ddl before checkIndexCondition
rg -n "Simplif|ConstantFold|Fold|Reduce|Unwrap|normalize|Eval" -S pkg/ddl/index.go pkg/ddl | head -n 200

Repository: pingcap/tidb

Length of output: 19073


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Negative literal usage in partial index test
rg -n -- "-[0-9]+|\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Show any '-' occurrences near WHERE clauses (broader)
rg -n -- "where .*-[0-9]+|where .*\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Inspect parser grammar around unary +/- rules
sed -n '4130,4205p' pkg/parser/parser.y | cat -n

# Parser tests for -1 or unary operation parsing
rg -n -- "\\b-1\\b" pkg/parser/parser_test.go | head -n 50
rg -n -- "UnaryOperationExpr" pkg/parser/parser_test.go | head -n 50
rg -n -- "opcode\\.Minus|opcode\\.Plus" pkg/parser/parser_test.go | head -n 50

# Search for expr simplification/constant folding in ddl before checkIndexCondition
rg -n -- "Simplif|ConstantFold|Fold|Reduce|Unwrap|normalize|Eval" pkg/ddl/index.go pkg/ddl | head -n 200

Repository: pingcap/tidb

Length of output: 19073


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search within partial index test file for any negative literals or unary +/- patterns
rg -n -- "-[0-9]+|\\+\\s*[0-9]+" tests/realtikvtest/addindextest4/partial_index_test.go || true

# Broader search for negative literals in partial index conditions
rg -n -- "partial index|where .*-[0-9]+|where .*\\+\\s*[0-9]+" tests -S | head -n 100

Repository: pingcap/tidb

Length of output: 13361


Unwrap signed numeric literals for partial index predicate validation

pkg/ddl/index.go’s checkIndexCondition only accepts the “constant” side of col <op> const as ast.ValueExpr (anotherSide.(ast.ValueExpr)), but the parser represents signed numerics as *ast.UnaryOperationExpr wrapping a ValueExpr (SignedLiteral: '-' NumLiteral / '+' NumLiteral in pkg/parser/parser.y). This rejects predicates like col1 > -1 / col1 <= +1 when defining a partial index. Unwrap unary +/- to the inner ValueExpr before the type assertion, and add a regression test in tests/realtikvtest/addindextest4/partial_index_test.go (e.g. where col1 > -100 and/or where col1 <= +1).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ddl/index.go` around lines 3808 - 3814, The partial-index predicate check
in checkIndexCondition (pkg/ddl/index.go) wrongly assumes the constant side is
an ast.ValueExpr; modify the logic to detect and unwrap an
ast.UnaryOperationExpr whose Operator is '+' or '-' and whose Operand is an
ast.ValueExpr before doing the type assertion, so signed numeric literals (e.g.,
-1, +1) are treated as ValueExprs; update the predicate validation to use the
unwrapped value for type checks and error messages. Also add a regression test
in tests/realtikvtest/addindextest4/partial_index_test.go that creates partial
indexes with predicates like "WHERE col1 > -100" and "WHERE col1 <= +1" to
ensure the unwrapping works.

// However, this switch-case still includes more types than the ones we have in that function
// to avoid breaking in the future.
//
// Accept tiny type conversion as the type of the literal value is too limited. We shouldn't
// force the user to use such a limited range of types.
//
// It'll allow precision / length difference in most of the cases.
switch constantExpr.GetType().GetType() {
case mysql.TypeTiny, mysql.TypeShort, mysql.TypeLong, mysql.TypeLonglong,
mysql.TypeInt24, mysql.TypeBit, mysql.TypeYear:
// the target column must be an integer type or enum or set
if columnInfo.GetType() != mysql.TypeTiny &&
columnInfo.GetType() != mysql.TypeShort &&
columnInfo.GetType() != mysql.TypeLong &&
columnInfo.GetType() != mysql.TypeLonglong &&
columnInfo.GetType() != mysql.TypeInt24 &&
columnInfo.GetType() != mysql.TypeBit &&
columnInfo.GetType() != mysql.TypeYear &&
columnInfo.GetType() != mysql.TypeEnum &&
columnInfo.GetType() != mysql.TypeSet {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("the type %s of the column `%s` in partial index condition is not compatible with the literal value type %s",
columnInfo.FieldType.String(), columnName.Name.Name.L, constantExpr.GetType().String()))
}
return nil
case mysql.TypeFloat, mysql.TypeDouble, mysql.TypeNewDecimal:
// the target column must be either a float or double type
// TODO: consider whether need to support decimal type in this branch
if columnInfo.GetType() != mysql.TypeFloat &&
columnInfo.GetType() != mysql.TypeDouble &&
columnInfo.GetType() != mysql.TypeNewDecimal {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("the type %s of the column `%s` in partial index condition is not compatible with the literal value type %s",
columnInfo.FieldType.String(), columnName.Name.Name.L, constantExpr.GetType().String()))
}
return nil
case mysql.TypeVarchar, mysql.TypeVarString, mysql.TypeString,
mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob, mysql.TypeBlob:
if types.IsString(columnInfo.GetType()) {
// check the collation of the column and the literal value
if columnInfo.FieldType.GetCharset() != constantExpr.GetType().GetCharset() {
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("the charset %s of the column `%s` in partial index condition is not compatible with the literal value charset %s",
columnInfo.FieldType.GetCharset(), columnName.Name.Name.L, constantExpr.GetType().GetCharset()))
}

return nil
}

// Allow to compare a datetime type column with a string literal, because we don't have a datetime literal.
// This branch will allow users to use datetime columns in index condition.
if columnInfo.GetType() == mysql.TypeTimestamp ||
columnInfo.GetType() == mysql.TypeDate ||
columnInfo.GetType() == mysql.TypeDuration ||
columnInfo.GetType() == mysql.TypeNewDate ||
columnInfo.GetType() == mysql.TypeDatetime {
return nil
}

// ENUM and SET are also allowed for string literal.
if columnInfo.GetType() == mysql.TypeEnum || columnInfo.GetType() == mysql.TypeSet {
return nil
}

return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("the type %s of the column `%s` in partial index condition is not compatible with the literal value type %s",
columnInfo.FieldType.String(), columnName.Name.Name.L, constantExpr.GetType().String()))
case mysql.TypeNull:
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
"= NULL is not supported in partial index condition because it is always false")
case mysql.TypeTimestamp, mysql.TypeDate, mysql.TypeDuration, mysql.TypeNewDate,
mysql.TypeDatetime, mysql.TypeJSON, mysql.TypeEnum, mysql.TypeSet:
// The `DATE '2025-07-28'` is actually a `cast` function, so they are also not supported yet.
intest.Assert(false, "should never generate literal values of these types")

return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("the type %s of the literal value in partial index condition is not supported",
constantExpr.GetType().String()))
default:
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
fmt.Sprintf("the type %s of the literal value in partial index condition is not supported",
constantExpr.GetType().String()))
}
default:
return dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs(
"the kind of partial index condition is not supported")
}
}
97 changes: 97 additions & 0 deletions pkg/ddl/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
package ddl_test

import (
"fmt"
"testing"

"github.com/pingcap/tidb/pkg/meta/model"
"github.com/pingcap/tidb/pkg/testkit"
"github.com/pingcap/tidb/pkg/testkit/testfailpoint"
"github.com/pingcap/tidb/pkg/util/dbterror"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -53,3 +55,98 @@ func TestDDLStatementsBackFill(t *testing.T) {
require.Equal(t, tc.expectedNeedReorg, needReorg, tc)
}
}

func TestPartialIndex(t *testing.T) {
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.MustExec("use test;")

// test validate column exists in create table
tk.MustExec("create table t (a int, b int, key(b) where a = 1);")
tk.MustGetDBError("create table t1 (a int, b int, key(b) where c = 1);",
dbterror.ErrUnsupportedAddPartialIndex)
tk.MustExec("drop table t;")

// test primary key is not allowed in partial index
tk.MustExec("create table t (a int, b int, key(b) where a = 1);")
tk.MustGetDBError("create table t2 (a int, b int, primary key(b) where a = 1);",
dbterror.ErrUnsupportedAddPartialIndex)
tk.MustExec("drop table t;")

checkColumnTypes := func(columnTypes []string, literals []string, shouldAllowed bool) {
for _, columnType := range columnTypes {
for _, literal := range literals {
tk.MustExec("drop table if exists t;")
sql := fmt.Sprintf("create table t (a %s, b int, key(b) where a = %s);", columnType, literal)
if shouldAllowed {
tk.MustExec(sql)
tk.MustExec("drop table t;")
} else {
tk.MustGetDBError(sql, dbterror.ErrUnsupportedAddPartialIndex)
}
}
}
}

// test create table type validation
differentTypeLiterals := [][]string{
{"1", "true", "1998"}, // int
{"'1'"}, // string with default collate
{"1.0"}, // float
{"b'101010'", "0x1234567890abcdef", "0b10"}, // binary literal
{"null"}, // null
}
differentColumnTypes := [][]string{
{"int", "bigint", "tinyint", "smallint", "year"},
{"char(25)", "varchar(123)", "text", "char(25) collate utf8mb4_general_ci", "char(25) collate utf8mb4_bin"},
{"float", "double"},
{"binary(25) collate binary", "varbinary(123)", "blob", "char(25) collate binary"},
{},
}
for i, columnTypes := range differentColumnTypes {
for j, literals := range differentTypeLiterals {
checkColumnTypes(columnTypes, literals, i == j)
}
}

// test comparing between time column and string constant is allowed.
timeColumnTypes := []string{"timestamp", "datetime", "date", "time"}
allowedLiterals := []string{"'2025-07-28 12:34:56'", "'2025-07-28'", "'12:34:56'"}
notAllowedLiterals := []string{"1", "1.0", "true", "null"}
checkColumnTypes(timeColumnTypes, allowedLiterals, true)
checkColumnTypes(timeColumnTypes, notAllowedLiterals, false)

// test comparing between enum/set column and int/string constant is allowed.
enumSetColumnTypes := []string{"enum('a', 'b', 'c')", "set('a', 'b', 'c')"}
allowedLiterals = []string{"1", "'1'", "'a'"}
notAllowedLiterals = []string{"1.0", "null"}
checkColumnTypes(enumSetColumnTypes, allowedLiterals, true)
checkColumnTypes(enumSetColumnTypes, notAllowedLiterals, false)

// test validate column exists in alter table
tk.MustExec("create table t (a int, b int);")
tk.MustExec("alter table t add index idx_b(b) where a = 1;")
tk.MustGetDBError("alter table t add index idx_b_2(b) where c = 1;",
dbterror.ErrUnsupportedAddPartialIndex)
tk.MustExec("drop table t;")

// test alter table type validation
for i, literals := range differentTypeLiterals {
for _, literal := range literals {
for j, columnTypes := range differentColumnTypes {
tk.MustExec("drop table if exists t;")
for _, columnType := range columnTypes {
sql := fmt.Sprintf("create table t (a %s, b int);", columnType)
tk.MustExec(sql)
sql = fmt.Sprintf("alter table t add index idx_b(b) where a = %s;", literal)
if i == j {
tk.MustExec(sql)
} else {
tk.MustGetDBError(sql, dbterror.ErrUnsupportedAddPartialIndex)
}
tk.MustExec("drop table t;")
}
}
}
}
}
Loading