-
Braden Shepherdson authored
Cards can depend on other Cards as their `:source-table`, but the code to extract `serdes-dependencies` from the MBQL query did not capture that case.
Braden Shepherdson authoredCards can depend on other Cards as their `:source-table`, but the code to extract `serdes-dependencies` from the MBQL query did not capture that case.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
extract_test.clj 74.58 KiB
(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)))))))))