Changeset 11881

Show
Ignore:
Timestamp:
08/30/07 16:07:23 (1 year ago)
Author:
rambo
Message:

refs #102, closes #64

use moving window to handle offset/limit for DBA QB

TODO: clean up

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/midcom/midcom.core/midcom/core/querybuilder.php

    r11844 r11881  
    182182     * While on-site, this is enabled by default, in AIS it is disabled by default. 
    183183     */ 
    184     var $hide_invisible = false; 
     184    var $hide_invisible = true; 
     185 
    185186    /** 
    186187     * The class this qb is working on. 
     
    188189     */ 
    189190    var $classname = null; 
     191 
     192    /** 
     193     * Keep track of GUIDs seen to avoid workaround ML bug 
     194     */ 
     195    var $_seen_guids = array(); 
     196     
     197    var $_qb_error_result = 'UNDEFINED'; 
     198     
     199    /** 
     200     * When determining window sizes for offset/limit queries use this as minimum size 
     201     */ 
     202    var $min_window_size = 10; 
     203 
     204    /** 
     205     * When determining window sizes for offset/limit queries use this as maximum size 
     206     */ 
     207    var $max_window_size = 500; 
    190208 
    191209    /** 
     
    196214     * 
    197215     * @param string $classname The classname which should be queried. 
     216     * @todo remove baseclass resolution, Midgard core can handle extended classnames correctly nowadays 
    198217     */ 
    199218    function midcom_core_querybuilder($classname) 
     
    205224        { 
    206225            $baseclass = $_class_mapping_cache[$classname]; 
    207            
     226       
    208227        else 
    209228        { 
     
    234253        } 
    235254 
     255        $this->_real_class = $classname; 
    236256        $this->_qb = new midgard_query_builder($baseclass); 
    237         $this->_real_class = $classname; 
    238  
    239         if (! array_key_exists("view_contentmgr", $GLOBALS)) 
    240         { 
    241             $this->hide_invisible = true; 
    242              
    243             if ($GLOBALS['midcom_config']['i18n_multilang_strict']) 
    244             { 
    245                 $this->_qb->set_lang($_MIDCOM->i18n->get_midgard_language()); 
    246             } 
    247         } 
    248     } 
    249  
     257 
     258        if ($GLOBALS['midcom_config']['i18n_multilang_strict']) 
     259        { 
     260            $this->_qb->set_lang($_MIDCOM->i18n->get_midgard_language()); 
     261        } 
     262    } 
    250263 
    251264    /** 
     
    258271    } 
    259272 
     273    function _execute_and_check_privileges($false_on_empty_mgd_resultset = false) 
     274    { 
     275        debug_push_class(__CLASS__, __FUNCTION__); 
     276        $result = $this->_qb->execute(); 
     277        if (!is_array($result)) 
     278        { 
     279            $this->_qb_error_result = $result; 
     280            debug_print_r('Result was:', $result); 
     281            debug_add('The querybuilder failed to execute, aborting.', MIDCOM_LOG_ERROR); 
     282            debug_add('Last Midgard error was: ' . mgd_errstr(), MIDCOM_LOG_ERROR); 
     283            if (isset($php_errormsg)) 
     284            { 
     285                debug_add("Error message was: {$php_errormsg}", MIDCOM_LOG_ERROR); 
     286            } 
     287 
     288            debug_pop(); 
     289            return $result; 
     290        } 
     291        debug_add('Got ' . count($result) . ' initial results'); 
     292        if (   empty($result) 
     293            && $false_on_empty_mgd_resultset) 
     294        { 
     295            debug_pop(); 
     296            return false; 
     297        } 
     298 
     299        // Workaround until the QB returns the correct type, refetch everything 
     300        $newresult = Array(); 
     301        $classname = $this->_real_class; 
     302        $skipped_objects = 0; 
     303        $this->denied = 0; 
     304        foreach ($result as $key => $value) 
     305        { 
     306            // Workaround to ML bug where we get multiple results in non-strict mode 
     307            if (isset($this->_seen_guids[$value->guid])) 
     308            { 
     309                debug_add("The {$classname} object {$value->guid} has already been seen, probably MultiLang bug", MIDCOM_LOG_WARN); 
     310                //debug_add('var_export($seen_guids): ' . var_export($this->_seen_guids, true)); 
     311                continue; 
     312            } 
     313            $this->_seen_guids[$value->guid] = true; 
     314 
     315            // Create a new object instance (checks read privilege implicitly) using the copy-constuctor. 
     316            $object = new $classname($value); 
     317 
     318            if (mgd_errno() == MGD_ERR_ACCESS_DENIED) 
     319            { 
     320                // This is logged by the callers 
     321                $this->denied++; 
     322                $skipped_objects++; 
     323                continue; 
     324            } 
     325 
     326            if (   ! $object 
     327                || ! is_object($object)) 
     328            { 
     329                debug_add("Could not create a MidCOM DBA instance of the {$classname} ID {$value->id}. See debug level log for details.", 
     330                    MIDCOM_LOG_INFO); 
     331                $skipped_objects++; 
     332                continue; 
     333            } 
     334 
     335            // Check visibility 
     336            if ($this->hide_invisible) 
     337            { 
     338                $metadata =& midcom_helper_metadata::retrieve($object); 
     339                if (! $metadata) 
     340                { 
     341                    debug_add("Could not create a MidCOM metadata instance for {$classname} ID {$value->id}, assuming an invisible object.", 
     342                        MIDCOM_LOG_INFO); 
     343                    $skipped_objects++; 
     344                    continue; 
     345                } 
     346 
     347                if (! $metadata->is_object_visible_onsite()) 
     348                { 
     349                    debug_add("The {$classname} ID {$value->id} is hidden by metadata.", MIDCOM_LOG_INFO); 
     350                    $skipped_objects++; 
     351                    continue; 
     352                } 
     353            } 
     354 
     355            $newresult[] = $object; 
     356        } 
     357        debug_add('Returning ' . count($newresult) . ' items'); 
     358        debug_pop(); 
     359        return $newresult; 
     360    } 
    260361 
    261362    /** 
     
    281382     * @todo Implement proper count / Limit support. 
    282383     */ 
    283     function execute() 
    284     { 
    285         debug_push_class(__CLASS__, __FUNCTION__); 
    286  
     384    function execute_windowed() 
     385    { 
     386        // Reset these two in case someone tries to re-execute this 
     387        $this->_seen_guids = array();  
     388        $this->_qb_error_result = 'UNDEFINED'; 
     389         
    287390        if (! call_user_func_array(array($this->_real_class, '_on_prepare_exec_query_builder'), array(&$this))) 
    288391        { 
     392            debug_push_class(__CLASS__, __FUNCTION__); 
    289393            debug_add('The _on_prepare_exec_query_builder callback returned false, so we abort now.'); 
    290394            debug_pop(); 
     
    294398        if ($this->_constraint_count == 0) 
    295399        { 
     400            debug_push_class(__CLASS__, __FUNCTION__); 
     401            debug_add('This Query Builder instance has no constraints (set loglevel to debug to see stack trace)', MIDCOM_LOG_WARN); 
     402            debug_print_function_stack('We were called from here:'); 
     403            debug_pop(); 
     404        } 
     405 
     406        if (   empty($this->_limit) 
     407            && empty($this->_offset)) 
     408        { 
     409            // No point to do windowing 
     410            $newresult = $this->_execute_and_check_privileges(); 
     411            if (!is_array($newresult)) 
     412            { 
     413                return $newresult; 
     414            } 
     415        } 
     416        else 
     417        { 
     418            //debug_push_class(__CLASS__, __FUNCTION__); 
     419            $newresult = array(); 
     420            // Must be copies 
     421            $limit = $this->_limit; 
     422            $offset = $this->_offset; 
     423            $i = 0; 
     424            $this->_set_limit_offset_window($i); 
     425             
     426            while (($resultset = $this->_execute_and_check_privileges(true)) !== false) 
     427            { 
     428                //debug_add("Iteration loop #{$i}"); 
     429                if ($this->_qb_error_result !== 'UNDEFINED') 
     430                { 
     431                    // QB failed in above method TODO: better catch 
     432                    /* 
     433                    debug_add('_execute_and_check_privileges caught QB error, returning that now', MIDCOM_LOG_WARN); 
     434                    debug_pop(); 
     435                    */ 
     436                    return $this->_qb_error_result; 
     437                } 
     438 
     439                foreach($resultset as $object) 
     440                { 
     441                    // We still have offset left to skip 
     442                    if ($offset) 
     443                    { 
     444                        //debug_add("Offset of {$this->_offset} not yet reached, continuing loop"); 
     445                        $offset--; 
     446                        continue; 
     447                    } 
     448                    // We have hit our limit 
     449                    if (   $this->_limit > 0 
     450                        && $limit == 0) 
     451                    { 
     452                        //debug_add("Limit of {$this->_limit} hit, breaking out of loops"); 
     453                        break 2; 
     454                    } 
     455 
     456                    $newresult[] = $object; 
     457 
     458                    if ($this->_limit > 0) 
     459                    { 
     460                        $limit--; 
     461                    } 
     462                } 
     463                ++$i; 
     464                $this->_set_limit_offset_window($i); 
     465            } 
     466        } 
     467 
     468        call_user_func_array(array($this->_real_class, '_on_process_query_result'), array(&$newresult)); 
     469 
     470        /* 
     471        // correct record count by the number of limit-skipped objects. 
     472        $this->count = count($newresult) + $skipped_objects; 
     473        */ 
     474        $this->count = count($newresult); 
     475 
     476        //debug_pop(); 
     477        return $newresult; 
     478    } 
     479 
     480    function _set_limit_offset_window($iteration) 
     481    { 
     482        /* 
     483        debug_push_class(__CLASS__, __FUNCTION__); 
     484        debug_add("Called for iteration #{$iteration}"); 
     485        */ 
     486        static $window_size = 0; 
     487        if (!$window_size) 
     488        { 
     489            // Try to be smart about the window size 
     490            switch (true) 
     491            { 
     492                case (   empty($this->_offset) 
     493                      && $this->_limit): 
     494                    // Get limited number from start (I supposed generally less than 50% will be unreadable) 
     495                    $window_size = round($this->_limit * 1.5); 
     496                    break; 
     497                case (   empty($this->_limit) 
     498                      && $this->_offset): 
     499                    // Get rest from offset 
     500                    /* TODO: Somehow factor in that if we have huge number of objects and relatively small offset we want to increase window size 
     501                    $full_object_count = $this->_qb->count(); 
     502                    */ 
     503                    $window_size = round($this->_offset * 2); 
     504                case (   $this->_offset > $this->_limit): 
     505                    // Offset is greater than limit, basically this is almost the same problem as above 
     506                    $window_size = round($this->_offset * 2); 
     507                    break; 
     508                case (   $this->_limit > $this->_offset): 
     509                    // Limit is greater than offset, this is probably similar to getting limited number from beginning 
     510                    $window_size = round($this->_limit * 2); 
     511                    break; 
     512                case ($this->_limit == $this->_offset): 
     513                    $window_size = round($this->_offset * 2); 
     514                    break; 
     515            } 
     516 
     517            if ($window_size > $this->max_window_size) 
     518            { 
     519                $window_size = $this->max_window_size; 
     520            } 
     521            if ($window_size < $this->min_window_size) 
     522            { 
     523                $window_size = $this->min_window_size; 
     524            } 
     525        } 
     526        //debug_add("Got window size {$window_size}"); 
     527        $offset = $iteration*$window_size; 
     528        if ($offset) 
     529        { 
     530            //debug_add("Setting offset to {$offset}"); 
     531            $this->_qb->set_offset($offset); 
     532        } 
     533        //debug_add("Setting limit to {$window_size}"); 
     534        $this->_qb->set_limit($window_size); 
     535        //debug_pop(); 
     536    } 
     537 
     538    function execute() 
     539    { 
     540        return $this->execute_windowed(); 
     541    } 
     542 
     543    /** 
     544     * This function will execute the Querybuilder and call the appropriate callbacks from the 
     545     * class it is associated to. This way, class authors have full control over what is actually 
     546     * returned to the application. 
     547     * 
     548     * The calling sequence of all event handlers of the associated class is like this: 
     549     * 
     550     * 1. bool _on_prepare_exec_query_builder(&$this) is called before the actual query execution. Return false to 
     551     *    abort the operation. 
     552     * 2. The query is executed. 
     553     * 3. void _on_process_query_result(&$result) is called after the successful execution of the query. You 
     554     *    may remove any unwanted entries from the resultset at this point. 
     555     * 
     556     * If the execution of the query fails for some reason all available error information is logged 
     557     * and a MIDCOM_ERRCRIT level error is triggered, halting execution. 
     558     * 
     559     * @param midgard_query_builder $qb An instance of the Query builder obtained by the new_query_builder 
     560     *     function of this class. 
     561     * @return Array The result of the query builder or null on any error. Note, that empty resultsets 
     562     *     will return an empty array. 
     563     * @todo Implement proper count / Limit support. 
     564     */ 
     565    function execute_notwindowed() 
     566    { 
     567        debug_push_class(__CLASS__, __FUNCTION__); 
     568 
     569        if (! call_user_func_array(array($this->_real_class, '_on_prepare_exec_query_builder'), array(&$this))) 
     570        { 
     571            debug_add('The _on_prepare_exec_query_builder callback returned false, so we abort now.'); 
     572            debug_pop(); 
     573            return null; 
     574        } 
     575 
     576        if ($this->_constraint_count == 0) 
     577        { 
    296578            debug_add('This Query Builder instance has no constraints.', MIDCOM_LOG_WARN); 
    297579            debug_print_function_stack('We were called from here:'); 
    298580        } 
    299581 
    300         // Workaround until the QB does return empty arrays: All errors are empty resultsets and errors are ignored. 
    301         $result = @$this->_qb->execute(); 
    302         if (! is_array($result)) 
    303         { 
    304             // Workaround mode for now 
    305             if (mgd_errno() != MGD_ERR_OK) 
    306             { 
    307                 debug_print_r('Result was:', $result); 
    308                 debug_add('The querybuilder failed to execute, aborting.', MIDCOM_LOG_ERROR); 
    309                 debug_add('Last Midgard error was: ' . mgd_errstr(), MIDCOM_LOG_ERROR); 
    310                 if (isset($php_errormsg)) 
    311                 { 
    312                     debug_add("Error message was: {$php_errormsg}", MIDCOM_LOG_ERROR); 
    313                 } 
    314  
    315                 $_MIDCOM->generate_error(MIDCOM_ERRCRIT, 
    316                     'The query builder failed to execute, see the log file for more information.'); 
    317                 // This will exit. 
    318             } 
    319  
    320             $result = Array(); 
     582        $result = $this->_qb->execute(); 
     583        if (!is_array($result)) 
     584        { 
     585            debug_print_r('Result was:', $result); 
     586            debug_add('The querybuilder failed to execute, aborting.', MIDCOM_LOG_ERROR); 
     587            debug_add('Last Midgard error was: ' . mgd_errstr(), MIDCOM_LOG_ERROR); 
     588            if (isset($php_errormsg)) 
     589            { 
     590                debug_add("Error message was: {$php_errormsg}", MIDCOM_LOG_ERROR); 
     591            } 
     592 
     593            /* 
     594            $_MIDCOM->generate_error(MIDCOM_ERRCRIT, 
     595                'The query builder failed to execute, see the log file for more information.'); 
     596            // This will exit. 
     597            */ 
     598            return false; 
    321599        } 
    322600 
     
    454732        } 
    455733 
    456         // Workaround until the QB does return empty arrays: All errors are empty resultsets and errors are ignored. 
    457         $result = @$this->_qb->execute(); 
    458         if (! is_array($result)) 
    459         { 
    460             // Workaround mode for now 
    461             if (mgd_errstr() != 'MGD_ERR_OK') 
    462             { 
    463                 debug_print_r('Result was:', $result); 
    464                 debug_add('The querybuilder failed to execute, aborting.', MIDCOM_LOG_ERROR); 
    465                 debug_add('Last Midgard error was: ' . mgd_errstr(), MIDCOM_LOG_ERROR); 
    466                 if (isset($php_errormsg)) 
    467                 { 
    468                     debug_add("Error message was: {$php_errormsg}", MIDCOM_LOG_ERROR); 
    469                 } 
    470  
    471                 $_MIDCOM->generate_error(MIDCOM_ERRCRIT, 
    472                     'The query builder failed to execute, see the log file for more information.'); 
    473                 // This will exit. 
    474             } 
    475  
    476             debug_pop(); 
    477             $result = Array(); 
     734        $result = $this->_qb->execute(); 
     735        if (!is_array($result)) 
     736        { 
     737            debug_print_r('Result was:', $result); 
     738            debug_add('The querybuilder failed to execute, aborting.', MIDCOM_LOG_ERROR); 
     739            debug_add('Last Midgard error was: ' . mgd_errstr(), MIDCOM_LOG_ERROR); 
     740            if (isset($php_errormsg)) 
     741            { 
     742                debug_add("Error message was: {$php_errormsg}", MIDCOM_LOG_ERROR); 
     743            } 
     744 
     745            /* 
     746            $_MIDCOM->generate_error(MIDCOM_ERRCRIT, 
     747                'The query builder failed to execute, see the log file for more information.'); 
     748            // This will exit. 
     749            */ 
     750            return false; 
    478751        } 
    479752 
     
    560833            return false; 
    561834        } 
     835 
    562836        $this->_constraint_count++; 
    563837 
     
    612886        { 
    613887            debug_push_class(__CLASS__, __FUNCTION__); 
    614             debug_add("Failed to exectue add_order: Unknown or invalid column '{$field}'.", MIDCOM_LOG_ERROR); 
     888            debug_add("Failed to exectue add_order for column '{$field}', midgard error: " . mgd_errstr(), MIDCOM_LOG_ERROR); 
    615889            debug_pop(); 
    616890        } 
     
    652926     * @param int $count The maximum number of records in the resultset. 
    653927     */ 
    654     function set_limit($count) 
    655     { 
    656         $this->_limit = $count; 
     928    function set_limit($limit) 
     929    { 
     930        $this->_limit = $limit; 
    657931    } 
    658932 
     
    7311005    function count_unchecked() 
    7321006    { 
     1007        // TODO: Handle limit and offset 
    7331008        return $this->_qb->count(); 
    7341009    }