Skip to content
Snippets Groups Projects
Unverified Commit b9bc8c1c authored by Noah Moss's avatar Noah Moss Committed by GitHub
Browse files

Make sure the query to fetch result_metadata for native sandboxes doesn't use...

Make sure the query to fetch result_metadata for native sandboxes doesn't use the streaming results handler (#50049)
parent 4d5aa805
No related branches found
No related tags found
No related merge requests found
...@@ -585,122 +585,92 @@ describeEE("formatting > sandboxes", () => { ...@@ -585,122 +585,92 @@ describeEE("formatting > sandboxes", () => {
cy.findAllByText("McClure-Lockman"); cy.findAllByText("McClure-Lockman");
}); });
/** it("Advanced sandboxing should not ignore data model features like object detail of FK (metabase-enterprise#520)", () => {
* This issue (metabase-enterprise#520) has a peculiar quirk: cy.intercept("POST", "/api/card/*/query").as("cardQuery");
* - It works ONLY if SQL question is first run (`result_metadata` builds), and then the question is saved. cy.intercept("PUT", "/api/card/*").as("questionUpdate");
* - In a real-world scenario it is quite possible for an admin to save that SQL question without running it first. This fails!
* (more info: https://github.com/metabase/metabase-enterprise/issues/520#issuecomment-772528159) cy.createNativeQuestion({
* name: "EE_520_Q1",
* That's why this test has 2 versions that reflect both scenarios. We'll call them "normal" and "workaround". native: {
* Until the underlying issue is fixed, "normal" scenario will be skipped. query:
* "SELECT * FROM ORDERS WHERE USER_ID={{sandbox}} AND TOTAL > 10",
* Related issues: metabase#10474, metabase#14629 "template-tags": {
* sandbox: {
* Update 2024/11/07: "display-name": "Sandbox",
* It's expected that this test fails - the issue is still there. id: "1115dc4f-6b9d-812e-7f72-b87ab885c88a",
* See https://github.com/metabase/metabase/issues/49671 for a proposed fix. name: "sandbox",
*/ type: "number",
["normal", "workaround"].forEach(test => {
it(
`${test.toUpperCase()} version:\n advanced sandboxing should not ignore data model features like object detail of FK (metabase-enterprise#520)`,
{ tags: "@quarantine" },
() => {
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
cy.intercept("PUT", "/api/card/*").as("questionUpdate");
cy.createNativeQuestion({
name: "EE_520_Q1",
native: {
query:
"SELECT * FROM ORDERS WHERE USER_ID={{sandbox}} AND TOTAL > 10",
"template-tags": {
sandbox: {
"display-name": "Sandbox",
id: "1115dc4f-6b9d-812e-7f72-b87ab885c88a",
name: "sandbox",
type: "number",
},
},
}, },
}).then(({ body: { id: CARD_ID } }) => { },
test === "workaround" },
? runQuestion({ question: CARD_ID, sandboxValue: "1" }) }).then(({ body: { id: CARD_ID } }) => {
: null; runQuestion({ question: CARD_ID, sandboxValue: "1" });
cy.sandboxTable({ cy.sandboxTable({
table_id: ORDERS_ID, table_id: ORDERS_ID,
card_id: CARD_ID, card_id: CARD_ID,
attribute_remappings: { attribute_remappings: {
attr_uid: ["variable", ["template-tag", "sandbox"]], attr_uid: ["variable", ["template-tag", "sandbox"]],
}, },
}); });
}); });
cy.createNativeQuestion({ cy.createNativeQuestion({
name: "EE_520_Q2", name: "EE_520_Q2",
native: { native: {
query: query:
"SELECT * FROM PRODUCTS WHERE CATEGORY={{sandbox}} AND PRICE > 10", "SELECT * FROM PRODUCTS WHERE CATEGORY={{sandbox}} AND PRICE > 10",
"template-tags": { "template-tags": {
sandbox: { sandbox: {
"display-name": "Sandbox", "display-name": "Sandbox",
id: "3d69ba99-7076-2252-30bd-0bb8810ba895", id: "3d69ba99-7076-2252-30bd-0bb8810ba895",
name: "sandbox", name: "sandbox",
type: "text", type: "text",
},
},
}, },
}).then(({ body: { id: CARD_ID } }) => { },
test === "workaround"
? runQuestion({
question: CARD_ID,
sandboxValue: "Widget",
})
: null;
cy.sandboxTable({
table_id: PRODUCTS_ID,
card_id: CARD_ID,
attribute_remappings: {
attr_cat: ["variable", ["template-tag", "sandbox"]],
},
});
});
cy.signOut();
cy.signInAsSandboxedUser();
openOrdersTable();
cy.log("Reported failing on v1.36.x");
cy.log(
"It should show remapped Display Values instead of Product ID",
);
cy.get("[data-testid=cell-data]")
.contains("Awesome Concrete Shoes")
.click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(/View details/i).click();
cy.log(
"It should show object details instead of filtering by this Product ID",
);
// The name of this Vendor is visible in "details" only
cy.findByTestId("object-detail");
cy.findAllByText("McClure-Lockman");
/**
* Helper function related to this test only!
*/
function runQuestion({ question, sandboxValue } = {}) {
// Run the question
cy.visit(`/question/${question}?sandbox=${sandboxValue}`);
// Wait for results
cy.wait("@cardQuery");
}
}, },
}).then(({ body: { id: CARD_ID } }) => {
runQuestion({ question: CARD_ID, sandboxValue: "Widget" });
cy.sandboxTable({
table_id: PRODUCTS_ID,
card_id: CARD_ID,
attribute_remappings: {
attr_cat: ["variable", ["template-tag", "sandbox"]],
},
});
});
cy.signOut();
cy.signInAsSandboxedUser();
openOrdersTable();
cy.log("Reported failing on v1.36.x");
cy.log("It should show remapped Display Values instead of Product ID");
cy.get("[data-testid=cell-data]")
.contains("Awesome Concrete Shoes")
.click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(/View details/i).click();
cy.log(
"It should show object details instead of filtering by this Product ID",
); );
// The name of this Vendor is visible in "details" only
cy.findByTestId("object-detail");
cy.findAllByText("McClure-Lockman");
/**
* Helper function related to this test only!
*/
function runQuestion({ question, sandboxValue } = {}) {
// Run the question
cy.visit(`/question/${question}?sandbox=${sandboxValue}`);
// Wait for results
cy.wait("@cardQuery");
}
}); });
it("simple sandboxing should work (metabase#14629)", () => { it("simple sandboxing should work (metabase#14629)", () => {
...@@ -1036,7 +1006,7 @@ describeEE("formatting > sandboxes", () => { ...@@ -1036,7 +1006,7 @@ describeEE("formatting > sandboxes", () => {
}); });
}); });
it.skip("should be able to visit ad-hoc/dirty question when permission is granted to the linked table column, but not to the linked table itself (metabase#15105)", () => { it("should be able to visit ad-hoc/dirty question when permission is granted to the linked table column, but not to the linked table itself (metabase#15105)", () => {
cy.sandboxTable({ cy.sandboxTable({
table_id: ORDERS_ID, table_id: ORDERS_ID,
attribute_remappings: { attribute_remappings: {
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
[metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.error-type :as qp.error-type]
^{:clj-kondo/ignore [:deprecated-namespace]} ^{:clj-kondo/ignore [:deprecated-namespace]}
[metabase.query-processor.middleware.fetch-source-query-legacy :as fetch-source-query-legacy] [metabase.query-processor.middleware.fetch-source-query-legacy :as fetch-source-query-legacy]
[metabase.query-processor.pipeline :as qp.pipeline]
[metabase.query-processor.store :as qp.store] [metabase.query-processor.store :as qp.store]
[metabase.server.middleware.session :as mw.session] [metabase.server.middleware.session :as mw.session]
[metabase.util :as u] [metabase.util :as u]
...@@ -173,12 +174,16 @@ ...@@ -173,12 +174,16 @@
(mu/defn- native-query-metadata :- [:+ :map] (mu/defn- native-query-metadata :- [:+ :map]
[source-query :- [:map [:source-query :any]]] [source-query :- [:map [:source-query :any]]]
(let [result (mw.session/as-admin (let [result
((requiring-resolve 'metabase.query-processor/process-query) ;; Rebind *result* in case the outer query is being streamed back to the client. The streaming code binds this
{:database (u/the-id (lib.metadata/database (qp.store/metadata-provider))) ;; to a custom handler, and we don't want to accidentally terminate the stream here!
:type :query (binding [qp.pipeline/*result* qp.pipeline/default-result-handler]
:query {:source-query source-query (mw.session/as-admin
:limit 0}}))] ((requiring-resolve 'metabase.query-processor/process-query)
{:database (u/the-id (lib.metadata/database (qp.store/metadata-provider)))
:type :query
:query {:source-query source-query
:limit 0}})))]
(or (-> result :data :results_metadata :columns not-empty) (or (-> result :data :results_metadata :columns not-empty)
(throw (ex-info (tru "Error running query to determine metadata") (throw (ex-info (tru "Error running query to determine metadata")
{:source-query source-query {:source-query source-query
...@@ -196,17 +201,17 @@ ...@@ -196,17 +201,17 @@
(let [table-metadata (original-table-metadata table-id) (let [table-metadata (original-table-metadata table-id)
;; make sure source query has `:source-metadata`; add it if needed ;; make sure source query has `:source-metadata`; add it if needed
[metadata save?] (cond [metadata save?] (cond
;; if it already has `:source-metadata`, we're good to go. ;; if it already has `:source-metadata`, we're good to go.
(seq source-metadata) (seq source-metadata)
[source-metadata false] [source-metadata false]
;; if it doesn't have source metadata, but it's an MBQL query, we can preprocess the query to ;; if it doesn't have source metadata, but it's an MBQL query, we can preprocess the query to
;; get the expected metadata. ;; get the expected metadata.
(not (get-in source-query [:source-query :native])) (not (get-in source-query [:source-query :native]))
[(mbql-query-metadata source-query) true] [(mbql-query-metadata source-query) true]
;; otherwise if it's a native query we'll have to run the query really quickly to get the ;; otherwise if it's a native query we'll have to run the query really quickly to get the
;; expected metadata. ;; expected metadata.
:else :else
[(native-query-metadata source-query) true]) [(native-query-metadata source-query) true])
metadata (reconcile-metadata metadata table-metadata)] metadata (reconcile-metadata metadata table-metadata)]
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
[metabase.query-processor.pivot :as qp.pivot] [metabase.query-processor.pivot :as qp.pivot]
[metabase.query-processor.preprocess :as qp.preprocess] [metabase.query-processor.preprocess :as qp.preprocess]
[metabase.query-processor.store :as qp.store] [metabase.query-processor.store :as qp.store]
[metabase.query-processor.streaming.test-util :as streaming.test-util]
[metabase.query-processor.util :as qp.util] [metabase.query-processor.util :as qp.util]
[metabase.query-processor.util.add-alias-info :as add] [metabase.query-processor.util.add-alias-info :as add]
[metabase.server.middleware.session :as mw.session] [metabase.server.middleware.session :as mw.session]
...@@ -1222,3 +1223,16 @@ ...@@ -1222,3 +1223,16 @@
:attributes {"cat" 50}} :attributes {"cat" 50}}
(data-perms/set-table-permission! &group (mt/id :people) :perms/view-data :blocked) (data-perms/set-table-permission! &group (mt/id :people) :perms/view-data :blocked)
(is (= 10 (count (mt/rows (qp/process-query (mt/mbql-query venues))))))))) (is (= 10 (count (mt/rows (qp/process-query (mt/mbql-query venues)))))))))
(deftest native-sandbox-no-query-metadata-streaming-test
(testing "A sandbox powered by a native query source card can be used via the streaming API even if the card has no
stored results_metadata (#49985)"
(met/with-gtaps! {:gtaps {:venues (venues-category-native-gtap-def)}
:attributes {"cat" 50}}
(let [sandbox-card-id (t2/select-one-fn :card_id
:model/GroupTableAccessPolicy
:group_id (:id &group)
:table_id (mt/id :venues))]
(is (nil? (t2/select-one-fn :result_metadata :model/Card sandbox-card-id)))
(is (= 10 (count (mt/rows (streaming.test-util/process-query-basic-streaming :api (mt/mbql-query venues))))))
(is (not (nil? (t2/select-one-fn :result_metadata :model/Card sandbox-card-id))))))))
...@@ -23,14 +23,19 @@ ...@@ -23,14 +23,19 @@
[] []
(some-> *canceled-chan* a/poll!)) (some-> *canceled-chan* a/poll!))
(defn ^:dynamic *result* (defn default-result-handler
"Called exactly once with the final result, which is the result of either [[*reduce*]] (if query completed "Default implementation for *result*."
successfully), or an Exception (if it did not)."
[result] [result]
(if (instance? Throwable result) (if (instance? Throwable result)
(throw result) (throw result)
result)) result))
(defn ^:dynamic *result*
"Called exactly once with the final result, which is the result of either [[*reduce*]] (if query completed
successfully), or an Exception (if it did not)."
[result]
(default-result-handler result))
(defn ^:dynamic *execute* (defn ^:dynamic *execute*
"Called by [[*run*]] to have driver run query. By default, [[metabase.driver/execute-reducible-query]]. `respond` is a "Called by [[*run*]] to have driver run query. By default, [[metabase.driver/execute-reducible-query]]. `respond` is a
callback with the signature: callback with the signature:
......
...@@ -136,18 +136,17 @@ ...@@ -136,18 +136,17 @@
(mu/defn- streaming-result-fn :- fn? (mu/defn- streaming-result-fn :- fn?
[results-writer :- (lib.schema.common/instance-of-class metabase.query_processor.streaming.interface.StreamingResultsWriter) [results-writer :- (lib.schema.common/instance-of-class metabase.query_processor.streaming.interface.StreamingResultsWriter)
^OutputStream os :- (lib.schema.common/instance-of-class OutputStream)] ^OutputStream os :- (lib.schema.common/instance-of-class OutputStream)]
(let [orig qp.pipeline/*result*] (fn result [result]
(fn result [result] (when (= (:status result) :completed)
(when (= (:status result) :completed) (log/debug "Finished writing results; closing results writer.")
(log/debug "Finished writing results; closing results writer.") (try
(try (qp.si/finish! results-writer result)
(qp.si/finish! results-writer result) (catch EofException e
(catch EofException e (log/error e "Client closed connection prematurely")))
(log/error e "Client closed connection prematurely"))) (u/ignore-exceptions
(u/ignore-exceptions (.flush os)
(.flush os) (.close os)))
(.close os))) (qp.pipeline/default-result-handler result)))
(orig result))))
(defn do-with-streaming-rff (defn do-with-streaming-rff
"Context to pass to the QP to streaming results as `export-format` to an output stream. Can be used independently of "Context to pass to the QP to streaming results as `export-format` to an output stream. Can be used independently of
......
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