Apply FieldUserSettings on read instead of writing them into metabase_field (#82246)
This commit is contained in:
committed by
GitHub
parent
14faa1e777
commit
82cc16d2b5
@@ -73,6 +73,7 @@
|
||||
:metabase/no-jsqlparser-imports {:level :warning}
|
||||
:metabase/prefer-with-dynamic-fn-redefs {:level :warning}
|
||||
:metabase/t2-query-namespace {:level :warning}
|
||||
:metabase/table-or-field-query {:level :warning}
|
||||
:metabase/toucan-model-ns {:level :warning}
|
||||
:metabase/require-shape-checker {:level :warning}
|
||||
:metabase/tests-must-live-in-known-modules {:level :warning}
|
||||
@@ -900,7 +901,12 @@
|
||||
:name app-db-namespaces}]
|
||||
|
||||
:config-in-ns
|
||||
{test-namespaces
|
||||
{metabase.app-db.custom-migrations
|
||||
{:linters
|
||||
;; a migration reads the rows as they were when it ran; applying today's user settings would make it drift
|
||||
{:metabase/table-or-field-query {:level :off}}}
|
||||
|
||||
test-namespaces
|
||||
{:linters
|
||||
{:inline-def {:level :off}
|
||||
:missing-docstring {:level :off}
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
settings
|
||||
users
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses}
|
||||
:model-exports #{:model/Action}
|
||||
:model-imports #{:model/Card
|
||||
@@ -156,7 +157,8 @@
|
||||
models
|
||||
permissions
|
||||
util
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/Dashboard
|
||||
@@ -199,7 +201,8 @@
|
||||
system
|
||||
task
|
||||
util
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/CacheConfig
|
||||
:model/Card
|
||||
:model/Collection
|
||||
@@ -209,7 +212,6 @@
|
||||
:model/Database
|
||||
:model/DatabaseRouter
|
||||
:model/Document
|
||||
:model/Field
|
||||
:model/ModerationReview
|
||||
:model/PermissionsGroup
|
||||
:model/Pulse
|
||||
@@ -378,7 +380,8 @@
|
||||
task
|
||||
task.history
|
||||
tracing
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-exports #{:model/AuditLog}
|
||||
:model-imports #{:model/ApiKey
|
||||
:model/Card
|
||||
@@ -599,7 +602,8 @@
|
||||
transforms
|
||||
upload
|
||||
util
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-exports #{:model/Collection}
|
||||
:model-imports #{:model/Card
|
||||
:model/CollectionBookmark
|
||||
@@ -613,6 +617,7 @@
|
||||
:model/Permissions
|
||||
:model/Pulse
|
||||
:model/Table
|
||||
:model/TableUserSettings
|
||||
:model/Timeline
|
||||
:model/Transform
|
||||
:model/User}}
|
||||
@@ -632,7 +637,8 @@
|
||||
premium-features
|
||||
queries
|
||||
request
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card :model/Collection}}
|
||||
|
||||
comments
|
||||
@@ -820,6 +826,7 @@
|
||||
request
|
||||
sync
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Database
|
||||
:model/Dimension
|
||||
@@ -857,7 +864,8 @@
|
||||
query-processor
|
||||
revisions
|
||||
search
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-exports #{:model/Document}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -910,6 +918,7 @@
|
||||
util
|
||||
util.classloader
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Database :model/Field :model/Table}}
|
||||
@@ -941,7 +950,8 @@
|
||||
metabot
|
||||
permissions
|
||||
premium-features
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/ConnectionImpersonation
|
||||
@@ -1170,7 +1180,8 @@
|
||||
lib.schema
|
||||
models
|
||||
settings
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Database
|
||||
:model/Dimension
|
||||
@@ -1219,7 +1230,8 @@
|
||||
lib.schema
|
||||
lib.types
|
||||
settings
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card :model/Field :model/Table}}
|
||||
|
||||
lib.schema
|
||||
@@ -1335,6 +1347,7 @@
|
||||
users
|
||||
util
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -1387,7 +1400,8 @@
|
||||
permissions
|
||||
remote-sync
|
||||
search
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-exports #{:model/Measure}
|
||||
:model-imports #{:model/Collection :model/Table :model/User}}
|
||||
|
||||
@@ -1458,6 +1472,7 @@
|
||||
transforms.base
|
||||
util
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema}
|
||||
:model-exports #{:model/AiUsageLog
|
||||
@@ -1548,6 +1563,7 @@
|
||||
sync
|
||||
util
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Card
|
||||
:model/Database
|
||||
@@ -1602,7 +1618,8 @@
|
||||
query-processor
|
||||
request
|
||||
server
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card :model/Field :model/Measure}}
|
||||
|
||||
mfa
|
||||
@@ -1641,7 +1658,6 @@
|
||||
models
|
||||
{:team "DevEx"
|
||||
:api #{metabase.models.db
|
||||
metabase.models.humanization
|
||||
metabase.models.init
|
||||
metabase.models.interface
|
||||
metabase.models.resolution
|
||||
@@ -1656,9 +1672,9 @@
|
||||
lib.legacy-mbql
|
||||
lib.metadata
|
||||
lib.schema
|
||||
settings
|
||||
util
|
||||
util.classloader}
|
||||
util.classloader
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/Dashboard
|
||||
@@ -1788,6 +1804,7 @@
|
||||
query-processor
|
||||
util
|
||||
util.classloader
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Card
|
||||
:model/Dashboard
|
||||
@@ -1816,6 +1833,7 @@
|
||||
users
|
||||
util
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
enterprise/permissions}
|
||||
:model-exports #{:model/ApplicationPermissionsRevision
|
||||
:model/CollectionPermissionGraphRevision
|
||||
@@ -1842,7 +1860,8 @@
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Dashboard
|
||||
:model/DataPermissions
|
||||
@@ -2022,6 +2041,7 @@
|
||||
util
|
||||
util.config
|
||||
util.graph
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-exports #{:model/Card
|
||||
:model/ParameterCard
|
||||
@@ -2066,7 +2086,8 @@
|
||||
request
|
||||
revisions
|
||||
search
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/CardBookmark
|
||||
:model/Collection
|
||||
@@ -2089,7 +2110,8 @@
|
||||
permissions
|
||||
query-processor
|
||||
request
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card :model/Field :model/Table}}
|
||||
|
||||
query-processor
|
||||
@@ -2165,7 +2187,8 @@
|
||||
tracing
|
||||
users
|
||||
util
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/DashboardCardSeries
|
||||
:model/Database
|
||||
@@ -2341,7 +2364,8 @@
|
||||
tracing
|
||||
transforms
|
||||
util
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/Dashboard
|
||||
@@ -2381,6 +2405,7 @@
|
||||
remote-sync
|
||||
search
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
xrays}
|
||||
:model-exports #{:model/Segment}
|
||||
:model-imports #{:model/Collection :model/Table :model/User}}
|
||||
@@ -2526,7 +2551,8 @@
|
||||
lib.schema
|
||||
settings
|
||||
sql-tools.parsing
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Table}}
|
||||
|
||||
sql-tools.parsing
|
||||
@@ -2622,6 +2648,7 @@
|
||||
tracing
|
||||
util
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Database
|
||||
@@ -2789,7 +2816,8 @@
|
||||
tracing
|
||||
transforms.base
|
||||
transforms.indexes
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-exports #{:model/Transform
|
||||
:model/TransformDagRun
|
||||
:model/TransformJob
|
||||
@@ -2832,7 +2860,8 @@
|
||||
query-processor
|
||||
sync
|
||||
transforms.indexes
|
||||
util}}
|
||||
util
|
||||
warehouse-schema-overlay}}
|
||||
|
||||
transforms.indexes
|
||||
{:team "Gadget"
|
||||
@@ -2896,7 +2925,8 @@
|
||||
models
|
||||
permissions
|
||||
system
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Action
|
||||
:model/Card
|
||||
:model/Collection
|
||||
@@ -2933,6 +2963,7 @@
|
||||
settings
|
||||
sync
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Card
|
||||
:model/Database
|
||||
@@ -2955,7 +2986,8 @@
|
||||
models
|
||||
settings
|
||||
task
|
||||
util}}
|
||||
util
|
||||
warehouse-schema-overlay}}
|
||||
|
||||
user-key-value
|
||||
{:team "UX West"
|
||||
@@ -3061,6 +3093,15 @@
|
||||
:api #{metabase.graph.core}
|
||||
:uses #{util}}
|
||||
|
||||
warehouse-schema-overlay
|
||||
{:team "UX West"
|
||||
:api #{metabase.warehouse-schema-overlay.core}
|
||||
:uses #{util}
|
||||
:model-imports #{:model/Field
|
||||
:model/FieldUserSettings
|
||||
:model/Table
|
||||
:model/TableUserSettings}}
|
||||
|
||||
warehouses
|
||||
{:team "UX West"
|
||||
:module-exports #{warehouses.schema
|
||||
@@ -3082,6 +3123,7 @@
|
||||
settings
|
||||
sync
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.secrets}
|
||||
:model-exports #{:model/Database}
|
||||
:model-imports #{:model/Card
|
||||
@@ -3116,6 +3158,7 @@
|
||||
util
|
||||
util.classloader
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema
|
||||
warehouses.secrets
|
||||
@@ -3129,14 +3172,18 @@
|
||||
warehouses.schema
|
||||
{:team "UX West"
|
||||
:ns-prefix "metabase.warehouse-schema"
|
||||
:api #{metabase.warehouse-schema.field
|
||||
:api #{metabase.warehouse-schema.core
|
||||
metabase.warehouse-schema.field
|
||||
metabase.warehouse-schema.field-values.distinct-batch
|
||||
metabase.warehouse-schema.humanization
|
||||
metabase.warehouse-schema.init
|
||||
metabase.warehouse-schema.metadata-from-qp
|
||||
metabase.warehouse-schema.metadata-queries
|
||||
metabase.warehouse-schema.models.field
|
||||
metabase.warehouse-schema.models.field-user-settings
|
||||
metabase.warehouse-schema.models.field-values
|
||||
metabase.warehouse-schema.models.table
|
||||
metabase.warehouse-schema.models.table-user-settings
|
||||
metabase.warehouse-schema.schema
|
||||
metabase.warehouse-schema.table}
|
||||
:uses #{api
|
||||
@@ -3155,17 +3202,20 @@
|
||||
query-processor
|
||||
remote-sync
|
||||
search
|
||||
settings
|
||||
sync.analyze
|
||||
util
|
||||
util.classloader
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
enterprise/permissions}
|
||||
:model-exports #{:model/Dimension
|
||||
:model/Field
|
||||
:model/FieldUserSettings
|
||||
:model/FieldValues
|
||||
:model/Table}
|
||||
:model/Table
|
||||
:model/TableUserSettings}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/Database
|
||||
@@ -3198,6 +3248,7 @@
|
||||
sync
|
||||
upload
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema
|
||||
xrays}
|
||||
:model-imports #{:model/Collection
|
||||
@@ -3243,6 +3294,7 @@
|
||||
settings
|
||||
sync.analyze
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -3272,7 +3324,8 @@
|
||||
models
|
||||
query-processor
|
||||
sync
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Database
|
||||
:model/Field
|
||||
:model/FieldValues
|
||||
@@ -3358,6 +3411,7 @@
|
||||
util
|
||||
util.classloader
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema
|
||||
enterprise/serialization}
|
||||
@@ -3529,7 +3583,8 @@
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/DataPermissions
|
||||
@@ -3549,11 +3604,12 @@
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/AuditLog
|
||||
:model/Collection
|
||||
:model/Dimension
|
||||
:model/Field
|
||||
:model/Table
|
||||
:model/User}}
|
||||
|
||||
@@ -3607,12 +3663,13 @@
|
||||
transforms
|
||||
transforms.base
|
||||
util
|
||||
util.graph}
|
||||
util.graph
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
:model/Dashboard
|
||||
:model/Document
|
||||
:model/Field
|
||||
:model/FieldUserSettings
|
||||
:model/Measure
|
||||
:model/NativeQuerySnippet
|
||||
:model/Sandbox
|
||||
@@ -3642,6 +3699,7 @@
|
||||
task
|
||||
transforms
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema
|
||||
enterprise/dependencies}
|
||||
:model-imports #{:model/Card
|
||||
@@ -3672,6 +3730,7 @@
|
||||
:uses #{lib.schema
|
||||
premium-features
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
enterprise/sso}
|
||||
:model-imports #{:model/Card
|
||||
:model/Collection
|
||||
@@ -3690,7 +3749,8 @@
|
||||
collections
|
||||
lib.schema
|
||||
premium-features
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card :model/Collection :model/Table}}
|
||||
|
||||
enterprise/mcp
|
||||
@@ -3797,6 +3857,7 @@
|
||||
task
|
||||
util
|
||||
util.config
|
||||
warehouse-schema-overlay
|
||||
enterprise/search.embeddings
|
||||
enterprise/search.semantic}
|
||||
:model-imports #{:model/Card
|
||||
@@ -3854,6 +3915,7 @@
|
||||
query-processor
|
||||
remote-sync
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
enterprise/permissions.impersonation}
|
||||
:model-imports #{:model/ApplicationPermissionsRevision
|
||||
@@ -3874,7 +3936,8 @@
|
||||
models
|
||||
permissions
|
||||
query-processor
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Card :model/User}}
|
||||
|
||||
enterprise/permissions.impersonation
|
||||
@@ -3918,6 +3981,7 @@
|
||||
users
|
||||
users.tenants
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses
|
||||
warehouses.schema
|
||||
enterprise/api}
|
||||
@@ -3960,6 +4024,7 @@
|
||||
task
|
||||
task.history
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
enterprise/data-apps
|
||||
enterprise/serialization
|
||||
enterprise/transforms.python}
|
||||
@@ -3975,6 +4040,7 @@
|
||||
:model/PythonLibrary
|
||||
:model/Segment
|
||||
:model/Table
|
||||
:model/TableUserSettings
|
||||
:model/Timeline
|
||||
:model/Transform
|
||||
:model/TransformTag
|
||||
@@ -4015,6 +4081,7 @@
|
||||
search.entity-retrieval
|
||||
task
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
enterprise/search.semantic}
|
||||
:model-imports #{:model/Card
|
||||
:model/Measure
|
||||
@@ -4055,7 +4122,8 @@
|
||||
task
|
||||
tracing
|
||||
util
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Collection :model/Dashboard :model/Table}}
|
||||
|
||||
enterprise/security-center
|
||||
@@ -4239,7 +4307,8 @@
|
||||
tracing
|
||||
transforms
|
||||
transforms.base
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Database
|
||||
:model/Field
|
||||
:model/Table
|
||||
@@ -4268,7 +4337,8 @@
|
||||
transforms
|
||||
transforms.base
|
||||
util
|
||||
util.config}
|
||||
util.config
|
||||
warehouse-schema-overlay}
|
||||
:model-exports #{:model/PythonLibrary}
|
||||
:model-imports #{:model/Database
|
||||
:model/Field
|
||||
@@ -4285,7 +4355,8 @@
|
||||
permissions
|
||||
premium-features
|
||||
upload
|
||||
util}
|
||||
util
|
||||
warehouse-schema-overlay}
|
||||
:model-imports #{:model/Database :model/Table}}
|
||||
|
||||
enterprise/warehouses
|
||||
@@ -4297,6 +4368,7 @@
|
||||
models
|
||||
permissions
|
||||
util
|
||||
warehouse-schema-overlay
|
||||
warehouses.schema}
|
||||
:model-imports #{:model/Database :model/Field :model/Table}}}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
:metabase/defsetting-namespace 1
|
||||
:metabase/prefer-with-dynamic-fn-redefs 3
|
||||
:metabase/t2-query-namespace 2
|
||||
:metabase/table-or-field-query 1
|
||||
:missing-docstring 1
|
||||
:private-call 1
|
||||
:redundant-ignore 1
|
||||
|
||||
@@ -42,4 +42,5 @@
|
||||
:message (format "Application database query calls like `%s` must live in metabase[-enterprise].<module>.db (or metabase.driver.<driver>.db) namespaces"
|
||||
(hooks/sexpr fn-node))
|
||||
:type :metabase/t2-query-namespace))))
|
||||
((requiring-resolve 'hooks.metabase.warehouse-schema-overlay.table-or-field-query/lint-read) input)
|
||||
input)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
(ns hooks.metabase.warehouse-schema-overlay.table-or-field-query
|
||||
"Lint that a query over `:model/Field` or `:model/Table` names its source with `field-query`/`table-query` from
|
||||
`metabase.warehouse-schema-overlay.core`, or reads sync's values with `{:user-settings? false}`."
|
||||
(:require
|
||||
[clj-kondo.hooks-api :as hooks]))
|
||||
|
||||
(def ^:private read-fn?
|
||||
"The Toucan 2 query functions that read rows. Writes are not affected: they go to `metabase_field` itself."
|
||||
#{"select" "select-one" "select-fn->fn" "select-fn->pk" "select-fn-reducible" "select-fn-set" "select-fn-vec"
|
||||
"select-one-fn" "select-one-pk" "select-pk->fn" "select-pks-set" "select-pks-vec" "select-reducible"
|
||||
"reducible-select" "reducible-query" "query" "query-one" "count" "exists?"})
|
||||
|
||||
(def ^:private model->helper
|
||||
{":model/Field" "field-query"
|
||||
":model/Table" "table-query"})
|
||||
|
||||
(defn- test-file? [filename]
|
||||
(boolean (and filename (re-find #"(?:^|/)test/" filename))))
|
||||
|
||||
(defn- tokens
|
||||
"Every token in `node`'s tree, as strings."
|
||||
[node]
|
||||
(tree-seq :children :children node))
|
||||
|
||||
(defn- mentions?
|
||||
[node pred]
|
||||
(boolean (some (fn [n] (when-let [s (some-> (hooks/sexpr n) pr-str)] (pred s)))
|
||||
(filter (complement :children) (tokens node)))))
|
||||
|
||||
(defn- writes?
|
||||
"Whether a `t2/query` form is really a write spelled as a query."
|
||||
[node]
|
||||
(mentions? node #{":update" ":delete-from" ":insert-into"}))
|
||||
|
||||
(defn lint-read
|
||||
"Register a `:metabase/table-or-field-query` finding when a read of `:model/Field` or `:model/Table` does not name
|
||||
its source with the matching helper."
|
||||
[{:keys [node filename]}]
|
||||
(let [fn-node (first (:children node))
|
||||
fn-name (some-> (hooks/sexpr fn-node) name)]
|
||||
(when (and fn-name
|
||||
(read-fn? fn-name)
|
||||
(not (test-file? filename))
|
||||
(not (writes? node)))
|
||||
(doseq [[model helper] model->helper
|
||||
:when (mentions? node #(= % model))
|
||||
:when (not (mentions? node #(re-find (re-pattern (str "\\b" helper "$")) %)))]
|
||||
(hooks/reg-finding!
|
||||
(assoc (meta fn-node)
|
||||
:message (format "A read of %s must name its source: add {:from [(%s)]} (or {:user-settings? false} for sync's own values)"
|
||||
model helper)
|
||||
:type :metabase/table-or-field-query))))))
|
||||
188
dev/test/metabase/core/table_or_field_raw_usage_test.clj
Normal file
188
dev/test/metabase/core/table_or_field_raw_usage_test.clj
Normal file
@@ -0,0 +1,188 @@
|
||||
(ns metabase.core.table-or-field-raw-usage-test
|
||||
"Guards against hand-written Honey SQL that names `metabase_table`/`metabase_field` directly.
|
||||
|
||||
`metabase.warehouse-schema-overlay.core/table-query` and `/field-query` are the source a query over
|
||||
`:model/Table`/`:model/Field` must read from, so that a user's Field values (`metabase_field_user_settings`) and a
|
||||
workspace's table remapping (`workspace_table_remapping`) are applied. The `:metabase/table-or-field-query` clj-kondo
|
||||
linter (`.clj-kondo/src/hooks/metabase/warehouse_schema_overlay/table_or_field_query.clj`) already catches this for
|
||||
`t2/select`-style calls naming `:model/Table`/`:model/Field`, but it only fires on those Toucan 2 query functions.
|
||||
Hand-written Honey SQL that instead names the raw table with the literal keyword `:metabase_table`/`:metabase_field`
|
||||
-- in a `:from` or a join -- sails right past it; clj-kondo has no `:discouraged-keyword` linter to catch a bare
|
||||
keyword literal irrespective of the function it's passed to (verified against the pinned 2026.07.24 build: only
|
||||
`:discouraged-var` and `:discouraged-namespace` exist, both keyed on a resolved var/namespace symbol, not a keyword
|
||||
value), and a hook can't fire on a keyword sitting in arbitrary data with no distinguishing enclosing call. Hence
|
||||
this repo-level test instead, in the style of [[metabase.core.modules-test]]."
|
||||
(:require
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer :all]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(def ^:private scanned-roots
|
||||
"Where hand-written application-database Honey SQL lives. Driver modules are globbed since each has its own
|
||||
`src` directory. `.clj-kondo/` (which spells out both keywords in the hook this test backs up) is simply never
|
||||
one of these roots, so it needs no explicit exclusion."
|
||||
(into ["src" "enterprise/backend/src"]
|
||||
(comp (filter #(.isDirectory ^java.io.File %))
|
||||
(map #(str "modules/drivers/" (.getName ^java.io.File %) "/src"))
|
||||
(filter #(.isDirectory (io/file %))))
|
||||
(some-> (io/file "modules/drivers") .listFiles)))
|
||||
|
||||
(def ^:private excluded-paths
|
||||
"Whole files/directories the scan never reports on, because a hit there isn't the linter's business:
|
||||
|
||||
- `metabase.warehouse-schema-overlay.core` itself builds `table-query`/`field-query` by naming the raw tables --
|
||||
that's the one place allowed to.
|
||||
- `metabase.sync.*` is sync's own read/write path: sync is the thing that writes `metabase_table`/`metabase_field`,
|
||||
so it always wants the raw columns, and a separate change owns this namespace right now.
|
||||
- Liquibase/custom migrations run against whatever shape the app DB had at that migration's version, before (or
|
||||
long after) `field-query`/`table-query` existed; they are not reads a *current* user sees rendered anywhere."
|
||||
#{"src/metabase/warehouse_schema_overlay/core.clj"})
|
||||
|
||||
(defn- excluded-dir?
|
||||
[path]
|
||||
(or (str/starts-with? path "src/metabase/sync/")
|
||||
(str/includes? path "custom_migrations")))
|
||||
|
||||
(def ^:private allowed-raw-usages
|
||||
"Files where a `:metabase_table`/`:metabase_field` literal survives review: every hit in the file was read back and
|
||||
confirmed to select only columns `table-query`/`field-query` don't touch (an id, a count, a foreign key used purely
|
||||
to join or filter), or the file has its own structural reason to want the raw row. Note that a Table's
|
||||
`display_name`, `description`, `is_published` and `collection_id` are user values like a Field's -- a rationale
|
||||
resting on those being sync-owned no longer holds. Add an entry here only after
|
||||
checking every hit it covers -- see the check's failure message for the two helpers this is standing in for.
|
||||
|
||||
Keep this in sync with what the scan actually finds: [[stale-allowed-raw-usage-test]] fails on an entry that no
|
||||
longer matches anything, same as clj-kondo's ignore ratchets flag a stale exemption."
|
||||
{"src/metabase/usage_metadata/db.clj"
|
||||
"raw-field-fingerprint selects only :fingerprint, a sync-owned column with no user override."
|
||||
|
||||
"src/metabase/collections/children.clj"
|
||||
"the :model case joins metabase_table only for :is_upload, which is sync-owned."
|
||||
|
||||
"src/metabase/queries/db.clj"
|
||||
"field-database-info-for-ids reads the Field's own raw :name (sync-owned, not user-settable); its Table join is
|
||||
already routed through table-query."
|
||||
|
||||
"src/metabase/models/db.clj"
|
||||
"field-in-path-query is a nested id-only lookup used as a :where filter, not a value read."
|
||||
|
||||
"src/metabase/warehouses_rest/db.clj"
|
||||
"delete-field-values-for-database! selects only :f.id inside a DELETE's subquery; database-usage-counts only
|
||||
counts rows. autocomplete-fields' Table join now goes through table-query."
|
||||
|
||||
"src/metabase/parameters/db.clj"
|
||||
"both joins bring in the Table only to filter (.active, .db_id) or project its :id; neither selects
|
||||
:schema/:name."
|
||||
|
||||
"enterprise/backend/src/metabase_enterprise/dependencies/db.clj"
|
||||
"the remaining hits select only :id, to build permission and dependency filters. The two that read a Table's
|
||||
display_name -- a user value -- now go through table-query."
|
||||
|
||||
"enterprise/backend/src/metabase_enterprise/remote_sync/db.clj"
|
||||
"remote sync matches and tracks content by its actual physical (db, schema, table) location across instances --
|
||||
overlaying the workspace-remapped name would work against the one thing this module does."
|
||||
|
||||
"enterprise/backend/src/metabase_enterprise/data_complexity_score/db.clj"
|
||||
"active-field-counts-by-table only counts active Fields per table; no Field/Table content column is read."})
|
||||
|
||||
(defn- source-file?
|
||||
[^java.io.File file]
|
||||
(and (.isFile file)
|
||||
(let [name (.getName file)]
|
||||
(or (str/ends-with? name ".clj") (str/ends-with? name ".cljc")))))
|
||||
|
||||
(defn- source-files
|
||||
[]
|
||||
(for [root scanned-roots
|
||||
^java.io.File file (file-seq (io/file root))
|
||||
:when (source-file? file)]
|
||||
(.getPath file)))
|
||||
|
||||
(defn- strip-strings-and-comments
|
||||
"Blank out string-literal and line-comment content in `text`, keeping newlines so line numbers still line up.
|
||||
A naive keyword scan over raw source text would otherwise flag docstrings and comments that merely *mention*
|
||||
`:metabase_table`/`:metabase_field` (see e.g. `metabase.warehouse-schema.schema`'s column-list docstrings)."
|
||||
[^String text]
|
||||
(let [n (.length text)
|
||||
sb (StringBuilder. n)]
|
||||
(loop [i 0, in-str? false]
|
||||
(if (>= i n)
|
||||
(str sb)
|
||||
(let [c (.charAt text i)]
|
||||
(cond
|
||||
(and in-str? (= c \\) (< (inc i) n))
|
||||
(do (.append sb (if (= (.charAt text (inc i)) \newline) "\n " " "))
|
||||
(recur (+ i 2) true))
|
||||
|
||||
(= c \")
|
||||
(do (.append sb \space)
|
||||
(recur (inc i) (not in-str?)))
|
||||
|
||||
in-str?
|
||||
(do (.append sb (if (= c \newline) \newline \space))
|
||||
(recur (inc i) true))
|
||||
|
||||
(= c \;)
|
||||
(let [nl (long (or (str/index-of text "\n" i) n))]
|
||||
(.append sb (apply str (repeat (- nl i) \space)))
|
||||
(recur nl false))
|
||||
|
||||
:else
|
||||
(do (.append sb c)
|
||||
(recur (inc i) false))))))))
|
||||
|
||||
(def ^:private raw-usage-pattern
|
||||
"A `:metabase_table`/`:metabase_field` keyword literal naming a table -- as opposed to `:metabase_field.column` or
|
||||
`:metabase_field/column`, which qualify a column of a source named elsewhere (often `table-query`/`field-query`'s
|
||||
own default alias, which is the raw table name)."
|
||||
#":metabase_(?:table|field)(?![./\w])")
|
||||
|
||||
(defn- raw-usage-hits
|
||||
"Every `{:file :line :text}` where `path`'s code (not its strings or comments) uses the literal keyword to name a
|
||||
table, skipping `t2/table-name` definitions -- the canonical place the keyword has to be spelled out."
|
||||
[path]
|
||||
(let [text (slurp path)
|
||||
code-only (strip-strings-and-comments text)
|
||||
lines (str/split-lines text)
|
||||
code-lines (str/split-lines code-only)]
|
||||
(into []
|
||||
(keep (fn [[line-no orig code]]
|
||||
(when (and (re-find raw-usage-pattern code)
|
||||
(not (str/includes? orig "t2/table-name")))
|
||||
{:file path, :line line-no, :text (str/trim orig)})))
|
||||
(map vector (rest (range)) lines code-lines))))
|
||||
|
||||
(defn- relative-path
|
||||
[path]
|
||||
(str/replace path #"^\./" ""))
|
||||
|
||||
(defn- all-hits
|
||||
[]
|
||||
(into []
|
||||
(mapcat raw-usage-hits)
|
||||
(remove excluded-dir? (map relative-path (source-files)))))
|
||||
|
||||
(defn- excused?
|
||||
[file]
|
||||
(boolean (or (contains? excluded-paths file)
|
||||
(contains? allowed-raw-usages file))))
|
||||
|
||||
(deftest no-raw-metabase-table-or-field-honey-sql-test
|
||||
(testing (str "Hand-written Honey SQL must not name `:metabase_table`/`:metabase_field` directly.\n"
|
||||
"Read from `(metabase.warehouse-schema-overlay.core/table-query)` / `(field-query)` instead"
|
||||
" (or add `{:alias ...}` for a join), or pass `{:user-settings? false}` for a deliberate raw read.\n"
|
||||
"If the hit really is fine as-is (only an id, a count, or a foreign key used to join/filter), review"
|
||||
" it and add it to `allowed-raw-usages` in this test with a one-line reason.")
|
||||
(doseq [{:keys [file line text]} (all-hits)]
|
||||
(testing (format "\n%s:%d\n %s" file line text)
|
||||
(is (excused? file) "unexpected raw metabase_table/metabase_field usage (see message above)")))))
|
||||
|
||||
(deftest stale-allowed-raw-usage-test
|
||||
(testing "every allowed-raw-usages entry still matches at least one hit, so the allowlist doesn't accumulate cruft"
|
||||
(let [files-with-hits (into #{} (map :file) (all-hits))]
|
||||
(doseq [file (keys allowed-raw-usages)]
|
||||
(testing (format "\n%s" file)
|
||||
(is (contains? files-with-hits file)
|
||||
"no raw metabase_table/metabase_field usage found here anymore -- remove this allowlist entry"))))))
|
||||
@@ -7,17 +7,18 @@
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table table-id))
|
||||
(t2/select-one :model/Table :id table-id {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn active-table
|
||||
"The active Table with `table-id`, or nil."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id table-id :active true))
|
||||
(t2/select-one :model/Table :id table-id :active true {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
@@ -27,28 +28,29 @@
|
||||
(mu/defn fields
|
||||
"The Fields with `field-ids`."
|
||||
[field-ids :- [:sequential ::lib.schema.id/field]]
|
||||
(t2/select :model/Field :id [:in field-ids]))
|
||||
(t2/select :model/Field :id [:in field-ids] {:from [(warehouse-schema-overlay/field-query)]}))
|
||||
|
||||
(mu/defn pk-fields-for-table
|
||||
"The active primary key Fields of the Table with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :semantic_type :type/PK :active true))
|
||||
(t2/select :model/Field :table_id table-id :semantic_type :type/PK :active true {:from [(warehouse-schema-overlay/field-query)]}))
|
||||
|
||||
(mu/defn fields-by-name
|
||||
"The Fields of the Table with `table-id` named one of `field-names`."
|
||||
[table-id :- ::lib.schema.id/table
|
||||
field-names :- [:sequential :string]]
|
||||
(t2/select :model/Field :table_id table-id :name [:in field-names]))
|
||||
(t2/select :model/Field :table_id table-id :name [:in field-names] {:from [(warehouse-schema-overlay/field-query)]}))
|
||||
|
||||
(mu/defn active-fields-in-position-order
|
||||
"The active Fields of the Table with `table-id`, in position order."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :active true {:order-by [[:position]]}))
|
||||
(t2/select :model/Field :table_id table-id :active true {:from [(warehouse-schema-overlay/field-query)]
|
||||
:order-by [[:position]]}))
|
||||
|
||||
(mu/defn field-requirements-by-name
|
||||
"A map of name to the name, required flag, and base type of the Fields of the Table with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-fn->fn :name identity [:model/Field :name :database_required :base_type] :table_id table-id))
|
||||
(t2/select-fn->fn :name identity [:model/Field :name :database_required :base_type] :table_id table-id {:from [(warehouse-schema-overlay/field-query)]}))
|
||||
|
||||
(mu/defn category-list-field-ids-by-name
|
||||
"The `:id` and `:lower_name` rows of the category list Fields of the Table with `table-id` whose name matches one
|
||||
@@ -56,7 +58,7 @@
|
||||
[table-id :- ::lib.schema.id/table
|
||||
names :- [:sequential :string]]
|
||||
(t2/query {:select [:id [[:lower :name] :lower_name]]
|
||||
:from [(t2/table-name :model/Field)]
|
||||
:from [(warehouse-schema-overlay/field-query)]
|
||||
:where [:and
|
||||
[:= :table_id table-id]
|
||||
[:in [:lower :name] (map u/lower-case-en names)]
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id table-id))
|
||||
(t2/select-one :model/Table :id table-id {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn group-manager?
|
||||
"Whether the User with `user-id` manages the PermissionsGroup with `group-id`."
|
||||
@@ -42,7 +43,8 @@
|
||||
database-ids :- [:sequential ::lib.schema.id/database]]
|
||||
(t2/query {:select [[:t.db_id :db_id]]
|
||||
:from [[(t2/table-name :model/Sandbox) :s]]
|
||||
:join [[(t2/table-name :model/Table) :t] [:= :s.table_id :t.id]]
|
||||
:join [(warehouse-schema-overlay/table-query {:alias :t, :user-settings? false})
|
||||
[:= :s.table_id :t.id]]
|
||||
:where [:and
|
||||
[:= :s.group_id group-id]
|
||||
[:in :t.db_id database-ids]]}))
|
||||
@@ -83,7 +85,8 @@
|
||||
group-ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/query {:select [[:s.group_id :group_id]]
|
||||
:from [[(t2/table-name :model/Sandbox) :s]]
|
||||
:join [[(t2/table-name :model/Table) :t] [:= :t.id :s.table_id]]
|
||||
:join [(warehouse-schema-overlay/table-query {:alias :t, :user-settings? false})
|
||||
[:= :t.id :s.table_id]]
|
||||
:where [:and
|
||||
[:in :s.group_id group-ids]
|
||||
[:= :t.db_id database-id]]}))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.queries.schema :as queries.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[metabase.warehouse-schema.schema :as warehouse-schema.schema]
|
||||
[metabase.warehouses.schema :as warehouses.schema]
|
||||
[toucan2.core :as t2]))
|
||||
@@ -56,17 +57,18 @@
|
||||
"The Tables of the Database with `database-id` named one of `table-names`."
|
||||
[database-id :- ::lib.schema.id/database
|
||||
table-names :- [:or [:set :string] [:sequential :string]]]
|
||||
(t2/select :model/Table :db_id database-id :name [:in table-names]))
|
||||
(t2/select :model/Table :db_id database-id :name [:in table-names] {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn tables-of-database-in-id-order
|
||||
"The `:id`, `:name`, `:schema`, and `:active` of the Tables of the Database with `database-id`, in ID order."
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select [:model/Table :id :name :schema :active] :db_id database-id {:order-by [[:id :asc]]}))
|
||||
(t2/select [:model/Table :id :name :schema :active] :db_id database-id {:from [(warehouse-schema-overlay/table-query {:user-settings? false})]
|
||||
:order-by [[:id :asc]]}))
|
||||
|
||||
(mu/defn active-public-table-exists?
|
||||
"Whether the Database with `database-id` has an active Table in the `public` schema."
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table :db_id database-id :schema "public" :active true))
|
||||
(t2/exists? :model/Table :db_id database-id :schema "public" :active true {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn table-ids-to-downcase
|
||||
"The IDs of the Metabase-managed Tables of the Database with `database-id` that have no lower-cased counterpart
|
||||
@@ -74,14 +76,14 @@
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(mapv :id
|
||||
(t2/query {:select [:table.id]
|
||||
:from [[(t2/table-name :model/Table) :table]]
|
||||
:from [(warehouse-schema-overlay/table-query {:alias :table, :user-settings? false})]
|
||||
:where [:and [:= :table.db_id database-id]
|
||||
;; Exclude DATABASECHANGELOG, DATABASECHANGELOGLOCK, and QRTZ_* tables, they are not metabase managed
|
||||
[:not= :table.name "DATABASECHANGELOG"]
|
||||
[:not= :table.name "DATABASECHANGELOGLOCK"] ;; new instances do not get this file, but existing instances may have it
|
||||
[:not [:like :table.name "QRTZ_%"]]
|
||||
[:not [:exists ^:allow-subquery {:select [1]
|
||||
:from [[(t2/table-name :model/Table) :self_table]]
|
||||
:from [(warehouse-schema-overlay/table-query {:alias :self_table, :user-settings? false})]
|
||||
:where [:and
|
||||
[:= :self_table.db_id :table.db_id]
|
||||
[:or
|
||||
@@ -102,15 +104,15 @@
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(mapv :id
|
||||
(t2/query {:select [:field.id]
|
||||
:from [[(t2/table-name :model/Field) :field]]
|
||||
:inner-join [[(t2/table-name :model/Table) :table]
|
||||
:from [(warehouse-schema-overlay/field-query {:alias :field})]
|
||||
:inner-join [(warehouse-schema-overlay/table-query {:alias :table})
|
||||
[:= :table.id :field.table_id]]
|
||||
:where [:and [:= :table.db_id database-id]
|
||||
[:not= :table.name "DATABASECHANGELOG"]
|
||||
[:not [:like :table.name "QRTZ_%"]]
|
||||
[:not [:exists ^:allow-subquery {:select [1]
|
||||
:from [[(t2/table-name :model/Field) :self_field]]
|
||||
:inner-join [[(t2/table-name :model/Table) :self_table]
|
||||
:from [(warehouse-schema-overlay/field-query {:alias :self_field})]
|
||||
:inner-join [(warehouse-schema-overlay/table-query {:alias :self_table})
|
||||
[:= :self_table.id :self_field.table_id]]
|
||||
:where [:and
|
||||
[:= :self_table.db_id :table.db_id]
|
||||
@@ -161,7 +163,7 @@
|
||||
(mu/defn field-names-of-table
|
||||
"The `:id` and `:name` of the Fields of the Table with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select [:model/Field :id :name] :table_id table-id))
|
||||
(t2/select [:model/Field :id :name] :table_id table-id {:from [(warehouse-schema-overlay/field-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn card-result-metadata-reducible
|
||||
"Reducible raw `:id` and `:result_metadata` rows of the Cards of the Database with `database-id`."
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
[metabase-enterprise.audit-app.pages.common :as common]
|
||||
[metabase-enterprise.audit-app.pages.common.cards :as cards]
|
||||
[metabase.app-db.core :as mdb]
|
||||
[metabase.audit-app.core :as audit]))
|
||||
[metabase.audit-app.core :as audit]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]))
|
||||
|
||||
;; List of all failing questions
|
||||
(defmethod audit.i/internal-query ::bad-table
|
||||
@@ -75,7 +76,7 @@
|
||||
:from [[:report_card :card]]
|
||||
:left-join [[:collection :coll] [:= :card.collection_id :coll.id]
|
||||
[:metabase_database :db] [:= :card.database_id :db.id]
|
||||
[:metabase_table :t] [:= :card.table_id :t.id]
|
||||
(warehouse-schema-overlay/table-query {:alias :t}) [:= :card.table_id :t.id]
|
||||
[:core_user :u] [:= :card.creator_id :u.id]
|
||||
:latest_qe [:= :card.id :latest_qe.card_id]
|
||||
:query_runs [:= :card.id :query_runs.card_id]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private non-blob-columns
|
||||
@@ -52,13 +53,15 @@
|
||||
(defn table-database-id
|
||||
"The database ID for the Table with `table-id`, or nil."
|
||||
[table-id]
|
||||
(t2/select-one-fn :db_id :model/Table :id table-id))
|
||||
(t2/select-one-fn :db_id :model/Table :id table-id
|
||||
{:from [(warehouse-schema-overlay/table-query {:user-settings? false})]}))
|
||||
|
||||
(defn existing-table-ids
|
||||
"The IDs from `table-ids` that belong to existing Tables."
|
||||
[table-ids]
|
||||
(if (seq table-ids)
|
||||
(t2/select-pks-set :model/Table :id [:in table-ids])
|
||||
(t2/select-pks-set :model/Table :id [:in table-ids]
|
||||
{:from [(warehouse-schema-overlay/table-query {:user-settings? false})]})
|
||||
#{}))
|
||||
|
||||
(defn users-for-permission-warnings
|
||||
@@ -196,7 +199,7 @@
|
||||
:t.schema
|
||||
[:t.db_id :database_id]
|
||||
[:d.name :database_name]]
|
||||
:from [[:metabase_table :t]]
|
||||
:from [(warehouse-schema-overlay/table-query {:alias :t})]
|
||||
:join [[:metabase_database :d] [:= :d.id :t.db_id]]
|
||||
:where [:in :t.id table-ids]
|
||||
:order-by [[:d.name :asc] [:t.schema :asc] [:t.display_name :asc]]}))
|
||||
@@ -222,11 +225,12 @@
|
||||
:from [[:permissions_group_membership :pgm]]
|
||||
:join [[:data_permissions :dp] [:= :dp.group_id :pgm.group_id]
|
||||
[:permissions_group :pg] [:= :pg.id :pgm.group_id]
|
||||
[:metabase_table :t] [:and
|
||||
[:= :t.db_id :dp.db_id]
|
||||
[:or
|
||||
[:= :dp.table_id nil]
|
||||
[:= :dp.table_id :t.id]]]]
|
||||
(warehouse-schema-overlay/table-query {:alias :t, :user-settings? false})
|
||||
[:and
|
||||
[:= :t.db_id :dp.db_id]
|
||||
[:or
|
||||
[:= :dp.table_id nil]
|
||||
[:= :dp.table_id :t.id]]]]
|
||||
:where [:and
|
||||
[:in :pgm.user_id user-ids]
|
||||
[:in :t.id table-ids]
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn active-field-counts-by-table
|
||||
@@ -64,7 +65,7 @@
|
||||
[audit-database-id :- ::lib.schema.id/database]
|
||||
(t2/select [:model/Table :id :name :collection_id :is_published :visibility_type :db_id :data_layer :data_authority]
|
||||
:active true
|
||||
:db_id [:not= audit-database-id]))
|
||||
:db_id [:not= audit-database-id] {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn metabot-by-entity-id
|
||||
"The Metabot with `entity-id`, or nil."
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[metabase.warehouse-schema.models.table-user-settings :as schema.table-user-settings]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private TableSelectors
|
||||
@@ -50,9 +52,9 @@
|
||||
[:not [:in output-table-id tables]])]
|
||||
(t2/reducible-query {:select [[output-table-id :table_id]]
|
||||
:from [[(t2/table-name :model/Dimension) :dim]]
|
||||
:join [[(t2/table-name :model/Field) :source_field]
|
||||
:join [(warehouse-schema-overlay/field-query {:alias :source_field, :user-settings? false})
|
||||
[:= :dim.field_id :source_field.id]
|
||||
[(t2/table-name :model/Field) :target_field]
|
||||
(warehouse-schema-overlay/field-query {:alias :target_field, :user-settings? false})
|
||||
[:= :dim.human_readable_field_id :target_field.id]]
|
||||
:where [:and
|
||||
[:= :dim.type "external"]
|
||||
@@ -66,7 +68,8 @@
|
||||
extra-table-ids :- [:maybe [:set ::lib.schema.id/table]]
|
||||
extra-mode :- [:enum :unpublished :any]]
|
||||
(t2/select-pks-set :model/Table
|
||||
{:where (let [selector-expr (table-selectors-expr selectors)]
|
||||
{:from [(warehouse-schema-overlay/table-query)]
|
||||
:where (let [selector-expr (table-selectors-expr selectors)]
|
||||
(if (seq extra-table-ids)
|
||||
[:or selector-expr (case extra-mode
|
||||
:unpublished [:and [:in :id extra-table-ids] [:= :is_published false]]
|
||||
@@ -76,12 +79,12 @@
|
||||
(mu/defn published-table-ids
|
||||
"The IDs of the published Tables among `table-ids`."
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select-pks-set :model/Table :id [:in table-ids] :is_published true))
|
||||
(t2/select-pks-set :model/Table :id [:in table-ids] :is_published true {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn tables
|
||||
"The Tables with `table-ids`."
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select :model/Table :id [:in table-ids]))
|
||||
(t2/select :model/Table :id [:in table-ids] {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn collection
|
||||
"The Collection with `collection-id`, or nil."
|
||||
@@ -106,12 +109,14 @@
|
||||
"Publish the Tables with `table-ids` into the Collection with `collection-id`, returning the number updated."
|
||||
[table-ids :- [:set ::lib.schema.id/table]
|
||||
collection-id :- ::lib.schema.id/collection]
|
||||
(t2/update! :model/Table :id [:in table-ids] {:collection_id collection-id, :is_published true}))
|
||||
(schema.table-user-settings/upsert-user-settings-for-tables!
|
||||
table-ids {:collection_id collection-id, :is_published true}))
|
||||
|
||||
(mu/defn unpublish-tables!
|
||||
"Unpublish the Tables with `table-ids` and detach them from their Collection, returning the number updated."
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/update! :model/Table :id [:in table-ids] {:collection_id nil, :is_published false}))
|
||||
(schema.table-user-settings/upsert-user-settings-for-tables!
|
||||
table-ids {:collection_id nil, :is_published false}))
|
||||
|
||||
(mu/defn published-table-visible-to-user?
|
||||
"Whether the Table with `table-id` is published in a Collection the User with `user-id` can read."
|
||||
@@ -119,7 +124,8 @@
|
||||
user-id :- ::lib.schema.id/user
|
||||
superuser? :- :boolean]
|
||||
(t2/exists? :model/Table
|
||||
{:where [:and
|
||||
{:from [(warehouse-schema-overlay/table-query)]
|
||||
:where [:and
|
||||
[:= :id table-id]
|
||||
[:= :is_published true]
|
||||
(collection/visible-collection-filter-clause
|
||||
@@ -130,7 +136,8 @@
|
||||
"Whether the current user can read the Collection of any published Table."
|
||||
[]
|
||||
(t2/exists? :model/Table
|
||||
{:where [:and
|
||||
{:from [(warehouse-schema-overlay/table-query)]
|
||||
:where [:and
|
||||
[:= :is_published true]
|
||||
(collection/visible-collection-filter-clause :collection_id)]}))
|
||||
|
||||
@@ -138,7 +145,8 @@
|
||||
"Whether the current user can read the Collection of any published Table in the Database with `database-id`."
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table
|
||||
{:where [:and
|
||||
{:from [(warehouse-schema-overlay/table-query)]
|
||||
:where [:and
|
||||
[:= :db_id database-id]
|
||||
[:= :is_published true]
|
||||
(collection/visible-collection-filter-clause :collection_id)]}))
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.premium-features.core :refer [defenterprise]]
|
||||
[metabase.util.honey-sql-2 :as h2x]))
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]))
|
||||
|
||||
(defenterprise user-published-table-permission
|
||||
"Returns :query-builder permission if table is published and user has collection access.
|
||||
@@ -44,7 +45,7 @@
|
||||
[:in table-id-column
|
||||
^:allow-subquery
|
||||
{:select [:id]
|
||||
:from [:metabase_table]
|
||||
:from [(warehouse-schema-overlay/table-query)]
|
||||
:where [:and
|
||||
[:= :is_published true]
|
||||
(collection/visible-collection-filter-clause
|
||||
@@ -67,7 +68,7 @@
|
||||
{:select [[:mt.id :id]
|
||||
[(h2x/literal :perms/create-queries) :perm_type]
|
||||
[(h2x/literal :query-builder) :perm_value]]
|
||||
:from [[:metabase_table :mt]]
|
||||
:from [(warehouse-schema-overlay/table-query {:alias :mt})]
|
||||
:where (cond-> [:and
|
||||
[:= :mt.is_published true]
|
||||
(collection/visible-collection-filter-clause
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private VisibleOpts
|
||||
@@ -277,7 +278,7 @@
|
||||
nil))]
|
||||
{:table-name (case entity-type
|
||||
:card :report_card
|
||||
:table :metabase_table
|
||||
:table (first (warehouse-schema-overlay/table-query))
|
||||
:transform :transform
|
||||
:snippet :native_query_snippet
|
||||
:dashboard :report_dashboard
|
||||
@@ -427,7 +428,7 @@
|
||||
(:collection joins) (conj :collection [:= :entity.collection_id :collection.id])
|
||||
(:dashboard joins) (conj [:report_dashboard :dashboard] [:= :entity.dashboard_id :dashboard.id])
|
||||
(:document joins) (conj :document [:= :entity.document_id :document.id])
|
||||
(:table joins) (conj [:metabase_table :table] [:= :entity.table_id :table.id])))
|
||||
(:table joins) (conj (warehouse-schema-overlay/table-query {:alias :table}) [:= :entity.table_id :table.id])))
|
||||
|
||||
(defn- dependency-item-select
|
||||
"The per-entity-type SELECT that [[dependency-item-ids]] and [[dependency-item-count]] union together, matching
|
||||
@@ -549,19 +550,28 @@
|
||||
ids :- [:set ::deps.dependency-types/entity-id]]
|
||||
(t2/select (deps.dependency-types/dependency-type->model entity-type) :id [:in ids]))
|
||||
|
||||
(defn- entity-source
|
||||
"What a read of `entity-type` selects from: the overlay for a Table, else the model's own table."
|
||||
[entity-type]
|
||||
(if (= entity-type :table)
|
||||
{:from [(warehouse-schema-overlay/table-query)]}
|
||||
{}))
|
||||
|
||||
(mu/defn instances-with-columns
|
||||
"The `columns` of the instances of the entity type `entity-type` with `ids`."
|
||||
[entity-type :- ::deps.dependency-types/dependency-types
|
||||
columns :- [:sequential :keyword]
|
||||
ids :- [:sequential ::deps.dependency-types/entity-id]]
|
||||
(t2/select (into [(deps.dependency-types/dependency-type->model entity-type)] columns) :id [:in ids]))
|
||||
(t2/select (into [(deps.dependency-types/dependency-type->model entity-type)] columns)
|
||||
:id [:in ids] (entity-source entity-type)))
|
||||
|
||||
(mu/defn instance-with-columns
|
||||
"The `columns` of the instance of the entity type `entity-type` with `id`, or nil."
|
||||
[entity-type :- ::deps.dependency-types/dependency-types
|
||||
columns :- [:sequential :keyword]
|
||||
id :- ::deps.dependency-types/entity-id]
|
||||
(t2/select-one (into [(deps.dependency-types/dependency-type->model entity-type)] columns) :id id))
|
||||
(t2/select-one (into [(deps.dependency-types/dependency-type->model entity-type)] columns)
|
||||
:id id (entity-source entity-type)))
|
||||
|
||||
(mu/defn card
|
||||
"The Card with `card-id`, or nil."
|
||||
@@ -587,19 +597,19 @@
|
||||
(mu/defn tables
|
||||
"The Tables with `table-ids`."
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select :model/Table :id [:in table-ids]))
|
||||
(t2/select :model/Table :id [:in table-ids] {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn table-database-ids
|
||||
"The `:id` and `:db_id` of the Tables with `table-ids`."
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/select [:model/Table :id :db_id] :id [:in table-ids]))
|
||||
(t2/select [:model/Table :id :db_id] :id [:in table-ids] {:from [(warehouse-schema-overlay/table-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn table-id-by-name
|
||||
"The ID of the Table named `table-name` in `schema` of the Database with `db-id`, or nil."
|
||||
[db-id :- ::lib.schema.id/database
|
||||
schema :- [:maybe :string]
|
||||
table-name :- :string]
|
||||
(t2/select-one-fn :id :model/Table :db_id db-id :schema schema :name table-name))
|
||||
(t2/select-one-fn :id :model/Table :db_id db-id :schema schema :name table-name {:from [(warehouse-schema-overlay/table-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn transform-sources
|
||||
"The `:id` and `:source` of the Transforms with `transform-ids`."
|
||||
@@ -857,17 +867,24 @@
|
||||
:limit batch-size})))
|
||||
|
||||
(mu/defn table-ids-with-outdated-findings
|
||||
"The IDs of the Tables of the Database with `db-id` whose dependents were analyzed before the Table or one of
|
||||
its Fields last changed."
|
||||
"The IDs of the Tables of the Database with `db-id` whose dependents were analyzed before the Table, one of its
|
||||
Fields, or one of its Fields' user settings last changed."
|
||||
[db-id :- ::lib.schema.id/database]
|
||||
(t2/select-fn-set :table_id :model/AnalysisFinding
|
||||
{:select [:field_updates/table_id]
|
||||
:from [[^:allow-subquery {:select [[:table/id :table_id]
|
||||
[:table/updated_at :last_table_update]
|
||||
[[:max :field/updated_at] :last_field_update]]
|
||||
:from [[(t2/table-name :model/Table) :table]]
|
||||
:left-join [[(t2/table-name :model/Field) :field]
|
||||
[:= :field/table_id :table/id]]
|
||||
[[:max [:case
|
||||
[:>= :field/updated_at
|
||||
[:coalesce :field_settings/updated_at :field/updated_at]]
|
||||
:field/updated_at
|
||||
:else :field_settings/updated_at]]
|
||||
:last_field_update]]
|
||||
:from [(warehouse-schema-overlay/table-query {:alias :table})]
|
||||
:left-join [(warehouse-schema-overlay/field-query {:alias :field})
|
||||
[:= :field/table_id :table/id]
|
||||
[(t2/table-name :model/FieldUserSettings) :field_settings]
|
||||
[:= :field_settings/field_id :field/id]]
|
||||
:where [:= :table/db_id db-id]
|
||||
:group-by [:table/id
|
||||
:table/updated_at]}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn user-database-exists?
|
||||
@@ -21,7 +22,8 @@
|
||||
(mu/defn upload-table-exists-in-database?
|
||||
"Whether the Database with `database-id` holds an active uploaded Table."
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/exists? :model/Table {:where [:and
|
||||
(t2/exists? :model/Table {:from [(warehouse-schema-overlay/table-query)]
|
||||
:where [:and
|
||||
[:= :active true]
|
||||
[:= :is_upload true]
|
||||
[:= :db_id database-id]]}))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn library-cards-in-collections
|
||||
@@ -25,7 +26,8 @@
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]
|
||||
id :- [:maybe ::lib.schema.id/table]]
|
||||
(t2/select [:model/Table :id :name :display_name :description]
|
||||
{:where [:and
|
||||
{:from [(warehouse-schema-overlay/table-query)]
|
||||
:where [:and
|
||||
[:in :collection_id collection-ids]
|
||||
[:= :is_published true]
|
||||
[:= :active true]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[metabase.warehouse-schema.models.table :as schema.table]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
@@ -22,7 +23,8 @@
|
||||
opts :- [:maybe ActiveTablesOpts]]
|
||||
(let [{:keys [table-ids schema]} opts]
|
||||
(t2/select :model/Table
|
||||
{:select columns
|
||||
{:from [(warehouse-schema-overlay/table-query)]
|
||||
:select columns
|
||||
:where (cond-> [:and [:= :db_id database-id] [:= :active true]]
|
||||
(contains? opts :table-ids) (conj [:in :id table-ids])
|
||||
(contains? opts :schema) (conj (if (= schema "")
|
||||
@@ -32,7 +34,8 @@
|
||||
(mu/defn active-fields-for-tables
|
||||
"The active Fields of the Tables with `table-ids`, in field order."
|
||||
[table-ids :- [:set ::lib.schema.id/table]]
|
||||
(t2/select :model/Field {:where [:and
|
||||
(t2/select :model/Field {:from [(warehouse-schema-overlay/field-query)]
|
||||
:where [:and
|
||||
[:in :table_id table-ids]
|
||||
[:= :active true]]
|
||||
:order-by schema.table/field-order-rule}))
|
||||
@@ -40,6 +43,7 @@
|
||||
(mu/defn active-fields
|
||||
"The active Fields with `field-ids`."
|
||||
[field-ids :- [:or [:set ::lib.schema.id/field] [:sequential ::lib.schema.id/field]]]
|
||||
(t2/select :model/Field {:where [:and
|
||||
(t2/select :model/Field {:from [(warehouse-schema-overlay/field-query)]
|
||||
:where [:and
|
||||
[:in :id field-ids]
|
||||
[:= :active true]]}))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[metabase.collections.models.collection :as collection]
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn card-types-in-collections
|
||||
@@ -15,7 +16,7 @@
|
||||
(mu/defn published-table-in-collections?
|
||||
"Whether a published Table exists in the Collections with `collection-ids`."
|
||||
[collection-ids :- [:sequential ::lib.schema.id/collection]]
|
||||
(t2/exists? :model/Table :is_published true :collection_id [:in collection-ids]))
|
||||
(t2/exists? :model/Table :is_published true :collection_id [:in collection-ids] {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn library-collections
|
||||
"The readable Library, Library-data, and Library-metrics Collections, ordered by name."
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn user-superuser?
|
||||
@@ -20,7 +21,7 @@
|
||||
(defn- blocked-tables-select
|
||||
[user-id tables-expr permissions-blocking permissions-granting]
|
||||
{:select [[:db.name :db_name] :blocked.schema [:blocked.name :table_name] [:pg.name :group_name]]
|
||||
:from [[:metabase_table :blocked]]
|
||||
:from [(warehouse-schema-overlay/table-query {:alias :blocked, :user-settings? false})]
|
||||
:join [[(perms/select-tables-and-groups-granting-perm
|
||||
{:user-id user-id :is-superuser? false}
|
||||
permissions-blocking) :perm_grant] [:= :blocked.id :perm_grant.id]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(def ^:private Conditions
|
||||
@@ -134,11 +135,15 @@
|
||||
(t2/select-one model :id id))
|
||||
|
||||
(mu/defn instance-with-columns
|
||||
"The `columns` of the instance of `model` with `id`, or nil."
|
||||
"The `columns` of the instance of `model` with `id`, or nil; a Table is read through the overlay."
|
||||
[model :- :keyword
|
||||
columns :- [:sequential :keyword]
|
||||
id :- ms/PositiveInt]
|
||||
(t2/select-one (into [model] columns) :id id))
|
||||
(t2/select-one (into [model] columns)
|
||||
:id id
|
||||
(if (= model :model/Table)
|
||||
{:from [(warehouse-schema-overlay/table-query)]}
|
||||
{})))
|
||||
|
||||
(mu/defn instance-names
|
||||
"The `:id` and `:name` of the instances of `model` with `ids`."
|
||||
@@ -178,15 +183,15 @@
|
||||
:model/Field {:alias "f"
|
||||
:select [:f.name :f.table_id [:t.collection_id :collection_id] [:t.name :table_name]]
|
||||
:from [[:metabase_field :f]]
|
||||
:join [[:metabase_table :t] [:= :f.table_id :t.id]]}
|
||||
:join [(warehouse-schema-overlay/table-query {:alias :t}) [:= :f.table_id :t.id]]}
|
||||
:model/Segment {:alias "s"
|
||||
:select [:s.name :s.table_id [:t.collection_id :collection_id] [:t.name :table_name]]
|
||||
:from [[:segment :s]]
|
||||
:join [[:metabase_table :t] [:= :s.table_id :t.id]]}
|
||||
:join [(warehouse-schema-overlay/table-query {:alias :t}) [:= :s.table_id :t.id]]}
|
||||
:model/Measure {:alias "s"
|
||||
:select [:s.name :s.table_id [:t.collection_id :collection_id] [:t.name :table_name]]
|
||||
:from [[:measure :s]]
|
||||
:join [[:metabase_table :t] [:= :s.table_id :t.id]]}))
|
||||
:join [(warehouse-schema-overlay/table-query {:alias :t}) [:= :s.table_id :t.id]]}))
|
||||
|
||||
(mu/defn tracking-details-by-id
|
||||
"The name, table id, collection id, and table name of the `model-key` (Field, Segment, or Measure) instance with
|
||||
@@ -269,10 +274,15 @@
|
||||
[card-ids :- [:sequential ::lib.schema.id/card]]
|
||||
(t2/select [:model/Card :id :type :card_schema] :id [:in card-ids]))
|
||||
|
||||
(mu/defn field-user-settings-exist?
|
||||
"Whether the Field with `field-id` has FieldUserSettings."
|
||||
[field-id :- ::lib.schema.id/field]
|
||||
(t2/exists? :model/FieldUserSettings :field_id field-id))
|
||||
(mu/defn user-settings-exist-for-table?
|
||||
"Whether the Table with `table-id`, or any of its Fields, has a user-settings row."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(or (t2/exists? :model/TableUserSettings :table_id table-id)
|
||||
(t2/exists? :model/FieldUserSettings
|
||||
{:from [[(t2/table-name :model/FieldUserSettings) :u]]
|
||||
:join [(warehouse-schema-overlay/field-query {:alias :f :user-settings? false})
|
||||
[:= :f.id :u.field_id]]
|
||||
:where [:= :f.table_id table-id]})))
|
||||
|
||||
(mu/defn snippets
|
||||
"The `:id`, `:name`, and `:collection_id` of every NativeQuerySnippet."
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
Tracked model types:
|
||||
- Card, Dashboard, Document, NativeQuerySnippet, Timeline, Collection
|
||||
- Table (when published in a remote-synced collection)
|
||||
- Field, Segment (when belonging to a published table in a remote-synced collection)
|
||||
- Table and TableUserSettings, which also carries a Field's own edits (when published in a remote-synced collection)
|
||||
- Segment, Measure (when belonging to a published table in a remote-synced collection)
|
||||
- Transform, TransformTag, transforms-namespace Collections (when remote-sync-transforms setting is enabled)
|
||||
- NativeQuerySnippet, snippets-namespace Collections (when Library is remote-synced)"
|
||||
(:require
|
||||
@@ -244,7 +244,7 @@
|
||||
|
||||
;;; --------------------------------- Spec-based Event Registration (Non-Collection) -----------------------------------
|
||||
|
||||
(doseq [[_model-key model-spec] (dissoc spec/remote-sync-specs :model/Collection :model/Field)]
|
||||
(doseq [[_model-key model-spec] (dissoc spec/remote-sync-specs :model/Collection :model/Field :model/Table)]
|
||||
(register-events-for-spec! model-spec))
|
||||
|
||||
;;; ----------------------------------------- Collection Event Handler -------------------------------------------------
|
||||
@@ -300,27 +300,51 @@
|
||||
(log/infof "Collection %s no longer needs syncing, marking as removed" (:id object))
|
||||
(create-or-update-remote-sync-object-entry! "Collection" (:id object) "removed" hydrate-collection-details)))))
|
||||
|
||||
;;; ----------------------------------------- FieldUserSettings Tracking -----------------------------------------------
|
||||
;; When a field is updated in a published table, also track any FieldUserSettings row for that field.
|
||||
;; FieldUserSettings has no separate event; it piggybacks on :event/field-update.
|
||||
;;; ----------------------------------------- TableUserSettings Tracking -----------------------------------------------
|
||||
|
||||
(def ^:private table-spec (get spec/remote-sync-specs :model/Table))
|
||||
(def ^:private field-spec (get spec/remote-sync-specs :model/Field))
|
||||
|
||||
(defn- hydrate-table-user-settings-details
|
||||
"The RemoteSyncObject details of the Table with `table-id`, as its own `:table_id`/`:table_name`."
|
||||
[table-id]
|
||||
(let [details (spec/hydrate-model-details table-spec table-id)]
|
||||
(assoc details :table_id (:id details) :table_name (:name details))))
|
||||
|
||||
(defn- sync-table-user-settings!
|
||||
"Track the Table's TableUserSettings when eligible and any of its user settings exist, else mark it removed."
|
||||
[table-id eligible?]
|
||||
(cond
|
||||
(and eligible? (remote-sync.db/user-settings-exist-for-table? table-id))
|
||||
(create-or-update-remote-sync-object-entry!
|
||||
"TableUserSettings" table-id "update" hydrate-table-user-settings-details)
|
||||
|
||||
(and (not eligible?)
|
||||
(remote-sync.db/rso-exists? "TableUserSettings" table-id))
|
||||
(create-or-update-remote-sync-object-entry!
|
||||
"TableUserSettings" table-id "removed" hydrate-table-user-settings-details)))
|
||||
|
||||
(events/derive! :event/field-update ::field-update-event)
|
||||
(events/derive! ::field-update-event :metabase/event)
|
||||
|
||||
(methodical/defmethod events/publish-event! ::field-update-event
|
||||
[_topic {:keys [object]}]
|
||||
(let [field-id (:id object)
|
||||
eligible? (spec/check-eligibility field-spec object)]
|
||||
(cond
|
||||
(and eligible? (remote-sync.db/field-user-settings-exist? field-id))
|
||||
(create-or-update-remote-sync-object-entry!
|
||||
"FieldUserSettings" field-id "update"
|
||||
(fn [id] (spec/hydrate-model-details field-spec id)))
|
||||
(when-let [table-id (:table_id object)]
|
||||
(sync-table-user-settings! table-id (spec/check-eligibility field-spec object))))
|
||||
|
||||
(and (not eligible?)
|
||||
(remote-sync.db/rso-exists? "FieldUserSettings" field-id))
|
||||
(create-or-update-remote-sync-object-entry!
|
||||
"FieldUserSettings" field-id "removed"
|
||||
(fn [id] (spec/hydrate-model-details field-spec id))))))
|
||||
(defn- handle-table-event!
|
||||
"The generic Table handler plus the Table's TableUserSettings; one handler, since a second primary method on the
|
||||
same events would displace the generic one."
|
||||
[topic {:keys [object] :as event}]
|
||||
(handle-model-event-from-spec table-spec topic event)
|
||||
(sync-table-user-settings! (:id object) (spec/check-eligibility table-spec object)))
|
||||
|
||||
(let [event-kws (spec/event-keywords table-spec)
|
||||
parent-kw (:parent event-kws)]
|
||||
(events/derive! parent-kw :metabase/event)
|
||||
(doseq [[_event-type event-kw] (dissoc event-kws :parent)]
|
||||
(events/derive! event-kw parent-kw))
|
||||
(methodical/add-primary-method!
|
||||
#'events/publish-event!
|
||||
parent-kw
|
||||
(fn [topic event] (handle-table-event! topic event))))
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
Returns:
|
||||
- [{:model_type :model_id :path :content_hash}] (entities that fail to serialize are omitted)"
|
||||
[rows repo-paths]
|
||||
(let [storage-opts (serdes/storage-base-context)
|
||||
(let [storage-opts (source/storage-context)
|
||||
repo-by-eid (into {} (map (fn [{:keys [model_type entity_id path]}] [[model_type entity_id] path])) repo-paths)
|
||||
serialize (fn [model-type opts id->eid instance]
|
||||
(try
|
||||
@@ -271,9 +271,9 @@
|
||||
(mapcat (fn [[model-type model-rows]]
|
||||
(let [spec (spec/spec-for-model-type model-type)
|
||||
model-key (:model-key spec)
|
||||
opts {:filter-column :id
|
||||
:filter-ids (mapv :model_id model-rows)
|
||||
:skip-archived true}
|
||||
opts (merge spec/git-sync-extract-opts
|
||||
{:filter-column :id
|
||||
:filter-ids (mapv :model_id model-rows)})
|
||||
;; entity-id models: map local id -> entity_id so we can look up the repo path
|
||||
id->eid (when (and model-key (= :entity-id (:identity spec)))
|
||||
(remote-sync.db/entity-ids-by-id model-key (mapv :model_id model-rows)))]
|
||||
@@ -839,11 +839,11 @@
|
||||
Return:
|
||||
- [[row entity]] (if no entity, then omit)"
|
||||
[{:keys [model_type rows]}]
|
||||
(let [pk-col (spec/pk-col model_type)
|
||||
(let [pk-col (serdes/primary-key model_type)
|
||||
id->row (u/index-by :model_id rows)
|
||||
opts {:filter-column pk-col
|
||||
:filter-ids (mapv :model_id rows)
|
||||
:skip-archived true}]
|
||||
opts (merge spec/git-sync-extract-opts
|
||||
{:filter-column pk-col
|
||||
:filter-ids (mapv :model_id rows)})]
|
||||
;; extract-one must run inside the extract-query reduction, while its ResultSet is open
|
||||
(into [] (keep (fn [instance]
|
||||
(when-let [row (id->row (get instance pk-col))]
|
||||
@@ -989,7 +989,7 @@
|
||||
- {:writes [{:id :model_type :model_id :file_path}] :delete-paths [path] :removed-ids [id]}, or
|
||||
- :remote-sync/incremental-not-possible when any row can't go incrementally"
|
||||
[snapshot rows]
|
||||
(let [opts (serdes/storage-base-context)
|
||||
(let [opts (source/storage-context)
|
||||
;; create/update rows on entity-id models need an entity (extracted per chunk); everything else
|
||||
;; (removed/delete, non-entity-id, bad status) is decided with no entity
|
||||
{cu-rows true other-rows false} (group-by #(boolean (and (#{"create" "update"} (:status %))
|
||||
@@ -1104,7 +1104,7 @@
|
||||
total (count export-rows)
|
||||
span (- export-progress-serialize export-progress-plan-done)]
|
||||
(report export-progress-plan-done {:force? true})
|
||||
(let [opts (serdes/storage-base-context)
|
||||
(let [opts (source/storage-context)
|
||||
[synced version] (commit-staged! snapshot message
|
||||
(fn [commit]
|
||||
(source.p/replace-all! commit) ; replace the managed dirs wholesale
|
||||
@@ -1136,7 +1136,7 @@
|
||||
total (max 1 (count writes))
|
||||
span (- export-progress-serialize export-progress-plan-done)]
|
||||
(report export-progress-plan-done {:force? true})
|
||||
(let [opts (serdes/storage-base-context)
|
||||
(let [opts (source/storage-context)
|
||||
[synced version] (commit-staged! snapshot message
|
||||
(fn [commit]
|
||||
(let [synced (stage-writes commit opts writes
|
||||
|
||||
@@ -68,6 +68,11 @@
|
||||
(atom nil) (atom []))
|
||||
(seq root-dependencies) (ingestable/wrap-root-dep-ingestable root-dependencies)))
|
||||
|
||||
(defn storage-context
|
||||
"The serdes storage context for git sync: a Table's user settings file stands in for the Table's own."
|
||||
[]
|
||||
(assoc (serdes/storage-base-context) :inline-user-settings true))
|
||||
|
||||
(defn entity->path
|
||||
"The repo-relative path an extracted `entity` serializes to, using storage context `opts`."
|
||||
[opts entity]
|
||||
@@ -84,7 +89,7 @@
|
||||
|
||||
(defn entity->file-spec
|
||||
"Serializes a single extracted entity into a `{:path :content}` file spec, using storage context
|
||||
`opts` (from `serdes/storage-base-context`)."
|
||||
`opts` (from [[storage-context]])."
|
||||
[opts entity]
|
||||
{:path (entity->path opts entity)
|
||||
:content (entity->content entity)})
|
||||
@@ -99,7 +104,7 @@
|
||||
can't be extracted. Hashes the live DB serialization (never on-disk bytes), so it's stable across sync points."
|
||||
[row]
|
||||
(when-let [entity (first (spec/extract-entities-for-rows [row]))]
|
||||
(content-hash (:content (entity->file-spec (serdes/storage-base-context) entity)))))
|
||||
(content-hash (:content (entity->file-spec (storage-context) entity)))))
|
||||
|
||||
(defn serialize-specs
|
||||
"Serializes a stream of entities into an eager vector of `{:path :content}` file specs. Reports progress
|
||||
@@ -108,7 +113,7 @@
|
||||
|
||||
Throws Exception if any entity in the stream is an Exception instance."
|
||||
[stream task-id]
|
||||
(let [opts (serdes/storage-base-context)
|
||||
(let [opts (storage-context)
|
||||
stream-count (bounded-count 10000 stream)]
|
||||
(into []
|
||||
(map-indexed (fn [idx entity]
|
||||
|
||||
@@ -819,15 +819,14 @@
|
||||
;;; ---------------------------------------------------- Hydration -----------------------------------------------------
|
||||
|
||||
(defn hydrate-model-details
|
||||
"Hydrates model details for RemoteSyncObject based on spec.
|
||||
Returns a map with the fields needed to populate the sync object."
|
||||
"Returns a map with the fields (always including :id) needed to populate a RemoteSyncObject for `spec`."
|
||||
[{:keys [model-key tracking]} model-id]
|
||||
(if (:hydrate-query? tracking)
|
||||
;; Use the tracking-join query for models needing a Table join (Field, Segment, Measure)
|
||||
(remote-sync.db/tracking-details-by-id model-key model-id)
|
||||
;; Simple select using select-fields
|
||||
(when-let [fields (:select-fields tracking)]
|
||||
(remote-sync.db/instance-with-columns model-key fields model-id))))
|
||||
(remote-sync.db/instance-with-columns model-key (into [:id] fields) model-id))))
|
||||
|
||||
(defn build-sync-object-fields
|
||||
"Builds the fields map for RemoteSyncObject from hydrated model details.
|
||||
@@ -1136,14 +1135,17 @@
|
||||
|
||||
(defmethod query-export-roots :default [_] nil)
|
||||
|
||||
(def ^:private git-sync-extract-opts
|
||||
"Serdes extraction opts for git sync. :user-edits-only restricts field-level export to
|
||||
FieldUserSettings (user-curated metadata only), omitting the full Field YAML for every column."
|
||||
(def git-sync-extract-opts
|
||||
"Serdes extraction opts for git sync."
|
||||
{:include-field-values false
|
||||
:include-database-secrets false
|
||||
:continue-on-error false
|
||||
:skip-archived true
|
||||
:user-edits-only true})
|
||||
:inline-user-settings true})
|
||||
|
||||
(def ^:private models-traversed-but-not-stored
|
||||
"Models git sync walks through but never writes."
|
||||
#{"Table" "Field"})
|
||||
|
||||
(defn exportable-entities
|
||||
"What a full export would serialize: a map of {model-name [id ...]} — the export roots plus their transitive
|
||||
@@ -1155,12 +1157,7 @@
|
||||
targets (-> #{}
|
||||
(into (keys (u/traverse root-targets #(serdes/descendants (first %) (second %) git-sync-extract-opts))))
|
||||
(into (keys (u/traverse root-targets #(serdes/required (first %) (second %))))))]
|
||||
(u/group-by first second targets)))
|
||||
|
||||
(defn pk-col
|
||||
"Returns the PK column keyword for `model`. FieldUserSettings uses :field_id; all others use :id."
|
||||
[model]
|
||||
(if (= model "FieldUserSettings") :field_id :id))
|
||||
(apply dissoc (u/group-by first second targets) models-traversed-but-not-stored)))
|
||||
|
||||
(defn extract-entities-for-export
|
||||
"Extracts all entities for remote-sync export based on enabled specs.
|
||||
@@ -1173,9 +1170,9 @@
|
||||
3. Are in one of the provided collections (or descendants)"
|
||||
[]
|
||||
(eduction (map (fn [[model ids]]
|
||||
(serdes/extract-all model {:filter-column (pk-col model)
|
||||
:filter-ids (vec ids)
|
||||
:skip-archived true})))
|
||||
(serdes/extract-all model (merge git-sync-extract-opts
|
||||
{:filter-column (serdes/primary-key model)
|
||||
:filter-ids (vec ids)}))))
|
||||
cat
|
||||
(exportable-entities)))
|
||||
|
||||
@@ -1186,8 +1183,8 @@
|
||||
[rows]
|
||||
(let [by-model (u/group-by :model_type :model_id conj #{} rows)]
|
||||
(eduction (map (fn [[model ids]]
|
||||
(serdes/extract-all model {:filter-column (pk-col model)
|
||||
:filter-ids (vec ids)
|
||||
:skip-archived true})))
|
||||
(serdes/extract-all model (merge git-sync-extract-opts
|
||||
{:filter-column (serdes/primary-key model)
|
||||
:filter-ids (vec ids)}))))
|
||||
cat
|
||||
by-model)))
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[metabase.util.honey-sql-2 :as h2x]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema.schema :as warehouse-schema.schema]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn run
|
||||
@@ -75,7 +75,7 @@
|
||||
(mu/defn tables-with-ids
|
||||
"The Tables with `ids`."
|
||||
[ids :- [:sequential ms/PositiveInt]]
|
||||
(t2/select :model/Table :id [:in ids]))
|
||||
(t2/select :model/Table :id [:in ids] {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn dashboards-with-ids
|
||||
"The Dashboards with `ids`."
|
||||
@@ -182,28 +182,22 @@
|
||||
(mu/defn table-database-id
|
||||
"The Database ID of the Table with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one-fn :db_id :model/Table :id table-id))
|
||||
(t2/select-one-fn :db_id :model/Table :id table-id {:from [(warehouse-schema-overlay/table-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn active-fields-of-table
|
||||
"The active Fields of the Table with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :active true))
|
||||
(t2/select :model/Field :table_id table-id :active true {:from [(warehouse-schema-overlay/field-query)]}))
|
||||
|
||||
(mu/defn active-field-ids-of-table
|
||||
"The IDs of the active Fields of the Table with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-pks-set :model/Field :table_id table-id :active true))
|
||||
(t2/select-pks-set :model/Field :table_id table-id :active true {:from [(warehouse-schema-overlay/field-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn active-fk-to-fields-exists?
|
||||
"Whether an active Field points at one of the Fields with `field-ids`."
|
||||
[field-ids :- [:set ::lib.schema.id/field]]
|
||||
(t2/exists? :model/Field :fk_target_field_id [:in field-ids] :active true))
|
||||
|
||||
(mu/defn update-field!
|
||||
"Apply `changes` to the Field with `field-id`, returning the number updated."
|
||||
[field-id :- ::lib.schema.id/field
|
||||
changes :- ::warehouse-schema.schema/field.update]
|
||||
(t2/update! :model/Field field-id changes))
|
||||
(t2/exists? :model/Field :fk_target_field_id [:in field-ids] :active true {:from [(warehouse-schema-overlay/field-query)]}))
|
||||
|
||||
(mu/defn sandbox-exists-for-table?
|
||||
"Whether a Sandbox is defined on the Table with `table-id`."
|
||||
|
||||
@@ -169,8 +169,8 @@
|
||||
[:description :display_name :semantic_type :fk_target_field_id :settings :visibility_type])
|
||||
|
||||
(defn- copy-model-metadata-overrides!
|
||||
"Copy user-edited metadata from a model's result_metadata onto the Fields of the
|
||||
output table. Writes to both Field and FieldUserSettings so overrides survive sync."
|
||||
"Copy user-edited metadata from a model's result_metadata onto the FieldUserSettings of the
|
||||
output table's Fields, so the overrides keep showing on the new Fields after the swap."
|
||||
[card-id table-id]
|
||||
(let [card (replacement.db/card card-id)
|
||||
result-metadata (:result_metadata card)
|
||||
@@ -180,7 +180,6 @@
|
||||
:let [field (field-by-name (source-swap.util/column-match-key col-meta))
|
||||
overrides (u/select-keys-when col-meta :non-nil metadata-override-keys)]
|
||||
:when (and field (seq overrides))]
|
||||
(replacement.db/update-field! (:id field) overrides)
|
||||
(field-user-settings/upsert-user-settings field overrides))))
|
||||
|
||||
(defn run-swap-model-with-transform!
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[metabase.users.schema :as users.schema]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn sandbox
|
||||
@@ -60,7 +61,8 @@
|
||||
(t2/select :model/Sandbox
|
||||
{:select [:s.group_id :s.table_id :t.db_id :t.schema]
|
||||
:from [[:sandboxes :s]]
|
||||
:join [[:metabase_table :t] [:= :s.table_id :t.id]]
|
||||
:join [(warehouse-schema-overlay/table-query {:alias :t, :user-settings? false})
|
||||
[:= :s.table_id :t.id]]
|
||||
:where [:and
|
||||
(when group-id [:= :s.group_id group-id])
|
||||
(when group-ids [:in :s.group_id group-ids])
|
||||
@@ -79,7 +81,7 @@
|
||||
[:table.db_id :db_id]
|
||||
[:table.schema :schema]]
|
||||
:from [[:sandboxes]]
|
||||
:left-join [[:metabase_table :table]
|
||||
:left-join [(warehouse-schema-overlay/table-query {:alias :table, :user-settings? false})
|
||||
[:= :sandboxes.table_id :table.id]]
|
||||
:where [:and
|
||||
[:in :sandboxes.group_id group-ids]
|
||||
@@ -149,7 +151,7 @@
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table :id table-id))
|
||||
(t2/select-one :model/Table :id table-id {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn tables-of-database
|
||||
"The `:id`, `:db_id`, and `:schema` of the Tables of the Database with `db-id`, restricted to `schema` when
|
||||
@@ -158,7 +160,8 @@
|
||||
schema-only? :- :boolean
|
||||
schema :- [:maybe :string]]
|
||||
(t2/select [:model/Table :id :db_id :schema]
|
||||
{:where [:and
|
||||
{:from [(warehouse-schema-overlay/table-query {:user-settings? false})]
|
||||
:where [:and
|
||||
[:= :db_id db-id]
|
||||
(when schema-only?
|
||||
[:= :schema schema])]}))
|
||||
@@ -168,14 +171,14 @@
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Database
|
||||
:id ^:allow-subquery {:select [:t.db_id]
|
||||
:from [[(t2/table-name :model/Table) :t]]
|
||||
:from [(warehouse-schema-overlay/table-query {:alias :t, :user-settings? false})]
|
||||
:where [:= :t.id table-id]}))
|
||||
|
||||
(mu/defn fields-of-table-named
|
||||
"The `:id` and `:name` of the Fields of the Table with `table-id` named one of `field-names`."
|
||||
[table-id :- ::lib.schema.id/table
|
||||
field-names :- [:set :string]]
|
||||
(t2/select [:model/Field :id :name] :table_id table-id :name [:in field-names]))
|
||||
(t2/select [:model/Field :id :name] :table_id table-id :name [:in field-names] {:from [(warehouse-schema-overlay/field-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn cards-by-id
|
||||
"A map of Card ID to the query, result metadata, and schema of the Cards with `card-ids`."
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[metabase.search.scoring :as search.scoring]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn library-root-collections
|
||||
@@ -27,7 +28,8 @@
|
||||
authoritative."
|
||||
[]
|
||||
(t2/reducible-select [:model/Table :id :is_published :data_layer :data_authority]
|
||||
{:where [:and
|
||||
{:from [(warehouse-schema-overlay/table-query)]
|
||||
:where [:and
|
||||
[:= :active true]
|
||||
[:or [:= :is_published true]
|
||||
[:= :data_authority ^:allow-raw-sql [:inline "authoritative"]]]]}))
|
||||
|
||||
@@ -166,11 +166,9 @@
|
||||
;; so cards that reference them can still be exported and imported correctly
|
||||
(and analytics-cards (contains? by-model "Card"))
|
||||
(update "Card" (fn [ids] (vec (remove analytics-cards ids)))))
|
||||
;; FieldUserSettings has a non-standard PK (field_id, not id) — use the right column.
|
||||
pk-col (fn [model] (if (= model "FieldUserSettings") :field_id :id))
|
||||
extract-by-ids (fn [[model ids]]
|
||||
(serdes/extract-all model (merge opts {:collection-set coll-set
|
||||
:filter-column (pk-col model)
|
||||
:filter-column (serdes/primary-key model)
|
||||
:filter-ids (vec ids)})))
|
||||
extract-all (fn [model]
|
||||
(serdes/extract-all model (assoc opts :collection-set coll-set)))]
|
||||
|
||||
@@ -5,19 +5,18 @@
|
||||
"Schema model types"
|
||||
["Database"
|
||||
"Field"
|
||||
"FieldUserSettings"
|
||||
"Measure"
|
||||
"Segment"
|
||||
"Table"
|
||||
"TableUserSettings"
|
||||
"Channel"])
|
||||
|
||||
(def data-model-in-collection
|
||||
"Data model types that can be found in collections (via published tables).
|
||||
These are extracted by ID when discovered via descendants, even if no-data-model is set.
|
||||
Includes both Field (full serdes) and FieldUserSettings (user-edits-only / git sync)."
|
||||
"Data model types that can be found in collections (via published tables), extracted by ID when discovered via
|
||||
descendants even if no-data-model is set."
|
||||
["Table"
|
||||
"TableUserSettings"
|
||||
"Field"
|
||||
"FieldUserSettings"
|
||||
"Segment"])
|
||||
|
||||
(def content
|
||||
@@ -53,6 +52,7 @@
|
||||
["DashboardCard"
|
||||
"DashboardTab"
|
||||
"Dimension"
|
||||
"FieldUserSettings"
|
||||
"ParameterCard"
|
||||
"DashboardCardSeries"
|
||||
"MetabotPrompt"
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn table-source-rows
|
||||
"The ID, name, schema, and Database ID of the Tables with `table-ids`, as source info."
|
||||
[table-ids :- [:or [:set ::lib.schema.id/table] [:sequential ::lib.schema.id/table]]]
|
||||
(t2/select [:model/Table [:id :table-id] [:name :table-name] :schema [:db_id :db-id]] :id [:in table-ids]))
|
||||
(t2/select [:model/Table [:id :table-id] [:name :table-name] :schema [:db_id :db-id]] :id [:in table-ids] {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn database-engine
|
||||
"The engine of the Database with `database-id`."
|
||||
@@ -19,4 +20,4 @@
|
||||
(mu/defn active-fields-for-table
|
||||
"The active Fields of the Table with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select :model/Field :table_id table-id :active true))
|
||||
(t2/select :model/Field :table_id table-id :active true {:from [(warehouse-schema-overlay/field-query)]}))
|
||||
|
||||
@@ -6,17 +6,18 @@
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.util.malli.schema :as ms]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn table-database-ids
|
||||
"The set of Database IDs of the Tables with `table-ids`."
|
||||
[table-ids :- [:sequential ::lib.schema.id/table]]
|
||||
(t2/select-fn-set :db_id [:model/Table :db_id] :id [:in table-ids]))
|
||||
(t2/select-fn-set :db_id [:model/Table :db_id] :id [:in table-ids] {:from [(warehouse-schema-overlay/table-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn table-database-id
|
||||
"The Database ID of the raw table row with `table-id`."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one-fn :db_id (t2/table-name :model/Table) :id table-id))
|
||||
(t2/select-one-fn :db_id (t2/table-name :model/Table) :id table-id {:from [(warehouse-schema-overlay/table-query {:user-settings? false})]}))
|
||||
|
||||
(mu/defn database
|
||||
"The Database with `database-id`, or nil."
|
||||
@@ -66,4 +67,5 @@
|
||||
;; we are only interested in top-level objects, so filter out nested fields (parent or path)
|
||||
:parent_id nil
|
||||
:nfc_path nil
|
||||
{:order-by [[:database_position :asc]]}))
|
||||
{:from [(warehouse-schema-overlay/field-query)]
|
||||
:order-by [[:database_position :asc]]}))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(mu/defn attached-dwh-database-id
|
||||
@@ -14,14 +15,15 @@
|
||||
(mu/defn non-upload-tables-for-database
|
||||
"The active Tables of the Database with `database-id` that were not uploaded."
|
||||
[database-id :- ::lib.schema.id/database]
|
||||
(t2/select :model/Table :db_id database-id :active true :is_upload false))
|
||||
(t2/select :model/Table :db_id database-id :active true :is_upload false {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(mu/defn upload-tables
|
||||
"The active uploaded Tables, ordered by name."
|
||||
[]
|
||||
(t2/select :model/Table :active true :is_upload true {:order-by [[:name :asc]]}))
|
||||
(t2/select :model/Table :active true :is_upload true {:from [(warehouse-schema-overlay/table-query)]
|
||||
:order-by [[:name :asc]]}))
|
||||
|
||||
(mu/defn table
|
||||
"The Table with `table-id`, or nil."
|
||||
[table-id :- ::lib.schema.id/table]
|
||||
(t2/select-one :model/Table table-id))
|
||||
(t2/select-one :model/Table :id table-id {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
(ns metabase-enterprise.action-v2.db-test
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.action-v2.db :as action-v2.db]
|
||||
[metabase.test :as mt]))
|
||||
|
||||
(deftest pk-fields-for-table-honors-user-set-semantic-type-test
|
||||
(testing "pk-fields-for-table honors a user-set semantic_type that differs from the sync value"
|
||||
(mt/with-temp [:model/Table {table-id :id} {}
|
||||
:model/Field {field-id :id} {:table_id table-id, :semantic_type nil}]
|
||||
(testing "sanity check: not a PK by sync"
|
||||
(is (= [] (map :id (action-v2.db/pk-fields-for-table table-id)))))
|
||||
(mt/with-temp [:model/FieldUserSettings _ {:field_id field-id
|
||||
:semantic_type :type/PK
|
||||
:semantic_type_set true}]
|
||||
(is (= [field-id] (map :id (action-v2.db/pk-fields-for-table table-id))))))))
|
||||
|
||||
(deftest category-list-field-ids-by-name-honors-user-settings-test
|
||||
(testing "category-list-field-ids-by-name honors user-set has_field_values/semantic_type"
|
||||
(mt/with-temp [:model/Table {table-id :id} {}
|
||||
:model/Field {field-id :id} {:table_id table-id, :name "STATUS"
|
||||
:has_field_values nil, :semantic_type nil}]
|
||||
(testing "sanity check: not a category-list field by sync"
|
||||
(is (= [] (action-v2.db/category-list-field-ids-by-name table-id ["status"]))))
|
||||
(mt/with-temp [:model/FieldUserSettings _ {:field_id field-id
|
||||
:has_field_values :list
|
||||
:semantic_type :type/Category
|
||||
:semantic_type_set true}]
|
||||
(is (= [{:id field-id, :lower_name "status"}]
|
||||
(action-v2.db/category-list-field-ids-by-name table-id ["status"])))))))
|
||||
@@ -11,12 +11,29 @@
|
||||
[metabase.permissions.models.permissions-group :as perms-group]
|
||||
[metabase.test :as mt]
|
||||
[metabase.test.fixtures :as fixtures]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(use-fixtures :once (fixtures/initialize :db))
|
||||
|
||||
(defn- user-table
|
||||
"The Table with `table-id` as users see it. A bare `t2/select :model/Table` shows what sync wrote."
|
||||
[table-id]
|
||||
(t2/select-one :model/Table :id table-id {:from [(warehouse-schema-overlay/table-query)]}))
|
||||
|
||||
(defn- user-tables
|
||||
"The Tables with `table-ids` as users see it, ordered by `order-by`."
|
||||
[table-ids order-by]
|
||||
(t2/select :model/Table :id [:in table-ids] {:from [(warehouse-schema-overlay/table-query)]
|
||||
:order-by [order-by]}))
|
||||
|
||||
(defn- user-table-fn
|
||||
"The value of `column` on the Table with `table-id` as users see it."
|
||||
[column table-id]
|
||||
(get (user-table table-id) column))
|
||||
|
||||
(deftest table-metadata-survives-deleted-transform-test
|
||||
(testing "a table whose creating transform has since been deleted (metabase#69904)"
|
||||
(mt/with-premium-features #{:transforms-basic :hosting}
|
||||
@@ -24,7 +41,7 @@
|
||||
:model/Table {table-id :id} {:transform_id (:id transform)}]
|
||||
(t2/delete! :model/Transform (:id transform))
|
||||
(testing "metabase_table.transform_id -> transform.id is ON DELETE SET NULL, not left dangling"
|
||||
(is (nil? (t2/select-one-fn :transform_id :model/Table :id table-id))))
|
||||
(is (nil? (user-table-fn :transform_id table-id))))
|
||||
(testing "GET /api/table/:id does not crash"
|
||||
(is (=? {:id table-id} (mt/user-http-request :crowberto :get 200 (str "table/" table-id)))))
|
||||
(testing "GET /api/table (list, which hydrates :transform when transforms are enabled) does not crash"
|
||||
@@ -52,7 +69,7 @@
|
||||
{:display_name "Venues"
|
||||
:collection_id collection-id
|
||||
:is_published true}]
|
||||
(t2/select :model/Table :id [:in [(mt/id :users) (mt/id :venues)]] {:order-by [:display_name]}))))
|
||||
(user-tables [(mt/id :users) (mt/id :venues)] :display_name))))
|
||||
(testing "audit log entries are created for publish"
|
||||
(is (=? {:topic :table-publish, :model "Table", :model_id (mt/id :users)}
|
||||
(mt/latest-audit-log-entry "table-publish" (mt/id :users))))
|
||||
@@ -67,7 +84,7 @@
|
||||
(is (=? {:display_name "Venues"
|
||||
:collection_id nil
|
||||
:is_published false}
|
||||
(t2/select-one :model/Table (mt/id :venues))))
|
||||
(user-table (mt/id :venues))))
|
||||
(testing "audit log entry is created for unpublish"
|
||||
(is (=? {:topic :table-unpublish, :model "Table", :model_id (mt/id :venues)}
|
||||
(mt/latest-audit-log-entry "table-unpublish" (mt/id :venues)))))))))
|
||||
@@ -75,7 +92,7 @@
|
||||
(is (=? {:display_name "Users"
|
||||
:collection_id nil
|
||||
:is_published false}
|
||||
(t2/select-one :model/Table (mt/id :users))))))
|
||||
(user-table (mt/id :users))))))
|
||||
(testing "returns 404 when no library-data collection exists"
|
||||
(is (= "Not found."
|
||||
(mt/user-http-request :crowberto :post 404 "ee/data-studio/table/publish-tables"
|
||||
@@ -99,7 +116,7 @@
|
||||
:collection_id subcollection-id})
|
||||
(is (=? {:collection_id subcollection-id
|
||||
:is_published true}
|
||||
(t2/select-one :model/Table (mt/id :users)))))))))
|
||||
(user-table (mt/id :users)))))))))
|
||||
|
||||
(deftest publishing-info-test
|
||||
(mt/with-premium-features #{:library :audit-app}
|
||||
@@ -151,13 +168,17 @@
|
||||
(without-library
|
||||
(mt/with-temp [:model/Collection {collection-id :id} {:type collection/library-data-collection-type}]
|
||||
;; a bumped updated_at proves the write went through the Toucan pipeline (:hook/timestamped?),
|
||||
;; the same pipeline that fires :hook/search-index; a raw UPDATE would leave it unchanged
|
||||
(let [baseline (t/offset-date-time 2020)
|
||||
;; raw update, precisely to keep the timestamped hook from overwriting the backdate
|
||||
backdate! #(t2/query {:update (t2/table-name :model/Table)
|
||||
:set {:updated_at baseline}
|
||||
:where [:= :id (mt/id :venues)]})
|
||||
updated-at #(t2/select-one-fn :updated_at :model/Table (mt/id :venues))]
|
||||
backdate! #(do (t2/query {:update (t2/table-name :model/TableUserSettings)
|
||||
:set {:updated_at baseline}
|
||||
:where [:= :table_id (mt/id :venues)]})
|
||||
(t2/query {:update (t2/table-name :model/Table)
|
||||
:set {:updated_at baseline}
|
||||
:where [:= :id (mt/id :venues)]}))
|
||||
updated-at #(or (t2/select-one-fn :updated_at :model/TableUserSettings
|
||||
:table_id (mt/id :venues))
|
||||
(user-table-fn :updated_at (mt/id :venues)))]
|
||||
(backdate!)
|
||||
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/publish-tables"
|
||||
{:table_ids [(mt/id :venues)]
|
||||
@@ -245,7 +266,7 @@
|
||||
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/publish-tables"
|
||||
{:table_ids [orders-id]
|
||||
:collection_id collection-id})
|
||||
(are [table-id] (true? (t2/select-one-fn :is_published :model/Table table-id))
|
||||
(are [table-id] (true? (user-table-fn :is_published table-id))
|
||||
orders-id products-id))))))
|
||||
|
||||
(deftest publish-tables-does-not-move-already-published-upstream-test
|
||||
@@ -275,10 +296,10 @@
|
||||
:collection_id coll-y})
|
||||
(testing "the selected table is published into the target collection"
|
||||
(is (=? {:is_published true :collection_id coll-y}
|
||||
(t2/select-one [:model/Table :is_published :collection_id] orders-id))))
|
||||
(user-table orders-id))))
|
||||
(testing "the already-published upstream table stays in its original collection"
|
||||
(is (=? {:is_published true :collection_id coll-x}
|
||||
(t2/select-one [:model/Table :is_published :collection_id] products-id))))))))
|
||||
(user-table products-id))))))))
|
||||
|
||||
(deftest publish-tables-recursive-upstream-test
|
||||
(mt/with-premium-features #{:library}
|
||||
@@ -316,7 +337,7 @@
|
||||
(mt/user-http-request :crowberto :post 200 "ee/data-studio/table/publish-tables"
|
||||
{:table_ids [items-id]
|
||||
:collection_id collection-id})
|
||||
(are [table-id] (true? (t2/select-one-fn :is_published :model/Table table-id))
|
||||
(are [table-id] (true? (user-table-fn :is_published table-id))
|
||||
items-id orders-id purchasers-id))))))
|
||||
|
||||
(deftest unpublish-tables-with-downstream-dependents-test
|
||||
@@ -345,7 +366,7 @@
|
||||
(testing "unpublishing products also unpublishes orders (downstream dependent)"
|
||||
(mt/user-http-request :crowberto :post 204 "ee/data-studio/table/unpublish-tables"
|
||||
{:table_ids [products-id]})
|
||||
(are [table-id] (false? (t2/select-one-fn :is_published :model/Table table-id))
|
||||
(are [table-id] (false? (user-table-fn :is_published table-id))
|
||||
products-id orders-id))))))
|
||||
|
||||
(deftest unpublish-tables-recursive-downstream-test
|
||||
@@ -386,7 +407,7 @@
|
||||
(testing "unpublishing customers also unpublishes orders and order_items (recursive downstream)"
|
||||
(mt/user-http-request :crowberto :post 204 "ee/data-studio/table/unpublish-tables"
|
||||
{:table_ids [purchasers-id]})
|
||||
(are [table-id] (false? (t2/select-one-fn :is_published :model/Table table-id))
|
||||
(are [table-id] (false? (user-table-fn :is_published table-id))
|
||||
purchasers-id orders-id items-id))))))
|
||||
|
||||
(defn- with-fk-linked-published-tables
|
||||
@@ -418,9 +439,9 @@
|
||||
(fn [{:keys [coll-a x-id y-id]}]
|
||||
(t2/delete! :model/Collection :id coll-a)
|
||||
(testing "Table X (in the deleted collection) is unpublished"
|
||||
(is (=? {:is_published false :collection_id nil} (t2/select-one :model/Table x-id))))
|
||||
(is (=? {:is_published false :collection_id nil} (user-table x-id))))
|
||||
(testing "Table Y (FK-linked, in another collection) is also unpublished"
|
||||
(is (=? {:is_published false :collection_id nil} (t2/select-one :model/Table y-id)))))))))
|
||||
(is (=? {:is_published false :collection_id nil} (user-table y-id)))))))))
|
||||
|
||||
(deftest unpublish-fk-linked-tables-on-collection-archive-test
|
||||
(mt/with-premium-features #{:library}
|
||||
@@ -431,9 +452,9 @@
|
||||
(collection/archive-or-unarchive-collection!
|
||||
(t2/select-one :model/Collection :id coll-a) {:archived true}))
|
||||
(testing "Table X (in the archived collection) is unpublished"
|
||||
(is (=? {:is_published false :collection_id nil} (t2/select-one :model/Table x-id))))
|
||||
(is (=? {:is_published false :collection_id nil} (user-table x-id))))
|
||||
(testing "Table Y (FK-linked, in another collection) is also unpublished"
|
||||
(is (=? {:is_published false :collection_id nil} (t2/select-one :model/Table y-id)))))))))
|
||||
(is (=? {:is_published false :collection_id nil} (user-table y-id)))))))))
|
||||
|
||||
;;; ------------------------------------------ Publish/Unpublish requires write and query perms ------------------------------------------
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[metabase.test :as mt]
|
||||
[metabase.test.fixtures :as fixtures]
|
||||
[metabase.util :as u]
|
||||
[metabase.warehouse-schema.models.table-user-settings :as schema.table-user-settings]
|
||||
[toucan2.core :as t2]))
|
||||
|
||||
(use-fixtures :once (fixtures/initialize :db))
|
||||
@@ -39,7 +40,9 @@
|
||||
(mt/with-temp [:model/User {user-id :id} {}
|
||||
:model/Collection {collection-id :id} {:type "library-data"}]
|
||||
(try
|
||||
(t2/update! :model/Table (mt/id :venues) {:is_published true :collection_id collection-id})
|
||||
(schema.table-user-settings/upsert-user-settings (t2/select-one :model/Table (mt/id :venues))
|
||||
{:is_published true
|
||||
:collection_id collection-id})
|
||||
(let [all-users (perms/all-users-group)]
|
||||
(perms/set-database-permission! all-users (mt/id) :perms/view-data db-view-data)
|
||||
(perms/set-database-permission! all-users (mt/id) :perms/create-queries :no)
|
||||
@@ -50,7 +53,8 @@
|
||||
(perms/set-table-permission! all-users (mt/id :venues) :perms/create-queries :no))
|
||||
(f user-id collection-id)
|
||||
(finally
|
||||
(t2/update! :model/Table (mt/id :venues) {:is_published false :collection_id nil})))))))
|
||||
(schema.table-user-settings/unset-user-settings! (t2/select-one :model/Table (mt/id :venues))
|
||||
[:is_published :collection_id])))))))
|
||||
|
||||
(deftest published-table-test
|
||||
(testing "Published tables grant query access via collection permissions only with library enabled\n"
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
(deftest content-metadata-matches-row-hash-test
|
||||
(testing "a full export writes a content_hash matching per-row row->content-hash for every identity flavor —
|
||||
including the extract-query overrides (Collection, NativeQuerySnippet), hybrids (Measure), and path
|
||||
models (Field) that have no entity_id (GHY-3933)"
|
||||
models (TableUserSettings, keyed by table_id) that have no entity_id (GHY-3933)"
|
||||
(with-library-synced
|
||||
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
||||
(mt/with-temp [:model/Database db {:name "DB"}
|
||||
@@ -209,14 +209,14 @@
|
||||
:model/Card card {:name "Card" :dataset_query (mt/mbql-query venues) :collection_id (:id rs)}
|
||||
:model/NativeQuerySnippet snip {:name "Snip" :content "SELECT 1" :collection_id (:id snips)}
|
||||
:model/Table table {:name "T" :schema "PUBLIC" :db_id (:id db) :is_published true :collection_id (:id data)}
|
||||
:model/Field field {:name "F" :table_id (:id table) :base_type :type/Integer}
|
||||
:model/FieldUserSettings _ {:field_id (:id field) :description "curated"}
|
||||
:model/Field _field {:name "F" :table_id (:id table) :base_type :type/Integer}
|
||||
:model/TableUserSettings _ {:table_id (:id table) :display_name "curated"}
|
||||
:model/Measure measure {:name "M" :table_id (:id table)}]
|
||||
(mt/with-model-cleanup [:model/RemoteSyncTask]
|
||||
(let [task-id (t2/insert-returning-pk! :model/RemoteSyncTask {:sync_task_type "export" :initiated_by (mt/user->id :rasta)})
|
||||
rows (mapv (fn [[mt id]] {:model_type mt :model_id id})
|
||||
[["Card" (:id card)] ["Collection" (:id rs)] ["NativeQuerySnippet" (:id snip)]
|
||||
["FieldUserSettings" (:id field)] ["Measure" (:id measure)]])]
|
||||
["TableUserSettings" (:id table)] ["Measure" (:id measure)]])]
|
||||
(t2/delete! :model/RemoteSyncObject)
|
||||
(doseq [row rows]
|
||||
(t2/insert! :model/RemoteSyncObject (merge row {:model_name "x" :status "synced"
|
||||
|
||||
@@ -1117,10 +1117,6 @@
|
||||
|
||||
;;; Field Event Tests
|
||||
|
||||
;; Field RSO tracking has been replaced by FieldUserSettings RSO tracking.
|
||||
;; field-update events now track FieldUserSettings (user-curated metadata) rather than raw Field rows.
|
||||
;; See the FieldUserSettings RSO Tracking section below for the current behavior tests.
|
||||
|
||||
(deftest field-update-event-no-entry-for-unpublished-table-test
|
||||
(testing "field-update event creates no RSOs for an unpublished table"
|
||||
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
||||
@@ -1149,12 +1145,11 @@
|
||||
{:object field :user-id (mt/user->id :rasta)})
|
||||
(is (empty? (t2/select :model/RemoteSyncObject))))))
|
||||
|
||||
;;; ----------------------------------------- FieldUserSettings RSO Tracking ------------------------------------------
|
||||
;; In git-sync mode, field updates should track FieldUserSettings RSOs only — not Field RSOs.
|
||||
;; Field YAMLs are replaced by FieldUserSettings YAMLs (user-curated metadata only).
|
||||
;;; ----------------------------------------- TableUserSettings RSO Tracking from Field Events --------------------------
|
||||
|
||||
(deftest field-update-creates-only-fus-rso-when-fus-exists-test
|
||||
(testing "field-update on an eligible field with a FUS row creates ONLY a FieldUserSettings RSO — no Field RSO"
|
||||
(deftest field-update-creates-table-user-settings-rso-when-fus-exists-test
|
||||
(testing "field-update on an eligible field with a FUS row creates a TableUserSettings RSO for its Table — no
|
||||
Field or FieldUserSettings RSO"
|
||||
(mt/with-temp [:model/Collection coll {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
||||
:model/Table table {:name "T" :is_published true :collection_id (:id coll)}
|
||||
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
||||
@@ -1162,9 +1157,9 @@
|
||||
(t2/delete! :model/RemoteSyncObject)
|
||||
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
||||
(let [entries (t2/select :model/RemoteSyncObject)]
|
||||
(is (= 1 (count entries)) "exactly one RSO — no Field RSO alongside the FUS RSO")
|
||||
(is (=? {:model_type "FieldUserSettings"
|
||||
:model_id (:id field)
|
||||
(is (= 1 (count entries)) "exactly one RSO — the Table's TableUserSettings, no Field/FieldUserSettings RSO")
|
||||
(is (=? {:model_type "TableUserSettings"
|
||||
:model_id (:id table)
|
||||
:status "update"
|
||||
:model_table_id (:id table)}
|
||||
(first entries)))))))
|
||||
@@ -1272,14 +1267,15 @@
|
||||
(assert-removal-survived coll-id card)))))
|
||||
|
||||
(deftest field-update-no-rso-when-no-fus-row-test
|
||||
(testing "field-update on an eligible field with NO FUS row creates no RSOs at all"
|
||||
(testing "field-update on an eligible field with NO FUS row, and no TableUserSettings row either, creates no
|
||||
RSOs at all"
|
||||
(mt/with-temp [:model/Collection coll {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
||||
:model/Table table {:name "T" :is_published true :collection_id (:id coll)}
|
||||
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
||||
(t2/delete! :model/RemoteSyncObject)
|
||||
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
||||
(is (empty? (t2/select :model/RemoteSyncObject))
|
||||
"no RSOs created — field has no user-curated metadata to track"))))
|
||||
"no RSOs created — neither the Field nor the Table has user-curated metadata to track"))))
|
||||
|
||||
(deftest field-update-no-rso-for-unpublished-table-test
|
||||
(testing "field-update creates no RSOs when the table is not published"
|
||||
@@ -1301,23 +1297,24 @@
|
||||
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
||||
(is (empty? (t2/select :model/RemoteSyncObject))))))
|
||||
|
||||
(deftest field-update-fus-rso-marks-removed-when-table-leaves-sync-scope-test
|
||||
(testing "existing FieldUserSettings RSO is marked removed when table leaves sync scope — no Field RSO created"
|
||||
(deftest field-update-table-user-settings-rso-marks-removed-when-table-leaves-sync-scope-test
|
||||
(testing "existing TableUserSettings RSO is marked removed when the field's table leaves sync scope — no
|
||||
Field or FieldUserSettings RSO created"
|
||||
(mt/with-temp [:model/Collection synced {:is_remote_synced true :name "Synced" :type "library-data"}
|
||||
:model/Collection normal {:name "Normal" :type "library-data"}
|
||||
:model/Table table {:name "T" :is_published true :collection_id (:id synced)}
|
||||
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
||||
(t2/insert! :model/FieldUserSettings {:field_id (:id field) :description "curated"})
|
||||
(t2/delete! :model/RemoteSyncObject)
|
||||
(t2/insert! :model/RemoteSyncObject {:model_type "FieldUserSettings"
|
||||
:model_id (:id field)
|
||||
:model_name "f"
|
||||
(t2/insert! :model/RemoteSyncObject {:model_type "TableUserSettings"
|
||||
:model_id (:id table)
|
||||
:model_name "T"
|
||||
:model_table_id (:id table)
|
||||
:status "synced"
|
||||
:status_changed_at (t/offset-date-time)})
|
||||
(t2/update! :model/Table (:id table) {:collection_id (:id normal)})
|
||||
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
||||
(let [entries (t2/select :model/RemoteSyncObject)]
|
||||
(is (= 1 (count entries)) "still only one RSO — no Field RSO added")
|
||||
(is (= 1 (count entries)) "still only one RSO — no Field/FieldUserSettings RSO added")
|
||||
(is (= "removed"
|
||||
(:status (t2/select-one :model/RemoteSyncObject :model_type "FieldUserSettings" :model_id (:id field)))))))))
|
||||
(:status (t2/select-one :model/RemoteSyncObject :model_type "TableUserSettings" :model_id (:id table)))))))))
|
||||
|
||||
@@ -2168,3 +2168,47 @@ serdes/meta:
|
||||
:base-snapshot nil)]
|
||||
(is (= :conflict (:status result)))
|
||||
(is (str/includes? (:message result) "rewritten"))))))
|
||||
|
||||
;;; --------------------------------- Table/Field user-settings inline round trip ---------------------------------
|
||||
|
||||
(deftest table-and-field-user-settings-round-trip-test
|
||||
(testing "a full export writes one file per published Table with user edits -- the TableUserSettings entity at
|
||||
the Table's own path, inlining its edited Fields' settings under :fields -- and a later import
|
||||
restores it, pruning field edits the file no longer lists"
|
||||
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
||||
(mt/with-temp [:model/Database {db-id :id} {:name "test-db"}
|
||||
:model/Collection {coll-id :id} {:name "RS" :is_remote_synced true :location "/"}
|
||||
:model/Table {table-id :id} {:name "Test Table" :db_id db-id
|
||||
:is_published true :collection_id coll-id}
|
||||
:model/Field {f1-id :id} {:name "F1" :table_id table-id :base_type :type/Text}
|
||||
:model/Field {f2-id :id} {:name "F2" :table_id table-id :base_type :type/Text}]
|
||||
(t2/insert! :model/TableUserSettings {:table_id table-id :display_name "Renamed"})
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f1-id :description "curated"})
|
||||
(let [export-task-id (t2/insert-returning-pk! :model/RemoteSyncTask {:sync_task_type "export" :initiated_by (mt/user->id :rasta)})
|
||||
mock-source (test-helpers/create-mock-source)
|
||||
export-result (impl/export! (source.p/snapshot mock-source) export-task-id "Test export" :force? true)]
|
||||
(is (= :success (:status export-result)))
|
||||
(remote-sync.task/complete-sync-task! export-task-id)
|
||||
(let [files (get @(:files-atom mock-source) "main")
|
||||
table-files (into {} (filter (fn [[p _]] (str/includes? p "test_table"))) files)]
|
||||
(is (= 1 (count table-files))
|
||||
(str "expected exactly one Table-related file, got " (keys table-files)))
|
||||
(let [[path content] (first table-files)]
|
||||
(is (str/ends-with? path "test_table.yaml")
|
||||
"the file lives at the Table's own path, not a ___tableusersettings/___fieldusersettings file")
|
||||
(is (not (str/includes? path "___tableusersettings")))
|
||||
(is (not (str/includes? path "___fieldusersettings")))
|
||||
(is (str/includes? content "Renamed"))
|
||||
(is (str/includes? content "fields:"))
|
||||
(is (str/includes? content "curated"))))
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f2-id :description "stale"})
|
||||
(t2/update! :model/TableUserSettings :table_id table-id {:display_name "Local Edit"})
|
||||
(let [import-task-id (t2/insert-returning-pk! :model/RemoteSyncTask {:sync_task_type "import" :initiated_by (mt/user->id :rasta)})
|
||||
import-result (impl/import! (source.p/snapshot mock-source) import-task-id :force? true)]
|
||||
(is (= :success (:status import-result)))
|
||||
(is (= "Renamed" (:display_name (t2/select-one :model/TableUserSettings :table_id table-id)))
|
||||
"T's user display_name is restored from the import")
|
||||
(is (= "curated" (:description (t2/select-one :model/FieldUserSettings :field_id f1-id)))
|
||||
"F1's edited description is present")
|
||||
(is (not (t2/exists? :model/FieldUserSettings :field_id f2-id))
|
||||
"F2's stale local row, absent from the imported file's :fields, is gone")))))))
|
||||
|
||||
@@ -623,3 +623,40 @@
|
||||
"the warning covers only the unsynced subset (the potential data loss)")
|
||||
(is (set/subset? flagged would-delete)
|
||||
"everything the warning flags would indeed be removed")))))
|
||||
|
||||
(deftest git-sync-exports-only-user-settings-test
|
||||
(testing "git sync stores what users changed about a Table and its Fields, never the Table or Fields themselves --
|
||||
those belong to sync, which runs against each instance's own warehouse -- as one TableUserSettings
|
||||
entity per Table inlining its Fields' edits, never separate FieldUserSettings entities"
|
||||
(mt/with-premium-features #{:library}
|
||||
(mt/with-temp [:model/Collection {coll-id :id} {:is_remote_synced true :name "RS" :type "library-data"}
|
||||
:model/Database {db-id :id} {:name "DB"}
|
||||
:model/Table {table-id :id} {:name "T" :db_id db-id
|
||||
:is_published true :collection_id coll-id}
|
||||
:model/Field {f1 :id} {:name "F1" :table_id table-id}
|
||||
:model/Field {f2 :id} {:name "F2" :table_id table-id}]
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f2 :description "curated" :description_set true})
|
||||
(t2/insert! :model/TableUserSettings {:table_id table-id :display_name "Renamed"})
|
||||
(let [exportable (spec/exportable-entities)]
|
||||
(is (contains? (set (get exportable "TableUserSettings")) table-id)
|
||||
"the Table's own edit, plus its edited Field's, are carried by one TableUserSettings entity")
|
||||
(is (nil? (get exportable "FieldUserSettings"))
|
||||
"FieldUserSettings is never exported as its own entity")
|
||||
(is (not (contains? (set (get exportable "Table")) table-id)))
|
||||
(is (empty? (filter #{f1 f2} (get exportable "Field")))))))))
|
||||
|
||||
(deftest git-sync-exports-table-user-settings-for-field-only-edit-test
|
||||
(testing "a Table with no TableUserSettings row of its own, but an edited Field, is still exportable -- the
|
||||
TableUserSettings entity is synthesized to carry the Field's edit"
|
||||
(mt/with-premium-features #{:library}
|
||||
(mt/with-temp [:model/Collection {coll-id :id} {:is_remote_synced true :name "RS" :type "library-data"}
|
||||
:model/Database {db-id :id} {:name "DB"}
|
||||
:model/Table {table-id :id} {:name "T" :db_id db-id
|
||||
:is_published true :collection_id coll-id}
|
||||
:model/Field {f1 :id} {:name "F1" :table_id table-id}]
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f1 :description "curated" :description_set true})
|
||||
(let [exportable (spec/exportable-entities)]
|
||||
(is (not (t2/exists? :model/TableUserSettings :table_id table-id))
|
||||
"the Table has no settings row of its own")
|
||||
(is (contains? (set (get exportable "TableUserSettings")) table-id)
|
||||
"the Table is still exportable, synthesized from its Field's edit"))))))
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
[:stages 0 :source-card]))))))))))))))
|
||||
|
||||
(deftest copy-model-metadata-overrides!-test
|
||||
(testing "copies user-edited metadata from model result_metadata to Field and FieldUserSettings"
|
||||
(testing "copies user-edited metadata from model result_metadata to FieldUserSettings only"
|
||||
(mt/with-temp [:model/Table {table-id :id} {:name "transform_output"
|
||||
:db_id (mt/id)
|
||||
:active true}
|
||||
@@ -299,14 +299,14 @@
|
||||
:base_type "type/DateTimeWithLocalTZ"}])}
|
||||
:where [:= :id card-id]})
|
||||
(#'replacement.runner/copy-model-metadata-overrides! card-id table-id)
|
||||
(testing "Field records are updated with overrides from model metadata"
|
||||
(testing "Field records are untouched; they stay sync-owned"
|
||||
(let [field-1 (t2/select-one :model/Field :id field-1-id)
|
||||
field-2 (t2/select-one :model/Field :id field-2-id)]
|
||||
(is (= "Order Total" (:display_name field-1)))
|
||||
(is (= "The total amount" (:description field-1)))
|
||||
(is (= :type/Currency (:semantic_type field-1)))
|
||||
(is (= "Order Date" (:display_name field-2)))
|
||||
(is (= :type/CreationTimestamp (:semantic_type field-2)))))
|
||||
(is (= "Total" (:display_name field-1)))
|
||||
(is (nil? (:description field-1)))
|
||||
(is (nil? (:semantic_type field-1)))
|
||||
(is (= "Created At" (:display_name field-2)))
|
||||
(is (nil? (:semantic_type field-2)))))
|
||||
(testing "FieldUserSettings are created so overrides survive sync"
|
||||
(let [fus-1 (t2/select-one :model/FieldUserSettings :field_id field-1-id)
|
||||
fus-2 (t2/select-one :model/FieldUserSettings :field_id field-2-id)]
|
||||
@@ -344,8 +344,8 @@
|
||||
:where [:= :id card-id]})
|
||||
(#'replacement.runner/copy-model-metadata-overrides! card-id table-id)
|
||||
(let [field (t2/select-one :model/Field :id field-id)]
|
||||
(is (= "Product ID" (:display_name field)))
|
||||
(is (= "The product identifier" (:description field))))
|
||||
(is (= "Products ID" (:display_name field)))
|
||||
(is (nil? (:description field))))
|
||||
(let [fus (t2/select-one :model/FieldUserSettings :field_id field-id)]
|
||||
(is (some? fus))
|
||||
(is (= "Product ID" (:display_name fus)))
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
:model/Field
|
||||
:model/FieldValues
|
||||
:model/FieldUserSettings
|
||||
:model/TableUserSettings
|
||||
;; OsiAiContext is identified by the entity it describes (entity_type + the entity's portable ref); its
|
||||
;; serdes path nests under that entity, so it has no generated entity_id.
|
||||
:model/OsiAiContext
|
||||
@@ -164,7 +165,7 @@
|
||||
(testing "All exported models should get entity id except those with other unique property (like name)"
|
||||
(is (= (set (concat serdes.models/exported-models
|
||||
;; those are inline models which still have entity_id
|
||||
["DashboardCard" "DashboardTab" "Dimension" "MetabotPrompt"]))
|
||||
["DashboardCard" "DashboardTab" "Dimension" "MetabotPrompt" "FieldUserSettings"]))
|
||||
(set (->> (concat entity-id-models
|
||||
entities-external-name)
|
||||
(map name))))))
|
||||
|
||||
@@ -1153,63 +1153,79 @@
|
||||
:data_sensitivity :PII}
|
||||
ser))
|
||||
(is (not (contains? ser :field_id))
|
||||
":field_id is dropped; its implied by the path")
|
||||
(testing "depend only on the Database; the parent Field is synthesized on import if missing"
|
||||
(is (= #{[{:model "Database" :id "My Database"}]}
|
||||
(set (serdes/deserialization-dependencies ser)))))))
|
||||
":field_id is dropped; its implied by the path")))
|
||||
(testing "data_sensitivity on the Field itself"
|
||||
(is (= :PII (:data_sensitivity (ts/extract-one "Field" field-id)))
|
||||
"a labeled field exports the keyword as-is")
|
||||
(is (not (contains? (ts/extract-one "Field" plain-id) :data_sensitivity))
|
||||
"an unlabeled field exports no key, so nil never reaches the YAML"))
|
||||
(testing "extract-metabase behavior"
|
||||
(let [models (->> {} (extract/extract) (map (comp :model last :serdes/meta)))]
|
||||
(is (= 1
|
||||
(t2/count :model/FieldUserSettings)
|
||||
(count (filter #{"FieldUserSettings"} models)))))))))
|
||||
(let [entities (into [] (extract/extract {}))
|
||||
models (map (comp :model last :serdes/meta) entities)]
|
||||
(is (empty? (filter #{"FieldUserSettings"} models))
|
||||
"written inside its Table's settings, never on its own")
|
||||
(is (= 1 (count (filter #{"TableUserSettings"} models))))
|
||||
(is (= 1 (count (:fields (first (filter #(= "TableUserSettings" (-> % :serdes/meta last :model)) entities)))))))))))
|
||||
|
||||
(deftest table-descendants-user-edits-only-test
|
||||
(deftest table-descendants-user-settings-test
|
||||
(mt/with-empty-h2-app-db!
|
||||
(ts/with-temp-dpc [:model/Database {db-id :id} {:name "DB"}
|
||||
:model/Table {table-id :id} {:name "T" :db_id db-id}
|
||||
:model/Field {f1-id :id} {:name "F1" :table_id table-id}
|
||||
:model/Field {f2-id :id} {:name "F2" :table_id table-id}
|
||||
:model/Field {f3-id :id} {:name "F3" :table_id table-id}]
|
||||
(testing "without user-edits-only: all fields returned as Field descendants"
|
||||
(let [desc (serdes/descendants "Table" table-id {})]
|
||||
(is (= #{["Field" f1-id] ["Field" f2-id] ["Field" f3-id]}
|
||||
(set (keys desc))))))
|
||||
(testing "with user-edits-only and no FieldUserSettings rows: no field descendants"
|
||||
(let [desc (serdes/descendants "Table" table-id {:user-edits-only true})]
|
||||
(is (empty? (filter (fn [[model _]] (#{"Field" "FieldUserSettings"} model)) (keys desc))))))
|
||||
(testing "with user-edits-only and one FieldUserSettings row: only that field appears as FieldUserSettings"
|
||||
(testing "every Field is a descendant, and none has user settings to speak of yet"
|
||||
(is (= #{["Field" f1-id] ["Field" f2-id] ["Field" f3-id]}
|
||||
(set (keys (serdes/descendants "Table" table-id {}))))))
|
||||
(testing "a Field with user settings makes the Table's settings a descendant, since they carry the Field's"
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f2-id :description "edited"})
|
||||
(let [desc (serdes/descendants "Table" table-id {:user-edits-only true})]
|
||||
(is (= #{["FieldUserSettings" f2-id]}
|
||||
(set (filter (fn [[model _]] (#{"Field" "FieldUserSettings"} model)) (keys desc))))))
|
||||
(is (= #{["Field" f1-id] ["Field" f2-id] ["Field" f3-id] ["TableUserSettings" table-id]}
|
||||
(set (keys (serdes/descendants "Table" table-id {})))))
|
||||
(t2/delete! :model/FieldUserSettings :field_id f2-id))
|
||||
(testing "with user-edits-only and a FieldUserSettings row holding only data_sensitivity: that field appears as FieldUserSettings"
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f1-id :data_sensitivity :PII})
|
||||
(let [desc (serdes/descendants "Table" table-id {:user-edits-only true})]
|
||||
(is (= #{["FieldUserSettings" f1-id]}
|
||||
(set (filter (fn [[model _]] (#{"Field" "FieldUserSettings"} model)) (keys desc))))))
|
||||
(t2/delete! :model/FieldUserSettings :field_id f1-id))
|
||||
(testing "with user-edits-only and all fields edited: all appear as FieldUserSettings, not Field"
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f1-id})
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f2-id})
|
||||
(t2/insert! :model/FieldUserSettings {:field_id f3-id})
|
||||
(let [desc (serdes/descendants "Table" table-id {:user-edits-only true})]
|
||||
(is (= #{["FieldUserSettings" f1-id] ["FieldUserSettings" f2-id] ["FieldUserSettings" f3-id]}
|
||||
(set (filter (fn [[model _]] (#{"Field" "FieldUserSettings"} model)) (keys desc)))))))
|
||||
(testing "Field and FieldUserSettings are leaf nodes in the descendants graph"
|
||||
;; Table's descendants method is the only source of field-level entries; if Field ever
|
||||
;; grows its own descendants (e.g. Field -> FieldUserSettings), traversal would visit
|
||||
;; every field and full exports would change shape. Cement the leaf-ness here.
|
||||
(doseq [opts [{} {:user-edits-only true}]]
|
||||
(is (empty? (serdes/descendants "Field" f1-id opts)))
|
||||
(is (empty? (serdes/descendants "FieldUserSettings" f1-id opts))))))))
|
||||
(testing "a Table with user settings appears as TableUserSettings"
|
||||
(t2/insert! :model/TableUserSettings {:table_id table-id :display_name "Renamed"})
|
||||
(is (contains? (set (keys (serdes/descendants "Table" table-id {})))
|
||||
["TableUserSettings" table-id])))
|
||||
(testing "Field is a leaf node in the descendants graph"
|
||||
(is (empty? (serdes/descendants "Field" f1-id {})))))))
|
||||
|
||||
(deftest user-edits-only-extract-test
|
||||
(deftest inline-user-settings-extract-test
|
||||
(mt/with-empty-h2-app-db!
|
||||
(ts/with-temp-dpc [:model/Database {db-id :id} {:name "DB"}
|
||||
:model/Table {table-id :id} {:name "T" :db_id db-id}
|
||||
:model/Table {other-id :id} {:name "Unedited T" :db_id db-id}
|
||||
:model/Field {f1-id :id} {:name "F1" :table_id table-id}
|
||||
:model/Field {_f2-id :id} {:name "F2" :table_id table-id}
|
||||
:model/FieldUserSettings _ {:field_id f1-id :description "edited"}]
|
||||
(testing "a Table with only a Field edit synthesizes a TableUserSettings entity with that Field inlined"
|
||||
(let [entities (into [] (serdes/extract-all "TableUserSettings"
|
||||
{:inline-user-settings true
|
||||
:filter-column :table_id
|
||||
:filter-ids [table-id]}))]
|
||||
(is (= 1 (count entities)))
|
||||
(let [entity (first entities)]
|
||||
(is (=? [{:model "Database" :id "DB"}
|
||||
{:model "Table" :id "T"}
|
||||
{:model "TableUserSettings" :id "1"}]
|
||||
(:serdes/meta entity))
|
||||
"synthesized for a Table with no settings row of its own")
|
||||
(is (nil? (:display_name entity)))
|
||||
(is (= 1 (count (:fields entity))))
|
||||
(is (=? {:description "edited"
|
||||
:serdes/meta [{:model "Database" :id "DB"}
|
||||
{:model "Table" :id "T"}
|
||||
{:model "Field" :id "F1"}
|
||||
{:model "FieldUserSettings" :id "1"}]}
|
||||
(first (:fields entity)))
|
||||
"the edited Field is inlined, with its own serdes/meta")
|
||||
(is (not (contains? (first (:fields entity)) :field_id))))))
|
||||
(testing "a Table with no edits at all yields nothing"
|
||||
(is (empty? (into [] (serdes/extract-all "TableUserSettings"
|
||||
{:inline-user-settings true
|
||||
:filter-column :table_id
|
||||
:filter-ids [other-id]}))))))))
|
||||
|
||||
(deftest collection-export-includes-user-settings-test
|
||||
(mt/with-empty-h2-app-db!
|
||||
(ts/with-temp-dpc [:model/Database {db-id :id} {:name "DB"}
|
||||
:model/Collection {coll-id :id} {:name "Library" :type "library-data"}
|
||||
@@ -1218,24 +1234,19 @@
|
||||
:collection_id coll-id}
|
||||
:model/Field _ {:name "F1" :table_id table-id}
|
||||
:model/Field {f2-id :id} {:name "F2" :table_id table-id}
|
||||
:model/FieldUserSettings _ {:field_id f2-id :description "curated"}]
|
||||
(testing "targeting the collection with user-edits-only: produces FieldUserSettings, not Field"
|
||||
(let [entities (into [] (extract/extract {:targets [["Collection" coll-id]]
|
||||
:user-edits-only true
|
||||
:no-data-model true}))
|
||||
by-model (group-by (comp :model last :serdes/meta) entities)]
|
||||
(is (contains? by-model "FieldUserSettings") "should include FieldUserSettings")
|
||||
(is (not (contains? by-model "Field")) "should not include Field")
|
||||
(is (= #{"F2"}
|
||||
(set (map #(-> % :serdes/meta (nth 2) :id) (by-model "FieldUserSettings"))))
|
||||
"only the edited field's FieldUserSettings (identified by field name in path)")
|
||||
(is (some #(= "T" (:name %)) (by-model "Table")) "the table itself is included")))
|
||||
(testing "without user-edits-only: produces Field, not FieldUserSettings"
|
||||
:model/FieldUserSettings _ {:field_id f2-id :description "curated"}
|
||||
:model/TableUserSettings _ {:table_id table-id :display_name "Renamed"}]
|
||||
(testing "targeting the collection exports the Table and its Fields, each beside its user settings"
|
||||
(let [entities (into [] (extract/extract {:targets [["Collection" coll-id]]
|
||||
:no-data-model true}))
|
||||
by-model (group-by (comp :model last :serdes/meta) entities)]
|
||||
(is (contains? by-model "Field") "should include Field")
|
||||
(is (not (contains? by-model "FieldUserSettings")) "should not include FieldUserSettings"))))))
|
||||
(is (some #(= "T" (:name %)) (by-model "Table")))
|
||||
(is (= #{"F1" "F2"}
|
||||
(set (map :name (by-model "Field")))))
|
||||
(is (= 1 (count (by-model "TableUserSettings"))))
|
||||
(is (= #{"F2"}
|
||||
(set (map #(-> % :serdes/meta (nth 2) :id) (:fields (first (by-model "TableUserSettings"))))))
|
||||
"only the field the user edited has settings to export, inside the Table's"))))))
|
||||
|
||||
(deftest cards-test
|
||||
(mt/with-empty-h2-app-db!
|
||||
|
||||
@@ -142,6 +142,47 @@
|
||||
(is (= "Basic Collection" (:name (first colls))))
|
||||
(is (= eid1 (:entity_id (first colls)))))))))))
|
||||
|
||||
(deftest inline-user-settings-round-trip-test
|
||||
(testing "git sync's inline-user-settings mode replaces a Table's FieldUserSettings wholesale on import"
|
||||
(let [serialized (atom nil)]
|
||||
(ts/with-dbs [source-db dest-db]
|
||||
(testing "extracting inline from the source"
|
||||
(ts/with-db source-db
|
||||
(let [db (ts/create! :model/Database :name "my-db")
|
||||
table (ts/create! :model/Table :name "customers" :db_id (:id db))
|
||||
f1 (ts/create! :model/Field :name "age" :table_id (:id table))
|
||||
f2 (ts/create! :model/Field :name "email" :table_id (:id table))]
|
||||
(t2/insert! :model/TableUserSettings {:table_id (:id table) :display_name "Renamed"})
|
||||
(t2/insert! :model/FieldUserSettings {:field_id (:id f1) :description "edited"})
|
||||
(reset! serialized
|
||||
(into [(ts/extract-one "Database" (:id db))
|
||||
(ts/extract-one "Table" (:id table))
|
||||
(ts/extract-one "Field" (:id f1))
|
||||
(ts/extract-one "Field" (:id f2))]
|
||||
(serdes/extract-all "TableUserSettings"
|
||||
{:inline-user-settings true
|
||||
:filter-column :table_id
|
||||
:filter-ids [(:id table)]}))))))
|
||||
(testing "the extracted TableUserSettings inlines F1's edit and no others"
|
||||
(let [tus (first (by-model @serialized "TableUserSettings"))]
|
||||
(is (= "Renamed" (:display_name tus)))
|
||||
(is (= 1 (count (:fields tus))))
|
||||
(is (= "edited" (:description (first (:fields tus)))))))
|
||||
(testing "loading into a destination where the same Database/Table/Fields exist, and F2 has a stale row"
|
||||
(ts/with-db dest-db
|
||||
(let [db (ts/create! :model/Database :name "my-db")
|
||||
table (ts/create! :model/Table :name "customers" :db_id (:id db))
|
||||
f1 (ts/create! :model/Field :name "age" :table_id (:id table))
|
||||
f2 (ts/create! :model/Field :name "email" :table_id (:id table))]
|
||||
(t2/insert! :model/FieldUserSettings {:field_id (:id f2) :description "stale"})
|
||||
(serdes.load/load-metabase! (ingestion-in-memory @serialized))
|
||||
(testing "the Table's settings arrive"
|
||||
(is (= "Renamed" (t2/select-one-fn :display_name :model/TableUserSettings :table_id (:id table)))))
|
||||
(testing "F1's settings arrive"
|
||||
(is (= "edited" (t2/select-one-fn :description :model/FieldUserSettings :field_id (:id f1)))))
|
||||
(testing "F2's stale row is deleted -- replace semantics"
|
||||
(is (nil? (t2/select-one :model/FieldUserSettings :field_id (:id f2))))))))))))
|
||||
|
||||
(deftest escape-continue-on-error-roundtrip-test
|
||||
(testing "archive exported past escape analysis imports under continue-on-error without crashing (#74622)"
|
||||
(let [serialized (atom nil)
|
||||
@@ -2132,9 +2173,9 @@
|
||||
(reset! serialized (into [] (serdes.extract/extract {})))
|
||||
(testing "the labeled Field and its FieldUserSettings both export the label"
|
||||
(is (= :PII (:data_sensitivity (field-ser @serialized "CONTACT_EMAIL"))))
|
||||
(is (= :PII (:data_sensitivity (u/seek #(and (in-db? %)
|
||||
(-> % :serdes/meta last :model (= "FieldUserSettings")))
|
||||
@serialized)))))
|
||||
(is (= :PII (-> (u/seek #(and (in-db? %) (-> % :serdes/meta last :model (= "TableUserSettings")))
|
||||
@serialized)
|
||||
:fields first :data_sensitivity))))
|
||||
(testing "the unlabeled Field exports no data_sensitivity key"
|
||||
(is (not (contains? (field-ser @serialized "CONTACT_ROW") :data_sensitivity))))))
|
||||
(ts/with-db dest-db
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
(testing "Nested models should declare `parent-ref`\n"
|
||||
(doseq [[_nested transform] (filter #(::serdes/nested (second %)) spec')
|
||||
:let [{:keys [model backward-fk]} transform
|
||||
inner-spec (serdes/make-spec (name model) nil)]]
|
||||
inner-spec (serdes/make-spec (name model) nil)]
|
||||
:when (not (:delete-children! (:opts transform)))]
|
||||
(testing (format "%s has %s declared as `parent-ref`" model backward-fk)
|
||||
(is (= (serdes/parent-ref)
|
||||
(get-in inner-spec [:transform backward-fk]))))))
|
||||
|
||||
@@ -158,12 +158,25 @@
|
||||
(storage/store! (into [] (extract/extract {})) (storage.files/file-writer dump-dir))
|
||||
(let [fields-dir (io/file dump-dir "databases" "my_company_data" "tables" "customers" "fields")
|
||||
read-yaml (fn [file-name] (yaml/from-file (io/file fields-dir file-name)))]
|
||||
(testing "the label is written as the bare enum string on both files"
|
||||
(testing "the label is written as the bare enum string on the Field and inside the Table's settings"
|
||||
(is (= "PII" (:data_sensitivity (read-yaml "email.yaml"))))
|
||||
(is (= "PII" (:data_sensitivity (read-yaml "email___fieldusersettings.yaml")))))
|
||||
(is (= "PII" (-> (yaml/from-file (io/file fields-dir ".." "customers___tableusersettings.yaml"))
|
||||
:fields first :data_sensitivity))))
|
||||
(testing "an unlabeled field's file has no data_sensitivity line"
|
||||
(is (not (contains? (read-yaml "id.yaml") :data_sensitivity)))))))))
|
||||
|
||||
(deftest inline-user-settings-storage-path-test
|
||||
(mt/with-empty-h2-app-db!
|
||||
(ts/with-temp-dpc [:model/Database db {:name "My Company Data"}
|
||||
:model/Table table {:name "Customers" :db_id (:id db)}
|
||||
:model/Field email {:name "Email" :table_id (:id table)}
|
||||
:model/FieldUserSettings _ {:field_id (:id email) :description "edited"}]
|
||||
(let [[tus] (into [] (serdes/extract-all "TableUserSettings" {:filter-column :table_id :filter-ids [(:id table)]}))]
|
||||
(is (= 1 (count (:fields tus))))
|
||||
(testing "git sync writes the entity at the Table's own path"
|
||||
(is (= "Customers" (-> (serdes/storage-path tus {:inline-user-settings true}) peek :label))))
|
||||
(testing "the CLI export writes it beside the Table's file"
|
||||
(is (= "Customers___tableusersettings" (-> (serdes/storage-path tus {}) peek :label))))))))
|
||||
(deftest entity-counts-report-test
|
||||
(ts/with-random-dump-dir [dump-dir "serdesv2-"]
|
||||
(mt/with-empty-h2-app-db!
|
||||
|
||||
@@ -67,8 +67,7 @@ const updateDataWrapper = (
|
||||
try {
|
||||
const editedFields = filterUntouchedFields(fields, props.entity);
|
||||
if (!isEmptyObject(editedFields)) {
|
||||
const newEntity = { ...props.entity, ...editedFields };
|
||||
await fn(newEntity);
|
||||
await fn({ id: props.entity.id, ...editedFields });
|
||||
}
|
||||
} finally {
|
||||
resetForm(props);
|
||||
@@ -127,7 +126,7 @@ export const rUpdateFields = (
|
||||
),
|
||||
}))
|
||||
.filter(({ formField }) => !isEmptyObject(formField))
|
||||
.map(({ field, formField }) => ({ ...field, ...formField }));
|
||||
.map(({ field, formField }) => ({ id: field.id, ...formField }));
|
||||
|
||||
await Promise.all(updatedFields.map(props.updateField));
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import _ from "underscore";
|
||||
|
||||
import { databaseApi, fieldApi, segmentApi, tableApi } from "metabase/api";
|
||||
import { runRtkEndpoint } from "metabase/api/utils/run-rtk-endpoint";
|
||||
import {
|
||||
@@ -8,19 +6,21 @@ import {
|
||||
tableFetched,
|
||||
} from "metabase/metadata-store";
|
||||
import type { Dispatch } from "metabase/redux/store";
|
||||
import type { Database, Field, Segment, Table } from "metabase-types/api";
|
||||
|
||||
/**
|
||||
* The edit forms in this module submit the entity they rendered, which came
|
||||
* from `state.entities` and carries the nested records the mirror stitched onto
|
||||
* it. The API rejects those, so each one is dropped before the request.
|
||||
*/
|
||||
import type {
|
||||
Database,
|
||||
Field,
|
||||
Table,
|
||||
UpdateDatabaseRequest,
|
||||
UpdateFieldRequest,
|
||||
UpdateSegmentRequest,
|
||||
UpdateTableRequest,
|
||||
} from "metabase-types/api";
|
||||
|
||||
export const updateDatabase =
|
||||
(database: Database) =>
|
||||
(request: UpdateDatabaseRequest) =>
|
||||
async (dispatch: Dispatch): Promise<unknown> => {
|
||||
const updated: Database = await runRtkEndpoint(
|
||||
_.omit(database, "tables", "tables_lookup"),
|
||||
request,
|
||||
dispatch,
|
||||
databaseApi.endpoints.updateDatabase,
|
||||
);
|
||||
@@ -29,16 +29,10 @@ export const updateDatabase =
|
||||
};
|
||||
|
||||
export const updateTable =
|
||||
(table: Table) =>
|
||||
(request: UpdateTableRequest) =>
|
||||
async (dispatch: Dispatch): Promise<unknown> => {
|
||||
const updated: Table = await runRtkEndpoint(
|
||||
_.omit(
|
||||
table,
|
||||
"fields",
|
||||
"fields_lookup",
|
||||
"aggregation_operators",
|
||||
"segments",
|
||||
),
|
||||
request,
|
||||
dispatch,
|
||||
tableApi.endpoints.updateTable,
|
||||
);
|
||||
@@ -47,10 +41,10 @@ export const updateTable =
|
||||
};
|
||||
|
||||
export const updateField =
|
||||
(field: Field) =>
|
||||
(request: UpdateFieldRequest) =>
|
||||
async (dispatch: Dispatch): Promise<unknown> => {
|
||||
const updated: Field = await runRtkEndpoint(
|
||||
_.omit(field, "filter_operators_lookup"),
|
||||
request,
|
||||
dispatch,
|
||||
fieldApi.endpoints.updateField,
|
||||
);
|
||||
@@ -59,6 +53,6 @@ export const updateField =
|
||||
};
|
||||
|
||||
export const updateSegment =
|
||||
(segment: Segment) =>
|
||||
(request: UpdateSegmentRequest) =>
|
||||
(dispatch: Dispatch): Promise<unknown> =>
|
||||
runRtkEndpoint(segment, dispatch, segmentApi.endpoints.updateSegment);
|
||||
runRtkEndpoint(request, dispatch, segmentApi.endpoints.updateSegment);
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
core.version
|
||||
audit-app.view-log
|
||||
warehouses.schema
|
||||
warehouse-schema-overlay
|
||||
xrays})
|
||||
|
||||
(defn- affected-modules
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
'[dev.modules-config-test
|
||||
metabase.core.modules-test])
|
||||
|
||||
(def ^:private honey-sql-check-namespaces
|
||||
'[metabase.core.table-or-field-raw-usage-test])
|
||||
|
||||
(def ^:private ratchet-test-namespaces
|
||||
;; These test the tooling itself. `./bin/mage kondo-ratchets` checks the source tree.
|
||||
'[metabase.core.kondo-ratchet-test
|
||||
@@ -20,7 +23,10 @@
|
||||
'[metabase.core.namespace-uniqueness-test])
|
||||
|
||||
(def ^:private backend-check-namespaces
|
||||
(vec (concat module-check-namespaces ratchet-test-namespaces source-layout-check-namespaces)))
|
||||
(vec (concat module-check-namespaces
|
||||
ratchet-test-namespaces
|
||||
source-layout-check-namespaces
|
||||
honey-sql-check-namespaces)))
|
||||
|
||||
(def ^:private default-suites
|
||||
"Suites the bare `project-tests` command runs, in order."
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
metabase.core.modules-test
|
||||
metabase.core.kondo-ratchet-test
|
||||
metabase.core.kondo-ratchet-check-test
|
||||
metabase.core.namespace-uniqueness-test])]]
|
||||
metabase.core.namespace-uniqueness-test
|
||||
metabase.core.table-or-field-raw-usage-test])]]
|
||||
calls)))
|
||||
(testing "the failed suite is reported"
|
||||
(is (= ["migrations"] failed))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
(:require
|
||||
[metabase.lib.schema.id :as lib.schema.id]
|
||||
[metabase.util.malli :as mu]
|
||||
[metabase.warehouse-schema-overlay.core :as warehouse-schema-overlay]
|
||||
[metabase.warehouses.schema :as warehouses.schema]
|
||||
;; the driver persists dataset-filter and project-id migrations of its Database details back to the app DB
|
||||
^{:clj-kondo/ignore [:discouraged-namespace]}
|
||||
@@ -37,4 +38,5 @@
|
||||
"Whether the Table with `table-id` has an active database-partitioned Field named `field-name`."
|
||||
[table-id :- ::lib.schema.id/table
|
||||
field-name :- :string]
|
||||
(t2/exists? :model/Field :table_id table-id :name field-name :database_partitioned true :active true))
|
||||
(t2/exists? :model/Field :table_id table-id :name field-name :database_partitioned true :active true
|
||||
{:from [(warehouse-schema-overlay/field-query {:user-settings? false})]}))
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/Quantity
|
||||
dimensions: []
|
||||
position: 17
|
||||
custom_position: 0
|
||||
database_position: 17
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/Quantity
|
||||
dimensions: []
|
||||
position: 18
|
||||
custom_position: 0
|
||||
database_position: 18
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/Quantity
|
||||
dimensions: []
|
||||
position: 7
|
||||
custom_position: 0
|
||||
database_position: 7
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -15,7 +15,6 @@ fk_target_field_id:
|
||||
- conversation_id
|
||||
dimensions: []
|
||||
position: 9
|
||||
custom_position: 0
|
||||
database_position: 9
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -13,7 +13,6 @@ effective_type: type/DateTimeWithLocalTZ
|
||||
semantic_type: type/CreationTimestamp
|
||||
dimensions: []
|
||||
position: 1
|
||||
custom_position: 0
|
||||
database_position: 1
|
||||
serdes/meta:
|
||||
- id: Internal Metabase Database
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 17
|
||||
custom_position: 0
|
||||
database_position: 17
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ base_type: type/Text
|
||||
effective_type: type/Text
|
||||
dimensions: []
|
||||
position: 18
|
||||
custom_position: 0
|
||||
database_position: 18
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 13
|
||||
custom_position: 0
|
||||
database_position: 13
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ base_type: type/Text
|
||||
effective_type: type/Text
|
||||
dimensions: []
|
||||
position: 14
|
||||
custom_position: 0
|
||||
database_position: 14
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 4
|
||||
custom_position: 0
|
||||
database_position: 4
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 5
|
||||
custom_position: 0
|
||||
database_position: 5
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/Quantity
|
||||
dimensions: []
|
||||
position: 6
|
||||
custom_position: 0
|
||||
database_position: 6
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 16
|
||||
custom_position: 0
|
||||
database_position: 16
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 20
|
||||
custom_position: 0
|
||||
database_position: 20
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -13,7 +13,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 2
|
||||
custom_position: 0
|
||||
database_position: 2
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 3
|
||||
custom_position: 0
|
||||
database_position: 3
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/FK
|
||||
dimensions: []
|
||||
position: 15
|
||||
custom_position: 0
|
||||
database_position: 15
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/Quantity
|
||||
dimensions: []
|
||||
position: 8
|
||||
custom_position: 0
|
||||
database_position: 8
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -13,7 +13,6 @@ effective_type: type/BigInteger
|
||||
semantic_type: type/PK
|
||||
dimensions: []
|
||||
position: 0
|
||||
custom_position: 0
|
||||
database_position: 0
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ base_type: type/Text
|
||||
effective_type: type/Text
|
||||
dimensions: []
|
||||
position: 19
|
||||
custom_position: 0
|
||||
database_position: 19
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Name
|
||||
dimensions: []
|
||||
position: 12
|
||||
custom_position: 0
|
||||
database_position: 12
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -15,7 +15,6 @@ fk_target_field_id:
|
||||
- user_id
|
||||
dimensions: []
|
||||
position: 10
|
||||
custom_position: 0
|
||||
database_position: 10
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 11
|
||||
custom_position: 0
|
||||
database_position: 11
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 7
|
||||
custom_position: 0
|
||||
database_position: 7
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Boolean
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 11
|
||||
custom_position: 0
|
||||
database_position: 11
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -8,7 +8,6 @@ base_type: type/Integer
|
||||
effective_type: type/Integer
|
||||
dimensions: []
|
||||
position: 5
|
||||
custom_position: 0
|
||||
database_position: 5
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -17,7 +17,6 @@ dimensions:
|
||||
serdes/meta:
|
||||
- {id: nzL7XHp1yPu74r7BjcoJX, label: card_qualified_id, model: Dimension}
|
||||
position: 6
|
||||
custom_position: 0
|
||||
database_position: 6
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/DateTimeWithLocalTZ
|
||||
semantic_type: type/CreationTimestamp
|
||||
dimensions: []
|
||||
position: 2
|
||||
custom_position: 0
|
||||
database_position: 2
|
||||
serdes/meta:
|
||||
- {id: Internal Metabase Database, model: Database}
|
||||
|
||||
@@ -17,7 +17,6 @@ dimensions:
|
||||
serdes/meta:
|
||||
- {id: AVykiWpDBL1s7oVrW86ru, label: creator_id, model: Dimension}
|
||||
position: 4
|
||||
custom_position: 0
|
||||
database_position: 4
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ base_type: type/Text
|
||||
effective_type: type/Text
|
||||
dimensions: []
|
||||
position: 12
|
||||
custom_position: 0
|
||||
database_position: 12
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/PK
|
||||
dimensions: []
|
||||
position: 0
|
||||
custom_position: 0
|
||||
database_position: 0
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/PK
|
||||
dimensions: []
|
||||
position: 1
|
||||
custom_position: 0
|
||||
database_position: 1
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 14
|
||||
custom_position: 0
|
||||
database_position: 14
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 12
|
||||
custom_position: 0
|
||||
database_position: 12
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 13
|
||||
custom_position: 0
|
||||
database_position: 13
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 9
|
||||
custom_position: 0
|
||||
database_position: 9
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 10
|
||||
custom_position: 0
|
||||
database_position: 10
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 8
|
||||
custom_position: 0
|
||||
database_position: 8
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/DateTimeWithLocalTZ
|
||||
semantic_type: type/UpdatedTimestamp
|
||||
dimensions: []
|
||||
position: 3
|
||||
custom_position: 0
|
||||
database_position: 3
|
||||
serdes/meta:
|
||||
- {id: Internal Metabase Database, model: Database}
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 8
|
||||
custom_position: 0
|
||||
database_position: 8
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -8,7 +8,6 @@ base_type: type/Text
|
||||
effective_type: type/Text
|
||||
dimensions: []
|
||||
position: 3
|
||||
custom_position: 0
|
||||
database_position: 3
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 6
|
||||
custom_position: 0
|
||||
database_position: 6
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -17,7 +17,6 @@ dimensions:
|
||||
serdes/meta:
|
||||
- {id: 3AntG1-Ylk_c9KthNvyxV, label: entity_qualified_id, model: Dimension}
|
||||
position: 7
|
||||
custom_position: 0
|
||||
database_position: 7
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 5
|
||||
custom_position: 0
|
||||
database_position: 5
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -9,7 +9,6 @@ effective_type: type/Integer
|
||||
semantic_type: type/PK
|
||||
dimensions: []
|
||||
position: 0
|
||||
custom_position: 0
|
||||
database_position: 0
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -10,7 +10,6 @@ effective_type: type/Text
|
||||
semantic_type: type/Category
|
||||
dimensions: []
|
||||
position: 5
|
||||
custom_position: 0
|
||||
database_position: 5
|
||||
has_field_values: auto-list
|
||||
serdes/meta:
|
||||
|
||||
@@ -11,7 +11,6 @@ semantic_type: type/FK
|
||||
fk_target_field_id: [Internal Metabase Database, public, v_content, id]
|
||||
dimensions: []
|
||||
position: 6
|
||||
custom_position: 0
|
||||
database_position: 6
|
||||
serdes/meta:
|
||||
- {id: Internal Metabase Database, model: Database}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user