@@ -16,8 +16,8 @@ package rowexec
1616
1717import (
1818 "fmt"
19+ "io"
1920 "strings"
20- "sync"
2121
2222 "github.com/dolthub/go-mysql-server/sql"
2323 "github.com/dolthub/go-mysql-server/sql/expression"
@@ -194,27 +194,137 @@ func (b *BaseBuilder) buildCall(ctx *sql.Context, n *plan.Call, row sql.Row) (sq
194194 }, nil
195195}
196196
197+ // buildLoop builds and returns an iterator that can be used to iterate over the result set returned from the
198+ // specified loop, |n|, for the specified row, |row|. Note that because of how we execute stored procedures and cache
199+ // the results in order to only send back the LAST result set (instead of supporting multiple results sets from
200+ // stored procedures, like MySQL does), building the iterator here also implicitly means that we're executing the
201+ // loop logic and caching the result set in memory. This will obviously be an issue for very large result sets.
202+ // Unfortunately, we can't know at analysis time what the last result set returned will be, since conditional logic
203+ // in stored procedures can't be known until execution time, hence why we end up caching result sets when we
204+ // see them and just playing back the last one. Adding support for MySQL's multiple result set behavior and better
205+ // matching MySQL on which statements are allowed to return result sets from a stored procedure seems like it could
206+ // potentially allow us to get rid of that caching.
197207func (b * BaseBuilder ) buildLoop (ctx * sql.Context , n * plan.Loop , row sql.Row ) (sql.RowIter , error ) {
198- var blockIter sql.RowIter
199- // Currently, acquiring the RowIter will actually run through the loop once, so we abuse this by grabbing the iter
200- // only if we're supposed to run through the iter once before evaluating the condition
208+ // Acquiring the RowIter will actually execute the loop body once (because of how we cache/scan for the right
209+ // SELECT result set to return), so we grab the iter ONLY if we're supposed to run through the loop body once
210+ // before evaluating the condition
211+ var loopBodyIter sql.RowIter
201212 if n .OnceBeforeEval {
202213 var err error
203- blockIter , err = b .loopAcquireRowIter (ctx , row , n .Label , n .Block , true )
214+ loopBodyIter , err = b .loopAcquireRowIter (ctx , row , n .Label , n .Block , true )
204215 if err != nil {
205216 return nil , err
206217 }
207218 }
208- iter := & loopIter {
209- block : n .Block ,
210- label : strings .ToLower (n .Label ),
211- condition : n .Condition ,
212- once : sync.Once {},
213- blockIter : blockIter ,
214- row : row ,
215- loopIteration : 0 ,
219+
220+ var returnRows []sql.Row
221+ var returnNode sql.Node
222+ var returnSch sql.Schema
223+ selectSeen := false
224+
225+ // It's technically valid to make an infinite loop, but we don't want to actually allow that
226+ const maxIterationCount = 10_000_000_000
227+
228+ for loopIteration := 0 ; loopIteration <= maxIterationCount ; loopIteration ++ {
229+ if loopIteration >= maxIterationCount {
230+ return nil , fmt .Errorf ("infinite LOOP detected" )
231+ }
232+
233+ // If the condition is false, then we stop evaluation
234+ condition , err := n .Condition .Eval (ctx , nil )
235+ if err != nil {
236+ return nil , err
237+ }
238+ conditionBool , err := types .ConvertToBool (condition )
239+ if err != nil {
240+ return nil , err
241+ }
242+ if ! conditionBool {
243+ // loopBodyIter should only be set if this is the first time through the loop and the loop has a
244+ // OnceBeforeEval condition. This ensures we return a result set, without us having to drain the iterator,
245+ // recache rows, and return a new iterator.
246+ if loopBodyIter != nil {
247+ return loopBodyIter , nil
248+ } else {
249+ break
250+ }
251+ }
252+
253+ if loopBodyIter == nil {
254+ var err error
255+ loopBodyIter , err = b .loopAcquireRowIter (ctx , nil , strings .ToLower (n .Label ), n .Block , false )
256+ if err == io .EOF {
257+ break
258+ } else if err != nil {
259+ return nil , err
260+ }
261+ }
262+
263+ includeResultSet := false
264+
265+ var subIterNode sql.Node = n .Block
266+ subIterSch := n .Block .Schema ()
267+ if blockRowIter , ok := loopBodyIter .(plan.BlockRowIter ); ok {
268+ subIterNode = blockRowIter .RepresentingNode ()
269+ subIterSch = blockRowIter .Schema ()
270+
271+ if plan .NodeRepresentsSelect (subIterNode ) {
272+ selectSeen = true
273+ includeResultSet = true
274+ returnNode = subIterNode
275+ returnSch = subIterSch
276+ } else if ! selectSeen {
277+ includeResultSet = true
278+ returnNode = subIterNode
279+ returnSch = subIterSch
280+ }
281+ }
282+
283+ // Wrap the caching code in an inline function so that we can use defer to safely dispose of the cache
284+ err = func () error {
285+ rowCache , disposeFunc := ctx .Memory .NewRowsCache ()
286+ defer disposeFunc ()
287+
288+ nextRow , err := loopBodyIter .Next (ctx )
289+ for ; err == nil ; nextRow , err = loopBodyIter .Next (ctx ) {
290+ rowCache .Add (nextRow )
291+ }
292+ if err != io .EOF {
293+ return err
294+ }
295+
296+ err = loopBodyIter .Close (ctx )
297+ if err != nil {
298+ return err
299+ }
300+ loopBodyIter = nil
301+
302+ if includeResultSet {
303+ returnRows = rowCache .Get ()
304+ }
305+ return nil
306+ }()
307+
308+ if err != nil {
309+ if err == io .EOF {
310+ // no-op for an EOF, just execute the next loop iteration
311+ } else if controlFlow , ok := err .(loopError ); ok && strings .ToLower (controlFlow .Label ) == n .Label {
312+ if controlFlow .IsExit {
313+ break
314+ }
315+ } else {
316+ // If the error wasn't a control flow error signaling to start the next loop iteration or to
317+ // exit the loop, then it must be a real error, so just return it.
318+ return nil , err
319+ }
320+ }
216321 }
217- return iter , nil
322+
323+ return & blockIter {
324+ internalIter : sql .RowsToRowIter (returnRows ... ),
325+ repNode : returnNode ,
326+ sch : returnSch ,
327+ }, nil
218328}
219329
220330func (b * BaseBuilder ) buildElseCaseError (ctx * sql.Context , n plan.ElseCaseError , row sql.Row ) (sql.RowIter , error ) {
0 commit comments