Skip to content
Snippets Groups Projects
Commit 1845feb2 authored by Sameer Al-Sakran's avatar Sameer Al-Sakran
Browse files

Merge branch 'release-0.21.1'

parents 60ac2473 272450eb
No related branches found
No related tags found
No related merge requests found
...@@ -74,8 +74,8 @@ export default class PinMap extends Component { ...@@ -74,8 +74,8 @@ export default class PinMap extends Component {
const latitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.latitude_column"]); const latitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.latitude_column"]);
const longitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.longitude_column"]); const longitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.longitude_column"]);
const points = rows.map(row => [ const points = rows.map(row => [
row[longitudeIndex], row[latitudeIndex],
row[latitudeIndex] row[longitudeIndex]
]); ]);
const bounds = L.latLngBounds(points); const bounds = L.latLngBounds(points);
return { points, bounds }; return { points, bounds };
......
...@@ -278,6 +278,10 @@ function applyChartTooltips(chart, series, onHoverChange) { ...@@ -278,6 +278,10 @@ function applyChartTooltips(chart, series, onHoverChange) {
{ key: getFriendlyName(cols[index]), value: value, col: cols[index] } { key: getFriendlyName(cols[index]), value: value, col: cols[index] }
)); ));
} else if (d.data) { // line, area, bar } else if (d.data) { // line, area, bar
if (!isSingleSeriesBar) {
let idx = determineSeriesIndexFromElement(this);
cols = series[idx].data.cols;
}
data = [ data = [
{ key: getFriendlyName(cols[0]), value: d.data.key, col: cols[0] }, { key: getFriendlyName(cols[0]), value: d.data.key, col: cols[0] },
{ key: getFriendlyName(cols[1]), value: d.data.value, col: cols[1] } { key: getFriendlyName(cols[1]), value: d.data.value, col: cols[1] }
...@@ -711,7 +715,8 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender, ...@@ -711,7 +715,8 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
if (isTimeseries) { if (isTimeseries) {
// replace xValues with // replace xValues with
xValues = d3.time[xInterval.interval] xValues = d3.time[xInterval.interval]
.range(xDomain[0], moment(xDomain[1]).add(1, "ms"), xInterval.count); .range(xDomain[0], moment(xDomain[1]).add(1, "ms"), xInterval.count)
.map(d => moment(d));
datas = fillMissingValues( datas = fillMissingValues(
datas, datas,
xValues, xValues,
...@@ -802,7 +807,9 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender, ...@@ -802,7 +807,9 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
let yExtents = groups.map(group => d3.extent(group[0].all(), d => d.value)); let yExtents = groups.map(group => d3.extent(group[0].all(), d => d.value));
let yExtent = d3.extent([].concat(...yExtents)); let yExtent = d3.extent([].concat(...yExtents));
if (!isScalarSeries && !isScatter && !isStacked && settings["graph.y_axis.auto_split"] !== false) { // don't auto-split if the metric columns are all identical, i.e. it's a breakout multiseries
const hasDifferentYAxisColumns = _.uniq(series.map(s => s.data.cols[1])).length > 1;
if (!isScalarSeries && !isScatter && !isStacked && hasDifferentYAxisColumns && settings["graph.y_axis.auto_split"] !== false) {
yAxisSplit = computeSplit(yExtents); yAxisSplit = computeSplit(yExtents);
} else { } else {
yAxisSplit = [series.map((s,i) => i)]; yAxisSplit = [series.map((s,i) => i)];
......
...@@ -93,6 +93,8 @@ ...@@ -93,6 +93,8 @@
"INTEGER" :type/Integer "INTEGER" :type/Integer
"RECORD" :type/Dictionary ; RECORD -> field has a nested schema "RECORD" :type/Dictionary ; RECORD -> field has a nested schema
"STRING" :type/Text "STRING" :type/Text
"DATE" :type/Date
"DATETIME" :type/DateTime
"TIMESTAMP" :type/DateTime}) "TIMESTAMP" :type/DateTime})
(defn- table-schema->metabase-field-info [^TableSchema schema] (defn- table-schema->metabase-field-info [^TableSchema schema]
...@@ -106,7 +108,7 @@ ...@@ -106,7 +108,7 @@
:fields (set (table-schema->metabase-field-info (.getSchema (get-table database table-name))))}) :fields (set (table-schema->metabase-field-info (.getSchema (get-table database table-name))))})
(def ^:private ^:const query-timeout-seconds 60) (def ^:private ^:const ^Integer query-timeout-seconds 60)
(defn- ^QueryResponse execute-bigquery (defn- ^QueryResponse execute-bigquery
([{{:keys [project-id]} :details, :as database} query-string] ([{{:keys [project-id]} :details, :as database} query-string]
...@@ -142,6 +144,8 @@ ...@@ -142,6 +144,8 @@
"INTEGER" #(Long/parseLong %) "INTEGER" #(Long/parseLong %)
"RECORD" identity "RECORD" identity
"STRING" identity "STRING" identity
"DATE" parse-timestamp-str
"DATETIME" parse-timestamp-str
"TIMESTAMP" parse-timestamp-str}) "TIMESTAMP" parse-timestamp-str})
(defn- post-process-native (defn- post-process-native
...@@ -236,6 +240,9 @@ ...@@ -236,6 +240,9 @@
:quarter-of-year (hx/quarter expr) :quarter-of-year (hx/quarter expr)
:year (hx/year expr))) :year (hx/year expr)))
(defn- date-string->literal [^String date-string]
(hx/->timestamp (hx/literal (u/format-date "yyyy-MM-dd 00:00" (u/->Date date-string)))))
(defn- unix-timestamp->timestamp [expr seconds-or-milliseconds] (defn- unix-timestamp->timestamp [expr seconds-or-milliseconds]
(case seconds-or-milliseconds (case seconds-or-milliseconds
:seconds (hsql/call :sec_to_timestamp expr) :seconds (hsql/call :sec_to_timestamp expr)
...@@ -326,6 +333,12 @@ ...@@ -326,6 +333,12 @@
ag-type))) ag-type)))
:else (str schema-name \. table-name \. field-name))) :else (str schema-name \. table-name \. field-name)))
;; TODO - Making 2 DB calls for each field to fetch its dataset is inefficient and makes me cry, but this method is currently only used for SQL params so it's not a huge deal at this point
(defn- field->identifier [{table-id :table_id, :as field}]
(let [db-id (db/select-one-field :db_id 'Table :id table-id)
dataset (:dataset-id (db/select-one-field :details Database, :id db-id))]
(hsql/raw (apply format "[%s.%s.%s]" dataset (field/qualified-name-components field)))))
;; We have to override the default SQL implementations of breakout and order-by because BigQuery propogates casting functions in SELECT ;; We have to override the default SQL implementations of breakout and order-by because BigQuery propogates casting functions in SELECT
;; BAD: ;; BAD:
;; SELECT msec_to_timestamp([sad_toucan_incidents.incidents.timestamp]) AS [sad_toucan_incidents.incidents.timestamp], count(*) AS [count] ;; SELECT msec_to_timestamp([sad_toucan_incidents.incidents.timestamp]) AS [sad_toucan_incidents.incidents.timestamp], count(*) AS [count]
...@@ -381,9 +394,11 @@ ...@@ -381,9 +394,11 @@
:connection-details->spec (constantly nil) ; since we don't use JDBC :connection-details->spec (constantly nil) ; since we don't use JDBC
:current-datetime-fn (constantly :%current_timestamp) :current-datetime-fn (constantly :%current_timestamp)
:date (u/drop-first-arg date) :date (u/drop-first-arg date)
:date-string->literal (u/drop-first-arg date-string->literal)
:field->alias (u/drop-first-arg field->alias) :field->alias (u/drop-first-arg field->alias)
:field->identifier (u/drop-first-arg field->identifier)
:prepare-value (u/drop-first-arg prepare-value) :prepare-value (u/drop-first-arg prepare-value)
:quote-style (constantly :sqlserver) ; we want identifiers quoted [like].[this] :quote-style (constantly :sqlserver) ; we want identifiers quoted [like].[this] initially (we have to convert them to [like.this] before executing)
:string-length-fn (u/drop-first-arg string-length-fn) :string-length-fn (u/drop-first-arg string-length-fn)
:unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}) :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)})
...@@ -419,9 +434,15 @@ ...@@ -419,9 +434,15 @@
;; people can manually specifiy "foreign key" relationships in admin and everything should work correctly. ;; people can manually specifiy "foreign key" relationships in admin and everything should work correctly.
;; Since we can't infer any "FK" relationships during sync our normal FK tests are not appropriate for BigQuery, so they're disabled for the time being. ;; Since we can't infer any "FK" relationships during sync our normal FK tests are not appropriate for BigQuery, so they're disabled for the time being.
;; TODO - either write BigQuery-speciifc tests for FK functionality or add additional code to manually set up these FK relationships for FK tables ;; TODO - either write BigQuery-speciifc tests for FK functionality or add additional code to manually set up these FK relationships for FK tables
:features (constantly (when-not config/is-test? :features (constantly (set/union #{:basic-aggregations
;; during unit tests don't treat bigquery as having FK support :standard-deviation-aggregations
#{:foreign-keys})) :native-parameters
;; Expression aggregations *would* work, but BigQuery doesn't support the auto-generated column names. BQ column names
;; can only be alphanumeric or underscores. If we slugified the auto-generated column names, we could enable this feature.
#_:expression-aggregations}
(when-not config/is-test?
;; during unit tests don't treat bigquery as having FK support
#{:foreign-keys})))
:field-values-lazy-seq (u/drop-first-arg field-values-lazy-seq) :field-values-lazy-seq (u/drop-first-arg field-values-lazy-seq)
:mbql->native (u/drop-first-arg mbql->native)})) :mbql->native (u/drop-first-arg mbql->native)}))
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
java.util.Map java.util.Map
(clojure.lang Keyword PersistentVector) (clojure.lang Keyword PersistentVector)
com.mchange.v2.c3p0.ComboPooledDataSource com.mchange.v2.c3p0.ComboPooledDataSource
metabase.models.field.FieldInstance
(metabase.query_processor.interface Field Value))) (metabase.query_processor.interface Field Value)))
(defprotocol ISQLDriver (defprotocol ISQLDriver
...@@ -58,9 +59,20 @@ ...@@ -58,9 +59,20 @@
(date [this, ^Keyword unit, field-or-value] (date [this, ^Keyword unit, field-or-value]
"Return a HoneySQL form for truncating a date or timestamp field or value to a given resolution, or extracting a date component.") "Return a HoneySQL form for truncating a date or timestamp field or value to a given resolution, or extracting a date component.")
(date-string->literal [this, ^String date-string]
"*OPTIONAL*. Return an appropriate HoneySQL form to represent a DATE-STRING literal.
The default implementation is just `hx/literal`; in other words, it just single-quotes DATE-STRING. Some drivers like BigQuery or Oracle need to do something more advanced.
(This is used for the implementation of SQL parameters).")
(excluded-schemas ^java.util.Set [this] (excluded-schemas ^java.util.Set [this]
"*OPTIONAL*. Set of string names of schemas to skip syncing tables from.") "*OPTIONAL*. Set of string names of schemas to skip syncing tables from.")
(field->identifier [this, ^FieldInstance field]
"*OPTIONAL*. Return a HoneySQL form that should be used as the identifier for FIELD.
The default implementation returns a keyword generated by from the components returned by `field/qualified-name-components`.
Other drivers like BigQuery need to do additional qualification, e.g. the dataset name as well.
(At the time of this writing, this is only used by the SQL parameters implementation; in the future it will probably be used in more places as well.)")
(field-percent-urls [this field] (field-percent-urls [this field]
"*OPTIONAL*. Implementation of the `:field-percent-urls-fn` to be passed to `make-analyze-table`. "*OPTIONAL*. Implementation of the `:field-percent-urls-fn` to be passed to `make-analyze-table`.
The default implementation is `fast-field-percent-urls`, which avoids a full table scan. Substitue this with `slow-field-percent-urls` for databases The default implementation is `fast-field-percent-urls`, which avoids a full table scan. Substitue this with `slow-field-percent-urls` for databases
...@@ -203,6 +215,7 @@ ...@@ -203,6 +215,7 @@
([table field] ([table field]
(hx/qualify-and-escape-dots (:schema table) (:name table) (:name field)))) (hx/qualify-and-escape-dots (:schema table) (:name table) (:name field))))
(defn- query (defn- query
"Execute a HONEYSQL-FROM query against DATABASE, DRIVER, and optionally TABLE." "Execute a HONEYSQL-FROM query against DATABASE, DRIVER, and optionally TABLE."
([driver database honeysql-form] ([driver database honeysql-form]
...@@ -412,7 +425,9 @@ ...@@ -412,7 +425,9 @@
:apply-page (resolve 'metabase.driver.generic-sql.query-processor/apply-page) :apply-page (resolve 'metabase.driver.generic-sql.query-processor/apply-page)
:column->special-type (constantly nil) :column->special-type (constantly nil)
:current-datetime-fn (constantly :%now) :current-datetime-fn (constantly :%now)
:date-string->literal (u/drop-first-arg hx/literal)
:excluded-schemas (constantly nil) :excluded-schemas (constantly nil)
:field->identifier (u/drop-first-arg (comp (partial apply hsql/qualify) field/qualified-name-components))
:field->alias (u/drop-first-arg name) :field->alias (u/drop-first-arg name)
:field-percent-urls fast-field-percent-urls :field-percent-urls fast-field-percent-urls
:prepare-value (u/drop-first-arg :value) :prepare-value (u/drop-first-arg :value)
......
...@@ -86,6 +86,11 @@ ...@@ -86,6 +86,11 @@
3) 3)
:year (hsql/call :extract :year v))) :year (hsql/call :extract :year v)))
(defn- date-string->literal [^String date-string]
(hsql/call :to_timestamp
(hx/literal (u/format-date "yyyy-MM-dd" (u/->Date date-string)))
(hx/literal "YYYY-MM-DD")))
(def ^:private ^:const now (hsql/raw "SYSDATE")) (def ^:private ^:const now (hsql/raw "SYSDATE"))
(def ^:private ^:const date-1970-01-01 (hsql/call :to_timestamp (hx/literal :1970-01-01) (hx/literal :YYYY-MM-DD))) (def ^:private ^:const date-1970-01-01 (hsql/call :to_timestamp (hx/literal :1970-01-01) (hx/literal :YYYY-MM-DD)))
...@@ -218,6 +223,7 @@ ...@@ -218,6 +223,7 @@
:connection-details->spec (u/drop-first-arg connection-details->spec) :connection-details->spec (u/drop-first-arg connection-details->spec)
:current-datetime-fn (constantly now) :current-datetime-fn (constantly now)
:date (u/drop-first-arg date) :date (u/drop-first-arg date)
:date-string->literal (u/drop-first-arg date-string->literal)
:excluded-schemas (fn [& _] :excluded-schemas (fn [& _]
(set/union (set/union
#{"ANONYMOUS" #{"ANONYMOUS"
......
...@@ -46,21 +46,10 @@ ...@@ -46,21 +46,10 @@
(first (hsql/format x (first (hsql/format x
:quoting ((resolve 'metabase.driver.generic-sql/quote-style) *driver*)))) :quoting ((resolve 'metabase.driver.generic-sql/quote-style) *driver*))))
(defn- format-oracle-date [s] (defn- format-date-string
(format "to_timestamp('%s', 'YYYY-MM-DD')" (u/format-date "yyyy-MM-dd" (u/->Date s)))) "Format DATE-STRING as an appropriate literal using the driver's definition of `date-string->literal`."
^String [^String date-string]
(defn- oracle-driver? ^Boolean [] (honeysql->sql ((resolve 'metabase.driver.generic-sql/date-string->literal) *driver* date-string)))
;; we can't just import OracleDriver the normal way here because that would cause a cyclic load dependency
(boolean (when-let [oracle-driver-class (u/ignore-exceptions (Class/forName "metabase.driver.oracle.OracleDriver"))]
(instance? oracle-driver-class *driver*))))
(defn- format-date
;; This is a dirty dirty HACK! Unfortuantely Oracle is super-dumb when it comes to automatically converting strings to dates
;; so we need to add the cast here
[date]
(if (oracle-driver?)
(format-oracle-date date)
(str \' date \')))
(extend-protocol ISQLParamSubstituion (extend-protocol ISQLParamSubstituion
nil (->sql [_] "NULL") nil (->sql [_] "NULL")
...@@ -73,20 +62,20 @@ ...@@ -73,20 +62,20 @@
FieldInstance FieldInstance
(->sql [this] (->sql [this]
(->sql (let [identifier (apply hsql/qualify (field/qualified-name-components this))] (->sql (let [identifier ((resolve 'metabase.driver.generic-sql/field->identifier) *driver* this)]
(if (re-find #"^date/" (:type this)) (if (re-find #"^date/" (:type this))
((resolve 'metabase.driver.generic-sql/date) *driver* :day identifier) ((resolve 'metabase.driver.generic-sql/date) *driver* :day identifier)
identifier)))) identifier))))
Date Date
(->sql [{:keys [s]}] (->sql [{:keys [s]}]
(format-date s)) (format-date-string s))
DateRange DateRange
(->sql [{:keys [start end]}] (->sql [{:keys [start end]}]
(if (= start end) (if (= start end)
(format "= %s" (format-date start)) (format "= %s" (format-date-string start))
(format "BETWEEN %s AND %s" (format-date start) (format-date end)))) (format "BETWEEN %s AND %s" (format-date-string start) (format-date-string end))))
Dimension Dimension
(->sql [{:keys [field param], :as dimension}] (->sql [{:keys [field param], :as dimension}]
...@@ -175,10 +164,10 @@ ...@@ -175,10 +164,10 @@
(defn- parse-value-for-type [param-type value] (defn- parse-value-for-type [param-type value]
(cond (cond
(= param-type "number") (->NumberValue value) (= param-type "number") (->NumberValue value)
(and (= param-type "dimension") (and (= param-type "dimension")
(= (get-in value [:param :type]) "number")) (update-in value [:param :value] ->NumberValue) (= (get-in value [:param :type]) "number")) (update-in value [:param :value] ->NumberValue)
:else value)) :else value))
(defn- value-for-tag (defn- value-for-tag
"Given a map TAG (a value in the `:template_tags` dictionary) return the corresponding value from the PARAMS sequence. "Given a map TAG (a value in the `:template_tags` dictionary) return the corresponding value from the PARAMS sequence.
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
{:bootstrap_json (json/generate-string (public-settings/public-settings))}) {:bootstrap_json (json/generate-string (public-settings/public-settings))})
(slurp (io/resource "frontend_client/init.html"))) (slurp (io/resource "frontend_client/init.html")))
resp/response resp/response
(resp/content-type "text/html"))) (resp/content-type "text/html; charset=utf-8")))
;; Redirect naughty users who try to visit a page other than setup if setup is not yet complete ;; Redirect naughty users who try to visit a page other than setup if setup is not yet complete
(defroutes ^{:doc "Top-level ring routes for Metabase."} routes (defroutes ^{:doc "Top-level ring routes for Metabase."} routes
......
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
[metabase.test.data.datasets :as datasets] [metabase.test.data.datasets :as datasets]
[metabase.test.data.generic-sql :as generic-sql] [metabase.test.data.generic-sql :as generic-sql]
[metabase.test.util :as tu] [metabase.test.util :as tu]
[metabase.test.data.generic-sql :as generic])) [metabase.test.data.generic-sql :as generic]
[metabase.util :as u]))
;;; ------------------------------------------------------------ simple substitution -- {{x}} ------------------------------------------------------------ ;;; ------------------------------------------------------------ simple substitution -- {{x}} ------------------------------------------------------------
...@@ -351,12 +352,15 @@ ...@@ -351,12 +352,15 @@
(generic-sql/quote-name datasets/*driver* identifier)) (generic-sql/quote-name datasets/*driver* identifier))
(defn- checkins-identifier [] (defn- checkins-identifier []
(let [{table-name :name, schema :schema} (db/select-one ['Table :name :schema], :id (data/id :checkins))] ;; HACK ! I don't have all day to write protocol methods to make this work the "right" way so for BigQuery we will just hackily return the correct identifier here
(str (when (seq schema) (if (= datasets/*engine* :bigquery)
(str (quote-name schema) \.)) "[test_data.checkins]"
(quote-name table-name)))) (let [{table-name :name, schema :schema} (db/select-one ['Table :name :schema], :id (data/id :checkins))]
(str (when (seq schema)
;; as with the MBQL parameters tests redshift and crate fail for unknown reasons; disable their tests for now (str (quote-name schema) \.))
(quote-name table-name)))))
;; as with the MBQL parameters tests Redshift and Crate fail for unknown reasons; disable their tests for now
(def ^:private ^:const sql-parameters-engines (def ^:private ^:const sql-parameters-engines
(set/difference (engines-that-support :native-parameters) #{:redshift :crate})) (set/difference (engines-that-support :native-parameters) #{:redshift :crate}))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment