From 92ff768e576e448bad3a56234a60aaac5e8cbba4 Mon Sep 17 00:00:00 2001 From: Cam Saul <cam@geotip.com> Date: Wed, 29 Apr 2015 17:44:17 -0700 Subject: [PATCH] the first combined QP test --- .dir-locals.el | 2 + .../generic_sql/query_processor/annotate.clj | 37 +----- src/metabase/driver/mongo.clj | 16 +-- src/metabase/driver/mongo/query_processor.clj | 22 ++-- src/metabase/driver/mongo/util.clj | 22 +++- src/metabase/driver/query_processor.clj | 44 ++++++- src/metabase/models/field_values.clj | 6 +- .../generic_sql/query_processor_test.clj | 18 --- test/metabase/driver/mongo/test_data.clj | 9 +- test/metabase/driver/query_processor_test.clj | 124 ++++++++++++++++++ test/metabase/test_data.clj | 7 + 11 files changed, 222 insertions(+), 85 deletions(-) create mode 100644 test/metabase/driver/query_processor_test.clj diff --git a/.dir-locals.el b/.dir-locals.el index ac1a14bb216..ee8ad0ec4f3 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -19,6 +19,7 @@ (expect-eval-actual-first 1) (expect-expansion 0) (expect-let 1) + (expect-with-all-drivers 1) (expect-with-data-loaded 1) (ins 1) (let-400 1) @@ -29,6 +30,7 @@ (macrolet 1) (org-perms-case 1) (pdoseq 1) + (qp-expect-with-all-drivers 1) (symbol-macrolet 1) (sync-in-context 2) (upd 2) diff --git a/src/metabase/driver/generic_sql/query_processor/annotate.clj b/src/metabase/driver/generic_sql/query_processor/annotate.clj index 0d5cd9231ae..063d8e615a7 100644 --- a/src/metabase/driver/generic_sql/query_processor/annotate.clj +++ b/src/metabase/driver/generic_sql/query_processor/annotate.clj @@ -1,11 +1,11 @@ (ns metabase.driver.generic-sql.query-processor.annotate "Functions related to annotating results returned by the Query Processor." (:require [metabase.db :refer :all] + [metabase.driver.query-processor :as qp] [metabase.models.field :refer [Field field->fk-table]])) (declare get-column-names get-column-info - get-special-column-info uncastify) (defn annotate @@ -95,36 +95,7 @@ (or (second (re-find #"CAST\(([^\s]+) AS [\w]+\)" column-name)) column-name)) -(defn- get-column-info - "Get extra information about result columns. This is done by looking up matching `Fields` for the `Table` in QUERY or looking up - information about special columns such as `count` via `get-special-column-info`." +(defn get-column-info + "Wrapper for `metabase.driver.query-processor/get-column-info` that calls `uncastify` on column names." [query column-names] - (let [table-id (get-in query [:query :source_table]) - column-names (map uncastify column-names) - columns (->> (sel :many [Field :id :table_id :name :description :base_type :special_type] ; lookup columns with matching names for this Table - :table_id table-id :name [in (set column-names)]) - (map (fn [{:keys [name] :as column}] ; build map of column-name -> column - {name (-> (select-keys column [:id :table_id :name :description :base_type :special_type]) - (assoc :extra_info (if-let [fk-table (field->fk-table column)] - {:target_table_id (:id fk-table)} - {})))})) - (into {}))] - (->> column-names - (map (fn [column-name] - (or (columns column-name) ; try to get matching column from the map we build earlier - (get-special-column-info query column-name))))))) ; if it's not there then it's a special column like `count` - -(defn- get-special-column-info - "Get info like `:base_type` and `:special_type` for a special aggregation column like `count` or `sum`." - [query column-name] - (merge {:name column-name - :id nil - :table_id nil - :description nil} - (let [aggregation-type (keyword column-name) ; For aggregations of a specific Field (e.g. `sum`) - field-aggregation? (contains? #{:avg :stddev :sum} aggregation-type)] ; lookup the field we're aggregating and return its - (if field-aggregation? (sel :one :fields [Field :base_type :special_type] ; type info. (The type info of the aggregate result - :id (-> query :query :aggregation second)) ; will be the same.) - (case aggregation-type ; Otherwise for general aggregations such as `count` - :count {:base_type :IntegerField ; just return hardcoded type info - :special_type :number}))))) + (qp/get-column-info query (map uncastify column-names))) diff --git a/src/metabase/driver/mongo.clj b/src/metabase/driver/mongo.clj index 5181ce41de8..fadcfa50d28 100644 --- a/src/metabase/driver/mongo.clj +++ b/src/metabase/driver/mongo.clj @@ -13,7 +13,7 @@ [metabase.driver :as driver] [metabase.driver.interface :refer :all] (metabase.driver.mongo [query-processor :as qp] - [util :refer [*mongo-connection* with-mongo-connection]]))) + [util :refer [*mongo-connection* with-mongo-connection values->base-type]]))) (declare driver) @@ -29,22 +29,12 @@ (r/reduce set/union)))) (defn- field->base-type - "Determine the base type of FIELD in the most ghetto way possible. - This just gets counts the types of *every* value of FIELD and returns the class whose count was highest." + "Determine the base type of FIELD in the most ghetto way possible, via `values->base-type`." [field] {:pre [(map? field)] :post [(keyword? %)]} (with-mongo-connection [_ @(:db @(:table field))] - (or (->> (field-values-lazy-seq driver field) - (filter identity) - (group-by type) - (map (fn [[type valus]] - [type (count valus)])) - (sort-by second) - first - first - driver/class->base-type) - :UnknownField))) + (values->base-type (field-values-lazy-seq driver field)))) ;;; ## MongoDriver diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj index 9099953f666..94d4903e4c4 100644 --- a/src/metabase/driver/mongo/query_processor.clj +++ b/src/metabase/driver/mongo/query_processor.clj @@ -9,8 +9,8 @@ [query :refer :all]) [metabase.db :refer :all] [metabase.driver :as driver] - [metabase.driver.query-processor :refer [*query*]] - [metabase.driver.mongo.util :refer [with-mongo-connection *mongo-connection*]] + [metabase.driver.query-processor :as qp :refer [*query*]] + [metabase.driver.mongo.util :refer [with-mongo-connection *mongo-connection* values->base-type]] (metabase.models [database :refer [Database]] [field :refer [Field]] [table :refer [Table]])) @@ -146,23 +146,17 @@ (defn annotate-results [{:keys [source_table] :as query} results] {:pre [(integer? source_table)]} - (let [field-name->id (sel :many :field->id [Field :name] :table_id source_table) - column-names (keys (first results))] + (let [field-name->field (sel :many :field->obj [Field :name] :table_id source_table) + column-keys (keys (first results)) + column-names (map name column-keys)] {:row_count (count results) :status :completed :data {:columns column-names - :cols (map (fn [column-name] - {:name column-name - :id (field-name->id (name column-name)) - :table_id source_table - :description nil - :base_type :UnknownField - :special_type nil - :extra_info {}}) - column-names) - :rows (map #(map % column-names) + :cols (qp/get-column-info {:query query} column-names) + :rows (map #(map % column-keys) results)}})) + ;; ## CLAUSE APPLICATION 2.0 (def field-id->kw diff --git a/src/metabase/driver/mongo/util.clj b/src/metabase/driver/mongo/util.clj index c6c45078164..71876b88973 100644 --- a/src/metabase/driver/mongo/util.clj +++ b/src/metabase/driver/mongo/util.clj @@ -2,7 +2,8 @@ "`*mongo-connection*`, `with-mongo-connection`, and other functions shared between several Mongo driver namespaces." (:require [clojure.tools.logging :as log] [colorize.core :as color] - [monger.core :as mg])) + [monger.core :as mg] + [metabase.driver :as driver])) (def ^:dynamic *mongo-connection* "Connection to a Mongo database. @@ -41,3 +42,22 @@ ~@body)] (if *mongo-connection* (f# *mongo-connection*) (-with-mongo-connection f# ~database)))) + +;; TODO - this is actually more sophisticated than the one used for annotation in the GenericSQL driver, which just takes the +;; types of the values in the first row. +;; We should move this somewhere where it can be shared amongst the drivers and rewrite GenericSQL to use it instead. +(defn values->base-type + "Given a sequence of values, return `Field` `base_type` in the most ghetto way possible. + This just gets counts the types of *every* value and returns the `base_type` for class whose count was highest." + [values-seq] + {:pre [(sequential? values-seq)]} + (or (->> values-seq + (filter identity) + (group-by type) + (map (fn [[type valus]] + [type (count valus)])) + (sort-by second) + first + first + driver/class->base-type) + :UnknownField)) diff --git a/src/metabase/driver/query_processor.clj b/src/metabase/driver/query_processor.clj index 338a6b582b5..6ecdb14c17d 100644 --- a/src/metabase/driver/query_processor.clj +++ b/src/metabase/driver/query_processor.clj @@ -1,7 +1,10 @@ (ns metabase.driver.query-processor - "Preprocessor that does simple transformations to all incoming queries, simplifing the driver-specific implementations.") + "Preprocessor that does simple transformations to all incoming queries, simplifing the driver-specific implementations." + (:require [metabase.db :refer :all] + [metabase.models.field :refer [Field field->fk-table]])) (declare add-implicit-breakout-order-by + get-special-column-info preprocess-structured remove-empty-clauses) @@ -57,3 +60,42 @@ [field-id "ascending"])) (apply conj (or order-by-subclauses [])) (assoc query :order_by))))) + + +;; # COMMON ANNOTATION FNS + +(defn get-column-info + "Get extra information about result columns. This is done by looking up matching `Fields` for the `Table` in QUERY or looking up + information about special columns such as `count` via `get-special-column-info`." + [query column-names] + {:pre [(:query query)]} + (let [table-id (get-in query [:query :source_table]) + columns (->> (sel :many [Field :id :table_id :name :description :base_type :special_type] ; lookup columns with matching names for this Table + :table_id table-id :name [in (set column-names)]) + (map (fn [{:keys [name] :as column}] ; build map of column-name -> column + {name (-> (select-keys column [:id :table_id :name :description :base_type :special_type]) + (assoc :extra_info (if-let [fk-table (field->fk-table column)] + {:target_table_id (:id fk-table)} + {})))})) + (into {}))] + (->> column-names + (map (fn [column-name] + (or (columns column-name) ; try to get matching column from the map we build earlier + (get-special-column-info query column-name))))))) ; if it's not there then it's a special column like `count` + + +(defn get-special-column-info + "Get info like `:base_type` and `:special_type` for a special aggregation column like `count` or `sum`." + [query column-name] + {:pre [(:query query)]} + (merge {:name column-name + :id nil + :table_id nil + :description nil} + (let [aggregation-type (keyword column-name) ; For aggregations of a specific Field (e.g. `sum`) + field-aggregation? (contains? #{:avg :stddev :sum} aggregation-type)] ; lookup the field we're aggregating and return its + (if field-aggregation? (sel :one :fields [Field :base_type :special_type] ; type info. (The type info of the aggregate result + :id (-> query :query :aggregation second)) ; will be the same.) + (case aggregation-type ; Otherwise for general aggregations such as `count` + :count {:base_type :IntegerField ; just return hardcoded type info + :special_type :number}))))) diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj index c65191233d2..71439494707 100644 --- a/src/metabase/models/field_values.clj +++ b/src/metabase/models/field_values.clj @@ -2,7 +2,6 @@ (:require [clojure.tools.logging :as log] [korma.core :refer :all] [metabase.db :refer :all] - [metabase.db.metadata-queries :as metadata] [metabase.util :as u])) ;; ## Entity + DB Multimethods @@ -36,6 +35,9 @@ (or (contains? #{:category :city :state :country} (keyword special_type)) (= (keyword base_type) :BooleanField))) +(def ^:private field-distinct-values + (u/runtime-resolved-fn 'metabase.db.metadata-queries 'field-distinct-values)) + (defn create-field-values "Create `FieldValues` for a `Field`." {:arglists '([field] @@ -46,7 +48,7 @@ (log/debug (format "Creating FieldValues for Field %d..." field-id)) (ins FieldValues :field_id field-id - :values (metadata/field-distinct-values field) + :values (field-distinct-values field) :human_readable_values human-readable-values)) (defn create-field-values-if-needed diff --git a/test/metabase/driver/generic_sql/query_processor_test.clj b/test/metabase/driver/generic_sql/query_processor_test.clj index a78883a79a7..80789d91d2e 100644 --- a/test/metabase/driver/generic_sql/query_processor_test.clj +++ b/test/metabase/driver/generic_sql/query_processor_test.clj @@ -15,24 +15,6 @@ {:extra_info {} :special_type :latitude, :base_type :FloatField, :description nil, :name "LATITUDE", :table_id (table->id :venues), :id (field->id :venues :latitude)} {:extra_info {} :special_type nil, :base_type :TextField, :description nil, :name "NAME", :table_id (table->id :venues), :id (field->id :venues :name)}])) -;; ## "COUNT" AGGREGATION -(expect {:status :completed - :row_count 1 - :data {:rows [[100]] - :columns ["count"] - :cols [{:base_type :IntegerField - :special_type :number - :name "count" - :id nil - :table_id nil - :description nil}]}} - (driver/process-query {:type :query - :database @db-id - :query {:source_table (table->id :venues) - :filter [nil nil] - :aggregation ["count"] - :breakout [nil] - :limit nil}})) ;; ## "SUM" AGGREGATION (expect {:status :completed diff --git a/test/metabase/driver/mongo/test_data.clj b/test/metabase/driver/mongo/test_data.clj index 0d80c7623c1..eda1d5b6af7 100644 --- a/test/metabase/driver/mongo/test_data.clj +++ b/test/metabase/driver/mongo/test_data.clj @@ -1,6 +1,7 @@ (ns metabase.driver.mongo.test-data "Functionality related to creating / loading a test database for the Mongo driver." - (:require [colorize.core :as color] + (:require [clojure.tools.logging :as log] + [colorize.core :as color] [medley.core :as m] (monger [collection :as mc] [core :as mg]) @@ -10,7 +11,7 @@ (metabase.models [database :refer [Database]] [field :refer [Field]] [table :refer [Table]]) - [metabase.test-data :refer :all] + [metabase.test-data :refer [org-id]] [metabase.test-data.data :as data])) (declare load-data) @@ -37,8 +38,10 @@ :name mongo-test-db-name :engine :mongo :details {:conn_str mongo-test-db-conn-str})] + (log/debug (color/cyan "Loading Mongo test data...")) (load-data) (driver/sync-database! db) + (log/debug (color/cyan "Done.")) db)))) (def mongo-test-db-id @@ -108,4 +111,4 @@ (mc/insert mongo-db (name collection) (assoc (zipmap fields row) :_id (inc i))) (catch com.mongodb.MongoException$DuplicateKey _))) - (println (color/cyan (format "Loaded data for collection '%s'." (name collection)))))))) + (log/debug (color/cyan (format "Loaded data for collection '%s'." (name collection)))))))) diff --git a/test/metabase/driver/query_processor_test.clj b/test/metabase/driver/query_processor_test.clj new file mode 100644 index 00000000000..1f1e4bd40c7 --- /dev/null +++ b/test/metabase/driver/query_processor_test.clj @@ -0,0 +1,124 @@ +(ns metabase.driver.query-processor-test + "Query processing tests that can be ran between any of the available drivers, and should give the same results." + (:require [expectations :refer :all] + [metabase.db :refer :all] + [metabase.driver :as driver] + [metabase.driver.mongo.test-data :as mongo-data] + (metabase.models [table :refer [Table]]) + [metabase.test-data :as generic-sql-data])) + +;; ## Functionality for writing driver-independent tests + +(def ^:dynamic *db* "Bound to `Database` for the current driver inside body of `with-driver`.") +(def ^:dynamic *db-id* "Bound to ID of `Database` for the current driver inside body of `with-driver`.") +(def ^:dynamic *driver-data*) + +(defprotocol DriverData + (db [this] + "Return `Database` containing test data for this driver.") + (table-name->table [this table-name] + "Given a TABLE-NAME keyword like `:venues`, *fetch* the corresponding `Table`.")) + +(deftype MongoDriverData [] + DriverData + (db [_] + @mongo-data/mongo-test-db) + (table-name->table [_ table-name] + (mongo-data/table-name->table table-name))) + +(deftype GenericSqlDriverData [] + DriverData + (db [_] + @generic-sql-data/test-db) + (table-name->table [_ table-name] + (generic-sql-data/table-name->table table-name))) + +(def driver-name->driver-data + {:mongo (MongoDriverData.) + :generic-sql (GenericSqlDriverData.)}) + +(def driver-name->db-delay + "Map of `driver-name` to a delay that will return the corresponding `Database`." + {:mongo mongo-data/mongo-test-db + :generic-sql generic-sql-data/test-db}) +(def valid-driver-names (set (keys driver-name->db-delay))) + +(defmacro with-driver + "Execute BODY with `*db*` and `*db-id*` bound to appropriate places for " + [driver-name & body] + {:pre [(keyword? driver-name) + (contains? valid-driver-names driver-name)]} + `(let [driver-data# (driver-name->driver-data ~driver-name) + db# (db driver-data#)] + (binding [*driver-data* driver-data# + *db* db# + *db-id* (:id db#)] + (assert (integer? *db-id*)) + ~@body))) + +(defmacro expect-with-all-drivers + "Like expect, but runs a test inside of `with-driver` for *each* of the available drivers." + [expected actual] + `(do ~@(mapcat (fn [driver-name] + `[(expect + (with-driver ~driver-name + ~expected) + (with-driver ~driver-name + ~actual))]) + valid-driver-names))) + + +;; ## Driver-Independent Data Fns + +(defn ->table + "Given keyword TABLE-NAME, fetch corresponding `Table` in `*db*`." + [table-name] + {:pre [*driver-data*] + :post [(map? %)]} + (table-name->table *driver-data* table-name)) + +(defn ->table-id [table-name] + {:pre [*driver-data*] + :post [(integer? %)]} + (:id (->table table-name))) + +#_(def ^{:arglists '([table-name])} table-name->id + "Given keyword TABLE-NAME, fetch ID of corresponding `Table` in `*db*`." + (let [-table-name->id (memoize (fn [db-id table-name] + {:pre [(integer? db-id) + (keyword? table-name)] + :post [(integer? %)]} + (sel :one :id Table :db_id db-id :name (name table-name))))] + (fn [table-name] + {:pre [(integer? *db-id*)]} + (-table-name->id *db-id* table-name)))) + + +;; ## Driver-Independent QP Tests + +(defmacro qp-expect-with-all-drivers + "Slightly more succinct way of writing QP tests. Adds standard boilerplate to actual/expected forms." + [data query] + `(expect-with-all-drivers + {:status :completed + :row_count ~(count (:rows data)) + :data ~data} + (driver/process-query {:type :query + :database *db-id* + :query ~query}))) + +;; ### "COUNT" AGGREGATION +(qp-expect-with-all-drivers + {:rows [[100]] + :columns ["count"] + :cols [{:base_type :IntegerField + :special_type :number + :name "count" + :id nil + :table_id nil + :description nil}]} + {:source_table (->table-id :venues) + :filter [nil nil] + :aggregation ["count"] + :breakout [nil] + :limit nil}) diff --git a/test/metabase/test_data.clj b/test/metabase/test_data.clj index 88e2b01b0a1..15e66cea811 100644 --- a/test/metabase/test_data.clj +++ b/test/metabase/test_data.clj @@ -75,6 +75,13 @@ (not (zero? %))]} (@tables table-name)) +(defn table-name->table + "Fetch `Table` with TABLE-NAME." + [table-name] + {:pre [(keyword? table-name)] + :post [(map? %)]} + (sel :one Table :id (table->id table-name))) + ;; ## Test Organization -- GitLab