diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj index 8e41b4eb61eca5b1f9d32c00d4fe337f17eb0953..7f96368d0f988256384549d397884e082048a7d5 100644 --- a/src/metabase/public_settings.clj +++ b/src/metabase/public_settings.clj @@ -942,3 +942,10 @@ See [fonts](../configuring-metabase/fonts.md).") :export? false :default true :type :boolean) + +(defsetting download-row-limit + (deferred-tru "Exports row limit excluding the header. xlsx downloads are limited to 1048575 rows even if this limit is higher.") + :visibility :internal + :export? true + :type :integer + :doc false) diff --git a/src/metabase/pulse/util.clj b/src/metabase/pulse/util.clj index 68f268e904aff9c6388ade55b19fc99df425e348..dc86765593972252cfceeea509af1c3b227dabe0 100644 --- a/src/metabase/pulse/util.clj +++ b/src/metabase/pulse/util.clj @@ -27,10 +27,11 @@ process-query (fn [] (binding [qp.perms/*card-id* card-id] (qp/process-query - (qp/userland-query-with-default-constraints - (assoc query :middleware {:skip-results-metadata? true - :process-viz-settings? true - :js-int-to-string? false}) + (qp/userland-query + (assoc query :middleware {:skip-results-metadata? true + :process-viz-settings? true + :js-int-to-string? false + :add-default-userland-constraints? false}) (merge (cond-> {:executed-by pulse-creator-id :context :pulse :card-id card-id} @@ -75,19 +76,21 @@ :dashcard dashcard :type :card :result (qp.dashboard/process-query-for-dashcard - :dashboard-id dashboard-id - :card-id card-id - :dashcard-id (u/the-id dashcard) - :context :dashboard-subscription - :export-format :api - :parameters parameters - :middleware {:process-viz-settings? true - :js-int-to-string? false} - :make-run (fn make-run [qp _export-format] - (^:once fn* [query info] - (qp - (qp/userland-query-with-default-constraints query info) - nil))))}) + :dashboard-id dashboard-id + :card-id card-id + :dashcard-id (u/the-id dashcard) + :context :dashboard-subscription + :export-format :api + :parameters parameters + :constraints {} + :middleware {:process-viz-settings? true + :js-int-to-string? false + :add-default-userland-constraints? false} + :make-run (fn make-run [qp _export-format] + (^:once fn* [query info] + (qp + (qp/userland-query query info) + nil))))}) result (result-fn card-id) series-results (map (comp result-fn :id) multi-cards)] (when-not (and (get-in dashcard [:visualization_settings :card.hide_empty]) diff --git a/src/metabase/query_processor/middleware/limit.clj b/src/metabase/query_processor/middleware/limit.clj index 45c9bb8d61279d2ba601c848f8161d51d4d58a67..9027554fa1134c3c4899f65ba36bc870fcbedf8b 100644 --- a/src/metabase/query_processor/middleware/limit.clj +++ b/src/metabase/query_processor/middleware/limit.clj @@ -2,6 +2,7 @@ "Middleware that handles limiting the maximum number of rows returned by a query." (:require [metabase.legacy-mbql.util :as mbql.u] + [metabase.public-settings :as public-settings] [metabase.query-processor.interface :as qp.i] [metabase.query-processor.util :as qp.util])) @@ -23,14 +24,25 @@ (qp.util/query-without-aggregations-or-limits? query)) (update :query assoc :limit max-rows, ::original-limit original-limit))) +(defn- xlsx-export? + [& {info :info}] + (let [context (:context info)] + (= context :xlsx-download))) + (defn determine-query-max-rows "Given a `query`, return the max rows that should be returned. This is either: 1. the output of [[metabase.legacy-mbql.util/query->max-rows-limit]] when called on the given query - 2. [[metabase.query-processor.interface/absolute-max-results]] (a constant, non-nil backstop value)" + 2. the value of `pubic-settings/download-row-limit` + a. if it is less than [[metabase.query-processor.interface/absolute-max-results]] or + b. if it is greater and the export is NOT xlsx otherwise, + 3. [[metabase.query-processor.interface/absolute-max-results]] (a constant, non-nil backstop value)" [query] (when-not (disable-max-results? query) - (or (mbql.u/query->max-rows-limit query) - qp.i/absolute-max-results))) + (cond-> (or (mbql.u/query->max-rows-limit query) + (public-settings/download-row-limit) + qp.i/absolute-max-results) + (xlsx-export? query) + (min qp.i/absolute-max-results)))) (defn add-default-limit "Pre-processing middleware. Add default `:limit` to MBQL queries without any aggregations." diff --git a/test/metabase/api/downloads_exports_test.clj b/test/metabase/api/downloads_exports_test.clj index bd5bb8aef9158951865e94980a69d497f7c849f5..a921a43d9aa9cf079bbdc8967b5dfb223404dfd6 100644 --- a/test/metabase/api/downloads_exports_test.clj +++ b/test/metabase/api/downloads_exports_test.clj @@ -15,6 +15,7 @@ [clojure.set :as set] [clojure.test :refer :all] [dk.ative.docjure.spreadsheet :as spreadsheet] + [metabase.public-settings :as public-settings] [metabase.pulse :as pulse] [metabase.pulse.test-util :as pulse.test-util] [metabase.query-processor.streaming.csv :as qp.csv] @@ -151,6 +152,15 @@ :user_id (mt/user->id :rasta)}] (subscription-attachment* pulse))))) +(defn all-outputs! + [card-or-dashcard export-format format-rows?] + (merge + (when-not (contains? card-or-dashcard :dashboard_id) + {:card-download (card-download card-or-dashcard export-format format-rows?) + :alert-attachment (alert-attachment! card-or-dashcard export-format format-rows?)}) + {:dashcard-download (card-download card-or-dashcard export-format format-rows?) + :subscription-attachment (subscription-attachment! card-or-dashcard export-format format-rows?)})) + (set! *warn-on-reflection* true) (def ^:private pivot-rows-query @@ -618,6 +628,33 @@ {:alert-attachment (first alert-result) :subscription-attachment (first subscription-result)})))))))) +(deftest downloads-row-limit-test + (testing "Downloads row limit works." + (mt/with-temporary-setting-values [public-settings/download-row-limit 1050000] + (mt/dataset test-data + (mt/with-temp [:model/Card card {:display :table + :dataset_query {:database (mt/id) + :type :native + :native {:query "SELECT 1 as A FROM generate_series(1,1100000);"}}}] + (let [results (all-outputs! card :csv true)] + (is (= {:card-download 1050001 + :alert-attachment 1050001 + :dashcard-download 1050001 + :subscription-attachment 1050001} + (update-vals results count)))))))) + (testing "Downloads row limit default works." + (mt/dataset test-data + (mt/with-temp [:model/Card card {:display :table + :dataset_query {:database (mt/id) + :type :native + :native {:query "SELECT 1 as A FROM generate_series(1,1100000);"}}}] + (let [results (all-outputs! card :csv true)] + (is (= {:card-download 1048576 + :alert-attachment 1048576 + :dashcard-download 1048576 + :subscription-attachment 1048576} + (update-vals results count)))))))) + (deftest ^:parallel model-viz-settings-downloads-test (testing "A model's visualization settings are respected in downloads." (testing "for csv" diff --git a/test/metabase/pulse_test.clj b/test/metabase/pulse_test.clj index bb2d63dc3a470593600416a0a722e68145104d64..724a065a35918857ec908ac13656755b765c7fd3 100644 --- a/test/metabase/pulse_test.clj +++ b/test/metabase/pulse_test.clj @@ -12,11 +12,11 @@ [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] [metabase.models.pulse :as pulse] + [metabase.public-settings :as public-settings] [metabase.pulse] [metabase.pulse.render :as render] [metabase.pulse.render.body :as body] [metabase.pulse.test-util :as pulse.test-util] - [metabase.query-processor.middleware.constraints :as qp.constraints] [metabase.test :as mt] [metabase.test.util :as tu] [metabase.util :as u] @@ -325,8 +325,7 @@ :fixture (fn [_ thunk] - (with-redefs [qp.constraints/default-query-constraints (constantly {:max-results 10000 - :max-results-bare-rows 30})] + (mt/with-temporary-setting-values [public-settings/download-row-limit 30] (thunk))) :pulse-card {:include_csv true} :assert diff --git a/test/metabase/query_processor/middleware/limit_test.clj b/test/metabase/query_processor/middleware/limit_test.clj index ea03e7f739ae816d276af89da68518e400b70369..e70f4a925dabe1e4538aeb8316f78e17283d6648 100644 --- a/test/metabase/query_processor/middleware/limit_test.clj +++ b/test/metabase/query_processor/middleware/limit_test.clj @@ -60,3 +60,59 @@ ::limit/original-limit nil}} (#'limit/add-default-limit query)) "Preprocessed query should have :limit added to it")))) + +(deftest ^:parallel download-row-max-results-test + (testing "Apply `absolute-max-results` when `download-row-limit` is not set." + (let [query {:type :query + :query {} + :info {:context :csv-download}}] + (is (= {:type :query + :query {:limit qp.i/absolute-max-results + ::limit/original-limit nil} + :info {:context :csv-download}} + (limit/add-default-limit query)))))) + +(deftest download-row-limit-test + (testing "Apply custom download row limits when" + (doseq [[limit expected context] [[1000 1000 :csv-download] + [1000 1000 :json-download] + [1000 1000 :xlsx-download] + [1100000 1100000 :csv-download] + [1100000 1100000 :json-download] + [1100000 qp.i/absolute-max-results :xlsx-download] + [nil qp.i/absolute-max-results :csv-download] + [nil qp.i/absolute-max-results :json-download] + [nil qp.i/absolute-max-results :xlsx-download]]] + (testing (format "%s the absolute limit for %s" + (if (< expected qp.i/absolute-max-results) + "below" + "above") + context) + (mt/with-temp-env-var-value! [mb-download-row-limit limit] + (is (= expected + (get-in (limit/add-default-limit + {:type :query + :query {} + :info {:context context}}) + [:query :limit]))))))) + (testing "Apply appropriate maximum when download-row-limit is unset, but `(mbql.u/query->max-rows-limit query)` returns a value above absolute-max-results" + (doseq [[limit expected context] [[1000 1000 :csv-download] + [1000 1000 :json-download] + [1000 1000 :xlsx-download] + [1100000 1100000 :csv-download] + [1100000 1100000 :json-download] + [1100000 qp.i/absolute-max-results :xlsx-download]]] + (testing (format "%s the absolute limit for %s" + (if (< expected qp.i/absolute-max-results) + "below" + "above") + context) + (is (= expected + (get-in (limit/add-default-limit + {:type :query + :query {} + ;; setting a constraint here will result in `(mbql.u/query->max-rows-limit query)` returning that limit + ;; so we can use this to check the behaviour of `limit/add-default-limit` when download-row-limit is unset + :constraints (when limit {:max-results-bare-rows limit}) + :info {:context context}}) + [:query :limit])))))))