From 0801ef01829f2dd4fbff6000691fbc820a3f6081 Mon Sep 17 00:00:00 2001
From: Jared Hancock <gravydish@gmail.com>
Date: Wed, 22 Aug 2018 04:50:59 +0000
Subject: [PATCH] queue: Add MySQL index hint

This adds the advanced option to the queue sort configuration. An index can be
specified to be used for the sorting operation. In some cases, the MySQL query
optimizer cannot select the most efficient index to use when dealing with large
querysets and sorting. This feature, if enabled, allows an administrator to specify
an index which MySQL should use when using the sort.

To use the feature, an `extra` column must be added to the `%queue_sort` table to
receive the index name.
---
 include/class.orm.php                         | 19 ++++++++-
 include/class.queue.php                       | 42 +++++++++++++++++++
 .../templates/queue-sorting-edit.tmpl.php     | 21 ++++++++++
 scp/css/scp.css                               |  6 +++
 4 files changed, 87 insertions(+), 1 deletion(-)

diff --git a/include/class.orm.php b/include/class.orm.php
index 473be838c..72b34c81c 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -333,6 +333,11 @@ class VerySimpleModel {
         return static::getMeta()->newInstance($row);
     }
 
+    function __wakeup() {
+        // If a model is stashed in a session, refresh the model from the database
+        $this->refetch();
+    }
+
     function get($field, $default=false) {
         if (array_key_exists($field, $this->ht))
             return $this->ht[$field];
@@ -1142,6 +1147,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
     const OPT_NOSORT    = 'nosort';
     const OPT_NOCACHE   = 'nocache';
     const OPT_MYSQL_FOUND_ROWS = 'found_rows';
+    const OPT_INDEX_HINT = 'indexhint';
 
     const ITER_MODELS   = 1;
     const ITER_HASH     = 2;
@@ -1477,6 +1483,14 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
         return isset($this->options[$option]);
     }
 
+    function getOption($option) {
+        return @$this->options[$option] ?: false;
+    }
+
+    function setOption($option, $value) {
+        $this->options[$option] = $value;
+    }
+
     function countSelectFields() {
         $count = count($this->values) + count($this->annotations);
         if (isset($this->extra['select']))
@@ -3083,12 +3097,15 @@ class MySqlCompiler extends SqlCompiler {
         $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : '';
 
         $joins = $this->getJoins($queryset);
+        if ($hint = $queryset->getOption(QuerySet::OPT_INDEX_HINT)) {
+            $hint = " USE INDEX ({$hint})";
+        }
 
         $sql = 'SELECT ';
         if ($queryset->hasOption(QuerySet::OPT_MYSQL_FOUND_ROWS))
             $sql .= 'SQL_CALC_FOUND_ROWS ';
         $sql .= implode(', ', $fields).' FROM '
-            .$table.$joins.$where.$group_by.$having.$sort;
+            .$table.$hint.$joins.$where.$group_by.$having.$sort;
         // UNIONS
         if ($queryset->chain) {
             // If the main query is sorted, it will need parentheses
diff --git a/include/class.queue.php b/include/class.queue.php
index 643fdb712..dca048263 100644
--- a/include/class.queue.php
+++ b/include/class.queue.php
@@ -2716,6 +2716,7 @@ extends VerySimpleModel {
     );
 
     var $_columns;
+    var $_extra;
 
     function getRoot($hint=false) {
         switch ($hint ?: $this->root) {
@@ -2733,6 +2734,12 @@ extends VerySimpleModel {
         return $this->id;
     }
 
+    function getExtra() {
+        if (isset($this->extra) && !isset($this->_extra))
+            $this->_extra = JsonDataParser::decode($this->extra);
+        return $this->_extra;
+    }
+
     function applySort(QuerySet $query, $reverse=false, $root=false) {
         $fields = CustomQueue::getSearchableFields($this->getRoot($root));
         foreach ($this->getColumnPaths() as $path=>$descending) {
@@ -2743,6 +2750,10 @@ extends VerySimpleModel {
                     CustomQueue::getOrmPath($path, $query));
             }
         }
+        // Add index hint if defined
+        if (($extra = $this->getExtra()) && isset($extra['index'])) {
+            $query->setOption(QuerySet::OPT_INDEX_HINT, $extra['index']);
+        }
         return $query;
     }
 
@@ -2776,6 +2787,11 @@ extends VerySimpleModel {
             array('id' => $this->id));
     }
 
+    function getAdvancedConfigForm($source=false) {
+        return new QueueSortAdvancedConfigForm($source ?: $this->getExtra(),
+            array('id' => $this->id));
+    }
+
     static function forQueue(CustomQueue $queue) {
         return static::objects()->filter([
             'root' => $queue->root ?: 'T',
@@ -2811,6 +2827,11 @@ extends VerySimpleModel {
             $this->columns = JsonDataEncoder::encode($columns);
         }
 
+        if ($this->getExtra() !== null) {
+            $extra = $this->getAdvancedConfigForm($vars)->getClean();
+            $this->extra = JsonDataEncoder::encode($extra);
+        }
+
         if (count($errors))
             return false;
 
@@ -3070,3 +3091,24 @@ extends AbstractForm {
         );
     }
 }
+
+class QueueSortAdvancedConfigForm
+extends AbstractForm {
+    function getInstructions() {
+        return __('If unsure, leave these options blank and unset');
+    }
+
+    function buildFields() {
+        return array(
+            'index' => new TextboxField(array(
+                'label' => __('Database Index'),
+                'hint' => __('Use this index when sorting on this column'),
+                'required' => false,
+                'layout' => new GridFluidCell(12),
+                'configuration' => array(
+                    'placeholder' => __('Automatic'),
+                ),
+            )),
+        );
+    }
+}
diff --git a/include/staff/templates/queue-sorting-edit.tmpl.php b/include/staff/templates/queue-sorting-edit.tmpl.php
index 0a2d98b02..a001201a2 100644
--- a/include/staff/templates/queue-sorting-edit.tmpl.php
+++ b/include/staff/templates/queue-sorting-edit.tmpl.php
@@ -5,6 +5,7 @@
  * $column - <QueueColumn> instance for this column
  */
 $sortid = $sort->getId();
+$advanced = in_array('extra', $sort::getMeta()->getFieldNames());
 ?>
 <h3 class="drag-handle"><?php echo __('Manage Sort Options'); ?> &mdash;
     <?php echo $sort->get('name') ?></h3>
@@ -14,10 +15,30 @@ $sortid = $sort->getId();
 <form method="post" action="#tickets/search/sort/edit/<?php
     echo $sortid; ?>">
 
+<?php if ($advanced) { ?>
+  <ul class="clean tabs">
+    <li class="active"><a href="#fields"><i class="icon-columns"></i>
+      <?php echo __('Fields'); ?></a></li>
+    <li><a href="#advanced"><i class="icon-cog"></i>
+      <?php echo __('Advanced'); ?></a></li>
+  </ul>
+
+  <div class="tab_content" id="fields">
+<?php } ?>
+
 <?php
 include 'queue-sorting.tmpl.php';
 ?>
 
+<?php if ($advanced) { ?>
+  </div>
+
+  <div class="hidden tab_content" id="advanced">
+    <?php echo $sort->getAdvancedConfigForm()->asTable(); ?>
+  </div>
+
+<?php } ?>
+
 <hr>
 <p class="full-width">
     <span class="buttons pull-left">
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 878333317..f255ae506 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -3525,6 +3525,12 @@ table.grid.form caption {
   margin-bottom: 5px;
 }
 
+.grid.form .field > .field-hint-text {
+  font-style: italic;
+  margin: 0 10px 5px 10px;
+  opacity: 0.8;
+}
+
 #basic_search {
   background-color: #f4f4f4;
   margin: -10px 0;
-- 
GitLab