From 9e1827bbbf502dbcf99c3ca6be81453875ab05bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cam=20Sau=CC=88l?= <cammsaul@gmail.com>
Date: Thu, 9 Jul 2015 13:03:51 -0700
Subject: [PATCH] Fix column ordering / test fix

---
 .../driver/generic_sql/query_processor.clj    | 59 ++++++++++++-------
 src/metabase/driver/query_processor.clj       | 59 ++++++++++---------
 test/metabase/driver/query_processor_test.clj | 32 +++++-----
 3 files changed, 86 insertions(+), 64 deletions(-)

diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index d7ab6f153f6..d3e704c4769 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -96,33 +96,50 @@
 
 
 (defprotocol IGenericSQLFormattable
-  (formatted [this]))
+  (formatted [this] [this include-as?]))
 
 (extend-protocol IGenericSQLFormattable
   Field
-  (formatted [{:keys [table-name field-name base-type special-type]}]
-    ;; TODO - add Table names
-    (cond
-      (contains? #{:DateField :DateTimeField} base-type) `(raw ~(format "CAST(\"%s\".\"%s\" AS DATE)" table-name field-name))
-      (= special-type :timestamp_seconds)                `(raw ~((:cast-timestamp-seconds-field-to-date-fn (:driver *query*)) table-name field-name))
-      (= special-type :timestamp_milliseconds)           `(raw ~((:cast-timestamp-milliseconds-field-to-date-fn (:driver *query*)) table-name field-name))
-      :else                                              (keyword (format "%s.%s" table-name field-name))))
+  (formatted
+    ([this]
+     (formatted this false))
+    ([{:keys [table-name field-name base-type special-type]} include-as?]
+     ;; TODO - add Table names
+     (cond
+       (contains? #{:DateField :DateTimeField} base-type) `(raw ~(str (format "CAST(\"%s\".\"%s\" AS DATE)" table-name field-name)
+                                                                      (when include-as?
+                                                                        (format " AS \"%s\"" field-name))))
+       (= special-type :timestamp_seconds)                `(raw ~(str ((:cast-timestamp-seconds-field-to-date-fn (:driver *query*)) table-name field-name)
+                                                                      (when include-as?
+                                                                        (format " AS \"%s\"" field-name))))
+       (= special-type :timestamp_milliseconds)           `(raw ~(str ((:cast-timestamp-milliseconds-field-to-date-fn (:driver *query*)) table-name field-name)
+                                                                      (when include-as?
+                                                                        (format " AS \"%s\"" field-name))))
+       :else                                              (keyword (format "%s.%s" table-name field-name)))))
+
 
   ;; e.g. the ["aggregation" 0] fields we allow in order-by
   OrderByAggregateField
-  (formatted [_]
-    (let [{:keys [aggregation-type]} (:aggregation (:query *query*))] ; determine the name of the aggregation field
-      `(raw ~(case aggregation-type
-               :avg      "\"avg\""
-               :count    "\"count\""
-               :distinct "\"count\""
-               :stddev   "\"stddev\""
-               :sum      "\"sum\""))))
+  (formatted
+    ([this]
+     (formatted this false))
+    ([_ _]
+     (let [{:keys [aggregation-type]} (:aggregation (:query *query*))] ; determine the name of the aggregation field
+       `(raw ~(case aggregation-type
+                :avg      "\"avg\""
+                :count    "\"count\""
+                :distinct "\"count\""
+                :stddev   "\"stddev\""
+                :sum      "\"sum\"")))))
+
 
   Value
-  (formatted [{:keys [value]}]
-    (if-not (instance? java.util.Date value) value
-            `(raw ~(format "CAST('%s' AS DATE)" (.toString ^java.util.Date value))))))
+  (formatted
+    ([this]
+     (formatted this false))
+    ([{:keys [value]} _]
+     (if-not (instance? java.util.Date value) value
+             `(raw ~(format "CAST('%s' AS DATE)" (.toString ^java.util.Date value)))))))
 
 
 (defmethod apply-form :aggregation [[_ {:keys [aggregation-type field]}]]
@@ -148,11 +165,11 @@
     ;; Add fields form only for fields that weren't specified in :fields clause -- we don't want to include it twice, or korma will barf
     (fields ~@(->> fields
                    (filter (partial (complement contains?) (set (:fields (:query *query*)))))
-                   (map formatted)))])
+                   (map (u/rpartial formatted :include-as))))])
 
 
 (defmethod apply-form :fields [[_ fields]]
-  `(fields ~@(map formatted fields)))
+  `(fields ~@(map (u/rpartial formatted :include-as) fields)))
 
 
 (defn- filter-subclause->predicate
diff --git a/src/metabase/driver/query_processor.clj b/src/metabase/driver/query_processor.clj
index cd3639b0294..f49e400b835 100644
--- a/src/metabase/driver/query_processor.clj
+++ b/src/metabase/driver/query_processor.clj
@@ -222,19 +222,19 @@
 (defn- order-cols
   "Construct a sequence of column keywords that should be used for pulling ordered rows from RESULTS.
    FIELDS should be a sequence of all `Fields` for the `Table` associated with QUERY."
-  [{{breakout-fields :breakout, fields-fields :fields, fields-is-implicit :fields-is-implicit} :query} results fields]
-  {:post [(= (set %)
-             (set (keys (first results))))]}
-  (let [;; TODO - This function was written before the advent of the expanded query it is designed to work with Field IDs rather than expanded forms
-        ;; Since this logic is delicate I've side-stepped the issue by converting the expanded Fields back to IDs for the time being.
-        ;; We should carefully re-work this function to use expanded Fields so we don't need the complicated logic below to fetch their names
+  [{{breakout-fields :breakout, {ag-type :aggregation-type} :aggregation, fields-fields :fields, fields-is-implicit :fields-is-implicit} :query} results fields]
+  (let [;; Get all the column name keywords returned by the results
+        result-kws       (set (keys (first results)))
+        valid-kw?        (partial contains? result-kws)
+
         breakout-ids     (map :field-id breakout-fields)
 
-        breakout-kws     (for [field breakout-fields]
-                           (->> (rest (expand/qualified-name-components field)) ; TODO - this "qualified name for results" should be calculated in the Query expander
-                                (interpose ".")
-                                (apply str)
-                                keyword))
+        breakout-kws     (->> (for [field breakout-fields]
+                                (->> (rest (expand/qualified-name-components field)) ; TODO - this "qualified name for results" should be calculated in the Query expander
+                                     (interpose ".")
+                                     (apply str)
+                                     keyword))
+                              (filter valid-kw?))
 
         fields-ids       (map :field-id fields-fields)
 
@@ -259,9 +259,6 @@
                               (filter (complement (partial contains? (set breakout-ids))))
                               distinct)
 
-        ;; Get all the column name keywords returned by the results
-        result-kws       (set (keys (first results)))
-
         ;; Make a helper function that will take a sequence of Field IDs and convert them to corresponding column name keywords.
         ;; Don't include names that aren't part of RESULT-KWS: we fetch *all* the Fields for a Table regardless of the Query, so
         ;; there are likely some unused ones.
@@ -269,26 +266,34 @@
                            (some->> (map field-id->field field-ids)
                                     (map :name)
                                     (map keyword)
-                                    (filter (partial contains? result-kws))))
+                                    (filter valid-kw?)))
 
         ;; Use fn above to get the keyword column names of other non-aggregation fields [#3 and #4]
         non-breakout-kws (ids->kws non-breakout-ids)
 
-        ;; Now get all the keyword column names specific to aggregation, such as :sum or :count [#2].
-        ;; Just get all the items in RESULT-KWS that *aren't* part of BREAKOUT-KWS or NON-BREAKOUT-KWS
-        ag-kws           (->> result-kws
-                              ;; TODO - Currently, this will never be more than a single Field, since we only
-                              ;; support a single aggregation clause at this point. When we add support for
-                              ;; multiple aggregation clauses, we'll need to add some logic to make sure they're
-                              ;; being ordered correctly, e.g. the first aggregate column before the second, etc.
-                              (filter (complement (partial contains? (set (concat breakout-kws non-breakout-kws))))))]
-    (assert (<= (count ag-kws) 1)
-      (format "Invalid number of aggregate columns: %d" (count ag-kws)))
+        ;; Get the aggregate column if any
+        ag-kws           (when (and ag-type
+                                    (not= ag-type :rows))
+                           (let [ag (if (= ag-type :distinct) :count
+                                        ag-type)]
+                             [ag]))
+
+        ;; Collect all other Fields
+        other-kws        (->> result-kws
+                              (filter (complement (partial contains? (set (concat breakout-kws non-breakout-kws ag-kws)))))
+                              sort)] ; sort by name so results are deterministic
+
+    (when (seq other-kws)
+      (log/warn (u/format-color 'red "Warning: not 100%% sure how to order these columns: %s" (vec other-kws))))
 
     ;; Now combine the breakout [#1] + aggregate [#2] + "non-breakout" [#3 &  #4] column name keywords into a single sequence
     (when-not *disable-qp-logging*
-      (log/debug (u/format-color 'magenta "Using this ordering: breakout: %s, ag: %s, other: %s" (vec breakout-kws) (vec ag-kws) (vec non-breakout-kws))))
-    (concat breakout-kws ag-kws non-breakout-kws)))
+      (log/debug (u/format-color 'magenta "Using this ordering: breakout: %s, ag: %s, non-breakout: %s, other: %s"
+                                 (vec breakout-kws) (vec ag-kws) (vec non-breakout-kws) (vec other-kws))))
+    (let [ordered-kws (concat breakout-kws ag-kws non-breakout-kws other-kws)]
+      (assert (= (set ordered-kws) result-kws)
+        (format "Order-cols returned invalid results: expected %s, got %s" result-kws (set ordered-kws)))
+      ordered-kws)))
 
 (defn- add-fields-extra-info
   "Add `:extra_info` about `ForeignKeys` to `Fields` whose `special_type` is `:fk`."
diff --git a/test/metabase/driver/query_processor_test.clj b/test/metabase/driver/query_processor_test.clj
index 0aa87799d72..aee567b2021 100644
--- a/test/metabase/driver/query_processor_test.clj
+++ b/test/metabase/driver/query_processor_test.clj
@@ -912,22 +912,22 @@
 ;; (What he was doing when we saw him, sighting ID)
 ;; Check that we can include an FK field in the :fields clause
 (datasets/expect-with-datasets #{:h2 :postgres}
-  [["In the Park" 772]
-   ["Working at a Pet Store" 894]
-   ["At the Airport" 684]
-   ["At a Restaurant" 199]
-   ["Working as a Limo Driver" 33]
-   ["At Starbucks" 902]
-   ["On TV" 927]
-   ["At a Restaurant" 996]
-   ["Wearing a Biggie Shirt" 897]
-   ["In the Expa Office" 499]]
-  (->> (query-with-temp-db defs/tupac-sightings
-         :source_table &sightings:id
-         :fields       [&sightings.id:id ["fk->" &sightings.category_id:id &categories.name:id]]
-         :order_by     [[&sightings.timestamp:id "descending"]]
-         :limit        10)
-       :data :rows))
+  [[772 "In the Park"]
+   [894 "Working at a Pet Store"]
+   [684 "At the Airport"]
+   [199 "At a Restaurant"]
+   [33 "Working as a Limo Driver"]
+   [902 "At Starbucks"]
+   [927 "On TV"]
+   [996 "At a Restaurant"]
+   [897 "Wearing a Biggie Shirt"]
+   [499 "In the Expa Office"]]
+  (Q run against tupac-sightings
+     return :data :rows
+     of sightings
+     fields id category_id->categories.name
+     order timestamp-
+     lim 10))
 
 
 ;; 1. Check that we can order by Foreign Keys
-- 
GitLab