1212 * because they see one node at a time and never the statement it belongs to:
1313 *
1414 * 1. a {@link WalkContext} on every callback (`stmtTag`, `isWrite`, `isRead`,
15- * `insideFunction`, `functionName`)
15+ * `insideFunction`, `functionName`), refined by the nearest enclosing
16+ * statement — so an `INSERT` nested in a CTE or in a PL/pgSQL body reports
17+ * `isWrite`, not the context of whatever statement contains it
1618 * 2. visitor composition — N visitors in a single pass, each free to mix SQL
1719 * and `PLpgSQL_*` tags in the same object
1820 * 3. `ctx.abort()`, which ends the whole walk (returning `false` from a
@@ -52,7 +54,7 @@ export const READ_STATEMENTS: ReadonlySet<string> = new Set([
5254 * inside a function body.
5355 */
5456export interface WalkContext {
55- /** Tag of the enclosing top-level statement, or `null` when walking a bare node. */
57+ /** Tag of the nearest enclosing statement, or `null` when walking a bare node. */
5658 readonly stmtTag : string | null ;
5759 /** Index of the enclosing statement within the script, or `-1` if unknown. */
5860 readonly stmtIndex : number ;
@@ -151,6 +153,46 @@ export function walk(
151153 const makeContext = ( fields : Omit < WalkContext , 'abort' > ) : WalkContext =>
152154 Object . assign ( { } , fields , ctxBase ) ;
153155
156+ /**
157+ * Statement context per node, derived from the path chain and memoized.
158+ *
159+ * A node's context is its parent's, except when the node is itself a
160+ * statement: then it becomes the enclosing statement for its whole subtree.
161+ * Nesting is what makes this necessary — the write target of an `INSERT`
162+ * inside a CTE, a rule action, or a PL/pgSQL body is a write, even though
163+ * the statement the script starts with may only read.
164+ */
165+ const contexts = new WeakMap < object , WalkContext > ( ) ;
166+
167+ const contextFor = (
168+ path : NodePath | PlpgsqlNodePath ,
169+ base : WalkContext ,
170+ ) : WalkContext => {
171+ const cached = contexts . get ( path ) ;
172+ if ( cached ) return cached ;
173+
174+ const parent = ( path as { parent ?: NodePath | PlpgsqlNodePath | null } ) . parent ;
175+ const inherited = parent ? contextFor ( parent , base ) : base ;
176+ const tag = path . tag ;
177+ const isWrite = WRITE_STATEMENTS . has ( tag ) ;
178+ const isRead = READ_STATEMENTS . has ( tag ) ;
179+
180+ const ctx =
181+ isWrite || isRead
182+ ? makeContext ( {
183+ stmtTag : tag ,
184+ stmtIndex : inherited . stmtIndex ,
185+ isWrite,
186+ isRead,
187+ insideFunction : inherited . insideFunction ,
188+ functionName : inherited . functionName ,
189+ } )
190+ : inherited ;
191+
192+ contexts . set ( path , ctx ) ;
193+ return ctx ;
194+ } ;
195+
154196 /** Fire every visitor for one node. Returns false if any asks to skip children. */
155197 const fire = ( path : NodePath | PlpgsqlNodePath , ctx : WalkContext ) : boolean => {
156198 let descend = true ;
@@ -177,11 +219,11 @@ export function walk(
177219 return descend ;
178220 } ;
179221
180- const sqlCallback = ( ctx : WalkContext ) : SqlWalker => ( path : NodePath ) =>
181- fire ( path , ctx ) ? undefined : false ;
222+ const sqlCallback = ( base : WalkContext ) : SqlWalker => ( path : NodePath ) =>
223+ fire ( path , contextFor ( path , base ) ) ? undefined : false ;
182224
183- const plpgsqlCallback = ( ctx : WalkContext ) : PlpgsqlWalker => ( path : PlpgsqlNodePath ) =>
184- fire ( path , ctx ) ? undefined : false ;
225+ const plpgsqlCallback = ( base : WalkContext ) : PlpgsqlWalker => ( path : PlpgsqlNodePath ) =>
226+ fire ( path , contextFor ( path , base ) ) ? undefined : false ;
185227
186228 const bareContext = ( ) : WalkContext =>
187229 makeContext ( {
@@ -201,24 +243,25 @@ export function walk(
201243 * every node after a `RawStmt` belongs to that statement.
202244 */
203245 const walkParseResult = ( parseResult : any ) : void => {
204- let ctx = bareContext ( ) ;
246+ const base = bareContext ( ) ;
205247
206248 const callback : SqlWalker = ( path : NodePath ) => {
207249 if ( path . tag !== 'RawStmt' ) {
208- return fire ( path , ctx ) ? undefined : false ;
250+ return fire ( path , contextFor ( path , base ) ) ? undefined : false ;
209251 }
210252
211253 const stmt = path . node ?. stmt ;
212254 const stmtTag = stmt ? Object . keys ( stmt ) [ 0 ] : null ;
213255 const last = path . keyPath [ path . keyPath . length - 1 ] ;
214- ctx = makeContext ( {
256+ const ctx = makeContext ( {
215257 stmtTag,
216258 stmtIndex : typeof last === 'number' ? last : - 1 ,
217259 isWrite : stmtTag != null && WRITE_STATEMENTS . has ( stmtTag ) ,
218260 isRead : stmtTag != null && READ_STATEMENTS . has ( stmtTag ) ,
219261 insideFunction : false ,
220262 functionName : null ,
221263 } ) ;
264+ contexts . set ( path , ctx ) ;
222265
223266 let descend = fire ( path , ctx ) ;
224267 if ( stmt && stmtTag ) {
0 commit comments