docs/agents/planner/rule/rule_ai_notes.md
This file records planner rule related PR experience and pitfalls. Update an existing section when the topic overlaps; append a new dated entry only for a genuinely new topic.
Background:
LEFT JOIN ... ON NOT NOT (t0.k0 = t2.k0) treated as an other condition and leading to a cartesian-like join behavior.Key takeaways:
PushDownNot inside applyPredicateSimplificationHelper applies to WHERE predicates; outer join ON conditions are not simplified there.InnerJoin/SemiJoin fold join conditions into tempCond and go through ApplyPredicateSimplificationForJoin, so NOT NOT on logical ops is already handled.PushDownNot to OtherConditions to avoid not(not(eq)) becoming an other condition that triggers cartesian joins.Implementation choice:
LogicalJoin.PredicatePushDown, before outer join processing, run PushDownNot on OtherConditions only to eliminate double NOT.UnaryNot presence check to avoid extra overhead.Test and verification:
pkg/planner/core/casetest/rule/testdata/predicate_pushdown_suite_in.json.go test ./pkg/planner/core/casetest/rule -run TestConstantPropagateWithCollation -record -tags=intest,deadlock.left outer join keeps equal:[eq(t0.k0, t2.k0)].Test data pattern used:
predicate_pushdown_suite_in.json.EXPLAIN format='brief' and query results in predicate_pushdown_suite_out.json, so the case list remains simple and readable.
Additional notes:pushNotAcrossExpr already eliminates not(not(expr)) when expr is a logical operator, because wrapWithIsTrue returns logical ops unchanged.IsTruthWithNull semantics.Background:
LEFT JOIN ... ON (a = b OR 0) and the OR constant prevented join key extraction, leading to a cartesian-like join behavior and wrong results.Key takeaways:
OtherConditions are not simplified by predicate pushdown, so trivial OR/AND constants must be normalized before updateEQCond.ApplyPredicateSimplificationForJoin on OtherConditions is sufficient to remove OR 0 and keep equality keys.Implementation choice:
LogicalJoin.normalizeJoinConditionsForOuterJoin, call ApplyPredicateSimplificationForJoin with propagateConstant=false on OtherConditions.Test and verification:
predicate_pushdown_suite_in.json; keep DDL in the test setup, otherwise explain will try to run DROP/CREATE.go test ./pkg/planner/core/casetest/rule -run TestConstantPropagateWithCollation -tags=intest,deadlock -record.tests/integrationtest/t/select.test and record via pushd tests/integrationtest && ./run-tests.sh -r select && popd (integration tests use -r, not -record).Background:
runtime error: index out of range [2] with length 2.ADD/DROP INDEX on the same table is serialized, so the issue is not caused by concurrent index DDL jobs running at the same time.Root cause:
path.IdxCols for execution-range construction.detachCondAndBuildRangeForPath, ranges were built from the extended column set, but row-count estimation used indexCols truncated to index-definition columns.index stats invalid + column stats available), cardinality estimation iterated using range dimension and read idxCols[i], which could overflow when ranges had an extra handle dimension.Why it is not deterministic:
Implementation choice:
indexCols) to preserve existing plan-estimation behavior.pkg/planner/core/stats.go, if execution ranges carry appended handle dimensions, prune each range to the first len(indexCols) dimensions and use the pruned ranges for estimation.DetachCondAndBuildRangeForIndex for estimation only.Test and verification:
TestIndexRangeEstimationWithAppendedHandleColumn in pkg/planner/cardinality/selectivity_test.go.missing index stats + available column stats) and verifies EXPLAIN does not panic.go test ./pkg/planner/cardinality -run TestIndexRangeEstimationWithAppendedHandleColumn --tags=intest -count=1Reusable lessons:
index stats invalid + partial column stats) instead of relying on random timing.Background:
setIndexMergeTableScanHandleCols and overwritePartialTableScanSchema to reuse UnMutableHandleCols instead of manufacturing _tidb_rowid.PhysicalIndexScan.InitSchema: when scanning p.Columns for the handle column, the code still created a fresh fallback column if DataSourceSchema lookup failed.Key takeaways:
DataSource, PhysicalIndexScan.DataSourceSchema is the original datasource schema (ds.Schema()), not a pruned KV schema.DataSource, via HandleCols / UnMutableHandleCols, so the handle column should already be discoverable from DataSourceSchema.InitSchema hides invariant violations and can silently diverge from the rest of the index-merge handle-column logic.Implementation choice:
UnMutableHandleCols in both setIndexMergeTableScanHandleCols and overwritePartialTableScanSchema.PhysicalIndexScan.InitSchema, require handle lookup to succeed from DataSourceSchema; use intest.Assert to make violations explicit in test builds instead of silently fabricating a new column.Reusable lessons:
DataSourceSchema for logical/original columns,HandleCols / UnMutableHandleCols for stable handle identity,_tidb_rowid only for tables that truly need extra handle.