(ns metabase-enterprise.serialization.v2.extract-test (:require [cheshire.core :as json] [clojure.set :as set] [clojure.test :refer :all] [metabase-enterprise.serialization.test-util :as ts] [metabase-enterprise.serialization.v2.extract :as extract] [metabase.models :refer [Card Collection Dashboard DashboardCard Database Dimension Field FieldValues Metric NativeQuerySnippet Pulse PulseCard Segment Table Timeline TimelineEvent User]] [metabase.models.serialization.base :as serdes.base] [schema.core :as s]) (:import [java.time LocalDateTime OffsetDateTime])) (defn- select-one [model-name where] (first (into [] (serdes.base/raw-reducible-query model-name {:where where})))) (defn- by-model [model-name extraction] (->> extraction (into []) (map (comp last :serdes/meta)) (filter #(= model-name (:model %))) (map :id) set)) (deftest fundamentals-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [Collection [{coll-id :id coll-eid :entity_id coll-slug :slug} {:name "Some Collection"}] Collection [{child-id :id child-eid :entity_id child-slug :slug} {:name "Nested Collection" :location (format "/%s/" coll-id)}] User [{mark-id :id} {:first_name "Mark" :last_name "Knopfler" :email "mark@direstrai.ts"}] Collection [{pc-id :id pc-eid :entity_id pc-slug :slug} {:name "Mark's Personal Collection" :personal_owner_id mark-id}]] (testing "a top-level collection is extracted correctly" (let [ser (serdes.base/extract-one "Collection" {} (select-one "Collection" [:= :id coll-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Collection" :id coll-eid :label coll-slug}]) :personal_owner_id (s/eq nil) :parent_id (s/eq nil) s/Keyword s/Any} ser)) (is (not (contains? ser :location))) (is (not (contains? ser :id))))) (testing "a nested collection is extracted with the right parent_id" (let [ser (serdes.base/extract-one "Collection" {} (select-one "Collection" [:= :id child-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Collection" :id child-eid :label child-slug}]) :personal_owner_id (s/eq nil) :parent_id (s/eq coll-eid) s/Keyword s/Any} ser)) (is (not (contains? ser :location))) (is (not (contains? ser :id))))) (testing "personal collections are extracted with email as key" (let [ser (serdes.base/extract-one "Collection" {} (select-one "Collection" [:= :id pc-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Collection" :id pc-eid :label pc-slug}]) :parent_id (s/eq nil) :personal_owner_id (s/eq "mark@direstrai.ts") s/Keyword s/Any} ser)) (is (not (contains? ser :location))) (is (not (contains? ser :id))))) (testing "overall extraction returns the expected set" (testing "no user specified" (is (= #{coll-eid child-eid} (by-model "Collection" (extract/extract-metabase nil))))) (testing "valid user specified" (is (= #{coll-eid child-eid pc-eid} (by-model "Collection" (extract/extract-metabase {:user mark-id}))))) (testing "invalid user specified" (is (= #{coll-eid child-eid} (by-model "Collection" (extract/extract-metabase {:user 218921}))))))))) (deftest dashboard-and-cards-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [Collection [{coll-id :id coll-eid :entity_id} {:name "Some Collection"}] User [{mark-id :id} {:first_name "Mark" :last_name "Knopfler" :email "mark@direstrai.ts"}] User [{dave-id :id} {:first_name "David" :last_name "Knopfler" :email "david@direstrai.ts"}] Collection [{mark-coll-eid :entity_id} {:name "MK Personal" :personal_owner_id mark-id}] Collection [{dave-coll-id :id dave-coll-eid :entity_id} {:name "DK Personal" :personal_owner_id dave-id}] Database [{db-id :id} {:name "My Database"}] Table [{no-schema-id :id} {:name "Schemaless Table" :db_id db-id}] Field [{field-id :id} {:name "Some Field" :table_id no-schema-id}] Table [{schema-id :id} {:name "Schema'd Table" :db_id db-id :schema "PUBLIC"}] Field [{field2-id :id} {:name "Other Field" :table_id schema-id}] Card [{c1-id :id c1-eid :entity_id} {:name "Some Question" :database_id db-id :table_id no-schema-id :collection_id coll-id :creator_id mark-id :dataset_query (json/generate-string {:query {:source-table no-schema-id :filter [:>= [:field field-id nil] 18] :aggregation [[:count]]} :database db-id})}] Card [{c2-id :id c2-eid :entity_id} {:name "Second Question" :database_id db-id :table_id schema-id :collection_id coll-id :creator_id mark-id :parameter_mappings [{:parameter_id "deadbeef" :card_id c1-id :target [:dimension [:field field-id {:source-field field2-id}]]}]}] Card [{c3-id :id c3-eid :entity_id} {:name "Third Question" :database_id db-id :table_id schema-id :collection_id coll-id :creator_id mark-id :visualization_settings {:table.pivot_column "SOURCE" :table.cell_column "sum" :table.columns [{:name "SOME_FIELD" :fieldRef [:field field-id nil] :enabled true} {:name "OTHER_FIELD" :fieldRef [:field field2-id nil] :enabled true} {:name "sum" :fieldRef [:field "sum" {:base-type :type/Float}] :enabled true} {:name "count" :fieldRef [:field "count" {:base-type :type/BigInteger}] :enabled true} {:name "Average order total" :fieldRef [:field "Average order total" {:base-type :type/Float}] :enabled true}] :column_settings {(str "[\"ref\",[\"field\"," field2-id ",null]]") {:column_title "Locus"}}}}] Card [{c4-id :id c4-eid :entity_id} {:name "Referenced Question" :database_id db-id :table_id schema-id :collection_id coll-id :creator_id mark-id :dataset_query (json/generate-string {:query {:source-table no-schema-id :filter [:>= [:field field-id nil] 18]} :database db-id})}] Card [{c5-id :id c5-eid :entity_id} {:name "Dependent Question" :database_id db-id :table_id schema-id :collection_id coll-id :creator_id mark-id :dataset_query (json/generate-string {:query {:source-table (str "card__" c4-id) :aggregation [[:count]]} :database db-id})}] Dashboard [{dash-id :id dash-eid :entity_id} {:name "Shared Dashboard" :collection_id coll-id :creator_id mark-id :parameters []}] Dashboard [{other-dash-id :id other-dash :entity_id} {:name "Dave's Dash" :collection_id dave-coll-id :creator_id mark-id :parameters []}] DashboardCard [{dc1-id :id dc1-eid :entity_id} {:card_id c1-id :dashboard_id dash-id :parameter_mappings [{:parameter_id "12345678" :card_id c1-id :target [:dimension [:field field-id {:source-field field2-id}]]}]}] DashboardCard [{dc2-id :id dc2-eid :entity_id} {:card_id c2-id :dashboard_id other-dash-id :visualization_settings {:table.pivot_column "SOURCE" :table.cell_column "sum" :table.columns [{:name "SOME_FIELD" :fieldRef [:field field-id nil] :enabled true} {:name "sum" :fieldRef [:field "sum" {:base-type :type/Float}] :enabled true} {:name "count" :fieldRef [:field "count" {:base-type :type/BigInteger}] :enabled true} {:name "Average order total" :fieldRef [:field "Average order total" {:base-type :type/Float}] :enabled true}] :column_settings {(str "[\"ref\",[\"field\"," field2-id ",null]]") {:column_title "Locus"}}}}]] (testing "table and database are extracted as [db schema table] triples" (let [ser (serdes.base/extract-one "Card" {} (select-one "Card" [:= :id c1-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Card" :id c1-eid}]) :table_id (s/eq ["My Database" nil "Schemaless Table"]) :creator_id (s/eq "mark@direstrai.ts") :collection_id (s/eq coll-eid) :dataset_query (s/eq {:query {:source-table ["My Database" nil "Schemaless Table"] :filter [">=" [:field ["My Database" nil "Schemaless Table" "Some Field"] nil] 18] :aggregation [[:count]]} :database "My Database"}) :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "cards depend on their Table and Collection, and also anything referenced in the query" (is (= #{[{:model "Database" :id "My Database"}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}] [{:model "Collection" :id coll-eid}]} (set (serdes.base/serdes-dependencies ser)))))) (let [ser (serdes.base/extract-one "Card" {} (select-one "Card" [:= :id c2-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Card" :id c2-eid}]) :table_id (s/eq ["My Database" "PUBLIC" "Schema'd Table"]) :creator_id (s/eq "mark@direstrai.ts") :collection_id (s/eq coll-eid) :dataset_query (s/eq {}) :parameter_mappings (s/eq [{:parameter_id "deadbeef" :card_id c1-eid :target [:dimension [:field ["My Database" nil "Schemaless Table" "Some Field"] {:source-field ["My Database" "PUBLIC" "Schema'd Table" "Other Field"]}]]}]) :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "cards depend on their Database, Table and Collection, and any fields in their parameter_mappings" (is (= #{[{:model "Database" :id "My Database"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"}] [{:model "Collection" :id coll-eid}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"} {:model "Field" :id "Other Field"}]} (set (serdes.base/serdes-dependencies ser)))))) (let [ser (serdes.base/extract-one "Card" {} (select-one "Card" [:= :id c3-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Card" :id c3-eid}]) :table_id (s/eq ["My Database" "PUBLIC" "Schema'd Table"]) :creator_id (s/eq "mark@direstrai.ts") :collection_id (s/eq coll-eid) :dataset_query (s/eq {}) :visualization_settings (s/eq {:table.pivot_column "SOURCE" :table.cell_column "sum" :table.columns [{:name "SOME_FIELD" :fieldRef ["field" ["My Database" nil "Schemaless Table" "Some Field"] nil] :enabled true} {:name "OTHER_FIELD" :fieldRef ["field" ["My Database" "PUBLIC" "Schema'd Table" "Other Field"] nil] :enabled true} {:name "sum" :fieldRef ["field" "sum" {:base-type "type/Float"}] :enabled true} {:name "count" :fieldRef ["field" "count" {:base-type "type/BigInteger"}] :enabled true} {:name "Average order total" :fieldRef ["field" "Average order total" {:base-type "type/Float"}] :enabled true}] :column_settings {"[\"ref\",[\"field\",[\"My Database\",\"PUBLIC\",\"Schema'd Table\",\"Other Field\"],null]]" {:column_title "Locus"}}}) :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "cards depend on their Database, Table and Collection, and any fields in their visualization_settings" (is (= #{[{:model "Database" :id "My Database"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"}] [{:model "Collection" :id coll-eid}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"} {:model "Field" :id "Other Field"}]} (set (serdes.base/serdes-dependencies ser)))))) (let [ser (serdes.base/extract-one "DashboardCard" {} (select-one "DashboardCard" [:= :id dc1-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Dashboard" :id dash-eid} {:model "DashboardCard" :id dc1-eid}]) :dashboard_id (s/eq dash-eid) :parameter_mappings (s/eq [{:parameter_id "12345678" :card_id c1-eid :target [:dimension [:field ["My Database" nil "Schemaless Table" "Some Field"] {:source-field ["My Database" "PUBLIC" "Schema'd Table" "Other Field"]}]]}]) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "cards depend on their Dashboard and Card, and any fields in their parameter_mappings" (is (= #{[{:model "Card" :id c1-eid}] [{:model "Dashboard" :id dash-eid}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"} {:model "Field" :id "Other Field"}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "Cards can be based on other cards" (let [ser (serdes.base/extract-one "Card" {} (select-one "Card" [:= :id c5-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Card" :id c5-eid}]) :table_id (s/eq ["My Database" "PUBLIC" "Schema'd Table"]) :creator_id (s/eq "mark@direstrai.ts") :collection_id (s/eq coll-eid) :dataset_query (s/eq {:query {:source-table c4-eid :aggregation [[:count]]} :database "My Database"}) :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "and depend on their Database, Table and Collection, and the upstream Card" (is (= #{[{:model "Database" :id "My Database"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"}] [{:model "Collection" :id coll-eid}] [{:model "Card" :id c4-eid}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "Dashcard :visualization_settings are included in their deps" (let [ser (serdes.base/extract-one "DashboardCard" {} (select-one "DashboardCard" [:= :id dc2-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Dashboard" :id other-dash} {:model "DashboardCard" :id dc2-eid}]) :dashboard_id (s/eq other-dash) :visualization_settings (s/eq {:table.pivot_column "SOURCE" :table.cell_column "sum" :table.columns [{:name "SOME_FIELD" :fieldRef ["field" ["My Database" nil "Schemaless Table" "Some Field"] nil] :enabled true} {:name "sum" :fieldRef ["field" "sum" {:base-type "type/Float"}] :enabled true} {:name "count" :fieldRef ["field" "count" {:base-type "type/BigInteger"}] :enabled true} {:name "Average order total" :fieldRef ["field" "Average order total" {:base-type "type/Float"}] :enabled true}] :column_settings {"[\"ref\",[\"field\",[\"My Database\",\"PUBLIC\",\"Schema'd Table\",\"Other Field\"],null]]" {:column_title "Locus"}}}) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "DashboardCard depend on their Dashboard and Card, and any fields in their visualization_settings" (is (= #{[{:model "Card" :id c2-eid}] [{:model "Dashboard" :id other-dash}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"} {:model "Field" :id "Other Field"}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "collection filtering based on :user option" (testing "only unowned collections are returned with no user" (is (= ["Some Collection"] (->> (serdes.base/extract-all "Collection" {}) (into []) (map :name))))) (testing "unowned collections and the personal one with a user" (is (= #{coll-eid mark-coll-eid} (by-model "Collection" (serdes.base/extract-all "Collection" {:user mark-id})))) (is (= #{coll-eid dave-coll-eid} (by-model "Collection" (serdes.base/extract-all "Collection" {:user dave-id})))))) (testing "dashboards are filtered based on :user" (testing "dashboards in unowned collections are always returned" (is (= #{dash-eid} (by-model "Dashboard" (serdes.base/extract-all "Dashboard" {})))) (is (= #{dash-eid} (by-model "Dashboard" (serdes.base/extract-all "Dashboard" {:user mark-id}))))) (testing "dashboards in personal collections are returned for the :user" (is (= #{dash-eid other-dash} (by-model "Dashboard" (serdes.base/extract-all "Dashboard" {:user dave-id})))))) (testing "dashboard cards are filtered based on :user" (testing "dashboard cards whose dashboards are in unowned collections are always returned" (is (= #{dc1-eid} (by-model "DashboardCard" (serdes.base/extract-all "DashboardCard" {})))) (is (= #{dc1-eid} (by-model "DashboardCard" (serdes.base/extract-all "DashboardCard" {:user mark-id}))))) (testing "dashboard cards whose dashboards are in personal collections are returned for the :user" (is (= #{dc1-eid dc2-eid} (by-model "DashboardCard" (serdes.base/extract-all "DashboardCard" {:user dave-id}))))))))) (deftest dimensions-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [;; Simple case: a singular field, no human-readable field. Database [{db-id :id} {:name "My Database"}] Table [{no-schema-id :id} {:name "Schemaless Table" :db_id db-id}] Field [{email-id :id} {:name "email" :table_id no-schema-id}] Dimension [{dim1-id :id dim1-eid :entity_id} {:name "Vanilla Dimension" :field_id email-id :type "internal"}] ;; Advanced case: :field_id is the foreign key, :human_readable_field_id the real target field. Table [{this-table :id} {:name "Schema'd Table" :db_id db-id :schema "PUBLIC"}] Field [{fk-id :id} {:name "foreign_id" :table_id this-table}] Table [{other-table :id} {:name "Foreign Table" :db_id db-id :schema "PUBLIC"}] Field [{target-id :id} {:name "real_field" :table_id other-table}] Dimension [{dim2-id :id dim2-eid :entity_id} {:name "Foreign Dimension" :type "external" :field_id fk-id :human_readable_field_id target-id}]] (testing "vanilla user-created dimensions" (let [ser (serdes.base/extract-one "Dimension" {} (select-one "Dimension" [:= :id dim1-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Dimension" :id dim1-eid}]) :field_id (s/eq ["My Database" nil "Schemaless Table" "email"]) :human_readable_field_id (s/eq nil) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depend on the one Field" (is (= #{[{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "email"}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "foreign key dimensions" (let [ser (serdes.base/extract-one "Dimension" {} (select-one "Dimension" [:= :id dim2-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Dimension" :id dim2-eid}]) :field_id (s/eq ["My Database" "PUBLIC" "Schema'd Table" "foreign_id"]) :human_readable_field_id (s/eq ["My Database" "PUBLIC" "Foreign Table" "real_field"]) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depend on both Fields" (is (= #{[{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Schema'd Table"} {:model "Field" :id "foreign_id"}] [{:model "Database" :id "My Database"} {:model "Schema" :id "PUBLIC"} {:model "Table" :id "Foreign Table"} {:model "Field" :id "real_field"}]} (set (serdes.base/serdes-dependencies ser)))))))))) (deftest metrics-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [User [{ann-id :id} {:first_name "Ann" :last_name "Wilson" :email "ann@heart.band"}] Database [{db-id :id} {:name "My Database"}] Table [{no-schema-id :id} {:name "Schemaless Table" :db_id db-id}] Field [{field-id :id} {:name "Some Field" :table_id no-schema-id}] Metric [{m1-id :id m1-eid :entity_id} {:name "My Metric" :creator_id ann-id :table_id no-schema-id :definition {:source-table no-schema-id :aggregation [[:sum [:field field-id nil]]]}}]] (testing "metrics" (let [ser (serdes.base/extract-one "Metric" {} (select-one "Metric" [:= :id m1-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Metric" :id m1-eid :label "My Metric"}]) :table_id (s/eq ["My Database" nil "Schemaless Table"]) :creator_id (s/eq "ann@heart.band") :definition (s/eq {:source-table ["My Database" nil "Schemaless Table"] :aggregation [[:sum [:field ["My Database" nil "Schemaless Table" "Some Field"] nil]]]}) :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depend on the Table and any fields referenced in :definition" (is (= #{[{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}]} (set (serdes.base/serdes-dependencies ser)))))))))) (deftest native-query-snippets-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [User [{ann-id :id} {:first_name "Ann" :last_name "Wilson" :email "ann@heart.band"}] Collection [{coll-id :id coll-eid :entity_id} {:name "Shared Collection" :personal_owner_id nil :namespace :snippets}] NativeQuerySnippet [{s1-id :id s1-eid :entity_id} {:name "Snippet 1" :collection_id coll-id :creator_id ann-id}] NativeQuerySnippet [{s2-id :id s2-eid :entity_id} {:name "Snippet 2" :collection_id nil :creator_id ann-id}]] (testing "native query snippets" (testing "can belong to :snippets collections" (let [ser (serdes.base/extract-one "NativeQuerySnippet" {} (select-one "NativeQuerySnippet" [:= :id s1-id]))] (is (schema= {:serdes/meta (s/eq [{:model "NativeQuerySnippet" :id s1-eid :label "Snippet 1"}]) :collection_id (s/eq coll-eid) :creator_id (s/eq "ann@heart.band") :created_at OffsetDateTime (s/optional-key :updated_at) OffsetDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "and depend on the Collection" (is (= #{[{:model "Collection" :id coll-eid}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "or can be outside collections" (let [ser (serdes.base/extract-one "NativeQuerySnippet" {} (select-one "NativeQuerySnippet" [:= :id s2-id]))] (is (schema= {:serdes/meta (s/eq [{:model "NativeQuerySnippet" :id s2-eid :label "Snippet 2"}]) (s/optional-key :collection_id) (s/eq nil) :creator_id (s/eq "ann@heart.band") :created_at OffsetDateTime (s/optional-key :updated_at) OffsetDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "and has no deps" (is (empty? (serdes.base/serdes-dependencies ser)))))))))) (deftest timelines-and-events-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [User [{ann-id :id} {:first_name "Ann" :last_name "Wilson" :email "ann@heart.band"}] Collection [{coll-id :id coll-eid :entity_id} {:name "Shared Collection" :personal_owner_id nil}] Timeline [{empty-id :id empty-eid :entity_id} {:name "Empty Timeline" :collection_id coll-id :creator_id ann-id}] Timeline [{line-id :id line-eid :entity_id} {:name "Populated Timeline" :collection_id coll-id :creator_id ann-id}] TimelineEvent [{e1-id :id} {:name "First Event" :creator_id ann-id :timestamp #t "2020-04-11T00:00Z" :timeline_id line-id}]] (testing "timelines" (testing "with no events" (let [ser (serdes.base/extract-one "Timeline" {} (select-one "Timeline" [:= :id empty-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Timeline" :id empty-eid}]) :collection_id (s/eq coll-eid) :creator_id (s/eq "ann@heart.band") :created_at OffsetDateTime (s/optional-key :updated_at) OffsetDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depend on the Collection" (is (= #{[{:model "Collection" :id coll-eid}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "with events" (let [ser (serdes.base/extract-one "Timeline" {} (select-one "Timeline" [:= :id line-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Timeline" :id line-eid}]) :collection_id (s/eq coll-eid) :creator_id (s/eq "ann@heart.band") :created_at OffsetDateTime (s/optional-key :updated_at) OffsetDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depend on the Collection" (is (= #{[{:model "Collection" :id coll-eid}]} (set (serdes.base/serdes-dependencies ser)))))))) (testing "timeline events" (let [ser (serdes.base/extract-one "TimelineEvent" {} (select-one "TimelineEvent" [:= :id e1-id])) stamp "2020-04-11T00:00:00Z"] (is (schema= {:serdes/meta (s/eq [{:model "Timeline" :id line-eid} {:model "TimelineEvent" :id stamp :label "First Event"}]) :timestamp (s/eq stamp) :timeline_id (s/eq line-eid) :creator_id (s/eq "ann@heart.band") :created_at OffsetDateTime (s/optional-key :updated_at) OffsetDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depend on the Timeline" (is (= #{[{:model "Timeline" :id line-eid}]} (set (serdes.base/serdes-dependencies ser)))))))))) (deftest segments-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [User [{ann-id :id} {:first_name "Ann" :last_name "Wilson" :email "ann@heart.band"}] Database [{db-id :id} {:name "My Database"}] Table [{no-schema-id :id} {:name "Schemaless Table" :db_id db-id}] Field [{field-id :id} {:name "Some Field" :table_id no-schema-id}] Segment [{s1-id :id s1-eid :entity_id} {:name "My Segment" :creator_id ann-id :table_id no-schema-id :definition {:source-table no-schema-id :aggregation [[:count]] :filter [:< [:field field-id nil] 18]}}]] (testing "segment" (let [ser (serdes.base/extract-one "Segment" {} (select-one "Segment" [:= :id s1-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Segment" :id s1-eid :label "My Segment"}]) :table_id (s/eq ["My Database" nil "Schemaless Table"]) :creator_id (s/eq "ann@heart.band") :created_at LocalDateTime :definition (s/eq {:source-table ["My Database" nil "Schemaless Table"] :aggregation [[:count]] :filter ["<" [:field ["My Database" nil "Schemaless Table" "Some Field"] nil] 18]}) (s/optional-key :updated_at) LocalDateTime s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depend on the Table and any fields from the definition" (is (= #{[{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"}] [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}]} (set (serdes.base/serdes-dependencies ser)))))))))) (deftest field-values-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [Database [{db-id :id} {:name "My Database"}] Table [{no-schema-id :id} {:name "Schemaless Table" :db_id db-id}] Field [{field-id :id} {:name "Some Field" :table_id no-schema-id :fingerprint {:global {:distinct-count 75 :nil% 0.0} :type {:type/Text {:percent-json 0.0 :percent-url 0.0 :percent-email 0.0 :percent-state 0.0 :average-length 8.333333333333334}}}}] FieldValues [{fv-id :id values :values} {:field_id field-id :hash_key nil :has_more_values false :type :full :human_readable_values [] :values ["Artisan" "Asian" "BBQ" "Bakery" "Bar" "Brewery" "Burger" "Coffee Shop" "Diner" "Indian" "Italian" "Japanese" "Mexican" "Middle Eastern" "Pizza" "Seafood" "Steakhouse" "Tea Room" "Winery"]}]] (testing "field values" (let [ser (serdes.base/extract-one "FieldValues" {} (select-one "FieldValues" [:= :id fv-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"} {:model "FieldValues" :id "0"}]) ; Always 0. :created_at LocalDateTime (s/optional-key :updated_at) OffsetDateTime :values (s/eq (json/generate-string values)) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (is (not (contains? ser :field_id)) ":field_id is dropped; its implied by the path") (testing "depend on the parent Field" (is (= #{[{:model "Database" :id "My Database"} {:model "Table" :id "Schemaless Table"} {:model "Field" :id "Some Field"}]} (set (serdes.base/serdes-dependencies ser)))))))))) (deftest pulses-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [User [{ann-id :id} {:first_name "Ann" :last_name "Wilson" :email "ann@heart.band"}] Collection [{coll-id :id coll-eid :entity_id} {:name "Some Collection"}] Dashboard [{dash-id :id dash-eid :entity_id} {:name "A Dashboard"}] Pulse [{p-none-id :id p-none-eid :entity_id} {:name "Pulse w/o collection or dashboard" :creator_id ann-id}] Pulse [{p-coll-id :id p-coll-eid :entity_id} {:name "Pulse with only collection" :creator_id ann-id :collection_id coll-id}] Pulse [{p-dash-id :id p-dash-eid :entity_id} {:name "Pulse with only dashboard" :creator_id ann-id :dashboard_id dash-id}] Pulse [{p-both-id :id p-both-eid :entity_id} {:name "Pulse with both collection and dashboard" :creator_id ann-id :collection_id coll-id :dashboard_id dash-id}]] (testing "pulse with neither collection nor dashboard" (let [ser (serdes.base/extract-one "Pulse" {} (select-one "Pulse" [:= :id p-none-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Pulse" :id p-none-eid}]) :creator_id (s/eq "ann@heart.band") :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime (s/optional-key :dashboard_id) (s/eq nil) (s/optional-key :collection_id) (s/eq nil) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "has no deps" (is (= #{} (set (serdes.base/serdes-dependencies ser))))))) (testing "pulse with just collection" (let [ser (serdes.base/extract-one "Pulse" {} (select-one "Pulse" [:= :id p-coll-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Pulse" :id p-coll-eid}]) :creator_id (s/eq "ann@heart.band") :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime (s/optional-key :dashboard_id) (s/eq nil) :collection_id (s/eq coll-eid) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depends on the collection" (is (= #{[{:model "Collection" :id coll-eid}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "pulse with just dashboard" (let [ser (serdes.base/extract-one "Pulse" {} (select-one "Pulse" [:= :id p-dash-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Pulse" :id p-dash-eid}]) :creator_id (s/eq "ann@heart.band") :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime :dashboard_id (s/eq dash-eid) (s/optional-key :collection_id) (s/eq nil) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depends on the dashboard" (is (= #{[{:model "Dashboard" :id dash-eid}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "pulse with both collection and dashboard" (let [ser (serdes.base/extract-one "Pulse" {} (select-one "Pulse" [:= :id p-both-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Pulse" :id p-both-eid}]) :creator_id (s/eq "ann@heart.band") :created_at LocalDateTime (s/optional-key :updated_at) LocalDateTime :dashboard_id (s/eq dash-eid) :collection_id (s/eq coll-eid) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depends on the collection and dashboard" (is (= #{[{:model "Collection" :id coll-eid}] [{:model "Dashboard" :id dash-eid}]} (set (serdes.base/serdes-dependencies ser)))))))))) (deftest pulse-cards-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [User [{ann-id :id} {:first_name "Ann" :last_name "Wilson" :email "ann@heart.band"}] Dashboard [{dash-id :id} {:name "A Dashboard"}] Database [{db-id :id} {:name "My Database"}] Table [{table-id :id} {:name "Schemaless Table" :db_id db-id}] Card [{card1-id :id card1-eid :entity_id} {:name "Some Question" :database_id db-id :table_id table-id :creator_id ann-id :dataset_query "{\"json\": \"string values\"}"}] DashboardCard [{dashcard-id :id dashcard-eid :entity_id} {:card_id card1-id :dashboard_id dash-id}] Pulse [{pulse-id :id pulse-eid :entity_id} {:name "Legacy Pulse" :creator_id ann-id}] Pulse [{sub-id :id sub-eid :entity_id} {:name "Dashboard sub" :creator_id ann-id :dashboard_id dash-id}] PulseCard [{pc1-pulse-id :id pc1-pulse-eid :entity_id} {:pulse_id pulse-id :card_id card1-id :position 1}] PulseCard [{pc2-pulse-id :id pc2-pulse-eid :entity_id} {:pulse_id pulse-id :card_id card1-id :position 2}] PulseCard [{pc1-sub-id :id pc1-sub-eid :entity_id} {:pulse_id sub-id :card_id card1-id :position 1 :dashboard_card_id dashcard-id}]] (testing "legacy pulse cards" (let [ser (serdes.base/extract-one "PulseCard" {} (select-one "PulseCard" [:= :id pc1-pulse-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Pulse" :id pulse-eid} {:model "PulseCard" :id pc1-pulse-eid}]) :card_id (s/eq card1-eid) (s/optional-key :dashboard_card_id) (s/eq nil) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depends on the pulse and card" (is (= #{[{:model "Pulse" :id pulse-eid}] [{:model "Card" :id card1-eid}]} (set (serdes.base/serdes-dependencies ser)))))) (let [ser (serdes.base/extract-one "PulseCard" {} (select-one "PulseCard" [:= :id pc2-pulse-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Pulse" :id pulse-eid} {:model "PulseCard" :id pc2-pulse-eid}]) :card_id (s/eq card1-eid) (s/optional-key :dashboard_card_id) (s/eq nil) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depends on the pulse and card" (is (= #{[{:model "Pulse" :id pulse-eid}] [{:model "Card" :id card1-eid}]} (set (serdes.base/serdes-dependencies ser))))))) (testing "dashboard sub cards" (let [ser (serdes.base/extract-one "PulseCard" {} (select-one "PulseCard" [:= :id pc1-sub-id]))] (is (schema= {:serdes/meta (s/eq [{:model "Pulse" :id sub-eid} {:model "PulseCard" :id pc1-sub-eid}]) :card_id (s/eq card1-eid) :dashboard_card_id (s/eq dashcard-eid) s/Keyword s/Any} ser)) (is (not (contains? ser :id))) (testing "depends on the pulse, card and dashcard" (is (= #{[{:model "Pulse" :id sub-eid}] [{:model "Card" :id card1-eid}] [{:model "DashboardCard" :id dashcard-eid}]} (set (serdes.base/serdes-dependencies ser)))))))))) (deftest selective-serialization-basic-test (ts/with-empty-h2-app-db (ts/with-temp-dpc [User [{mark-id :id} {:first_name "Mark" :last_name "Knopfler" :email "mark@direstrai.ts"}] Collection [{coll1-id :id coll1-eid :entity_id} {:name "Some Collection"}] Collection [{coll2-id :id coll2-eid :entity_id} {:name "Nested Collection" :location (str "/" coll1-id "/")}] Collection [{coll3-id :id coll3-eid :entity_id} {:name "Grandchild Collection" :location (str "/" coll1-id "/" coll2-id "/")}] Database [{db-id :id} {:name "My Database"}] Table [{no-schema-id :id} {:name "Schemaless Table" :db_id db-id}] Field [_ {:name "Some Field" :table_id no-schema-id}] Table [{schema-id :id} {:name "Schema'd Table" :db_id db-id :schema "PUBLIC"}] Field [_ {:name "Other Field" :table_id schema-id}] ;; One dashboard and three cards in each of the three collections: ;; Two cards contained in the dashboard and one freestanding. Dashboard [{dash1-id :id dash1-eid :entity_id} {:name "Dashboard 1" :collection_id coll1-id :creator_id mark-id}] Card [{c1-1-id :id c1-1-eid :entity_id} {:name "Question 1-1" :database_id db-id :table_id no-schema-id :collection_id coll1-id :creator_id mark-id}] Card [{c1-2-id :id c1-2-eid :entity_id} {:name "Question 1-2" :database_id db-id :table_id schema-id :collection_id coll1-id :creator_id mark-id}] Card [{c1-3-eid :entity_id} {:name "Question 1-3" :database_id db-id :table_id schema-id :collection_id coll1-id :creator_id mark-id}] DashboardCard [{dc1-1-eid :entity_id} {:card_id c1-1-id :dashboard_id dash1-id}] DashboardCard [{dc1-2-eid :entity_id} {:card_id c1-2-id :dashboard_id dash1-id}] ;; Second dashboard, in the middle collection. Dashboard [{dash2-id :id dash2-eid :entity_id} {:name "Dashboard 2" :collection_id coll2-id :creator_id mark-id}] Card [{c2-1-id :id c2-1-eid :entity_id} {:name "Question 2-1" :database_id db-id :table_id no-schema-id :collection_id coll2-id :creator_id mark-id}] Card [{c2-2-id :id c2-2-eid :entity_id} {:name "Question 2-2" :database_id db-id :table_id schema-id :collection_id coll2-id :creator_id mark-id}] Card [{c2-3-eid :entity_id} {:name "Question 2-3" :database_id db-id :table_id schema-id :collection_id coll2-id :creator_id mark-id}] DashboardCard [{dc2-1-eid :entity_id} {:card_id c2-1-id :dashboard_id dash2-id}] DashboardCard [{dc2-2-eid :entity_id} {:card_id c2-2-id :dashboard_id dash2-id}] ;; Third dashboard, in the grandchild collection. Dashboard [{dash3-id :id dash3-eid :entity_id} {:name "Dashboard 3" :collection_id coll3-id :creator_id mark-id}] Card [{c3-1-id :id c3-1-eid :entity_id} {:name "Question 3-1" :database_id db-id :table_id no-schema-id :collection_id coll3-id :creator_id mark-id}] Card [{c3-2-id :id c3-2-eid :entity_id} {:name "Question 3-2" :database_id db-id :table_id schema-id :collection_id coll3-id :creator_id mark-id}] Card [{c3-3-eid :entity_id} {:name "Question 3-3" :database_id db-id :table_id schema-id :collection_id coll3-id :creator_id mark-id}] DashboardCard [{dc3-1-eid :entity_id} {:card_id c3-1-id :dashboard_id dash3-id}] DashboardCard [{dc3-2-eid :entity_id} {:card_id c3-2-id :dashboard_id dash3-id}]] (testing "selecting a dashboard gets its dashcards and cards as well" (testing "grandparent dashboard" (is (= #{[{:model "Dashboard" :id dash1-eid}] [{:model "Dashboard" :id dash1-eid} {:model "DashboardCard" :id dc1-1-eid}] [{:model "Dashboard" :id dash1-eid} {:model "DashboardCard" :id dc1-2-eid}] [{:model "Card" :id c1-1-eid}] [{:model "Card" :id c1-2-eid}]} (->> (extract/extract-subtrees {:targets [["Dashboard" dash1-id]]}) (map serdes.base/serdes-path) set)))) (testing "middle dashboard" (is (= #{[{:model "Dashboard" :id dash2-eid}] [{:model "Dashboard" :id dash2-eid} {:model "DashboardCard" :id dc2-1-eid}] [{:model "Dashboard" :id dash2-eid} {:model "DashboardCard" :id dc2-2-eid}] [{:model "Card" :id c2-1-eid}] [{:model "Card" :id c2-2-eid}]} (->> (extract/extract-subtrees {:targets [["Dashboard" dash2-id]]}) (map serdes.base/serdes-path) set)))) (testing "grandchild dashboard" (is (= #{[{:model "Dashboard" :id dash3-eid}] [{:model "Dashboard" :id dash3-eid} {:model "DashboardCard" :id dc3-1-eid}] [{:model "Dashboard" :id dash3-eid} {:model "DashboardCard" :id dc3-2-eid}] [{:model "Card" :id c3-1-eid}] [{:model "Card" :id c3-2-eid}]} (->> (extract/extract-subtrees {:targets [["Dashboard" dash3-id]]}) (map serdes.base/serdes-path) set))))) (testing "selecting a collection gets all its contents" (let [grandchild-paths #{[{:model "Collection" :id coll3-eid :label "grandchild_collection"}] [{:model "Dashboard" :id dash3-eid}] [{:model "Dashboard" :id dash3-eid} {:model "DashboardCard" :id dc3-1-eid}] [{:model "Dashboard" :id dash3-eid} {:model "DashboardCard" :id dc3-2-eid}] [{:model "Card" :id c3-1-eid}] [{:model "Card" :id c3-2-eid}] [{:model "Card" :id c3-3-eid}]} middle-paths #{[{:model "Collection" :id coll2-eid :label "nested_collection"}] [{:model "Dashboard" :id dash2-eid}] [{:model "Dashboard" :id dash2-eid} {:model "DashboardCard" :id dc2-1-eid}] [{:model "Dashboard" :id dash2-eid} {:model "DashboardCard" :id dc2-2-eid}] [{:model "Card" :id c2-1-eid}] [{:model "Card" :id c2-2-eid}] [{:model "Card" :id c2-3-eid}]} grandparent-paths #{[{:model "Collection" :id coll1-eid :label "some_collection"}] [{:model "Dashboard" :id dash1-eid}] [{:model "Dashboard" :id dash1-eid} {:model "DashboardCard" :id dc1-1-eid}] [{:model "Dashboard" :id dash1-eid} {:model "DashboardCard" :id dc1-2-eid}] [{:model "Card" :id c1-1-eid}] [{:model "Card" :id c1-2-eid}] [{:model "Card" :id c1-3-eid}]}] (testing "grandchild collection has all its own contents" (is (= grandchild-paths ; Includes the third card not found in the collection (->> (extract/extract-subtrees {:targets [["Collection" coll3-id]]}) (map serdes.base/serdes-path) set)))) (testing "middle collection has all its own plus the grandchild and its contents" (is (= (set/union middle-paths grandchild-paths) (->> (extract/extract-subtrees {:targets [["Collection" coll2-id]]}) (map serdes.base/serdes-path) set)))) (testing "grandparent collection has all its own plus the grandchild and middle collections with contents" (is (= (set/union grandparent-paths middle-paths grandchild-paths) (->> (extract/extract-subtrees {:targets [["Collection" coll1-id]]}) (map serdes.base/serdes-path) set)))))))))