MySQL Lists are EOL. Please join:

List:Internals« Previous MessageNext Message »
From:konstantin Date:April 29 2005 5:56pm
Subject:bk commit into 5.0 tree (konstantin:1.1920) BUG#9520
View as plain text  
Below is the list of changes that have just been committed into a local
5.0 repository of kostja. When kostja does a push these changes will
be propagated to the main repository and, within 24 hours after the
push, to the public repository.
For information on how to access the public repository
see http://dev.mysql.com/doc/mysql/en/installing-source-tree.html

ChangeSet
  1.1920 05/04/29 21:56:13 konstantin@stripped +4 -0
  A fix and a test case for Bug#9520 "SELECT DISTINCT crashes server
  with cursor". The patch refactors do_select/sub_select
  functions, which implement the nested loop algorithm, and reuses them to
  fetch rows for cursors as well.

  tests/mysql_client_test.c
    1.113 05/04/29 21:56:03 konstantin@stripped +54 -0
    A test case for Bug#9520 "SELECT DISTINCT crashes server with cursor"

  sql/sql_select.h
    1.83 05/04/29 21:56:03 konstantin@stripped +16 -2
    Replace all return values of sub_select family with enum.
    Add JOIN::resume_nested_loop flag to indicate we are restarting the nested loop
    for execution of next chunk of cursor's rows.

  sql/sql_select.cc
    1.321 05/04/29 21:56:03 konstantin@stripped +382 -354
    A fix for Bug#9520 "SELECT DISTINCT crashes server with cursor":
    * rename sub_select returns codes to be able to track down what's going
      on in which case.
    * move record processing and outer join record processing to a separate
      function, out of sub_select read-record loop.
    * use generalized sub_select() nested loop function for
    cursors instead of own loop implementation used in Cursor::fetch() before

  sql/sql_prepare.cc
    1.113 05/04/29 21:56:03 konstantin@stripped +1 -1
     Cursor::fetch() now returns void

# This is a BitKeeper patch.  What follows are the unified diffs for the
# set of deltas contained in the patch.  The rest of the patch, the part
# that BitKeeper cares about, is below these diffs.
# User:	konstantin
# Host:	dragonfly.local
# Root:	/media/sda1/mysql/mysql-5.0-9520-port

--- 1.320/sql/sql_select.cc	2005-04-21 12:32:55 +04:00
+++ 1.321/sql/sql_select.cc	2005-04-29 21:56:03 +04:00
@@ -114,17 +114,31 @@
 static Next_select_func setup_end_select_func(JOIN *join);
 static int do_select(JOIN *join,List<Item> *fields,TABLE *tmp_table,
 		     Procedure *proc);
-static int sub_select_cache(JOIN *join,JOIN_TAB *join_tab,bool end_of_records);
-static int sub_select(JOIN *join,JOIN_TAB *join_tab,bool end_of_records);
-static int flush_cached_records(JOIN *join,JOIN_TAB *join_tab,bool skip_last);
-static int end_send(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
-static int end_send_group(JOIN *join, JOIN_TAB *join_tab,bool end_of_records);
-static int end_write(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
-static int end_update(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
-static int end_unique_update(JOIN *join,JOIN_TAB *join_tab,
-			     bool end_of_records);
-static int end_write_group(JOIN *join, JOIN_TAB *join_tab,
-			   bool end_of_records);
+
+static enum_nested_loop_state
+sub_select_cache(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
+static enum_nested_loop_state
+evaluate_join_record(JOIN *join, JOIN_TAB *join_tab,
+                     int error, my_bool *report_error);
+static enum_nested_loop_state
+evaluate_null_complemented_join_record(JOIN *join, JOIN_TAB *join_tab);
+static enum_nested_loop_state
+sub_select(JOIN *join,JOIN_TAB *join_tab, bool end_of_records);
+static enum_nested_loop_state
+flush_cached_records(JOIN *join, JOIN_TAB *join_tab, bool skip_last);
+static enum_nested_loop_state
+end_send(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
+static enum_nested_loop_state
+end_send_group(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
+static enum_nested_loop_state
+end_write(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
+static enum_nested_loop_state
+end_update(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
+static enum_nested_loop_state
+end_unique_update(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
+static enum_nested_loop_state
+end_write_group(JOIN *join, JOIN_TAB *join_tab, bool end_of_records);
+
 static int test_if_group_changed(List<Item_buff> &list);
 static int join_read_const_table(JOIN_TAB *tab, POSITION *pos);
 static int join_read_system(JOIN_TAB *tab);
@@ -1800,13 +1814,7 @@
     happen for the first table in join_tab list
   */
   DBUG_ASSERT(join_tab->table->null_row == 0);
-
-  /*
-    There is always at least one record in the table, as otherwise we
-    wouldn't have opened the cursor. Therefore a failure is the only
-    reason read_first_record can return not 0.
-  */
-  DBUG_RETURN(join_tab->read_first_record(join_tab));
+  DBUG_RETURN(0);
 }
 
 
@@ -1816,97 +1824,36 @@
   PRECONDITION:
     Cursor is open
   RETURN VALUES:
-    -4  there are more rows, send_eof sent to the client
-     0  no more rows, send_eof was sent to the client, cursor is closed
- other  fatal fetch error, cursor is closed (error is not reported)
+    none, this function will send error or OK to network if necessary.
 */
 
-int
+void
 Cursor::fetch(ulong num_rows)
 {
   THD *thd= join->thd;
   JOIN_TAB *join_tab= join->join_tab + join->const_tables;
-  COND *on_expr= *join_tab->on_expr_ref;
-  COND *select_cond= join_tab->select_cond;
-  READ_RECORD *info= &join_tab->read_record;
-  int error= 0;
+  enum_nested_loop_state error= NESTED_LOOP_OK;
 
   /* save references to memory, allocated during fetch */
   thd->set_n_backup_item_arena(this, &thd->stmt_backup);
 
   join->fetch_limit+= num_rows;
 
-  /*
-    Run while there are new rows in the first table;
-    For each row, satisfying ON and WHERE clauses (those parts of them which
-    can be evaluated early), call next_select.
-  */
-  do
-  {
-    int no_more_rows;
-
-    join->examined_rows++;
 
-    if (thd->killed)                            /* Aborted by user */
-    {
-      my_message(ER_SERVER_SHUTDOWN, ER(ER_SERVER_SHUTDOWN), MYF(0));
-      return -1;
-    }
-
-    if (on_expr == 0 || on_expr->val_int())
-    {
-      if (select_cond == 0 || select_cond->val_int())
-      {
-        /*
-          TODO: call table->unlock_row() to unlock row failed selection,
-          when this feature will be used.
-        */
-        error= join_tab->next_select(join, join_tab + 1, 0);
-        DBUG_ASSERT(error <= 0);
-        if (error)
-        {
-          /* real error or LIMIT/FETCH LIMIT worked */
-          if (error == -4)
-          {
-            /*
-              FETCH LIMIT, read ahead one row, and close cursor
-              if there is no more rows XXX: to be fixed to support
-              non-equi-joins!
-            */
-            if ((no_more_rows= info->read_record(info)))
-              error= no_more_rows > 0 ? -1: 0;
-          }
-          break;
-        }
-      }
-    }
-    /* read next row; break loop if there was an error */
-    if ((no_more_rows= info->read_record(info)))
-    {
-      if (no_more_rows > 0)
-        error= -1;
-      else
-      {
-        enum { END_OF_RECORDS= 1 };
-        error= join_tab->next_select(join, join_tab+1, (int) END_OF_RECORDS);
-      }
-      break;
-    }
-  }
-  while (thd->net.report_error == 0);
-
-  if (thd->net.report_error)
-    error= -1;
-
-  if (error == -3)                              /* LIMIT clause worked */
-    error= 0;
+  error= sub_select(join, join_tab, 0);
+  if (error == NESTED_LOOP_OK || error == NESTED_LOOP_NO_MORE_ROWS)
+    error= sub_select(join,join_tab,1);
+  if (error == NESTED_LOOP_QUERY_LIMIT)
+    error= NESTED_LOOP_OK;                    /* select_limit used */
+  if (error == NESTED_LOOP_CURSOR_LIMIT)
+    join->resume_nested_loop= TRUE;
 
 #ifdef USING_TRANSACTIONS
     ha_release_temporary_latches(thd);
 #endif
 
   thd->restore_backup_item_arena(this, &thd->stmt_backup);
-  if (error == -4)
+  if (error == NESTED_LOOP_CURSOR_LIMIT)
   {
     /* Fetch limit worked, possibly more rows are there */
     thd->server_status|= SERVER_STATUS_CURSOR_EXISTS;
@@ -1916,20 +1863,19 @@
   else
   {
     close();
-    if (error == 0)
+    if (error == NESTED_LOOP_OK)
     {
       thd->server_status|= SERVER_STATUS_LAST_ROW_SENT;
       ::send_eof(thd);
       thd->server_status&= ~SERVER_STATUS_LAST_ROW_SENT;
     }
-    else
+    else if (error != NESTED_LOOP_KILLED)
       my_message(ER_OUT_OF_RESOURCES, ER(ER_OUT_OF_RESOURCES), MYF(0));
     /* free cursor memory */
     free_items(free_list);
     free_list= 0;
     free_root(&main_mem_root, MYF(0));
   }
-  return error;
 }
 
 
@@ -8948,7 +8894,8 @@
 static int
 do_select(JOIN *join,List<Item> *fields,TABLE *table,Procedure *procedure)
 {
-  int error= 0;
+  int rc= 0;
+  enum_nested_loop_state error= NESTED_LOOP_OK;
   JOIN_TAB *join_tab;
   DBUG_ENTER("do_select");
 
@@ -8978,24 +8925,30 @@
     if (!join->conds || join->conds->val_int())
     {
       Next_select_func end_select= join->join_tab[join->tables-1].next_select;
-      if (!(error=(*end_select)(join,join_tab,0)) || error == -3)
-	error=(*end_select)(join,join_tab,1);
+      error= (*end_select)(join,join_tab,0);
+      if (error == NESTED_LOOP_OK || error == NESTED_LOOP_QUERY_LIMIT)
+	error= (*end_select)(join,join_tab,1);
     }
     else if (join->send_row_on_empty_set())
-      error= join->result->send_data(*join->fields);
+      rc= join->result->send_data(*join->fields);
   }
   else
   {
     error= sub_select(join,join_tab,0);
-    if (error >= 0)
+    if (error == NESTED_LOOP_OK || error == NESTED_LOOP_NO_MORE_ROWS)
       error= sub_select(join,join_tab,1);
-    if (error == -3)
-      error= 0;					/* select_limit used */
+    if (error == NESTED_LOOP_QUERY_LIMIT)
+      error= NESTED_LOOP_OK;                    /* select_limit used */
   }
+  if (error == NESTED_LOOP_NO_MORE_ROWS)
+    error= NESTED_LOOP_OK;
 
-  if (error >= 0)
+  if (error == NESTED_LOOP_OK)
   {
-    error=0;
+    /*
+      Sic: this branch works even if rc != 0, e.g. when
+      send_data above returns an error.
+    */
     if (!table)					// If sending data to client
     {
       /*
@@ -9004,10 +8957,12 @@
       */
       join->join_free(0);				// Unlock all cursors
       if (join->result->send_eof())
-	error= 1;				// Don't send error
+	rc= 1;                                  // Don't send error
     }
     DBUG_PRINT("info",("%ld records output",join->send_records));
   }
+  else
+    rc= -1;
   if (table)
   {
     int tmp, new_errno= 0;
@@ -9025,40 +8980,42 @@
       table->file->print_error(new_errno,MYF(0));
   }
 #ifndef DBUG_OFF
-  if (error)
+  if (rc)
   {
     DBUG_PRINT("error",("Error: do_select() failed"));
   }
 #endif
-  DBUG_RETURN(join->thd->net.report_error ? -1 : error);
+  DBUG_RETURN(join->thd->net.report_error ? -1 : rc);
 }
 
 
-static int
+static enum_nested_loop_state
 sub_select_cache(JOIN *join,JOIN_TAB *join_tab,bool end_of_records)
 {
-  int error;
+  enum_nested_loop_state rc;
 
   if (end_of_records)
   {
-    if ((error=flush_cached_records(join,join_tab,FALSE)) < 0)
-      return error; /* purecov: inspected */
-    return sub_select(join,join_tab,end_of_records);
+    rc= flush_cached_records(join,join_tab,FALSE);
+    if (rc == NESTED_LOOP_OK || rc == NESTED_LOOP_NO_MORE_ROWS)
+      rc= sub_select(join,join_tab,end_of_records);
+    return rc;
   }
   if (join->thd->killed)		// If aborted by user
   {
     join->thd->send_kill_message();
-    return -2;				 /* purecov: inspected */
+    return NESTED_LOOP_KILLED;                   /* purecov: inspected */
   }
   if (join_tab->use_quick != 2 || test_if_quick_select(join_tab) <= 0)
   {
     if (!store_record_in_cache(&join_tab->cache))
-      return 0;					// There is more room in cache
+      return NESTED_LOOP_OK;                     // There is more room in cache
     return flush_cached_records(join,join_tab,FALSE);
   }
-  if ((error=flush_cached_records(join,join_tab,TRUE)) < 0)
-    return error; /* purecov: inspected */
-  return sub_select(join,join_tab,end_of_records); /* Use ordinary select */
+  rc= flush_cached_records(join, join_tab, TRUE);
+  if (rc == NESTED_LOOP_OK || rc == NESTED_LOOP_NO_MORE_ROWS)
+    rc= sub_select(join, join_tab, end_of_records);
+  return rc;
 }
 
 /*
@@ -9170,11 +9127,10 @@
     table of the embedding nested join, if any.
 
   RETURN
-    0, if success
-    # of the error, otherwise
+    return one of enum_nested_loop_state, except NESTED_LOOP_NO_MORE_ROWS.
 */
 
-static int
+static enum_nested_loop_state
 sub_select(JOIN *join,JOIN_TAB *join_tab,bool end_of_records)
 {
   join_tab->table->null_row=0;
@@ -9182,206 +9138,258 @@
     return (*join_tab->next_select)(join,join_tab+1,end_of_records);
 
   int error;
-  JOIN_TAB *first_unmatched;
-  JOIN_TAB *tab;
-  /* Cache variables for faster loop */
-  COND *select_cond= join_tab->select_cond;
+  enum_nested_loop_state rc;
   my_bool *report_error= &(join->thd->net.report_error);
+  READ_RECORD *info= &join_tab->read_record;
 
-  join->return_tab= join_tab;
-
-  if (join_tab->last_inner)
+  if (join->resume_nested_loop)
   {
-    /* join_tab is the first inner table for an outer join operation. */
+    /* If not the last table, plunge down the nested loop */
+    if (join_tab < join->join_tab + join->tables - 1)
+      rc= (*join_tab->next_select)(join, join_tab + 1, 0);
+    else
+    {
+      join->resume_nested_loop= FALSE;
+      rc= NESTED_LOOP_OK;
+    }
+  }
+  else
+  {
+    join->return_tab= join_tab;
+
+    if (join_tab->last_inner)
+    {
+      /* join_tab is the first inner table for an outer join operation. */
 
-    /* Set initial state of guard variables for this table.*/
-    join_tab->found=0;
-    join_tab->not_null_compl= 1;
+      /* Set initial state of guard variables for this table.*/
+      join_tab->found=0;
+      join_tab->not_null_compl= 1;
 
-    /* Set first_unmatched for the last inner table of this group */
-    join_tab->last_inner->first_unmatched= join_tab; 
+      /* Set first_unmatched for the last inner table of this group */
+      join_tab->last_inner->first_unmatched= join_tab;
+    }
+    join->thd->row_count= 0;
+
+    error= (*join_tab->read_first_record)(join_tab);
+    rc= evaluate_join_record(join, join_tab, error, report_error);
   }
 
-  if (!(error=(*join_tab->read_first_record)(join_tab)))
+  while (rc == NESTED_LOOP_OK)
   {
-    bool not_exists_optimize= join_tab->table->reginfo.not_exists_optimize;
-    bool not_used_in_distinct=join_tab->not_used_in_distinct;
-    ha_rows found_records=join->found_records;
-    READ_RECORD *info= &join_tab->read_record;
+    error= info->read_record(info);
+    rc= evaluate_join_record(join, join_tab, error, report_error);
+  }
 
-    join->thd->row_count= 0;
-    do
+  if (rc == NESTED_LOOP_NO_MORE_ROWS &&
+      join_tab->last_inner && !join_tab->found)
+    rc= evaluate_null_complemented_join_record(join, join_tab);
+
+  if (rc == NESTED_LOOP_NO_MORE_ROWS)
+    rc= NESTED_LOOP_OK;
+  return rc;
+}
+
+
+/*
+  Process one record of the nested loop join.
+
+  DESCRIPTION
+    This function will evaluate parts of WHERE/ON clauses that are
+    applicable to the partial record on hand and in case of success
+    submit this record to the next level of the nested loop.
+*/
+
+static enum_nested_loop_state
+evaluate_join_record(JOIN *join, JOIN_TAB *join_tab,
+                     int error, my_bool *report_error)
+{
+  bool not_exists_optimize= join_tab->table->reginfo.not_exists_optimize;
+  bool not_used_in_distinct=join_tab->not_used_in_distinct;
+  ha_rows found_records=join->found_records;
+  COND *select_cond= join_tab->select_cond;
+
+  if (error > 0 || (*report_error))				// Fatal error
+    return NESTED_LOOP_ERROR;
+  if (error < 0)
+    return NESTED_LOOP_NO_MORE_ROWS;
+  if (join->thd->killed)			// Aborted by user
+  {
+    join->thd->send_kill_message();
+    return NESTED_LOOP_KILLED;               /* purecov: inspected */
+  }
+  DBUG_PRINT("info", ("select cond 0x%lx", (ulong)select_cond));
+  if (!select_cond || select_cond->val_int())
+  {
+    /*
+      There is no select condition or the attached pushed down
+      condition is true => a match is found.
+    */
+    bool found= 1;
+    while (join_tab->first_unmatched && found)
     {
-      if (join->thd->killed)			// Aborted by user
-      {
-	join->thd->send_kill_message();
-	return -2;				/* purecov: inspected */
-      }
-      DBUG_PRINT("info", ("select cond 0x%lx", (ulong)select_cond));
-      if (!select_cond || select_cond->val_int())
+      /*
+        The while condition is always false if join_tab is not
+        the last inner join table of an outer join operation.
+      */
+      JOIN_TAB *first_unmatched= join_tab->first_unmatched;
+      /*
+        Mark that a match for current outer table is found.
+        This activates push down conditional predicates attached
+        to the all inner tables of the outer join.
+      */
+      first_unmatched->found= 1;
+      for (JOIN_TAB *tab= first_unmatched; tab <= join_tab; tab++)
       {
-        /* 
-          There is no select condition or the attached pushed down
-          condition is true => a match is found.
-	*/
-        bool found= 1;
-	while (join_tab->first_unmatched && found)
+        /* Check all predicates that has just been activated. */
+        /*
+          Actually all predicates non-guarded by first_unmatched->found
+          will be re-evaluated again. It could be fixed, but, probably,
+          it's not worth doing now.
+        */
+        if (tab->select_cond && !tab->select_cond->val_int())
         {
-          /*
-             The while condition is always false if join_tab is not
-             the last inner join table of an outer join operation. 
-	  */ 
-          first_unmatched= join_tab->first_unmatched;
-          /*
-             Mark that a match for current outer table is found.
-             This activates push down conditional predicates attached
-             to the all inner tables of the outer join.
-	  */  
-          first_unmatched->found= 1;
-          for (tab= first_unmatched; tab <= join_tab; tab++)
-          { 
-            /* Check all predicates that has just been activated. */
+          /* The condition attached to table tab is false */
+          if (tab == join_tab)
+            found= 0;
+          else
+          {
             /*
-              Actually all predicates non-guarded by first_unmatched->found
-              will be re-evaluated again. It could be fixed, but, probably,
-              it's not worth doing now.
-	    */ 
-            if (tab->select_cond && !tab->select_cond->val_int())
-            {
-              /* The condition attached to table tab is false */
-              if (tab == join_tab)
-                found= 0;
-              else
-              {
-                /*
-                  Set a return point if rejected predicate is attached 
-                  not to the last table of the current nest level.
-		*/
-                join->return_tab= tab;
-                return 0;
-              }
-            }
+              Set a return point if rejected predicate is attached
+              not to the last table of the current nest level.
+            */
+            join->return_tab= tab;
+            return NESTED_LOOP_OK;
           }
-          /* 
-             Check whether join_tab is not the last inner table
-             for another embedding outer join.
-          */
-          if ((first_unmatched= first_unmatched->first_upper) &&
-              first_unmatched->last_inner != join_tab)
-            first_unmatched= 0;
-          join_tab->first_unmatched= first_unmatched;
         }
-        
-        /*
-           It was not just a return to lower loop level when one
-           of the newly activated predicates is evaluated as false 
-           (See above join->return_tab= tab).
-	*/             
-        join->examined_rows++;
-        join->thd->row_count++;
-              
-        if (found)
-        {
-          if (not_exists_optimize)
-            break;
-          /* A match from join_tab is found for the current partial join. */
-	  if ((error=(*join_tab->next_select)(join, join_tab+1, 0)) < 0)
-	    return error;
-          if (join->return_tab < join_tab)
-              return 0;
-	  /*
-	    Test if this was a SELECT DISTINCT query on a table that
-	    was not in the field list;  In this case we can abort if
-	    we found a row, as no new rows can be added to the result.
-	  */
-	  if (not_used_in_distinct && found_records != join->found_records)
-	    return 0;
-	}
-	else
-	  info->file->unlock_row();    
-      }
-      else
-      {
-        /* 
-           The condition pushed down to the table join_tab rejects all rows 
-           with the beginning coinciding with the current partial join.
-	*/ 
-        join->examined_rows++;
-        join->thd->row_count++;
       }
-
-    } while (!(error=info->read_record(info)) && !(*report_error));
-  }
-  if (error > 0 || (*report_error))				// Fatal error
-    return -1;
-
-  if (join_tab->last_inner && !join_tab->found)
-  {        
-    /* 
-      The table join_tab is the first inner table of a outer join operation
-      and no matches has been found for the current outer row.
-    */
-    JOIN_TAB *last_inner_tab= join_tab->last_inner;
-    for ( ; join_tab <= last_inner_tab ; join_tab++)
-    { 
-      /* Change the the values of guard predicate variables. */
-      join_tab->found= 1;
-      join_tab->not_null_compl= 0;
-      /* The outer row is complemented by nulls for each inner tables */
-      restore_record(join_tab->table,s->default_values);  // Make empty record
-      mark_as_null_row(join_tab->table);       // For group by without error
-      select_cond= join_tab->select_cond;
-      /* Check all attached conditions for inner table rows. */
-      if (select_cond && !select_cond->val_int())
-        return 0;
-    }    
-    join_tab--;
-    /* 
-       The row complemented by nulls might be the first row
-       of embedding outer joins. 
-       If so, perform the same actions as in the code 
-       for the first regular outer join row above.
-    */
-    for ( ; ; )
-    {
-      first_unmatched= join_tab->first_unmatched;
+      /*
+        Check whether join_tab is not the last inner table
+        for another embedding outer join.
+      */
       if ((first_unmatched= first_unmatched->first_upper) &&
           first_unmatched->last_inner != join_tab)
         first_unmatched= 0;
       join_tab->first_unmatched= first_unmatched;
-      if (!first_unmatched)
-        break;
-      first_unmatched->found= 1;
-      for (JOIN_TAB *tab= first_unmatched; tab <= join_tab; tab++)
-      {  
-        if (tab->select_cond && !tab->select_cond->val_int())
-        {
-	  join->return_tab= tab;
-          return 0;
-        }
-      }
     }
+
     /*
-      The row complemented by nulls satisfies all conditions
-      attached to inner tables.
-      Send the row complemented by nulls to be joined with the 
-      remaining tables.
-    */     
-    if ((error=(*join_tab->next_select)(join, join_tab+1 ,0)) < 0)
-      return error;
+      It was not just a return to lower loop level when one
+      of the newly activated predicates is evaluated as false
+      (See above join->return_tab= tab).
+    */
+    join->examined_rows++;
+    join->thd->row_count++;
+
+    if (found)
+    {
+      enum enum_nested_loop_state rc;
+      if (not_exists_optimize)
+        return NESTED_LOOP_NO_MORE_ROWS;
+      /* A match from join_tab is found for the current partial join. */
+      rc= (*join_tab->next_select)(join, join_tab+1, 0);
+      if (rc != NESTED_LOOP_OK && rc != NESTED_LOOP_NO_MORE_ROWS)
+        return rc;
+      if (join->return_tab < join_tab)
+        return NESTED_LOOP_OK;
+      /*
+        Test if this was a SELECT DISTINCT query on a table that
+        was not in the field list;  In this case we can abort if
+        we found a row, as no new rows can be added to the result.
+      */
+      if (not_used_in_distinct && found_records != join->found_records)
+        return NESTED_LOOP_OK;
+    }
+    else
+      join_tab->read_record.file->unlock_row();
   }
-  return 0;
+  else
+  {
+    /*
+      The condition pushed down to the table join_tab rejects all rows
+      with the beginning coinciding with the current partial join.
+    */
+    join->examined_rows++;
+    join->thd->row_count++;
+  }
+  return NESTED_LOOP_OK;
 }
 
 
-static int
+/*
+  DESCRIPTION
+    Construct a NULL complimented partial join record and feed it to the next
+    level of the nested loop. This function is used in case we have
+    an OUTER join and no matching record was found.
+*/
+
+static enum_nested_loop_state
+evaluate_null_complemented_join_record(JOIN *join, JOIN_TAB *join_tab)
+{
+  /*
+    The table join_tab is the first inner table of a outer join operation
+    and no matches has been found for the current outer row.
+  */
+  JOIN_TAB *last_inner_tab= join_tab->last_inner;
+  /* Cache variables for faster loop */
+  COND *select_cond;
+  for ( ; join_tab <= last_inner_tab ; join_tab++)
+  {
+    /* Change the the values of guard predicate variables. */
+    join_tab->found= 1;
+    join_tab->not_null_compl= 0;
+    /* The outer row is complemented by nulls for each inner tables */
+    restore_record(join_tab->table,s->default_values);  // Make empty record
+    mark_as_null_row(join_tab->table);       // For group by without error
+    select_cond= join_tab->select_cond;
+    /* Check all attached conditions for inner table rows. */
+    if (select_cond && !select_cond->val_int())
+      return NESTED_LOOP_OK;
+  }
+  join_tab--;
+  /*
+    The row complemented by nulls might be the first row
+    of embedding outer joins.
+    If so, perform the same actions as in the code
+    for the first regular outer join row above.
+  */
+  for ( ; ; )
+  {
+    JOIN_TAB *first_unmatched= join_tab->first_unmatched;
+    if ((first_unmatched= first_unmatched->first_upper) &&
+        first_unmatched->last_inner != join_tab)
+      first_unmatched= 0;
+    join_tab->first_unmatched= first_unmatched;
+    if (!first_unmatched)
+      break;
+    first_unmatched->found= 1;
+    for (JOIN_TAB *tab= first_unmatched; tab <= join_tab; tab++)
+    {
+      if (tab->select_cond && !tab->select_cond->val_int())
+      {
+        join->return_tab= tab;
+        return NESTED_LOOP_OK;
+      }
+    }
+  }
+  /*
+    The row complemented by nulls satisfies all conditions
+    attached to inner tables.
+    Send the row complemented by nulls to be joined with the
+    remaining tables.
+  */
+  return (*join_tab->next_select)(join, join_tab+1, 0);
+}
+
+
+static enum_nested_loop_state
 flush_cached_records(JOIN *join,JOIN_TAB *join_tab,bool skip_last)
 {
+  enum_nested_loop_state rc= NESTED_LOOP_OK;
   int error;
   READ_RECORD *info;
 
   if (!join_tab->cache.records)
-    return 0;				/* Nothing to do */
+    return NESTED_LOOP_OK;                      /* Nothing to do */
   if (skip_last)
     (void) store_record_in_cache(&join_tab->cache); // Must save this for later
   if (join_tab->use_quick == 2)
@@ -9396,7 +9404,7 @@
   if ((error=join_init_read_record(join_tab)))
   {
     reset_cache_write(&join_tab->cache);
-    return -error;			/* No records or error */
+    return error < 0 ? NESTED_LOOP_NO_MORE_ROWS: NESTED_LOOP_ERROR;
   }
 
   for (JOIN_TAB *tmp=join->join_tab; tmp != join_tab ; tmp++)
@@ -9411,11 +9419,11 @@
     if (join->thd->killed)
     {
       join->thd->send_kill_message();
-      return -2;				// Aborted by user /* purecov: inspected */
+      return NESTED_LOOP_KILLED; // Aborted by user /* purecov: inspected */
     }
     SQL_SELECT *select=join_tab->select;
-    if (!error && (!join_tab->cache.select ||
-		   !join_tab->cache.select->skip_record()))
+    if (rc == NESTED_LOOP_OK &&
+        (!join_tab->cache.select || !join_tab->cache.select->skip_record()))
     {
       uint i;
       reset_cache_read(&join_tab->cache);
@@ -9423,11 +9431,14 @@
       {
 	read_cached_record(join_tab);
 	if (!select || !select->skip_record())
-	  if ((error=(join_tab->next_select)(join,join_tab+1,0)) < 0)
+        {
+          rc= (join_tab->next_select)(join,join_tab+1,0);
+	  if (rc != NESTED_LOOP_OK && rc != NESTED_LOOP_NO_MORE_ROWS)
           {
             reset_cache_write(&join_tab->cache);
-	    return error; /* purecov: inspected */
+            return rc;
           }
+        }
       }
     }
   } while (!(error=info->read_record(info)));
@@ -9436,10 +9447,10 @@
     read_cached_record(join_tab);		// Restore current record
   reset_cache_write(&join_tab->cache);
   if (error > 0)				// Fatal error
-    return -1;					/* purecov: inspected */
+    return NESTED_LOOP_ERROR;                   /* purecov: inspected */
   for (JOIN_TAB *tmp2=join->join_tab; tmp2 != join_tab ; tmp2++)
     tmp2->table->status=tmp2->status;
-  return 0;
+  return NESTED_LOOP_OK;
 }
 
 
@@ -9905,13 +9916,32 @@
 
 
 /*****************************************************************************
-  The different end of select functions
-  These functions returns < 0 when end is reached, 0 on ok and > 0 if a
-  fatal error (like table corruption) was detected
+  DESCRIPTION
+    Functions that end one nested loop iteration. Different functions
+    are used to support GROUP BY clause and to redirect records
+    to a table (e.g. in case of SELECT into a temporary table) or to the
+    network client.
+
+  RETURN VALUES
+    NESTED_LOOP_OK           - the record has been successfully handled
+    NESTED_LOOP_ERROR        - a fatal error (like table corruption)
+                               was detected
+    NESTED_LOOP_KILLED       - thread shutdown was requested while processing
+                               the record
+    NESTED_LOOP_QUERY_LIMIT  - the record has been successfully handled;
+                               additionally, the nested loop produced the
+                               number of rows specified in the LIMIT clause
+                               for the query
+    NESTED_LOOP_CURSOR_LIMIT - the record has been successfully handled;
+                               additionally, there is a cursor and the nested
+                               loop algorithm produced the number of rows
+                               that is specified for current cursor fetch
+                               operation.
+   All return values except NESTED_LOOP_OK abort the nested loop.
 *****************************************************************************/
 
 /* ARGSUSED */
-static int
+static enum_nested_loop_state
 end_send(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)),
 	 bool end_of_records)
 {
@@ -9920,14 +9950,14 @@
   {
     int error;
     if (join->having && join->having->val_int() == 0)
-      DBUG_RETURN(0);				// Didn't match having
+      DBUG_RETURN(NESTED_LOOP_OK);               // Didn't match having
     error=0;
     if (join->procedure)
       error=join->procedure->send_row(*join->fields);
     else if (join->do_send_rows)
       error=join->result->send_data(*join->fields);
     if (error)
-      DBUG_RETURN(-1); /* purecov: inspected */
+      DBUG_RETURN(NESTED_LOOP_ERROR); /* purecov: inspected */
     if (++join->send_records >= join->unit->select_limit_cnt &&
 	join->do_send_rows)
     {
@@ -9961,10 +9991,10 @@
 	  join->do_send_rows= 0;
 	  if (join->unit->fake_select_lex)
 	    join->unit->fake_select_lex->select_limit= HA_POS_ERROR;
-	  DBUG_RETURN(0);
+	  DBUG_RETURN(NESTED_LOOP_OK);
 	}
       }
-      DBUG_RETURN(-3);				// Abort nicely
+      DBUG_RETURN(NESTED_LOOP_QUERY_LIMIT);      // Abort nicely
     }
     else if (join->send_records >= join->fetch_limit)
     {
@@ -9972,20 +10002,20 @@
         There is a server side cursor and all rows for
         this fetch request are sent.
       */
-      DBUG_RETURN(-4);
+      DBUG_RETURN(NESTED_LOOP_CURSOR_LIMIT);
     }
   }
   else
   {
     if (join->procedure && join->procedure->end_of_records())
-      DBUG_RETURN(-1);
+      DBUG_RETURN(NESTED_LOOP_ERROR);
   }
-  DBUG_RETURN(0);
+  DBUG_RETURN(NESTED_LOOP_OK);
 }
 
 
 	/* ARGSUSED */
-static int
+static enum_nested_loop_state
 end_send_group(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)),
 	       bool end_of_records)
 {
@@ -10037,14 +10067,14 @@
 	  }
 	}
 	if (error > 0)
-	  DBUG_RETURN(-1);			/* purecov: inspected */
+          DBUG_RETURN(NESTED_LOOP_ERROR);        /* purecov: inspected */
 	if (end_of_records)
-	  DBUG_RETURN(0);
+	  DBUG_RETURN(NESTED_LOOP_OK);
 	if (join->send_records >= join->unit->select_limit_cnt &&
 	    join->do_send_rows)
 	{
 	  if (!(join->select_options & OPTION_FOUND_ROWS))
-	    DBUG_RETURN(-3);				// Abort nicely
+	    DBUG_RETURN(NESTED_LOOP_QUERY_LIMIT); // Abort nicely
 	  join->do_send_rows=0;
 	  join->unit->select_limit_cnt = HA_POS_ERROR;
         }
@@ -10054,14 +10084,14 @@
             There is a server side cursor and all rows
             for this fetch request are sent.
           */
-          DBUG_RETURN(-4);
+          DBUG_RETURN(NESTED_LOOP_CURSOR_LIMIT);
         }
       }
     }
     else
     {
       if (end_of_records)
-	DBUG_RETURN(0);
+	DBUG_RETURN(NESTED_LOOP_OK);
       join->first_record=1;
       VOID(test_if_group_changed(join->group_fields));
     }
@@ -10069,33 +10099,32 @@
     {
       copy_fields(&join->tmp_table_param);
       if (init_sum_functions(join->sum_funcs, join->sum_funcs_end[idx+1]))
-	DBUG_RETURN(-1);
+	DBUG_RETURN(NESTED_LOOP_ERROR);
       if (join->procedure)
 	join->procedure->add();
-      DBUG_RETURN(0);
+      DBUG_RETURN(NESTED_LOOP_OK);
     }
   }
   if (update_sum_func(join->sum_funcs))
-    DBUG_RETURN(-1);
+    DBUG_RETURN(NESTED_LOOP_ERROR);
   if (join->procedure)
     join->procedure->add();
-  DBUG_RETURN(0);
+  DBUG_RETURN(NESTED_LOOP_OK);
 }
 
 
 	/* ARGSUSED */
-static int
+static enum_nested_loop_state
 end_write(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)),
 	  bool end_of_records)
 {
   TABLE *table=join->tmp_table;
-  int error;
   DBUG_ENTER("end_write");
 
   if (join->thd->killed)			// Aborted by user
   {
     join->thd->send_kill_message();
-    DBUG_RETURN(-2);				/* purecov: inspected */
+    DBUG_RETURN(NESTED_LOOP_KILLED);             /* purecov: inspected */
   }
   if (!end_of_records)
   {
@@ -10120,6 +10149,7 @@
 #endif
     if (!join->having || join->having->val_int())
     {
+      int error;
       join->found_records++;
       if ((error=table->file->write_row(table->record[0])))
       {
@@ -10128,28 +10158,28 @@
 	  goto end;
 	if (create_myisam_from_heap(join->thd, table, &join->tmp_table_param,
 				    error,1))
-	  DBUG_RETURN(-1);			// Not a table_is_full error
+	  DBUG_RETURN(NESTED_LOOP_ERROR);        // Not a table_is_full error
 	table->s->uniques=0;			// To ensure rows are the same
       }
       if (++join->send_records >= join->tmp_table_param.end_write_records &&
 	  join->do_send_rows)
       {
 	if (!(join->select_options & OPTION_FOUND_ROWS))
-	  DBUG_RETURN(-3);
+	  DBUG_RETURN(NESTED_LOOP_QUERY_LIMIT);
 	join->do_send_rows=0;
 	join->unit->select_limit_cnt = HA_POS_ERROR;
-	DBUG_RETURN(0);
+	DBUG_RETURN(NESTED_LOOP_OK);
       }
     }
   }
 end:
-  DBUG_RETURN(0);
+  DBUG_RETURN(NESTED_LOOP_OK);
 }
 
 /* Group by searching after group record and updating it if possible */
 /* ARGSUSED */
 
-static int
+static enum_nested_loop_state
 end_update(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)),
 	   bool end_of_records)
 {
@@ -10159,11 +10189,11 @@
   DBUG_ENTER("end_update");
 
   if (end_of_records)
-    DBUG_RETURN(0);
+    DBUG_RETURN(NESTED_LOOP_OK);
   if (join->thd->killed)			// Aborted by user
   {
     join->thd->send_kill_message();
-    DBUG_RETURN(-2);				/* purecov: inspected */
+    DBUG_RETURN(NESTED_LOOP_KILLED);             /* purecov: inspected */
   }
 
   join->found_records++;
@@ -10187,9 +10217,9 @@
 				       table->record[0])))
     {
       table->file->print_error(error,MYF(0));	/* purecov: inspected */
-      DBUG_RETURN(-1);				/* purecov: inspected */
+      DBUG_RETURN(NESTED_LOOP_ERROR);            /* purecov: inspected */
     }
-    DBUG_RETURN(0);
+    DBUG_RETURN(NESTED_LOOP_OK);
   }
 
   /*
@@ -10211,19 +10241,19 @@
   {
     if (create_myisam_from_heap(join->thd, table, &join->tmp_table_param,
 				error, 0))
-      DBUG_RETURN(-1);				// Not a table_is_full error
+      DBUG_RETURN(NESTED_LOOP_ERROR);            // Not a table_is_full error
     /* Change method to update rows */
     table->file->ha_index_init(0);
     join->join_tab[join->tables-1].next_select=end_unique_update;
   }
   join->send_records++;
-  DBUG_RETURN(0);
+  DBUG_RETURN(NESTED_LOOP_OK);
 }
 
 
 /* Like end_update, but this is done with unique constraints instead of keys */
 
-static int
+static enum_nested_loop_state
 end_unique_update(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)),
 		  bool end_of_records)
 {
@@ -10232,11 +10262,11 @@
   DBUG_ENTER("end_unique_update");
 
   if (end_of_records)
-    DBUG_RETURN(0);
+    DBUG_RETURN(NESTED_LOOP_OK);
   if (join->thd->killed)			// Aborted by user
   {
     join->thd->send_kill_message();
-    DBUG_RETURN(-2);				/* purecov: inspected */
+    DBUG_RETURN(NESTED_LOOP_KILLED);             /* purecov: inspected */
   }
 
   init_tmptable_sum_functions(join->sum_funcs);
@@ -10250,12 +10280,12 @@
     if ((int) table->file->get_dup_key(error) < 0)
     {
       table->file->print_error(error,MYF(0));	/* purecov: inspected */
-      DBUG_RETURN(-1);				/* purecov: inspected */
+      DBUG_RETURN(NESTED_LOOP_ERROR);            /* purecov: inspected */
     }
     if (table->file->rnd_pos(table->record[1],table->file->dupp_ref))
     {
       table->file->print_error(error,MYF(0));	/* purecov: inspected */
-      DBUG_RETURN(-1);				/* purecov: inspected */
+      DBUG_RETURN(NESTED_LOOP_ERROR);            /* purecov: inspected */
     }
     restore_record(table,record[1]);
     update_tmptable_sum_func(join->sum_funcs,table);
@@ -10263,27 +10293,26 @@
 				       table->record[0])))
     {
       table->file->print_error(error,MYF(0));	/* purecov: inspected */
-      DBUG_RETURN(-1);				/* purecov: inspected */
+      DBUG_RETURN(NESTED_LOOP_ERROR);            /* purecov: inspected */
     }
   }
-  DBUG_RETURN(0);
+  DBUG_RETURN(NESTED_LOOP_OK);
 }
 
 
 	/* ARGSUSED */
-static int
+static enum_nested_loop_state
 end_write_group(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)),
 		bool end_of_records)
 {
   TABLE *table=join->tmp_table;
-  int	  error;
   int	  idx= -1;
   DBUG_ENTER("end_write_group");
 
   if (join->thd->killed)
   {						// Aborted by user
     join->thd->send_kill_message();
-    DBUG_RETURN(-2);				/* purecov: inspected */
+    DBUG_RETURN(NESTED_LOOP_KILLED);             /* purecov: inspected */
   }
   if (!join->first_record || end_of_records ||
       (idx=test_if_group_changed(join->group_fields)) >= 0)
@@ -10302,28 +10331,27 @@
 	}
         copy_sum_funcs(join->sum_funcs,
                        join->sum_funcs_end[send_group_parts]);
-	if (join->having && join->having->val_int() == 0)
-          error= -1;
-        else if ((error= table->file->write_row(table->record[0])))
-	{
-	  if (create_myisam_from_heap(join->thd, table,
-				      &join->tmp_table_param,
-				      error, 0))
-	    DBUG_RETURN(-1);		       
+	if (!join->having || join->having->val_int())
+	{
+          int error= table->file->write_row(table->record[0]);
+          if (error && create_myisam_from_heap(join->thd, table,
+                                               &join->tmp_table_param,
+                                               error, 0))
+	    DBUG_RETURN(NESTED_LOOP_ERROR);
         }
         if (join->rollup.state != ROLLUP::STATE_NONE)
 	{
 	  if (join->rollup_write_data((uint) (idx+1), table))
-	    DBUG_RETURN(-1);
+	    DBUG_RETURN(NESTED_LOOP_ERROR);
 	}
 	if (end_of_records)
-	  DBUG_RETURN(0);
+	  DBUG_RETURN(NESTED_LOOP_OK);
       }
     }
     else
     {
       if (end_of_records)
-	DBUG_RETURN(0);
+	DBUG_RETURN(NESTED_LOOP_OK);
       join->first_record=1;
       VOID(test_if_group_changed(join->group_fields));
     }
@@ -10332,17 +10360,17 @@
       copy_fields(&join->tmp_table_param);
       copy_funcs(join->tmp_table_param.items_to_copy);
       if (init_sum_functions(join->sum_funcs, join->sum_funcs_end[idx+1]))
-	DBUG_RETURN(-1);
+	DBUG_RETURN(NESTED_LOOP_ERROR);
       if (join->procedure)
 	join->procedure->add();
-      DBUG_RETURN(0);
+      DBUG_RETURN(NESTED_LOOP_OK);
     }
   }
   if (update_sum_func(join->sum_funcs))
-    DBUG_RETURN(-1);
+    DBUG_RETURN(NESTED_LOOP_ERROR);
   if (join->procedure)
     join->procedure->add();
-  DBUG_RETURN(0);
+  DBUG_RETURN(NESTED_LOOP_OK);
 }
 
 

--- 1.82/sql/sql_select.h	2005-04-20 12:07:19 +04:00
+++ 1.83/sql/sql_select.h	2005-04-29 21:56:03 +04:00
@@ -91,7 +91,15 @@
 
 class JOIN;
 
-typedef int (*Next_select_func)(JOIN *,struct st_join_table *,bool);
+enum enum_nested_loop_state
+{
+  NESTED_LOOP_KILLED= -2, NESTED_LOOP_ERROR= -1,
+  NESTED_LOOP_OK= 0, NESTED_LOOP_NO_MORE_ROWS= 1,
+  NESTED_LOOP_QUERY_LIMIT= 3, NESTED_LOOP_CURSOR_LIMIT= 4
+};
+
+typedef enum_nested_loop_state
+(*Next_select_func)(JOIN *, struct st_join_table *, bool);
 typedef int (*Read_record_func)(struct st_join_table *tab);
 
 
@@ -162,6 +170,11 @@
   uint	   send_group_parts;
   bool	   sort_and_group,first_record,full_join,group, no_field_update;
   bool	   do_send_rows;
+  /*
+    TRUE when we want to resume nested loop iterations when
+    fetching data from a cursor
+  */
+  bool     resume_nested_loop;
   table_map const_table_map,found_const_table_map,outer_join;
   ha_rows  send_records,found_records,examined_rows,row_limit, select_limit;
   /*
@@ -263,6 +276,7 @@
     sort_and_group= 0;
     first_record= 0;
     do_send_rows= 1;
+    resume_nested_loop= FALSE;
     send_records= 0;
     found_records= 0;
     fetch_limit= HA_POS_ERROR;
@@ -374,7 +388,7 @@
   void reset_thd(THD *thd);
 
   int open(JOIN *join);
-  int fetch(ulong num_rows);
+  void fetch(ulong num_rows);
   void reset() { join= 0; }
   bool is_open() const { return join != 0; }
   void close();

--- 1.112/sql/sql_prepare.cc	2005-04-22 10:50:58 +04:00
+++ 1.113/sql/sql_prepare.cc	2005-04-29 21:56:03 +04:00
@@ -2223,7 +2223,7 @@
     my_pthread_setprio(pthread_self(), QUERY_PRIOR);
 
   thd->protocol= &thd->protocol_prep;		// Switch to binary protocol
-  (void) stmt->cursor->fetch(num_rows);
+  stmt->cursor->fetch(num_rows);
   thd->protocol= &thd->protocol_simple;         // Use normal protocol
 
   if (!(specialflag & SPECIAL_NO_PRIOR))

--- 1.112/tests/mysql_client_test.c	2005-04-12 10:51:20 +04:00
+++ 1.113/tests/mysql_client_test.c	2005-04-29 21:56:03 +04:00
@@ -12854,6 +12854,59 @@
   myquery(rc);
 }
 
+
+/* Crash when opening a cursor to a query with DISTICNT and no key */
+
+static void test_bug9520()
+{
+  MYSQL_STMT *stmt;
+  MYSQL_BIND bind[1];
+  char a[6];
+  ulong a_len;
+  int rc, row_count= 0;
+
+  myheader("test_bug9520");
+
+  mysql_query(mysql, "drop table if exists t1");
+  mysql_query(mysql, "create table t1 (a char(5), b char(5), c char(5),"
+                     " primary key (a, b, c))");
+  rc= mysql_query(mysql, "insert into t1 values ('x', 'y', 'z'), "
+                  " ('a', 'b', 'c'), ('k', 'l', 'm')");
+  myquery(rc);
+
+  stmt= open_cursor("select distinct b from t1");
+
+  /*
+    Not crashes with:
+    stmt= open_cursor("select distinct a from t1");
+  */
+
+  rc= mysql_stmt_execute(stmt);
+  check_execute(stmt, rc);
+
+  bzero(bind, sizeof(bind));
+  bind[0].buffer_type= MYSQL_TYPE_STRING;
+  bind[0].buffer= (char*) a;
+  bind[0].buffer_length= sizeof(a);
+  bind[0].length= &a_len;
+
+  mysql_stmt_bind_result(stmt, bind);
+
+  while (!(rc= mysql_stmt_fetch(stmt)))
+    row_count++;
+
+  DIE_UNLESS(rc == MYSQL_NO_DATA);
+
+  printf("Fetched %d rows\n", row_count);
+  DBUG_ASSERT(row_count == 3);
+
+  mysql_stmt_close(stmt);
+
+  rc= mysql_query(mysql, "drop table t1");
+  myquery(rc);
+}
+
+
 /*
   Read and parse arguments and MySQL options from my.cnf
 */
@@ -13079,6 +13132,7 @@
   { "test_bug8722", test_bug8722 },
   { "test_bug8880", test_bug8880 },
   { "test_bug9159", test_bug9159 },
+  { "test_bug9520", test_bug9520 },
   { 0, 0 }
 };
 
Thread
bk commit into 5.0 tree (konstantin:1.1920) BUG#9520konstantin29 Apr