diff --git a/e2e/test/scenarios/models/models.cy.spec.js b/e2e/test/scenarios/models/models.cy.spec.js index 8d88f8dec6cf9480f18d2582dfce3a50d4ce3be3..48ee9eb601957f8d608e7dfe4880baf7f765f595 100644 --- a/e2e/test/scenarios/models/models.cy.spec.js +++ b/e2e/test/scenarios/models/models.cy.spec.js @@ -162,6 +162,91 @@ describe("scenarios > models", () => { cy.location("pathname").should("eq", "/collection/root"); }); + it("allows to turn a native question with a long alias into a model (metabase#47584)", () => { + const nativeQuery = ` + SELECT + count(*) AS coun, + state AS Total_number_of_people_from_each_state_separated_by_state_and_then_we_do_a_count + FROM people + GROUP BY + Total_number_of_people_from_each_state_separated_by_state_and_then_we_do_a_count`; + cy.createNativeQuestion( + { + name: "People Model with long alias", + native: { + query: nativeQuery, + }, + }, + { visitQuestion: true, wrapId: true }, + ); + + turnIntoModel(); + openQuestionActions(); + assertIsModel(); + + cy.get("@questionId").then(questionId => { + cy.wait("@dataset").then(({ response }) => { + expect(response.body.json_query.query["source-table"]).to.equal( + `card__${questionId}`, + ); + expect(response.body.error).to.not.exist; + }); + }); + + // Filtering on the long column is currently broken in master (metabase#47863), + // but this works in the release-x.50.x branch. + // + // filter(); + // filterField( + // "TOTAL_NUMBER_OF_PEOPLE_FROM_EACH_STATE_SEPARATED_BY_STATE_AND_THEN_WE_DO_A_COUNT", + // { + // operator: "Contains", + // value: "A", + // }, + // ); + + // cy.findByTestId("apply-filters").click(); + // cy.wait("@dataset").then(({ response }) => { + // expect(response.body.error).to.not.exist; + // }); + + filter(); + filterField("COUN", { + operator: "Greater than", + value: 30, + }); + + cy.findByTestId("apply-filters").click(); + cy.wait("@dataset").then(({ response }) => { + expect(response.body.error).to.not.exist; + }); + + assertQuestionIsBasedOnModel({ + model: "People Model with long alias", + collection: "Our analytics", + table: "People", + }); + + cy.get("@questionId").then(questionId => { + saveQuestionBasedOnModel({ modelId: questionId, name: "Q1" }); + }); + + assertQuestionIsBasedOnModel({ + questionName: "Q1", + model: "People Model with long alias", + collection: "Our analytics", + table: "People", + }); + + cy.findByTestId("qb-header").findAllByText("Our analytics").first().click(); + getCollectionItemCard("People Model with long alias").within(() => { + cy.icon("model"); + }); + getCollectionItemRow("Q1").icon("table2"); + + cy.location("pathname").should("eq", "/collection/root"); + }); + it("changes model's display to table", () => { visitQuestion(ORDERS_BY_YEAR_QUESTION_ID); @@ -520,11 +605,7 @@ describe("scenarios > models", () => { it("should automatically pin newly created models", () => { visitQuestion(ORDERS_QUESTION_ID); - - cy.intercept("PUT", "/api/card/*").as("cardUpdate"); turnIntoModel(); - cy.wait("@cardUpdate"); - visitCollection("root"); cy.findByTestId("pinned-items").within(() => { cy.findByText("Models"); diff --git a/src/metabase/query_processor/middleware/annotate.clj b/src/metabase/query_processor/middleware/annotate.clj index 347c610ceb2de98c7113d662603600e49ce852c2..787971ae1ac965bb4cb44eb9bba7e6342f4ce3a9 100644 --- a/src/metabase/query_processor/middleware/annotate.clj +++ b/src/metabase/query_processor/middleware/annotate.clj @@ -14,7 +14,6 @@ [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.schema.common :as lib.schema.common] - [metabase.lib.util :as lib.util] [metabase.lib.util.match :as lib.util.match] [metabase.models.humanization :as humanization] [metabase.query-processor.debug :as qp.debug] @@ -80,7 +79,7 @@ :type qp.error-type/qp})))))) (defn- annotate-native-cols [cols] - (let [unique-name-fn (lib.util/unique-name-generator (qp.store/metadata-provider))] + (let [unique-name-fn (mbql.u/unique-name-generator)] (mapv (fn [{col-name :name, base-type :base_type, :as driver-col-metadata}] (let [col-name (name col-name)] (merge diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj index 2ca996148a59b41d8b8177ac32ba9eabe1fd3c06..78ef32bbc6ba0f7ef6a0c33c7d46f29002ed87d7 100644 --- a/test/metabase/api/dataset_test.clj +++ b/test/metabase/api/dataset_test.clj @@ -16,11 +16,14 @@ [metabase.lib.core :as lib] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.jvm :as lib.metadata.jvm] + [metabase.lib.metadata.protocols :as lib.metadata.protocols] [metabase.lib.schema.id :as lib.schema.id] + [metabase.lib.util :as lib.util] [metabase.models.card :refer [Card]] [metabase.models.data-permissions :as data-perms] [metabase.models.permissions-group :as perms-group] [metabase.models.query-execution :refer [QueryExecution]] + [metabase.query-processor.compile :as qp.compile] [metabase.query-processor.middleware.constraints :as qp.constraints] [metabase.query-processor.test-util :as qp.test-util] [metabase.query-processor.util :as qp.util] @@ -227,6 +230,73 @@ (data-perms/set-database-permission! (perms-group/all-users) (mt/id) :perms/create-queries :no) (do-test))))))) +(deftest native-query-with-long-column-alias + (testing "nested native query with long column alias (metabase#47584)" + (let [short-col-name "coun" + long-col-name "Total_number_of_people_from_each_state_separated_by_state_and_then_we_do_a_count" + + ;; Lightly validate the native form that comes back. Resist the urge to check for exact equality. + validate-native-form (fn [native-form-lines] + (and (some #(str/includes? % short-col-name) native-form-lines) + (some #(str/includes? % long-col-name) native-form-lines))) + + ;; Disable truncate-alias when compiling the native query to ensure we don't truncate the column. + ;; We want to simulate a user-defined query where the column name is long, but valid for the driver. + native-sub-query (with-redefs [lib.util/truncate-alias + (fn mock-truncate-alias + [ss & _] ss)] + (-> (mt/mbql-query people + {:source-table $$people + :aggregation [[:aggregation-options [:count] {:name short-col-name}]] + :breakout [[:field %state {:name long-col-name}]] + :limit 5}) + qp.compile/compile + :query)) + + native-query (mt/native-query {:query native-sub-query}) + + ;; Let metadata-provider-with-cards-with-metadata-for-queries calculate the result-metadata. + metadata-provider (qp.test-util/metadata-provider-with-cards-with-metadata-for-queries [native-query])] + (t2.with-temp/with-temp + [Card card (assoc {:dataset_query native-query} + :result_metadata + (-> (lib.metadata.protocols/metadatas metadata-provider :metadata/card [1]) + first + :result-metadata))] + (let [card-query {:database (mt/id) + :type "query" + :query {:source-table (str "card__" (u/the-id card))}}] + (mt/with-native-query-testing-context card-query + (testing "POST /api/dataset/native" + (is (=? {:query validate-native-form + :params nil} + (-> (mt/user-http-request :crowberto :post 200 "dataset/native" card-query) + (update :query #(str/split-lines (or (driver/prettify-native-form :h2 %) + "error: no query generated"))))))) + (testing "POST /api/dataset" + (is (=? + {:data {:rows [["AK" 68] ["AL" 56] ["AR" 49] ["AZ" 20] ["CA" 90]] + :cols [{:name long-col-name + :display_name long-col-name + :field_ref ["field" long-col-name {}] + :source "fields"} + {:name short-col-name + :display_name short-col-name + :field_ref ["field" short-col-name {}] + :source "fields"}] + :native_form {:query validate-native-form + :params nil} + :results_timezone "UTC"} + :row_count 5 + :status "completed" + :context "ad-hoc" + :json_query (merge query-defaults card-query) + :database_id (mt/id)} + (-> (mt/user-http-request :crowberto :post 202 "dataset" card-query) + (update-in [:data :native_form :query] + #(str/split-lines (or (driver/prettify-native-form :h2 %) + "error: no query generated"))))))))))))) + (deftest formatted-results-ignore-query-constraints (testing "POST /api/dataset/:format" (testing "Downloading CSV/JSON/XLSX results shouldn't be subject to the default query constraints (#9831)" diff --git a/test/metabase/query_processor_test/nested_queries_test.clj b/test/metabase/query_processor_test/nested_queries_test.clj index 5cf8bf840186737c3aa9cec9b40ef926ee1c9e05..dc48a7ec39e896612f35f84157e42259e1b5a861 100644 --- a/test/metabase/query_processor_test/nested_queries_test.clj +++ b/test/metabase/query_processor_test/nested_queries_test.clj @@ -15,6 +15,7 @@ [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.test-metadata :as meta] [metabase.lib.test-util :as lib.tu] + [metabase.lib.util :as lib.util] [metabase.models :refer [Field Table]] [metabase.models.card :as card :refer [Card]] [metabase.models.collection :as collection :refer [Collection]] @@ -350,6 +351,60 @@ (run-native-query (str native-sub-query " -- small comment here\n"))) "Ensure trailing comments followed by a newline are trimmed and don't cause a wrapping SQL query to fail")))) +(defn- col-max-for-driver [driver] + ;; There is a default impl for driver/column-name-length-limit, but apparently not all drivers implement + ;; table-name-length, so calling column-name-length results in an exception for some drivers. + (try (driver/column-name-length-limit driver) + (catch Exception _ nil))) + +(deftest card-id-native-source-query-with-long-alias-test + (testing "nested card with native query and long column alias (metabase##47584)" + (mt/test-drivers (set/intersection (mt/normal-drivers-with-feature :nested-queries) + (descendants driver/hierarchy :sql)) + (let [coun-col-name "coun" + long-col-full-name "Total_number_of_people_from_each_state_separated_by_state_and_then_we_do_a_count" + + ;; Truncate the long column to something the driver can actually execute. + long-col-name (subs long-col-full-name + 0 + (if-let [col-max (col-max-for-driver driver/*driver*)] + (min col-max (count long-col-full-name)) + (count long-col-full-name))) + + ;; Disable truncate-alias when compiling the native query to ensure we don't further truncate the column. + ;; We want to simulate a user-defined query where the column name is long, but valid for the driver. + native-sub-query (with-redefs [lib.util/truncate-alias + (fn mock-truncate-alias + ([ss] ss) + ([ss _] ss))] + (-> (mt/mbql-query people + {:source-table $$people + :aggregation [[:aggregation-options [:count] {:name coun-col-name}]] + :breakout [[:field %state {:name long-col-name}]] + :limit 5}) + qp.compile/compile + :query)) + + query (query-with-source-card 1)] + (qp.store/with-metadata-provider (qp.test-util/metadata-provider-with-cards-with-metadata-for-queries + [(mt/native-query {:query native-sub-query})]) + (mt/with-native-query-testing-context query + (let [coun-col-re (re-pattern (str "(?i)" coun-col-name)) + long-col-re (re-pattern (str "(?i)" long-col-name))] + (is (=? {:rows [["AK" 68] ["AL" 56] ["AR" 49] ["AZ" 20] ["CA" 90]], + :cols + [{:source :fields + :name long-col-re + :display_name long-col-re + :field_ref [:field long-col-re {}]} + {:source :fields + :name coun-col-re + :display_name coun-col-re + :field_ref [:field coun-col-re {}]}]} + (qp.test-util/rows-and-cols + (mt/format-rows-by [str int] + (qp/process-query query)))))))))))) + (defmethod driver/database-supports? [::driver/driver ::filter-by-field-literal-test] [_driver _feature _database] false)