diff --git a/pkg/embed/prepared_specialized_domains_test.go b/pkg/embed/prepared_specialized_domains_test.go index 45d600d3e662a..e3dfc8a824d4e 100644 --- a/pkg/embed/prepared_specialized_domains_test.go +++ b/pkg/embed/prepared_specialized_domains_test.go @@ -89,6 +89,153 @@ func TestPreparedSpecializedDomains(t *testing.T) { require.Equal(t, [][]string{{tc.want}}, query(t, "execute numeric_sum using @numeric_source"), tc.assignment) } }) + t.Run("prepared_round_truncate_value_domains", func(t *testing.T) { + for _, tc := range []struct { + name string + fn string + }{ + {name: "round", fn: "round"}, + {name: "truncate", fn: "truncate"}, + } { + fractionalResult := "1.4" + if tc.fn == "round" { + fractionalResult = "1.5" + } + t.Run(tc.name+"/sql_execute", func(t *testing.T) { + stmtName := "numeric_" + tc.name + exec(t, "prepare "+stmtName+" from 'select "+tc.fn+"(?,?)'") + defer conn.ExecContext(ctx, "deallocate prepare "+stmtName) + for _, value := range []struct { + assignment string + precision string + want string + }{ + {"'1.46'", "1", fractionalResult + "0"}, + {"cast(1.46 as decimal(10,2))", "1", fractionalResult + "0"}, + {"2", "0", "2"}, + {"null", "1", "NULL"}, + } { + exec(t, "set @numeric_value="+value.assignment+", @numeric_precision="+value.precision) + require.Equal(t, [][]string{{value.want}}, query(t, + "execute "+stmtName+" using @numeric_value,@numeric_precision"), value) + } + exec(t, "set @numeric_value='not-a-number', @numeric_precision=1") + err := func() error { + rows, err := conn.QueryContext(ctx, + "execute "+stmtName+" using @numeric_value,@numeric_precision") + if rows == nil { + return err + } + defer rows.Close() + for rows.Next() { + } + return rows.Err() + }() + require.Error(t, err, "invalid text should fail as on the GOOD baseline") + exec(t, "set @numeric_value='1.46'") + require.Equal(t, [][]string{{fractionalResult + "0"}}, + query(t, "execute "+stmtName+" using @numeric_value,@numeric_precision"), + "a failed execution must not poison the next binding") + }) + + t.Run(tc.name+"/binary_protocol", func(t *testing.T) { + stmt, err := conn.PrepareContext(ctx, "select "+tc.fn+"(?,?)") + require.NoError(t, err) + defer stmt.Close() + for _, value := range []struct { + input any + want string + }{ + {"1.46", fractionalResult + "0"}, + {int64(2), "2"}, + {float64(1.46), fractionalResult}, + {[]byte("1.46"), fractionalResult + "0"}, + {nil, "NULL"}, + } { + var got sql.NullString + require.NoError(t, stmt.QueryRowContext(ctx, value.input, 1).Scan(&got), value) + gotString := "NULL" + if got.Valid { + gotString = got.String + } + require.Equal(t, value.want, gotString, value) + } + }) + for _, shape := range []struct { + name string + value string + }{ + {"scalar", "(select ?)"}, + {"derived", "x"}, + } { + t.Run(tc.name+"/"+shape.name, func(t *testing.T) { + statement := "select cast(" + tc.fn + "(" + shape.value + ",1) as double)" + if shape.name == "derived" { + statement += " from (select ? x limit 1) d" + } + stmt, err := conn.PrepareContext(ctx, statement) + require.NoError(t, err) + defer stmt.Close() + var got sql.NullString + require.NoError(t, stmt.QueryRowContext(ctx, "1.46").Scan(&got)) + require.Equal(t, fractionalResult, got.String) + tieStatement := "select cast(" + tc.fn + "(" + shape.value + ",0) as double)" + if shape.name == "derived" { + tieStatement += " from (select ? x limit 1) d" + } + tie, tieErr := conn.PrepareContext(ctx, tieStatement) + require.NoError(t, tieErr) + defer tie.Close() + require.NoError(t, tie.QueryRowContext(ctx, "2.5").Scan(&got)) + if tc.name == "round" { + require.Equal(t, "3", got.String) + } else { + require.Equal(t, "2", got.String) + } + if shape.name == "derived" { + require.Error(t, stmt.QueryRowContext(ctx, "not-a-number").Scan(&got)) + } + require.NoError(t, stmt.QueryRowContext(ctx, "1.46").Scan(&got)) + require.Equal(t, fractionalResult, got.String) + }) + } + t.Run(tc.name+"/set_operation_domain", func(t *testing.T) { + stmt, err := conn.PrepareContext(ctx, + "select cast("+tc.fn+"(x,1) as double) from "+ + "(select ? x union all select cast(1.46 as decimal(10,2))) d") + require.NoError(t, err) + defer stmt.Close() + rows, err := stmt.QueryContext(ctx, int64(1)) + require.NoError(t, err) + defer rows.Close() + var got []string + for rows.Next() { + var value string + require.NoError(t, rows.Scan(&value)) + got = append(got, value) + } + require.NoError(t, rows.Err()) + fraction := "1.5" + if tc.name == "truncate" { + fraction = "1.4" + } + require.ElementsMatch(t, []string{"1", fraction}, got) + }) + } + + exec(t, "prepare explicit_decimal_round from 'select round(cast(? as decimal(10,2)),1)'") + defer conn.ExecContext(ctx, "deallocate prepare explicit_decimal_round") + exec(t, "set @explicit_decimal='1.46'") + require.Equal(t, [][]string{{"1.5"}}, query(t, + "execute explicit_decimal_round using @explicit_decimal")) + explicitDerived, err := conn.PrepareContext(ctx, + "select cast(round(x,0) as double) from (select cast(? as decimal(10,2)) x) d") + require.NoError(t, err) + defer explicitDerived.Close() + var explicitGot sql.NullString + require.NoError(t, explicitDerived.QueryRowContext(ctx, "2.5").Scan(&explicitGot)) + require.Equal(t, "3", explicitGot.String) + }) t.Run("ntile_null_runtime_error", func(t *testing.T) { exec(t, "create table ntile_source(id int)") exec(t, "insert into ntile_source values (1),(2)") diff --git a/pkg/frontend/computation_wrapper.go b/pkg/frontend/computation_wrapper.go index 03ab449e1d01e..7db8a83c17e28 100644 --- a/pkg/frontend/computation_wrapper.go +++ b/pkg/frontend/computation_wrapper.go @@ -643,7 +643,12 @@ func (cwft *TxnComputationWrapper) Compile(any any, fill func(*batch.Batch, *per */ } else { var planSnapshotTS *timestamp.Timestamp - if cwft.hasPlanSnapshotTS { + if cwft.preparedStmt != nil { + // Executable EXPLAIN uses the already bound prepared plan. Carry the + // binding's diagnostic proof and snapshot into Compile as EXECUTE does. + preparedExprRetry = cwft.preparedExecutionRetry() + planSnapshotTS = &cwft.preparedStmt.Ts + } else if cwft.hasPlanSnapshotTS { planSnapshotTS = &cwft.planSnapshotTS } cwft.compile, err = createCompile( diff --git a/pkg/sql/plan/base_binder.go b/pkg/sql/plan/base_binder.go index 2273e737b2e98..c2a61fbbce27e 100644 --- a/pkg/sql/plan/base_binder.go +++ b/pkg/sql/plan/base_binder.go @@ -1211,7 +1211,7 @@ func (b *baseBinder) bindNumericExprWithContextMode( defer func() { b.numericParamType = paramType }() return b.impl.BindExpr(astExpr, depth, false) } - if outer != nil && preparedSourceBindings(b.GetContext()) != nil { + if outer != nil && preparedSourceBindings(b.GetContext()) != nil && !functionTarget { if _, direct := unwrapParenExpr(astExpr).(*tree.ParamExpr); direct { // A bare assignment marker is a source value. Its destination cast // must retain string bytes (not arithmetic numeric coercion). @@ -1501,6 +1501,12 @@ func (b *baseBinder) numericAstTypesInternalWithHint( return numericAstTypeScan{}, err } scan := numericAstTypedOperand(typ) + if _, direct := unwrapParenExpr(expr.Expr).(*tree.ParamExpr); direct { + // The cast owns the result domain. Looking at the marker's text + // spelling here would make a type-stable plan value dependent. + scan.hasParam, scan.hasParamRef = true, true + return scan, nil + } // The explicit cast fixes the resulting type, but its source can still // contain a prepared marker. Preserve that marker for callers that need // to decide whether the value is execution-time supplied. @@ -3211,6 +3217,28 @@ func (b *baseBinder) bindFuncExpr(astExpr *tree.FuncExpr, depth int32, isRoot bo // the cached plan, and CHAR keeps a numeric context for prepared parameters // without changing the ordinary string-prefix semantics of direct CHAR // calls. + if b.builder != nil && (b.builder.isPrepareStatement || preparedSourceBindings(b.GetContext()) != nil) { + if (b.numericParamType == nil || b.ctx == nil || len(b.ctx.numericProjectionTypes) == 0) && + isPreparedNumericPrecisionFunction(funcName, len(astExpr.Exprs)) && + !isDirectExplicitNumericCast(astExpr.Exprs[0]) { + hasValueParam, err := b.hasPreparedNumericParamExprs(astExpr.Exprs[:1], depth) + if err != nil { + return nil, err + } + if hasValueParam { + return b.bindPreparedNumericPrecisionFuncExpr(funcName, astExpr.Exprs, depth, nil, -1) + } + if _, column := unwrapParenExpr(astExpr.Exprs[0]).(*tree.UnresolvedName); column { + value, bindErr := b.impl.BindExpr(astExpr.Exprs[0], depth, false) + if bindErr != nil { + return nil, bindErr + } + if pos, ok := b.preparedProjectedParamPosition(value); ok { + return b.bindPreparedNumericPrecisionFuncExpr(funcName, astExpr.Exprs, depth, value, pos) + } + } + } + } if b.builder != nil && b.builder.isPrepareStatement { // GET_LOCK distinguishes DECIMAL timeout conversion from DOUBLE. A bare // marker has no source type at PREPARE time, so use the established @@ -3256,6 +3284,139 @@ func (b *baseBinder) bindFuncExpr(astExpr *tree.FuncExpr, depth int32, isRoot bo return expr, err } +func isPreparedNumericPrecisionFunction(name string, argCount int) bool { + return (strings.EqualFold(name, "round") || strings.EqualFold(name, "truncate")) && + (argCount == 1 || argCount == 2) +} + +// bindPreparedNumericPrecisionFuncExpr keeps the value argument's overload +// open until EXECUTE. ROUND and TRUNCATE also have an integer precision +// argument, so only the value is bound in the deferred numeric domain. +func (b *baseBinder) bindPreparedNumericPrecisionFuncExpr( + name string, + astArgs []tree.Expr, + depth int32, + projectedValue *Expr, + projectedPosition int32, +) (*plan.Expr, error) { + if b.builder == nil || (!b.builder.isPrepareStatement && preparedSourceBindings(b.GetContext()) == nil) || + !isPreparedNumericPrecisionFunction(name, len(astArgs)) { + return b.bindFuncExprImplByAstExpr(name, astArgs, depth) + } + + doubleType := types.T_float64.ToType() + target := makePlan2Type(&doubleType) + hasExplicitFloatCast := containsExplicitFloatCast(astArgs[0]) + var value *Expr + var err error + if projectedValue != nil { + value = projectedValue + if !b.builder.isPrepareStatement { + binding, bindingErr := preparedSourceBindingAt(b.GetContext(), int(projectedPosition+1)) + if bindingErr != nil { + return nil, bindingErr + } + runtimeType, known := binding.NumericType, binding.NumericType.IsNumeric() + if !known && binding.Type.IsNumeric() { + runtimeType, known = binding.Type, true + } + if !known && binding.Type.Oid.IsMySQLString() { + runtimeType, known = preparedExactNumericStringType(b.GetContext(), int(projectedPosition)) + } + if !known && binding.Type.Oid.IsMySQLString() { + return b.bindFuncExprImplByAstExpr(name, astArgs, depth) + } + if known { + target = makePlan2Type(&runtimeType) + } + } + if makeTypeByPlan2Expr(value) != makeTypeByPlan2Type(target) { + value, err = appendCastBeforeExpr(b.GetContext(), value, target) + } + } else if !b.builder.isPrepareStatement && preparedSourceBindings(b.GetContext()) != nil { + if param, direct := unwrapParenExpr(astArgs[0]).(*tree.ParamExpr); direct { + binding, bindingErr := preparedSourceBindingAt(b.GetContext(), param.Offset) + if bindingErr != nil { + return nil, bindingErr + } + runtimeType, hasRuntimeType := binding.NumericType, binding.NumericType.IsNumeric() + if !hasRuntimeType && binding.Type.IsNumeric() { + runtimeType, hasRuntimeType = binding.Type, true + } + if !hasRuntimeType && binding.Type.Oid.IsMySQLString() { + runtimeType, hasRuntimeType = preparedExactNumericStringType( + b.GetContext(), int(param.Offset-1)) + } + if !hasRuntimeType { + // Keep invalid or nonnumeric text on ROUND/TRUNCATE's ordinary + // overload path. Its existing integer conversion reports the + // established error instead of silently turning the input into 0. + return b.bindFuncExprImplByAstExpr(name, astArgs, depth) + } + target = makePlan2Type(&runtimeType) + value, err = b.impl.BindExpr(astArgs[0], depth, false) + if err == nil && makeTypeByPlan2Expr(value) != runtimeType { + value, err = appendCastBeforeExpr(b.GetContext(), value, target) + } + } else { + exactScalar := false + if subquery, scalar := unwrapParenExpr(astArgs[0]).(*tree.Subquery); scalar { + scan, scanErr := b.numericScalarSubqueryAstTypes(subquery, depth) + if scanErr != nil { + return nil, scanErr + } + if scan.hasStringParam && len(scan.weakDecimals) == 1 && len(scan.strong) == 0 { + target = scan.weakDecimals[0] + exactScalar = true + } + } + if exactScalar { + previousParam, previousSubquery := b.numericParamType, b.numericSubqueryTarget + b.numericParamType, b.numericSubqueryTarget = &target, &target + value, err = b.impl.BindExpr(astArgs[0], depth, false) + b.numericParamType, b.numericSubqueryTarget = previousParam, previousSubquery + } else { + value, err = b.bindNumericExprWithContext(astArgs[0], depth, &target) + } + } + } else { + value, err = b.bindNumericExprWithContext(astArgs[0], depth, &target) + } + if err != nil { + return nil, err + } + if !hasExplicitFloatCast && !makeTypeByPlan2Expr(value).IsNumeric() { + // Execution-time source bindings intentionally keep a bare marker's + // transport type. ROUND/TRUNCATE still need a numeric provisional + // overload so a text marker cannot select the earlier integer overload. + value, err = appendCastBeforeExpr(b.GetContext(), value, target) + if err != nil { + return nil, err + } + } + if b.builder.isPrepareStatement && !hasExplicitFloatCast { + b.markPreparedNumericFallback(value) + } + + args := []*Expr{value} + if len(astArgs) == 2 { + var precision *Expr + if intTarget, integerContext := function.IntegerArgumentTarget(name, 1); integerContext { + precision, err = b.bindIntegerArgumentAst(astArgs[1], depth, intTarget) + } else { + precision, err = b.impl.BindExpr(astArgs[1], depth, false) + } + if err != nil { + return nil, err + } + args = append(args, precision) + } + return bindBoundFuncExprAndConstFoldWithObserver( + b.GetContext(), b.builder.compCtx.GetProcess(), name, args, + b.observePersistedExpressionProtocol, + ) +} + // bindGenericFunctionExpr keeps a whitespace-separated sensitive function // name on the stored-function/UDF path. With IGNORE_SPACE disabled, MySQL does // not recognize that spelling as a native built-in call. @@ -3454,6 +3615,25 @@ func (b *baseBinder) preparedExprContainsProjectedParam(expr *Expr) bool { return contains(expr) } +// preparedProjectedParamPosition follows a transparent derived-column projection +// back to its marker. Set operations and computed outputs establish their own +// domains, so use the plan's existing transparency check. +func (b *baseBinder) preparedProjectedParamPosition(expr *Expr) (int32, bool) { + if b.builder == nil || b.builder.qry == nil { + return 0, false + } + col := expr.GetCol() + if col == nil { + return 0, false + } + nodeID, ok := b.builder.tag2NodeID[col.RelPos] + if !ok { + return 0, false + } + return preparedProjectedOutputParamPosition( + b.builder.qry, nodeID, col.ColPos, make(map[preparedSetOperationNullKey]bool), false) +} + func isPreparedNumericAggregate(name string, argCount int) bool { return argCount == 1 && (strings.EqualFold(name, "sum") || strings.EqualFold(name, "avg")) } @@ -7262,6 +7442,11 @@ func bindFuncExprImplByPlanExpr( } return false } + if colOid.IsDecimal() && otherOid == types.T_float64 && otherExpr != nil { + if value, ok := decimalFloatComparisonConstant(otherExpr); ok { + return decimalFloatComparisonHasUniqueValue(value, colType) + } + } return false } @@ -9204,6 +9389,99 @@ func integerMetadataWidth(oid types.T) int32 { } } +func decimalFloatComparisonConstant(expr *Expr) (float64, bool) { + if expr == nil || expr.Typ.Id != int32(types.T_float64) { + return 0, false + } + if literal := expr.GetLit(); literal != nil { + if value, ok := literal.GetValue().(*plan.Literal_Dval); ok { + return value.Dval, true + } + } + fn := expr.GetF() + if fn == nil || fn.Func == nil || fn.Func.GetObjName() != "cast" || len(fn.Args) != 2 { + return 0, false + } + if expr.Typ.Width != 0 || expr.Typ.Scale >= 0 { + // DOUBLE(M,D) can round the source before the comparison. + return 0, false + } + source := fn.Args[0] + if inner := source.GetF(); inner != nil && inner.Func != nil && inner.Func.GetObjName() == "cast" && + len(inner.Args) == 2 && !isExplicitPreparedCast(source) { + if text := inner.Args[0].GetLit(); text != nil && + text.LiteralForm == plan.StringLiteralForm_STRING_LITERAL_TEXT && + types.T(inner.Args[0].Typ.Id).IsMySQLString() { + switch types.T(source.Typ.Id) { + case types.T_decimal64: + decimal, err := types.ParseDecimal64(text.GetSval(), source.Typ.Width, source.Typ.Scale) + if err == nil { + return types.Decimal64ToFloat64(decimal, source.Typ.Scale), true + } + case types.T_decimal128: + decimal, err := types.ParseDecimal128(text.GetSval(), source.Typ.Width, source.Typ.Scale) + if err == nil { + return types.Decimal128ToFloat64(decimal, source.Typ.Scale), true + } + } + } + } + literal := source.GetLit() + if literal == nil { + return 0, false + } + switch value := literal.GetValue().(type) { + case *plan.Literal_I64Val: + return float64(value.I64Val), true + case *plan.Literal_U64Val: + return float64(value.U64Val), true + case *plan.Literal_Dval: + return value.Dval, true + case *plan.Literal_Decimal64Val: + if value.Decimal64Val != nil { + return types.Decimal64ToFloat64(types.Decimal64(value.Decimal64Val.A), fn.Args[0].Typ.Scale), true + } + case *plan.Literal_Decimal128Val: + if value.Decimal128Val != nil { + coefficient := types.Decimal128{B0_63: uint64(value.Decimal128Val.A), B64_127: uint64(value.Decimal128Val.B)} + return types.Decimal128ToFloat64(coefficient, fn.Args[0].Typ.Scale), true + } + } + return 0, false +} + +// A native DECIMAL equality is equivalent to the usual DOUBLE comparison only +// when exactly one value at the column's scale converts to the peer DOUBLE. +// Conversion is monotone, so checking the adjacent representable DECIMAL +// values rules out every other value in the column domain. +func decimalFloatComparisonHasUniqueValue(value float64, column types.Type) bool { + if math.IsNaN(value) || math.IsInf(value, 0) || column.Scale < 0 { + return false + } + switch column.Oid { + case types.T_decimal64: + candidate, err := types.Decimal64FromFloat64(value, column.Width, column.Scale) + if err != nil || types.Decimal64ToFloat64(candidate, column.Scale) != value { + return false + } + return types.Decimal64ToFloat64(candidate-1, column.Scale) != value && + types.Decimal64ToFloat64(candidate+1, column.Scale) != value + case types.T_decimal128: + candidate, err := types.Decimal128FromFloat64(value, column.Width, column.Scale) + if err != nil || types.Decimal128ToFloat64(candidate, column.Scale) != value { + return false + } + previous, err := candidate.Add128(types.Decimal128{B0_63: 1}.Minus()) + if err != nil || types.Decimal128ToFloat64(previous, column.Scale) == value { + return false + } + next, err := candidate.Add128(types.Decimal128{B0_63: 1}) + return err == nil && types.Decimal128ToFloat64(next, column.Scale) != value + default: + return false + } +} + // foldDecimalStringComparisonConstants materializes only deterministic string // constants before the generic numeric overload resolver can erase their exact // value. Runtime expressions retain the existing REAL comparison domain. diff --git a/pkg/sql/plan/bind_context.go b/pkg/sql/plan/bind_context.go index 0c5560aa15bfa..50b671054194c 100644 --- a/pkg/sql/plan/bind_context.go +++ b/pkg/sql/plan/bind_context.go @@ -75,7 +75,6 @@ func NewBindContext(builder *QueryBuilder, parent *BindContext) *BindContext { bc.snapshot = parent.snapshot bc.remapOption = parent.remapOption bc.numericCteByName = parent.numericCteByName - bc.assignmentIgnore = parent.assignmentIgnore if len(parent.viewChain) > 0 { bc.viewChain = append([]string{}, parent.viewChain...) } diff --git a/pkg/sql/plan/bind_insert.go b/pkg/sql/plan/bind_insert.go index 4f11e95109198..e47ce9f71af9d 100644 --- a/pkg/sql/plan/bind_insert.go +++ b/pkg/sql/plan/bind_insert.go @@ -4697,7 +4697,7 @@ func (builder *QueryBuilder) initInsertReplaceStmt(bindCtx *BindContext, astRows astSelect = astRows subCtx := NewBindContext(builder, bindCtx) - subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef) + subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef, builder.isInsertIgnore) lastNodeID, err = builder.bindSelect(astSelect, subCtx, false) if err != nil { return 0, nil, nil, -1, err @@ -4711,7 +4711,7 @@ func (builder *QueryBuilder) initInsertReplaceStmt(bindCtx *BindContext, astRows astSelect = selectImpl.Select subCtx := NewBindContext(builder, bindCtx) - subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef) + subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef, builder.isInsertIgnore) lastNodeID, err = builder.bindSelect(astSelect, subCtx, false) if err != nil { return 0, nil, nil, -1, err @@ -5006,13 +5006,29 @@ func isNumericAssignmentTarget(typ Type) bool { return makeTypeByPlan2Type(typ).IsNumeric() } -func insertProjectionTypes(insertColumns []string, tableDef *plan.TableDef) []Type { +// useNumericAssignmentContext keeps IGNORE conversion at the final assignment +// cast. A destination hint must not insert an ordinary strict cast into its +// source expression. Functions and aggregates establish their own numeric +// consumer contexts independently. +func useNumericAssignmentContext(typ Type, ignore bool) bool { + return isNumericAssignmentTarget(typ) && !(ignore && useIgnoreConversionAssignmentCast(typ)) +} + +func isPreparedAssignmentParam(builder *QueryBuilder, expr tree.Expr) bool { + if builder == nil || !builder.isReusablePlan() { + return false + } + _, ok := unwrapParenExpr(expr).(*tree.ParamExpr) + return ok +} + +func insertProjectionTypes(insertColumns []string, tableDef *plan.TableDef, ignore bool) []Type { // only numeric targets may seed the numeric assignment context; a zero // Type keeps the projection binder on the default binding path targets := make([]Type, len(insertColumns)) for i, column := range insertColumns { typ := tableDef.Cols[tableDef.Name2ColIndex[column]].Typ - if isNumericAssignmentTarget(typ) { + if useNumericAssignmentContext(typ, ignore) { targets[i] = typ } } @@ -5490,7 +5506,7 @@ func (builder *QueryBuilder) buildValueScan( valueBinder = funcBinder } boundWithNumericContext := false - if isNumericAssignmentTarget(col.Typ) { + if useNumericAssignmentContext(col.Typ, builder.isInsertIgnore) { if builder.isPrepareStatement { // Analyze prepared functions with the target-free binder. The // explicit numeric context below supplies the assignment domain only @@ -5505,21 +5521,12 @@ func (builder *QueryBuilder) buildValueScan( if err != nil { return 0, nil, err } - // A bare marker is the source value of the assignment, not a - // numeric expression. Integer assignments must retain that source - // until execute-time numeric typing; IGNORE must retain TEXT - // until the outer cast_ignore runs. Otherwise the prepare-time - // numeric context creates an ordinary cast(? AS INT/DECIMAL), and - // malformed values fail before the IGNORE warning/adjustment mode - // is reached. Keep numeric context for compound expressions such - // as ? + 1, whose operands genuinely need numeric binding. - directPreparedParam := false - if _, ok := unwrapParenExpr(r[i]).(*tree.ParamExpr); ok { - directPreparedParam = true - } - if scan.hasParam && !(directPreparedParam && - (types.T(col.Typ.Id).IsInteger() || (builder.isInsertIgnore && - useIgnoreConversionAssignmentCast(targetTyp.Typ)))) { + // The target type shapes compound numeric expressions, but a + // bare marker is still the assignment source. Preserve it for the + // final assignment cast, including string-to-BIT byte semantics and + // IGNORE conversion warnings. Expressions such as ? + 1 still bind + // in the target numeric context. + if scan.hasParam && !isPreparedAssignmentParam(builder, r[i]) { switch numericBinder := funcBinder.(type) { case *DefaultBinder: defExpr, err = numericBinder.bindNumericExprWithContext(r[i], 0, &col.Typ) @@ -5537,7 +5544,7 @@ func (builder *QueryBuilder) buildValueScan( if !boundWithNumericContext && valuesExprIsFuncCall(r[i]) { valueBinder = funcBinder } - } else if valuesExprIsFuncCall(r[i]) { + } else if valuesExprIsFuncCall(r[i]) || (builder.isInsertIgnore && isNumericAssignmentTarget(col.Typ)) { // Geometry and other non-numeric functions need their // arguments to bind independently of the destination type. valueBinder = funcBinder diff --git a/pkg/sql/plan/bind_update.go b/pkg/sql/plan/bind_update.go index dbe37c2809ac7..dce47396714b3 100644 --- a/pkg/sql/plan/bind_update.go +++ b/pkg/sql/plan/bind_update.go @@ -212,7 +212,7 @@ func (builder *QueryBuilder) appendSequentialSingleTableUpdateAssignments( rhsBindCtx, rhsBinder := builder.newSequentialUpdateProjectionBinder( bindCtx, currentNodeID, currentTag, tableDef, alias, currentProjectList, ) - if isNumericAssignmentTarget(tableDef.Cols[columnIndex].Typ) { + if useNumericAssignmentContext(tableDef.Cols[columnIndex].Typ, ignore) && !isPreparedAssignmentParam(builder, astExpr) { target := tableDef.Cols[columnIndex].Typ rhsBinder.numericTargetType = &target } @@ -349,7 +349,6 @@ func (builder *QueryBuilder) bindUpdate(stmt *tree.Update, bindCtx *BindContext) if err := validateUpdateWindowFunctions(builder.compCtx, stmt); err != nil { return 0, err } - bindCtx.assignmentIgnore = stmt.Ignore dmlCtx := NewDMLContext() err = dmlCtx.ResolveUpdateTables(builder.compCtx, stmt) @@ -511,7 +510,7 @@ func (builder *QueryBuilder) bindUpdate(stmt *tree.Update, bindCtx *BindContext) } oldPos := oldColName2Idx[alias+"."+colName] - if typ := tableDef.Cols[tableDef.Name2ColIndex[colName]].Typ; isNumericAssignmentTarget(typ) { + if typ := tableDef.Cols[tableDef.Name2ColIndex[colName]].Typ; useNumericAssignmentContext(typ, stmt.Ignore) && !isPreparedAssignmentParam(builder, updateExpr) { updateNumericTargets[oldPos] = typ } newColName2Idx[alias+"."+colName] = oldPos diff --git a/pkg/sql/plan/build_constraint_util.go b/pkg/sql/plan/build_constraint_util.go index 73534ba324ed1..baecd96d46188 100644 --- a/pkg/sql/plan/build_constraint_util.go +++ b/pkg/sql/plan/build_constraint_util.go @@ -763,7 +763,7 @@ func initInsertStmt(builder *QueryBuilder, bindCtx *BindContext, stmt *tree.Inse astSlt = stmt.Rows subCtx := NewBindContext(builder, bindCtx) - subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef) + subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef, builder.isInsertIgnore) info.rootId, err = builder.bindSelect(astSlt, subCtx, false) if err != nil { return false, nil, nil, err @@ -774,7 +774,7 @@ func initInsertStmt(builder *QueryBuilder, bindCtx *BindContext, stmt *tree.Inse astSlt = slt.Select subCtx := NewBindContext(builder, bindCtx) - subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef) + subCtx.numericProjectionTypes = insertProjectionTypes(insertColumns, tableDef, builder.isInsertIgnore) info.rootId, err = builder.bindSelect(astSlt, subCtx, false) if err != nil { return false, nil, nil, err diff --git a/pkg/sql/plan/comparison_cast_optimization_test.go b/pkg/sql/plan/comparison_cast_optimization_test.go index 8678f9d906e23..02c17e2cd1fb4 100644 --- a/pkg/sql/plan/comparison_cast_optimization_test.go +++ b/pkg/sql/plan/comparison_cast_optimization_test.go @@ -16,6 +16,7 @@ package plan import ( "context" + "math" "strconv" "testing" @@ -25,6 +26,41 @@ import ( "github.com/stretchr/testify/require" ) +func TestDecimalFloatComparisonUniqueValue(t *testing.T) { + decimal, err := makePlan2DecimalExprWithType(context.Background(), "12345.00") + require.NoError(t, err) + target := makeSimplePlan2Type(types.T_float64) + target.Scale = -1 + cast, err := makePlan2CastExpr(context.Background(), decimal, target) + require.NoError(t, err) + value, ok := decimalFloatComparisonConstant(cast) + require.True(t, ok) + require.Equal(t, float64(12345), value) + cast.Typ.Width, cast.Typ.Scale = 3, 1 + _, ok = decimalFloatComparisonConstant(cast) + require.False(t, ok, "DOUBLE(M,D) rounds before comparison") + + for _, test := range []struct { + name string + value float64 + column types.Type + unique bool + }{ + {"integral decimal64", 54321, types.New(types.T_decimal64, 12, 2), true}, + {"fractional decimal64", 0.1, types.New(types.T_decimal64, 12, 2), true}, + {"between scale points", 0.104, types.New(types.T_decimal64, 12, 2), false}, + {"negative", -54321, types.New(types.T_decimal64, 12, 2), true}, + {"outside width", 1e11, types.New(types.T_decimal64, 12, 2), false}, + {"decimal128 collision", 9007199254740992, types.New(types.T_decimal128, 20, 0), false}, + {"decimal128 ordinary", 54321, types.New(types.T_decimal128, 20, 2), true}, + {"infinity", math.Inf(1), types.New(types.T_decimal64, 12, 2), false}, + } { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.unique, decimalFloatComparisonHasUniqueValue(test.value, test.column)) + }) + } +} + // TestComparisonTypeCastOptimization tests that comparison operators avoid casting columns // when comparing with constants to preserve index usage func TestComparisonTypeCastOptimization(t *testing.T) { diff --git a/pkg/sql/plan/expr_opt.go b/pkg/sql/plan/expr_opt.go index 0fc6359b07825..b12384b101547 100644 --- a/pkg/sql/plan/expr_opt.go +++ b/pkg/sql/plan/expr_opt.go @@ -20,6 +20,7 @@ import ( "hash/fnv" "strconv" + "github.com/matrixorigin/matrixone/pkg/container/batch" "github.com/matrixorigin/matrixone/pkg/container/types" "github.com/matrixorigin/matrixone/pkg/container/vector" "github.com/matrixorigin/matrixone/pkg/pb/plan" @@ -48,6 +49,237 @@ func (builder *QueryBuilder) mergeFiltersOnCompositeKey(nodeID int32) { resetHashMapStats(node.Stats) } +func comparisonCastSource(expr *plan.Expr) *plan.Expr { + fn := expr.GetF() + if fn == nil || fn.Func == nil || fn.Func.GetObjName() != "cast" || len(fn.Args) != 2 { + return nil + } + return fn.Args[0] +} + +// Evaluate a bound DOUBLE peer only for the uniqueness proof. Keep the +// executable expression intact, and keep value-dependent plans out of the +// type-only prepared cache. +func (builder *QueryBuilder) decimalFloatPeerValue(expr *plan.Expr) (float64, bool) { + if value, ok := decimalFloatComparisonConstant(expr); ok { + return value, true + } + state := preparedBindingState(builder.GetContext()) + if state == nil || !state.selectStatement || expr.Typ.Id != int32(types.T_float64) || + !preparedExprContainsParam(expr) { + return 0, false + } + copy := DeepCopyExpr(expr) + var substitute func(*plan.Expr) bool + substitute = func(current *plan.Expr) bool { + if param := current.GetP(); param != nil { + binding, ok := state.bindingForPosition(param.Pos) + if !ok { + return false + } + if binding.Type.Oid == types.T_float64 && current.Typ.Id == int32(types.T_float64) { + bound, ok := preparedBoundDoubleValue(builder.GetContext(), current) + if !ok { + return false + } + current.Expr = &plan.Expr_Lit{Lit: &plan.Literal{Value: &plan.Literal_Dval{Dval: bound}}} + current.Typ = makeSimplePlan2Type(types.T_float64) + return true + } + if binding.Type.Oid.IsMySQLString() && types.T(current.Typ.Id).IsMySQLString() { + raw, present := preparedConfigurationValue(builder.GetContext(), current) + value, isText := raw.(string) + if !present || !isText || !PreparedNumericStringIsComplete(value) { + return false + } + current.Expr = &plan.Expr_Lit{Lit: &plan.Literal{Value: &plan.Literal_Sval{Sval: value}}} + return true + } + return false + } + if fn := current.GetF(); fn != nil { + for _, arg := range fn.Args { + if !substitute(arg) { + return false + } + } + return true + } + return current.GetLit() != nil || current.GetT() != nil + } + if !substitute(copy) || !rule.IsConstant(copy, false) { + return 0, false + } + folded, err := ConstantFold(batch.EmptyForConstFoldBatch, copy, builder.compCtx.GetProcess(), false, true) + if err != nil { + return 0, false + } + return decimalFloatComparisonConstant(folded) +} + +func (builder *QueryBuilder) singletonProjectedFloatValue(node *plan.Node, expr *plan.Expr) (float64, bool) { + column := expr.GetCol() + if column == nil { + return 0, false + } + for _, childID := range node.Children { + project := builder.qry.Nodes[childID] + if project.NodeType != plan.Node_PROJECT || len(project.BindingTags) != 1 || + project.BindingTags[0] != column.RelPos || len(project.Children) != 1 || + column.ColPos < 0 || int(column.ColPos) >= len(project.ProjectList) { + continue + } + input := builder.qry.Nodes[project.Children[0]] + if input.NodeType == plan.Node_VALUE_SCAN && input.TableDef == nil && + input.Limit == nil && input.Offset == nil && input.RankOption == nil { + candidate := project.ProjectList[column.ColPos] + if value, ok := builder.decimalFloatPeerValue(candidate); ok { + return value, true + } + if !rule.IsConstant(candidate, false) { + return 0, false + } + folded, err := ConstantFold(batch.EmptyForConstFoldBatch, DeepCopyExpr(candidate), + builder.compCtx.GetProcess(), false, true) + if err == nil { + return decimalFloatComparisonConstant(folded) + } + } + } + return 0, false +} + +func (builder *QueryBuilder) rewriteUniqueDecimalFloatComparison(node *plan.Node, expr *plan.Expr) *plan.Expr { + fn := expr.GetF() + if fn == nil || fn.Func == nil { + return expr + } + name := fn.Func.GetObjName() + isList := (name == "in" || name == "not_in") && len(fn.Args) == 2 && fn.Args[1].GetList() != nil + isBetween := name == "between" && len(fn.Args) == 3 + if !isList && !isBetween && (!isDecimalComparisonOperator(name) || len(fn.Args) != 2) { + return expr + } + for side := range fn.Args { + if (isList || isBetween) && side != 0 { + break + } + castColumn := fn.Args[side] + column := comparisonCastSource(castColumn) + if castColumn.Typ.Id != int32(types.T_float64) || isExplicitPreparedCast(castColumn) || + column == nil || column.GetCol() == nil || !types.T(column.Typ.Id).IsDecimal() { + continue + } + peers := []*plan.Expr{fn.Args[1-side]} + if isList { + peers = fn.Args[1].GetList().List + } else if isBetween { + peers = fn.Args[1:] + } + if len(peers) == 0 { + return expr + } + converted := make([]*plan.Expr, len(peers)) + for i, peer := range peers { + value, ok := builder.decimalFloatPeerValue(peer) + if !ok && node.NodeType == plan.Node_JOIN { + value, ok = builder.singletonProjectedFloatValue(node, peer) + } + if !ok || !decimalFloatComparisonHasUniqueValue(value, makeTypeByPlan2Expr(column)) { + break + } + var err error + converted[i], err = makePlan2CastExpr(builder.GetContext(), DeepCopyExpr(peer), column.Typ) + if err != nil { + break + } + } + if converted[len(converted)-1] == nil { + continue + } + args := make([]*plan.Expr, len(fn.Args)) + for i, arg := range fn.Args { + args[i] = DeepCopyExpr(arg) + } + args[side] = DeepCopyExpr(column) + if isList { + args[1].Typ = column.Typ + args[1].GetList().List = converted + } else if isBetween { + copy(args[1:], converted) + } else { + args[1-side] = converted[0] + } + rewritten, err := BindFuncExprImplByPlanExpr(builder.GetContext(), name, args) + if err == nil { + return rewritten + } + } + return expr +} + +// Scalar subqueries can expose constants only after filter pushdown. Rewrite +// proven numeric domains before scan statistics choose block filters. +func (builder *QueryBuilder) rewriteNumericDomainFilters(nodeID int32) { + state := preparedBindingState(builder.GetContext()) + roundEnabled := state != nil && state.selectStatement && state.hasRoundingFunction + var visit func(int32) + visit = func(id int32) { + node := builder.qry.Nodes[id] + for _, child := range node.Children { + visit(child) + } + if node.NodeType == plan.Node_JOIN { + for _, condition := range node.OnList { + _ = plan.VisitExprTree(condition, func(current *plan.Expr) error { + if rewritten := builder.rewriteUniqueDecimalFloatComparison(node, current); rewritten != current { + *current = *rewritten + } + return nil + }) + } + } + if node.NodeType != plan.Node_TABLE_SCAN { + return + } + for _, filter := range node.FilterList { + _ = plan.VisitExprTree(filter, func(current *plan.Expr) error { + if rewritten := builder.rewriteUniqueDecimalFloatComparison(node, current); rewritten != current { + *current = *rewritten + } + if !roundEnabled { + return nil + } + fn := current.GetF() + if fn == nil || fn.Func == nil || fn.Func.GetObjName() != "=" || len(fn.Args) != 2 { + return nil + } + for side := range fn.Args { + if isExplicitPreparedCast(fn.Args[side]) || isExplicitPreparedCast(fn.Args[1-side]) { + continue + } + column := comparisonCastSource(fn.Args[side]) + value := comparisonCastSource(fn.Args[1-side]) + if column == nil || column.GetCol() == nil || !types.T(column.Typ.Id).IsSignedInt() || value == nil { + continue + } + castValue, ok, err := preparedSafeRoundIntegerComparison(builder.GetContext(), DeepCopyExpr(value), column.Typ) + if err != nil || !ok { + continue + } + rewritten, err := BindFuncExprImplByPlanExpr(builder.GetContext(), "=", []*plan.Expr{DeepCopyExpr(column), castValue}) + if err == nil { + *current = *rewritten + } + break + } + return nil + }) + } + } + visit(nodeID) +} + // collectCompositePartBlockFilters preserves zonemappable predicates on the // physical columns that make up a composite primary/cluster key. The regular // filter rewrite replaces a useful equality/range prefix with one predicate on diff --git a/pkg/sql/plan/function/func_cast.go b/pkg/sql/plan/function/func_cast.go index ca8b09f6d0be9..c6e290a6aee01 100644 --- a/pkg/sql/plan/function/func_cast.go +++ b/pkg/sql/plan/function/func_cast.go @@ -1196,7 +1196,7 @@ func newCast(parameters []*vector.Vector, result vector.FunctionResultWrapper, p case types.T_json: s := vector.GenerateFunctionStrParameter(from) err = jsonToOthers(execProc, execProc.Ctx, s, *toType, result, length, selectList, - strictStringWidth, allowTrailingSpaceTrim, mode.isAssignment(), reportDataTooLong) + strictStringWidth, allowTrailingSpaceTrim, mode.isAssignment(), mode == castModeExplicit, reportDataTooLong) case types.T_enum: s := vector.GenerateFunctionFixedTypeParameter[types.Enum](from) err = enumToOthers(execProc.Ctx, s, *toType, result, length, selectList, strictStringWidth, reportDataTooLong) @@ -2944,7 +2944,7 @@ func blockidToOthers(ctx context.Context, func jsonToOthers(proc *process.Process, ctx context.Context, source vector.FunctionParameterWrapper[types.Varlena], toType types.Type, result vector.FunctionResultWrapper, length int, selectList *FunctionSelectList, - strictStringWidth bool, allowTrailingSpaceTrim bool, assignment bool, reportDataTooLong bool) error { + strictStringWidth bool, allowTrailingSpaceTrim bool, assignment bool, explicitCast bool, reportDataTooLong bool) error { switch toType.Oid { case types.T_json: rs := vector.MustFunctionResult[types.Varlena](result) @@ -2958,7 +2958,7 @@ func jsonToOthers(proc *process.Process, ctx context.Context, case types.T_char, types.T_varchar, types.T_blob, types.T_text, types.T_datalink: rs := vector.MustFunctionResult[types.Varlena](result) return jsonToStr(proc, ctx, source, rs, length, selectList, - strictStringWidth, allowTrailingSpaceTrim, assignment, reportDataTooLong) + strictStringWidth, allowTrailingSpaceTrim, assignment, explicitCast, reportDataTooLong) case types.T_bool: return jsonToBool(ctx, source, result, length) case types.T_int8, types.T_int16, types.T_int32, types.T_int64, @@ -9847,7 +9847,7 @@ func jsonToStr( ctx context.Context, from vector.FunctionParameterWrapper[types.Varlena], to *vector.FunctionResult[types.Varlena], length int, selectList *FunctionSelectList, - strictStringWidth bool, allowTrailingSpaceTrim bool, assignment bool, reportDataTooLong bool) error { + strictStringWidth bool, allowTrailingSpaceTrim bool, assignment bool, explicitCast bool, reportDataTooLong bool) error { var i uint64 toType := to.GetType() for i = 0; i < uint64(length); i++ { @@ -9859,7 +9859,9 @@ func jsonToStr( } else { bj := types.DecodeJson(v) var str string - if bj.Type == bytejson.TpCodeString { + if !explicitCast && bj.Type == bytejson.TpCodeString { + // Implicit casts and assignments expose the JSON string's character + // payload; only explicit CAST serializes the JSON string literal. s, err := bj.Unquote() if err != nil { return err diff --git a/pkg/sql/plan/function/func_cast_test.go b/pkg/sql/plan/function/func_cast_test.go index b4447f1deeadf..085d2489cb34b 100644 --- a/pkg/sql/plan/function/func_cast_test.go +++ b/pkg/sql/plan/function/func_cast_test.go @@ -4623,26 +4623,54 @@ func TestBitToJSONRestoresDeclaredWidth(t *testing.T) { require.Error(t, err) } -// TestCastJsonToVarchar verifies that casting a JSON value to VARCHAR uses JSON_UNQUOTE semantics, -// i.e. JSON strings lose their outer double-quotes (MySQL-compatible behavior). +// TestCastJsonToVarchar distinguishes explicit JSON serialization from the +// scalar text used by implicit casts, comparisons, and assignments. func TestCastJsonToVarchar(t *testing.T) { proc := testutil.NewProcess(t) jsonTexts := []string{`"active"`, `42`, `true`, `null`, `[1,2,3]`, `{"k":"v"}`} - // After unquote: JSON strings lose outer quotes; other types keep their JSON text representation. - expected := []string{"active", "42", "true", "null", "[1, 2, 3]", `{"k": "v"}`} nulls := []bool{false, false, false, false, false, false} encoded := makeJSONEncodedFromText(t, jsonTexts, nulls) toType := types.New(types.T_varchar, 256, 0) - inputs := []FunctionTestInput{ - NewFunctionTestInput(types.T_json.ToType(), encoded, nulls), - NewFunctionTestInput(toType, []string{}, []bool{}), + cases := []struct { + name string + cast fEvalFn + expected []string + }{ + { + name: "explicit cast serializes JSON strings", + cast: NewExplicitCast, + expected: []string{`"active"`, "42", "true", "null", "[1, 2, 3]", `{"k": "v"}`}, + }, + { + name: "implicit cast exposes string payload", + cast: NewCast, + expected: []string{"active", "42", "true", "null", "[1, 2, 3]", `{"k": "v"}`}, + }, + { + name: "comparison cast exposes string payload", + cast: NewComparisonCast, + expected: []string{"active", "42", "true", "null", "[1, 2, 3]", `{"k": "v"}`}, + }, + { + name: "assignment cast exposes string payload", + cast: NewAssignCast, + expected: []string{"active", "42", "true", "null", "[1, 2, 3]", `{"k": "v"}`}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + inputs := []FunctionTestInput{ + NewFunctionTestInput(types.T_json.ToType(), encoded, nulls), + NewFunctionTestInput(toType, []string{}, []bool{}), + } + expect := NewFunctionTestResult(toType, false, tc.expected, nulls) + fcTC := NewFunctionTestCase(proc, inputs, expect, tc.cast) + succeed, info := fcTC.Run() + require.True(t, succeed, info) + }) } - expect := NewFunctionTestResult(toType, false, expected, nulls) - fcTC := NewFunctionTestCase(proc, inputs, expect, NewCast) - succeed, info := fcTC.Run() - require.True(t, succeed, info) } // emptySliceForCastTarget returns an empty slice of the right type for the second (target type) cast parameter. diff --git a/pkg/sql/plan/function/func_cast_width_test.go b/pkg/sql/plan/function/func_cast_width_test.go index 911ecca9afb41..d94b4ba72b06d 100644 --- a/pkg/sql/plan/function/func_cast_width_test.go +++ b/pkg/sql/plan/function/func_cast_width_test.go @@ -868,7 +868,7 @@ func runJSONToStrWidth(t *testing.T, mp *mpool.MPool, jsonText string, toType ty defer to.Free() require.NoError(t, to.PreExtendAndReset(1)) - if err := jsonToStr(proc, context.Background(), from, to, 1, nil, strict, allowTrim, allowTrim, allowTrim); err != nil { + if err := jsonToStr(proc, context.Background(), from, to, 1, nil, strict, allowTrim, allowTrim, false, allowTrim); err != nil { return "", err } got, _ := vector.GenerateFunctionStrParameter(to.GetResultVector()).GetStrValue(0) diff --git a/pkg/sql/plan/issue_29317_29318_test.go b/pkg/sql/plan/issue_29317_29318_test.go index c4dda4568d700..8a2ac38fb3494 100644 --- a/pkg/sql/plan/issue_29317_29318_test.go +++ b/pkg/sql/plan/issue_29317_29318_test.go @@ -21,6 +21,8 @@ import ( "github.com/matrixorigin/matrixone/pkg/container/types" "github.com/matrixorigin/matrixone/pkg/container/vector" + "github.com/matrixorigin/matrixone/pkg/sql/parsers" + "github.com/matrixorigin/matrixone/pkg/sql/parsers/dialect" "github.com/stretchr/testify/require" ) @@ -462,3 +464,65 @@ func TestPreparedRoundRebindsNestedFixedDecimalChild(t *testing.T) { require.NotNil(t, round) require.Equal(t, int32(types.T_float64), round.GetF().Args[0].Typ.Id, round.String()) } + +func TestPreparedRoundAndTruncateKeepRuntimeValueDomain(t *testing.T) { + for _, name := range []string{"round", "truncate"} { + t.Run(name, func(t *testing.T) { + prepared, err := runOneStmt(NewMockOptimizer(false), t, + "prepare p from 'select "+name+"(?,?)'") + require.NoError(t, err) + template := prepared.GetDcl().GetPrepare().Plan + require.Equal(t, []int32{0, 1}, PreparedPlanNumericFallbackParamPositions(template), + "value overload and integer precision each require execution-time source decoding") + fn := findPlanFunctionExpr(template, name) + require.NotNil(t, fn) + require.Equal(t, int32(types.T_float64), fn.GetF().Args[0].Typ.Id, fn.String()) + require.Equal(t, int32(types.T_int64), fn.GetF().Args[1].Typ.Id, fn.String()) + + textValue := ParamValue{ + Value: "1.46", SourceType: types.T_varchar.ToType(), + HasSourceType: true, EnableNumericPrefix: true, + } + filled, specialized, err := FillValuesOfParamsInPlanWithSpecialization( + context.Background(), template, []any{textValue, int64(1)}) + require.NoError(t, err) + require.True(t, specialized) + fn = findPlanFunctionExpr(filled, name) + require.NotNil(t, fn) + require.True(t, types.T(fn.GetF().Args[0].Typ.Id).IsDecimal(), fn.String(), + "numeric text with a complete decimal spelling should keep its exact domain") + + decimalValue := ParamValue{ + Value: "1.46", PrepareParamKind: vector.PrepareParamDecimal, + } + filled, specialized, err = FillValuesOfParamsInPlanWithSpecialization( + context.Background(), template, []any{decimalValue, int64(1)}) + require.NoError(t, err) + require.True(t, specialized) + fn = findPlanFunctionExpr(filled, name) + require.NotNil(t, fn) + require.True(t, types.T(fn.GetF().Args[0].Typ.Id).IsDecimal(), fn.String()) + + explicit, err := runOneStmt(NewMockOptimizer(false), t, + "prepare p from 'select "+name+"(cast(? as decimal(10,2)),1)'") + require.NoError(t, err) + explicitPlan := explicit.GetDcl().GetPrepare().Plan + require.Empty(t, PreparedPlanNumericFallbackParamPositions(explicitPlan)) + + for _, valueExpr := range []string{"cast(? as decimal(10,2))", "(select cast(? as decimal(10,2)))"} { + stmt, parseErr := parsers.ParseOne(context.Background(), dialect.MYSQL, + "select "+name+"("+valueExpr+",1)", 1) + require.NoError(t, parseErr) + mock := NewMockOptimizer(false) + source := types.T_varchar.ToType() + bound, bindErr := BuildPreparedExecutionPlan(&mock.ctxt, stmt, + []PreparedSourceBinding{{Position: 0, Type: source}}, + []any{ParamValue{Value: "1.46", SourceType: source, HasSourceType: true}}) + require.NoError(t, bindErr) + require.False(t, bound.ValueDependent, + "an explicit numeric cast fixes the overload without inspecting text: %s", valueExpr) + stmt.Free() + } + }) + } +} diff --git a/pkg/sql/plan/ondup_update_binder.go b/pkg/sql/plan/ondup_update_binder.go index 21dbd02562084..5440928470a82 100644 --- a/pkg/sql/plan/ondup_update_binder.go +++ b/pkg/sql/plan/ondup_update_binder.go @@ -93,7 +93,10 @@ func (b *OndupUpdateBinder) BindExpr(astExpr tree.Expr, depth int32, isRoot bool } func (b *OndupUpdateBinder) BindAssignmentExpr(astExpr tree.Expr, target Type) (*plan.Expr, error) { - if !isNumericAssignmentTarget(target) { + if !useNumericAssignmentContext(target, b.builder.isInsertIgnore) { + return b.BindExpr(astExpr, 0, true) + } + if isPreparedAssignmentParam(b.builder, astExpr) { return b.BindExpr(astExpr, 0, true) } if subquery, ok := scalarSubqueryExpr(astExpr); ok && !subquery.Exists { diff --git a/pkg/sql/plan/opt_misc.go b/pkg/sql/plan/opt_misc.go index 9e01657412bc8..17f8d259b9363 100644 --- a/pkg/sql/plan/opt_misc.go +++ b/pkg/sql/plan/opt_misc.go @@ -2396,6 +2396,7 @@ func (builder *QueryBuilder) optimizeFilters(rootID int32) int32 { rootID, _ = builder.pushdownFilters(rootID, nil, false) transposeTableScanFilters(builder.compCtx.GetProcess(), builder.qry, rootID) foldTableScanFilters(builder.compCtx.GetProcess(), builder.qry, rootID, false) + builder.rewriteNumericDomainFilters(rootID) ReCalcNodeStats(rootID, builder, true, true, true) builder.rewriteInDomainNotInFilters(rootID) compositePartBlockFilters := builder.collectCompositePartBlockFilters(rootID) diff --git a/pkg/sql/plan/prepared_binding.go b/pkg/sql/plan/prepared_binding.go index 9fd3b3f333ac8..7b3fb4a712b8d 100644 --- a/pkg/sql/plan/prepared_binding.go +++ b/pkg/sql/plan/prepared_binding.go @@ -17,6 +17,7 @@ package plan import ( "context" "fmt" + "strconv" "strings" "github.com/matrixorigin/matrixone/pkg/common/moerr" @@ -86,6 +87,8 @@ type preparedSourceBindingState struct { bindings []PreparedSourceBinding values []any valueDependent bool + selectStatement bool + hasRoundingFunction bool diagnosticCandidates []*Expr diagnosticFree bool } @@ -159,6 +162,128 @@ func preparedConfigurationValue(ctx context.Context, expr *Expr) (any, bool) { return value, true } +func preparedBoundDoubleValue(ctx context.Context, expr *Expr) (float64, bool) { + raw, present := preparedConfigurationValue(ctx, expr) + if !present { + return 0, false + } + switch value := raw.(type) { + case float64: + return value, true + case string: + parsed, err := strconv.ParseFloat(value, 64) + return parsed, err == nil + default: + return 0, false + } +} + +func preparedNumericValueSpelling(value any) string { + if bytes, ok := value.([]byte); ok { + return string(bytes) + } + return fmt.Sprint(value) +} + +// preparedZeroPrecisionRoundParam follows only planner casts and a direct +// projected marker. Other expressions may change the value, so they keep the +// normal comparison domain. +func preparedZeroPrecisionRoundParam(ctx context.Context, expr *Expr) *Expr { + fn := expr.GetF() + if fn == nil || fn.Func == nil || (fn.Func.GetObjName() != "round" && fn.Func.GetObjName() != "truncate") || len(fn.Args) != 2 { + return nil + } + if !preparedZeroIntegerArgument(ctx, fn.Args[1]) { + return nil + } + value := fn.Args[0] + cast := value.GetF() + if cast == nil || cast.Func == nil || cast.Func.GetObjName() != "cast" || isExplicitPreparedCast(value) || len(cast.Args) != 2 || + (!types.T(value.Typ.Id).IsFloat() && !types.T(value.Typ.Id).IsDecimal()) { + return nil + } + value = cast.Args[0] + for value != nil { + if value.GetP() != nil { + return value + } + if sub := value.GetSub(); sub != nil { + value = sub.Child + continue + } + if source := value.GetPreparedNumeric().GetStringDomainSource(); source != nil { + value = source + continue + } + if nested := value.GetF(); nested != nil && nested.Func != nil && + nested.Func.GetObjName() == "cast" && !isExplicitPreparedCast(value) && len(nested.Args) == 2 { + value = nested.Args[0] + continue + } + break + } + return nil +} + +// Prove zero through numeric casts only. A zero source survives those casts; +// other values keep their executable conversion, even if it might round to zero. +func preparedZeroIntegerArgument(ctx context.Context, expr *Expr) bool { + if literal := expr.GetLit(); literal != nil { + zero, ok := literal.GetValue().(*plan.Literal_I64Val) + return !literal.Isnull && ok && zero.I64Val == 0 + } + for { + cast := expr.GetF() + if cast == nil || cast.Func == nil || cast.Func.ObjName != "cast" || len(cast.Args) != 2 || + !makeTypeByPlan2Expr(expr).IsNumeric() { + break + } + expr = cast.Args[0] + } + param := expr.GetP() + state := preparedBindingState(ctx) + if param == nil || state == nil { + return false + } + binding, found := state.bindingForPosition(param.Pos) + if !found || !(binding.Type.Oid.IsInteger() || binding.Type.Oid.IsFloat() || + binding.Type.Oid.IsDecimal() || binding.Type.Oid.IsMySQLString()) { + return false + } + value, present := preparedConfigurationValue(ctx, expr) + if !present || value == nil { + return false + } + integer := makeSimplePlan2Type(types.T_int64) + zero, exact, err := preparedComparisonExactIntegerExpr(ctx, preparedNumericValueSpelling(value), integer) + return err == nil && exact && zero.GetLit().GetI64Val() == 0 +} + +func preparedSafeRoundIntegerComparison(ctx context.Context, source *Expr, target Type) (*Expr, bool, error) { + param := preparedZeroPrecisionRoundParam(ctx, source) + if param == nil { + return nil, false, nil + } + state := preparedBindingState(ctx) + if state == nil || !state.selectStatement { + return nil, false, nil + } + binding, bound := state.bindingForPosition(param.GetP().Pos) + if !bound || !binding.Type.Oid.IsMySQLString() { + return nil, false, nil + } + value, present := preparedConfigurationValue(ctx, param) + if !present || value == nil { + return nil, false, nil + } + _, exact, err := preparedComparisonExactIntegerExpr(ctx, preparedNumericValueSpelling(value), target) + if err != nil || !exact { + return nil, false, err + } + cast, err := makePlan2CastExpr(ctx, source, target) + return cast, err == nil, err +} + func preparedSourceBindingAt(ctx context.Context, ordinal int) (PreparedSourceBinding, error) { bindings := preparedSourceBindings(ctx) if ordinal <= 0 || ordinal > len(bindings) { @@ -302,12 +427,26 @@ func bindPreparedConsumerArguments(ctx context.Context, name string, args []*Exp return args, nil } name = strings.ToLower(name) + if name == "round" || name == "truncate" { + state.hasRoundingFunction = true + } args = append([]*Expr(nil), args...) for i, source := range args { if source == nil { continue } if source.GetP() == nil { + if len(args) == 2 && state.selectStatement && isPreparedNumericComparisonContext(name) && + args[1-i] != nil && types.T(args[1-i].Typ.Id).IsSignedInt() { + converted, ok, err := preparedSafeRoundIntegerComparison(ctx, source, args[1-i].Typ) + if err != nil { + return nil, err + } + if ok { + args[i] = converted + continue + } + } if len(args) == 1 && types.T(source.Typ.Id).IsMySQLString() && (name == "sum" || name == "avg" || name == "abs" || name == "sign" || name == "sleep") { // The source may be a projected marker, scalar subquery, or @@ -348,18 +487,43 @@ func bindPreparedConsumerArguments(ctx context.Context, name string, args []*Exp continue } } - if len(args) == 2 && isPreparedNumericComparisonContext(name) && - binding.Type.Oid.IsMySQLString() && args[1-i] != nil && - (types.T(args[1-i].Typ.Id).IsUnsignedInt() || args[1-i].Typ.Id == int32(types.T_bit)) { - // A proven integral string must compare in an exact UINT/BIT - // domain before the generic matcher can round it through DOUBLE. + if len(args) == 2 && state.selectStatement && isPreparedNumericComparisonContext(name) && + binding.Type.Oid == types.T_float64 && args[1-i] != nil && args[1-i].GetCol() != nil && + types.T(args[1-i].Typ.Id).IsDecimal() { + if value, ok := preparedBoundDoubleValue(ctx, source); ok && + decimalFloatComparisonHasUniqueValue(value, makeTypeByPlan2Expr(args[1-i])) { + converted, castErr := makePlan2CastExpr(ctx, source, args[1-i].Typ) + if castErr != nil { + return nil, castErr + } + args[i] = converted + continue + } + } + if len(args) == 2 && isPreparedNumericComparisonContext(name) && args[1-i] != nil && + (types.T(args[1-i].Typ.Id).IsInteger() || args[1-i].Typ.Id == int32(types.T_bit)) && + (binding.Type.Oid.IsMySQLString() || + (binding.Type.Oid.IsFloat() && types.T(args[1-i].Typ.Id).IsSignedInt()) || + (state.selectStatement && binding.Type.Oid.IsInteger() && + (binding.Type.Oid.TypeLen() > types.T(args[1-i].Typ.Id).TypeLen() || + binding.Type.Oid.IsSignedInt() != types.T(args[1-i].Typ.Id).IsSignedInt()))) { + // A proven integral value can compare in the peer's integer domain + // without casting the indexed column to a wider domain. // The proof depends on this execution's value, so the existing // binding state keeps the resulting plan out of the type-only cache. if value, present := preparedConfigurationValue(ctx, source); present && value != nil { - _, exact, proofErr := preparedComparisonExactIntegerExpr(ctx, fmt.Sprint(value), args[1-i].Typ) + spelling := preparedNumericValueSpelling(value) + _, exact, proofErr := preparedComparisonExactIntegerExpr(ctx, spelling, args[1-i].Typ) if proofErr != nil { return nil, proofErr } + if binding.Type.Oid.IsInteger() { + // Some mixed integer comparisons enter an approximate domain. + // Keep the existing comparison at values + // whose adjacent integers may collide in DOUBLE. + integer, err := strconv.ParseInt(spelling, 10, 54) + exact = exact && err == nil && integer >= -(1<<53)+1 && integer <= (1<<53)-1 + } if exact { target := args[1-i].Typ if target.Id == int32(types.T_bit) { @@ -368,16 +532,18 @@ func bindPreparedConsumerArguments(ctx context.Context, name string, args []*Exp unsigned := types.T_uint64.ToType() target = makePlan2Type(&unsigned) } - // The proof accepts complete decimal/scientific spellings such - // as "100.0" and "1e2". Stringβ†’UINT's integer parser does - // not accept those spellings, while DECIMAL(38,0) parses them - // exactly without rounding adjacent values above 2^53. - decimalType := types.New(types.T_decimal128, 38, 0) - decimalValue, castErr := makePlan2CastExpr(ctx, source, makePlan2Type(&decimalType)) - if castErr != nil { - return nil, castErr + converted := source + var castErr error + if binding.Type.Oid.IsMySQLString() { + // Decimal/scientific text needs an exact intermediate parser; + // direct stringβ†’integer casts reject those spellings. + decimalType := types.New(types.T_decimal128, 38, 0) + converted, castErr = makePlan2CastExpr(ctx, source, makePlan2Type(&decimalType)) + if castErr != nil { + return nil, castErr + } } - converted, castErr := makePlan2CastExpr(ctx, decimalValue, target) + converted, castErr = makePlan2CastExpr(ctx, converted, target) if castErr != nil { return nil, castErr } @@ -579,6 +745,7 @@ func BuildPreparedExecutionPlan(ctx CompilerContext, stmt tree.Statement, return nil, moerr.NewInvalidInput(previous, "Incorrect arguments to EXECUTE") } planning := withPreparedSourceBindings(previous, bindings, values) + preparedBindingState(planning).selectStatement = stmt.GetQueryType() == tree.QueryTypeDQL ctx.SetContext(planning) defer ctx.SetContext(previous) query, err := NewPrepareOptimizer(ctx).Optimize(stmt, false) diff --git a/pkg/sql/plan/prepared_binding_test.go b/pkg/sql/plan/prepared_binding_test.go index e3f0ccdc87107..d6811ddac3e37 100644 --- a/pkg/sql/plan/prepared_binding_test.go +++ b/pkg/sql/plan/prepared_binding_test.go @@ -33,6 +33,121 @@ import ( "github.com/stretchr/testify/require" ) +func TestPreparedDecimalFloatFilterUsesUniqueValueProof(t *testing.T) { + for _, test := range []struct { + name string + value string + column types.Type + native bool + }{ + {"integral", "54321", types.New(types.T_decimal64, 12, 2), true}, + {"fractional", "0.1", types.New(types.T_decimal64, 12, 2), true}, + {"between scale points", "0.104", types.New(types.T_decimal64, 12, 2), false}, + {"float collision", "9007199254740992", types.New(types.T_decimal128, 20, 0), false}, + } { + t.Run(test.name, func(t *testing.T) { + ctx := withPreparedSourceBindings(context.Background(), + []PreparedSourceBinding{{Position: 0, Type: types.T_float64.ToType()}}, + []any{ParamValue{Value: test.value, IsBinaryProtocol: true}}) + state := preparedBindingState(ctx) + state.selectStatement = true + column := &Expr{Typ: makePlan2Type(&test.column), Expr: &planpb.Expr_Col{Col: &planpb.ColRef{}}} + param := &Expr{Typ: makeSimplePlan2Type(types.T_float64), Expr: &planpb.Expr_P{P: &planpb.ParamRef{Pos: 0}}} + args, err := bindPreparedConsumerArguments(ctx, "=", []*Expr{column, param}) + require.NoError(t, err) + require.Equal(t, test.native, args[1].Typ.Id == column.Typ.Id) + require.True(t, state.valueDependent) + }) + } +} + +func TestPreparedNumericPredicateFiltering(t *testing.T) { + for _, tc := range []struct { + name, predicate string + values []string + native bool + integerKey bool + }{ + {"or expressions", "c=abs(?) or c=abs(?)", []string{"54321", "54322"}, true, false}, + {"in expressions", "c in (abs(?),abs(?))", []string{"54321", "54322"}, true, false}, + {"between expressions", "c between abs(?) and abs(?)", []string{"54321", "54322"}, true, false}, + {"in markers", "c in (?,?)", []string{"54321", "54322"}, true, false}, + {"between markers", "c between ? and ?", []string{"54321", "54322"}, true, false}, + {"nested boolean", "(c=abs(?) or c=abs(?)) and c>=abs(?)", []string{"54321", "54322", "54320"}, true, false}, + {"mixed unsafe in", "c in (?,?)", []string{"54321", "0.104"}, false, false}, + {"mixed unsafe between", "c between ? and ?", []string{"54321", "54322.104"}, false, false}, + {"round zero precision", "c=round(?,?)", []string{"54321.0", "0"}, true, true}, + {"truncate zero precision", "c=truncate(?,?)", []string{"54321.0", "0"}, true, true}, + {"explicit precision cast", "c=round(?,cast(? as signed))", []string{"54321.0", "0"}, true, true}, + {"round nonzero precision", "c=round(?,?)", []string{"54321.0", "1"}, false, true}, + {"round negative precision", "c=round(?,?)", []string{"54321.0", "-1"}, false, true}, + {"explicit column cast", "cast(c as decimal(5,0))=round(?,0)", []string{"54321.0"}, false, true}, + {"explicit value cast", "c=cast(round(?,0) as decimal(4,0))", []string{"54321.0"}, false, true}, + } { + t.Run(tc.name, func(t *testing.T) { + mock := NewMockOptimizer(false) + table := makeExprOptCompositeSortKeyTableDef() + table.Name, table.TblId = "numeric_filters", 99003 + decimal := types.New(types.T_decimal64, 12, 2) + table.Cols[2].Typ = makePlan2Type(&decimal) + if tc.integerKey { + table.Cols[2].Typ = makeSimplePlan2Type(types.T_int64) + } + mock.ctxt.tables[table.Name] = table + mock.ctxt.objects[table.Name] = &ObjectRef{ObjName: table.Name, Obj: int64(table.TblId)} + proc := mock.ctxt.GetProcess() + params := vector.NewVec(types.T_text.ToType()) + defer func() { proc.SetPrepareParams(nil); params.Free(proc.Mp()) }() + bindings := make([]PreparedSourceBinding, len(tc.values)) + values := make([]any, len(tc.values)) + for i, value := range tc.values { + bindings[i] = PreparedSourceBinding{Position: int32(i), Type: types.T_float64.ToType()} + if tc.integerKey { + bindings[i].Type = types.T_int64.ToType() + if i == 0 { + bindings[i].Type = types.T_varchar.ToType() + } + } + values[i] = ParamValue{Value: value, IsBinaryProtocol: true} + require.NoError(t, vector.AppendBytes(params, []byte(value), false, proc.Mp())) + } + proc.SetPrepareParams(params) + stmt, err := parsers.ParseOne(context.Background(), dialect.MYSQL, + "select c from numeric_filters where "+tc.predicate, 1) + require.NoError(t, err) + defer stmt.Free() + bound, err := BuildPreparedExecutionPlan(&mock.ctxt, stmt, bindings, values) + require.NoError(t, err) + require.True(t, bound.ValueDependent, "runtime value proof must not enter the type-only cache") + foundScan, columnCast, executableParam := false, false, false + for _, node := range bound.Plan.GetQuery().Nodes { + if node.NodeType != planpb.Node_TABLE_SCAN || node.TableDef.Name != table.Name { + continue + } + foundScan = true + for _, filter := range node.FilterList { + if tc.native { + // BuildPreparedExecutionPlan deliberately skips scan statistics; + // assert pruning eligibility here and actual blocks in public QA. + require.True(t, ExprIsZonemappable(context.Background(), filter), "safe predicate must allow block pruning") + } + executableParam = executableParam || function.ContainsParameter(filter) + require.NoError(t, planpb.VisitExprTree(filter, func(expr *Expr) error { + if fn := expr.GetF(); fn != nil && fn.Func.ObjName == "cast" && len(fn.Args) == 2 && + fn.Args[0].GetCol() != nil && fn.Args[0].Typ.Id == table.Cols[2].Typ.Id { + columnCast = true + } + return nil + })) + } + } + require.True(t, foundScan) + require.Equal(t, !tc.native, columnCast, bound.Plan.String()) + require.True(t, executableParam, "proof witnesses must not replace executable parameters") + }) + } +} + func TestPreparedDomainlessNullUsesConcreteRelationalColumns(t *testing.T) { for _, query := range []string{ "select ? group by 1", diff --git a/pkg/sql/plan/projection_binder.go b/pkg/sql/plan/projection_binder.go index ca50b0be51bf6..397e44cbed3a0 100644 --- a/pkg/sql/plan/projection_binder.go +++ b/pkg/sql/plan/projection_binder.go @@ -15,6 +15,7 @@ package plan import ( + "github.com/matrixorigin/matrixone/pkg/container/types" "github.com/matrixorigin/matrixone/pkg/pb/plan" "github.com/matrixorigin/matrixone/pkg/sql/parsers/tree" ) @@ -122,13 +123,11 @@ func (b *ProjectionBinder) BindExpr(astExpr tree.Expr, depth int32, isRoot bool) target := b.numericTargetType b.numericTargetType = nil defer func() { b.numericTargetType = target }() - _, isDirectPreparedParam := unwrapParenExpr(astExpr).(*tree.ParamExpr) - if b.builder != nil && b.builder.isPrepareStatement && isDirectPreparedParam && - (b.builder.isInsertIgnore || (b.ctx != nil && b.ctx.assignmentIgnore)) && - useIgnoreConversionAssignmentCast(*target) { - // A bare marker is the assignment source, not a numeric expression. - // Leave it as TEXT so the final DML assignment boundary can use - // cast_ignore and emit the per-row warning/adjustment at execution. + if types.T(target.Id) == types.T_bit && isPreparedAssignmentParam(b.builder, astExpr) { + // The marker is the assignment source, not an arithmetic expression. + // BIT assignments distinguish string bytes from numeric values. + // Preserve that source until the final assignment cast. Numeric + // function and aggregate inputs still require their target context. return b.baseBindExpr(astExpr, depth, isRoot) } _, isBareColumn := unwrapParenExpr(astExpr).(*tree.UnresolvedName) diff --git a/pkg/sql/plan/query_builder.go b/pkg/sql/plan/query_builder.go index 5532c6f1e49db..a0bb65725fa45 100644 --- a/pkg/sql/plan/query_builder.go +++ b/pkg/sql/plan/query_builder.go @@ -10249,7 +10249,9 @@ func (builder *QueryBuilder) bindValues( for j := 0; j < rowCount; j++ { var planExpr *plan.Expr if i < len(ctx.numericProjectionTypes) && - isNumericAssignmentTarget(ctx.numericProjectionTypes[i]) { + isNumericAssignmentTarget(ctx.numericProjectionTypes[i]) && + !(types.T(ctx.numericProjectionTypes[i].Id) == types.T_bit && + isPreparedAssignmentParam(builder, valuesClause.Rows[j][i])) { target := ctx.numericProjectionTypes[i] planExpr, err = valuesBinder.bindNumericExprWithContext(valuesClause.Rows[j][i], 0, &target) } else { diff --git a/pkg/sql/plan/types.go b/pkg/sql/plan/types.go index 848c326dc4911..6858453786ca4 100644 --- a/pkg/sql/plan/types.go +++ b/pkg/sql/plan/types.go @@ -987,11 +987,6 @@ type BindContext struct { numericTableProjectionTypes map[string][]Type numericTableProjectionAmbiguous map[string][]bool numericCteByName map[string]*tree.CTE - // assignmentIgnore marks a prepared UPDATE IGNORE projection. A direct - // parameter must stay TEXT until the writer's cast_ignore; otherwise the - // numeric projection context can materialize an ordinary strict cast during - // PREPARE and reject malformed values before IGNORE can adjust them. - assignmentIgnore bool timeAsts []tree.Expr diff --git a/pkg/sql/plan/utils.go b/pkg/sql/plan/utils.go index 1cb64fb2f8c0b..7ca5971102f08 100644 --- a/pkg/sql/plan/utils.go +++ b/pkg/sql/plan/utils.go @@ -1477,7 +1477,7 @@ func preparedPlanFunctionFallbackParamPositions(preparePlan *Plan, functionName func isPreparedNumericFallbackFunction(name string) bool { switch strings.ToLower(name) { - case "abs", "sign", "elt": + case "abs", "sign", "elt", "round", "truncate": return true default: return false @@ -1493,6 +1493,8 @@ func isPreparedNumericFallbackFunctionCall(name string, argCount int) bool { return argCount >= 2 case "abs", "sign": return argCount == 1 + case "round", "truncate": + return argCount == 1 || argCount == 2 default: return false } diff --git a/pkg/sql/plan/visit_plan_rule.go b/pkg/sql/plan/visit_plan_rule.go index 87040e1a16a2d..193144cca5198 100644 --- a/pkg/sql/plan/visit_plan_rule.go +++ b/pkg/sql/plan/visit_plan_rule.go @@ -3700,7 +3700,7 @@ func (rule *ResetParamRefRule) applyExpr(e *plan.Expr) (*plan.Expr, error) { } if changed { reboundArgs := []*Expr{refreshed} - if functionName == "elt" { + if len(boundArgs) > 1 { reboundArgs = append([]*Expr(nil), boundArgs...) reboundArgs[0] = refreshed } @@ -3727,7 +3727,7 @@ func (rule *ResetParamRefRule) applyExpr(e *plan.Expr) (*plan.Expr, error) { } if changed { reboundArgs := []*Expr{rebound} - if functionName == "elt" { + if len(boundArgs) > 1 { reboundArgs = append([]*Expr(nil), boundArgs...) reboundArgs[0] = rebound } @@ -3978,7 +3978,16 @@ func preparedComparisonExactIntegerExpr( } targetType := makeTypeByPlan2Type(target) bits := 0 + signed := false switch targetType.Oid { + case types.T_int8: + bits, signed = 8, true + case types.T_int16: + bits, signed = 16, true + case types.T_int32: + bits, signed = 32, true + case types.T_int64: + bits, signed = 64, true case types.T_uint8: bits = 8 case types.T_uint16: @@ -3995,6 +4004,16 @@ func preparedComparisonExactIntegerExpr( default: return nil, false, nil } + if signed { + value, err := strconv.ParseInt(integerText, 10, bits) + if err != nil || value < -(1<<53)+1 || value > (1<<53)-1 { + // BIGINT values at and above 2^53 can collide when the original + // comparison converts the column to DOUBLE. + return nil, false, nil + } + expr, err := preparedRuntimeParamExpr(ctx, integerText, false, targetType) + return expr, err == nil, err + } if strings.HasPrefix(integerText, "-") { return nil, false, nil } diff --git a/pkg/sql/plan/visit_plan_rule_test.go b/pkg/sql/plan/visit_plan_rule_test.go index a0d3d86357f6a..cf49d618505ae 100644 --- a/pkg/sql/plan/visit_plan_rule_test.go +++ b/pkg/sql/plan/visit_plan_rule_test.go @@ -2874,6 +2874,24 @@ func TestPreparedComparisonExactIntegerExpr(t *testing.T) { require.Equal(t, test.target.Id, expr.Typ.Id) }) } + for _, test := range []struct { + name string + value string + target planpb.Type + want int64 + }{ + {name: "signed text", value: "-54321.0", target: int64Type, want: -54321}, + {name: "signed exponent", value: "5.4321e4", target: int64Type, want: 54321}, + {name: "last safe positive", value: "9007199254740991", target: int64Type, want: 9007199254740991}, + {name: "last safe negative", value: "-9007199254740991", target: int64Type, want: -9007199254740991}, + } { + t.Run(test.name, func(t *testing.T) { + expr, ok, err := preparedComparisonExactIntegerExpr(ctx, test.value, test.target) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, test.want, expr.GetLit().GetI64Val()) + }) + } for _, test := range []struct { name string @@ -2884,6 +2902,10 @@ func TestPreparedComparisonExactIntegerExpr(t *testing.T) { {name: "uint64 overflow", value: "18446744073709551616", target: uint64Type}, {name: "negative unsigned", value: "-1", target: uint64Type}, {name: "int64 overflow", value: "9223372036854775808", target: int64Type}, + {name: "double collision positive", value: "9007199254740992", target: int64Type}, + {name: "double collision negative", value: "-9007199254740992", target: int64Type}, + {name: "signed fractional", value: "54321.5", target: int64Type}, + {name: "signed suffix", value: "54321tail", target: int64Type}, {name: "nonnumeric", value: "tail", target: bit64Type}, {name: "numeric prefix keeps warning path", value: "9007199254740993tail", target: bit64Type}, {name: "huge positive exponent", value: "1e1000000", target: uint64Type}, diff --git a/pkg/tests/issues/issue_26725_test.go b/pkg/tests/issues/issue_26725_test.go index a0544519323f6..2e3050a8f3879 100644 --- a/pkg/tests/issues/issue_26725_test.go +++ b/pkg/tests/issues/issue_26725_test.go @@ -373,6 +373,50 @@ func TestIssue26725PreparedBit64Numeric(t *testing.T) { require.NoError(t, rows.Err()) require.NoError(t, rows.Close()) + // Derived VALUES and ON DUPLICATE KEY UPDATE bind assignment markers + // through separate numeric-context entry points. Reusing each statement + // across source domains must keep the same BIT assignment semantics. + execSQLRequire(t, ctx, db, "insert into "+dbName+".t64(id, b) values (933, 0)") + upsertStmt, err := db.PrepareContext(ctx, + "insert into "+dbName+".t64(id, b) values (?, 0) on duplicate key update b = ?") + require.NoError(t, err) + defer upsertStmt.Close() + for _, tc := range []struct { + value any + want string + }{ + {value: float64(5), want: "5"}, + {value: "5", want: "53"}, + {value: float64(6), want: "6"}, + } { + _, err = upsertStmt.ExecContext(ctx, int64(933), tc.value) + require.NoError(t, err) + var actual string + require.NoError(t, db.QueryRowContext(ctx, + "select cast(b as unsigned) from "+dbName+".t64 where id = 933").Scan(&actual)) + require.Equal(t, tc.want, actual) + } + derivedValuesStmt, err := db.PrepareContext(ctx, + "insert into "+dbName+".t64(id, b) select ?, x from (values row(?)) as d(x)") + require.NoError(t, err) + defer derivedValuesStmt.Close() + for _, tc := range []struct { + id int64 + value any + want string + }{ + {id: 930, value: float64(5), want: "5"}, + {id: 931, value: "5", want: "53"}, + {id: 932, value: float64(6), want: "6"}, + } { + _, err = derivedValuesStmt.ExecContext(ctx, tc.id, tc.value) + require.NoError(t, err) + var actual string + require.NoError(t, db.QueryRowContext(ctx, + "select cast(b as unsigned) from "+dbName+".t64 where id = ?", tc.id).Scan(&actual)) + require.Equal(t, tc.want, actual) + } + // Aggregates materialize a new result vector. Aggregates that return an // unchanged input value must preserve numeric-vs-string source semantics // across that boundary for both protocol and SQL prepared statements. diff --git a/pkg/tests/issues/issue_28680_test.go b/pkg/tests/issues/issue_28680_test.go index c04a6895313d6..5fe651f0b4538 100644 --- a/pkg/tests/issues/issue_28680_test.go +++ b/pkg/tests/issues/issue_28680_test.go @@ -149,6 +149,73 @@ func TestIssue28680IgnoreConversionExecutionBoundaries(t *testing.T) { "8/100/1.25/2024-01-07", }, got) + // Forwarding a marker through SELECT or VALUES is still assignment, + // while SUM consumes it numerically before assignment. + for caseIndex, source := range []string{ + "select ?", + "select x from (select ? as x) d", + "select x from (values row(?)) as d(x)", + } { + id := 940 + 3*caseIndex + for operationIndex, operation := range []struct { + name, sql string + existingRow bool + }{ + {"insert", fmt.Sprintf("insert ignore into %s.target(id,d) select %d,x from (%s) as src(x)", dbName, id, source), false}, + {"update scalar", fmt.Sprintf("update ignore %s.target set d=(%s) where id=%d", dbName, source, id+1), true}, + {"upsert scalar", fmt.Sprintf("insert ignore into %s.target(id,d) values (%d,0) on duplicate key update d=(%s)", dbName, id+2, source), true}, + } { + t.Run(operation.name+"/"+source, func(t *testing.T) { + id := id + operationIndex + statement, prepareErr := conn.PrepareContext(ctx, operation.sql) + require.NoError(t, prepareErr) + defer statement.Close() + for _, value := range []string{"abc", "1.25", "abc"} { + if operation.existingRow { + _, createErr := conn.ExecContext(ctx, fmt.Sprintf("insert into %s.target(id,d) values (%d,0)", dbName, id)) + require.NoError(t, createErr) + } + _, execErr := statement.ExecContext(ctx, value) + require.NoError(t, execErr) + if value == "abc" { + requireWarningCodes(t, ctx, conn, map[uint16]int{1366: 1}) + } else { + requireNoWarnings(t, ctx, conn) + } + var result string + require.NoError(t, conn.QueryRowContext(ctx, fmt.Sprintf("select cast(d as char) from %s.target where id=%d", dbName, id)).Scan(&result)) + want := "0.00" + if value != "abc" { + want = "1.25" + } + require.Equal(t, want, result) + _, deleteErr := conn.ExecContext(ctx, fmt.Sprintf("delete from %s.target where id=%d", dbName, id)) + require.NoError(t, deleteErr) + } + }) + } + } + + for _, source := range []struct{ sql, want string }{ + {"select ? + 1", "2.25"}, + {"select round(?, 1)", "1.30"}, + {"select sum(x) from (select ? as x) d", "1.25"}, + } { + t.Run(source.sql, func(t *testing.T) { + statement, prepareErr := conn.PrepareContext(ctx, + "insert ignore into "+dbName+".target(id,d) select 949,x from ("+source.sql+") as src(x)") + require.NoError(t, prepareErr) + defer statement.Close() + _, execErr := statement.ExecContext(ctx, "1.25") + require.NoError(t, execErr) + requireNoWarnings(t, ctx, conn) + var result string + require.NoError(t, queryRow("select cast(d as char) from "+dbName+".target where id=949").Scan(&result)) + require.Equal(t, source.want, result) + exec("delete from " + dbName + ".target where id=949") + }) + } + // UPDATE must convert once for every matching row. Reusing this prepared // statement with a zero-match predicate must not manufacture warnings. exec("update " + dbName + ".target set i=5, d=5.00, dt='2024-01-01' where id=1") diff --git a/pkg/tests/issues/issue_29400_test.go b/pkg/tests/issues/issue_29400_test.go index ac73b058d338d..15c84b7a1e742 100644 --- a/pkg/tests/issues/issue_29400_test.go +++ b/pkg/tests/issues/issue_29400_test.go @@ -293,6 +293,8 @@ func TestIssue29400DropDatabaseDoesNotHoldBranchDAGAfterTable(t *testing.T) { defer db0.Close() db1 := open(cn1.GetServiceConfig().CN.Frontend.Port) defer db1.Close() + // Open the peer connection before the concurrency proof starts. + require.NoError(t, db1.PingContext(ctx)) const a, b = "issue_29400_dag_a", "issue_29400_dag_b" defer func() { cleanupCtx, done := context.WithTimeout(context.Background(), 30*time.Second) @@ -350,15 +352,26 @@ func TestIssue29400DropDatabaseDoesNotHoldBranchDAGAfterTable(t *testing.T) { t.Fatalf("A finished before the post-table barrier was released: %v", err) default: } - fastCtx, fastCancel := context.WithTimeout(ctx, 2*time.Second) + fastCtx, fastCancel := context.WithTimeout(ctx, 10*time.Second) defer fastCancel() _, err = db1.ExecContext(fastCtx, "drop table "+b+".b_child") require.NoError(t, err, "unrelated branch DROP waited for A's post-table work") + // Completion must precede A leaving the barrier. The deadline bounds + // the test; it must never turn A's cancellation into apparent progress. + require.NoError(t, dropCtx.Err()) + waiters, _, waiting := fault.TriggerFault(probe) + require.True(t, waiting) + require.Equal(t, int64(1), waiters) + select { + case err := <-dropA: + t.Fatalf("A left the post-table barrier before B completed: %v", err) + default: + } release() require.NoError(t, <-dropA) // A now retains all statement locks until COMMIT. Its component must // not pin B's root during this explicit transaction tail. - otherCtx, otherCancel := context.WithTimeout(ctx, 2*time.Second) + otherCtx, otherCancel := context.WithTimeout(ctx, 10*time.Second) defer otherCancel() _, err = db1.ExecContext(otherCtx, "drop table "+b+".a_root") require.NoError(t, err, "unrelated branch DROP waited for A's COMMIT") diff --git a/test/distributed/cases/function/issue_29471_json_character_cast.result b/test/distributed/cases/function/issue_29471_json_character_cast.result new file mode 100644 index 0000000000000..ddbaf70cb9fd5 --- /dev/null +++ b/test/distributed/cases/function/issue_29471_json_character_cast.result @@ -0,0 +1,20 @@ +select cast(cast('"abc"' as json) as char), json_unquote(cast('"abc"' as json)); +➀ cast(cast("abc" as json) as varchar)[12,65535,0] Β¦ json_unquote(cast("abc" as json))[12,65535,0] 𝄀 +"abc" Β¦ abc +select cast('"abc"' as json) = 'abc', cast('"abc"' as json) like 'abc'; +➀ cast("abc" as json) = abc[-7,1,0] Β¦ cast("abc" as json) like abc[-7,1,0] 𝄀 +1 Β¦ 1 +select cast(cast('"a\\"b"' as json) as varchar(20)); +➀ cast(cast("a\\"b" as json) as varchar(20))[12,20,0] 𝄀 +"a\"b" +select cast(cast('42' as json) as char), cast(cast('null' as json) as char); +➀ cast(cast(42 as json) as varchar)[12,65535,0] Β¦ cast(cast(null as json) as varchar)[12,65535,0] 𝄀 +42 Β¦ null +create table issue_29471_assignment(v varchar(10)); +insert into issue_29471_assignment values (cast('"abc"' as json)); +insert into issue_29471_assignment values (cast(cast('"abc"' as json) as char)); +select v from issue_29471_assignment order by v; +➀ v[12,10,0] 𝄀 +"abc" 𝄀 +abc +drop table issue_29471_assignment; diff --git a/test/distributed/cases/function/issue_29471_json_character_cast.sql b/test/distributed/cases/function/issue_29471_json_character_cast.sql new file mode 100644 index 0000000000000..8dd33bcbe8845 --- /dev/null +++ b/test/distributed/cases/function/issue_29471_json_character_cast.sql @@ -0,0 +1,14 @@ +-- @case +-- @desc: JSON expression CAST serializes strings; assignment keeps its character payload. +-- @label:bvt + +select cast(cast('"abc"' as json) as char), json_unquote(cast('"abc"' as json)); +select cast('"abc"' as json) = 'abc', cast('"abc"' as json) like 'abc'; +select cast(cast('"a\\"b"' as json) as varchar(20)); +select cast(cast('42' as json) as char), cast(cast('null' as json) as char); + +create table issue_29471_assignment(v varchar(10)); +insert into issue_29471_assignment values (cast('"abc"' as json)); +insert into issue_29471_assignment values (cast(cast('"abc"' as json) as char)); +select v from issue_29471_assignment order by v; +drop table issue_29471_assignment; diff --git a/test/distributed/cases/prepare/issue_29505_round_truncate.result b/test/distributed/cases/prepare/issue_29505_round_truncate.result new file mode 100644 index 0000000000000..bed70051b523b --- /dev/null +++ b/test/distributed/cases/prepare/issue_29505_round_truncate.result @@ -0,0 +1,25 @@ +prepare round_value from 'select cast(round(?,1) as double) as v'; +set @v = '1.46'; +execute round_value using @v; +➀ v[8,53,31] 𝄀 +1.5 +set @v = 2; +execute round_value using @v; +➀ v[8,53,31] 𝄀 +2.0 +set @v = '2.5'; +execute round_value using @v; +➀ v[8,53,31] 𝄀 +2.5 +deallocate prepare round_value; +prepare round_scalar from 'select cast(round((select ?),0) as double) as v'; +execute round_scalar using @v; +➀ v[8,53,31] 𝄀 +3.0 +deallocate prepare round_scalar; +prepare truncate_derived from 'select cast(truncate(x,1) as double) as v from (select ? x limit 1) d'; +set @v = '1.46'; +execute truncate_derived using @v; +➀ v[8,53,31] 𝄀 +1.4 +deallocate prepare truncate_derived; diff --git a/test/distributed/cases/prepare/issue_29505_round_truncate.sql b/test/distributed/cases/prepare/issue_29505_round_truncate.sql new file mode 100644 index 0000000000000..736f2c57e30e0 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29505_round_truncate.sql @@ -0,0 +1,21 @@ +-- @case +-- @desc: Prepared ROUND and TRUNCATE retain runtime value domains through projections. +-- @label:bvt + +prepare round_value from 'select cast(round(?,1) as double) as v'; +set @v = '1.46'; +execute round_value using @v; +set @v = 2; +execute round_value using @v; +set @v = '2.5'; +execute round_value using @v; +deallocate prepare round_value; + +prepare round_scalar from 'select cast(round((select ?),0) as double) as v'; +execute round_scalar using @v; +deallocate prepare round_scalar; + +prepare truncate_derived from 'select cast(truncate(x,1) as double) as v from (select ? x limit 1) d'; +set @v = '1.46'; +execute truncate_derived using @v; +deallocate prepare truncate_derived; diff --git a/test/distributed/cases/prepare/issue_29506_integer_filter_domains.result b/test/distributed/cases/prepare/issue_29506_integer_filter_domains.result new file mode 100644 index 0000000000000..8875cae3dce45 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29506_integer_filter_domains.result @@ -0,0 +1,114 @@ +drop database if exists issue_29506_filters; +create database issue_29506_filters; +use issue_29506_filters; +create table keys32(id int primary key); +insert into keys32 select result from generate_series(1, 20000) g; +prepare equal_key from 'select count(*) from keys32 where id = ?'; +set @key = '12345.0'; +explain force execute equal_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29506_filters.keys32 𝄀 + Filter Cond: (keys32.id = 12345) 𝄀 + Block Filter Cond: (keys32.id = 12345) +explain analyze force execute equal_key using @key; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=4 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29506_filters.keys32 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=4 bytes OutputSize=4 bytes ReadSize=64.33 KiB|0 bytes|32.16 KiB MemorySize=9 bytes (min=9 bytes, max=9 bytes) 𝄀 + Filter Cond: (keys32.id = 12345) 𝄀 + Block Filter Cond: (keys32.id = 12345) +execute equal_key using @key; +➀ count(*)[-5,64,0] 𝄀 +1 +set @key = cast(12345 as signed); +explain force execute equal_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29506_filters.keys32 𝄀 + Filter Cond: (keys32.id = cast(cast('12345' AS BIGINT) AS INT)) 𝄀 + Block Filter Cond: (keys32.id = cast(cast('12345' AS BIGINT) AS INT)) +execute equal_key using @key; +➀ count(*)[-5,64,0] 𝄀 +1 +set @key = cast(4294967296 as signed); +explain force execute equal_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29506_filters.keys32 𝄀 + Filter Cond: (cast(keys32.id AS BIGINT) = cast('4294967296' AS BIGINT)) + -- HINT: Cast expression may prevent index usage +execute equal_key using @key; +➀ count(*)[-5,64,0] 𝄀 +0 +set @key = '12345tail'; +execute equal_key using @key; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare equal_key; +prepare between_keys from 'select count(*) from keys32 where id between ? and ?'; +set @lo = '12344'; +set @hi = '12346'; +explain force execute between_keys using @lo, @hi; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29506_filters.keys32 𝄀 + Filter Cond: (keys32.id >= 12344), (keys32.id <= 12346) 𝄀 + Block Filter Cond: (keys32.id >= 12344), (keys32.id <= 12346) +execute between_keys using @lo, @hi; +➀ count(*)[-5,64,0] 𝄀 +3 +deallocate prepare between_keys; +prepare in_keys from 'select count(*) from keys32 where id in (?, ?)'; +explain force execute in_keys using @lo, @hi; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29506_filters.keys32 𝄀 + Filter Cond: keys32.id in (12344, 12346) 𝄀 + Block Filter Cond: keys32.id in (12344, 12346) +execute in_keys using @lo, @hi; +➀ count(*)[-5,64,0] 𝄀 +2 +deallocate prepare in_keys; +create table keys64(id bigint primary key); +insert into keys64 values (9007199254740991), (9007199254740992), (9007199254740993); +prepare large_key from 'select group_concat(id order by id) from keys64 where id = ?'; +set @key = '9007199254740992'; +explain force execute large_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: group_concat(keys64.id ORDER BY keys64.id ASC SEPARATOR ',') 𝄀 + -> Table Scan on issue_29506_filters.keys64 𝄀 + Filter Cond: (cast(keys64.id AS DOUBLE) = cast(cast('9007199254740992' AS DOUBLE) AS DOUBLE)) + -- HINT: Cast expression may prevent index usage +execute large_key using @key; +➀ group_concat(keys64.id order by id separator ,)[-1,536870911,0] 𝄀 +9007199254740992,9007199254740993 +set @key = '9007199254740991'; +explain force execute large_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: group_concat(keys64.id ORDER BY keys64.id ASC SEPARATOR ',') 𝄀 + -> Table Scan on issue_29506_filters.keys64 𝄀 + Filter Cond: (keys64.id = 9007199254740991) +execute large_key using @key; +➀ group_concat(keys64.id order by id separator ,)[-1,536870911,0] 𝄀 +9007199254740991 +deallocate prepare large_key; +drop database issue_29506_filters; diff --git a/test/distributed/cases/prepare/issue_29506_integer_filter_domains.sql b/test/distributed/cases/prepare/issue_29506_integer_filter_domains.sql new file mode 100644 index 0000000000000..a8f9fa183764e --- /dev/null +++ b/test/distributed/cases/prepare/issue_29506_integer_filter_domains.sql @@ -0,0 +1,50 @@ +-- @case +-- @desc: Prepared integer comparisons keep native filters only for safe bound values. +-- @label:bvt + +drop database if exists issue_29506_filters; +create database issue_29506_filters; +use issue_29506_filters; +create table keys32(id int primary key); +insert into keys32 select result from generate_series(1, 20000) g; + +prepare equal_key from 'select count(*) from keys32 where id = ?'; +set @key = '12345.0'; +explain force execute equal_key using @key; +-- @ignore:0 +explain analyze force execute equal_key using @key; +execute equal_key using @key; +set @key = cast(12345 as signed); +explain force execute equal_key using @key; +execute equal_key using @key; +set @key = cast(4294967296 as signed); +explain force execute equal_key using @key; +execute equal_key using @key; +set @key = '12345tail'; +execute equal_key using @key; +deallocate prepare equal_key; + +prepare between_keys from 'select count(*) from keys32 where id between ? and ?'; +set @lo = '12344'; +set @hi = '12346'; +explain force execute between_keys using @lo, @hi; +execute between_keys using @lo, @hi; +deallocate prepare between_keys; + +prepare in_keys from 'select count(*) from keys32 where id in (?, ?)'; +explain force execute in_keys using @lo, @hi; +execute in_keys using @lo, @hi; +deallocate prepare in_keys; + +create table keys64(id bigint primary key); +insert into keys64 values (9007199254740991), (9007199254740992), (9007199254740993); +prepare large_key from 'select group_concat(id order by id) from keys64 where id = ?'; +set @key = '9007199254740992'; +explain force execute large_key using @key; +execute large_key using @key; +set @key = '9007199254740991'; +explain force execute large_key using @key; +execute large_key using @key; +deallocate prepare large_key; + +drop database issue_29506_filters; diff --git a/test/distributed/cases/prepare/issue_29509_explain_analyze_execute.result b/test/distributed/cases/prepare/issue_29509_explain_analyze_execute.result new file mode 100644 index 0000000000000..cb05f9bdc64b1 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29509_explain_analyze_execute.result @@ -0,0 +1,52 @@ +drop database if exists issue_29509_explain; +create database issue_29509_explain; +use issue_29509_explain; +create table t(id bigint primary key); +insert into t values (1), (2); +prepare fixed from 'select count(*) from t where id = 1'; +explain analyze force execute fixed; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29509_explain.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=17 bytes (min=17 bytes, max=17 bytes) 𝄀 + Filter Cond: (t.id = 1) +explain analyze force execute fixed; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29509_explain.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=17 bytes (min=17 bytes, max=17 bytes) 𝄀 + Filter Cond: (t.id = 1) +deallocate prepare fixed; +prepare parameterized from 'select count(*) from t where id = ?'; +set @id = 1; +explain analyze force execute parameterized using @id; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29509_explain.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=26 bytes (min=8 bytes, max=16 bytes) 𝄀 + Filter Cond: (t.id = cast('1' AS BIGINT)) +set @id = 2; +explain analyze force execute parameterized using @id; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29509_explain.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=26 bytes (min=8 bytes, max=16 bytes) 𝄀 + Filter Cond: (t.id = cast('2' AS BIGINT)) +deallocate prepare parameterized; +drop database issue_29509_explain; diff --git a/test/distributed/cases/prepare/issue_29509_explain_analyze_execute.sql b/test/distributed/cases/prepare/issue_29509_explain_analyze_execute.sql new file mode 100644 index 0000000000000..86b7f5c410e28 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29509_explain_analyze_execute.sql @@ -0,0 +1,27 @@ +-- @case +-- @desc: Executable EXPLAIN retains the prepared plan snapshot across executions. +-- @label:bvt + +drop database if exists issue_29509_explain; +create database issue_29509_explain; +use issue_29509_explain; +create table t(id bigint primary key); +insert into t values (1), (2); + +prepare fixed from 'select count(*) from t where id = 1'; +-- @ignore:0 +explain analyze force execute fixed; +-- @ignore:0 +explain analyze force execute fixed; +deallocate prepare fixed; + +prepare parameterized from 'select count(*) from t where id = ?'; +set @id = 1; +-- @ignore:0 +explain analyze force execute parameterized using @id; +set @id = 2; +-- @ignore:0 +explain analyze force execute parameterized using @id; +deallocate prepare parameterized; + +drop database issue_29509_explain; diff --git a/test/distributed/cases/prepare/issue_29510_decimal_float_filter.result b/test/distributed/cases/prepare/issue_29510_decimal_float_filter.result new file mode 100644 index 0000000000000..3d789bea3ab16 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29510_decimal_float_filter.result @@ -0,0 +1,116 @@ +drop database if exists issue_29510_decimal_float; +create database issue_29510_decimal_float; +use issue_29510_decimal_float; +create table t(d decimal(12,2)); +insert into t select result from generate_series(1, 20000) g; +insert into t values (0.10), (0.11), (-12345.00); +explain select count(*) from t where d = cast(12345 as double); +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29510_decimal_float.t 𝄀 + Filter Cond: (t.d = 12345.00) 𝄀 + Block Filter Cond: (t.d = 12345.00) +explain analyze select count(*) from t where d = cast(12345 as double); +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29510_decimal_float.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=2 inputRows=8195 outputRows=1 (min=1, max=1) InputSize=64.02 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|35.21 KiB MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = 12345.00) 𝄀 + Block Filter Cond: (t.d = 12345.00) +select count(*) from t where d = cast(12345 as double); +➀ count(*)[-5,64,0] 𝄀 +1 +explain select count(*) from t where d = cast(12345.00 as double); +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29510_decimal_float.t 𝄀 + Filter Cond: (t.d = 12345.00) 𝄀 + Block Filter Cond: (t.d = 12345.00) +select count(*) from t where d = cast(12345.00 as double); +➀ count(*)[-5,64,0] 𝄀 +1 +select count(*) from t where d = cast(0.1 as double); +➀ count(*)[-5,64,0] 𝄀 +1 +select count(*) from t where d = cast(0.11 as double(3,1)); +➀ count(*)[-5,64,0] 𝄀 +1 +select count(*) from t where d = cast(0.14 as double(3,1)); +➀ count(*)[-5,64,0] 𝄀 +1 +explain select count(*) from t where d = cast(0.104 as double); +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29510_decimal_float.t 𝄀 + Filter Cond: (cast(t.d AS DOUBLE) = 0.104) + -- HINT: Cast expression may prevent index usage +select count(*) from t where d = cast(0.104 as double); +➀ count(*)[-5,64,0] 𝄀 +0 +select count(*) from t where d < cast(0.104 as double); +➀ count(*)[-5,64,0] 𝄀 +2 +select count(*) from t where d <= cast(0.1 as double); +➀ count(*)[-5,64,0] 𝄀 +2 +select count(*) from t where d = cast(-12345 as double); +➀ count(*)[-5,64,0] 𝄀 +1 +select count(*) from t where d = cast(100000000000 as double); +➀ count(*)[-5,64,0] 𝄀 +0 +prepare p from 'select count(*) from t where d = ?'; +set @v = cast(12345 as double); +explain force execute p using @v; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29510_decimal_float.t 𝄀 + Filter Cond: (cast(t.d AS DOUBLE) = 12345) + -- HINT: Cast expression may prevent index usage 𝄀 + Block Filter Cond: (cast(t.d AS DOUBLE) = 12345) +explain analyze force execute p using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29510_decimal_float.t 𝄀 + Analyze: timeConsumed=1ms waitTime=0ms inputBlocks=2 inputRows=8195 outputRows=1 (min=1, max=1) InputSize=64.02 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (cast(t.d AS DOUBLE) = 12345) + -- HINT: Cast expression may prevent index usage 𝄀 + Block Filter Cond: (cast(t.d AS DOUBLE) = 12345) +execute p using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = cast(0.104 as double); +execute p using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +deallocate prepare p; +create table wide(d decimal(20,0)); +insert into wide values (9007199254740992), (9007199254740993); +explain select count(*) from wide where d = cast(9007199254740992 as double); +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29510_decimal_float.wide 𝄀 + Filter Cond: (cast(wide.d AS DOUBLE) = 9007199254740992) + -- HINT: Cast expression may prevent index usage +select count(*) from wide where d = cast(9007199254740992 as double); +➀ count(*)[-5,64,0] 𝄀 +2 +drop database issue_29510_decimal_float; diff --git a/test/distributed/cases/prepare/issue_29510_decimal_float_filter.sql b/test/distributed/cases/prepare/issue_29510_decimal_float_filter.sql new file mode 100644 index 0000000000000..d57c8bc6ab148 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29510_decimal_float_filter.sql @@ -0,0 +1,43 @@ +-- @case +-- @desc: Unique DOUBLE peers retain native DECIMAL filters; ambiguous peers keep FLOAT comparison. +-- @label:bvt + +drop database if exists issue_29510_decimal_float; +create database issue_29510_decimal_float; +use issue_29510_decimal_float; +create table t(d decimal(12,2)); +insert into t select result from generate_series(1, 20000) g; +insert into t values (0.10), (0.11), (-12345.00); + +explain select count(*) from t where d = cast(12345 as double); +-- @ignore:0 +explain analyze select count(*) from t where d = cast(12345 as double); +select count(*) from t where d = cast(12345 as double); +explain select count(*) from t where d = cast(12345.00 as double); +select count(*) from t where d = cast(12345.00 as double); +select count(*) from t where d = cast(0.1 as double); +select count(*) from t where d = cast(0.11 as double(3,1)); +select count(*) from t where d = cast(0.14 as double(3,1)); +explain select count(*) from t where d = cast(0.104 as double); +select count(*) from t where d = cast(0.104 as double); +select count(*) from t where d < cast(0.104 as double); +select count(*) from t where d <= cast(0.1 as double); +select count(*) from t where d = cast(-12345 as double); +select count(*) from t where d = cast(100000000000 as double); + +prepare p from 'select count(*) from t where d = ?'; +set @v = cast(12345 as double); +explain force execute p using @v; +-- @ignore:0 +explain analyze force execute p using @v; +execute p using @v; +set @v = cast(0.104 as double); +execute p using @v; +deallocate prepare p; + +create table wide(d decimal(20,0)); +insert into wide values (9007199254740992), (9007199254740993); +explain select count(*) from wide where d = cast(9007199254740992 as double); +select count(*) from wide where d = cast(9007199254740992 as double); + +drop database issue_29510_decimal_float; diff --git a/test/distributed/cases/prepare/issue_29511_wider_integer_bindings.result b/test/distributed/cases/prepare/issue_29511_wider_integer_bindings.result new file mode 100644 index 0000000000000..9a63081bf0a89 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29511_wider_integer_bindings.result @@ -0,0 +1,112 @@ +drop database if exists issue_29511_filters; +create database issue_29511_filters; +use issue_29511_filters; +create table lookup_key(id int primary key, u bigint unsigned); +insert into lookup_key select result, result from generate_series(1, 20000) g; +prepare signed_key from 'select count(*) from lookup_key where id = ?'; +set @key = cast(12345 as signed); +explain force execute signed_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(cast('12345' AS BIGINT) AS INT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(cast('12345' AS BIGINT) AS INT)) +explain analyze force execute signed_key using @key; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=4 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=4 bytes OutputSize=4 bytes ReadSize=64.33 KiB|0 bytes|32.16 KiB MemorySize=9 bytes (min=9 bytes, max=9 bytes) 𝄀 + Filter Cond: (lookup_key.id = cast(cast('12345' AS BIGINT) AS INT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(cast('12345' AS BIGINT) AS INT)) +execute signed_key using @key; +➀ count(*)[-5,64,0] 𝄀 +1 +set @key = cast(4294967296 as signed); +explain force execute signed_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Filter Cond: (cast(lookup_key.id AS BIGINT) = cast('4294967296' AS BIGINT)) + -- HINT: Cast expression may prevent index usage +execute signed_key using @key; +➀ count(*)[-5,64,0] 𝄀 +0 +set @key = cast(-1 as signed); +explain force execute signed_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(cast('-1' AS BIGINT) AS INT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(cast('-1' AS BIGINT) AS INT)) +execute signed_key using @key; +➀ count(*)[-5,64,0] 𝄀 +0 +set @key = cast(12346 as signed); +execute signed_key using @key; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare signed_key; +prepare unsigned_key from 'select count(*) from lookup_key where u = ?'; +set @key = cast(12345 as signed); +explain force execute unsigned_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Filter Cond: (lookup_key.u = cast(cast('12345' AS BIGINT) AS BIGINT UNSIGNED)) 𝄀 + Block Filter Cond: (lookup_key.u = cast(cast('12345' AS BIGINT) AS BIGINT UNSIGNED)) +explain analyze force execute unsigned_key using @key; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=32.07 KiB|0 bytes|32.07 KiB MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (lookup_key.u = cast(cast('12345' AS BIGINT) AS BIGINT UNSIGNED)) 𝄀 + Block Filter Cond: (lookup_key.u = cast(cast('12345' AS BIGINT) AS BIGINT UNSIGNED)) +execute unsigned_key using @key; +➀ count(*)[-5,64,0] 𝄀 +1 +set @key = cast(-1 as signed); +explain force execute unsigned_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Filter Cond: (cast(lookup_key.u AS DECIMAL128(38, 0)) = cast(cast('-1' AS BIGINT) AS DECIMAL128(38, 0))) + -- HINT: Cast expression may prevent index usage +execute unsigned_key using @key; +➀ count(*)[-5,64,0] 𝄀 +0 +set @key = cast(9007199254740993 as signed); +explain force execute unsigned_key using @key; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29511_filters.lookup_key 𝄀 + Filter Cond: (cast(lookup_key.u AS DECIMAL128(38, 0)) = cast(cast('9007199254740993' AS BIGINT) AS DECIMAL128(38, 0))) + -- HINT: Cast expression may prevent index usage +execute unsigned_key using @key; +➀ count(*)[-5,64,0] 𝄀 +0 +set @key = cast(12346 as signed); +execute unsigned_key using @key; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare unsigned_key; +drop database issue_29511_filters; diff --git a/test/distributed/cases/prepare/issue_29511_wider_integer_bindings.sql b/test/distributed/cases/prepare/issue_29511_wider_integer_bindings.sql new file mode 100644 index 0000000000000..8d5e4c47178b6 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29511_wider_integer_bindings.sql @@ -0,0 +1,43 @@ +-- @case +-- @desc: Wider and differently signed integer bindings use native key filters only in a proven safe domain. +-- @label:bvt + +drop database if exists issue_29511_filters; +create database issue_29511_filters; +use issue_29511_filters; +create table lookup_key(id int primary key, u bigint unsigned); +insert into lookup_key select result, result from generate_series(1, 20000) g; + +prepare signed_key from 'select count(*) from lookup_key where id = ?'; +set @key = cast(12345 as signed); +explain force execute signed_key using @key; +-- @ignore:0 +explain analyze force execute signed_key using @key; +execute signed_key using @key; +set @key = cast(4294967296 as signed); +explain force execute signed_key using @key; +execute signed_key using @key; +set @key = cast(-1 as signed); +explain force execute signed_key using @key; +execute signed_key using @key; +set @key = cast(12346 as signed); +execute signed_key using @key; +deallocate prepare signed_key; + +prepare unsigned_key from 'select count(*) from lookup_key where u = ?'; +set @key = cast(12345 as signed); +explain force execute unsigned_key using @key; +-- @ignore:0 +explain analyze force execute unsigned_key using @key; +execute unsigned_key using @key; +set @key = cast(-1 as signed); +explain force execute unsigned_key using @key; +execute unsigned_key using @key; +set @key = cast(9007199254740993 as signed); +explain force execute unsigned_key using @key; +execute unsigned_key using @key; +set @key = cast(12346 as signed); +execute unsigned_key using @key; +deallocate prepare unsigned_key; + +drop database issue_29511_filters; diff --git a/test/distributed/cases/prepare/issue_29512_round_filter_pushdown.result b/test/distributed/cases/prepare/issue_29512_round_filter_pushdown.result new file mode 100644 index 0000000000000..2e463f661ce79 --- /dev/null +++ b/test/distributed/cases/prepare/issue_29512_round_filter_pushdown.result @@ -0,0 +1,214 @@ +drop database if exists issue_29512_round_filter; +create database issue_29512_round_filter; +use issue_29512_round_filter; +create table lookup_key(id bigint primary key); +insert into lookup_key select result from generate_series(1, 20000) g; +insert into lookup_key values (-12345), (9007199254740991), (9007199254740992), (9007199254740993); +prepare round_key from 'select count(*) from lookup_key where id = round(?, 0)'; +set @value = '12345.0'; +explain force execute round_key using @value; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +explain analyze force execute round_key using @value; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=64.13 KiB|0 bytes|32.07 KiB MemorySize=17 bytes (min=17 bytes, max=17 bytes) 𝄀 + Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +execute round_key using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +set @value = '1.2345e4'; +explain force execute round_key using @value; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(round(cast('1.2345e4' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(round(cast('1.2345e4' AS DOUBLE), 0) AS BIGINT)) +execute round_key using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +set @value = '-12345.0'; +execute round_key using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +set @value = '12345.5'; +explain force execute round_key using @value; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (cast(lookup_key.id AS DECIMAL128(38, 0)) = cast(round(cast('12345.5' AS DOUBLE), 0) AS DECIMAL128(38, 0))) + -- HINT: Cast expression may prevent index usage +execute round_key using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +set @value = '9007199254740992'; +explain force execute round_key using @value; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (cast(lookup_key.id AS DOUBLE) = round(cast('9007199254740992' AS DOUBLE), 0)) + -- HINT: Cast expression may prevent index usage 𝄀 + Block Filter Cond: (cast(lookup_key.id AS DOUBLE) = round(cast('9007199254740992' AS DOUBLE), 0)) +execute round_key using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +set @value = null; +execute round_key using @value; +➀ count(*)[-5,64,0] 𝄀 +0 +deallocate prepare round_key; +prepare scalar_round from 'select count(*) from lookup_key where id = round((select ?), 0)'; +set @value = '12345.0'; +explain force execute scalar_round using @value; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +explain analyze force execute scalar_round using @value; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=64.13 KiB|0 bytes|0 bytes MemorySize=17 bytes (min=17 bytes, max=17 bytes) 𝄀 + Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +execute scalar_round using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare scalar_round; +prepare derived_round from 'select count(*) from lookup_key k join (select ? as v) x where k.id = round(x.v, 0)'; +explain force execute derived_round using @value; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Join Type: INNER 𝄀 + Join Cond: (k.id = cast(round(cast(x.v AS DECIMAL64(5, 0)), 0) AS BIGINT)) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Runtime Filter Probe: k.id 𝄀 + -> Project 𝄀 + -> Values Scan "*VALUES*" +explain analyze force execute derived_round using @value; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=2 outputRows=1 (min=1, max=1) InputSize=32 bytes OutputSize=0 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=32.16 KiB (min=16.06 KiB, max=16.09 KiB) 𝄀 + Join Type: INNER 𝄀 + Join Cond: (k.id = cast(round(cast(x.v AS DECIMAL64(5, 0)), 0) AS BIGINT)) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=64.13 KiB|0 bytes|0 bytes MemorySize=16 bytes (min=16 bytes, max=16 bytes) 𝄀 + Runtime Filter Probe: k.id 𝄀 + -> Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=24 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=24 bytes (min=24 bytes, max=24 bytes) 𝄀 + -> Values Scan "*VALUES*" 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) +execute derived_round using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare derived_round; +prepare truncate_key from 'select count(*) from lookup_key where id = truncate(?, 0)'; +explain force execute truncate_key using @value; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(truncate(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(truncate(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +explain analyze force execute truncate_key using @value; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=64.13 KiB|0 bytes|0 bytes MemorySize=17 bytes (min=17 bytes, max=17 bytes) 𝄀 + Filter Cond: (lookup_key.id = cast(truncate(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(truncate(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +execute truncate_key using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +set @value = '12345.5'; +execute truncate_key using @value; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare truncate_key; +prepare dynamic_round from 'select count(*) from lookup_key where id = round(?, ?)'; +set @value = '12345.0', @precision = 0; +explain force execute dynamic_round using @value, @precision; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +execute dynamic_round using @value, @precision; +➀ count(*)[-5,64,0] 𝄀 +1 +set @precision = -1; +execute dynamic_round using @value, @precision; +➀ count(*)[-5,64,0] 𝄀 +1 +set @precision = null; +execute dynamic_round using @value, @precision; +➀ count(*)[-5,64,0] 𝄀 +0 +set @precision = 0; +explain force execute dynamic_round using @value, @precision; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(round(cast('12345.0' AS DOUBLE), 0) AS BIGINT)) +execute dynamic_round using @value, @precision; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare dynamic_round; +prepare dynamic_truncate from 'select count(*) from lookup_key where id = truncate(?, cast(? as signed))'; +explain force execute dynamic_truncate using @value, @precision; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29512_round_filter.lookup_key 𝄀 + Filter Cond: (lookup_key.id = cast(truncate(cast('12345.0' AS DOUBLE), cast(cast('0' AS BIGINT) AS BIGINT)) AS BIGINT)) 𝄀 + Block Filter Cond: (lookup_key.id = cast(truncate(cast('12345.0' AS DOUBLE), cast(cast('0' AS BIGINT) AS BIGINT)) AS BIGINT)) +execute dynamic_truncate using @value, @precision; +➀ count(*)[-5,64,0] 𝄀 +1 +deallocate prepare dynamic_truncate; +drop database issue_29512_round_filter; diff --git a/test/distributed/cases/prepare/issue_29512_round_filter_pushdown.sql b/test/distributed/cases/prepare/issue_29512_round_filter_pushdown.sql new file mode 100644 index 0000000000000..72e2e388cef5b --- /dev/null +++ b/test/distributed/cases/prepare/issue_29512_round_filter_pushdown.sql @@ -0,0 +1,75 @@ +-- @case +-- @desc: Proven integral ROUND and TRUNCATE text bindings retain integer-key filtering. +-- @label:bvt + +drop database if exists issue_29512_round_filter; +create database issue_29512_round_filter; +use issue_29512_round_filter; +create table lookup_key(id bigint primary key); +insert into lookup_key select result from generate_series(1, 20000) g; +insert into lookup_key values (-12345), (9007199254740991), (9007199254740992), (9007199254740993); + +prepare round_key from 'select count(*) from lookup_key where id = round(?, 0)'; +set @value = '12345.0'; +explain force execute round_key using @value; +-- @ignore:0 +explain analyze force execute round_key using @value; +execute round_key using @value; +set @value = '1.2345e4'; +explain force execute round_key using @value; +execute round_key using @value; +set @value = '-12345.0'; +execute round_key using @value; +set @value = '12345.5'; +explain force execute round_key using @value; +execute round_key using @value; +set @value = '9007199254740992'; +explain force execute round_key using @value; +execute round_key using @value; +set @value = null; +execute round_key using @value; +deallocate prepare round_key; + +prepare scalar_round from 'select count(*) from lookup_key where id = round((select ?), 0)'; +set @value = '12345.0'; +explain force execute scalar_round using @value; +-- @ignore:0 +explain analyze force execute scalar_round using @value; +execute scalar_round using @value; +deallocate prepare scalar_round; + +prepare derived_round from 'select count(*) from lookup_key k join (select ? as v) x where k.id = round(x.v, 0)'; +explain force execute derived_round using @value; +-- @ignore:0 +explain analyze force execute derived_round using @value; +execute derived_round using @value; +deallocate prepare derived_round; + +prepare truncate_key from 'select count(*) from lookup_key where id = truncate(?, 0)'; +explain force execute truncate_key using @value; +-- @ignore:0 +explain analyze force execute truncate_key using @value; +execute truncate_key using @value; +set @value = '12345.5'; +execute truncate_key using @value; +deallocate prepare truncate_key; + +-- Reuse one execution shape across precision changes; zero alone is selective. +prepare dynamic_round from 'select count(*) from lookup_key where id = round(?, ?)'; +set @value = '12345.0', @precision = 0; +explain force execute dynamic_round using @value, @precision; +execute dynamic_round using @value, @precision; +set @precision = -1; +execute dynamic_round using @value, @precision; +set @precision = null; +execute dynamic_round using @value, @precision; +set @precision = 0; +explain force execute dynamic_round using @value, @precision; +execute dynamic_round using @value, @precision; +deallocate prepare dynamic_round; +prepare dynamic_truncate from 'select count(*) from lookup_key where id = truncate(?, cast(? as signed))'; +explain force execute dynamic_truncate using @value, @precision; +execute dynamic_truncate using @value, @precision; +deallocate prepare dynamic_truncate; + +drop database issue_29512_round_filter; diff --git a/test/distributed/cases/prepare/issue_29514_decimal_expression_filter.result b/test/distributed/cases/prepare/issue_29514_decimal_expression_filter.result new file mode 100644 index 0000000000000..f8d3c38ee4ffa --- /dev/null +++ b/test/distributed/cases/prepare/issue_29514_decimal_expression_filter.result @@ -0,0 +1,563 @@ +drop database if exists issue_29514_decimal_expr; +create database issue_29514_decimal_expr; +use issue_29514_decimal_expr; +create table t(d decimal(12,2)); +insert into t select result from generate_series(1, 100000) g; +explain select count(*) from t where d = (select cast(54321 as double)); +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Filter Cond: (t.d = 54321.00) 𝄀 + Block Filter Cond: (t.d = 54321.00) +explain analyze select count(*) from t where d = (select cast(54321 as double)); +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|35.21 KiB MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = 54321.00) 𝄀 + Block Filter Cond: (t.d = 54321.00) +select count(*) from t where d = (select cast(54321 as double)); +➀ count(*)[-5,64,0] 𝄀 +1 +explain select count(*) from t where d = abs(cast(54321 as double)); +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Filter Cond: (t.d = 54321.00) 𝄀 + Block Filter Cond: (t.d = 54321.00) +explain analyze select count(*) from t where d = abs(cast(54321 as double)); +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = 54321.00) 𝄀 + Block Filter Cond: (t.d = 54321.00) +select count(*) from t where d = abs(cast(54321 as double)); +➀ count(*)[-5,64,0] 𝄀 +1 +explain select count(*) from t join (select cast(54321 as double) as v) x on t.d = x.v; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + -> Values Scan "*VALUES*" +explain analyze select count(*) from t join (select cast(54321 as double) as v) x on t.d = x.v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=0 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=32.14 KiB (min=16.06 KiB, max=16.08 KiB) 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Values Scan "*VALUES*" 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) +select count(*) from t join (select cast(54321 as double) as v) x on t.d = x.v; +➀ count(*)[-5,64,0] 𝄀 +1 +explain select count(*) from t join (select abs(cast(54321 as double)) as v) x on t.d = x.v; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + -> Values Scan "*VALUES*" +explain analyze select count(*) from t join (select abs(cast(54321 as double)) as v) x on t.d = x.v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=0 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=32.14 KiB (min=16.06 KiB, max=16.08 KiB) 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Values Scan "*VALUES*" 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) +select count(*) from t join (select abs(cast(54321 as double)) as v) x on t.d = x.v; +➀ count(*)[-5,64,0] 𝄀 +1 +select count(*) from t where d = (select cast(54321.104 as double)); +➀ count(*)[-5,64,0] 𝄀 +0 +select count(*) from t where d = abs(cast(54321.104 as double)); +➀ count(*)[-5,64,0] 𝄀 +0 +select count(*) from t join (select cast(54321.104 as double) as v) x on t.d = x.v; +➀ count(*)[-5,64,0] 𝄀 +0 +select count(*) from t join (select abs(cast(54321.104 as double)) as v) x on t.d = x.v; +➀ count(*)[-5,64,0] 𝄀 +0 +select count(*) from t where d = (select cast(null as double)); +➀ count(*)[-5,64,0] 𝄀 +0 +select count(*) from t join (select cast(null as double) as v) x on t.d = x.v; +➀ count(*)[-5,64,0] 𝄀 +0 +select count(*) from t join (select d as v from t where d in (54321, 54322)) x on t.d = x.v; +➀ count(*)[-5,64,0] 𝄀 +2 +set @v = cast(54321 as double); +prepare scalar_peer from 'select count(*) from t where d = (select ?)'; +explain analyze force execute scalar_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (cast(t.d AS DOUBLE) = 54321) + -- HINT: Cast expression may prevent index usage 𝄀 + Block Filter Cond: (cast(t.d AS DOUBLE) = 54321) +execute scalar_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare abs_peer from 'select count(*) from t where d = abs(?)'; +explain analyze force execute abs_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = cast(abs(54321) AS DECIMAL64(12, 2))) 𝄀 + Block Filter Cond: (t.d = cast(abs(54321) AS DECIMAL64(12, 2))) +execute abs_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare derived_peer from 'select count(*) from t join (select ? as v) x on t.d = x.v'; +explain analyze force execute derived_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=0 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=32.14 KiB (min=16.06 KiB, max=16.08 KiB) 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Values Scan "*VALUES*" 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) +execute derived_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare derived_abs_peer from 'select count(*) from t join (select abs(?) as v) x on t.d = x.v'; +explain analyze force execute derived_abs_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=0 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=32.14 KiB (min=16.06 KiB, max=16.08 KiB) 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Values Scan "*VALUES*" 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) +execute derived_abs_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @one = 1; +prepare second_peer from 'select count(*) from t where ? = 1 and d = abs(?)'; +explain analyze force execute second_peer using @one, @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = cast(abs(54321) AS DECIMAL64(12, 2))), (cast('1' AS BIGINT) = 1) 𝄀 + Block Filter Cond: (t.d = cast(abs(54321) AS DECIMAL64(12, 2))) +execute second_peer using @one, @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = cast(0.104 as double); +execute scalar_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute abs_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute derived_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute derived_abs_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute second_peer using @one, @v; +➀ count(*)[-5,64,0] 𝄀 +0 +set @v = null; +execute scalar_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute abs_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute derived_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute derived_abs_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute second_peer using @one, @v; +➀ count(*)[-5,64,0] 𝄀 +0 +deallocate prepare scalar_peer; +deallocate prepare abs_peer; +deallocate prepare derived_peer; +deallocate prepare derived_abs_peer; +deallocate prepare second_peer; +set @v = cast(54321 as double); +prepare cast_peer from 'select count(*) from t where d = cast(? as double)'; +explain analyze force execute cast_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = cast(cast(cast('54321' AS DOUBLE) AS DOUBLE) AS DECIMAL64(12, 2))) 𝄀 + Block Filter Cond: (t.d = cast(cast(cast('54321' AS DOUBLE) AS DOUBLE) AS DECIMAL64(12, 2))) +execute cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare scalar_cast_peer from 'select count(*) from t where d = (select cast(? as double))'; +explain analyze force execute scalar_cast_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = cast(cast(cast('54321' AS DOUBLE) AS DOUBLE) AS DECIMAL64(12, 2))) 𝄀 + Block Filter Cond: (t.d = cast(cast(cast('54321' AS DOUBLE) AS DOUBLE) AS DECIMAL64(12, 2))) +execute scalar_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare derived_cast_peer from 'select count(*) from t join (select cast(? as double) as v) x on t.d = x.v'; +explain analyze force execute derived_cast_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=0 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=32.14 KiB (min=16.06 KiB, max=16.08 KiB) 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Values Scan "*VALUES*" 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) +execute derived_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare precision_cast_peer from 'select count(*) from t where d = cast(? as double(5,0))'; +execute precision_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = cast(0.104 as double); +execute cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute scalar_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute derived_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute precision_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +set @v = null; +execute cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute scalar_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +execute derived_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +set @v = '54321junk'; +execute cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = 'abc'; +execute cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +deallocate prepare cast_peer; +deallocate prepare scalar_cast_peer; +deallocate prepare derived_cast_peer; +deallocate prepare precision_cast_peer; +set @v = '54321'; +prepare text_cast_peer from 'select count(*) from t where d = cast(? as double)'; +explain analyze force execute text_cast_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = cast(cast('54321' AS DOUBLE) AS DECIMAL64(12, 2))) 𝄀 + Block Filter Cond: (t.d = cast(cast('54321' AS DOUBLE) AS DECIMAL64(12, 2))) +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare text_scalar_cast_peer from 'select count(*) from t where d = (select cast(? as double))'; +explain analyze force execute text_scalar_cast_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Filter Cond: (t.d = cast(cast('54321' AS DOUBLE) AS DECIMAL64(12, 2))) 𝄀 + Block Filter Cond: (t.d = cast(cast('54321' AS DOUBLE) AS DECIMAL64(12, 2))) +execute text_scalar_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare text_derived_cast_peer from 'select count(*) from t join (select cast(? as double) as v) x on t.d = x.v'; +explain analyze force execute text_derived_cast_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=64.00 KiB (min=64.00 KiB, max=64.00 KiB) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Join 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=2 outputRows=1 (min=1, max=1) InputSize=16 bytes OutputSize=0 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=32.14 KiB (min=16.06 KiB, max=16.08 KiB) 𝄀 + Join Type: INNER 𝄀 + Join Cond: (t.d = cast(x.v AS DECIMAL64(12, 2))) 𝄀 + Runtime Filter Build: #[-1,0] 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputBlocks=1 inputRows=8192 outputRows=1 (min=1, max=1) InputSize=64.00 KiB OutputSize=8 bytes ReadSize=35.21 KiB|0 bytes|0 bytes MemorySize=72.01 KiB (min=72.01 KiB, max=72.01 KiB) 𝄀 + Runtime Filter Probe: t.d 𝄀 + -> Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Values Scan "*VALUES*" 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) +execute text_derived_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +prepare text_abs_cast_peer from 'select count(*) from t where d = abs(cast(? as double))'; +execute text_abs_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = ' 54321 '; +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = '+54321'; +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = '5.4321e4'; +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +set @v = '0.104'; +explain analyze force execute text_cast_peer using @v; +➀ tp query plan[12,-1,0] 𝄀 +Project 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=1 outputRows=1 (min=1, max=1) InputSize=8 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=8 bytes (min=8 bytes, max=8 bytes) 𝄀 + -> Aggregate 𝄀 + Analyze: timeConsumed=0ms waitTime=0ms inputRows=0 outputRows=1 (min=1, max=1) InputSize=0 bytes OutputSize=8 bytes ReadSize=0 bytes|0 bytes|0 bytes MemorySize=0 bytes (min=0 bytes, max=0 bytes) 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Analyze: timeConsumed=70ms waitTime=0ms inputBlocks=13 inputRows=100000 outputRows=0 (min=0, max=0) InputSize=781.25 KiB OutputSize=0 bytes ReadSize=429.79 KiB|0 bytes|0 bytes MemorySize=161.66 KiB (min=161.66 KiB, max=161.66 KiB) 𝄀 + Filter Cond: (cast(t.d AS DOUBLE) = cast('0.104' AS DOUBLE)) + -- HINT: Cast expression may prevent index usage +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +set @v = '54321junk'; +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +1 +show warnings; +➀ Level[12,0,0] Β¦ Code[5,0,0] Β¦ Message[12,0,0] 𝄀 +Warning Β¦ 1292 Β¦ Truncated incorrect DOUBLE value: '54321junk' +set @v = 'abc'; +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +show warnings; +➀ Level[12,0,0] Β¦ Code[5,0,0] Β¦ Message[12,0,0] 𝄀 +Warning Β¦ 1292 Β¦ Truncated incorrect DOUBLE value: 'abc' +set @v = null; +execute text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +0 +deallocate prepare text_cast_peer; +deallocate prepare text_scalar_cast_peer; +deallocate prepare text_derived_cast_peer; +deallocate prepare text_abs_cast_peer; +create table wide(d decimal(20,0)); +insert into wide values (9007199254740992), (9007199254740993); +select count(*) from wide where d = (select cast(9007199254740992 as double)); +➀ count(*)[-5,64,0] 𝄀 +2 +select count(*) from wide join (select cast(9007199254740992 as double) as v) x on wide.d = x.v; +➀ count(*)[-5,64,0] 𝄀 +2 +set @v = cast(9007199254740992 as double); +prepare wide_scalar_peer from 'select count(*) from wide where d = (select ?)'; +execute wide_scalar_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +2 +deallocate prepare wide_scalar_peer; +prepare wide_derived_peer from 'select count(*) from wide join (select ? as v) x on wide.d = x.v'; +execute wide_derived_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +2 +deallocate prepare wide_derived_peer; +set @v = cast(9007199254740992 as double); +prepare wide_cast_peer from 'select count(*) from wide where d = cast(? as double)'; +execute wide_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +2 +deallocate prepare wide_cast_peer; +set @v = '9007199254740992'; +prepare wide_text_cast_peer from 'select count(*) from wide where d = cast(? as double)'; +execute wide_text_cast_peer using @v; +➀ count(*)[-5,64,0] 𝄀 +2 +deallocate prepare wide_text_cast_peer; +set @a = cast(54321 as double), @b = cast(54322 as double); +prepare boolean_peers from 'select count(*) from t where d = abs(?) or d = abs(?)'; +explain force execute boolean_peers using @a, @b; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Filter Cond: t.d in (cast(abs(54321) AS DECIMAL64(12, 2)), cast(abs(54322) AS DECIMAL64(12, 2))) 𝄀 + Block Filter Cond: t.d in (cast(abs(54321) AS DECIMAL64(12, 2)), cast(abs(54322) AS DECIMAL64(12, 2))) +execute boolean_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +2 +deallocate prepare boolean_peers; +prepare list_peers from 'select count(*) from t where d in (abs(?), abs(?))'; +explain force execute list_peers using @a, @b; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Filter Cond: t.d in (cast(abs(54321) AS DECIMAL64(12, 2)), cast(abs(54322) AS DECIMAL64(12, 2))) 𝄀 + Block Filter Cond: t.d in (cast(abs(54321) AS DECIMAL64(12, 2)), cast(abs(54322) AS DECIMAL64(12, 2))) +execute list_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +2 +set @b = cast(54322.104 as double); +execute list_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +1 +set @b = null; +execute list_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +1 +set @b = cast(54322 as double); +execute list_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +2 +deallocate prepare list_peers; +prepare range_peers from 'select count(*) from t where d between abs(?) and abs(?)'; +explain force execute range_peers using @a, @b; +➀ TP QUERY PLAN[12,-1,0] 𝄀 +Project 𝄀 + -> Aggregate 𝄀 + Aggregate Functions: starcount(1) 𝄀 + -> Table Scan on issue_29514_decimal_expr.t 𝄀 + Filter Cond: t.d BETWEEN cast(abs(54321) AS DECIMAL64(12, 2)) AND cast(abs(54322) AS DECIMAL64(12, 2)) 𝄀 + Block Filter Cond: t.d BETWEEN cast(abs(54321) AS DECIMAL64(12, 2)) AND cast(abs(54322) AS DECIMAL64(12, 2)) +execute range_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +2 +set @a = cast(54321.104 as double); +execute range_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +1 +set @a = cast(54323 as double); +execute range_peers using @a, @b; +➀ count(*)[-5,64,0] 𝄀 +0 +deallocate prepare range_peers; +drop database issue_29514_decimal_expr; diff --git a/test/distributed/cases/prepare/issue_29514_decimal_expression_filter.sql b/test/distributed/cases/prepare/issue_29514_decimal_expression_filter.sql new file mode 100644 index 0000000000000..051ccf66ad9aa --- /dev/null +++ b/test/distributed/cases/prepare/issue_29514_decimal_expression_filter.sql @@ -0,0 +1,196 @@ +-- @case +-- @desc: Foldable DOUBLE peers keep selective DECIMAL filtering across scalar, function and singleton-derived forms. +-- @label:bvt + +drop database if exists issue_29514_decimal_expr; +create database issue_29514_decimal_expr; +use issue_29514_decimal_expr; +create table t(d decimal(12,2)); +insert into t select result from generate_series(1, 100000) g; + +explain select count(*) from t where d = (select cast(54321 as double)); +-- @ignore:0 +explain analyze select count(*) from t where d = (select cast(54321 as double)); +select count(*) from t where d = (select cast(54321 as double)); +explain select count(*) from t where d = abs(cast(54321 as double)); +-- @ignore:0 +explain analyze select count(*) from t where d = abs(cast(54321 as double)); +select count(*) from t where d = abs(cast(54321 as double)); +explain select count(*) from t join (select cast(54321 as double) as v) x on t.d = x.v; +-- @ignore:0 +explain analyze select count(*) from t join (select cast(54321 as double) as v) x on t.d = x.v; +select count(*) from t join (select cast(54321 as double) as v) x on t.d = x.v; +explain select count(*) from t join (select abs(cast(54321 as double)) as v) x on t.d = x.v; +-- @ignore:0 +explain analyze select count(*) from t join (select abs(cast(54321 as double)) as v) x on t.d = x.v; +select count(*) from t join (select abs(cast(54321 as double)) as v) x on t.d = x.v; + +select count(*) from t where d = (select cast(54321.104 as double)); +select count(*) from t where d = abs(cast(54321.104 as double)); +select count(*) from t join (select cast(54321.104 as double) as v) x on t.d = x.v; +select count(*) from t join (select abs(cast(54321.104 as double)) as v) x on t.d = x.v; +select count(*) from t where d = (select cast(null as double)); +select count(*) from t join (select cast(null as double) as v) x on t.d = x.v; +select count(*) from t join (select d as v from t where d in (54321, 54322)) x on t.d = x.v; + +-- #29515: execution-time DOUBLE parameters in equivalent expression shapes. +set @v = cast(54321 as double); +prepare scalar_peer from 'select count(*) from t where d = (select ?)'; +-- @ignore:0 +explain analyze force execute scalar_peer using @v; +execute scalar_peer using @v; +prepare abs_peer from 'select count(*) from t where d = abs(?)'; +-- @ignore:0 +explain analyze force execute abs_peer using @v; +execute abs_peer using @v; +prepare derived_peer from 'select count(*) from t join (select ? as v) x on t.d = x.v'; +-- @ignore:0 +explain analyze force execute derived_peer using @v; +execute derived_peer using @v; +prepare derived_abs_peer from 'select count(*) from t join (select abs(?) as v) x on t.d = x.v'; +-- @ignore:0 +explain analyze force execute derived_abs_peer using @v; +execute derived_abs_peer using @v; +set @one = 1; +prepare second_peer from 'select count(*) from t where ? = 1 and d = abs(?)'; +-- @ignore:0 +explain analyze force execute second_peer using @one, @v; +execute second_peer using @one, @v; +set @v = cast(0.104 as double); +execute scalar_peer using @v; +execute abs_peer using @v; +execute derived_peer using @v; +execute derived_abs_peer using @v; +execute second_peer using @one, @v; +set @v = null; +execute scalar_peer using @v; +execute abs_peer using @v; +execute derived_peer using @v; +execute derived_abs_peer using @v; +execute second_peer using @one, @v; +deallocate prepare scalar_peer; +deallocate prepare abs_peer; +deallocate prepare derived_peer; +deallocate prepare derived_abs_peer; +deallocate prepare second_peer; + +-- #29516: the explicit CAST type argument must participate in safe folding. +set @v = cast(54321 as double); +prepare cast_peer from 'select count(*) from t where d = cast(? as double)'; +-- @ignore:0 +explain analyze force execute cast_peer using @v; +execute cast_peer using @v; +prepare scalar_cast_peer from 'select count(*) from t where d = (select cast(? as double))'; +-- @ignore:0 +explain analyze force execute scalar_cast_peer using @v; +execute scalar_cast_peer using @v; +prepare derived_cast_peer from 'select count(*) from t join (select cast(? as double) as v) x on t.d = x.v'; +-- @ignore:0 +explain analyze force execute derived_cast_peer using @v; +execute derived_cast_peer using @v; +prepare precision_cast_peer from 'select count(*) from t where d = cast(? as double(5,0))'; +execute precision_cast_peer using @v; +set @v = cast(0.104 as double); +execute cast_peer using @v; +execute scalar_cast_peer using @v; +execute derived_cast_peer using @v; +execute precision_cast_peer using @v; +set @v = null; +execute cast_peer using @v; +execute scalar_cast_peer using @v; +execute derived_cast_peer using @v; +set @v = '54321junk'; +execute cast_peer using @v; +set @v = 'abc'; +execute cast_peer using @v; +deallocate prepare cast_peer; +deallocate prepare scalar_cast_peer; +deallocate prepare derived_cast_peer; +deallocate prepare precision_cast_peer; + +-- #29517: complete text parameters inside explicit DOUBLE casts can be +-- folded for the uniqueness proof; partial numeric text must retain warnings. +set @v = '54321'; +prepare text_cast_peer from 'select count(*) from t where d = cast(? as double)'; +-- @ignore:0 +explain analyze force execute text_cast_peer using @v; +execute text_cast_peer using @v; +prepare text_scalar_cast_peer from 'select count(*) from t where d = (select cast(? as double))'; +-- @ignore:0 +explain analyze force execute text_scalar_cast_peer using @v; +execute text_scalar_cast_peer using @v; +prepare text_derived_cast_peer from 'select count(*) from t join (select cast(? as double) as v) x on t.d = x.v'; +-- @ignore:0 +explain analyze force execute text_derived_cast_peer using @v; +execute text_derived_cast_peer using @v; +prepare text_abs_cast_peer from 'select count(*) from t where d = abs(cast(? as double))'; +execute text_abs_cast_peer using @v; +set @v = ' 54321 '; +execute text_cast_peer using @v; +set @v = '+54321'; +execute text_cast_peer using @v; +set @v = '5.4321e4'; +execute text_cast_peer using @v; +set @v = '0.104'; +-- @ignore:0 +explain analyze force execute text_cast_peer using @v; +execute text_cast_peer using @v; +set @v = '54321junk'; +execute text_cast_peer using @v; +show warnings; +set @v = 'abc'; +execute text_cast_peer using @v; +show warnings; +set @v = null; +execute text_cast_peer using @v; +deallocate prepare text_cast_peer; +deallocate prepare text_scalar_cast_peer; +deallocate prepare text_derived_cast_peer; +deallocate prepare text_abs_cast_peer; + +create table wide(d decimal(20,0)); +insert into wide values (9007199254740992), (9007199254740993); +select count(*) from wide where d = (select cast(9007199254740992 as double)); +select count(*) from wide join (select cast(9007199254740992 as double) as v) x on wide.d = x.v; +set @v = cast(9007199254740992 as double); +prepare wide_scalar_peer from 'select count(*) from wide where d = (select ?)'; +execute wide_scalar_peer using @v; +deallocate prepare wide_scalar_peer; +prepare wide_derived_peer from 'select count(*) from wide join (select ? as v) x on wide.d = x.v'; +execute wide_derived_peer using @v; +deallocate prepare wide_derived_peer; +set @v = cast(9007199254740992 as double); +prepare wide_cast_peer from 'select count(*) from wide where d = cast(? as double)'; +execute wide_cast_peer using @v; +deallocate prepare wide_cast_peer; +set @v = '9007199254740992'; +prepare wide_text_cast_peer from 'select count(*) from wide where d = cast(? as double)'; +execute wide_text_cast_peer using @v; +deallocate prepare wide_text_cast_peer; + +-- Boolean and multi-peer predicates must retain native DECIMAL pruning. +set @a = cast(54321 as double), @b = cast(54322 as double); +prepare boolean_peers from 'select count(*) from t where d = abs(?) or d = abs(?)'; +explain force execute boolean_peers using @a, @b; +execute boolean_peers using @a, @b; +deallocate prepare boolean_peers; +prepare list_peers from 'select count(*) from t where d in (abs(?), abs(?))'; +explain force execute list_peers using @a, @b; +execute list_peers using @a, @b; +set @b = cast(54322.104 as double); +execute list_peers using @a, @b; +set @b = null; +execute list_peers using @a, @b; +set @b = cast(54322 as double); +execute list_peers using @a, @b; +deallocate prepare list_peers; +prepare range_peers from 'select count(*) from t where d between abs(?) and abs(?)'; +explain force execute range_peers using @a, @b; +execute range_peers using @a, @b; +set @a = cast(54321.104 as double); +execute range_peers using @a, @b; +set @a = cast(54323 as double); +execute range_peers using @a, @b; +deallocate prepare range_peers; + +drop database issue_29514_decimal_expr;