Skip to content

Commit bfc91ee

Browse files
committed
tree: handle statement fingerprints as HintInjectionDonors
The hint injection code added in #153689 could handle regular statements as hint injection donors, but we also need to handle statement *fingerprints* as hint injection donors. (In fact, we expect this will be the common case.) Statement fingerprints have several differences from regular SQL statements: - Constants and placeholders are replaced with _, which parse as `UnresolvedName("_")`. - Tuples, Arrays, and ValuesClauses could be collapsed to `(_)` or `(_, __more__)` depending on how many items they contain. - ValuesClauses could also look like `((1, 2, __more__), (__more__))` We need to be able to match a donor statement fingerprint against a regular target statement during hint injection. To do so, this commit makes several changes: 1. A "collapsed" stack of booleans is maintained while walking the ASTs to track whether the current Tuple, Array, or ValuesClause in the donor has been collapsed into `__more__`. When this is true, we skip over the rest of the nodes until the end of the current Tuple, Array, or ValuesClause. 2. We now skip ParenExpr in the AST walks, since they're sometimes added to statement fingerprints. 3. `_` and `__more__` now act as wildcards during the target statement AST walk, matching any node. (And then after `__more__` we mark the current list as collapsed.) 4. JoinType of INNER (e.g. the INNER in INNER LOOKUP JOIN) needed special handling, because it's required when a join hint is added, but it is optional without a hint. To make validation work, we now elide INNER when FmtHideHints is set. But we also carry over an optional INNER from donor statement to target statement even when there is no accompanying join hint, which seems harmless but makes TestRandomInjectHints possible. The validation step should prevent accidental injection of a LEFT or other outer join type that wasn't present in the target statement. Finally, testing has been updated to use statement fingerprints as the hint injection donors. Informs: #153633 Release note: None
1 parent 7fd87d1 commit bfc91ee

File tree

3 files changed

+267
-43
lines changed

3 files changed

+267
-43
lines changed

pkg/sql/sem/tree/inject_hints.go

Lines changed: 188 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import (
1313
)
1414

1515
// HintInjectionDonor holds the donor statement that provides the hints to be
16-
// injected into a target statement.
16+
// injected into a target statement. The donor statement could be a regular SQL
17+
// statement or a statement fingerprint.
1718
type HintInjectionDonor struct {
1819
// ast is the donor statement AST after parsing.
1920
ast Statement
@@ -23,9 +24,16 @@ type HintInjectionDonor struct {
2324
// statements match.
2425
validationSQL string
2526

26-
// walk is a pre-order traversal of the AST, holding Expr and TableExpr. As we
27-
// walk the target AST we expect to find matching nodes in the same order.
27+
// walk is a pre-order traversal of the AST, holding Expr, TableExpr, and
28+
// Statement nodes. As we walk the target AST we expect to find matching nodes
29+
// in the same order. Some nodes are ignored, such as ParenExpr and nodes
30+
// after collapse of a Tuple or Array or ValuesClause.
2831
walk []any
32+
33+
// collapsed is a stack indicating whether we've collapsed the rest of the
34+
// current ValuesClause, Tuple, or Array list in the donor AST. Collapsed is
35+
// only used during AST walks.
36+
collapsed []bool
2937
}
3038

3139
// validationFmtFlags returns the FmtFlags used to check that the donor
@@ -37,49 +45,148 @@ func validationFmtFlags(sv *settings.Values) FmtFlags {
3745
return stmtFingerprintFmtMask | FmtHideHints
3846
}
3947

40-
// NewHintInjectionDonor creates a HintInjectionDonor from a parsed AST.
48+
// NewHintInjectionDonor creates a HintInjectionDonor from a parsed AST. The
49+
// parsed donor statement could be a regular SQL statement or a statement
50+
// fingerprint.
4151
func NewHintInjectionDonor(ast Statement, sv *settings.Values) (*HintInjectionDonor, error) {
42-
donor := &HintInjectionDonor{
52+
hd := &HintInjectionDonor{
4353
ast: ast,
4454
validationSQL: AsStringWithFlags(ast, validationFmtFlags(sv)),
55+
collapsed: []bool{false},
56+
}
57+
WalkStmt(hd, ast)
58+
return hd, nil
59+
}
60+
61+
// The ExtendedVisitor methods of HintInjectionDonor are used to walk the donor
62+
// AST and build the ahead-of-time pre-order traversal.
63+
var _ ExtendedVisitor = &HintInjectionDonor{}
64+
65+
func (hd *HintInjectionDonor) VisitPre(expr Expr) (recurse bool, newExpr Expr) {
66+
if hd.collapsed[len(hd.collapsed)-1] {
67+
return false, expr
68+
}
69+
switch expr.(type) {
70+
case *ParenExpr:
71+
// We sometimes wrap scalar expressions in extra ParenExpr when
72+
// printing. Skip over ParenExpr to match in more cases.
73+
return true, expr
74+
case *Tuple, *Array:
75+
// If the donor was a statement fingerprint, we might have collapse a Tuple
76+
// or Array into __more__. Push on a boolean tracking whether we have
77+
// collapsed it.
78+
hd.collapsed = append(hd.collapsed, false)
79+
case *UnresolvedName:
80+
if isArityIndicatorString(expr.String()) {
81+
// If we find __more__ or another arity indicator, treat the rest of the
82+
// current Tuple or Array or ValuesClause as collapsed, and skip forward
83+
// until we VisitPost it.
84+
hd.collapsed[len(hd.collapsed)-1] = true
85+
hd.walk = append(hd.walk, expr)
86+
return false, expr
87+
}
88+
}
89+
hd.walk = append(hd.walk, expr)
90+
return true, expr
91+
}
92+
93+
func (hd *HintInjectionDonor) VisitPost(expr Expr) (newNode Expr) {
94+
switch expr.(type) {
95+
case *Tuple, *Array:
96+
// Pop off the boolean tracking whether we collapsed the Tuple or Array.
97+
hd.collapsed = hd.collapsed[:len(hd.collapsed)-1]
98+
}
99+
return expr
100+
}
101+
102+
func (hd *HintInjectionDonor) VisitTablePre(expr TableExpr) (recurse bool, newExpr TableExpr) {
103+
if hd.collapsed[len(hd.collapsed)-1] {
104+
return false, expr
105+
}
106+
hd.walk = append(hd.walk, expr)
107+
return true, expr
108+
}
109+
110+
func (hd *HintInjectionDonor) VisitTablePost(expr TableExpr) (newNode TableExpr) {
111+
return expr
112+
}
113+
114+
func (hd *HintInjectionDonor) VisitStatementPre(expr Statement) (recurse bool, newNode Statement) {
115+
if hd.collapsed[len(hd.collapsed)-1] {
116+
return false, expr
117+
}
118+
switch expr.(type) {
119+
case *ValuesClause:
120+
// If the donor was a statement fingerprint, we might have collapse a
121+
// ValuesClause into __more__. Push on a boolean tracking whether we have
122+
// collapsed it.
123+
hd.collapsed = append(hd.collapsed, false)
45124
}
46-
if _, err := ExtendedSimpleStmtVisit(
47-
ast,
48-
func(expr Expr) (bool, Expr, error) {
49-
donor.walk = append(donor.walk, expr)
50-
return true, expr, nil
51-
},
52-
func(expr TableExpr) (bool, TableExpr, error) {
53-
donor.walk = append(donor.walk, expr)
54-
return true, expr, nil
55-
},
56-
); err != nil {
57-
return nil, err
125+
hd.walk = append(hd.walk, expr)
126+
return true, expr
127+
}
128+
129+
func (hd *HintInjectionDonor) VisitStatementPost(expr Statement) (newNode Statement) {
130+
switch expr.(type) {
131+
case *ValuesClause:
132+
// Pop off the boolean tracking whether we collapsed the ValuesClause.
133+
hd.collapsed = hd.collapsed[:len(hd.collapsed)-1]
58134
}
59-
return donor, nil
135+
return expr
60136
}
61137

138+
// hintInjectionVisitor is an ExtendedVisitor used to walk the target AST at
139+
// injection time. It performs the rewrite of the target AST.
62140
type hintInjectionVisitor struct {
63-
donor *HintInjectionDonor
141+
hd *HintInjectionDonor
64142
walkIdx int
65143
err error
66144
}
67145

146+
var _ ExtendedVisitor = &hintInjectionVisitor{}
147+
68148
func (v *hintInjectionVisitor) VisitPre(expr Expr) (recurse bool, newExpr Expr) {
69-
if v.err != nil {
149+
if v.err != nil || v.hd.collapsed[len(v.hd.collapsed)-1] {
70150
return false, expr
71151
}
72152

73-
if v.walkIdx >= len(v.donor.walk) {
153+
switch expr.(type) {
154+
case *ParenExpr:
155+
// Skip ParenExpr to match the donor walk.
156+
return true, expr
157+
}
158+
159+
if v.walkIdx >= len(v.hd.walk) {
74160
v.err = errors.Newf(
75161
"hint injection donor statement missing AST node corresponding to AST node %v", expr,
76162
)
77163
return false, expr
78164
}
79165

80-
donorExpr := v.donor.walk[v.walkIdx]
166+
donorExpr := v.hd.walk[v.walkIdx]
81167
v.walkIdx += 1
82168

169+
switch donor := donorExpr.(type) {
170+
case *Tuple, *Array:
171+
// If the donor was a statement fingerprint, we might have collapse a Tuple
172+
// or Array into __more__. Push on a boolean tracking whether we have
173+
// collapsed it.
174+
v.hd.collapsed = append(v.hd.collapsed, false)
175+
case *UnresolvedName:
176+
// If the donor was a fingerprint, then we might not exactly match _ or
177+
// __more__ or __more10_100__ etc in the donor. Treat these as wildcards
178+
// matching any subtree and don't recurse any further into the target AST.
179+
if donor.String() == string(StmtFingerprintPlaceholder) {
180+
return false, expr
181+
}
182+
if isArityIndicatorString(donor.String()) {
183+
// And skip the rest of the current Tuple or Array or ValuesClause if it
184+
// has been collapsed to __more__ or one of the other arity indicators.
185+
v.hd.collapsed[len(v.hd.collapsed)-1] = true
186+
return false, expr
187+
}
188+
}
189+
83190
// Check that the corresponding donor Expr matches this node.
84191
if reflect.TypeOf(expr) != reflect.TypeOf(donorExpr) {
85192
v.err = errors.Newf(
@@ -90,19 +197,28 @@ func (v *hintInjectionVisitor) VisitPre(expr Expr) (recurse bool, newExpr Expr)
90197
return true, expr
91198
}
92199

200+
func (v *hintInjectionVisitor) VisitPost(expr Expr) Expr {
201+
switch expr.(type) {
202+
case *Tuple, *Array:
203+
// Pop off the boolean tracking whether we collapsed the Tuple or Array.
204+
v.hd.collapsed = v.hd.collapsed[:len(v.hd.collapsed)-1]
205+
}
206+
return expr
207+
}
208+
93209
func (v *hintInjectionVisitor) VisitTablePre(expr TableExpr) (recurse bool, newExpr TableExpr) {
94-
if v.err != nil {
210+
if v.err != nil || v.hd.collapsed[len(v.hd.collapsed)-1] {
95211
return false, expr
96212
}
97213

98-
if v.walkIdx >= len(v.donor.walk) {
214+
if v.walkIdx >= len(v.hd.walk) {
99215
v.err = errors.Newf(
100216
"hint injection donor statement missing AST node corresponding to AST node", expr,
101217
)
102218
return false, expr
103219
}
104220

105-
donorExpr := v.donor.walk[v.walkIdx]
221+
donorExpr := v.hd.walk[v.walkIdx]
106222
v.walkIdx += 1
107223

108224
// Copy hints from the corresponding donor TableExpr.
@@ -132,10 +248,11 @@ func (v *hintInjectionVisitor) VisitTablePre(expr TableExpr) (recurse bool, newE
132248
case *JoinTableExpr:
133249
switch orig := expr.(type) {
134250
case *JoinTableExpr:
135-
if donor.Hint != "" {
251+
if donor.JoinType != orig.JoinType || donor.Hint != orig.Hint {
136252
// Create a new node with any pre-existing inline hints replaced with
137253
// hints from the donor.
138254
newNode := *orig
255+
newNode.JoinType = donor.JoinType
139256
newNode.Hint = donor.Hint
140257
return true, &newNode
141258
}
@@ -153,10 +270,51 @@ func (v *hintInjectionVisitor) VisitTablePre(expr TableExpr) (recurse bool, newE
153270
return true, expr
154271
}
155272

156-
func (v *hintInjectionVisitor) VisitPost(expr Expr) Expr { return expr }
157-
func (v *hintInjectionVisitor) VisitTablePost(expr TableExpr) TableExpr { return expr }
273+
func (v *hintInjectionVisitor) VisitTablePost(expr TableExpr) TableExpr {
274+
return expr
275+
}
276+
277+
func (v *hintInjectionVisitor) VisitStatementPre(expr Statement) (recurse bool, newExpr Statement) {
278+
if v.err != nil || v.hd.collapsed[len(v.hd.collapsed)-1] {
279+
return false, expr
280+
}
158281

159-
var _ ExtendedVisitor = &hintInjectionVisitor{}
282+
if v.walkIdx >= len(v.hd.walk) {
283+
v.err = errors.Newf(
284+
"hint injection donor statement missing AST node corresponding to AST node %v", expr,
285+
)
286+
return false, expr
287+
}
288+
289+
donorExpr := v.hd.walk[v.walkIdx]
290+
v.walkIdx += 1
291+
292+
switch donorExpr.(type) {
293+
case *ValuesClause:
294+
// If the donor was a statement fingerprint, we might have collapse a
295+
// ValuesClause into __more__. Push on a boolean tracking whether we have
296+
// collapsed it.
297+
v.hd.collapsed = append(v.hd.collapsed, false)
298+
}
299+
300+
// Check that the corresponding donor Expr matches this node.
301+
if reflect.TypeOf(expr) != reflect.TypeOf(donorExpr) {
302+
v.err = errors.Newf(
303+
"hint injection donor statement AST node %v did not match AST node %v", donorExpr, expr,
304+
)
305+
return false, expr
306+
}
307+
return true, expr
308+
}
309+
310+
func (v *hintInjectionVisitor) VisitStatementPost(expr Statement) Statement {
311+
switch expr.(type) {
312+
case *ValuesClause:
313+
// Pop off the boolean tracking whether we collapsed the ValuesClause.
314+
v.hd.collapsed = v.hd.collapsed[:len(v.hd.collapsed)-1]
315+
}
316+
return expr
317+
}
160318

161319
// Validate checks that the target statement exactly matches the donor (except
162320
// for hints).
@@ -185,7 +343,7 @@ func (hd *HintInjectionDonor) Validate(stmt Statement, sv *settings.Values) erro
185343
// semantic checks happen elsewhere.
186344
func (hd *HintInjectionDonor) InjectHints(stmt Statement) (Statement, bool, error) {
187345
// Walk the given statement, copying hints from the hints donor.
188-
v := hintInjectionVisitor{donor: hd}
346+
v := hintInjectionVisitor{hd: hd}
189347
newStmt, changed := WalkStmt(&v, stmt)
190348
if v.err != nil {
191349
return stmt, false, v.err

0 commit comments

Comments
 (0)