Convert db.clj public functions to mu/defn with schemas (#82139)

This commit is contained in:
Alexander Polyankin
2026-09-09 00:47:13 -04:00
committed by GitHub
parent 22b2577799
commit c1805286bb
285 changed files with 13653 additions and 6701 deletions

View File

@@ -146,6 +146,7 @@
collections
config
events
lib
models
permissions
util}
@@ -158,7 +159,10 @@
agent-api
{:team "Metabot"
:api #{metabase.agent-api.api metabase.agent-api.init metabase.agent-api.usage}
:api #{metabase.agent-api.api
metabase.agent-api.init
metabase.agent-api.schema
metabase.agent-api.usage}
:uses #{ai-tracing
api
auth-identity
@@ -222,6 +226,7 @@
driver
eid-translation
internal-stats
lib
lib-be
models
permissions
@@ -291,6 +296,7 @@
:uses #{api-scope
config
events
lib
models
settings
util}
@@ -325,6 +331,7 @@
:uses #{api
app-db
events
lib
models
permissions
users
@@ -363,17 +370,17 @@
{:team "UX West"
:api #{metabase.appearance.core
metabase.appearance.init}
:uses #{settings util}
:uses #{lib settings util}
:model-imports #{:model/Dashboard}}
audit-app
{:team "UX West"
:api #{metabase.audit-app.core
metabase.audit-app.init}
:api #{metabase.audit-app.core metabase.audit-app.init}
:friends #{enterprise/memoize-monitor}
:uses #{api
app-db
events
lib
models
premium-features
settings
@@ -407,14 +414,19 @@
auth-identity
{:team "UX West"
:api #{metabase.auth-identity.core metabase.auth-identity.db metabase.auth-identity.init}
:api #{metabase.auth-identity.core
metabase.auth-identity.db
metabase.auth-identity.init
metabase.auth-identity.schema}
:uses #{channel
events
lib
login-history
models
notification
premium-features
request
users
util}
:model-exports #{:model/AuthIdentity}
:model-imports #{:model/Session :model/User}}
@@ -438,6 +450,7 @@
:uses #{api
app-db
collections
lib
permissions
queries
util}
@@ -464,9 +477,7 @@
cache
{:team "UX West"
:api #{metabase.cache.api
metabase.cache.core
metabase.cache.init}
:api #{metabase.cache.api metabase.cache.core metabase.cache.init}
:uses #{api
app-db
config
@@ -601,6 +612,7 @@
audit-app
config
events
lib
models
permissions
premium-features
@@ -633,6 +645,7 @@
documents
eid-translation
events
lib
lib-be
models
notification
@@ -665,6 +678,7 @@
api
channel
events
lib
models
request
users
@@ -687,16 +701,16 @@
content-translation
{:team "UX West"
:api #{metabase.content-translation.models}
:api #{metabase.content-translation.models metabase.content-translation.schema}
:uses #{premium-features
util}
:model-exports #{:model/ContentTranslation}}
content-verification
{:team "UX West"
:api #{metabase.content-verification.core
metabase.content-verification.init}
:api #{metabase.content-verification.core metabase.content-verification.init}
:uses #{app-db
lib
models
util}
:model-exports #{:model/ModerationReview}
@@ -809,6 +823,7 @@
database-routing
driver
events
lib
request
sync
util
@@ -837,6 +852,7 @@
batch-processing
collections
events
lib
lib-be
models
parameters
@@ -961,6 +977,7 @@
{:team "Embedding"
:api #{metabase.embedding.init
metabase.embedding.jwt
metabase.embedding.schema
metabase.embedding.settings
metabase.embedding.util
metabase.embedding.validation}
@@ -1004,7 +1021,7 @@
{:team "Metabot"
:api #{metabase.entity-retrieval.core
metabase.entity-retrieval.mirror}
:uses #{premium-features}
:uses #{premium-features util}
:model-imports #{:model/OsiAiContext}}
events
@@ -1028,9 +1045,7 @@
explorations
{:team "UX West"
:api #{metabase.explorations.api
metabase.explorations.core
metabase.explorations.init}
:api #{metabase.explorations.api metabase.explorations.core metabase.explorations.init}
:uses #{analytics
analytics-interface
api
@@ -1103,6 +1118,7 @@
:api #{metabase.glossary.api}
:uses #{api
events
lib
models
util}
:model-exports #{:model/Glossary}
@@ -1127,8 +1143,7 @@
indexed-entities
{:team "UX West"
:api #{metabase.indexed-entities.api
metabase.indexed-entities.init}
:api #{metabase.indexed-entities.api metabase.indexed-entities.init}
:uses #{analytics
api
driver
@@ -1150,6 +1165,7 @@
metabase.indexes.reconcile
metabase.indexes.schema}
:uses #{driver
lib
models
util}
:model-exports #{:model/TableIndex}}
@@ -1159,6 +1175,7 @@
:api #{metabase.indexes-rest.api}
:uses #{api
indexes
lib
transforms-base
util}
:model-imports #{:model/Database
@@ -1178,8 +1195,7 @@
internal-stats
{:team "UX West"
:api #{metabase.internal-stats.core}
:uses #{app-db
models}
:uses #{app-db models util}
:model-imports #{:model/AiUsageLog
:model/Card
:model/Dashboard
@@ -1316,13 +1332,12 @@
login-history
{:team "UX West"
:api #{metabase.login-history.api
metabase.login-history.core
metabase.login-history.init}
:api #{metabase.login-history.api metabase.login-history.core metabase.login-history.init}
:uses #{analytics
api
app-db
channel
lib
request
settings
util}
@@ -1334,6 +1349,7 @@
metabase.mcp.callback-api
metabase.mcp.core
metabase.mcp.init
metabase.mcp.schema
metabase.mcp.usage}
:model-exports #{:model/McpSessionLog
:model/McpToolCallLog}
@@ -1342,6 +1358,7 @@
api
app-db
config
lib
llm
metabot
oauth-server
@@ -1355,7 +1372,7 @@
measures
{:team "Graphy"
:api #{metabase.measures.api}
:api #{metabase.measures.api metabase.measures.schema}
:uses #{api
events
lib
@@ -1376,6 +1393,7 @@
metabase.metabot.core
metabase.metabot.init
metabase.metabot.metadata-perms
metabase.metabot.schema
metabase.metabot.self
metabase.metabot.settings
metabase.metabot.tools.entity-details}
@@ -1473,9 +1491,7 @@
model-persistence
{:team "Graphy"
:api #{metabase.model-persistence.api
metabase.model-persistence.core
metabase.model-persistence.init}
:api #{metabase.model-persistence.api metabase.model-persistence.core metabase.model-persistence.init}
:uses #{api
app-db
driver
@@ -1492,7 +1508,8 @@
task
task-history
tracing
util}
util
warehouses}
:model-exports #{:model/PersistedInfo}
:model-imports #{:model/Card :model/Database}}
@@ -1542,8 +1559,7 @@
native-query-snippets
{:team "Graphy"
:api #{metabase.native-query-snippets.api
metabase.native-query-snippets.core}
:api #{metabase.native-query-snippets.api metabase.native-query-snippets.core}
:uses #{api
collections
events
@@ -1612,6 +1628,7 @@
api-scope
appearance
events
lib
mcp
models
request
@@ -1682,6 +1699,7 @@
remote-sync
settings
tracing
users
util
enterprise/advanced-permissions}
:model-exports #{:model/ApplicationPermissionsRevision
@@ -1776,9 +1794,7 @@
:api #{metabase.public-sharing.core
metabase.public-sharing.init
metabase.public-sharing.validation}
:uses #{api
settings
util}
:uses #{api settings util}
:model-imports #{:model/Action
:model/Card
:model/Dashboard
@@ -1811,9 +1827,7 @@
pulse
{:team "Gadget"
:api #{metabase.pulse.api
metabase.pulse.core
metabase.pulse.init}
:api #{metabase.pulse.api metabase.pulse.core metabase.pulse.init}
:uses #{api
app-db
channel
@@ -2040,8 +2054,10 @@
metabase.remote-sync.init}
:uses #{api
events
lib
models
premium-features}
premium-features
util}
:model-imports #{:model/Collection}}
request
@@ -2055,7 +2071,8 @@
api
config
embedding
permissions ; FIXME
lib
permissions
premium-features
session
settings
@@ -2107,10 +2124,12 @@
:api #{metabase.sample-data.core
metabase.sample-data.init}
:uses #{driver
lib
plugins
settings
sync
util}
util
warehouses}
:model-imports #{:model/DataPermissions :model/Database :model/Table}}
search
@@ -2158,10 +2177,21 @@
:model/Table
:model/User}}
secrets
{:team "Graphy"
:api #{metabase.secrets.core}
:uses #{api driver models premium-features util}}
:uses #{api
driver
lib
models
premium-features
util}}
security-center
{:team "Gadget"
:api #{metabase.security-center.schema}
:uses #{util}}
segments
{:team "Graphy"
@@ -2199,6 +2229,7 @@
driver
embedding
initialization-status
lib
lib-be
mcp
oauth-server
@@ -2228,6 +2259,7 @@
channel
config
events
lib
premium-features
request
settings
@@ -2246,6 +2278,7 @@
app-db
config
events
lib
models
premium-features
util
@@ -2267,6 +2300,7 @@
metabase.setup.init}
:uses #{app-db
config
lib
settings
util}
:model-imports #{:model/User}}
@@ -2279,6 +2313,7 @@
appearance
auth-identity
events
lib
request
settings
setup
@@ -2353,6 +2388,7 @@
:uses #{api
auth-identity
config
lib
permissions
premium-features
request
@@ -2433,11 +2469,10 @@
task-history
{:team "UX West"
:api #{metabase.task-history.api
metabase.task-history.core
metabase.task-history.init}
:api #{metabase.task-history.api metabase.task-history.core metabase.task-history.init}
:uses #{api
config
lib
models
permissions
premium-features
@@ -2469,9 +2504,11 @@
lib-be
llm
mcp
metabot
permissions
premium-features
search
security-center
session
util}
:model-imports #{:model/AiUsageLog
@@ -2502,12 +2539,12 @@
timeline
{:team "UX West"
:api #{metabase.timeline.api
metabase.timeline.core}
:api #{metabase.timeline.api metabase.timeline.core}
:uses #{analytics
api
collections
events
lib
models
util}
:model-exports #{:model/Timeline}
@@ -2610,6 +2647,7 @@
metabase.transforms-rest.api.transform-job
metabase.transforms-rest.api.transform-tag}
:uses #{api
lib
models
request
transforms
@@ -2725,6 +2763,7 @@
channel
config
events
lib
models
notification
permissions
@@ -2751,6 +2790,7 @@
collections
config
events
lib
models
permissions
premium-features
@@ -2786,14 +2826,14 @@
view-log
{:team "UX West"
:api #{metabase.view-log.core
metabase.view-log.init}
:api #{metabase.view-log.core metabase.view-log.init}
:uses #{analytics
api
app-db
audit-app
batch-processing
events
lib
models
premium-features
query-permissions
@@ -2814,6 +2854,7 @@
metabase.warehouse-schema.models.field-user-settings
metabase.warehouse-schema.models.field-values
metabase.warehouse-schema.models.table
metabase.warehouse-schema.schema
metabase.warehouse-schema.table}
:uses #{analyze
api
@@ -2879,7 +2920,8 @@
{:team "UX West"
:api #{metabase.warehouses.core
metabase.warehouses.init
metabase.warehouses.models.database}
metabase.warehouses.models.database
metabase.warehouses.schema}
:uses #{analytics-interface
api
app-db
@@ -3008,7 +3050,8 @@
setup
sync
users
util}
util
warehouses}
:model-imports #{:model/ApiKey :model/Database :model/User}}
enterprise/advanced-permissions
@@ -3051,6 +3094,7 @@
:api #{}
:uses #{driver
premium-features
util
enterprise/advanced-config
enterprise/mfa
enterprise/scim
@@ -3085,6 +3129,7 @@
permissions
plugins
premium-features
queries
query-permissions
query-processor
settings
@@ -3093,6 +3138,8 @@
sync
users
util
warehouse-schema
warehouses
enterprise/serialization}
:model-imports #{:model/AuditLog
:model/Card
@@ -3125,6 +3172,7 @@
{:team "UX West"
:api #{metabase-enterprise.billing.api.routes}
:uses #{api
lib
premium-features
store-api
util}
@@ -3134,6 +3182,7 @@
{:team "UX West"
:api #{metabase-enterprise.cache.init}
:uses #{cache
lib
premium-features
query-processor
task
@@ -3186,6 +3235,7 @@
:api #{metabase-enterprise.content-verification.api.routes}
:uses #{api
content-verification
lib
util
enterprise/api}
:model-imports #{:model/Card :model/Dashboard}}
@@ -3240,6 +3290,7 @@
config
driver
embeddings
lib
metabot
models
premium-features
@@ -3263,6 +3314,7 @@
:uses #{api
collections
events
lib
models
permissions
premium-features
@@ -3280,6 +3332,7 @@
metabase-enterprise.database-replication.init}
:uses #{api
driver
lib
premium-features
settings
util
@@ -3367,8 +3420,10 @@
appearance
audit-app
embedding
lib
permissions
premium-features
util
enterprise/sso}
:model-imports #{:model/Card
:model/Collection
@@ -3396,6 +3451,7 @@
collections
entity-retrieval
health-inspector
lib
premium-features
search
task
@@ -3411,6 +3467,7 @@
{:team "Gadget"
:api #{metabase-enterprise.erd.api}
:uses #{api
lib
models
permissions
util
@@ -3426,9 +3483,11 @@
:uses #{analytics
analytics-interface
api
lib
premium-features
settings
util
warehouses
enterprise/harbormaster}
:model-imports #{:model/Database :model/Setting}}
@@ -3466,13 +3525,16 @@
:api #{metabase-enterprise.library.api}
:uses #{api
collections
premium-features}
lib
premium-features
util}
:model-imports #{:model/Card :model/Collection :model/Table}}
enterprise/mcp
{:team "Metabot"
:api #{metabase-enterprise.mcp.init}
:uses #{analytics
lib
mcp
metabot
premium-features
@@ -3504,6 +3566,7 @@
mcp
metabot}
:uses #{api
lib
lib-be
llm
metabot
@@ -3527,6 +3590,7 @@
:api #{metabase-enterprise.metabot-analytics.api}
:uses #{api
driver
lib
metabot
permissions
query-processor
@@ -3547,6 +3611,7 @@
channel
config
events
lib
premium-features
request
settings
@@ -3581,6 +3646,7 @@
app-db
collections
events
lib
models
premium-features
search
@@ -3615,12 +3681,15 @@
:uses #{analytics
api
app-db
dashboards
events
lib
lib-be
measures
model-persistence
models
queries
segments
source-swap
task
transforms
@@ -3640,8 +3709,7 @@
enterprise/sandbox
{:team "UX West"
:api #{metabase-enterprise.sandbox.api.routes
metabase-enterprise.sandbox.api.util}
:api #{metabase-enterprise.sandbox.api.routes metabase-enterprise.sandbox.api.util}
:uses #{api
app-db
audit-app
@@ -3653,6 +3721,7 @@
models
permissions
premium-features
queries
query-processor
request
tenants
@@ -3678,6 +3747,7 @@
:uses #{analytics-interface
api
api-keys
lib
models
permissions
server
@@ -3698,8 +3768,7 @@
enterprise/security-center
{:team "Gadget"
:api #{metabase-enterprise.security-center.api
metabase-enterprise.security-center.init}
:api #{metabase-enterprise.security-center.api metabase-enterprise.security-center.init}
:uses #{analytics
analytics-interface
api
@@ -3708,10 +3777,12 @@
config
driver
events
lib
models
notification
permissions
premium-features
security-center
settings
system
task
@@ -3739,6 +3810,7 @@
embeddings
events
health-inspector
lib
lib-be
llm
metabot
@@ -3783,7 +3855,8 @@
enterprise/snippet-collections
{:team "Graphy"
:api #{}
:uses #{models
:uses #{lib
models
native-query-snippets
permissions
premium-features
@@ -3819,6 +3892,7 @@
:uses #{analytics
api
collections
lib
premium-features
queries
request
@@ -3835,11 +3909,13 @@
app-db
auth-identity
events
lib
models
request
settings
system
task
users
util}
:model-imports #{:model/AuthIdentity :model/Session :model/User}}
@@ -3851,6 +3927,7 @@
auth-identity
collections
events
lib
models
permissions
premium-features
@@ -3863,7 +3940,8 @@
enterprise/transforms
{:team "Graphy"
:api #{metabase-enterprise.transforms.api}
:uses #{premium-features
:uses #{lib
premium-features
util
enterprise/transforms-inspector}
:model-imports #{:model/Transform}}
@@ -3920,6 +3998,7 @@
{:team "UX West"
:api #{metabase-enterprise.upload-management.api}
:uses #{api
lib
models
permissions
premium-features

View File

@@ -2,53 +2,59 @@
"Application database queries for the action-v2 module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
(:require
[metabase-enterprise.action-v2.schema :as action-v2.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util :as u]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn table
(mu/defn table
"The Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one :model/Table table-id))
(defn active-table
(mu/defn active-table
"The active Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one :model/Table :id table-id :active true))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database :id database-id))
(defn fields
(mu/defn fields
"The Fields with `field-ids`."
[field-ids]
[field-ids :- [:sequential ::lib.schema.id/field]]
(t2/select :model/Field :id [:in field-ids]))
(defn pk-fields-for-table
(mu/defn pk-fields-for-table
"The active primary key Fields of the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select :model/Field :table_id table-id :semantic_type :type/PK :active true))
(defn fields-by-name
(mu/defn fields-by-name
"The Fields of the Table with `table-id` named one of `field-names`."
[table-id field-names]
[table-id :- ::lib.schema.id/table
field-names :- [:sequential :string]]
(t2/select :model/Field :table_id table-id :name [:in field-names]))
(defn active-fields-in-position-order
(mu/defn active-fields-in-position-order
"The active Fields of the Table with `table-id`, in position order."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select :model/Field :table_id table-id :active true {:order-by [[:position]]}))
(defn field-requirements-by-name
(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]
[table-id :- ::lib.schema.id/table]
(t2/select-fn->fn :name identity [:model/Field :name :database_required :base_type] :table_id table-id))
(defn category-list-field-ids-by-name
(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
of `names`, case-insensitively."
[table-id names]
[table-id :- ::lib.schema.id/table
names :- [:sequential :string]]
(t2/query {:select [:id [[:lower :name] :lower_name]]
:from [(t2/table-name :model/Field)]
:where [:and
@@ -57,9 +63,9 @@
[:in :has_field_values ["list" "auto-list"]]
[:= :semantic_type "type/Category"]]}))
(defn field-values-of-fields
(mu/defn field-values-of-fields
"The value lists of the FieldValues of the Fields with `field-ids`."
[field-ids]
[field-ids :- [:sequential ::lib.schema.id/field]]
(t2/select-fn-vec :values :model/FieldValues :field_id [:in field-ids]))
(defn- scope-and-user-expr
@@ -68,9 +74,11 @@
(when scope [:= :scope scope])
(when user-id [:= :user_id user-id])])
(defn next-undo-batch
(mu/defn next-undo-batch
"The Undo rows of the newest not-undone (when `undo?`) or oldest undone batch of `user-id` in `scope`."
[undo? user-id scope]
[undo? :- :boolean
user-id :- ::lib.schema.id/user
scope :- :string]
(t2/select :model/Undo
:batch_num [:in
^:allow-subquery
@@ -83,10 +91,12 @@
[:not :undone]
:undone)]}]))
(defn batch-to-prune-from
(mu/defn batch-to-prune-from
"The `:batch_num` row of the newest Undo batch beyond the `batches-to-keep` most recent ones, narrowed by the
optional `scope` and `user-id`, or nil."
[batches-to-keep scope user-id]
[batches-to-keep :- ms/PositiveInt
scope :- [:maybe :string]
user-id :- [:maybe ::lib.schema.id/user]]
(t2/query {:select [:batch_num]
:from [(t2/table-name :model/Undo)]
:where (scope-and-user-expr scope user-id)
@@ -95,56 +105,66 @@
:limit 1
:offset batches-to-keep}))
(defn batch-to-prune-from-for-rows
(mu/defn batch-to-prune-from-for-rows
"The `:batch_num` row of the Undo row beyond the `rows-to-keep` most recent ones, or nil."
[rows-to-keep]
[rows-to-keep :- ms/PositiveInt]
(t2/query {:select [:batch_num]
:from [(t2/table-name :model/Undo)]
:order-by [[:id :desc]]
:limit 1
:offset rows-to-keep}))
(defn delete-undo-batches-up-to!
(mu/defn delete-undo-batches-up-to!
"Delete the Undo rows of batches up to `batch-num`, narrowed by the optional `scope` and `user-id`."
[batch-num scope user-id]
[batch-num :- ms/IntGreaterThanOrEqualToZero
scope :- [:maybe :string]
user-id :- [:maybe ::lib.schema.id/user]]
(t2/delete! :model/Undo :batch_num [:<= batch-num] {:where (scope-and-user-expr scope user-id)}))
(defn delete-undone-batches-from!
(mu/defn delete-undone-batches-from!
"Delete the undone Undo rows in `scope` from batch `batch-num` onwards."
[batch-num scope]
[batch-num :- ms/PositiveInt
scope :- :string]
(t2/delete! :model/Undo :batch_num [:>= batch-num] :scope scope :undone true))
(defn insert-undos!
(mu/defn insert-undos!
"Insert the Undo `undos`."
[undos]
[undos :- [:sequential
::action-v2.schema/undo.update]]
(t2/insert! :model/Undo undos))
(defn mark-batch-undone!
(mu/defn mark-batch-undone!
"Set the undone flag of the Undo batch `batch-num` to `undone?`."
[batch-num undone?]
[batch-num :- ms/PositiveInt
undone? :- :boolean]
(t2/update! :model/Undo {:batch_num batch-num} {:undone undone?}))
(defn superseding-change-exists?
(mu/defn superseding-change-exists?
"Whether a later (when `undo?`) or earlier Undo row for the rows `row-pks` of the Tables `table-ids` exists beyond
batch `batch-num` with the opposite undone state."
[undo? table-ids row-pks batch-num]
[undo? :- :boolean
table-ids :- [:set ::lib.schema.id/table]
row-pks :- [:set :some]
batch-num :- ms/PositiveInt]
(t2/exists? :model/Undo
:table_id [:in table-ids]
:row_pk [:in row-pks]
:batch_num [(if undo? :> :<) batch-num]
:undone (not undo?)))
(defn lock-sequence-next-val
(mu/defn lock-sequence-next-val
"The next value of the raw sequence row named `sequence-name`, locked for update, or nil."
[sequence-name]
[sequence-name :- :string]
(t2/select-one-fn :next_val [:sequences :next_val] :name sequence-name {:for :update}))
(defn set-sequence-next-val!
(mu/defn set-sequence-next-val!
"Set the next value of the raw sequence row named `sequence-name`."
[sequence-name next-val]
[sequence-name :- :string
next-val :- :int]
(t2/update! :sequences {:name sequence-name} {:next_val next-val}))
(defn insert-sequence!
(mu/defn insert-sequence!
"Insert a raw sequence row named `sequence-name` whose next value is `next-val`."
[sequence-name next-val]
[sequence-name :- :string
next-val :- :int]
(t2/insert! :sequences {:name sequence-name :next_val next-val}))

View File

@@ -0,0 +1,49 @@
(ns metabase-enterprise.action-v2.schema
"Malli schemas for the action-v2 module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::undo.row-pk
"The `:row_pk` column of a Undo, decoded."
:map)
(mr/def ::undo.raw-before
"The `:raw_before` column of a Undo, decoded."
:map)
(mr/def ::undo.raw-after
"The `:raw_after` column of a Undo, decoded."
:map)
(mr/def ::undo
"A Undo as selected from the app DB: every column of `:data_edit_undo_chain`."
[:map {:closed true}
[:id ms/PositiveInt]
[:batch_num :int]
[:table_id ::lib.schema.id/table]
[:row_pk ::undo.row-pk]
[:user_id ::lib.schema.id/user]
[:scope [:or :keyword :string]]
[:undoable :boolean]
[:raw_before [:maybe ::undo.raw-before]]
[:raw_after [:maybe ::undo.raw-after]]
[:undone :boolean]
[:created_at ms/TemporalInstant]
[:updated_at ms/TemporalInstant]])
(mr/def ::undo.update
"What an update (or insert) of a Undo accepts: every column of `:data_edit_undo_chain` except `id`, all optional."
[:map {:closed true}
[:batch_num {:optional true} [:maybe :int]]
[:table_id {:optional true} [:maybe ::lib.schema.id/table]]
[:row_pk {:optional true} [:maybe ::undo.row-pk]]
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:scope {:optional true} [:maybe [:or :keyword :string]]]
[:undoable {:optional true} [:maybe :boolean]]
[:raw_before {:optional true} [:maybe ::undo.raw-before]]
[:raw_after {:optional true} [:maybe ::undo.raw-after]]
[:undone {:optional true} [:maybe :boolean]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]])

View File

@@ -2,12 +2,19 @@
"Application database queries for the advanced-config module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself."
(:require
[metabase.api-keys.schema :as api-keys.schema]
[metabase.app-db.core :as mdb]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.users.schema :as users.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[metabase.warehouses.schema :as warehouses.schema]
[toucan2.core :as t2]))
(defn query-executions-in-month
(mu/defn query-executions-in-month
"The raw query execution rows started in `month` of `year`, newest first."
[year month]
[year :- ms/PositiveInt
month :- ms/PositiveInt]
(let [date-part (fn [part-key part-value]
(if (= (mdb/db-type) :postgres)
[:= [:date_part ^:allow-raw-sql [:inline (name part-key)] :started_at] part-value]
@@ -18,62 +25,67 @@
(date-part :year year)
(date-part :month month)]})))
(defn api-key-by-name
(mu/defn api-key-by-name
"The ApiKey named `api-key-name`, or nil."
[api-key-name]
[api-key-name :- :string]
(t2/select-one :model/ApiKey :name api-key-name))
(defn api-key-prefix-exists?
(mu/defn api-key-prefix-exists?
"Whether an ApiKey with `prefix` exists."
[prefix]
[prefix :- :string]
(t2/exists? :model/ApiKey :key_prefix prefix))
(defn insert-api-key!
"Insert `api-key` and return the new instance."
[api-key]
(mu/defn insert-api-key!
"Insert `api-key` and return the new instance. `::api-keys/unhashed-key` has a special meaning to the ApiKey
model's before-insert hook: it is hashed into `:key` and used to derive `:key_prefix`."
[api-key :- ::api-keys.schema/api-key.create]
(t2/insert-returning-instance! :model/ApiKey api-key))
(defn user-by-email
(mu/defn user-by-email
"The User with `email`, or nil."
[email]
[email :- :string]
(t2/select-one :model/User :email email))
(defn user-columns-by-email
(mu/defn user-columns-by-email
"The `columns` of the User with `email`, or nil."
[columns email]
[columns :- [:sequential :keyword]
email :- :string]
(t2/select-one (into [:model/User] columns) :email email))
(defn insert-user!
(mu/defn insert-user!
"Insert `user` and return the new instance."
[user]
[user :- ::users.schema/user.update]
(t2/insert-returning-instance! :model/User user))
(defn update-user!
(mu/defn update-user!
"Apply `changes` to the User with `user-id`."
[user-id changes]
[user-id :- ::lib.schema.id/user
changes :- ::users.schema/user.update]
(t2/update! :model/User user-id changes))
(defn sample-database-exists?
(mu/defn sample-database-exists?
"Whether the sample Database exists."
[]
(t2/exists? :model/Database :is_sample true))
(defn database-id-by-engine-and-name
(mu/defn database-id-by-engine-and-name
"The ID of the Database of `engine` named `database-name`, or nil."
[engine database-name]
[engine :- :string
database-name :- :string]
(t2/select-one-pk :model/Database :engine engine :name database-name))
(defn insert-database!
(mu/defn insert-database!
"Insert `database` and return the new instance."
[database]
[database :- ::warehouses.schema/database.update]
(t2/insert-returning-instance! :model/Database database))
(defn update-database!
(mu/defn update-database!
"Apply `changes` to the Database with `database-id`."
[database-id changes]
[database-id :- ::lib.schema.id/database
changes :- ::warehouses.schema/database.update]
(t2/update! :model/Database database-id changes))
(defn delete-database!
(mu/defn delete-database!
"Delete the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/delete! :model/Database database-id))

View File

@@ -2,22 +2,27 @@
"Application database queries for the advanced-permissions module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn table
(mu/defn table
"The Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one :model/Table :id table-id))
(defn group-manager?
(mu/defn group-manager?
"Whether the User with `user-id` manages the PermissionsGroup with `group-id`."
[user-id group-id]
[user-id :- ::lib.schema.id/user
group-id :- ms/PositiveInt]
(t2/select-one-fn :is_group_manager :model/PermissionsGroupMembership :user_id user-id :group_id group-id))
(defn blocked-database-ids-for-group
(mu/defn blocked-database-ids-for-group
"The subset of `database-ids` the group with `group-id` is blocked from viewing."
[group-id database-ids]
[group-id :- ms/PositiveInt
database-ids :- [:sequential ::lib.schema.id/database]]
(t2/select-fn-set :db_id :model/DataPermissions
:perm_type :perms/view-data
:perm_value :blocked
@@ -25,14 +30,16 @@
:db_id [:in database-ids]
{:select-distinct [:db_id]}))
(defn impersonated-database-ids-for-group
(mu/defn impersonated-database-ids-for-group
"The subset of `database-ids` the group with `group-id` accesses through connection impersonation."
[group-id database-ids]
[group-id :- ms/PositiveInt
database-ids :- [:sequential ::lib.schema.id/database]]
(t2/select-fn-set :db_id :model/ConnectionImpersonation :group_id group-id :db_id [:in database-ids]))
(defn sandboxed-database-ids-for-group
(mu/defn sandboxed-database-ids-for-group
"The `:db_id` rows of the Databases among `database-ids` the group with `group-id` has a sandbox on."
[group-id database-ids]
[group-id :- ms/PositiveInt
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]]
@@ -40,28 +47,29 @@
[:= :s.group_id group-id]
[:in :t.db_id database-ids]]}))
(defn blocked-group-ids
(mu/defn blocked-group-ids
"The subset of `group-ids` blocked from viewing some Database."
[group-ids]
[group-ids :- [:sequential ms/PositiveInt]]
(t2/select-fn-set :group_id :model/DataPermissions
:perm_type :perms/view-data
:perm_value :blocked
:group_id [:in group-ids]
{:select-distinct [:group_id]}))
(defn impersonated-group-ids
(mu/defn impersonated-group-ids
"The subset of `group-ids` with a connection impersonation."
[group-ids]
[group-ids :- [:sequential ms/PositiveInt]]
(t2/select-fn-set :group_id :model/ConnectionImpersonation :group_id [:in group-ids]))
(defn sandboxed-group-ids
(mu/defn sandboxed-group-ids
"The subset of `group-ids` with a sandbox."
[group-ids]
[group-ids :- [:sequential ms/PositiveInt]]
(t2/select-fn-set :group_id :model/Sandbox :group_id [:in group-ids]))
(defn blocked-group-ids-for-database
(mu/defn blocked-group-ids-for-database
"The subset of `group-ids` blocked from viewing the Database with `database-id`."
[database-id group-ids]
[database-id :- ::lib.schema.id/database
group-ids :- [:sequential ms/PositiveInt]]
(t2/select-fn-set :group_id :model/DataPermissions
:db_id database-id
:perm_type :perms/view-data
@@ -69,9 +77,10 @@
:group_id [:in group-ids]
{:select-distinct [:group_id]}))
(defn sandboxed-group-ids-for-database
(mu/defn sandboxed-group-ids-for-database
"The `:group_id` rows of the groups among `group-ids` with a sandbox on the Database with `database-id`."
[database-id group-ids]
[database-id :- ::lib.schema.id/database
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]]
@@ -79,7 +88,7 @@
[:in :s.group_id group-ids]
[:= :t.db_id database-id]]}))
(defn application-permissions
(mu/defn application-permissions
"The Permissions rows for the root object and every application object."
[]
(t2/select :model/Permissions
@@ -87,12 +96,12 @@
[:= :object "/"]
[:like :object (h2x/literal "/application/%")]]}))
(defn user-group-memberships
(mu/defn user-group-memberships
"The group ID (as `:id`) and manager flag of the memberships of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select [:model/PermissionsGroupMembership [:group_id :id] :is_group_manager] :user_id user-id))
(defn managed-group-ids
(mu/defn managed-group-ids
"The IDs of the PermissionsGroups the User with `user-id` manages."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-fn-set :group_id :model/PermissionsGroupMembership :user_id user-id :is_group_manager true))

View File

@@ -2,14 +2,17 @@
"Application database queries for the agent-api module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.agent-api.schema :as agent-api.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn insert-call-log!
(mu/defn insert-call-log!
"Insert the AgentApiCallLog `row`."
[row]
[row :- ::agent-api.schema/agent-api-call-log.update]
(t2/insert! :model/AgentApiCallLog row))
(defn delete-call-logs-created-before!
(mu/defn delete-call-logs-created-before!
"Delete the AgentApiCallLogs created before `cutoff`, returning the number deleted."
[cutoff]
[cutoff :- ms/TemporalInstant]
(t2/delete! :model/AgentApiCallLog {:where [:< :created_at cutoff]}))

View File

@@ -2,14 +2,15 @@
"Application database queries for the analytics module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn database-with-engine-exists?
(mu/defn database-with-engine-exists?
"Whether a Database whose engine is one of `engines` exists."
[engines]
[engines :- [:sequential :keyword]]
(t2/exists? :model/Database :engine [:in engines]))
(defn sandbox-exists?
(mu/defn sandbox-exists?
"Whether any Sandbox exists."
[]
(t2/exists? :model/Sandbox))

View File

@@ -67,8 +67,8 @@
;; one transaction so a crash can't commit the database row yet skip the permissions guard below —
;; the next boot would see the database and no-op, leaving the stale permissions with no re-run path
(t2/with-transaction [_conn]
(audit-app.db/insert-database! {:is_audit true
:id id
(audit-app.db/insert-database! id
{:is_audit true
:name default-db-name
:description "Internal Audit DB used to power metabase analytics."
:engine engine

View File

@@ -2,67 +2,76 @@
"Application database queries for the audit-app module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for transactions and connections."
(:require
[malli.util :as mut]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.queries.schema :as queries.schema]
[metabase.util.malli :as mu]
[metabase.warehouse-schema.schema :as warehouse-schema.schema]
[metabase.warehouses.schema :as warehouses.schema]
[toucan2.core :as t2]))
(defn audit-database
(mu/defn audit-database
"The audit Database, or nil."
[]
(t2/select-one :model/Database :is_audit true))
(defn non-audit-database-named
(mu/defn non-audit-database-named
"The non-audit Database named `database-name`, or nil."
[database-name]
[database-name :- :string]
(t2/select-one :model/Database :name database-name :is_audit false))
(defn insert-database!
"Insert the Database `row`."
[row]
(t2/insert! :model/Database row))
(mu/defn insert-database!
"Insert the Database `row` with the fixed `id`."
[id :- ::lib.schema.id/database
row :- ::warehouses.schema/database.update]
(t2/insert! :model/Database (assoc row :id id)))
(defn insert-returning-database!
(mu/defn insert-returning-database!
"Insert the Database `row` and return the new instance."
[row]
[row :- ::warehouses.schema/database.update]
(t2/insert-returning-instance! :model/Database row))
(defn set-database-engine!
(mu/defn set-database-engine!
"Set the engine of the Database with `database-id`."
[database-id engine]
[database-id :- ::lib.schema.id/database
engine :- :string]
(t2/update! :model/Database database-id {:engine engine}))
(defn delete-database!
(mu/defn delete-database!
"Delete the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/delete! :model/Database :id database-id))
(defn delete-audit-databases!
(mu/defn delete-audit-databases!
"Delete every audit Database."
[]
(t2/delete! :model/Database :is_audit true))
(defn delete-permissions-for-database!
(mu/defn delete-permissions-for-database!
"Delete the Permissions rows on the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/delete! :model/Permissions {:where [:like :object (str "%/db/" database-id "/%")]}))
(defn tables-of-database-named
(mu/defn tables-of-database-named
"The Tables of the Database with `database-id` named one of `table-names`."
[database-id 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]))
(defn tables-of-database-in-id-order
(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]
[database-id :- ::lib.schema.id/database]
(t2/select [:model/Table :id :name :schema :active] :db_id database-id {:order-by [[:id :asc]]}))
(defn active-public-table-exists?
(mu/defn active-public-table-exists?
"Whether the Database with `database-id` has an active Table in the `public` schema."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Table :db_id database-id :schema "public" :active true))
(defn table-ids-to-downcase
(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
yet."
[database-id]
[database-id :- ::lib.schema.id/database]
(mapv :id
(t2/query {:select [:table.id]
:from [[(t2/table-name :model/Table) :table]]
@@ -82,15 +91,15 @@
[:= :table.schema nil]]]
[:= :self_table.name [:lower :table.name]]]}]]]})))
(defn downcase-tables!
(mu/defn downcase-tables!
"Move the Tables with `table-ids` to the `public` schema and lower-case their names."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/update! :model/Table :id [:in table-ids] {:schema "public" :name [:lower :name]}))
(defn field-ids-to-downcase
(mu/defn field-ids-to-downcase
"The IDs of the Fields of the Metabase-managed Tables of the Database with `database-id` that have no
lower-cased counterpart yet."
[database-id]
[database-id :- ::lib.schema.id/database]
(mapv :id
(t2/query {:select [:field.id]
:from [[(t2/table-name :model/Field) :field]]
@@ -112,24 +121,24 @@
[:= :table.schema nil]]]
[:= :self_field.name [:lower :field.name]]]}]]]})))
(defn downcase-fields!
(mu/defn downcase-fields!
"Lower-case the names of the Fields with `field-ids`."
[field-ids]
[field-ids :- [:sequential ::lib.schema.id/field]]
(t2/update! :model/Field :id [:in field-ids] {:name [:lower :name]}))
(defn clear-table-schemas!
(mu/defn clear-table-schemas!
"Clear the schema of the Tables of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/update! :model/Table {:db_id database-id} {:schema nil}))
(defn upcase-tables!
(mu/defn upcase-tables!
"Upper-case the schemas and names of the Tables of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/update! :model/Table {:db_id database-id} {:schema [:upper :schema] :name [:upper :name]}))
(defn upcase-fields!
(mu/defn upcase-fields!
"Upper-case the names of the Fields of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/update! :model/Field
{:table_id
[:in
@@ -138,74 +147,76 @@
:where [:= :db_id database-id]}]}
{:name [:upper :name]}))
(defn update-table!
(mu/defn update-table!
"Apply `changes` to the Table with `table-id`."
[table-id changes]
[table-id :- ::lib.schema.id/table
changes :- (mut/select-keys ::warehouse-schema.schema/table.update [:active :name :schema :is_defective_duplicate])]
(t2/update! :model/Table table-id changes))
(defn delete-table!
(mu/defn delete-table!
"Delete the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/delete! :model/Table table-id))
(defn field-names-of-table
(mu/defn field-names-of-table
"The `:id` and `:name` of the Fields of the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select [:model/Field :id :name] :table_id table-id))
(defn card-result-metadata-reducible
(mu/defn card-result-metadata-reducible
"Reducible raw `:id` and `:result_metadata` rows of the Cards of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/reducible-select [(t2/table-name :model/Card) :id :result_metadata] :database_id database-id))
(defn cards-of-table
(mu/defn cards-of-table
"The Cards on the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select :model/Card :table_id table-id))
(defn table-ids-referenced-by-cards
(mu/defn table-ids-referenced-by-cards
"The distinct `:table_id` rows of the Cards on the Tables with `table-ids`."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/query {:select-distinct [:table_id]
:from [(t2/table-name :model/Card)]
:where [:in :table_id table-ids]}))
(defn update-card!
(mu/defn update-card!
"Apply `changes` to the Card with `card-id`."
[card-id changes]
[card-id :- ::lib.schema.id/card
changes :- (mut/select-keys ::queries.schema/card.update [:dataset_query :result_metadata])]
(t2/update! :model/Card card-id changes))
(defn first-superuser
(mu/defn first-superuser
"The `:id` and `:email` of the oldest superuser, or nil."
[]
(t2/select-one [:model/User :id :email] :is_superuser true {:order-by [[:id :asc]]}))
(defn collection-by-entity-id
(mu/defn collection-by-entity-id
"The Collection with `entity-id`, or nil."
[entity-id]
[entity-id :- :string]
(t2/select-one :model/Collection :entity_id entity-id))
(defn delete-analytics-collections!
(mu/defn delete-analytics-collections!
"Delete the Collections of the analytics namespace."
[]
(t2/delete! :model/Collection :namespace "analytics"))
(defn delete-pulse-channel-recipients-of-user!
(mu/defn delete-pulse-channel-recipients-of-user!
"Delete the PulseChannelRecipients of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/delete! :model/PulseChannelRecipient :user_id user-id))
(defn delete-notification-recipients-of-user!
(mu/defn delete-notification-recipients-of-user!
"Delete the NotificationRecipients of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/delete! :model/NotificationRecipient :user_id user-id))
(defn archive-pulses-of-creator!
(mu/defn archive-pulses-of-creator!
"Archive the unarchived Pulses created by the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/update! :model/Pulse {:creator_id user-id, :archived false} {:archived true}))
(defn deactivate-notifications-of-creator!
(mu/defn deactivate-notifications-of-creator!
"Deactivate the active Notifications created by the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/update! :model/Notification {:creator_id user-id :active true} {:active false}))

View File

@@ -2,14 +2,16 @@
"Application database queries for the billing module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn active-personal-user-count
(mu/defn active-personal-user-count
"The number of active personal Users."
[]
(t2/count :model/User :is_active true :type :personal))
(defn user-email
(mu/defn user-email
"The email of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-fn :email :model/User :id user-id))

View File

@@ -2,12 +2,30 @@
"Application database queries for the cache module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn card-cache-config
(def ^:private DurationScope
"One `{:model :model-id :rerun-cutoff}` scope, as built by `duration-scopes`."
[:map {:closed true}
[:model :string]
[:model-id ms/PositiveInt]
[:rerun-cutoff ms/TemporalInstant]])
(def ^:private CacheConfigChanges
"The keys callers pass to [[update-cache-config!]]."
[:map {:closed true}
[:next_run_at {:optional true} [:maybe ms/TemporalInstant]]
[:invalidated_at {:optional true} ms/TemporalInstant]])
(mu/defn card-cache-config
"The most specific CacheConfig applying to the Card with `card-id` on the Dashboard with `dashboard-id` in the
Database with `database-id`, or nil."
[card-id dashboard-id database-id]
[card-id :- ::lib.schema.id/card
dashboard-id :- [:maybe ::lib.schema.id/dashboard]
database-id :- [:maybe ::lib.schema.id/database]]
(let [qs (for [[i model model-id] [[1 "question" card-id]
[2 "dashboard" dashboard-id]
[3 "database" database-id]
@@ -26,25 +44,27 @@
:order-by :ordering
:limit [:inline 1]})))
(defn cards-by-id
(mu/defn cards-by-id
"A map of ID to Card for `card-ids`."
[card-ids]
[card-ids :- [:set ::lib.schema.id/card]]
(t2/select-pk->fn identity :model/Card :id [:in card-ids]))
(defn router-database-ids
(mu/defn router-database-ids
"The subset of `database-ids` that are database routers."
[database-ids]
[database-ids :- [:set ::lib.schema.id/database]]
(t2/select-fn-set :database_id :model/DatabaseRouter :database_id [:in database-ids]))
(defn duration-cache-configs
(mu/defn duration-cache-configs
"The duration CacheConfigs that refresh automatically."
[]
(t2/select :model/CacheConfig :strategy :duration :refresh_automatically true))
(defn duration-queries-to-rerun
(mu/defn duration-queries-to-rerun
"The query definitions to rerun for the duration cache `scopes`, each `{:model :model-id :rerun-cutoff}`, counting
only executions started after `started-after`; `parameterized?` selects parameterized or plain queries."
[scopes started-after parameterized?]
[scopes :- [:sequential DurationScope]
started-after :- ms/TemporalInstant
parameterized? :- :boolean]
(t2/select
:model/Query
{:select [:u.query :u.cache-hash :u.card-id :u.dashboard-id :u.count]
@@ -82,10 +102,11 @@
:group-by [:q.query_hash :q.query :qc.query_hash :qe.card_id :qe.dashboard_id]}})}
:u]]}))
(defn scheduled-base-query-to-rerun
(mu/defn scheduled-base-query-to-rerun
"The unparameterized query definition of the Card with `card-id` executed most recently after `started-after`, or
nil."
[card-id started-after]
[card-id :- ::lib.schema.id/card
started-after :- ms/TemporalInstant]
(t2/select-one :model/Query
{:select [:q.query [:qe.card_id :card-id]]
:from [[(t2/table-name :model/Query) :q]]
@@ -99,9 +120,11 @@
:order-by [[:qe.started_at :desc]]
:limit 1}))
(defn scheduled-parameterized-queries-to-rerun
(mu/defn scheduled-parameterized-queries-to-rerun
"The `limit` most common parameterized query definitions of the Card with `card-id` executed after `rerun-cutoff`."
[card-id rerun-cutoff limit]
[card-id :- ::lib.schema.id/card
rerun-cutoff :- ms/TemporalInstant
limit :- ms/PositiveInt]
(t2/select :model/Query
{:select [:q.query [:qe.card_id :card-id]]
:from [[(t2/table-name :model/Query) :q]]
@@ -119,26 +142,28 @@
[[:min :qe.started_at] :asc]]
:limit limit}))
(defn delete-query-caches!
(mu/defn delete-query-caches!
"Delete the QueryCache entries with `query-hashes`."
[query-hashes]
[query-hashes :- [:sequential bytes?]]
(t2/delete! :model/QueryCache :query_hash [:in query-hashes]))
(defn dashboard
(mu/defn dashboard
"The Dashboard with `dashboard-id`, or nil."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-one :model/Dashboard :id dashboard-id))
(defn cache-configs-ready-to-run
(mu/defn cache-configs-ready-to-run
"The CacheConfigs of `strategy` whose next run is unset or due at `now`."
[strategy now]
[strategy :- :keyword
now :- ms/TemporalInstant]
(t2/select :model/CacheConfig
:strategy strategy
{:where [:or
[:= :next_run_at nil]
[:<= :next_run_at now]]}))
(defn update-cache-config!
(mu/defn update-cache-config!
"Apply `changes` to the CacheConfig with `cache-config-id`."
[cache-config-id changes]
[cache-config-id :- ms/PositiveInt
changes :- CacheConfigChanges]
(t2/update! :model/CacheConfig {:id cache-config-id} changes))

View File

@@ -2,14 +2,17 @@
"Application database queries for the content-translation module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[malli.util :as mut]
[metabase.content-translation.schema :as content-translation.schema]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn delete-all-translations!
"Delete every ContentTranslation."
(mu/defn delete-all-translations!
"Delete every ContentTranslation, returning the number deleted."
[]
(t2/delete! :model/ContentTranslation))
(defn insert-translations!
"Insert the ContentTranslation `rows`."
[rows]
(mu/defn insert-translations!
"Insert the ContentTranslation `rows`, returning the number inserted."
[rows :- [:sequential (mut/select-keys ::content-translation.schema/content-translation.update [:locale :msgid :msgstr])]]
(t2/insert! :model/ContentTranslation rows))

View File

@@ -2,14 +2,16 @@
"Application database queries for the content-verification module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn card-exists?
(mu/defn card-exists?
"Whether a Card with `id` exists."
[id]
[id :- ::lib.schema.id/card]
(t2/exists? :model/Card id))
(defn dashboard-exists?
(mu/defn dashboard-exists?
"Whether a Dashboard with `id` exists."
[id]
[id :- ::lib.schema.id/dashboard]
(t2/exists? :model/Dashboard id))

View File

@@ -2,6 +2,9 @@
"Application database queries for the custom-viz-plugin module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[metabase-enterprise.custom-viz-plugin.schema :as custom-viz-plugin.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(def ^:private non-blob-columns
@@ -10,52 +13,53 @@
:manifest :metabase_version :bundle_hash :dev_bundle_url
:created_at :updated_at])
(defn non-blob-plugin
(mu/defn non-blob-plugin
"The CustomVizPlugin with `plugin-id` without its bundle, or nil."
[plugin-id]
[plugin-id :- ms/PositiveInt]
(t2/select-one non-blob-columns :id plugin-id))
(defn non-blob-plugins
(mu/defn non-blob-plugins
"Every CustomVizPlugin without its bundle, ordered by display name."
[]
(t2/select non-blob-columns {:order-by [[:display_name :asc]]}))
(defn active-enabled-non-blob-plugins
(mu/defn active-enabled-non-blob-plugins
"The active, enabled CustomVizPlugins without their bundle, ordered by display name."
[]
(t2/select non-blob-columns :status :active :enabled true {:order-by [[:display_name :asc]]}))
(defn plugin-by-identifier
(mu/defn plugin-by-identifier
"The CustomVizPlugin with `identifier`, or nil."
[identifier]
[identifier :- :string]
(t2/select-one :model/CustomVizPlugin :identifier identifier))
(defn plugin-identifier-exists?
(mu/defn plugin-identifier-exists?
"Whether a CustomVizPlugin with `identifier` exists."
[identifier]
[identifier :- :string]
(t2/exists? :model/CustomVizPlugin :identifier identifier))
(defn plugin-bundle
(mu/defn plugin-bundle
"The bundle bytes of the CustomVizPlugin with `plugin-id`."
[plugin-id]
[plugin-id :- ms/PositiveInt]
(t2/select-one-fn :bundle :model/CustomVizPlugin :id plugin-id))
(defn plugin-dev-bundle-url
(mu/defn plugin-dev-bundle-url
"The dev bundle URL of the CustomVizPlugin with `plugin-id`."
[plugin-id]
[plugin-id :- ms/PositiveInt]
(t2/select-one-fn :dev_bundle_url :model/CustomVizPlugin :id plugin-id))
(defn insert-plugin!
(mu/defn insert-plugin!
"Insert `plugin` and return the new instance."
[plugin]
[plugin :- ::custom-viz-plugin.schema/custom-viz-plugin.update]
(t2/insert-returning-instance! :model/CustomVizPlugin plugin))
(defn update-plugin!
"Apply `changes` to the CustomVizPlugin with `plugin-id`."
[plugin-id changes]
(mu/defn update-plugin!
"Apply `changes` to the CustomVizPlugin with `plugin-id`, returning the number updated."
[plugin-id :- ms/PositiveInt
changes :- ::custom-viz-plugin.schema/custom-viz-plugin.update]
(t2/update! :model/CustomVizPlugin plugin-id changes))
(defn delete-plugin!
"Delete the CustomVizPlugin with `plugin-id`."
[plugin-id]
(mu/defn delete-plugin!
"Delete the CustomVizPlugin with `plugin-id`, returning the number deleted."
[plugin-id :- ms/PositiveInt]
(t2/delete! :model/CustomVizPlugin :id plugin-id))

View File

@@ -0,0 +1,44 @@
(ns metabase-enterprise.custom-viz-plugin.schema
"Malli schemas for the custom-viz-plugin module."
(:require
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::custom-viz-plugin.manifest
"The `:manifest` column of a CustomVizPlugin, decoded."
:map)
(mr/def ::custom-viz-plugin
"A CustomVizPlugin as selected from the app DB: every column of `:custom_viz_plugin`."
[:map {:closed true}
[:id ms/PositiveInt]
[:identifier :string]
[:display_name :string]
[:status [:or :keyword :string]]
[:error_message [:maybe :string]]
[:bundle [:maybe [:or bytes? :string]]]
[:bundle_hash [:maybe :string]]
[:created_at ms/TemporalInstant]
[:updated_at ms/TemporalInstant]
[:enabled :boolean]
[:icon [:maybe :string]]
[:manifest [:maybe ::custom-viz-plugin.manifest]]
[:metabase_version [:maybe :string]]
[:dev_bundle_url [:maybe :string]]])
(mr/def ::custom-viz-plugin.update
"What an update (or insert) of a CustomVizPlugin accepts: every column of `:custom_viz_plugin` except `id`, all optional."
[:map {:closed true}
[:identifier {:optional true} [:maybe :string]]
[:display_name {:optional true} [:maybe :string]]
[:status {:optional true} [:maybe [:or :keyword :string]]]
[:error_message {:optional true} [:maybe :string]]
[:bundle {:optional true} [:maybe [:or bytes? :string]]]
[:bundle_hash {:optional true} [:maybe :string]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
[:enabled {:optional true} [:maybe :boolean]]
[:icon {:optional true} [:maybe :string]]
[:manifest {:optional true} [:maybe ::custom-viz-plugin.manifest]]
[:metabase_version {:optional true} [:maybe :string]]
[:dev_bundle_url {:optional true} [:maybe :string]]])

View File

@@ -2,6 +2,9 @@
"Application database queries for the data-apps module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[metabase-enterprise.data-apps.schema :as data-apps.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(def ^:private non-blob-columns
@@ -10,71 +13,73 @@
:bundle_hash :last_synced_sha :last_synced_at :sync_error
:created_at :updated_at])
(defn non-blob-data-app
(mu/defn non-blob-data-app
"The DataApp with `data-app-id` without its bundle, or nil."
[data-app-id]
[data-app-id :- ms/PositiveInt]
(t2/select-one non-blob-columns :id data-app-id))
(defn non-blob-data-app-by-slug
(mu/defn non-blob-data-app-by-slug
"The DataApp named `slug` without its bundle, or nil."
[slug]
[slug :- :string]
(t2/select-one non-blob-columns :name slug))
(defn enabled-non-blob-data-app-by-slug
(mu/defn enabled-non-blob-data-app-by-slug
"The enabled DataApp named `slug` without its bundle, or nil."
[slug]
[slug :- :string]
(t2/select-one non-blob-columns :name slug :enabled true))
(defn non-blob-data-apps
(mu/defn non-blob-data-apps
"Every DataApp without its bundle, ordered by display name; only the enabled, error-free ones when `available?`."
[available?]
[available? :- [:maybe :boolean]]
(t2/select non-blob-columns
(cond-> {:order-by [[:display_name :asc]]}
available? (assoc :where [:and
[:= :enabled true]
[:= :sync_error nil]]))))
(defn data-app-bundle
(mu/defn data-app-bundle
"The bundle bytes of the DataApp with `data-app-id`."
[data-app-id]
[data-app-id :- ms/PositiveInt]
(t2/select-one-fn :bundle :model/DataApp :id data-app-id))
(defn data-apps-sync-info
(mu/defn data-apps-sync-info
"The sync-relevant columns of every DataApp."
[]
(t2/select [:model/DataApp :name :display_name :description :allowed_hosts :bundle_path :bundle_hash :sync_error]))
(defn data-app-exists?
(mu/defn data-app-exists?
"Whether a DataApp named `slug` exists."
[slug]
[slug :- :string]
(t2/exists? :model/DataApp :name slug))
(defn insert-data-app!
(mu/defn insert-data-app!
"Insert the DataApp `row`."
[row]
[row :- ::data-apps.schema/data-app.update]
(t2/insert! :model/DataApp row))
(defn update-data-app!
(mu/defn update-data-app!
"Apply `changes` to the DataApp with `data-app-id`."
[data-app-id changes]
[data-app-id :- ms/PositiveInt
changes :- ::data-apps.schema/data-app.update]
(t2/update! :model/DataApp :id data-app-id changes))
(defn update-data-app-by-slug!
(mu/defn update-data-app-by-slug!
"Apply `changes` to the DataApp named `slug`."
[slug changes]
[slug :- :string
changes :- ::data-apps.schema/data-app.update]
(t2/update! :model/DataApp :name slug changes))
(defn delete-data-app-by-slug!
(mu/defn delete-data-app-by-slug!
"Delete the DataApp named `slug`, returning the number deleted."
[slug]
[slug :- :string]
(t2/delete! :model/DataApp :name slug))
(defn delete-data-apps-not-named!
(mu/defn delete-data-apps-not-named!
"Delete the DataApps whose name is not one of `slugs`, returning the number deleted."
[slugs]
[slugs :- [:set :string]]
(t2/delete! :model/DataApp :name [:not-in slugs]))
(defn delete-all-data-apps!
(mu/defn delete-all-data-apps!
"Delete every DataApp, returning the number deleted."
[]
(t2/delete! :model/DataApp))

View File

@@ -31,6 +31,24 @@
(derive :metabase/model)
(derive :hook/timestamped?))
(defn- default-last-synced-at
"A sync that records `last_synced_sha` happened now unless it says otherwise."
[row]
(cond-> row
(and (contains? row :last_synced_sha) (not (contains? row :last_synced_at)))
(assoc :last_synced_at (mi/now))))
(t2/define-before-insert :model/DataApp
[data-app]
(default-last-synced-at data-app))
(t2/define-before-update :model/DataApp
[data-app]
(let [changes (t2/changes data-app)]
(cond-> data-app
(and (contains? changes :last_synced_sha) (not (contains? changes :last_synced_at)))
(assoc :last_synced_at (mi/now)))))
;; Reads always see `allowed_hosts` as a vector, never nil — a row synced before
;; the column existed has NULL until it's re-synced. Guard on `contains?` so
;; selects that don't fetch the column (e.g. `select-one-fn :bundle`) are left

View File

@@ -0,0 +1,40 @@
(ns metabase-enterprise.data-apps.schema
"Malli schemas for the data-apps module."
(:require
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::data-app
"A DataApp as selected from the app DB: every column of `:data_app`."
[:map {:closed true}
[:id ms/PositiveInt]
[:name :string]
[:display_name :string]
[:allowed_hosts [:maybe [:sequential :string]]]
[:bundle_path :string]
[:bundle [:maybe [:or bytes? :string]]]
[:bundle_hash [:maybe :string]]
[:last_synced_sha [:maybe :string]]
[:last_synced_at [:maybe ms/TemporalInstant]]
[:sync_error [:maybe :string]]
[:enabled :boolean]
[:created_at ms/TemporalInstant]
[:updated_at ms/TemporalInstant]
[:description [:maybe :string]]])
(mr/def ::data-app.update
"What an update (or insert) of a DataApp accepts: every column of `:data_app` except `id`, all optional."
[:map {:closed true}
[:name {:optional true} [:maybe :string]]
[:display_name {:optional true} [:maybe :string]]
[:allowed_hosts {:optional true} [:maybe [:sequential :string]]]
[:bundle_path {:optional true} [:maybe :string]]
[:bundle {:optional true} [:maybe [:or bytes? :string]]]
[:bundle_hash {:optional true} [:maybe :string]]
[:last_synced_sha {:optional true} [:maybe :string]]
[:last_synced_at {:optional true} [:maybe ms/TemporalInstant]]
[:sync_error {:optional true} [:maybe :string]]
[:enabled {:optional true} [:maybe :boolean]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
[:description {:optional true} [:maybe :string]]])

View File

@@ -150,7 +150,6 @@
(upsert-by-name! slug (assoc fields
:bundle bytes
:last_synced_sha sha
:last_synced_at :%now
:sync_error nil))
(app-content-changed? existing fields)))
(catch Throwable e

View File

@@ -2,13 +2,19 @@
"Application database queries for the data-complexity-score module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[java-time.api :as t]
[malli.util :as mut]
[metabase-enterprise.data-complexity-score.schema :as data-complexity-score.schema]
[metabase.app-db.core :as mdb]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn active-field-counts-by-table
(mu/defn active-field-counts-by-table
"Rows of `:table_id` and `:field_count` of active Fields for `table-ids`."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/query {:select [:table_id [:%count.* :field_count]]
:from [:metabase_field]
:where [:and
@@ -16,17 +22,17 @@
[:in :table_id table-ids]]
:group-by [:table_id]}))
(defn unarchived-measure-names
(mu/defn unarchived-measure-names
"The Table ID and name of the unarchived Measures on the Tables with `table-ids`."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/select [:model/Measure :table_id :name] :archived false :table_id [:in table-ids]))
(defn collection
(mu/defn collection
"The Collection with `collection-id`, or nil."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/select-one :model/Collection :id collection-id))
(defn verified-card-ids
(mu/defn verified-card-ids
"The IDs of the Cards whose most recent moderation review is verified."
[]
(t2/select-fn-set :moderated_item_id :model/ModerationReview
@@ -34,63 +40,66 @@
:most_recent true
:status "verified"))
(defn official-collection-ids
(mu/defn official-collection-ids
"The IDs of the official Collections."
[]
(t2/select-fn-set :id :model/Collection :authority_level "official"))
(defn routed-child-database-ids
(mu/defn routed-child-database-ids
"The IDs of the Databases that are routing destinations."
[]
(t2/select-fn-set :id :model/Database :router_database_id [:not= nil]))
(defn universe-cards
(mu/defn universe-cards
"The ID, name, type, and Collection of the unarchived metric and model Cards outside the Database with
`audit-database-id`."
[audit-database-id]
[audit-database-id :- ::lib.schema.id/database]
(t2/select [:model/Card :id :name :type :collection_id :card_schema]
:type [:in ["metric" "model"]]
:archived false
:database_id [:not= audit-database-id]))
(defn universe-tables
(mu/defn universe-tables
"The scoring columns of the active Tables outside the Database with `audit-database-id`."
[audit-database-id]
[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]))
(defn metabot-by-entity-id
(mu/defn metabot-by-entity-id
"The Metabot with `entity-id`, or nil."
[entity-id]
[entity-id :- :string]
(t2/select-one :model/Metabot :entity_id entity-id))
(defn latest-score-entry
(mu/defn latest-score-entry
"The most recent DataComplexityScore of `source` for `fingerprint`, or nil."
[fingerprint source]
[fingerprint :- :string
source :- :string]
(t2/select-one :model/DataComplexityScore :fingerprint fingerprint :source source {:order-by [[:id :desc]]}))
(defn score-entry
(mu/defn score-entry
"The DataComplexityScore with `id`, or nil."
[id]
[id :- ms/PositiveInt]
(t2/select-one :model/DataComplexityScore :id id))
(defn scored-within-hours?
(mu/defn scored-within-hours?
"Whether a DataComplexityScore of `source` for `fingerprint` was created within the last `hours` hours of
database time."
[fingerprint source hours]
[fingerprint :- :string
source :- :string
hours :- number?]
(t2/exists? :model/DataComplexityScore
{:where [:and
[:= :fingerprint fingerprint]
[:= :source source]
[:>= :created_at (h2x/add-interval-honeysql-form (mdb/db-type) :%now (- hours) :hour)]]}))
(defn insert-score!
(mu/defn insert-score!
"Insert `score-entry` and return its ID."
[score-entry]
[score-entry :- (mut/select-keys ::data-complexity-score.schema/data-complexity-score.update [:fingerprint :source :score_data])]
(t2/insert-returning-pk! :model/DataComplexityScore score-entry))
(defn delete-scores-created-before!
"Delete the DataComplexityScores created before `cutoff`, returning the number deleted."
[cutoff]
(t2/delete! :model/DataComplexityScore {:where [:< :created_at cutoff]}))
(mu/defn delete-scores-older-than!
"Delete the DataComplexityScores created more than `months` months ago, returning the number deleted."
[months :- ms/PositiveInt]
(t2/delete! :model/DataComplexityScore {:where [:< :created_at (t/minus (t/offset-date-time) (t/months months))]}))

View File

@@ -0,0 +1,26 @@
(ns metabase-enterprise.data-complexity-score.schema
"Malli schemas for the data-complexity-score module."
(:require
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::data-complexity-score.score-data
"The `:score_data` column of a DataComplexityScore, decoded."
:map)
(mr/def ::data-complexity-score
"A DataComplexityScore as selected from the app DB: every column of `:data_complexity_score`."
[:map {:closed true}
[:id ms/PositiveInt]
[:fingerprint :string]
[:score_data ::data-complexity-score.score-data]
[:created_at ms/TemporalInstant]
[:source [:or :keyword :string]]])
(mr/def ::data-complexity-score.update
"What an update (or insert) of a DataComplexityScore accepts: every column of `:data_complexity_score` except `id`, all optional."
[:map {:closed true}
[:fingerprint {:optional true} [:maybe :string]]
[:score_data {:optional true} [:maybe ::data-complexity-score.score-data]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:source {:optional true} [:maybe [:or :keyword :string]]]])

View File

@@ -4,12 +4,10 @@
[clojurewerkz.quartzite.jobs :as jobs]
[clojurewerkz.quartzite.schedule.cron :as cron]
[clojurewerkz.quartzite.triggers :as triggers]
[java-time.api :as t]
[metabase-enterprise.data-complexity-score.db :as data-complexity-score.db]
[metabase.task.core :as task]
[metabase.util.log :as log])
(:import
(java.sql Timestamp)
(org.quartz DisallowConcurrentExecution)))
(set! *warn-on-reflection* true)
@@ -22,16 +20,10 @@
(def ^:private retention-months 3)
(defn- retention-cutoff-timestamp
[months]
(Timestamp/valueOf ^java.time.LocalDateTime
(t/minus (t/local-date-time) (t/months months))))
(defn- trim-old-complexity-score-data!
[]
(log/info "Trimming old Data Complexity Score snapshots.")
(let [cutoff (retention-cutoff-timestamp retention-months)
deleted (data-complexity-score.db/delete-scores-created-before! cutoff)]
(let [deleted (data-complexity-score.db/delete-scores-older-than! retention-months)]
(log/infof "Data Complexity Score cleanup complete. Deleted %d rows." (or deleted 0))))
(task/defjob ^{DisallowConcurrentExecution true

View File

@@ -4,8 +4,17 @@
(:require
[clojure.string :as str]
[metabase.collections.models.collection :as collection]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(def ^:private TableSelectors
[:map {:closed true}
[:database-ids {:optional true} [:maybe [:or [:set ::lib.schema.id/database] [:sequential ::lib.schema.id/database]]]]
[:table-ids {:optional true} [:maybe [:or [:set ::lib.schema.id/table] [:sequential ::lib.schema.id/table]]]]
[:schema-ids {:optional true} [:maybe [:or [:set :string] [:sequential :string]]]]])
(defn- table-selectors-expr
"Matches the Tables selected by `database-ids`, `table-ids`, and `schema-ids` (each `\"<db-id>:<schema>\"`)."
[{:keys [database-ids table-ids schema-ids]}]
@@ -21,11 +30,13 @@
[selectors]
^:allow-subquery {:select [:id] :from [(t2/table-name :model/Table)] :where (table-selectors-expr selectors)})
(defn remapped-table-ids-reducible
(mu/defn remapped-table-ids-reducible
"Reducible `:table_id` rows of the Tables reachable from `tables` through FK remapping Dimensions, from the
`input-field` side to the `output-field` side (`:source_field` or `:target_field`), excluding `tables` themselves.
`tables` is a set of Table IDs or a selectors map (see [[table-ids-matching-selectors]])."
[input-field output-field tables]
[input-field :- :keyword
output-field :- :keyword
tables :- [:or [:set ms/PositiveInt] TableSelectors]]
(let [input-table-id (keyword (name input-field) "table_id")
output-table-id (keyword (name output-field) "table_id")
table-ids (if (map? tables)
@@ -48,10 +59,12 @@
[:in input-table-id table-ids]
not-in-tables]})))
(defn table-ids-matching-selectors
(mu/defn table-ids-matching-selectors
"The IDs of the Tables selected by `selectors` (`{:database-ids :table-ids :schema-ids}`) plus, when given, the
`extra-table-ids` that are unpublished (`:unpublished` mode) or any of them (`:any` mode)."
[selectors extra-table-ids extra-mode]
[selectors :- TableSelectors
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)]
(if (seq extra-table-ids)
@@ -60,48 +73,51 @@
:any [:in :id extra-table-ids])]
selector-expr))}))
(defn published-table-ids
(mu/defn published-table-ids
"The IDs of the published Tables among `table-ids`."
[table-ids]
[table-ids :- [:set ::lib.schema.id/table]]
(t2/select-pks-set :model/Table :id [:in table-ids] :is_published true))
(defn tables
(mu/defn tables
"The Tables with `table-ids`."
[table-ids]
[table-ids :- [:set ::lib.schema.id/table]]
(t2/select :model/Table :id [:in table-ids]))
(defn collection
(mu/defn collection
"The Collection with `collection-id`, or nil."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/select-one :model/Collection collection-id))
(defn latest-table-publishing-event
(mu/defn latest-table-publishing-event
"The most recent publish or unpublish AuditLog event for `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one [:model/AuditLog :timestamp :topic :user_id]
:topic [:in [:table-publish :table-unpublish]]
:model "Table"
:model_id table-id
{:order-by [[:timestamp :desc] [:id :desc]]}))
(defn user-name-and-email
(mu/defn user-name-and-email
"The id, first name, last name, and email of the User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one [:model/User :id :first_name :last_name :email] user-id))
(defn publish-tables!
"Publish the Tables with `table-ids` into the Collection with `collection-id`."
[table-ids collection-id]
(mu/defn publish-tables!
"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}))
(defn unpublish-tables!
"Unpublish the Tables with `table-ids` and detach them from their Collection."
[table-ids]
(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}))
(defn published-table-visible-to-user?
(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."
[table-id user-id superuser?]
[table-id :- ::lib.schema.id/table
user-id :- ::lib.schema.id/user
superuser? :- :boolean]
(t2/exists? :model/Table
{:where [:and
[:= :id table-id]
@@ -110,7 +126,7 @@
:collection_id {} {:current-user-id user-id
:is-superuser? superuser?})]}))
(defn any-published-table-visible?
(mu/defn any-published-table-visible?
"Whether the current user can read the Collection of any published Table."
[]
(t2/exists? :model/Table
@@ -118,9 +134,9 @@
[:= :is_published true]
(collection/visible-collection-filter-clause :collection_id)]}))
(defn published-table-visible-in-database?
(mu/defn published-table-visible-in-database?
"Whether the current user can read the Collection of any published Table in the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Table
{:where [:and
[:= :db_id database-id]

View File

@@ -2,9 +2,11 @@
"Application database queries for the database-replication module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database :id database-id))

View File

@@ -2,74 +2,82 @@
"Application database queries for the database-routing module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.warehouses.schema :as warehouses.schema]
[toucan2.core :as t2]))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database :id database-id))
(defn database-name
(mu/defn database-name
"The name of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one-fn :name :model/Database database-id))
(defn destination-database?
(mu/defn destination-database?
"Whether the Database with `database-id` is a routing destination."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Database :id database-id :router_database_id [:not= nil]))
(defn destination-database-id
(mu/defn destination-database-id
"The ID of the destination Database of the router with `router-database-id` named `database-name`, or nil."
[router-database-id database-name]
[router-database-id :- ::lib.schema.id/database
database-name :- :string]
(t2/select-one-pk :model/Database :router_database_id router-database-id :name database-name))
(defn destination-name-exists?
(mu/defn destination-name-exists?
"Whether the router with `router-database-id` already has a destination Database named one of `names`."
[router-database-id names]
[router-database-id :- ::lib.schema.id/database
names :- [:sequential :string]]
(t2/exists? :model/Database :router_database_id router-database-id :name [:in names]))
(defn insert-databases!
(mu/defn insert-databases!
"Insert the Database `rows` and return the new instances."
[rows]
[rows :- [:sequential
::warehouses.schema/database.update]]
(t2/insert-returning-instances! :model/Database rows))
(defn transform-exists-for-source-database?
(mu/defn transform-exists-for-source-database?
"Whether a Transform reads from the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Transform :source_database_id database-id))
(defn router-exists?
(mu/defn router-exists?
"Whether the Database with `database-id` is a router."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/DatabaseRouter :database_id database-id))
(defn router-for-database
(mu/defn router-for-database
"The DatabaseRouter of the Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/DatabaseRouter :database_id database-id))
(defn router-user-attribute
(mu/defn router-user-attribute
"The user attribute the router of the Database with `database-id` routes on."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one-fn :user_attribute :model/DatabaseRouter :database_id database-id))
(defn router-user-attributes-by-database
(mu/defn router-user-attributes-by-database
"A map of Database ID to routing user attribute for `database-ids`."
[database-ids]
[database-ids :- [:sequential ::lib.schema.id/database]]
(t2/select-fn->fn :database_id :user_attribute :model/DatabaseRouter :database_id [:in database-ids]))
(defn insert-router!
(mu/defn insert-router!
"Insert a DatabaseRouter for the Database with `database-id` routing on `user-attribute`."
[database-id user-attribute]
[database-id :- ::lib.schema.id/database
user-attribute :- :string]
(t2/insert! :model/DatabaseRouter {:database_id database-id :user_attribute user-attribute}))
(defn update-router-user-attribute!
(mu/defn update-router-user-attribute!
"Set the routing user attribute of the router of the Database with `database-id`."
[database-id user-attribute]
[database-id :- ::lib.schema.id/database
user-attribute :- :string]
(t2/update! :model/DatabaseRouter :database_id database-id {:user_attribute user-attribute}))
(defn delete-router!
(mu/defn delete-router!
"Delete the DatabaseRouter of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/delete! :model/DatabaseRouter :database_id database-id))

View File

@@ -0,0 +1,19 @@
(ns metabase-enterprise.database-routing.schema
"Malli schemas for the database-routing module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::database-router
"A DatabaseRouter as selected from the app DB: every column of `:db_router`."
[:map {:closed true}
[:id ms/PositiveInt]
[:database_id ::lib.schema.id/database]
[:user_attribute :string]])
(mr/def ::database-router.update
"What an update (or insert) of a DatabaseRouter accepts: every column of `:db_router` except `id`, all optional."
[:map {:closed true}
[:database_id {:optional true} [:maybe ::lib.schema.id/database]]
[:user_attribute {:optional true} [:maybe :string]]])

View File

@@ -3,15 +3,49 @@
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
(:require
[clojure.set :as set]
[malli.util :as mut]
[metabase-enterprise.dependencies.dependency-types :as deps.dependency-types]
[metabase-enterprise.dependencies.schema :as dependencies.schema]
[metabase.app-db.core :as mdb]
[metabase.collections.models.collection :as collection]
[metabase.collections.models.collection.root :as collection.root]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.models.interface :as mi]
[metabase.permissions.core :as perms]
[metabase.queries.schema :as queries.schema]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(def ^:private VisibleOpts
"Opts consumed by [[visible-entities-expr]]: the current user (`:user-id`, `:is-superuser?`,
`:is-data-analyst?`), plus whether to include archived items (`:include-archived-items`,
default `:exclude`)."
[:map {:closed true}
[:user-id ::lib.schema.id/user]
[:is-superuser? {:optional true} [:maybe :boolean]]
[:is-data-analyst? {:optional true} [:maybe :boolean]]
[:include-archived-items {:optional true} [:enum :exclude :all :only]]])
(def ^:private EntityType
"A dependency-type keyword, or the equivalent string. Every column these entity types are filtered against uses
Toucan's keyword transform, which coerces either representation, so callers (and tests) sometimes pass a plain
string instead of the keyword."
[:or ::deps.dependency-types/dependency-types :string])
(def ^:private RestrictionSpec
"The edge-restriction filter spec consumed by [[edge-restriction-expr]]: nil for no restriction, or a map
combining `:visible` ([[VisibleOpts]]), `:broken?`, `:types` (a set of dependency types), and/or
`:entity-type` + `:ids` (restricting to specific entities of one type)."
[:maybe
[:map {:closed true}
[:visible {:optional true} VisibleOpts]
[:broken? {:optional true} :boolean]
[:types {:optional true} [:set EntityType]]
[:entity-type {:optional true} EntityType]
[:ids {:optional true} [:sequential ::deps.dependency-types/entity-id]]]])
;;; ------------------------------------------------ Graph edge restrictions ------------------------------------------------
;;; The dependency graph is traversed generically over every entity type the module knows about; these private
;;; helpers build the permission and type-allowlist fragments the graph and item-list queries below restrict edges
@@ -169,13 +203,22 @@
1 (first fragments)
(into [:and] fragments))))
(defn dependency-edges
(mu/defn dependency-edges
"The Dependencies from the entities of type `entity-type` with `entity-ids`, where `src-type`/`src-id` name the
Dependency columns identifying those entities and `dst-type`/`dst-id` name the columns identifying the related
entities on the other side of the edge, optionally restricted by `destination-restriction` (applied to
`dst-type`/`dst-id`) and `source-restriction` (applied to `src-type`/`src-id`) — see [[edge-restriction-expr]] for the
filter spec shape."
[{:keys [src-type src-id dst-type dst-id entity-type entity-ids destination-restriction source-restriction]}]
[{:keys [src-type src-id dst-type dst-id entity-type entity-ids destination-restriction source-restriction]}
:- [:map {:closed true}
[:src-type [:enum :from_entity_type :to_entity_type]]
[:src-id [:enum :from_entity_id :to_entity_id]]
[:dst-type [:enum :from_entity_type :to_entity_type]]
[:dst-id [:enum :from_entity_id :to_entity_id]]
[:entity-type EntityType]
[:entity-ids [:or [:set ::deps.dependency-types/entity-id] [:sequential ::deps.dependency-types/entity-id]]]
[:destination-restriction RestrictionSpec]
[:source-restriction RestrictionSpec]]]
(t2/select :model/Dependency
{:where (into [:and
[:= src-type (name entity-type)]
@@ -184,10 +227,12 @@
[(edge-restriction-expr dst-type dst-id destination-restriction)
(edge-restriction-expr src-type src-id source-restriction)])}))
(defn finding-errors-from-sources
(mu/defn finding-errors-from-sources
"The AnalysisFindingErrors caused by any of the entities `source-entity-type` `source-entity-ids`, whose analyzed
entity is visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`."
[source-entity-type source-entity-ids {:keys [user-id is-superuser? is-data-analyst?]}]
[source-entity-type :- EntityType
source-entity-ids :- [:sequential ::deps.dependency-types/entity-id]
{:keys [user-id is-superuser? is-data-analyst?]} :- VisibleOpts]
(t2/select :model/AnalysisFindingError
{:where [:and
[:= :source_entity_type (name source-entity-type)]
@@ -196,10 +241,12 @@
{:user-id user-id :is-superuser? is-superuser?
:is-data-analyst? is-data-analyst?})]}))
(defn finding-errors-for-entities-with-visible-sources
(mu/defn finding-errors-for-entities-with-visible-sources
"The AnalysisFindingErrors analyzing any of the entities `entity-type` `entity-ids`, excluding those whose source
entity exists but is not visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`."
[entity-type entity-ids {:keys [user-id is-superuser? is-data-analyst?]}]
[entity-type :- EntityType
entity-ids :- [:sequential ::deps.dependency-types/entity-id]
{:keys [user-id is-superuser? is-data-analyst?]} :- VisibleOpts]
(t2/select :model/AnalysisFindingError
{:where [:and
[:= :analyzed_entity_type (name entity-type)]
@@ -414,14 +461,30 @@
:left-join (build-left-joins join all-joins)
:where (into [:and join-filter item-visible-expr] (keep identity) exprs)}))
(defn dependency-item-ids
(def ^:private DependencyItemParams
"Params shared by [[dependency-item-ids]] and [[dependency-item-count]]."
[:map {:closed true}
[:query-type [:enum :unreferenced :breaking]]
[:entity-types [:sequential ::deps.dependency-types/dependency-types]]
[:card-types {:optional true} [:maybe [:sequential [:or :keyword :string]]]]
[:search-text {:optional true} [:maybe :string]]
[:include-personal-collections? :boolean]
[:sort-column {:optional true} [:maybe [:enum :name :location :dependents-errors :dependents-with-errors]]]
[:sort-direction [:enum :asc :desc]]
[:offset ms/IntGreaterThanOrEqualToZero]
[:limit ms/PositiveInt]
[:user-id ::lib.schema.id/user]
[:is-superuser? {:optional true} [:maybe :boolean]]
[:is-data-analyst? {:optional true} [:maybe :boolean]]])
(mu/defn dependency-item-ids
"A page of `[entity-type entity-id]` pairs for `query-type` (`:unreferenced` or `:breaking`), restricted to
`entity-types`, `card-types` (applied only to `:card` entities), `search-text` (nil for none), and
`include-personal-collections?`; sorted by `sort-column` (`:name`, `:location`, `:dependents-errors`, or
`:dependents-with-errors`) in `sort-direction`, skipping `offset` and returning up to `limit`. Entities are
restricted to those visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`; throws
when `user-id` is missing."
[{:keys [entity-types sort-direction offset limit] :as params}]
[{:keys [entity-types sort-direction offset limit] :as params} :- DependencyItemParams]
(let [union-query ^:allow-subquery {:union-all (mapv #(dependency-item-select (assoc params :entity-type %))
entity-types)}]
(->> (t2/query (assoc union-query
@@ -430,22 +493,30 @@
:limit limit))
(map (fn [{:keys [entity_id entity_type]}] [(keyword entity_type) entity_id])))))
(defn dependency-item-count
(mu/defn dependency-item-count
"The total count of items matching the same criteria as [[dependency-item-ids]] (ignoring sort direction, offset,
and limit); throws when `user-id` is missing."
[{:keys [entity-types] :as params}]
[{:keys [entity-types] :as params} :- DependencyItemParams]
(let [union-query ^:allow-subquery {:union-all (mapv #(dependency-item-select (assoc params :entity-type %))
entity-types)}]
(-> (t2/query {:select [[:%count.* :total]] :from [[union-query :subquery]]})
first
:total)))
(defn broken-entity-pairs
(mu/defn broken-entity-pairs
"The `[:analyzed_entity_type :analyzed_entity_id]` pairs whose analysis failed and were caused by the entity
`source-entity-type` `source-entity-id`, restricted to `dependent-types` and `dependent-card-types` (each nil
for no restriction), visible to the user described by `user-id`, `is-superuser?`, and `is-data-analyst?`."
[{:keys [source-entity-type source-entity-id dependent-types dependent-card-types
user-id is-superuser? is-data-analyst?]}]
user-id is-superuser? is-data-analyst?]}
:- [:map {:closed true}
[:source-entity-type EntityType]
[:source-entity-id ms/PositiveInt]
[:dependent-types {:optional true} [:maybe [:sequential :string]]]
[:dependent-card-types {:optional true} [:maybe [:sequential :string]]]
[:user-id ::lib.schema.id/user]
[:is-superuser? {:optional true} [:maybe :boolean]]
[:is-data-analyst? {:optional true} [:maybe :boolean]]]]
(t2/query
(cond-> {:select-distinct [[:afe.analyzed_entity_type :entity_type] [:afe.analyzed_entity_id :entity_id]]
:from [[:analysis_finding_error :afe]]
@@ -472,124 +543,153 @@
;;; ---------------------------------------------------- Entities ----------------------------------------------------
(defn instances
(mu/defn instances
"The instances of the entity type `entity-type` with `ids`."
[entity-type ids]
[entity-type :- ::deps.dependency-types/dependency-types
ids :- [:set ::deps.dependency-types/entity-id]]
(t2/select (deps.dependency-types/dependency-type->model entity-type) :id [:in ids]))
(defn instances-with-columns
(mu/defn instances-with-columns
"The `columns` of the instances of the entity type `entity-type` with `ids`."
[entity-type columns 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]))
(defn instance-with-columns
(mu/defn instance-with-columns
"The `columns` of the instance of the entity type `entity-type` with `id`, or nil."
[entity-type columns id]
[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))
(defn card
(mu/defn card
"The Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one :model/Card :id card-id))
(defn card-types-by-id
(mu/defn card-types-by-id
"A map of Card ID to type for `card-ids`."
[card-ids]
[card-ids :- [:sequential ::lib.schema.id/card]]
(t2/select-fn->fn :id :type [:model/Card :id :type :card_schema] :id [:in card-ids]))
(defn card-database-ids
(mu/defn card-database-ids
"The `:id`, `:database_id`, and `:card_schema` of the Cards with `card-ids`."
[card-ids]
[card-ids :- [:sequential ::lib.schema.id/card]]
(t2/select [:model/Card :id :database_id :card_schema] :id [:in card-ids]))
(defn set-card-result-metadata!
"Set the result metadata of the Card with `card-id`."
[card-id result-metadata]
(mu/defn set-card-result-metadata!
"Set the result metadata of the Card with `card-id`, returning the number updated."
[card-id :- ::lib.schema.id/card
result-metadata :- [:maybe ::queries.schema/card.result-metadata]]
(t2/update! :model/Card card-id {:result_metadata result-metadata}))
(defn tables
(mu/defn tables
"The Tables with `table-ids`."
[table-ids]
[table-ids :- [:set ::lib.schema.id/table]]
(t2/select :model/Table :id [:in table-ids]))
(defn table-database-ids
(mu/defn table-database-ids
"The `:id` and `:db_id` of the Tables with `table-ids`."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/select [:model/Table :id :db_id] :id [:in table-ids]))
(defn table-id-by-name
(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 schema table-name]
[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))
(defn transform-sources
(mu/defn transform-sources
"The `:id` and `:source` of the Transforms with `transform-ids`."
[transform-ids]
[transform-ids :- [:sequential ::lib.schema.id/transform]]
(t2/select [:model/Transform :id :source] :id [:in transform-ids]))
(defn transform-ids-of-source-database
(mu/defn transform-ids-of-source-database
"The IDs of the Transforms reading from the Database with `db-id`."
[db-id]
[db-id :- ::lib.schema.id/database]
(t2/select-pks-set :model/Transform :source_database_id db-id))
;;; -------------------------------------------------- Dependencies --------------------------------------------------
(defn dependencies-from
(mu/defn dependencies-from
"The `:id`, `:to_entity_type`, and `:to_entity_id` of the Dependencies of the entity `entity-type` `entity-id`."
[entity-type entity-id]
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(t2/select [:model/Dependency :id :to_entity_type :to_entity_id]
:from_entity_type entity-type
:from_entity_id entity-id))
(defn dependency-exists?
(mu/defn dependency-exists?
"Whether the entity `from-type` `from-id` depends on the entity `to-type` `to-id`."
[from-type from-id to-type to-id]
[from-type :- EntityType
from-id :- ms/PositiveInt
to-type :- EntityType
to-id :- ms/PositiveInt]
(t2/exists? :model/Dependency
:from_entity_type from-type :from_entity_id from-id
:to_entity_type to-type :to_entity_id to-id))
(defn insert-dependencies!
"Insert the Dependency `rows`."
[rows]
(mu/defn insert-dependencies!
"Insert the Dependency `rows`, returning the number inserted."
[rows :- [:sequential [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:from_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:from_entity_id {:optional true} [:maybe ms/PositiveInt]]
[:to_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:to_entity_id {:optional true} [:maybe ms/PositiveInt]]]]]
(t2/insert! :model/Dependency rows))
(defn retarget-dependency!
(mu/defn retarget-dependency!
"Point the Dependency of the entity `from-type` `from-id` on the entity `old-to-type` `old-to-id` at the entity
`new-to-type` `new-to-id`."
[from-type from-id old-to-type old-to-id new-to-type new-to-id]
`new-to-type` `new-to-id`, returning the number updated."
[from-type :- EntityType
from-id :- ms/PositiveInt
old-to-type :- EntityType
old-to-id :- ms/PositiveInt
new-to-type :- EntityType
new-to-id :- ms/PositiveInt]
(t2/update! :model/Dependency
{:from_entity_type from-type :from_entity_id from-id
:to_entity_type old-to-type :to_entity_id old-to-id}
{:to_entity_type new-to-type :to_entity_id new-to-id}))
(defn delete-dependencies!
"Delete the Dependencies with `dependency-ids`."
[dependency-ids]
(mu/defn delete-dependencies!
"Delete the Dependencies with `dependency-ids`, returning the number deleted."
[dependency-ids :- [:sequential ms/PositiveInt]]
(t2/delete! :model/Dependency :id [:in dependency-ids]))
(defn delete-dependency!
"Delete the Dependency of the entity `from-type` `from-id` on the entity `to-type` `to-id`."
[from-type from-id to-type to-id]
(mu/defn delete-dependency!
"Delete the Dependency of the entity `from-type` `from-id` on the entity `to-type` `to-id`, returning the number
deleted."
[from-type :- EntityType
from-id :- ms/PositiveInt
to-type :- EntityType
to-id :- ms/PositiveInt]
(t2/delete! :model/Dependency
:from_entity_type from-type :from_entity_id from-id
:to_entity_type to-type :to_entity_id to-id))
(defn delete-dependencies-from!
"Delete the Dependencies of the entity `entity-type` `entity-id`."
[entity-type entity-id]
(mu/defn delete-dependencies-from!
"Delete the Dependencies of the entity `entity-type` `entity-id`, returning the number deleted."
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(t2/delete! :model/Dependency :from_entity_type entity-type :from_entity_id entity-id))
(defn downstream-table-ids-of-transform
(mu/defn downstream-table-ids-of-transform
"The IDs of the Tables that depend on the Transform with `transform-id`."
[transform-id]
[transform-id :- ::lib.schema.id/transform]
(t2/select-fn-set :from_entity_id :model/Dependency
:from_entity_type :table
:to_entity_type :transform
:to_entity_id transform-id))
(defn delete-table-dependencies-on-transform!
"Delete the Dependencies of the Tables with `table-ids` on the Transform with `transform-id`."
[table-ids transform-id]
(mu/defn delete-table-dependencies-on-transform!
"Delete the Dependencies of the Tables with `table-ids` on the Transform with `transform-id`, returning the
number deleted."
[table-ids :- [:sequential ::lib.schema.id/table]
transform-id :- ::lib.schema.id/transform]
(t2/delete! :model/Dependency
:from_entity_type :table
:from_entity_id [:in table-ids]
@@ -598,20 +698,23 @@
;;; ------------------------------------------------ Dependency status ------------------------------------------------
(defn dependency-status
(mu/defn dependency-status
"The DependencyStatus of the entity `entity-type` `entity-id`, or nil."
[entity-type entity-id]
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(t2/select-one :model/DependencyStatus :entity_type entity-type :entity_id entity-id))
(defn delete-dependency-status!
"Delete the DependencyStatus of the entity `entity-type` `entity-id`."
[entity-type entity-id]
(mu/defn delete-dependency-status!
"Delete the DependencyStatus of the entity `entity-type` `entity-id`, returning the number deleted."
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(t2/delete! :model/DependencyStatus :entity_type entity-type :entity_id entity-id))
(defn mark-dependency-status-stale!
(mu/defn mark-dependency-status-stale!
"Mark the DependencyStatus of `entity-type` `entity-id` as stale for dependency recalculation, creating it if it
doesn't exist. Resets retry state so previously-failed entities get a fresh chance."
[entity-type entity-id]
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(mdb/update-or-insert!
:model/DependencyStatus
{:entity_type entity-type :entity_id entity-id}
@@ -620,10 +723,12 @@
{:stale true :fail_count 0 :next_retry_at nil :terminal false}
{:stale true :dependency_analysis_version 0}))))
(defn upsert-dependency-status!
(mu/defn upsert-dependency-status!
"Upsert the DependencyStatus of `entity-type` `entity-id`, setting stale=false, `dependency_analysis_version` to
`current-version`, and clearing any failure state."
[entity-type entity-id current-version]
[entity-type :- EntityType
entity-id :- ms/PositiveInt
current-version :- ms/PositiveInt]
(mdb/update-or-insert!
:model/DependencyStatus
{:entity_type entity-type :entity_id entity-id}
@@ -633,24 +738,29 @@
:fail_count 0
:next_retry_at nil})))
(defn record-dependency-status-failure!
(mu/defn record-dependency-status-failure!
"Upsert the DependencyStatus of `entity-type` `entity-id`, creating it if needed. `update-fn` receives the
existing DependencyStatus row (or nil if none exists) and must return the columns to set."
[entity-type entity-id update-fn]
[entity-type :- EntityType
entity-id :- ms/PositiveInt
update-fn :- fn?]
(mdb/update-or-insert!
:model/DependencyStatus
{:entity_type entity-type :entity_id entity-id}
update-fn))
(defn pending-retry-exists?
(mu/defn pending-retry-exists?
"Whether a non-terminal DependencyStatus is waiting for a retry."
[]
(t2/exists? :model/DependencyStatus :terminal false :next_retry_at [:not= nil]))
(defn instances-for-dependency-calculation
(mu/defn instances-for-dependency-calculation
"Up to `batch-size` instances of the entity type `entity-type` without a DependencyStatus, or whose status is
stale or below `current-version`, is not terminal, and whose retry time has passed at `now`; stale ones first."
[entity-type batch-size current-version now]
[entity-type :- ::deps.dependency-types/dependency-types
batch-size :- ms/PositiveInt
current-version :- ms/PositiveInt
now :- ms/TemporalInstant]
(let [model (deps.dependency-types/dependency-type->model entity-type)
table-name (t2/table-name model)
id-field (keyword (name table-name) "id")
@@ -676,45 +786,57 @@
;;; ------------------------------------------------ Analysis findings ------------------------------------------------
(defn finding-id
(mu/defn finding-id
"The ID of the AnalysisFinding of the entity `entity-type` `entity-id`, or nil."
[entity-type entity-id]
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(t2/select-one-fn :id [:model/AnalysisFinding :id]
:analyzed_entity_type entity-type
:analyzed_entity_id entity-id))
(defn insert-finding!
"Insert the AnalysisFinding `row`."
[row]
(mu/defn insert-finding!
"Insert the AnalysisFinding `row`, returning the number inserted."
[row :- [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:analyzed_at {:optional true} [:maybe ms/TemporalInstant]]
[:analysis_version {:optional true} ms/PositiveInt]
[:result {:optional true} :boolean]
[:stale {:optional true} :boolean]
[:analyzed_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:analyzed_entity_id {:optional true} [:maybe ms/PositiveInt]]]]
(t2/insert! :model/AnalysisFinding row))
(defn update-finding!
"Apply `changes` to the AnalysisFinding with `finding-id`."
[finding-id changes]
(mu/defn update-finding!
"Apply `changes` to the AnalysisFinding with `finding-id`, returning the number updated."
[finding-id :- ms/PositiveInt
changes :- (mut/select-keys ::dependencies.schema/analysis-finding.update [:analyzed_at :analysis_version :result :stale])]
(t2/update! :model/AnalysisFinding finding-id changes))
(defn mark-findings-stale!
"Mark the AnalysisFindings of the entities `entity-type` `entity-ids` as stale."
[entity-type entity-ids]
(mu/defn mark-findings-stale!
"Mark the AnalysisFindings of the entities `entity-type` `entity-ids` as stale, returning the number updated."
[entity-type :- EntityType
entity-ids :- [:sequential ms/PositiveInt]]
(t2/update! :model/AnalysisFinding
:analyzed_entity_type entity-type
:analyzed_entity_id [:in entity-ids]
{:stale true}))
(defn stale-finding-exists?
(mu/defn stale-finding-exists?
"Whether a stale AnalysisFinding exists."
[]
(t2/exists? :model/AnalysisFinding :stale true))
(defn stale-finding-count
(mu/defn stale-finding-count
"The number of stale AnalysisFindings."
[]
(t2/count :model/AnalysisFinding :stale true))
(defn instances-for-analysis
(mu/defn instances-for-analysis
"Up to `batch-size` instances of the entity type `entity-type` whose AnalysisFinding is stale, missing, or below
`current-version`; stale ones first, then the longest unanalyzed."
[entity-type batch-size current-version]
[entity-type :- ::deps.dependency-types/dependency-types
batch-size :- ms/PositiveInt
current-version :- ms/PositiveInt]
(let [model (deps.dependency-types/dependency-type->model entity-type)
table-name (t2/table-name model)
id-field (keyword (name table-name) "id")
@@ -734,10 +856,10 @@
[:analysis_finding.analyzed_at :asc]]
:limit batch-size})))
(defn table-ids-with-outdated-findings
(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."
[db-id]
[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]
@@ -766,22 +888,25 @@
;;; --------------------------------------------- Analysis finding errors ---------------------------------------------
(defn finding-errors-for-entity
(mu/defn finding-errors-for-entity
"The AnalysisFindingErrors of the entity `entity-type` `entity-id`."
[entity-type entity-id]
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(t2/select :model/AnalysisFindingError :analyzed_entity_type entity-type :analyzed_entity_id entity-id))
(defn finding-errors-from-source
(mu/defn finding-errors-from-source
"The AnalysisFindingErrors caused by the entity `source-type` `source-id`."
[source-type source-id]
[source-type :- [:maybe :metabase.lib.schema.validate/source-entity-type]
source-id :- ms/PositiveInt]
(t2/select :model/AnalysisFindingError :source_entity_type source-type :source_entity_id source-id))
(defn insert-finding-errors!
"Insert the AnalysisFindingError `rows`."
[rows]
(mu/defn insert-finding-errors!
"Insert the AnalysisFindingError `rows`, returning the number inserted."
[rows :- [:sequential (mut/select-keys ::dependencies.schema/analysis-finding-error.update [:analyzed_entity_type :analyzed_entity_id :error_type :error_detail :source_entity_type :source_entity_id])]]
(t2/insert! :model/AnalysisFindingError rows))
(defn delete-finding-errors-for-entity!
"Delete the AnalysisFindingErrors of the entity `entity-type` `entity-id`."
[entity-type entity-id]
(mu/defn delete-finding-errors-for-entity!
"Delete the AnalysisFindingErrors of the entity `entity-type` `entity-id`, returning the number deleted."
[entity-type :- EntityType
entity-id :- ms/PositiveInt]
(t2/delete! :model/AnalysisFindingError :analyzed_entity_type entity-type :analyzed_entity_id entity-id))

View File

@@ -14,6 +14,17 @@
(t2/deftransforms :model/AnalysisFinding
{:analyzed_entity_type mi/transform-keyword})
(t2/define-before-insert :model/AnalysisFinding
[finding]
(merge {:analyzed_at (mi/now)} finding))
(t2/define-before-update :model/AnalysisFinding
[finding]
(let [changes (t2/changes finding)]
(cond-> finding
(and (contains? changes :result) (not (contains? changes :analyzed_at)))
(assoc :analyzed_at (mi/now)))))
(def ^:dynamic *current-analysis-finding-version*
"Current version of the query validation logic.
This should be incremented when the analysis logic changes.
@@ -45,8 +56,7 @@
Also writes individual errors to the analysis_finding_error table with source information."
[type instance-id result finding-details]
(t2/with-transaction [_conn]
(let [update {:analyzed_at (mi/now)
:analysis_version *current-analysis-finding-version*
(let [update {:analysis_version *current-analysis-finding-version*
:result result
:stale false}
existing-id (dependencies.db/finding-id type instance-id)]

View File

@@ -2,7 +2,8 @@
(:require
[metabase.documents.schema :as documents.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]))
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::upstream-deps
[:map
@@ -14,3 +15,85 @@
[:document {:optional true} [:set ::documents.schema/document.id]]
[:sandbox {:optional true} [:set ::lib.schema.id/sandbox]]
[:segment {:optional true} [:set ::lib.schema.id/segment]]])
(mr/def ::analysis-finding
"A AnalysisFinding as selected from the app DB: every column of `:analysis_finding`."
[:map {:closed true}
[:id ms/PositiveInt]
[:analyzed_entity_type [:or :keyword :string]]
[:analyzed_entity_id ms/PositiveInt]
[:analysis_version :int]
[:analyzed_at ms/TemporalInstant]
[:result :boolean]
[:stale :boolean]])
(mr/def ::analysis-finding.update
"What an update (or insert) of a AnalysisFinding accepts: every column of `:analysis_finding` except `id`, all optional."
[:map {:closed true}
[:analyzed_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:analyzed_entity_id {:optional true} [:maybe ms/PositiveInt]]
[:analysis_version {:optional true} [:maybe :int]]
[:analyzed_at {:optional true} [:maybe ms/TemporalInstant]]
[:result {:optional true} [:maybe :boolean]]
[:stale {:optional true} [:maybe :boolean]]])
(mr/def ::analysis-finding-error
"A AnalysisFindingError as selected from the app DB: every column of `:analysis_finding_error`."
[:map {:closed true}
[:id ms/PositiveInt]
[:analyzed_entity_type [:or :keyword :string]]
[:analyzed_entity_id ms/PositiveInt]
[:error_type [:or :keyword :string]]
[:error_detail [:maybe :string]]
[:source_entity_type [:maybe [:or :keyword :string]]]
[:source_entity_id [:maybe ms/PositiveInt]]])
(mr/def ::analysis-finding-error.update
"What an update (or insert) of a AnalysisFindingError accepts: every column of `:analysis_finding_error` except `id`, all optional."
[:map {:closed true}
[:analyzed_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:analyzed_entity_id {:optional true} [:maybe ms/PositiveInt]]
[:error_type {:optional true} [:maybe [:or :keyword :string]]]
[:error_detail {:optional true} [:maybe :string]]
[:source_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:source_entity_id {:optional true} [:maybe ms/PositiveInt]]])
(mr/def ::dependency
"A Dependency as selected from the app DB: every column of `:dependency`."
[:map {:closed true}
[:id ms/PositiveInt]
[:from_entity_type [:or :keyword :string]]
[:from_entity_id ms/PositiveInt]
[:to_entity_type [:or :keyword :string]]
[:to_entity_id ms/PositiveInt]])
(mr/def ::dependency.update
"What an update (or insert) of a Dependency accepts: every column of `:dependency` except `id`, all optional."
[:map {:closed true}
[:from_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:from_entity_id {:optional true} [:maybe ms/PositiveInt]]
[:to_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:to_entity_id {:optional true} [:maybe ms/PositiveInt]]])
(mr/def ::dependency-status
"A DependencyStatus as selected from the app DB: every column of `:dependency_status`."
[:map {:closed true}
[:id ms/PositiveInt]
[:entity_type [:or :keyword :string]]
[:entity_id ms/PositiveInt]
[:dependency_analysis_version :int]
[:stale :boolean]
[:fail_count :int]
[:next_retry_at [:maybe ms/TemporalInstant]]
[:terminal :boolean]])
(mr/def ::dependency-status.update
"What an update (or insert) of a DependencyStatus accepts: every column of `:dependency_status` except `id`, all optional."
[:map {:closed true}
[:entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:entity_id {:optional true} [:maybe ms/PositiveInt]]
[:dependency_analysis_version {:optional true} [:maybe :int]]
[:stale {:optional true} [:maybe :boolean]]
[:fail_count {:optional true} [:maybe :int]]
[:next_retry_at {:optional true} [:maybe ms/TemporalInstant]]
[:terminal {:optional true} [:maybe :boolean]]])

View File

@@ -2,32 +2,35 @@
"Application database queries for the embedding-hub module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn user-database-exists?
(mu/defn user-database-exists?
"Whether a non-sample, non-audit Database exists."
[]
(t2/exists? :model/Database {:where [:and
[:= :is_sample false]
[:= :is_audit false]]}))
(defn sample-database-id
(mu/defn sample-database-id
"The ID of the sample Database, or nil."
[]
(t2/select-one-pk :model/Database :is_sample true))
(defn upload-table-exists-in-database?
(mu/defn upload-table-exists-in-database?
"Whether the Database with `database-id` holds an active uploaded Table."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Table {:where [:and
[:= :active true]
[:= :is_upload true]
[:= :db_id database-id]]}))
(defn user-dashboard-exists?
(mu/defn user-dashboard-exists?
"Whether an unarchived Dashboard exists other than `example-dashboard-id` and outside the Collections with
`excluded-collection-ids`."
[example-dashboard-id excluded-collection-ids]
[example-dashboard-id :- [:maybe ::lib.schema.id/dashboard]
excluded-collection-ids :- [:maybe [:sequential ::lib.schema.id/collection]]]
(t2/exists? :model/Dashboard {:where (cond-> [:and
[:= :archived false]]
example-dashboard-id (conj [:not= :id example-dashboard-id])
@@ -35,15 +38,15 @@
[:is :collection_id nil]
[:not-in :collection_id excluded-collection-ids]]))}))
(defn sandbox-exists?
(mu/defn sandbox-exists?
"Whether any Sandbox exists."
[]
(t2/exists? :model/Sandbox))
(defn user-model-exists?
(mu/defn user-model-exists?
"Whether an unarchived model Card exists outside the sample Collections and the Collection with
`audit-collection-id`."
[audit-collection-id]
[audit-collection-id :- [:maybe ::lib.schema.id/collection]]
(t2/exists? :model/Card {:where [:and
[:= :type "model"]
[:= :archived false]
@@ -58,48 +61,48 @@
[:= :sample_coll.id :report_card.collection_id]]}]]]
[:is :collection_id nil]]]}))
(defn active-tenant-exists?
(mu/defn active-tenant-exists?
"Whether an active Tenant exists."
[]
(t2/exists? :model/Tenant :is_active true))
(defn shared-tenant-collection-exists?
(mu/defn shared-tenant-collection-exists?
"Whether an unarchived shared tenant Collection exists."
[]
(t2/exists? :model/Collection {:where [:and
[:= :namespace "shared-tenant-collection"]
[:= :archived false]]}))
(defn shared-tenant-collection-id
(mu/defn shared-tenant-collection-id
"The ID of an unarchived shared tenant Collection, or nil."
[]
(t2/select-one-pk :model/Collection {:where [:and
[:= :namespace "shared-tenant-collection"]
[:= :archived false]]}))
(defn unarchived-dashboard-exists-in-collection?
(mu/defn unarchived-dashboard-exists-in-collection?
"Whether an unarchived Dashboard exists in the Collection with `collection-id`."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/exists? :model/Dashboard {:where [:and
[:= :collection_id collection-id]
[:= :archived false]]}))
(defn impersonation-exists?
(mu/defn impersonation-exists?
"Whether any ConnectionImpersonation exists."
[]
(t2/exists? :model/ConnectionImpersonation))
(defn database-router-exists?
(mu/defn database-router-exists?
"Whether any DatabaseRouter exists."
[]
(t2/exists? :model/DatabaseRouter))
(defn embedded-card-exists?
(mu/defn embedded-card-exists?
"Whether any Card has embedding enabled."
[]
(t2/exists? :model/Card :enable_embedding true))
(defn embedded-dashboard-exists?
(mu/defn embedded-dashboard-exists?
"Whether any Dashboard has embedding enabled."
[]
(t2/exists? :model/Dashboard :enable_embedding true))

View File

@@ -2,12 +2,16 @@
"Application database queries for the entity-retrieval module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn library-cards-in-collections
(mu/defn library-cards-in-collections
"The ID, name, description, and type of the unarchived metric and model Cards in the Collections with
`collection-ids`, optionally narrowed to just `id`."
[collection-ids id]
[collection-ids :- [:sequential ::lib.schema.id/collection]
id :- [:maybe ::lib.schema.id/card]]
(t2/select [:model/Card :id :name :description :type :card_schema]
{:where [:and
[:in :collection_id collection-ids]
@@ -15,10 +19,11 @@
[:in :type ["metric" "model"]]
(when id [:= :id id])]}))
(defn library-tables-in-collections
(mu/defn library-tables-in-collections
"The ID, names, and description of the active published Tables in the Collections with `collection-ids`, optionally
narrowed to just `id`."
[collection-ids id]
[collection-ids :- [:sequential ::lib.schema.id/collection]
id :- [:maybe ::lib.schema.id/table]]
(t2/select [:model/Table :id :name :display_name :description]
{:where [:and
[:in :collection_id collection-ids]
@@ -26,44 +31,47 @@
[:= :active true]
(when id [:= :id id])]}))
(defn library-measures-of-tables
(mu/defn library-measures-of-tables
"The ID, name, and description of the unarchived Measures on the Tables with `table-ids`, optionally narrowed to
just `id`."
[table-ids id]
[table-ids :- [:sequential ::lib.schema.id/table]
id :- [:maybe ::lib.schema.id/measure]]
(t2/select [:model/Measure :id :name :description]
{:where [:and
[:in :table_id table-ids]
[:= :archived false]
(when id [:= :id id])]}))
(defn library-segments-of-tables
(mu/defn library-segments-of-tables
"The ID, name, and description of the unarchived Segments on the Tables with `table-ids`, optionally narrowed to
just `id`."
[table-ids id]
[table-ids :- [:sequential ::lib.schema.id/table]
id :- [:maybe ::lib.schema.id/segment]]
(t2/select [:model/Segment :id :name :description]
{:where [:and
[:in :table_id table-ids]
[:= :archived false]
(when id [:= :id id])]}))
(defn measure-table-id
(mu/defn measure-table-id
"The Table ID of the Measure with `id`."
[id]
[id :- ::lib.schema.id/measure]
(t2/select-one-fn :table_id :model/Measure :id id))
(defn segment-table-id
(mu/defn segment-table-id
"The Table ID of the Segment with `id`."
[id]
[id :- ::lib.schema.id/segment]
(t2/select-one-fn :table_id :model/Segment :id id))
(defn ai-contexts
(mu/defn ai-contexts
"The entity type, entity ID, and AI context of every OsiAiContext."
[]
(t2/select [:model/OsiAiContext :entity_type :entity_local_id :ai_context]))
(defn ai-context
(mu/defn ai-context
"The AI context of the entity of `entity-type` with `entity-local-id`."
[entity-local-id entity-type]
[entity-local-id :- ms/PositiveInt
entity-type :- :string]
(t2/select-one-fn :ai_context :model/OsiAiContext
:entity_local_id entity-local-id
:entity_type entity-type))

View File

@@ -2,33 +2,44 @@
"Application database queries for the erd module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.warehouse-schema.models.table :as schema.table]
[toucan2.core :as t2]))
(defn active-tables-in-database
(def ^:private ActiveTablesOpts
"The optional restrictions [[active-tables-in-database]] accepts."
[:map {:closed true}
[:table-ids {:optional true} [:or [:set ::lib.schema.id/table] [:sequential ::lib.schema.id/table]]]
[:schema {:optional true} [:maybe :string]]])
(mu/defn active-tables-in-database
"The `columns` of the active Tables in the Database with `database-id`. `opts` may restrict the result: a
`:table-ids` key restricts to those IDs (even when its value is empty), and a `:schema` key restricts to that
schema (`\"\"` matches both nil and empty-string schemas)."
[columns database-id {:keys [table-ids schema] :as opts}]
(t2/select :model/Table
{: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 "")
[:or [:= :schema nil] [:= :schema ""]]
[:= :schema schema])))}))
[columns :- [:sequential :keyword]
database-id :- ::lib.schema.id/database
opts :- [:maybe ActiveTablesOpts]]
(let [{:keys [table-ids schema]} opts]
(t2/select :model/Table
{: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 "")
[:or [:= :schema nil] [:= :schema ""]]
[:= :schema schema])))})))
(defn active-fields-for-tables
(mu/defn active-fields-for-tables
"The active Fields of the Tables with `table-ids`, in field order."
[table-ids]
[table-ids :- [:set ::lib.schema.id/table]]
(t2/select :model/Field {:where [:and
[:in :table_id table-ids]
[:= :active true]]
:order-by schema.table/field-order-rule}))
(defn active-fields
(mu/defn active-fields
"The active Fields with `field-ids`."
[field-ids]
[field-ids :- [:or [:set ::lib.schema.id/field] [:sequential ::lib.schema.id/field]]]
(t2/select :model/Field {:where [:and
[:in :id field-ids]
[:= :active true]]}))

View File

@@ -2,24 +2,28 @@
"Application database queries for the gsheets module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.warehouses.schema :as warehouses.schema]
[toucan2.core :as t2]))
(defn attached-dwh-database-id
(mu/defn attached-dwh-database-id
"The ID of the attached data warehouse Database, or nil."
[]
(t2/select-one-fn :id :model/Database :is_attached_dwh true))
(defn attached-dwh-database
(mu/defn attached-dwh-database
"The attached data warehouse Database, or nil."
[]
(t2/select-one :model/Database :is_attached_dwh true))
(defn setting
(mu/defn setting
"The Setting with `setting-key`, or nil."
[setting-key]
[setting-key :- :string]
(t2/select-one :model/Setting :key setting-key))
(defn update-database!
"Apply `changes` to the Database with `database-id`."
[database-id changes]
(mu/defn update-database!
"Apply `changes` to the Database with `database-id`, returning the number updated."
[database-id :- ::lib.schema.id/database
changes :- ::warehouses.schema/database.update]
(t2/update! :model/Database database-id changes))

View File

@@ -2,37 +2,47 @@
"Application database queries for the impersonation module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[malli.util :as mut]
[metabase-enterprise.impersonation.schema :as impersonation.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn impersonation
(mu/defn impersonation
"The ConnectionImpersonation with `impersonation-id`, or nil."
[impersonation-id]
[impersonation-id :- ms/PositiveInt]
(t2/select-one :model/ConnectionImpersonation :id impersonation-id))
(defn impersonation-for-group-and-database
(mu/defn impersonation-for-group-and-database
"The ConnectionImpersonation of the group with `group-id` on the Database with `database-id`, or nil."
[group-id database-id]
[group-id :- ms/PositiveInt
database-id :- ::lib.schema.id/database]
(t2/select-one :model/ConnectionImpersonation :group_id group-id :db_id database-id))
(defn all-impersonations
(mu/defn all-impersonations
"Every ConnectionImpersonation, in ID order."
[]
(t2/select :model/ConnectionImpersonation {:order-by [[:id :asc]]}))
(defn impersonations-for-groups
(mu/defn impersonations-for-groups
"The ConnectionImpersonations of the groups with `group-ids`."
[group-ids]
[group-ids :- [:set ms/PositiveInt]]
(t2/select :model/ConnectionImpersonation :group_id [:in group-ids]))
(defn impersonations-for-groups-and-database
(mu/defn impersonations-for-groups-and-database
"The ConnectionImpersonations of the groups with `group-ids` on the Database with `database-id`."
[group-ids database-id]
[group-ids :- [:set ms/PositiveInt]
database-id :- ::lib.schema.id/database]
(t2/select :model/ConnectionImpersonation :group_id [:in group-ids] :db_id database-id))
(defn impersonations-matching
(mu/defn impersonations-matching
"The ConnectionImpersonations narrowed by the optional `database-id`, `group-id`, and `group-ids`, excluding the
Database with `excluded-database-id` when given."
[database-id group-id group-ids excluded-database-id]
[database-id :- [:maybe ::lib.schema.id/database]
group-id :- [:maybe ms/PositiveInt]
group-ids :- [:maybe [:sequential ms/PositiveInt]]
excluded-database-id :- [:maybe ::lib.schema.id/database]]
(t2/select :model/ConnectionImpersonation
{:where [:and
(when database-id [:= :db_id database-id])
@@ -40,29 +50,31 @@
(when group-ids [:in :group_id group-ids])
(when excluded-database-id [:not [:= :db_id excluded-database-id]])]}))
(defn impersonation-exists-for-database?
(mu/defn impersonation-exists-for-database?
"Whether the Database with `database-id` has a ConnectionImpersonation."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/ConnectionImpersonation :db_id database-id))
(defn insert-impersonation!
(mu/defn insert-impersonation!
"Insert `impersonation` and return the new instance."
[impersonation]
[impersonation :- (mut/select-keys ::impersonation.schema/connection-impersonation.update [:db_id :group_id :attribute])]
(first (t2/insert-returning-instances! :model/ConnectionImpersonation impersonation)))
(defn delete-impersonation!
(mu/defn delete-impersonation!
"Delete the ConnectionImpersonation with `impersonation-id`."
[impersonation-id]
[impersonation-id :- ms/PositiveInt]
(t2/delete! :model/ConnectionImpersonation :id impersonation-id))
(defn delete-impersonations-for-group-and-database!
(mu/defn delete-impersonations-for-group-and-database!
"Delete the ConnectionImpersonations of the group with `group-id` on the Database with `database-id`."
[group-id database-id]
[group-id :- ms/PositiveInt
database-id :- ::lib.schema.id/database]
(t2/delete! :model/ConnectionImpersonation :group_id group-id :db_id database-id))
(defn view-data-permission-values
(mu/defn view-data-permission-values
"The set of database-level view-data permission values `group-ids` hold on the Database with `database-id`."
[database-id group-ids]
[database-id :- ::lib.schema.id/database
group-ids :- [:set ms/PositiveInt]]
(t2/select-fn-set :perm_value :model/DataPermissions
{:where [:and
[:= :db_id database-id]
@@ -70,12 +82,13 @@
[:= :perm_type "perms/view-data"]
[:in :group_id group-ids]]}))
(defn group-ids-for-user
"The IDs of the PermissionsGroups the User with `user-id` belongs to."
[user-id]
(mu/defn group-ids-for-user
"The IDs of the PermissionsGroups the User with `user-id` belongs to, or nil when `user-id` is nil (e.g. no
current user, such as an internal or unauthenticated context)."
[user-id :- [:maybe ::lib.schema.id/user]]
(t2/select-fn-set :group_id :model/PermissionsGroupMembership :user_id user-id))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database :id database-id))

View File

@@ -0,0 +1,21 @@
(ns metabase-enterprise.impersonation.schema
"Malli schemas for the impersonation module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::connection-impersonation
"A ConnectionImpersonation as selected from the app DB: every column of `:connection_impersonations`."
[:map {:closed true}
[:id ms/PositiveInt]
[:db_id ::lib.schema.id/database]
[:group_id ms/PositiveInt]
[:attribute [:maybe :string]]])
(mr/def ::connection-impersonation.update
"What an update (or insert) of a ConnectionImpersonation accepts: every column of `:connection_impersonations` except `id`, all optional."
[:map {:closed true}
[:db_id {:optional true} [:maybe ::lib.schema.id/database]]
[:group_id {:optional true} [:maybe ms/PositiveInt]]
[:attribute {:optional true} [:maybe :string]]])

View File

@@ -3,19 +3,21 @@
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
(:require
[metabase.collections.models.collection :as collection]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn card-types-in-collections
(mu/defn card-types-in-collections
"The set of Card types present in the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select-fn-set :type [:model/Card :type] :collection_id [:in collection-ids]))
(defn published-table-in-collections?
(mu/defn published-table-in-collections?
"Whether a published Table exists in the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/exists? :model/Table :is_published true :collection_id [:in collection-ids]))
(defn library-collections
(mu/defn library-collections
"The readable Library, Library-data, and Library-metrics Collections, ordered by name."
[]
(t2/select :model/Collection
@@ -31,12 +33,12 @@
:archive-operation-id nil})]
:order-by [[:%lower.name :asc]]}))
(defn collection-type
(mu/defn collection-type
"The type of the Collection with `collection-id`."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/select-one-fn :type [:model/Collection :type] :id collection-id))
(defn unarchived-card-collection-types-reducible
(mu/defn unarchived-card-collection-types-reducible
"Reducible distinct Collection ID and Card type pairs of the unarchived Cards."
[]
(t2/reducible-query {:select-distinct [:collection_id :type]

View File

@@ -2,39 +2,50 @@
"Application database queries for the mcp module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.mcp.schema :as mcp.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn session-log-exists?
(mu/defn session-log-exists?
"Whether an McpSessionLog with `session-id` exists."
[session-id]
[session-id :- :string]
(t2/exists? :model/McpSessionLog :id session-id))
(defn session-client-identity
(mu/defn session-client-identity
"The client name and version of the McpSessionLog with `session-id`, or nil."
[session-id]
[session-id :- :string]
(t2/select-one [:model/McpSessionLog :client_name :client_version] :id session-id))
(defn insert-session-log!
(mu/defn insert-session-log!
"Insert the McpSessionLog `row`."
[row]
[row :- [:map {:closed true}
[:id {:optional true} :string]
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
[:client_name {:optional true} [:maybe :string]]
[:client_version {:optional true} [:maybe :string]]
[:ip_address {:optional true} [:maybe :string]]
[:user_agent {:optional true} [:maybe :string]]]]
(t2/insert! :model/McpSessionLog row))
(defn end-session-log!
(mu/defn end-session-log!
"Stamp `ended_at` on the McpSessionLog with `session-id`."
[session-id]
[session-id :- :string]
(t2/update! :model/McpSessionLog :id session-id {:ended_at :%now}))
(defn insert-tool-call-log!
(mu/defn insert-tool-call-log!
"Insert the McpToolCallLog `row`."
[row]
[row :- ::mcp.schema/mcp-tool-call-log.update]
(t2/insert! :model/McpToolCallLog row))
(defn delete-tool-call-logs-created-before!
(mu/defn delete-tool-call-logs-created-before!
"Delete the McpToolCallLogs created before `cutoff`, returning the number deleted."
[cutoff]
[cutoff :- ms/TemporalInstant]
(t2/delete! :model/McpToolCallLog {:where [:< :created_at cutoff]}))
(defn delete-session-logs-created-before!
(mu/defn delete-session-logs-created-before!
"Delete the McpSessionLogs created before `cutoff`, returning the number deleted."
[cutoff]
[cutoff :- ms/TemporalInstant]
(t2/delete! :model/McpSessionLog {:where [:< :created_at cutoff]}))

View File

@@ -2,8 +2,12 @@
"Application database queries for the metabot module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.metabot.schema :as metabot.schema]
[metabase.permissions.core :as perms]
[metabase.util :as u]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn- default-group-ids
@@ -19,19 +23,20 @@
[:not-in :group_id (default-group-ids)]
[:in :group_id (conj (default-group-ids) (u/the-id (perms/admin-group)))]))
(defn all-groups
(mu/defn all-groups
"Every PermissionsGroup, in ID order."
[]
(t2/select :model/PermissionsGroup {:order-by [[:id :asc]]}))
(defn all-stored-permissions
(mu/defn all-stored-permissions
"Every MetabotPermissions row, ordered by group and permission type."
[]
(t2/select :model/MetabotPermissions {:order-by [[:group_id :asc] [:perm_type :asc]]}))
(defn visible-permissions-for-user
(mu/defn visible-permissions-for-user
"The MetabotPermissions rows of the groups of the User with `user-id` that the mode selected by `advanced?` shows."
[user-id advanced?]
[user-id :- ::lib.schema.id/user
advanced? :- :boolean]
(t2/select :model/MetabotPermissions
{:where [:and
[:in :group_id
@@ -41,40 +46,47 @@
:where [:= :user_id user-id]}]
(visible-groups-expr advanced?)]}))
(defn permission-exists?
(mu/defn permission-exists?
"Whether the group with `group-id` has a MetabotPermissions row of `perm-type`."
[group-id perm-type]
[group-id :- ms/PositiveInt
perm-type :- :keyword]
(t2/exists? :model/MetabotPermissions :group_id group-id :perm_type perm-type))
(defn update-permission-value!
(mu/defn update-permission-value!
"Set the value of the MetabotPermissions row of `perm-type` for the group with `group-id`."
[group-id perm-type perm-value]
[group-id :- ms/PositiveInt
perm-type :- :keyword
perm-value :- :keyword]
(t2/update! :model/MetabotPermissions {:group_id group-id :perm_type perm-type} {:perm_value perm-value}))
(defn insert-permission!
(mu/defn insert-permission!
"Insert the MetabotPermissions `row`."
[row]
[row :- [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:group_id {:optional true} [:maybe ms/PositiveInt]]
[:perm_type {:optional true} [:maybe [:or :keyword :string]]]
[:perm_value {:optional true} [:maybe [:or :keyword :string]]]]]
(t2/insert! :model/MetabotPermissions row))
(defn delete-hidden-group-permissions!
(mu/defn delete-hidden-group-permissions!
"Delete the MetabotPermissions rows of the groups the mode selected by `advanced?` hides."
[advanced?]
[advanced? :- :boolean]
(t2/delete! :model/MetabotPermissions {:where [:not (visible-groups-expr advanced?)]}))
(defn group-limits
(mu/defn group-limits
"Every MetabotGroupLimit, in group order."
[]
(t2/select :model/MetabotGroupLimit {:order-by [[:group_id :asc]]}))
(defn group-limit
(mu/defn group-limit
"The MetabotGroupLimit of the group with `group-id`, or nil."
[group-id]
[group-id :- ms/PositiveInt]
(t2/select-one :model/MetabotGroupLimit :group_id group-id))
(defn max-usage-for-user
(mu/defn max-usage-for-user
"The `:max_usage` row holding the largest group limit of the User with `user-id`, or nil if any of their groups
is unlimited."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/query-one {:select [[[:case
[:= [[:count :*]] [[:count :gl.max_usage]]]
[[:max :gl.max_usage]]]
@@ -83,49 +95,57 @@
:left-join [[:metabot_group_limit :gl] [:= :pgm.group_id :gl.group_id]]
:where [:= :pgm.user_id user-id]}))
(defn insert-group-limit!
(mu/defn insert-group-limit!
"Insert the MetabotGroupLimit `row`."
[row]
[row :- [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:group_id {:optional true} [:maybe ms/PositiveInt]]
[:max_usage {:optional true} [:maybe :int]]]]
(t2/insert! :model/MetabotGroupLimit row))
(defn update-group-limit!
(mu/defn update-group-limit!
"Set the maximum usage of the MetabotGroupLimit with `limit-id`."
[limit-id max-usage]
[limit-id :- ms/PositiveInt
max-usage :- [:maybe :int]]
(t2/update! :model/MetabotGroupLimit limit-id {:max_usage max-usage}))
(defn delete-group-limit!
(mu/defn delete-group-limit!
"Delete the MetabotGroupLimit of the group with `group-id`."
[group-id]
[group-id :- ms/PositiveInt]
(t2/delete! :model/MetabotGroupLimit :group_id group-id))
(defn instance-limit
(mu/defn instance-limit
"The MetabotInstanceLimit of the Tenant with `tenant-id` (nil for the instance-wide limit), or nil."
[tenant-id]
[tenant-id :- [:maybe ms/PositiveInt]]
(t2/select-one :model/MetabotInstanceLimit :tenant_id tenant-id))
(defn tenant-limits
(mu/defn tenant-limits
"The MetabotInstanceLimits of tenants, ordered by tenant."
[]
(t2/select :model/MetabotInstanceLimit :tenant_id [:not= nil] {:order-by [[:tenant_id :asc]]}))
(defn insert-instance-limit!
(mu/defn insert-instance-limit!
"Insert the MetabotInstanceLimit `row`."
[row]
[row :- [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
[:max_usage {:optional true} [:maybe :int]]]]
(t2/insert! :model/MetabotInstanceLimit row))
(defn update-instance-limit!
(mu/defn update-instance-limit!
"Set the maximum usage of the MetabotInstanceLimit with `limit-id`."
[limit-id max-usage]
[limit-id :- ms/PositiveInt
max-usage :- [:maybe :int]]
(t2/update! :model/MetabotInstanceLimit limit-id {:max_usage max-usage}))
(defn delete-instance-limit!
(mu/defn delete-instance-limit!
"Delete the MetabotInstanceLimit of the Tenant with `tenant-id`."
[tenant-id]
[tenant-id :- [:maybe ms/PositiveInt]]
(t2/delete! :model/MetabotInstanceLimit :tenant_id tenant-id))
(defn insert-usage-log!
(mu/defn insert-usage-log!
"Insert the AiUsageLog `row`."
[row]
[row :- ::metabot.schema/ai-usage-log.update]
(t2/insert! :model/AiUsageLog row))
(defn- usage-window-expr
@@ -135,36 +155,40 @@
(when user-id [:= :user_id user-id])
(when tenant-id [:= :tenant_id tenant-id])])
(defn usage-token-sum
(mu/defn usage-token-sum
"The `:sum` row of tokens logged since `period-start`, narrowed by the optional `user-id` and `tenant-id`."
[period-start user-id tenant-id]
[period-start :- ms/TemporalInstant
user-id :- [:maybe ::lib.schema.id/user]
tenant-id :- [:maybe ms/PositiveInt]]
(t2/query-one {:select [[[:sum :total_tokens] :sum]]
:from [:ai_usage_log]
:where (usage-window-expr period-start user-id tenant-id)}))
(defn usage-message-count
(mu/defn usage-message-count
"The `:cnt` row of messages logged since `period-start`, narrowed by the optional `user-id` and `tenant-id`."
[period-start user-id tenant-id]
[period-start :- ms/TemporalInstant
user-id :- [:maybe ::lib.schema.id/user]
tenant-id :- [:maybe ms/PositiveInt]]
(t2/query-one {:select [[[:count :*] :cnt]]
:from [:ai_usage_log]
:where (usage-window-expr period-start user-id tenant-id)}))
(defn delete-usage-logs-created-before!
(mu/defn delete-usage-logs-created-before!
"Delete the AiUsageLogs created before `cutoff`, returning the number deleted."
[cutoff]
[cutoff :- ms/TemporalInstant]
(t2/delete! :model/AiUsageLog {:where [:< :created_at cutoff]}))
(defn transform
(mu/defn transform
"The Transform with `transform-id`, or nil."
[transform-id]
[transform-id :- ::lib.schema.id/transform]
(t2/select-one :model/Transform :id transform-id))
(defn transforms
(mu/defn transforms
"The Transforms with `transform-ids`."
[transform-ids]
[transform-ids :- [:sequential ::lib.schema.id/transform]]
(t2/select :model/Transform :id [:in transform-ids]))
(defn cards
(mu/defn cards
"The Cards with `card-ids`."
[card-ids]
[card-ids :- [:sequential ::lib.schema.id/card]]
(t2/select :model/Card :id [:in card-ids]))

View File

@@ -0,0 +1,46 @@
(ns metabase-enterprise.metabot.schema
"Malli schemas for the metabot module."
(:require
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::metabot-group-limit
"A MetabotGroupLimit as selected from the app DB: every column of `:metabot_group_limit`."
[:map {:closed true}
[:id ms/PositiveInt]
[:group_id ms/PositiveInt]
[:max_usage :int]])
(mr/def ::metabot-group-limit.update
"What an update (or insert) of a MetabotGroupLimit accepts: every column of `:metabot_group_limit` except `id`, all optional."
[:map {:closed true}
[:group_id {:optional true} [:maybe ms/PositiveInt]]
[:max_usage {:optional true} [:maybe :int]]])
(mr/def ::metabot-instance-limit
"A MetabotInstanceLimit as selected from the app DB: every column of `:metabot_instance_limit`."
[:map {:closed true}
[:id ms/PositiveInt]
[:tenant_id [:maybe ms/PositiveInt]]
[:max_usage [:maybe :int]]])
(mr/def ::metabot-instance-limit.update
"What an update (or insert) of a MetabotInstanceLimit accepts: every column of `:metabot_instance_limit` except `id`, all optional."
[:map {:closed true}
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
[:max_usage {:optional true} [:maybe :int]]])
(mr/def ::metabot-permissions
"A MetabotPermissions as selected from the app DB: every column of `:metabot_permissions`."
[:map {:closed true}
[:id ms/PositiveInt]
[:group_id ms/PositiveInt]
[:perm_type [:or :keyword :string]]
[:perm_value [:or :keyword :string]]])
(mr/def ::metabot-permissions.update
"What an update (or insert) of a MetabotPermissions accepts: every column of `:metabot_permissions` except `id`, all optional."
[:map {:closed true}
[:group_id {:optional true} [:maybe ms/PositiveInt]]
[:perm_type {:optional true} [:maybe [:or :keyword :string]]]
[:perm_value {:optional true} [:maybe [:or :keyword :string]]]])

View File

@@ -2,15 +2,18 @@
"Application database queries for the metabot-analytics module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.permissions.core :as perms]
[metabase.query-processor.parameters.dates :as qp.parameters.dates]
[metabase.util.date-2 :as u.date]
[metabase.util.i18n :refer [tru]]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn conversation
(mu/defn conversation
"The MetabotConversation with `conversation-id`, or nil."
[conversation-id]
[conversation-id :- :string]
(t2/select-one :model/MetabotConversation :id conversation-id))
(defn- date-range-expr
@@ -104,11 +107,20 @@
[:core_user :u] [:= :u.id :c.user_id]]
:group-by [:c.id]})
(defn list-conversations
(mu/defn list-conversations
"A page of conversation summary rows (see [[conversation-list-select]]), restricted to `user-id`, `group-id`,
`tenant-id`, and `date` (each nil for no restriction), sorted by the allow-listed `sort-by` column name in
`sort-direction` (`:asc` or `:desc`), skipping `offset` and returning up to `limit`."
[{:keys [user-id group-id tenant-id date sort-by sort-direction offset limit]}]
[{:keys [user-id group-id tenant-id date sort-by sort-direction offset limit]}
:- [:map {:closed true}
[:user-id {:optional true} [:maybe ::lib.schema.id/user]]
[:group-id {:optional true} [:maybe ms/PositiveInt]]
[:tenant-id {:optional true} [:maybe ms/PositiveInt]]
[:date {:optional true} [:maybe :string]]
[:sort-by {:optional true} [:maybe :string]]
[:sort-direction {:optional true} [:enum :asc :desc]]
[:offset {:optional true} ms/IntGreaterThanOrEqualToZero]
[:limit {:optional true} ms/PositiveInt]]]
(let [where (conversation-list-expr {:user-id user-id :group-id group-id :tenant-id tenant-id :date date})
sort-exprs (get sort-columns sort-by [:c.created_at])
order-by (conj (mapv #(vector % sort-direction) sort-exprs)
@@ -120,29 +132,34 @@
:offset offset)
where (assoc :where where)))))
(defn conversation-count
(mu/defn conversation-count
"The total count of conversations matching the same criteria as [[list-conversations]] (ignoring sort, offset,
and limit)."
[{:keys [user-id group-id tenant-id date]}]
[{:keys [user-id group-id tenant-id date]}
:- [:map {:closed true}
[:user-id {:optional true} [:maybe ::lib.schema.id/user]]
[:group-id {:optional true} [:maybe ms/PositiveInt]]
[:tenant-id {:optional true} [:maybe ms/PositiveInt]]
[:date {:optional true} [:maybe :string]]]]
(let [where (conversation-list-expr {:user-id user-id :group-id group-id :tenant-id tenant-id :date date})]
(:count (t2/query-one (cond-> {:select [[[:count :*] :count]]
:from [[:metabot_conversation :c]]}
where (assoc :where where))))))
(defn messages-for-conversation
(mu/defn messages-for-conversation
"The MetabotMessages of the MetabotConversation with `conversation-id`, oldest first."
[conversation-id]
[conversation-id :- :string]
(t2/select :model/MetabotMessage :conversation_id conversation-id {:order-by [[:created_at :asc] [:id :asc]]}))
(defn message-data-for-conversations
(mu/defn message-data-for-conversations
"The conversation, data, and data version of the MetabotMessages of the MetabotConversations with
`conversation-ids`."
[conversation-ids]
[conversation-ids :- [:sequential :string]]
(t2/select [:model/MetabotMessage :conversation_id :data :data_version] :conversation_id [:in conversation-ids]))
(defn feedback-for-conversation
(mu/defn feedback-for-conversation
"The MetabotFeedback rows on the messages of the MetabotConversation with `conversation-id`, oldest first."
[conversation-id]
[conversation-id :- :string]
(t2/select :model/MetabotFeedback
{:select [:metabot_feedback.id
:metabot_feedback.message_id

View File

@@ -3,7 +3,12 @@
additional logic, so the rest of the module only touches `toucan2.core` for transactions."
(:require
[clojure.string :as str]
[malli.util :as mut]
[metabase.auth-identity.schema :as auth-identity.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(def ^:private totp-provider "totp")
@@ -53,65 +58,80 @@
[:like :%lower.last_name pattern]
[:like :%lower.email pattern]]))))
(defn user
(mu/defn user
"The User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one :model/User :id user-id))
(defn user-email
(mu/defn user-email
"The email of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-fn :email :model/User :id user-id))
(defn lock-user
(mu/defn lock-user
"The `:id` row of the User with `user-id`, locked for update."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one [:model/User :id] :id user-id {:for :update}))
(defn totp-identity
(mu/defn totp-identity
"The TOTP AuthIdentity of the User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one :model/AuthIdentity :user_id user-id :provider totp-provider))
(defn lock-totp-identity
(mu/defn lock-totp-identity
"The TOTP AuthIdentity of the User with `user-id`, locked for update, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one :model/AuthIdentity :user_id user-id :provider totp-provider {:for :update}))
(defn password-credentials
(mu/defn password-credentials
"The password credentials of the User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-fn :credentials :model/AuthIdentity :user_id user-id :provider "password"))
(defn insert-auth-identity!
"Insert the AuthIdentity `row`."
[row]
(mu/defn insert-auth-identity!
"Insert the AuthIdentity `row`, returning the number inserted."
[row :- [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:provider {:optional true} [:maybe [:or :keyword :string]]]
[:credentials {:optional true} [:maybe :map]]
[:metadata {:optional true} [:maybe :map]]
[:provider_id {:optional true} [:maybe :string]]
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]]]
(t2/insert! :model/AuthIdentity row))
(defn update-auth-identity!
"Apply `changes` to the AuthIdentity with `auth-identity-id`."
[auth-identity-id changes]
(mu/defn update-auth-identity!
"Apply `changes` to the AuthIdentity with `auth-identity-id`, returning the number updated."
[auth-identity-id :- ms/PositiveInt
changes :- (mut/select-keys ::auth-identity.schema/auth-identity.update [:credentials :confirmed_at])]
(t2/update! :model/AuthIdentity auth-identity-id changes))
(defn delete-totp-identity!
(mu/defn delete-totp-identity!
"Delete the TOTP AuthIdentity of the User with `user-id`, returning the number deleted."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/delete! :model/AuthIdentity :user_id user-id :provider totp-provider))
(defn confirmed-totp-count
(mu/defn confirmed-totp-count
"The number of confirmed TOTP enrollments."
[]
(t2/count :model/AuthIdentity :provider totp-provider :confirmed_at [:not= nil]))
(defn unenrolled-user-count
(mu/defn unenrolled-user-count
"The number of active personal Users without a confirmed TOTP enrollment."
[]
(t2/count :model/User {:where unenrolled-user-where}))
(defn user-list
(mu/defn user-list
"The name-ordered admin list of enrolled (with their enrollment time) or unenrolled Users matching `search`, paged
by the optional `limit` and `offset`."
[enrolled? search limit offset]
[enrolled? :- :boolean
search :- [:maybe :string]
limit :- [:maybe ms/PositiveInt]
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
(t2/select :model/User
(cond-> {:select (cond-> list-columns enrolled? (into enrolled-at-select))
:where (user-list-where enrolled? search)
@@ -121,7 +141,8 @@
;; a nil limit would emit `LIMIT NULL`
limit (assoc :limit limit :offset offset))))
(defn user-list-count
(mu/defn user-list-count
"The number of enrolled or unenrolled Users matching `search`."
[enrolled? search]
[enrolled? :- :boolean
search :- [:maybe :string]]
(t2/count :model/User {:where (user-list-where enrolled? search)}))

View File

@@ -2,17 +2,19 @@
"Application database queries for the permission-debug module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.permissions.core :as perms]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn user-superuser?
(mu/defn user-superuser?
"Whether the User with `user-id` is a superuser."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-fn :is_superuser :model/User :id user-id))
(defn card
(mu/defn card
"The Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one :model/Card :id card-id))
(defn- blocked-tables-select
@@ -31,14 +33,26 @@
{:user-id user-id :is-superuser? false}
permissions-granting)]]]})
(defn blocked-tables-in-database
(mu/defn blocked-tables-in-database
"The `[db-name schema table-name group-name]` rows of every Table in the Database with `database-id` blocked (per
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`)."
[user-id database-id permissions-blocking permissions-granting]
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`).
`permissions-blocking`/`permissions-granting` are plain permission-type -> permission-value (or
`[value :most|:least]`) mappings, per `perms/PermissionMapping`; db.clj builds the Honey SQL from them."
[user-id :- ::lib.schema.id/user
database-id :- ::lib.schema.id/database
permissions-blocking :- perms/PermissionMapping
permissions-granting :- perms/PermissionMapping]
(t2/query (blocked-tables-select user-id [:= :blocked.db_id database-id] permissions-blocking permissions-granting)))
(defn blocked-tables-among
(mu/defn blocked-tables-among
"The `[db-name schema table-name group-name]` rows of the Tables with `table-ids` blocked (per
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`)."
[user-id table-ids permissions-blocking permissions-granting]
`permissions-blocking`) for the User with `user-id`, excluding those granted (per `permissions-granting`).
`permissions-blocking`/`permissions-granting` are plain permission-type -> permission-value (or
`[value :most|:least]`) mappings, per `perms/PermissionMapping`; db.clj builds the Honey SQL from them."
[user-id :- ::lib.schema.id/user
table-ids :- [:set ::lib.schema.id/table]
permissions-blocking :- perms/PermissionMapping
permissions-granting :- perms/PermissionMapping]
(t2/query (blocked-tables-select user-id [:in :blocked.id table-ids] permissions-blocking permissions-granting)))

View File

@@ -2,25 +2,62 @@
"Application database queries for the remote-sync module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself."
(:require
[metabase-enterprise.remote-sync.schema :as remote-sync.schema]
[metabase.collections.core :as collections]
[metabase.collections.schema :as collections.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn eligible-children
(def ^:private Conditions
"A map of column to value (possibly nil) or Toucan 2 operator-vector value, or nil for none."
[:maybe [:map-of :keyword [:maybe [:or :string :int :boolean :keyword sequential?]]]])
(def ^:private RemovalOpts
"The `:scope-key`, `:synced-collection-ids`, `:entity-ids`, and `:removal-conditions` describing which rows an
import removes (see [[removal-exprs]])."
[:map {:closed true}
[:scope-key [:maybe :keyword]]
[:synced-collection-ids [:maybe [:or [:set ::lib.schema.id/collection] [:sequential ::lib.schema.id/collection]]]]
[:entity-ids [:maybe [:or [:set :string] [:sequential :string]]]]
[:removal-conditions Conditions]])
(def ^:private ModelId
"A `:model_id`: the primary key of the referenced entity, or the `-1` sentinel
(`metabase-enterprise.remote-sync.settings/transforms-root-id`) standing in for the virtual Transforms root
Collection."
[:or ms/PositiveInt [:= -1]])
(def ^:private Path
"A `{:db_name :schema :table_name :field_name}` path used to locate a Table or Field."
[:map
[:db_name :string]
[:schema {:optional true} [:maybe :string]]
[:table_name :string]
[:field_name {:optional true} :string]])
(mu/defn eligible-children
"The instances of `model-key` whose `fk` column equals `model-id`, additionally matching `conditions` (a map of
column to value or Toucan 2 operator-vector value, or nil for none)."
[model-key fk model-id conditions]
[model-key :- :keyword
fk :- :keyword
model-id :- ms/PositiveInt
conditions :- Conditions]
(apply t2/select model-key fk model-id (mapcat identity conditions)))
(defn ids-where
(mu/defn ids-where
"The IDs of the instances of `model-key` matching `conditions` (a map of column to value or Toucan 2
operator-vector value, or nil for every instance)."
[model-key conditions]
[model-key :- :keyword
conditions :- Conditions]
(apply t2/select-fn-set :id model-key (mapcat identity conditions)))
(defn count-where
(mu/defn count-where
"The number of instances of `model-key` matching `conditions` (a map of column to value or Toucan 2
operator-vector value, or nil for every instance)."
[model-key conditions]
[model-key :- :keyword
conditions :- Conditions]
(apply t2/count model-key (mapcat identity conditions)))
(defn- removal-condition-exprs
@@ -45,10 +82,11 @@
(seq entity-ids) (conj [:not-in :entity_id entity-ids])
:always (into (removal-condition-exprs removal-conditions)))))
(defn delete-removed-instances!
(mu/defn delete-removed-instances!
"Deletes the `model-key` rows an import removes (see [[removal-exprs]]); a no-op for a scoped model with no
synced collections, and a delete of every row when nothing restricts it."
[model-key removal-opts]
[model-key :- :keyword
removal-opts :- RemovalOpts]
(when-let [exprs (removal-exprs removal-opts)]
(if (seq exprs)
(t2/delete! model-key {:where (if (= 1 (count exprs)) (first exprs) (into [:and] exprs))})
@@ -71,50 +109,64 @@
(let [id-column (keyword (str (name (t2/table-name model-key)) ".id"))]
(into [:and (unsynced-anti-join-expr model-type id-column)] (removal-exprs removal-opts))))
(defn unsynced-instance-count
(mu/defn unsynced-instance-count
"The number of `model-key` rows [[delete-removed-instances!]] would remove (see [[removal-exprs]]) that also
have no RemoteSyncObject of `model-type` in synced status — unsynced local work an import would otherwise wipe
out."
[model-key model-type removal-opts]
[model-key :- :keyword
model-type :- :string
removal-opts :- RemovalOpts]
(t2/count model-key {:where (unsynced-instance-expr model-key model-type removal-opts)}))
(defn unsynced-instance-names
(mu/defn unsynced-instance-names
"Up to `limit` names of the rows [[unsynced-instance-count]] counts."
[model-key model-type removal-opts limit]
[model-key :- :keyword
model-type :- :string
removal-opts :- RemovalOpts
limit :- ms/PositiveInt]
(t2/select-fn-vec :name model-key {:where (unsynced-instance-expr model-key model-type removal-opts)
:limit limit}))
(defn instance
(mu/defn instance
"The instance of `model` with `id`, or nil."
[model id]
[model :- :keyword
id :- ms/PositiveInt]
(t2/select-one model :id id))
(defn instance-with-columns
(mu/defn instance-with-columns
"The `columns` of the instance of `model` with `id`, or nil."
[model columns id]
[model :- :keyword
columns :- [:sequential :keyword]
id :- ms/PositiveInt]
(t2/select-one (into [model] columns) :id id))
(defn instance-names
(mu/defn instance-names
"The `:id` and `:name` of the instances of `model` with `ids`."
[model ids]
[model :- :keyword
ids :- [:sequential ms/PositiveInt]]
(t2/select [model :id :name] :id [:in ids]))
(defn instances-in-collections
(mu/defn instances-in-collections
"The instances of `model` in the Collections with `collection-ids`, excluding those archived under the optional
`archived-key` column."
[model collection-ids archived-key]
[model :- :keyword
collection-ids :- [:sequential ::lib.schema.id/collection]
archived-key :- [:maybe :keyword]]
(t2/select model {:where [:and
[:in :collection_id collection-ids]
(when archived-key [:= archived-key false])]}))
(defn instances-with-columns-by-entity-ids
(mu/defn instances-with-columns-by-entity-ids
"The `columns` of the instances of `model` with `entity-ids`."
[model columns entity-ids]
[model :- :keyword
columns :- [:sequential :keyword]
entity-ids :- [:set :string]]
(t2/select (into [model] columns) :entity_id [:in entity-ids]))
(defn delete-instances!
(mu/defn delete-instances!
"Delete the instances of `model` with `ids`."
[model ids]
[model :- :keyword
ids :- [:sequential ms/PositiveInt]]
(t2/delete! model :id [:in ids]))
(defn- tracking-select-parts
@@ -136,17 +188,19 @@
:from [[:measure :s]]
:join [[:metabase_table :t] [:= :s.table_id :t.id]]}))
(defn tracking-details-by-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
`model-id`, or nil."
[model-key model-id]
[model-key :- :keyword
model-id :- ms/PositiveInt]
(let [{:keys [alias select from join]} (tracking-select-parts model-key)]
(first (t2/query {:select select :from from :join join :where [:= (keyword alias "id") model-id]}))))
(defn tracking-details-by-entity-ids
(mu/defn tracking-details-by-entity-ids
"The `:id`, name, table id, collection id, and table name of the `model-key` (Segment or Measure) instances with
`entity-ids`."
[model-key entity-ids]
[model-key :- :keyword
entity-ids :- [:or [:set :string] [:sequential :string]]]
(let [{:keys [alias select from join]} (tracking-select-parts model-key)
id-column (keyword alias "id")]
(t2/query {:select (into [id-column] select)
@@ -154,24 +208,28 @@
:join join
:where [:in (keyword alias "entity_id") entity-ids]})))
(defn entity-id
(mu/defn entity-id
"The entity ID of the instance of `model` with `id`."
[model id]
[model :- :keyword
id :- ms/PositiveInt]
(t2/select-one-fn :entity_id model :id id))
(defn entity-ids-by-id
(mu/defn entity-ids-by-id
"A map of ID to entity ID for the instances of `model` with `ids`."
[model ids]
[model :- :keyword
ids :- [:sequential ms/PositiveInt]]
(t2/select-pk->fn :entity_id model :id [:in ids]))
(defn existing-entity-ids
(mu/defn existing-entity-ids
"The subset of `entity-ids` that instances of `model` have."
[model entity-ids]
[model :- :keyword
entity-ids :- [:or [:set :string] [:sequential :string]]]
(t2/select-fn-set :entity_id model :entity_id [:in entity-ids]))
(defn ids-by-entity-ids
(mu/defn ids-by-entity-ids
"The IDs of the instances of `model` with `entity-ids`."
[model entity-ids]
[model :- :keyword
entity-ids :- [:set :string]]
(t2/select-pks-vec model :entity_id [:in entity-ids]))
(defn- path-expr
@@ -188,35 +246,35 @@
(and has-field? field_name)
(conj [:= :f.name field_name]))))))
(defn tables-at-paths
(mu/defn tables-at-paths
"The `:id`, `:name`, and `:collection_id` rows of the Tables at `paths` (`{:db_name :schema :table_name}`)."
[paths]
[paths :- [:sequential Path]]
(t2/query {:select [:t.id :t.name :t.collection_id]
:from [[:metabase_table :t]]
:join [[:metabase_database :db] [:= :db.id :t.db_id]]
:where (path-expr paths false)}))
(defn fields-at-paths
(mu/defn fields-at-paths
"The `:id`, `:name`, `:table_id`, `:collection_id`, and `:table_name` rows of the Fields at `paths`
(`{:db_name :schema :table_name :field_name}`)."
[paths]
[paths :- [:sequential Path]]
(t2/query {:select [:f.id :f.name :f.table_id [:t.collection_id :collection_id] [:t.name :table_name]]
:from [[:metabase_field :f]]
:join [[:metabase_table :t] [:= :t.id :f.table_id]
[:metabase_database :db] [:= :db.id :t.db_id]]
:where (path-expr paths true)}))
(defn card-types
(mu/defn card-types
"The `:id`, `:type`, and `:card_schema` of the Cards with `card-ids`."
[card-ids]
[card-ids :- [:sequential ::lib.schema.id/card]]
(t2/select [:model/Card :id :type :card_schema] :id [:in card-ids]))
(defn field-user-settings-exist?
(mu/defn field-user-settings-exist?
"Whether the Field with `field-id` has FieldUserSettings."
[field-id]
[field-id :- ::lib.schema.id/field]
(t2/exists? :model/FieldUserSettings :field_id field-id))
(defn snippets
(mu/defn snippets
"The `:id`, `:name`, and `:collection_id` of every NativeQuerySnippet."
[]
(t2/select [:model/NativeQuerySnippet :id :name :collection_id]))
@@ -228,57 +286,57 @@
(for [collection collections]
[:like :location (str (collections/location-path collection) "%")])))
(defn collections
(mu/defn collections
"The Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select :model/Collection :id [:in collection-ids]))
(defn collections-by-id
(mu/defn collections-by-id
"A map of ID to the ID, name, location, and personal owner of the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/select-pk->fn identity [:model/Collection :id :name :location :personal_owner_id] :id [:in collection-ids]))
(defn collection-sync-states
(mu/defn collection-sync-states
"The `:id` and `:is_remote_synced` of the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select [:model/Collection :id :is_remote_synced] :id [:in collection-ids]))
(defn collection-name-and-id
(mu/defn collection-name-and-id
"The `:name` and `:collection_id` (its own ID) of the Collection with `collection-id`, or nil."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/select-one [:model/Collection :name [:id :collection_id]] :id collection-id))
(defn library-collection
(mu/defn library-collection
"The Library Collection of `library-type`, or nil."
[library-type]
[library-type :- :string]
(t2/select-one :model/Collection :type library-type))
(defn snippet-collections
(mu/defn snippet-collections
"The `:id` and `:name` of the Collections of the snippets namespace."
[]
(t2/select [:model/Collection :id :name] :namespace "snippets"))
(defn snippet-collection-ids
(mu/defn snippet-collection-ids
"The IDs of the Collections of the snippets namespace."
[]
(t2/select-pks-set :model/Collection :namespace "snippets"))
(defn collections-in-namespace
(mu/defn collections-in-namespace
"The `:id` and `:entity_id` of the Collections of `namespace-name`."
[namespace-name]
[namespace-name :- :string]
(t2/select [:model/Collection :id :entity_id] :namespace namespace-name))
(defn collection-ids-in-namespace
(mu/defn collection-ids-in-namespace
"The IDs of the Collections of `namespace-name`."
[namespace-name]
[namespace-name :- :string]
(t2/select-pks-vec :model/Collection :namespace namespace-name))
(defn remote-synced-collection-ids
(mu/defn remote-synced-collection-ids
"The IDs of the remote-synced Collections."
[]
(t2/select-pks-vec :model/Collection :is_remote_synced true))
(defn unarchived-remote-synced-root-collection-ids
(mu/defn unarchived-remote-synced-root-collection-ids
"The IDs of the unarchived remote-synced root Collections."
[]
(t2/select-fn-set :id :model/Collection
@@ -287,40 +345,40 @@
[:= :location "/"]
[:not :archived]]}))
(defn unarchived-root-collection-ids-in-namespace
(mu/defn unarchived-root-collection-ids-in-namespace
"The IDs of the unarchived root Collections of `namespace-name`."
[namespace-name]
[namespace-name :- :string]
(t2/select-fn-set :id :model/Collection
{:where [:and
[:= :namespace namespace-name]
[:= :location "/"]
[:not :archived]]}))
(defn subtree-collection-ids
(mu/defn subtree-collection-ids
"The IDs of `collections` and all of their descendants."
[collections]
[collections :- [:sequential ::collections.schema/collection]]
(t2/select-pks-set :model/Collection {:where (subtree-expr collections)}))
(defn remote-synced-subtree-collection-ids
(mu/defn remote-synced-subtree-collection-ids
"The IDs of the remote-synced Collections among `collections` and their descendants."
[collections]
[collections :- [:sequential ::collections.schema/collection]]
(t2/select-pks-set :model/Collection
{:where [:and
[:= :is_remote_synced true]
(subtree-expr collections)]}))
(defn mark-subtree-remote-synced!
(mu/defn mark-subtree-remote-synced!
"Mark `collections` and all of their descendants as remote-synced."
[collections]
[collections :- [:sequential ::collections.schema/collection]]
(t2/query {:update (t2/table-name :model/Collection)
:set {:is_remote_synced true}
:where [:and
[:= :is_remote_synced false]
(subtree-expr collections)]}))
(defn unmark-collections-remote-synced!
(mu/defn unmark-collections-remote-synced!
"Mark the Collections with `collection-ids` as not remote-synced."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/query {:update (t2/table-name :model/Collection)
:set {:is_remote_synced false}
:where [:in :id collection-ids]}))
@@ -339,137 +397,151 @@
[:and [:= :model_type model_type] [:= :model_id model_id]]))
rows))
(defn rso
(mu/defn rso
"The RemoteSyncObject of the entity `model-type` `model-id`, or nil."
[model-type model-id]
[model-type :- :string
model-id :- ModelId]
(t2/select-one :model/RemoteSyncObject :model_type model-type :model_id model-id))
(defn lock-rso
(mu/defn lock-rso
"The RemoteSyncObject of the entity `model-type` `model-id`, locked for update, or nil."
[model-type model-id]
[model-type :- :string
model-id :- ModelId]
(t2/select-one :model/RemoteSyncObject
{:where [:and [:= :model_type model-type] [:= :model_id model-id]]
:for :update}))
(defn rso-by-file-path
(mu/defn rso-by-file-path
"The RemoteSyncObject at `file-path`, or nil."
[file-path]
[file-path :- :string]
(t2/select-one :model/RemoteSyncObject :file_path file-path))
(defn rso-exists?
(mu/defn rso-exists?
"Whether the entity `model-type` `model-id` has a RemoteSyncObject."
[model-type model-id]
[model-type :- :string
model-id :- ModelId]
(t2/exists? :model/RemoteSyncObject :model_type model-type :model_id model-id))
(defn rso-of-type-exists?
(mu/defn rso-of-type-exists?
"Whether any entity of `model-type` has a RemoteSyncObject."
[model-type]
[model-type :- :string]
(t2/exists? :model/RemoteSyncObject :model_type model-type))
(defn rso-count-of-type
(mu/defn rso-count-of-type
"The number of RemoteSyncObjects of `model-type`."
[model-type]
[model-type :- :string]
(t2/count :model/RemoteSyncObject :model_type model-type))
(defn rso-keys
(mu/defn rso-keys
"The `:id`, `:model_type`, and `:model_id` of every RemoteSyncObject."
[]
(t2/select [:model/RemoteSyncObject :id :model_type :model_id]))
(defn departed-rso-keys
(mu/defn departed-rso-keys
"The `:id`, `:model_type`, and `:model_id` of the RemoteSyncObjects pending removal or deletion."
[]
(t2/select [:model/RemoteSyncObject :id :model_type :model_id] :status [:in ["removed" "delete"]]))
(defn all-rso-ids
(mu/defn all-rso-ids
"The IDs of every RemoteSyncObject."
[]
(t2/select-pks-set :model/RemoteSyncObject))
(defn unsynced-rsos
(mu/defn unsynced-rsos
"The RemoteSyncObjects whose status is not synced."
[]
(t2/select :model/RemoteSyncObject {:where [:not= :status "synced"]}))
(defn dirty-rso-exists?
(mu/defn dirty-rso-exists?
"Whether a RemoteSyncObject of a model type other than `excluded-model-types` is not synced."
[excluded-model-types]
[excluded-model-types :- [:set :string]]
(t2/exists? :model/RemoteSyncObject
{:where [:and
[:not= :status "synced"]
(when (seq excluded-model-types)
[:not-in :model_type excluded-model-types])]}))
(defn dirty-rsos
(mu/defn dirty-rsos
"The RemoteSyncObjects of model types other than `excluded-model-types` that are not synced."
[excluded-model-types]
[excluded-model-types :- [:set :string]]
(t2/select :model/RemoteSyncObject
{:where [:and
[:not= :status "synced"]
(when (seq excluded-model-types)
[:not-in :model_type excluded-model-types])]}))
(defn tracked-model-ids
(mu/defn tracked-model-ids
"The model IDs of the RemoteSyncObjects of `model-type`."
[model-type]
[model-type :- :string]
(t2/select-fn-set :model_id :model/RemoteSyncObject :model_type model-type))
(defn rsos-of-models
(mu/defn rsos-of-models
"The RemoteSyncObjects of the entities of `model-type` with `model-ids`."
[model-type model-ids]
[model-type :- :string
model-ids :- [:sequential ms/PositiveInt]]
(t2/select :model/RemoteSyncObject :model_type model-type :model_id [:in model-ids]))
(defn active-child-rsos
(mu/defn active-child-rsos
"The RemoteSyncObjects of `model-type` under the Table with `table-id` that are not pending removal or deletion."
[model-type table-id]
[model-type :- :string
table-id :- ::lib.schema.id/table]
(t2/select :model/RemoteSyncObject
:model_type model-type
:model_table_id table-id
:status [:not-in ["removed" "delete"]]))
(defn content-rso-statuses
(mu/defn content-rso-statuses
"The `:id` and `:status` of the RemoteSyncObjects of the Collections with `collection-ids` and their contents."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/select [:model/RemoteSyncObject :id :status] {:where (contents-rso-expr collection-ids)}))
(defn removed-content-rso-ids
(mu/defn removed-content-rso-ids
"The IDs of the RemoteSyncObjects pending removal among those of the Collections with `collection-ids` and their
contents."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select-pks-set :model/RemoteSyncObject
{:where [:and
[:= :status "removed"]
(contents-rso-expr collection-ids)]}))
(defn insert-rso!
(mu/defn insert-rso!
"Insert the RemoteSyncObject `row`."
[row]
[row :- ::remote-sync.schema/remote-sync-object.update]
(t2/insert! :model/RemoteSyncObject row))
(defn insert-rsos!
(mu/defn insert-rsos!
"Insert the RemoteSyncObject `rows`."
[rows]
[rows :- [:sequential ::remote-sync.schema/remote-sync-object.update]]
(t2/insert! :model/RemoteSyncObject rows))
(defn update-rso!
(mu/defn update-rso!
"Apply `changes` to the RemoteSyncObject with `rso-id`."
[rso-id changes]
[rso-id :- ms/PositiveInt
changes :- ::remote-sync.schema/remote-sync-object.update]
(t2/update! :model/RemoteSyncObject rso-id changes))
(defn set-rsos-status!
(mu/defn set-rsos-status!
"Set the status of the RemoteSyncObjects with `rso-ids` to `status` as of `timestamp`."
[rso-ids status timestamp]
[rso-ids :- [:sequential ms/PositiveInt]
status :- :string
timestamp :- ms/TemporalInstant]
(t2/update! :model/RemoteSyncObject :id [:in rso-ids] {:status status :status_changed_at timestamp}))
(defn mark-all-rsos-synced!
(mu/defn mark-all-rsos-synced!
"Mark every RemoteSyncObject as synced as of `timestamp`."
[timestamp]
[timestamp :- ms/TemporalInstant]
(t2/update! :model/RemoteSyncObject {:status "synced" :status_changed_at timestamp}))
(defn mark-rsos-synced!
(mu/defn mark-rsos-synced!
"Mark the RemoteSyncObjects with `rso-ids` as synced as of `timestamp`, writing the `:file_path` and
`:content_hash` of those in `metadata-by-id` and keeping the existing values of the others."
[rso-ids metadata-by-id timestamp]
[rso-ids :- [:sequential ms/PositiveInt]
metadata-by-id :- [:map-of ms/PositiveInt
[:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:file_path {:optional true} [:maybe :string]]
[:content_hash {:optional true} [:maybe :string]]]]
timestamp :- ms/TemporalInstant]
(t2/update! :model/RemoteSyncObject
{:id [:in (vec rso-ids)]}
(cond-> {:status "synced" :status_changed_at timestamp}
@@ -487,59 +559,61 @@
metadata-by-id)
[:else :content_hash]))))))
(defn delete-rso!
(mu/defn delete-rso!
"Delete the RemoteSyncObject with `rso-id`."
[rso-id]
[rso-id :- ms/PositiveInt]
(t2/delete! :model/RemoteSyncObject rso-id))
(defn delete-rsos!
(mu/defn delete-rsos!
"Delete the RemoteSyncObjects with `rso-ids`."
[rso-ids]
[rso-ids :- [:sequential ms/PositiveInt]]
(t2/delete! :model/RemoteSyncObject :id [:in rso-ids]))
(defn delete-rso-of!
(mu/defn delete-rso-of!
"Delete the RemoteSyncObject of the entity `model-type` `model-id`."
[model-type model-id]
[model-type :- :string
model-id :- ModelId]
(t2/delete! :model/RemoteSyncObject :model_type model-type :model_id model-id))
(defn delete-rsos-of-type!
(mu/defn delete-rsos-of-type!
"Delete the RemoteSyncObjects of `model-type`."
[model-type]
[model-type :- :string]
(t2/delete! :model/RemoteSyncObject :model_type model-type))
(defn delete-rsos-of-models!
(mu/defn delete-rsos-of-models!
"Delete the RemoteSyncObjects of the entities of `model-type` with `model-ids`."
[model-type model-ids]
[model-type :- :string
model-ids :- [:set ms/PositiveInt]]
(t2/delete! :model/RemoteSyncObject :model_type model-type :model_id [:in model-ids]))
(defn delete-rsos-of-keys!
"Delete the RemoteSyncObjects of the `[{:model_type :model_id}]` `rows`."
[rows]
(mu/defn delete-rsos-of-keys!
"Delete the RemoteSyncObjects keyed by the `:model_type`/`:model_id` of `rows` (other keys are ignored)."
[rows :- [:sequential [:map [:model_type :string] [:model_id ms/PositiveInt]]]]
(t2/delete! :model/RemoteSyncObject {:where (rso-keys-expr rows)}))
(defn delete-all-rsos!
(mu/defn delete-all-rsos!
"Delete every RemoteSyncObject."
[]
(t2/delete! :model/RemoteSyncObject))
(defn task
(mu/defn task
"The RemoteSyncTask with `task-id`, or nil."
[task-id]
[task-id :- ms/PositiveInt]
(t2/select-one :model/RemoteSyncTask task-id))
(defn lock-task
(mu/defn lock-task
"The RemoteSyncTask with `task-id`, locked for update, or nil."
[task-id]
[task-id :- ms/PositiveInt]
(t2/select-one :model/RemoteSyncTask :id task-id {:for :update}))
(defn task-cancelled?
(mu/defn task-cancelled?
"The cancelled flag of the RemoteSyncTask with `task-id`."
[task-id]
[task-id :- ms/PositiveInt]
(t2/select-one-fn :cancelled :model/RemoteSyncTask :id task-id))
(defn current-task
(mu/defn current-task
"The newest started, unfinished RemoteSyncTask that reported progress after `progress-cutoff`, or nil."
[progress-cutoff]
[progress-cutoff :- ms/TemporalInstant]
(t2/select-one :model/RemoteSyncTask
{:where [:and
[:<> :started_at nil]
@@ -549,7 +623,7 @@
:order-by [[:started_at :desc]
[:id :desc]]}))
(defn most-recent-task
(mu/defn most-recent-task
"The newest started RemoteSyncTask, or nil."
[]
(t2/select-one :model/RemoteSyncTask
@@ -559,7 +633,7 @@
:order-by [[:started_at :desc]
[:id :desc]]}))
(defn last-successful-task
(mu/defn last-successful-task
"The newest finished RemoteSyncTask that was neither cancelled nor failed and recorded a version, or nil."
[]
(t2/select-one :model/RemoteSyncTask
@@ -572,34 +646,47 @@
:order-by [[:started_at :desc]
[:id :desc]]}))
(defn insert-task!
(mu/defn insert-task!
"Insert `task` and return the new instance."
[task]
[task :- ::remote-sync.schema/remote-sync-task.update]
(t2/insert-returning-instance! :model/RemoteSyncTask task))
(defn update-task!
(mu/defn update-task!
"Apply `changes` to the RemoteSyncTask with `task-id`."
[task-id changes]
[task-id :- ms/PositiveInt
changes :- ::remote-sync.schema/remote-sync-task.update]
(t2/update! :model/RemoteSyncTask task-id changes))
(defn supersede-stale-tasks!
"Cancel and end at `now` the started, unfinished RemoteSyncTasks that last reported progress before `cutoff`."
[cutoff now]
(mu/defn end-task!
"Apply `changes` to the RemoteSyncTask with `task-id` and mark it ended now."
[task-id :- ms/PositiveInt
changes :- ::remote-sync.schema/remote-sync-task.update]
(t2/update! :model/RemoteSyncTask task-id (assoc changes :ended_at :%now)))
(mu/defn report-task-progress!
"Record `progress` for the RemoteSyncTask with `task-id`, stamping the progress report time."
[task-id :- ms/PositiveInt
progress :- number?]
(t2/update! :model/RemoteSyncTask task-id {:progress progress, :last_progress_report_at :%now}))
(mu/defn supersede-stale-tasks!
"Cancel and end now the started, unfinished RemoteSyncTasks that last reported progress before `cutoff`."
[cutoff :- ms/TemporalInstant]
(t2/query {:update (t2/table-name :model/RemoteSyncTask)
:set {:cancelled true
:ended_at now
:ended_at :%now
:error_message "Superseded after staleness timeout"}
:where [:and
[:<> :started_at nil]
[:= :ended_at nil]
[:< :last_progress_report_at cutoff]]}))
(defn delete-tasks-started-before!
(mu/defn delete-tasks-started-before!
"Delete the RemoteSyncTasks started before `cutoff`, returning the number deleted."
[cutoff]
[cutoff :- ms/TemporalInstant]
(t2/delete! :model/RemoteSyncTask {:where [:< :started_at cutoff]}))
(defn users-by-id
(mu/defn users-by-id
"A map of User ID to User for `user-ids`."
[user-ids]
[user-ids :- [:sequential [:maybe ::lib.schema.id/user]]]
(t2/select-pk->fn identity :model/User :id [:in user-ids]))

View File

@@ -31,7 +31,7 @@
[task]
(when-let [existing (current-task)]
(throw (ex-info "A running task exists" {:existing-task existing})))
task)
(merge {:started_at (mi/now)} task))
;;; ------------------------------------------- Helper Functions -------------------------------------------
@@ -49,8 +49,7 @@
[additional-fields :- [:map]]]
(remote-sync.db/insert-task! (merge {:sync_task_type sync-task-type
:initiated_by user-id
:progress 0
:started_at (mi/now)}
:progress 0}
additional-fields)))
(defn cancel-sync-task!
@@ -63,10 +62,9 @@
This signal will be checked in update-progress! to stop further processing. Note that the worker thread must
manually check this flag rather than being interrupted, as interrupting Quartz threads can cause issues."
[task-id]
(remote-sync.db/update-task! task-id
{:cancelled true
:ended_at (mi/now)
:error_message "Task cancelled"}))
(remote-sync.db/end-task! task-id
{:cancelled true
:error_message "Task cancelled"}))
(defn update-progress!
"Updates the progress of a sync task.
@@ -80,9 +78,7 @@
(when (true? (remote-sync.db/task-cancelled? task-id))
(throw (ex-info "Remote sync task has been cancelled" {:task-id task-id
:cancelled? true})))
(remote-sync.db/update-task! task-id
{:progress progress
:last_progress_report_at (mi/now)}))
(remote-sync.db/report-task-progress! task-id progress))
(def ^:private default-progress-throttle-ms
"Minimum ms between throttled (non-boundary) progress writes."
@@ -136,10 +132,9 @@
Returns the number of rows updated (should be 1 if successful)."
([task-id] (complete-sync-task! task-id nil))
([task-id outcome]
(remote-sync.db/update-task! task-id
{:progress 1.0
:ended_at (mi/now)
:outcome outcome})))
(remote-sync.db/end-task! task-id
{:progress 1.0
:outcome outcome})))
(defn fail-sync-task!
"Marks a sync task as failed.
@@ -148,9 +143,8 @@
Returns the number of rows updated (should be 1 if successful)."
[task-id error-msg]
(remote-sync.db/update-task! task-id
{:ended_at (mi/now)
:error_message error-msg}))
(remote-sync.db/end-task! task-id
{:error_message error-msg}))
(defn current-task
"Gets the current active sync task.
@@ -177,7 +171,7 @@
[]
(let [cutoff (t/minus (t/offset-date-time)
(t/millis (setting/get :remote-sync-task-time-limit-ms)))]
(remote-sync.db/supersede-stale-tasks! cutoff (mi/now))))
(remote-sync.db/supersede-stale-tasks! cutoff)))
(defn most-recent-task
"Gets the most recently run task, including currently running tasks.
@@ -265,9 +259,8 @@
Returns the number of rows updated (should be 1 if successful)."
[task-id conflicts]
(remote-sync.db/update-task! task-id
{:ended_at (mi/now)
:conflicts conflicts}))
(remote-sync.db/end-task! task-id
{:conflicts conflicts}))
;;; ------------------------------------------- Hydration -------------------------------------------

View File

@@ -1,5 +1,9 @@
(ns metabase-enterprise.remote-sync.schema
"Malli schemas for remote sync API request and response bodies.")
"Malli schemas for remote sync API request and response bodies."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
;;; ------------------------------------------- Task Schemas -------------------------------------------
@@ -164,3 +168,69 @@
"Schema for POST /test-connection response."
[:map
[:status [:= :success]]])
(mr/def ::remote-sync-object
"A RemoteSyncObject as selected from the app DB: every column of `:remote_sync_object`."
[:map {:closed true}
[:id ms/PositiveInt]
[:model_type [:or :keyword :string]]
[:model_id [:maybe :int]]
[:status [:or :keyword :string]]
[:status_changed_at ms/TemporalInstant]
[:model_name :string]
[:model_collection_id [:maybe ::lib.schema.id/collection]]
[:model_display [:maybe [:or :keyword :string]]]
[:model_table_id [:maybe ::lib.schema.id/table]]
[:model_table_name [:maybe :string]]
[:file_path [:maybe :string]]
[:content_hash [:maybe :string]]])
(mr/def ::remote-sync-object.update
"What an update (or insert) of a RemoteSyncObject accepts: every column of `:remote_sync_object` except `id`, all optional."
[:map {:closed true}
[:model_type {:optional true} [:maybe [:or :keyword :string]]]
[:model_id {:optional true} [:maybe :int]]
[:status {:optional true} [:maybe [:or :keyword :string]]]
[:status_changed_at {:optional true} [:maybe ms/TemporalInstant]]
[:model_name {:optional true} [:maybe :string]]
[:model_collection_id {:optional true} [:maybe ::lib.schema.id/collection]]
[:model_display {:optional true} [:maybe [:or :keyword :string]]]
[:model_table_id {:optional true} [:maybe ::lib.schema.id/table]]
[:model_table_name {:optional true} [:maybe :string]]
[:file_path {:optional true} [:maybe :string]]
[:content_hash {:optional true} [:maybe :string]]])
(mr/def ::remote-sync-task.outcome
"The `:outcome` column of a RemoteSyncTask, decoded."
:map)
(mr/def ::remote-sync-task
"A RemoteSyncTask as selected from the app DB: every column of `:remote_sync_task`."
[:map {:closed true}
[:id ms/PositiveInt]
[:sync_task_type [:or :keyword :string]]
[:progress [:maybe number?]]
[:cancelled :boolean]
[:started_at ms/TemporalInstant]
[:ended_at [:maybe ms/TemporalInstant]]
[:last_progress_report_at ms/TemporalInstant]
[:initiated_by [:maybe ::lib.schema.id/user]]
[:error_message [:maybe :string]]
[:version [:maybe :string]]
[:conflicts [:maybe [:sequential :string]]]
[:outcome [:maybe ::remote-sync-task.outcome]]])
(mr/def ::remote-sync-task.update
"What an update (or insert) of a RemoteSyncTask accepts: every column of `:remote_sync_task` except `id`, all optional."
[:map {:closed true}
[:sync_task_type {:optional true} [:maybe [:or :keyword :string]]]
[:progress {:optional true} [:maybe number?]]
[:cancelled {:optional true} [:maybe :boolean]]
[:started_at {:optional true} [:maybe ms/TemporalInstant]]
[:ended_at {:optional true} [:maybe ms/TemporalInstant]]
[:last_progress_report_at {:optional true} [:maybe ms/TemporalInstant]]
[:initiated_by {:optional true} [:maybe ::lib.schema.id/user]]
[:error_message {:optional true} [:maybe :string]]
[:version {:optional true} [:maybe :string]]
[:conflicts {:optional true} [:maybe [:sequential :string]]]
[:outcome {:optional true} [:maybe ::remote-sync-task.outcome]]])

View File

@@ -2,50 +2,63 @@
"Application database queries for the replacement module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions."
(:require
[metabase-enterprise.replacement.schema :as replacement.schema]
[metabase.app-db.core :as mdb]
[metabase.dashboards.schema :as dashboards.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.measures.schema :as measures.schema]
[metabase.queries.schema :as queries.schema]
[metabase.segments.schema :as segments.schema]
[metabase.transforms.schema :as transforms.schema]
[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]
[toucan2.core :as t2]))
(defn run
(mu/defn run
"The ReplacementRun with `run-id`, or nil."
[run-id]
[run-id :- ms/PositiveInt]
(t2/select-one :model/ReplacementRun :id run-id))
(defn runs
(mu/defn runs
"The ReplacementRuns, newest first, restricted to the `is-active` flag when given."
[is-active]
[is-active :- [:maybe :boolean]]
(t2/select :model/ReplacementRun
(cond-> {:order-by [[:start_time :desc]]}
(some? is-active) (assoc :where [:= :is_active is-active]))))
(defn active-run
(mu/defn active-run
"The active ReplacementRun, or nil."
[]
(t2/select-one :model/ReplacementRun :is_active true))
(defn run-active-flag
(mu/defn run-active-flag
"The `:is_active` row of the ReplacementRun with `run-id`, or nil."
[run-id]
[run-id :- ms/PositiveInt]
(t2/select-one [:model/ReplacementRun :is_active] :id run-id))
(defn insert-run!
(mu/defn insert-run!
"Insert `run` and return the new instance."
[run]
[run :- ::replacement.schema/replacement-run.update]
(t2/insert-returning-instance! :model/ReplacementRun run))
(defn update-run!
"Apply `changes` to the ReplacementRun with `run-id`."
[run-id changes]
(mu/defn update-run!
"Apply `changes` to the ReplacementRun with `run-id`, returning the number updated."
[run-id :- ms/PositiveInt
changes :- ::replacement.schema/replacement-run.update]
(t2/update! :model/ReplacementRun :id run-id changes))
(defn update-active-run!
"Apply `changes` to the ReplacementRun with `run-id` if it is active."
[run-id changes]
(mu/defn update-active-run!
"Apply `changes` to the ReplacementRun with `run-id` if it is active, returning the number updated."
[run-id :- ms/PositiveInt
changes :- ::replacement.schema/replacement-run.update]
(t2/update! :model/ReplacementRun :id run-id :is_active true changes))
(defn time-out-active-runs-older-than!
"Mark the active ReplacementRuns started more than `age` `unit`s ago as timed out."
[age unit]
(mu/defn time-out-active-runs-older-than!
"Mark the active ReplacementRuns started more than `age` `unit`s ago as timed out, returning the number updated."
[age :- ms/PositiveInt
unit :- :keyword]
(t2/update! :model/ReplacementRun
:is_active true
:start_time [:< (h2x/add-interval-honeysql-form (mdb/db-type) :%now (- age) unit)]
@@ -54,147 +67,155 @@
:end_time :%now
:message "Timed out by metabase"}))
(defn cards-with-ids
(mu/defn cards-with-ids
"The Cards with `ids`."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Card :id [:in ids]))
(defn tables-with-ids
(mu/defn tables-with-ids
"The Tables with `ids`."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Table :id [:in ids]))
(defn dashboards-with-ids
(mu/defn dashboards-with-ids
"The Dashboards with `ids`."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Dashboard :id [:in ids]))
(defn transforms-with-ids
(mu/defn transforms-with-ids
"The Transforms with `ids`."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Transform :id [:in ids]))
(defn segments-with-ids
(mu/defn segments-with-ids
"The Segments with `ids`."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Segment :id [:in ids]))
(defn measures-with-ids
(mu/defn measures-with-ids
"The Measures with `ids`."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Measure :id [:in ids]))
(defn card
(mu/defn card
"The Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one :model/Card :id card-id))
(defn cards-by-id
(mu/defn cards-by-id
"A map of Card ID to Card for `card-ids`."
[card-ids]
[card-ids :- [:set ::lib.schema.id/card]]
(t2/select-pk->fn identity :model/Card :id [:in card-ids]))
(defn card-database-id
(mu/defn card-database-id
"The Database ID of the Card with `card-id`."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one-fn :database_id :model/Card :id card-id))
(defn card-with-table-exists?
(mu/defn card-with-table-exists?
"Whether one of the Cards with `card-ids` is on the Table with `table-id`."
[card-ids table-id]
[card-ids :- [:set ::lib.schema.id/card]
table-id :- ::lib.schema.id/table]
(t2/exists? :model/Card :id [:in card-ids] :table_id table-id))
(defn update-card!
"Apply `changes` to the Card with `card-id`."
[card-id changes]
(mu/defn update-card!
"Apply `changes` to the Card with `card-id`, returning the number updated."
[card-id :- ::lib.schema.id/card
changes :- ::queries.schema/card.update]
(t2/update! :model/Card card-id changes))
(defn transform
(mu/defn transform
"The Transform with `transform-id`, or nil."
[transform-id]
[transform-id :- ::lib.schema.id/transform]
(t2/select-one :model/Transform :id transform-id))
(defn update-transform!
"Apply `changes` to the Transform with `transform-id`."
[transform-id changes]
(mu/defn update-transform!
"Apply `changes` to the Transform with `transform-id`, returning the number updated."
[transform-id :- ::lib.schema.id/transform
changes :- ::transforms.schema/transform.update]
(t2/update! :model/Transform transform-id changes))
(defn segment
(mu/defn segment
"The Segment with `segment-id`, or nil."
[segment-id]
[segment-id :- ::lib.schema.id/segment]
(t2/select-one :model/Segment :id segment-id))
(defn update-segment!
"Apply `changes` to the Segment with `segment-id`."
[segment-id changes]
(mu/defn update-segment!
"Apply `changes` to the Segment with `segment-id`, returning the number updated."
[segment-id :- ::lib.schema.id/segment
changes :- ::segments.schema/segment.update]
(t2/update! :model/Segment segment-id changes))
(defn measure
(mu/defn measure
"The Measure with `measure-id`, or nil."
[measure-id]
[measure-id :- ::lib.schema.id/measure]
(t2/select-one :model/Measure :id measure-id))
(defn update-measure!
"Apply `changes` to the Measure with `measure-id`."
[measure-id changes]
(mu/defn update-measure!
"Apply `changes` to the Measure with `measure-id`, returning the number updated."
[measure-id :- ::lib.schema.id/measure
changes :- ::measures.schema/measure.update]
(t2/update! :model/Measure measure-id changes))
(defn dashboard
(mu/defn dashboard
"The Dashboard with `dashboard-id`, or nil."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-one :model/Dashboard :id dashboard-id))
(defn update-dashboard!
"Apply `changes` to the Dashboard with `dashboard-id`."
[dashboard-id changes]
(mu/defn update-dashboard!
"Apply `changes` to the Dashboard with `dashboard-id`, returning the number updated."
[dashboard-id :- ::lib.schema.id/dashboard
changes :- ::dashboards.schema/dashboard.update]
(t2/update! :model/Dashboard dashboard-id changes))
(defn dashboard-cards
(mu/defn dashboard-cards
"The DashboardCards of the Dashboard with `dashboard-id`."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select :model/DashboardCard :dashboard_id dashboard-id))
(defn update-dashboard-card!
"Apply `changes` to the DashboardCard with `dashcard-id`."
[dashcard-id changes]
(mu/defn update-dashboard-card!
"Apply `changes` to the DashboardCard with `dashcard-id`, returning the number updated."
[dashcard-id :- ::lib.schema.id/dashcard
changes :- ::dashboards.schema/dashboard-card.update]
(t2/update! :model/DashboardCard dashcard-id changes))
(defn table-database-id
(mu/defn table-database-id
"The Database ID of the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one-fn :db_id :model/Table :id table-id))
(defn active-fields-of-table
(mu/defn active-fields-of-table
"The active Fields of the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select :model/Field :table_id table-id :active true))
(defn active-field-ids-of-table
(mu/defn active-field-ids-of-table
"The IDs of the active Fields of the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-pks-set :model/Field :table_id table-id :active true))
(defn active-fk-to-fields-exists?
(mu/defn active-fk-to-fields-exists?
"Whether an active Field points at one of the Fields with `field-ids`."
[field-ids]
[field-ids :- [:set ::lib.schema.id/field]]
(t2/exists? :model/Field :fk_target_field_id [:in field-ids] :active true))
(defn update-field!
"Apply `changes` to the Field with `field-id`."
[field-id changes]
(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))
(defn sandbox-exists-for-table?
(mu/defn sandbox-exists-for-table?
"Whether a Sandbox is defined on the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/exists? :model/Sandbox :table_id table-id))
(defn sandbox-card-ids
(mu/defn sandbox-card-ids
"The IDs of the Cards Sandboxes are built on."
[]
(t2/select-fn-set :card_id :model/Sandbox :card_id [:not= nil]))
(defn persisted-info-for-card
(mu/defn persisted-info-for-card
"The PersistedInfo of the Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one :model/PersistedInfo :card_id card-id))

View File

@@ -13,6 +13,16 @@
(derive :model/ReplacementRun :metabase/model)
(def ^:private terminal-statuses
#{:succeeded :failed :canceled :timeout})
(t2/define-before-update :model/ReplacementRun
[run]
(let [changes (t2/changes run)]
(cond-> run
(and (contains? terminal-statuses (keyword (:status changes))) (not (contains? changes :end_time)))
(assoc :end_time (mi/now)))))
(t2/deftransforms :model/ReplacementRun
{:status mi/transform-keyword
:source_entity_type mi/transform-keyword
@@ -48,8 +58,7 @@
(replacement.db/update-active-run! run-id
{:status :succeeded
:progress 1.0
:is_active nil
:end_time :%now}))
:is_active nil}))
(defn fail-run!
"Mark the active run as failed."
@@ -57,7 +66,6 @@
(replacement.db/update-active-run! run-id
{:status :failed
:is_active nil
:end_time :%now
:message message}))
(defn cancel-run!
@@ -66,7 +74,6 @@
(replacement.db/update-active-run! run-id
{:status :canceled
:is_active nil
:end_time :%now
:message "Canceled by user"}))
(defn timeout-old-runs!

View File

@@ -57,3 +57,34 @@
[:success :boolean]
[:errors {:optional true} [:sequential ::error-type]]
[:column_mappings {:optional true} [:sequential ::column-mapping]]])
(mr/def ::replacement-run
"A ReplacementRun as selected from the app DB: every column of `:source_replacement_run`."
[:map {:closed true}
[:id ms/PositiveInt]
[:source_entity_type [:or :keyword :string]]
[:source_entity_id ms/PositiveInt]
[:target_entity_type [:or :keyword :string]]
[:target_entity_id ms/PositiveInt]
[:status [:or :keyword :string]]
[:is_active [:maybe :boolean]]
[:progress [:maybe number?]]
[:message [:maybe :string]]
[:user_id [:maybe ::lib.schema.id/user]]
[:start_time ms/TemporalInstant]
[:end_time [:maybe ms/TemporalInstant]]])
(mr/def ::replacement-run.update
"What an update (or insert) of a ReplacementRun accepts: every column of `:source_replacement_run` except `id`, all optional."
[:map {:closed true}
[:source_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:source_entity_id {:optional true} [:maybe ms/PositiveInt]]
[:target_entity_type {:optional true} [:maybe [:or :keyword :string]]]
[:target_entity_id {:optional true} [:maybe ms/PositiveInt]]
[:status {:optional true} [:maybe [:or :keyword :string]]]
[:is_active {:optional true} [:maybe :boolean]]
[:progress {:optional true} [:maybe number?]]
[:message {:optional true} [:maybe :string]]
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:start_time {:optional true} [:maybe ms/TemporalInstant]]
[:end_time {:optional true} [:maybe ms/TemporalInstant]]])

View File

@@ -2,37 +2,46 @@
"Application database queries for the sandbox module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions."
(:require
[malli.util :as mut]
[metabase-enterprise.sandbox.schema :as sandbox.schema]
[metabase.app-db.core :as mdb]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.queries.schema :as queries.schema]
[metabase.users.schema :as users.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn sandbox
(mu/defn sandbox
"The Sandbox with `sandbox-id`, or nil."
[sandbox-id]
[sandbox-id :- ::lib.schema.id/sandbox]
(t2/select-one :model/Sandbox :id sandbox-id))
(defn sandboxes
(mu/defn sandboxes
"Every Sandbox, in ID order."
[]
(t2/select :model/Sandbox {:order-by [[:id :asc]]}))
(defn sandbox-for-group-and-table
(mu/defn sandbox-for-group-and-table
"The Sandbox of the group with `group-id` on the Table with `table-id`, or nil."
[group-id table-id]
[group-id :- ms/PositiveInt
table-id :- ::lib.schema.id/table]
(t2/select-one :model/Sandbox :group_id group-id :table_id table-id))
(defn sandboxes-for-groups-and-table
(mu/defn sandboxes-for-groups-and-table
"The Sandboxes of the groups with `group-ids` on the Table with `table-id`."
[group-ids table-id]
[group-ids :- [:set ms/PositiveInt]
table-id :- ::lib.schema.id/table]
(t2/select :model/Sandbox :group_id [:in group-ids] :table_id table-id))
(defn sandboxes-using-card
(mu/defn sandboxes-using-card
"The `:id` and `:table_id` of the Sandboxes built on the Card with `card-id`."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select [:model/Sandbox :id :table_id] :card_id card-id))
(defn user-sandboxes-with-group-ids
(mu/defn user-sandboxes-with-group-ids
"The Sandboxes of the groups of the User with `user-id`, each with the `:group_id` of the membership."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select :model/Sandbox
{:select [[:pgm.group_id :group_id]
[:s.*]]
@@ -41,10 +50,13 @@
:where [:and
[:= :pgm.user_id user-id]]}))
(defn sandboxes-with-table-info
(mu/defn sandboxes-with-table-info
"The group, Table, Database, and schema of the Sandboxes of the optional `group-id` or `group-ids` in the optional
Database `db-id`, excluding the Database `excluded-db-id` when given."
[group-id group-ids db-id excluded-db-id]
[group-id :- [:maybe ms/PositiveInt]
group-ids :- [:maybe [:sequential ms/PositiveInt]]
db-id :- [:maybe ::lib.schema.id/database]
excluded-db-id :- [:maybe ::lib.schema.id/database]]
(t2/select :model/Sandbox
{:select [:s.group_id :s.table_id :t.db_id :t.schema]
:from [[:sandboxes :s]]
@@ -55,10 +67,11 @@
(when db-id [:= :t.db_id db-id])
(when excluded-db-id [:not [:= :t.db_id excluded-db-id]])]}))
(defn candidate-sandboxes-for-groups-and-databases
(mu/defn candidate-sandboxes-for-groups-and-databases
"The `:id`, `:group_id`, `:table_id`, `:db_id`, and `:schema` of the Sandboxes of the groups with `group-ids` on
Tables of the Databases with `db-ids`."
[group-ids db-ids]
[group-ids :- [:set ms/PositiveInt]
db-ids :- [:set ::lib.schema.id/database]]
(mdb/query
{:select [[:sandboxes.id :id]
[:sandboxes.group_id :group_id]
@@ -72,47 +85,54 @@
[:in :sandboxes.group_id group-ids]
[:in :table.db_id db-ids]]}))
(defn insert-sandbox!
(mu/defn insert-sandbox!
"Insert `sandbox` and return the new instance."
[sandbox]
[sandbox :- [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:table_id ::lib.schema.id/table]
[:card_id {:optional true} [:maybe ::lib.schema.id/card]]
[:group_id ms/PositiveInt]
[:attribute_remappings {:optional true} [:maybe :map]]]]
(first (t2/insert-returning-instances! :model/Sandbox sandbox)))
(defn update-sandbox!
"Apply `changes` to the Sandbox with `sandbox-id`."
[sandbox-id changes]
(mu/defn update-sandbox!
"Apply `changes` to the Sandbox with `sandbox-id`, returning the number updated."
[sandbox-id :- ::lib.schema.id/sandbox
changes :- (mut/select-keys ::sandbox.schema/sandbox.update [:card_id :attribute_remappings])]
(t2/update! :model/Sandbox sandbox-id changes))
(defn delete-sandbox!
"Delete the Sandbox with `sandbox-id`."
[sandbox-id]
(mu/defn delete-sandbox!
"Delete the Sandbox with `sandbox-id`, returning the number deleted."
[sandbox-id :- ::lib.schema.id/sandbox]
(t2/delete! :model/Sandbox :id sandbox-id))
(defn delete-sandboxes!
"Delete the Sandboxes with `sandbox-ids`."
[sandbox-ids]
(mu/defn delete-sandboxes!
"Delete the Sandboxes with `sandbox-ids`, returning the number deleted."
[sandbox-ids :- [:set ::lib.schema.id/sandbox]]
(t2/delete! :model/Sandbox :id [:in sandbox-ids]))
(defn impersonations-for-groups
(mu/defn impersonations-for-groups
"The ConnectionImpersonations of the groups with `group-ids`."
[group-ids]
[group-ids :- [:set ms/PositiveInt]]
(t2/select :model/ConnectionImpersonation :group_id [:in group-ids]))
(defn user-group-ids
(mu/defn user-group-ids
"The IDs of the groups of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-fn-set :group_id :model/PermissionsGroupMembership :user_id user-id))
(defn personal-user
(mu/defn personal-user
"The personal User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one :model/User :id user-id :type :personal))
(defn set-user-login-attributes!
(mu/defn set-user-login-attributes!
"Set the login attributes of the User with `user-id`, returning the number of rows updated."
[user-id login-attributes]
[user-id :- ::lib.schema.id/user
login-attributes :- [:maybe users.schema/LoginAttributes]]
(t2/update! :model/User user-id {:login_attributes login-attributes}))
(defn user-attributes-reducible
(mu/defn user-attributes-reducible
"Reducible merged JWT and login attribute maps of the Users that have any."
[]
(t2/select-fn-reducible (comp (partial apply merge)
@@ -126,50 +146,53 @@
[:not= :login_attributes nil]
[:not= :login_attributes "{}"]]]}))
(defn table
(mu/defn table
"The Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one :model/Table :id table-id))
(defn tables-of-database
(mu/defn tables-of-database
"The `:id`, `:db_id`, and `:schema` of the Tables of the Database with `db-id`, restricted to `schema` when
`schema-only?`."
[db-id schema-only? schema]
[db-id :- ::lib.schema.id/database
schema-only? :- :boolean
schema :- [:maybe :string]]
(t2/select [:model/Table :id :db_id :schema]
{:where [:and
[:= :db_id db-id]
(when schema-only?
[:= :schema schema])]}))
(defn database-of-table
(mu/defn database-of-table
"The Database of the Table with `table-id`, or nil."
[table-id]
[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]]
:where [:= :t.id table-id]}))
(defn fields-of-table-named
(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 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]))
(defn cards-by-id
(mu/defn cards-by-id
"A map of Card ID to the query, result metadata, and schema of the Cards with `card-ids`."
[card-ids]
[card-ids :- [:set ::lib.schema.id/card]]
(t2/select-pk->fn identity [:model/Card :id :dataset_query :result_metadata :card_schema] :id [:in card-ids]))
(defn cards-result-metadata
(mu/defn cards-result-metadata
"The `:id`, `:result_metadata`, and `:card_schema` of the Cards with `card-ids`."
[card-ids]
[card-ids :- [:set ::lib.schema.id/card]]
(t2/select [:model/Card :id :result_metadata :card_schema] :id [:in card-ids]))
(defn card-result-metadata
(mu/defn card-result-metadata
"The result metadata of the Card with `card-id`."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one-fn :result_metadata :model/Card :id card-id))
(defn sandboxing-cards
(mu/defn sandboxing-cards
"The `:id`, `:dataset_query`, `:database_id`, and `:card_schema` of the Cards Sandboxes are built on."
[]
(t2/select :model/Card
@@ -179,7 +202,8 @@
:from [[(t2/table-name :model/Sandbox) :s]]
:where [:= :s.card_id :c.id]}]}))
(defn set-card-result-metadata!
"Set the result metadata of the Card with `card-id`."
[card-id result-metadata]
(mu/defn set-card-result-metadata!
"Set the result metadata of the Card with `card-id`, returning the number updated."
[card-id :- ::lib.schema.id/card
result-metadata :- [:maybe ::queries.schema/card.result-metadata]]
(t2/update! :model/Card card-id {:result_metadata result-metadata}))

View File

@@ -32,6 +32,23 @@
{:in (comp mi/json-in normalize-attribute-remappings)
:out (comp normalize-attribute-remappings mi/json-out-without-keywordization)})
(mr/def ::sandbox.attribute-remappings
"The `:attribute_remappings` column of a Sandbox, decoded."
:map)
(mr/def ::sandbox
[:map
[:id ::lib.schema.id/sandbox]])
"A Sandbox as selected from the app DB: every column of `:sandboxes`."
[:map {:closed true}
[:id ms/PositiveInt]
[:group_id ms/PositiveInt]
[:table_id ::lib.schema.id/table]
[:card_id [:maybe ::lib.schema.id/card]]
[:attribute_remappings [:maybe ::sandbox.attribute-remappings]]])
(mr/def ::sandbox.update
"What an update (or insert) of a Sandbox accepts: every column of `:sandboxes` except `id`, all optional."
[:map {:closed true}
[:group_id {:optional true} [:maybe ms/PositiveInt]]
[:table_id {:optional true} [:maybe ::lib.schema.id/table]]
[:card_id {:optional true} [:maybe ::lib.schema.id/card]]
[:attribute_remappings {:optional true} [:maybe ::sandbox.attribute-remappings]]])

View File

@@ -2,7 +2,14 @@
"Application database queries for the scim module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for transactions."
(:require
[malli.util :as mut]
[metabase.api-keys.schema :as api-keys.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.permissions.schema :as permissions.schema]
[metabase.users.schema :as users.schema]
[metabase.util :as u]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(def ^:private user-columns
@@ -13,19 +20,19 @@
"Required columns when fetching groups for SCIM."
[:model/PermissionsGroup :id :name :entity_id])
(defn scim-api-key
(mu/defn scim-api-key
"The SCIM ApiKey, or nil."
[]
(t2/select-one :model/ApiKey :scope :scim))
(defn delete-scim-api-keys!
"Delete every SCIM ApiKey."
(mu/defn delete-scim-api-keys!
"Delete every SCIM ApiKey, returning the number deleted."
[]
(t2/delete! :model/ApiKey :scope :scim))
(defn insert-api-key!
(mu/defn insert-api-key!
"Insert `api-key` and return the new instance."
[api-key]
[api-key :- ::api-keys.schema/api-key.create]
(t2/insert-returning-instance! :model/ApiKey api-key))
(defn- personal-user-expr
@@ -33,55 +40,59 @@
[:and [:= :type "personal"]
(when email [:= :%lower.email (u/lower-case-en email)])])
(defn scim-user-by-entity-id
(mu/defn scim-user-by-entity-id
"The SCIM columns of the personal User with `entity-id`, or nil."
[entity-id]
[entity-id :- :string]
(t2/select-one user-columns :entity_id entity-id {:where [:= :type "personal"]}))
(defn scim-user-by-email
(mu/defn scim-user-by-email
"The SCIM columns of the User with `email`, or nil."
[email]
[email :- :string]
(t2/select-one user-columns :email (u/lower-case-en email)))
(defn scim-users
(mu/defn scim-users
"The SCIM columns of the personal Users, narrowed to the optional `email` (case-insensitive), paged by `limit`
and `offset` in ID order."
[email limit offset]
[email :- [:maybe :string]
limit :- [:maybe ms/PositiveInt]
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
(t2/select user-columns
{:where (personal-user-expr email)
:limit limit
:offset offset
:order-by [[:id :asc]]}))
(defn scim-user-count
(mu/defn scim-user-count
"The number of personal Users, narrowed to the optional `email` (case-insensitive)."
[email]
[email :- [:maybe :string]]
(t2/count :model/User {:where (personal-user-expr email)}))
(defn user-email-exists?
(mu/defn user-email-exists?
"Whether a User with `email` (case-insensitive) exists."
[email]
[email :- :string]
(t2/exists? :model/User :%lower.email (u/lower-case-en email)))
(defn user-ids-by-entity-ids
(mu/defn user-ids-by-entity-ids
"The IDs of the Users with `entity-ids`."
[entity-ids]
[entity-ids :- [:sequential :string]]
(t2/select-fn-set :id :model/User {:where [:in :entity_id entity-ids]}))
(defn insert-user!
"Insert the User `row`."
[row]
(mu/defn insert-user!
"Insert the User `row`, returning the number inserted."
[row :- ::users.schema/user.update]
(t2/insert! :model/User row))
(defn update-user!
"Apply `changes` to the User with `user-id`."
[user-id changes]
(mu/defn update-user!
"Apply `changes` to the User with `user-id`, returning the number updated."
[user-id :- ::lib.schema.id/user
changes :- ::users.schema/user.update]
(t2/update! :model/User user-id changes))
(defn user-group-memberships
(mu/defn user-group-memberships
"Rows of User ID, group name, and group entity ID for the memberships of the Users with `user-ids`, excluding the
groups with `excluded-group-ids`."
[user-ids excluded-group-ids]
[user-ids :- [:sequential ::lib.schema.id/user]
excluded-group-ids :- [:sequential ms/PositiveInt]]
(t2/select [:model/PermissionsGroupMembership :pgm.user_id :pg.name :pg.entity_id]
{:from [[:permissions_group_membership :pgm]]
:join [[:permissions_group :pg] [:= :pg.id :group_id]]
@@ -95,50 +106,56 @@
(map (fn [group-id] [:not= :id group-id]))
excluded-group-ids))
(defn scim-group-by-entity-id
(mu/defn scim-group-by-entity-id
"The SCIM columns of the PermissionsGroup with `entity-id` other than `excluded-group-ids`, or nil."
[entity-id excluded-group-ids]
[entity-id :- :string
excluded-group-ids :- [:sequential ms/PositiveInt]]
(t2/select-one group-columns :entity_id entity-id {:where (manageable-group-expr excluded-group-ids nil)}))
(defn scim-groups
(mu/defn scim-groups
"The SCIM columns of the PermissionsGroups other than `excluded-group-ids`, narrowed to the optional `group-name`,
paged by `limit` and `offset` in ID order."
[excluded-group-ids group-name limit offset]
[excluded-group-ids :- [:sequential ms/PositiveInt]
group-name :- [:maybe :string]
limit :- [:maybe ms/PositiveInt]
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
(t2/select group-columns
{:where (manageable-group-expr excluded-group-ids group-name)
:limit limit
:offset offset
:order-by [[:id :asc]]}))
(defn scim-group-count
(mu/defn scim-group-count
"The number of PermissionsGroups other than `excluded-group-ids`, narrowed to the optional `group-name`."
[excluded-group-ids group-name]
[excluded-group-ids :- [:sequential ms/PositiveInt]
group-name :- [:maybe :string]]
(t2/count :model/PermissionsGroup {:where (manageable-group-expr excluded-group-ids group-name)}))
(defn group-name-exists?
(mu/defn group-name-exists?
"Whether a PermissionsGroup with `group-name` (case-insensitive) exists."
[group-name]
[group-name :- :string]
(t2/exists? :model/PermissionsGroup :%lower.name (u/lower-case-en group-name)))
(defn insert-group!
(mu/defn insert-group!
"Insert `group` and return the new instance."
[group]
[group :- (mut/select-keys ::permissions.schema/permissions-group.update [:name :entity_id :magic_group_type :is_tenant_group])]
(first (t2/insert-returning-instances! :model/PermissionsGroup group)))
(defn update-group!
"Apply `changes` to the PermissionsGroup with `group-id`."
[group-id changes]
(mu/defn update-group!
"Apply `changes` to the PermissionsGroup with `group-id`, returning the number updated."
[group-id :- ms/PositiveInt
changes :- (mut/select-keys ::permissions.schema/permissions-group.update [:name :entity_id :magic_group_type :is_tenant_group])]
(t2/update! :model/PermissionsGroup group-id changes))
(defn delete-group!
"Delete the PermissionsGroup with `group-id`."
[group-id]
(mu/defn delete-group!
"Delete the PermissionsGroup with `group-id`, returning the number deleted."
[group-id :- ms/PositiveInt]
(t2/delete! :model/PermissionsGroup group-id))
(defn group-members
(mu/defn group-members
"Rows of group ID, member email, and member entity ID for the memberships of the PermissionsGroups with
`group-ids`."
[group-ids]
[group-ids :- [:sequential ms/PositiveInt]]
(t2/select [:model/PermissionsGroupMembership :pgm.group_id :u.email :u.entity_id]
{:from [[:permissions_group_membership :pgm]]
:join [[:core_user :u] [:= :u.id :pgm.user_id]]

View File

@@ -24,8 +24,8 @@
(defenterprise scorers
"Return the select-item expressions used to calculate the score for each search result."
:feature :none
[search-ctx]
(merge (appdb.scoring/base-scorers search-ctx) (additional-scorers)))
[search-ctx view-count-percentiles]
(merge (appdb.scoring/base-scorers search-ctx view-count-percentiles) (additional-scorers)))
;; ------------ LEGACY ----------

View File

@@ -5,7 +5,6 @@
[metabase-enterprise.security-center.db :as security-center.db]
[metabase-enterprise.security-center.models.security-advisory :as security-advisory]
[metabase-enterprise.security-center.notification :as notification]
[metabase-enterprise.security-center.schema :as security-center.schema]
[metabase-enterprise.security-center.settings :as settings]
[metabase-enterprise.security-center.task.sync-advisories :as sync-advisories]
[metabase.api.common :as api]
@@ -13,6 +12,7 @@
[metabase.api.routes.common :as routes.common :refer [+auth]]
[metabase.notification.models :as models.notification]
[metabase.premium-features.core :as premium-features]
[metabase.security-center.schema :as security-center.schema]
[metabase.util.i18n :refer [tru]]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2])

View File

@@ -2,60 +2,78 @@
"Application database queries for the security-center module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[malli.util :as mut]
[metabase.app-db.core :as mdb]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.security-center.schema :as security-center.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn advisory
(mu/defn advisory
"The SecurityAdvisory with `id`, or nil."
[id]
[id :- ms/PositiveInt]
(t2/select-one :model/SecurityAdvisory :id id))
(defn advisory-by-advisory-id
(mu/defn advisory-by-advisory-id
"The SecurityAdvisory with the external `advisory-id`, or nil."
[advisory-id]
[advisory-id :- :string]
(t2/select-one :model/SecurityAdvisory :advisory_id advisory-id))
(defn max-advisory-updated-at
(mu/defn max-advisory-updated-at
"The latest `updated_at` across every SecurityAdvisory, or nil when there are none."
[]
(:updated_at (t2/query-one {:select [[[:max :updated_at] :updated_at]]
:from [:security_advisory]})))
(defn advisories-newest-first
(mu/defn advisories-newest-first
"Every SecurityAdvisory, newest first."
[]
(t2/select :model/SecurityAdvisory {:order-by [[:published_at :desc]]}))
(defn advisories-reducible
(mu/defn advisories-reducible
"Reducible SecurityAdvisories."
[]
(t2/reducible-select :model/SecurityAdvisory))
(defn advisories-with-statuses-reducible
(mu/defn advisories-with-statuses-reducible
"Reducible severity and acknowledgement time of the SecurityAdvisories whose match status is one of `statuses`."
[statuses]
[statuses :- [:set [:or :keyword :string]]]
(t2/reducible-select [:model/SecurityAdvisory :severity :acknowledged_at] :match_status [:in statuses]))
(defn unacknowledged-advisories-with-statuses
(mu/defn unacknowledged-advisories-with-statuses
"The unacknowledged SecurityAdvisories whose match status is one of `statuses`."
[statuses]
[statuses :- [:sequential [:or :keyword :string]]]
(t2/select :model/SecurityAdvisory :acknowledged_at nil :match_status [:in statuses]))
(defn unacknowledged-advisories-by-advisory-ids
(mu/defn unacknowledged-advisories-by-advisory-ids
"The unacknowledged SecurityAdvisories with the external `advisory-ids`."
[advisory-ids]
[advisory-ids :- [:set :string]]
(t2/select :model/SecurityAdvisory :advisory_id [:in advisory-ids] :acknowledged_at nil))
(defn update-advisory!
"Apply `changes` to the SecurityAdvisory with `id`."
[id changes]
(mu/defn update-advisory!
"Apply `changes` to the SecurityAdvisory with `id`, returning the number updated."
[id :- ms/PositiveInt
changes :- (mut/select-keys ::security-center.schema/security-advisory.update [:match_status :last_evaluated_at :acknowledged_by :acknowledged_at :last_notified_at])]
(t2/update! :model/SecurityAdvisory id changes))
(defn upsert-advisory!
(mu/defn record-advisory-evaluation!
"Apply `changes` to the SecurityAdvisory with `id` and stamp `last_evaluated_at` with now, returning the number
updated."
[id :- ms/PositiveInt
changes :- (mut/select-keys ::security-center.schema/security-advisory.update [:match_status :acknowledged_by :acknowledged_at])]
(t2/update! :model/SecurityAdvisory id (assoc changes :last_evaluated_at :%now)))
(mu/defn record-advisory-notification!
"Set `last_notified_at` of the SecurityAdvisory with `id` to now, returning the number updated."
[id :- ms/PositiveInt]
(t2/update! :model/SecurityAdvisory id {:last_notified_at :%now}))
(mu/defn upsert-advisory!
"Insert or update a SecurityAdvisory by `:advisory_id`. On insert, `:match_status` starts as `:unknown` until the
matching engine evaluates it. On update, sets `advisory`'s columns, leaving `:match_status`, `:last_evaluated_at`,
and acknowledgement fields (which `advisory` doesn't include) untouched."
[advisory]
[advisory :- ::security-center.schema/security-advisory.update]
(mdb/update-or-insert! :model/SecurityAdvisory
{:advisory_id (:advisory_id advisory)}
(fn [existing]
@@ -63,7 +81,7 @@
advisory
(assoc advisory :match_status :unknown)))))
(defn user-summaries-by-id
(mu/defn user-summaries-by-id
"A map of ID to the ID, names, and email of the Users with `user-ids`."
[user-ids]
[user-ids :- [:set ::lib.schema.id/user]]
(t2/select-fn->fn :id identity [:model/User :id :first_name :last_name :email] :id [:in user-ids]))

View File

@@ -6,9 +6,9 @@
[clojure.set :as set]
[java-time.api :as t]
[metabase-enterprise.security-center.db :as security-center.db]
[metabase-enterprise.security-center.schema :as security-center.schema]
[metabase.config.core :as config]
[metabase.premium-features.core :as premium-features]
[metabase.security-center.schema :as security-center.schema]
[metabase.util.json :as json]
[metabase.util.log :as log]
[metabase.util.malli :as mu]

View File

@@ -3,11 +3,10 @@
and combines with version checks to determine match status."
(:require
[metabase-enterprise.security-center.db :as security-center.db]
[metabase-enterprise.security-center.schema :as schema]
[metabase.app-db.core :as mdb]
[metabase.config.core :as config]
[metabase.driver.sql-jdbc.execute :as sql-jdbc.execute]
[metabase.models.interface :as mi]
[metabase.security-center.schema :as schema]
[metabase.util.connection :as u.connection]
[metabase.util.log :as log]
[metabase.util.malli :as mu]
@@ -148,11 +147,10 @@
reactivated? (and (#{:active :error} match-status)
currently-unaffected
(some? (:acknowledged_at advisory)))]
(security-center.db/update-advisory! (:id advisory)
(cond-> {:match_status match-status
:last_evaluated_at (mi/now)}
reactivated? (assoc :acknowledged_at nil
:acknowledged_by nil))))))))
(security-center.db/record-advisory-evaluation! (:id advisory)
(cond-> {:match_status match-status}
reactivated? (assoc :acknowledged_at nil
:acknowledged_by nil))))))))
(defn evaluate-all-advisories!
"Re-evaluate every advisory, including acknowledged ones — an acked
@@ -166,6 +164,5 @@
(evaluate-advisory! advisory instance-version)
(catch Exception e
(log/warnf "Error evaluating advisory %s: %s" (:advisory_id advisory) (ex-message e))
(security-center.db/update-advisory! (:id advisory)
{:match_status :error
:last_evaluated_at (mi/now)}))))))))
(security-center.db/record-advisory-evaluation! (:id advisory)
{:match_status :error}))))))))

View File

@@ -24,6 +24,13 @@
[advisory]
(assoc advisory :fetched_at (mi/now)))
(t2/define-before-update :model/SecurityAdvisory
[advisory]
(let [changes (t2/changes advisory)]
(cond-> advisory
(and (some? (:acknowledged_by changes)) (not (contains? changes :acknowledged_at)))
(assoc :acknowledged_at (mi/now)))))
(methodical/defmethod t2/batched-hydrate [:model/SecurityAdvisory :acknowledged_by_user]
"Hydrate `:acknowledged_by_user` from the `:acknowledged_by` FK to a User map with `:id`, `:common_name`, and `:email`."
[_model k advisories]
@@ -43,18 +50,15 @@
[advisory user-id]
(when (:acknowledged_at advisory)
(throw (ex-info "Advisory already acknowledged" {:status-code 409})))
(let [now (mi/now)]
(security-center.db/update-advisory! (:id advisory)
{:acknowledged_by user-id
:acknowledged_at now})
(events/publish-event! :event/security-advisory-acknowledge
{:object advisory
:user-id user-id})
(analytics/track-event! :snowplow/simple_event
{:event "security_advisory_acknowledged"
:event_detail (name (:severity advisory))})
(-> (security-center.db/advisory (:id advisory))
(t2/hydrate :acknowledged_by_user))))
(security-center.db/update-advisory! (:id advisory) {:acknowledged_by user-id})
(events/publish-event! :event/security-advisory-acknowledge
{:object advisory
:user-id user-id})
(analytics/track-event! :snowplow/simple_event
{:event "security_advisory_acknowledged"
:event_detail (name (:severity advisory))})
(-> (security-center.db/advisory (:id advisory))
(t2/hydrate :acknowledged_by_user)))
(defn acknowledge-many!
"Acknowledge multiple security advisories by their advisory_id strings. Skips already-acknowledged

View File

@@ -15,7 +15,6 @@
[metabase.analytics.core :as analytics]
[metabase.channel.settings :as channel.settings]
[metabase.events.core :as events]
[metabase.models.interface :as mi]
[metabase.notification.core :as notification]
[metabase.permissions.core :as perms]
[metabase.settings.core :as setting]
@@ -169,7 +168,7 @@
(try
(notification/send-notification! notif :notification/sync? true)
(track-notification-sent! notif triggered-from "success")
(security-center.db/update-advisory! (:id advisory) {:last_notified_at (mi/now)})
(security-center.db/record-advisory-notification! (:id advisory))
(catch Exception e
(track-notification-sent! notif triggered-from "failure")
(throw e))))))

View File

@@ -1,39 +0,0 @@
(ns metabase-enterprise.security-center.schema
"Malli schemas for the Security Center module."
(:require
[metabase.util.malli.registry :as mr]))
(mr/def ::advisory-id
[:string {:min 1}])
(mr/def ::semver
[:re #"^\d+(?:\.\d+)*$"])
(mr/def ::severity
[:enum :critical :high :medium :low])
(mr/def ::match-status
[:enum :unknown :active :resolved :not_affected :error])
(mr/def ::version-range
"A single affected version range with inclusive min and exclusive fixed."
[:map
[:min ::semver]
[:fixed ::semver]])
(mr/def ::affected-versions
[:sequential ::version-range])
(mr/def ::download-jar-url
"A downloadable JAR for a given fixed version."
[:map
[:version ::semver]
[:url :string]])
(mr/def ::download-jar-urls
[:sequential ::download-jar-url])
(mr/def ::matching-query
"HoneySQL query keyed by dialect. nil means affects all instances.
Stored as EDN to preserve keywords that HoneySQL requires for identifiers/operators."
[:maybe [:map-of :keyword :map]])

View File

@@ -0,0 +1,14 @@
(ns metabase-enterprise.semantic-search.appdb-scoring
"The app-DB based scorers of semantic search results. Expression building only:
`metabase-enterprise.semantic-search.db` runs the query, so this namespace must not require it."
(:require
[metabase.search.config :as search.config]
[metabase.search.scoring :as search.scoring]))
(defn appdb-scorers
"The appdb-based scorers for search ranking results. Like `base-scorers`, but for scorers that need to query the appdb."
[search-ctx]
(when-not (search.scoring/no-scoring-required? search-ctx)
{:bookmarked search.scoring/bookmark-score-expr
:user-recency (search.scoring/inverse-duration
(search.scoring/user-recency-expr search-ctx) [:now] search.config/stale-time-in-days)}))

View File

@@ -2,21 +2,27 @@
"Application database queries for the semantic-search module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[malli.util :as mut]
[metabase-enterprise.semantic-search.appdb-scoring :as appdb-scoring]
[metabase-enterprise.semantic-search.schema :as semantic-search.schema]
[metabase.app-db.core :as mdb]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.search.scoring :as search.scoring]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn library-root-collections
(mu/defn library-root-collections
"The ID and type of the top-level Collections whose type is one of `types`."
[types]
[types :- [:sequential :string]]
(t2/select [:model/Collection :id :type] :type [:in types] :location "/"))
(defn descendant-collection-ids
(mu/defn descendant-collection-ids
"The IDs of the Collections under the top-level Collection with `root-id`."
[root-id]
[root-id :- ms/PositiveInt]
(t2/select-pks-set :model/Collection :location [:like (str "/" root-id "/%")]))
(defn curated-tables-reducible
(mu/defn curated-tables-reducible
"Reducible ID, published flag, data layer, and data authority of the active Tables that are published or
authoritative."
[]
@@ -26,38 +32,39 @@
[:or [:= :is_published true]
[:= :data_authority ^:allow-raw-sql [:inline "authoritative"]]]]}))
(defn official-collection-ids
(mu/defn official-collection-ids
"The IDs of the official Collections."
[]
(t2/select-pks-set :model/Collection :authority_level :official))
(defn unarchived-dashboard-ids-in-collections-reducible
(mu/defn unarchived-dashboard-ids-in-collections-reducible
"Reducible `:id` rows of the unarchived Dashboards in the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/reducible-select [:model/Dashboard :id]
{:where [:and [:= :archived false]
[:in :collection_id collection-ids]]}))
(defn collection-owners-and-locations
(mu/defn collection-owners-and-locations
"The ID, owner, and location of the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/select [:model/Collection :id :personal_owner_id :location] :id [:in collection-ids]))
(defn personal-collection-owners
(mu/defn personal-collection-owners
"The ID and owner of the personal Collections among `collection-ids`."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/select [:model/Collection :id :personal_owner_id]
:id [:in collection-ids]
:personal_owner_id [:not= nil]))
(defn collection-locations
(mu/defn collection-locations
"The ID and location of the raw collection rows with `collection-ids`."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select [:collection :id :location] :id [:in collection-ids]))
(defn instances
(mu/defn instances
"The instances of `model` with `ids`."
[model ids]
[model :- :keyword
ids :- [:sequential ms/PositiveInt]]
(t2/select model :id [:in ids]))
(defn- search-doc-select
@@ -78,21 +85,27 @@
[:search_index.model :model]]
:from [:search_index]})
(defn appdb-scored-rows
"The `:id`/`:model` rows of `search-results` (each `{:id :model}`) augmented with the SELECT expressions of
`scorers` (a map of scorer key to SELECT expression, see
`metabase-enterprise.semantic-search.scoring/appdb-scorers`) evaluated under `search-ctx`, joining bookmark
tables when `:bookmarked` is among `scorers`."
[search-results search-ctx scorers]
(t2/query (cond-> (search.scoring/with-scores search-ctx scorers (search-index-select search-results))
(:bookmarked scorers) (search.scoring/join-bookmarks (:current-user-id search-ctx)))))
(mu/defn appdb-scored-rows
"The `:id`/`:model` rows of `search-results` augmented with the app-DB scorers of `search-ctx` (see
`metabase-enterprise.semantic-search.appdb-scoring/appdb-scorers`), joining the bookmark tables when `:bookmarked`
is among them."
[search-results :- [:sequential [:map {:closed true}
[:id [:or :string ms/PositiveInt]]
[:model :string]]]
search-ctx :- [:map
[:current-user-id {:optional true} [:maybe ms/PositiveInt]]
[:context {:optional true} [:maybe :keyword]]
[:weights {:optional true} [:maybe [:map-of :keyword number?]]]]]
(let [scorers (appdb-scoring/appdb-scorers search-ctx)]
(t2/query (cond-> (search.scoring/with-scores search-ctx scorers (search-index-select search-results))
(:bookmarked scorers) (search.scoring/join-bookmarks (:current-user-id search-ctx))))))
(defn insert-token-tracking!
(mu/defn insert-token-tracking!
"Insert the SemanticSearchTokenTracking `row`."
[row]
[row :- (mut/select-keys ::semantic-search.schema/semantic-search-token-tracking.update [:model_name :request_type :total_tokens])]
(t2/insert! :model/SemanticSearchTokenTracking row))
(defn delete-token-tracking-created-before!
(mu/defn delete-token-tracking-created-before!
"Delete the SemanticSearchTokenTracking rows created before `cutoff`."
[cutoff]
[cutoff :- ms/TemporalInstant]
(t2/delete! :model/SemanticSearchTokenTracking {:where [:< :created_at cutoff]}))

View File

@@ -8,6 +8,7 @@
[honey.sql :as sql]
[honey.sql.helpers :as sql.helpers]
[java-time.api :as t]
[metabase-enterprise.semantic-search.appdb-scoring :as appdb-scoring]
[metabase-enterprise.semantic-search.db :as semantic-search.db]
;; TODO: extract schema code to go under db.migration
[metabase-enterprise.semantic-search.embedding :as embedding]
@@ -1261,7 +1262,7 @@
(mapv search/collapse-id)))
filter-time-ms (u/since-ms filter-timer)
appdb-scorers (scoring/appdb-scorers search-context)
appdb-scorers (appdb-scoring/appdb-scorers search-context)
appdb-scores-timer (u/start-timer)
final-results (->> filtered-results
(scoring/with-appdb-scores search-context appdb-scorers weights))

View File

@@ -0,0 +1,22 @@
(ns metabase-enterprise.semantic-search.schema
"Malli schemas for the semantic-search module."
(:require
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::semantic-search-token-tracking
"A SemanticSearchTokenTracking as selected from the app DB: every column of `:semantic_search_token_tracking`."
[:map {:closed true}
[:id ms/PositiveInt]
[:model_name :string]
[:request_type [:maybe [:or :keyword :string]]]
[:created_at ms/TemporalInstant]
[:total_tokens :int]])
(mr/def ::semantic-search-token-tracking.update
"What an update (or insert) of a SemanticSearchTokenTracking accepts: every column of `:semantic_search_token_tracking` except `id`, all optional."
[:map {:closed true}
[:model_name {:optional true} [:maybe :string]]
[:request_type {:optional true} [:maybe [:or :keyword :string]]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:total_tokens {:optional true} [:maybe :int]]])

View File

@@ -186,14 +186,6 @@
;; #{"segment" "database" "action" "indexed-entity"}
(set/difference (set search.spec/search-models) appdb-scorer-models))
(defn appdb-scorers
"The appdb-based scorers for search ranking results. Like `base-scorers`, but for scorers that need to query the appdb."
[search-ctx]
(when-not (search.scoring/no-scoring-required? search-ctx)
{:bookmarked search.scoring/bookmark-score-expr
:user-recency (search.scoring/inverse-duration
(search.scoring/user-recency-expr search-ctx) [:now] search.config/stale-time-in-days)}))
(defn with-appdb-scores
"Add appdb-based scores to `search-results` and re-sort the results based on the new combined scores.
@@ -208,7 +200,8 @@
(if-not (and (seq search-results-to-score)
(seq appdb-scorers))
search-results
(->> (semantic-search.db/appdb-scored-rows search-results-to-score search-ctx appdb-scorers)
(->> (semantic-search.db/appdb-scored-rows (mapv #(select-keys % [:id :model]) search-results-to-score)
search-ctx)
(update-with-appdb-scores weights (keys appdb-scorers) search-results)
(sort-by :score >)
vec))))

View File

@@ -2,45 +2,52 @@
"Application database queries for the serialization module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model-level helpers."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn all-users
(mu/defn all-users
"Every User."
[]
(t2/select :model/User))
(defn collection-id-row
(mu/defn collection-id-row
"The `:collection_id` row of the instance of `model` with `id`, or nil."
[model id]
[model :- :keyword
id :- ms/PositiveInt]
(t2/select-one [model :collection_id] :id id))
(defn existing-ids
(mu/defn existing-ids
"The subset of `ids` for which an instance of `model` exists."
[model ids]
[model :- :keyword
ids :- [:sequential ms/PositiveInt]]
(t2/select-pks-set model {:where [:in :id ids]}))
(defn instance-exists?
(mu/defn instance-exists?
"Whether an instance of `model` whose primary key is `id` exists."
[model id]
[model :- :keyword
id :- [:maybe ms/PositiveInt]]
(t2/exists? model (first (t2/primary-keys model)) id))
(defn root-collections-for-user
(mu/defn root-collections-for-user
"The top-level non-analytics Collections that are not personal or belong to the User with `user-id`."
[user-id]
[user-id :- [:maybe ::lib.schema.id/user]]
(t2/select :model/Collection {:where [:and [:= :location "/"]
[:or [:= :personal_owner_id nil]
[:= :personal_owner_id user-id]]
[:or [:= :namespace nil]
[:!= :namespace "analytics"]]]}))
(defn analytics-root-collections
(mu/defn analytics-root-collections
"The Collections in the analytics namespace."
[]
(t2/select :model/Collection {:where [:= :namespace "analytics"]}))
(defn card-ids-in-collections
(mu/defn card-ids-in-collections
"The subset of `card-ids` living in the Collections with `collection-ids`."
[card-ids collection-ids]
[card-ids :- [:sequential ::lib.schema.id/card]
collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select-pks-set :model/Card {:where [:and
[:in :id card-ids]
[:in :collection_id collection-ids]]}))

View File

@@ -2,11 +2,13 @@
"Application database queries for the snippet-collections module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn snippet-with-collection-id
(mu/defn snippet-with-collection-id
"The `:collection_id` of the NativeQuerySnippet with `id`, or nil if no such NativeQuerySnippet exists. Unlike a
bare column lookup, this distinguishes a missing snippet (nil) from one filed under the root collection
(`{:collection_id nil}`)."
[id]
[id :- ::lib.schema.id/native-query-snippet]
(t2/select-one [:model/NativeQuerySnippet :collection_id] :id id))

View File

@@ -2,42 +2,50 @@
"Application database queries for the sso module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself."
(:require
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn session-user-email-and-source
(mu/defn session-user-email-and-source
"The email and SSO source of the User owning the Session with `session-key-hashed`, or nil."
[session-key-hashed]
[session-key-hashed :- :string]
(t2/query-one {:select [:u.email :u.sso_source]
:from [[:core_user :u]]
:join [[:core_session :session] [:= :u.id :session.user_id]]
:where [:= :key_hashed session-key-hashed]}))
(defn delete-session!
"Delete the Session with `session-key-hashed`."
[session-key-hashed]
(mu/defn delete-session!
"Delete the Session with `session-key-hashed`, returning the number deleted."
[session-key-hashed :- :string]
(t2/delete! :model/Session :key_hashed session-key-hashed))
(defn group-ids-by-name
(mu/defn group-ids-by-name
"The IDs of the PermissionsGroups named one of `group-names`."
[group-names]
[group-names :- [:set :string]]
(t2/select-pks-set :model/PermissionsGroup :name [:in group-names]))
(defn insert-relay-state!
"Insert the SsoRelayState `row`."
[row]
(mu/defn insert-relay-state!
"Insert the SsoRelayState `row`, returning the number inserted."
[row :- [:map {:closed true}
[:id :string]
[:continue_url [:maybe :string]]
[:origin [:maybe :string]]
[:embedding :boolean]
[:expires_at ms/TemporalInstant]]]
(t2/insert! :model/SsoRelayState row))
(defn unexpired-relay-state
(mu/defn unexpired-relay-state
"The SsoRelayState with `hashed-key` expiring after `now`, or nil."
[hashed-key now]
[hashed-key :- :string
now :- ms/TemporalInstant]
(t2/select-one :model/SsoRelayState :id hashed-key :expires_at [:> now]))
(defn delete-relay-state!
"Delete the SsoRelayState with `hashed-key`."
[hashed-key]
(mu/defn delete-relay-state!
"Delete the SsoRelayState with `hashed-key`, returning the number deleted."
[hashed-key :- :string]
(t2/delete! :model/SsoRelayState :id hashed-key))
(defn delete-relay-states-expired-at!
(mu/defn delete-relay-states-expired-at!
"Delete the SsoRelayStates expired at or before `now`, returning the number deleted."
[now]
[now :- ms/TemporalInstant]
(t2/delete! :model/SsoRelayState :expires_at [:<= now]))

View File

@@ -0,0 +1,24 @@
(ns metabase-enterprise.sso.schema
"Malli schemas for the sso module."
(:require
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::sso-relay-state
"A SsoRelayState as selected from the app DB: every column of `:sso_relay_state`."
[:map {:closed true}
[:id :string]
[:continue_url :string]
[:origin [:maybe :string]]
[:embedding :boolean]
[:expires_at ms/TemporalInstant]
[:created_at ms/TemporalInstant]])
(mr/def ::sso-relay-state.update
"What an update (or insert) of a SsoRelayState accepts: every column of `:sso_relay_state` except `id`, all optional."
[:map {:closed true}
[:continue_url {:optional true} [:maybe :string]]
[:origin {:optional true} [:maybe :string]]
[:embedding {:optional true} [:maybe :boolean]]
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]])

View File

@@ -2,21 +2,23 @@
"Application database queries for the stale module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn collection
(mu/defn collection
"The Collection with `collection-id`, or nil."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/select-one :model/Collection collection-id))
(defn collections-by-id
(mu/defn collections-by-id
"A map of ID to Collection for `collection-ids`."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/select-pk->fn identity :model/Collection :id [:in collection-ids]))
(defn stale-cards
(mu/defn stale-cards
"The listing columns of the Cards with `card-ids`, with their latest moderation status."
[card-ids]
[card-ids :- [:set ::lib.schema.id/card]]
(t2/select [:model/Card
:id
:dashboard_id
@@ -47,9 +49,9 @@
:moderated_status]]
:id [:in card-ids]))
(defn stale-dashboards
(mu/defn stale-dashboards
"The listing columns of the Dashboards with `dashboard-ids`."
[dashboard-ids]
[dashboard-ids :- [:set ::lib.schema.id/dashboard]]
(t2/select [:model/Dashboard
:id
:description
@@ -71,11 +73,15 @@
[union-queries]
[[^:allow-subquery {:union-all union-queries} :dummy_alias]])
(defn stale-content-rows
(mu/defn stale-content-rows
"A page of `:id`/`:model` rows from the union of `union-queries`, sorted by `sort-column` (`:name` or
`:last_used_at`) in `sort-direction`, skipping `offset` and returning up to `limit` (either may be nil for no
restriction)."
[union-queries sort-column sort-direction limit offset]
[union-queries :- [:sequential :map]
sort-column :- [:enum :name :last_used_at]
sort-direction :- [:enum :asc :desc]
limit :- [:maybe :int]
offset :- [:maybe :int]]
(t2/query (cond-> {:select [:id :model]
:from (stale-content-union union-queries)
:order-by [[(case sort-column
@@ -85,8 +91,8 @@
(some? limit) (assoc :limit limit)
(some? offset) (assoc :offset offset))))
(defn stale-content-count
(mu/defn stale-content-count
"The total count of rows across every page [[stale-content-rows]] would return for `union-queries`."
[union-queries]
[union-queries :- [:sequential :map]]
(:count (t2/query-one {:select [[:%count.* :count]]
:from (stale-content-union union-queries)})))

View File

@@ -2,19 +2,26 @@
"Application database queries for the support-access-grants module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
(:require
[malli.util :as mut]
[metabase-enterprise.support-access-grants.schema :as support-access-grants.schema]
[metabase.auth-identity.schema :as auth-identity.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.users.schema :as users.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn active-grant-exists?
(mu/defn active-grant-exists?
"Whether a SupportAccessGrantLog is unrevoked and ends after `now`."
[now]
[now :- ms/TemporalInstant]
(t2/exists? :model/SupportAccessGrantLog :revoked_at nil :grant_end_timestamp [:> now]))
(defn grant
(mu/defn grant
"The SupportAccessGrantLog with `grant-id`, or nil."
[grant-id]
[grant-id :- ms/PositiveInt]
(t2/select-one :model/SupportAccessGrantLog :id grant-id))
(defn current-grant
(mu/defn current-grant
"The newest unrevoked SupportAccessGrantLog that has not ended, or nil."
[]
(t2/select-one :model/SupportAccessGrantLog
@@ -32,10 +39,14 @@
(when (seq conditions)
(into [:and] conditions))))
(defn grants-page
(mu/defn grants-page
"The newest-first SupportAccessGrantLogs, optionally narrowed to `ticket-number` and `user-id` and excluding revoked
grants unless `include-revoked?`, paged by `limit` and `offset`."
[include-revoked? ticket-number user-id limit offset]
[include-revoked? :- [:maybe :boolean]
ticket-number :- [:maybe :string]
user-id :- [:maybe ::lib.schema.id/user]
limit :- ms/PositiveInt
offset :- ms/IntGreaterThanOrEqualToZero]
(let [where (grants-where include-revoked? ticket-number user-id)]
(t2/select :model/SupportAccessGrantLog
(cond-> {:limit limit
@@ -43,85 +54,104 @@
:order-by [[:created_at :desc]]}
where (assoc :where where)))))
(defn grant-count
(mu/defn grant-count
"The number of SupportAccessGrantLogs [[grants-page]] would page through."
[include-revoked? ticket-number user-id]
[include-revoked? :- [:maybe :boolean]
ticket-number :- [:maybe :string]
user-id :- [:maybe ::lib.schema.id/user]]
(let [where (grants-where include-revoked? ticket-number user-id)]
(t2/count :model/SupportAccessGrantLog
(cond-> {}
where (assoc :where where)))))
(defn insert-grant!
(mu/defn insert-grant!
"Insert `grant` and return the new instance."
[grant]
[grant :- (mut/select-keys ::support-access-grants.schema/support-access-grant-log.update [:user_id :ticket_number :notes :grant_start_timestamp :grant_end_timestamp])]
(t2/insert-returning-instance! :model/SupportAccessGrantLog grant))
(defn update-grant!
"Apply `changes` to the SupportAccessGrantLog with `grant-id`."
[grant-id changes]
(mu/defn update-grant!
"Apply `changes` to the SupportAccessGrantLog with `grant-id`, returning the number updated."
[grant-id :- ms/PositiveInt
changes :- (mut/select-keys ::support-access-grants.schema/support-access-grant-log.update [:revoked_at :revoked_by_user_id])]
(t2/update! :model/SupportAccessGrantLog grant-id changes))
(defn user
(mu/defn user
"The User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one :model/User user-id))
(defn user-by-email
(mu/defn user-by-email
"The User with `email`, or nil."
[email]
[email :- :string]
(t2/select-one :model/User :email email))
(defn user-superuser-flag-by-email
(mu/defn user-superuser-flag-by-email
"The `:id` and `:is_superuser` of the User with `email`, or nil."
[email]
[email :- :string]
(t2/select-one [:model/User :id :is_superuser] :email email))
(defn user-names-and-emails
(mu/defn user-names-and-emails
"A map of User ID to first name and email for `user-ids`."
[user-ids]
[user-ids :- [:sequential ::lib.schema.id/user]]
(t2/select-pk->fn #(select-keys % [:first_name :email]) [:model/User :id :first_name :email] :id [:in user-ids]))
(defn insert-user!
(mu/defn insert-user!
"Insert `user` and return the new instance."
[user]
[user :- (mut/select-keys ::users.schema/user.update [:email :first_name :last_name :is_superuser])]
(t2/insert-returning-instance! :model/User user))
(defn update-user!
"Apply `changes` to the User with `user-id`."
[user-id changes]
(mu/defn update-user!
"Apply `changes` to the User with `user-id`, returning the number updated."
[user-id :- ::lib.schema.id/user
changes :- (mut/select-keys ::users.schema/user.update [:is_active :is_superuser])]
(t2/update! :model/User user-id changes))
(defn session-exists-for-user?
(mu/defn session-exists-for-user?
"Whether the User with `user-id` has a Session."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/exists? :model/Session :user_id user-id))
(defn delete-sessions-of-user!
"Delete the Sessions of the User with `user-id`."
[user-id]
(mu/defn delete-sessions-of-user!
"Delete the Sessions of the User with `user-id`, returning the number deleted."
[user-id :- ::lib.schema.id/user]
(t2/delete! :model/Session :user_id user-id))
(defn auth-identity-ids-of-user
(mu/defn auth-identity-ids-of-user
"The IDs of the AuthIdentities of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-pks-vec :model/AuthIdentity :user_id user-id))
(defn support-access-auth-identity-id
(mu/defn support-access-auth-identity-id
"The ID of the support-access-grant AuthIdentity of the User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-pk :model/AuthIdentity :user_id user-id :provider "support-access-grant"))
(defn insert-auth-identity!
"Insert the AuthIdentity `row`."
[row]
(mu/defn insert-auth-identity!
"Insert the AuthIdentity `row`, returning the number inserted."
[row :- (mut/select-keys ::auth-identity.schema/auth-identity.update [:user_id :provider :provider_id :expires_at :credentials :metadata])]
(t2/insert! :model/AuthIdentity row))
(defn update-auth-identity!
"Apply `changes` to the AuthIdentity with `auth-identity-id`."
[auth-identity-id changes]
(mu/defn update-auth-identity!
"Apply `changes` to the AuthIdentity with `auth-identity-id`, returning the number updated. `changes` may be a
whole AuthIdentity instance re-saved after a partial edit (e.g. [[metabase.auth-identity.providers.emailed-secret/mark-token-consumed]]
round-trips the full row it was given), so every column is accepted."
[auth-identity-id :- ms/PositiveInt
changes :- [:map {:closed true}
[:id {:optional true} ms/PositiveInt]
[:user_id {:optional true} ::lib.schema.id/user]
[:provider {:optional true} [:maybe [:or :keyword :string]]]
[:provider_id {:optional true} [:maybe :string]]
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]
[:created_at {:optional true} ms/TemporalInstant]
[:updated_at {:optional true} ms/TemporalInstant]
[:credentials {:optional true} [:maybe :map]]
[:metadata {:optional true} [:maybe :map]]]]
(t2/update! :model/AuthIdentity auth-identity-id changes))
(defn expire-auth-identities!
"Set the expiry of the AuthIdentities with `auth-identity-ids` to `expires-at`."
[auth-identity-ids expires-at]
(mu/defn expire-auth-identities!
"Set the expiry of the AuthIdentities with `auth-identity-ids` to `expires-at`, returning the number updated."
[auth-identity-ids :- [:sequential ms/PositiveInt]
expires-at :- ms/TemporalInstant]
(t2/update! :model/AuthIdentity :id [:in auth-identity-ids] {:expires_at expires-at}))

View File

@@ -1,6 +1,7 @@
(ns metabase-enterprise.support-access-grants.schema
"Malli schemas for support access grant API request and response bodies."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
@@ -53,3 +54,30 @@
(mr/def ::current-grant-response
"Schema for GET /api/ee/support-access-grants/current response."
[:maybe ::grant-response])
(mr/def ::support-access-grant-log
"A SupportAccessGrantLog as selected from the app DB: every column of `:support_access_grant_log`."
[:map {:closed true}
[:id ms/PositiveInt]
[:user_id [:maybe ::lib.schema.id/user]]
[:ticket_number [:maybe :string]]
[:notes [:maybe :string]]
[:grant_start_timestamp ms/TemporalInstant]
[:grant_end_timestamp ms/TemporalInstant]
[:revoked_at [:maybe ms/TemporalInstant]]
[:revoked_by_user_id [:maybe ::lib.schema.id/user]]
[:created_at ms/TemporalInstant]
[:updated_at ms/TemporalInstant]])
(mr/def ::support-access-grant-log.update
"What an update (or insert) of a SupportAccessGrantLog accepts: every column of `:support_access_grant_log` except `id`, all optional."
[:map {:closed true}
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:ticket_number {:optional true} [:maybe :string]]
[:notes {:optional true} [:maybe :string]]
[:grant_start_timestamp {:optional true} [:maybe ms/TemporalInstant]]
[:grant_end_timestamp {:optional true} [:maybe ms/TemporalInstant]]
[:revoked_at {:optional true} [:maybe ms/TemporalInstant]]
[:revoked_by_user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]])

View File

@@ -2,32 +2,40 @@
"Application database queries for the tenants module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for model definitions, hydration methods, and transactions."
(:require
[malli.util :as mut]
[metabase-enterprise.tenants.schema :as tenants.schema]
[metabase.collections.schema :as collections.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn tenant
(mu/defn tenant
"The Tenant with `tenant-id`, or nil."
[tenant-id]
[tenant-id :- ms/PositiveInt]
(t2/select-one :model/Tenant :id tenant-id))
(defn tenant-by-slug
(mu/defn tenant-by-slug
"The Tenant with `slug`, or nil."
[slug]
[slug :- :string]
(t2/select-one :model/Tenant :slug slug))
(defn tenant-slug
(mu/defn tenant-slug
"The slug of the Tenant with `tenant-id`."
[tenant-id]
[tenant-id :- ms/PositiveInt]
(t2/select-one-fn :slug :model/Tenant :id tenant-id))
(defn tenant-collection-id
(mu/defn tenant-collection-id
"The root Collection ID of the Tenant with `tenant-id`."
[tenant-id]
[tenant-id :- ms/PositiveInt]
(t2/select-one-fn :tenant_collection_id :model/Tenant :id tenant-id))
(defn tenants-page
(mu/defn tenants-page
"The Tenants in ID order, restricted by `status` (`\"all\"`, `\"active\"`, or `\"deactivated\"`) and paged by the
optional `limit` and `offset`."
[status limit offset]
[status :- [:enum "all" "active" "deactivated"]
limit :- [:maybe ms/PositiveInt]
offset :- [:maybe ms/IntGreaterThanOrEqualToZero]]
(t2/select :model/Tenant (cond-> {:order-by [[:id :asc]]
:where (case status
"all" [:= [:inline 1] [:inline 1]]
@@ -35,7 +43,7 @@
"deactivated" [:= :is_active false])}
limit (assoc :limit limit :offset offset))))
(defn tenant-attributes-reducible
(mu/defn tenant-attributes-reducible
"Reducible attribute maps of the Tenants that have attributes."
[]
(t2/select-fn-reducible :attributes [:model/Tenant :attributes]
@@ -43,41 +51,44 @@
[:not= :attributes nil]
[:not= :attributes "{}"]]}))
(defn tenant-names-and-ids-by-collection
(mu/defn tenant-names-and-ids-by-collection
"A map of root Collection ID to `[name id]` for the Tenants owning `collection-ids`."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select-fn->fn :tenant_collection_id (juxt :name :id) :model/Tenant :tenant_collection_id [:in collection-ids]))
(defn active-tenant-exists?
(mu/defn active-tenant-exists?
"Whether the Tenant with `tenant-id` exists and is active."
[tenant-id]
[tenant-id :- ms/PositiveInt]
(t2/exists? :model/Tenant :id tenant-id :is_active true))
(defn tenant-name-or-slug-exists?
(mu/defn tenant-name-or-slug-exists?
"Whether a Tenant named `tenant-name` or with `slug` exists."
[tenant-name slug]
[tenant-name :- :string
slug :- :string]
(t2/exists? :model/Tenant {:where [:or
[:= :slug slug]
[:= :name tenant-name]]}))
(defn other-tenant-named?
(mu/defn other-tenant-named?
"Whether a Tenant other than `tenant-id` is named `tenant-name`."
[tenant-name tenant-id]
[tenant-name :- :string
tenant-id :- ms/PositiveInt]
(t2/exists? :model/Tenant :name tenant-name :id [:not= tenant-id]))
(defn insert-tenant!
(mu/defn insert-tenant!
"Insert `tenant` and return the new instance."
[tenant]
[tenant :- (mut/select-keys ::tenants.schema/tenant.update [:name :slug :attributes])]
(t2/insert-returning-instance! :model/Tenant tenant))
(defn update-tenant!
"Apply `changes` to the Tenant with `tenant-id`."
[tenant-id changes]
(mu/defn update-tenant!
"Apply `changes` to the Tenant with `tenant-id`, returning the number updated."
[tenant-id :- ms/PositiveInt
changes :- (mut/select-keys ::tenants.schema/tenant.update [:name :attributes :is_active])]
(t2/update! :model/Tenant {:id tenant-id} changes))
(defn active-member-counts
(mu/defn active-member-counts
"Rows of `:tenant_id` and `:count` of active personal Users for `tenant-ids`."
[tenant-ids]
[tenant-ids :- [:sequential ms/PositiveInt]]
(t2/query {:select [[:tenant_id] [[:count :*] :count]]
:from [(t2/table-name :model/User)]
:where [:and
@@ -86,39 +97,41 @@
:is_active]
:group-by [:tenant_id]}))
(defn deactivate-tenant-users!
"Deactivate the active Users of the Tenant with `tenant-id`, marking them as deactivated with it."
[tenant-id]
(mu/defn deactivate-tenant-users!
"Deactivate the active Users of the Tenant with `tenant-id`, marking them as deactivated with it, returning the
number updated."
[tenant-id :- ms/PositiveInt]
(t2/update! :model/User {:is_active true :tenant_id tenant-id} {:is_active false :deactivated_with_tenant true}))
(defn reactivate-tenant-users!
"Reactivate the Users of the Tenant with `tenant-id` that were deactivated with it."
[tenant-id]
(mu/defn reactivate-tenant-users!
"Reactivate the Users of the Tenant with `tenant-id` that were deactivated with it, returning the number updated."
[tenant-id :- ms/PositiveInt]
(t2/update! :model/User {:is_active false :tenant_id tenant-id :deactivated_with_tenant true}
{:is_active true :deactivated_with_tenant nil}))
(defn user-tenant-id
(mu/defn user-tenant-id
"The Tenant ID of the User with `user-id`."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-fn :tenant_id :model/User :id user-id))
(defn collection-with-archived-state
(mu/defn collection-with-archived-state
"The Collection with `collection-id` if its archived flag is `archived?`, or nil."
[collection-id archived?]
[collection-id :- ::lib.schema.id/collection
archived? :- :boolean]
(t2/select-one :model/Collection :id collection-id :archived archived?))
(defn descendant-collection-ids
(mu/defn descendant-collection-ids
"The IDs of the Collections under the Collection with `collection-id`."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/select-pks-set :model/Collection :location [:like (str "/" collection-id "/%")]))
(defn insert-collection!
(mu/defn insert-collection!
"Insert `collection` and return its ID."
[collection]
[collection :- ::collections.schema/collection.update]
(t2/insert-returning-pk! :model/Collection collection))
(defn delete-permissions-with-objects!
"Delete the Permissions rows for `objects`."
[objects]
(mu/defn delete-permissions-with-objects!
"Delete the Permissions rows for `objects`, returning the number deleted."
[objects :- [:sequential :string]]
(t2/query-one {:delete-from :permissions
:where [:in :object objects]}))

View File

@@ -0,0 +1,33 @@
(ns metabase-enterprise.tenants.schema
"Malli schemas for the tenants module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::tenant.attributes
"The `:attributes` column of a Tenant, decoded."
[:map-of :string [:maybe [:or :string number? :boolean]]])
(mr/def ::tenant
"A Tenant as selected from the app DB: every column of `:tenant`."
[:map {:closed true}
[:id ms/PositiveInt]
[:name :string]
[:slug :string]
[:is_active :boolean]
[:updated_at ms/TemporalInstant]
[:created_at ms/TemporalInstant]
[:attributes [:maybe ::tenant.attributes]]
[:tenant_collection_id ::lib.schema.id/collection]])
(mr/def ::tenant.update
"What an update (or insert) of a Tenant accepts: every column of `:tenant` except `id`, all optional."
[:map {:closed true}
[:name {:optional true} [:maybe :string]]
[:slug {:optional true} [:maybe :string]]
[:is_active {:optional true} [:maybe :boolean]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:attributes {:optional true} [:maybe ::tenant.attributes]]
[:tenant_collection_id {:optional true} [:maybe ::lib.schema.id/collection]]])

View File

@@ -2,16 +2,21 @@
"Application database queries for the transforms module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn cache-table-dependencies!
"Store `dependencies-json` as the cached table dependencies of the Transform with `transform-id`."
[transform-id dependencies-json]
(mu/defn cache-table-dependencies!
"Store `dependencies-json` as the cached table dependencies of the Transform with `transform-id`, returning the
number updated."
[transform-id :- ::lib.schema.id/transform
dependencies-json :- :string]
(t2/update! (t2/table-name :model/Transform) transform-id {:table_dependencies dependencies-json}))
(defn succeeded-run-counts-by-meter
(mu/defn succeeded-run-counts-by-meter
"Rows of `:metered_as` and `:cnt` of the TransformRuns that succeeded on `date`."
[date]
[date :- ms/TemporalInstant]
(t2/query {:select [:r.metered_as [[:count :r.id] :cnt]]
:from [[:transform_run :r]]
:where [:and

View File

@@ -2,19 +2,21 @@
"Application database queries for the transforms-inspector module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn table-source-rows
(mu/defn table-source-rows
"The ID, name, schema, and Database ID of the Tables with `table-ids`, as source info."
[table-ids]
[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]))
(defn database-engine
(mu/defn database-engine
"The engine of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one-fn :engine :model/Database :id database-id))
(defn active-fields-for-table
(mu/defn active-fields-for-table
"The active Fields of the Table with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select :model/Field :table_id table-id :active true))

View File

@@ -3,58 +3,63 @@
additional logic, so the rest of the module only touches `toucan2.core` for model definitions and hydration methods."
(:require
[metabase.app-db.core :as mdb]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn table-database-ids
(mu/defn table-database-ids
"The set of Database IDs of the Tables with `table-ids`."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/select-fn-set :db_id [:model/Table :db_id] :id [:in table-ids]))
(defn table-database-id
(mu/defn table-database-id
"The Database ID of the raw table row with `table-id`."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one-fn :db_id (t2/table-name :model/Table) :id table-id))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database database-id))
(defn database-engine
(mu/defn database-engine
"The engine of the Database with `database-id`."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one-fn :engine :model/Database database-id))
(defn update-run-message!
(mu/defn update-run-message!
"Set the message of the TransformRun with `run-id`."
[run-id message]
[run-id :- ms/PositiveInt
message :- [:maybe :string]]
(t2/update! :model/TransformRun :id run-id {:message message}))
(defn python-library
(mu/defn python-library
"The PythonLibrary with `library-id`, or nil."
[library-id]
[library-id :- ms/PositiveInt]
(t2/select-one :model/PythonLibrary library-id))
(defn python-library-by-path
(mu/defn python-library-by-path
"The PythonLibrary at `path`, or nil."
[path]
[path :- :string]
(t2/select-one :model/PythonLibrary :path path))
(defn upsert-python-library-source!
(mu/defn upsert-python-library-source!
"Insert or update the PythonLibrary at `path`, setting its source to `source`. Returns the ID of the row."
[path source]
[path :- :string
source :- :string]
(mdb/update-or-insert! :model/PythonLibrary
{:path path}
(constantly {:path path :source source})))
(defn library-sources-by-path
(mu/defn library-sources-by-path
"A map of path to source for every PythonLibrary."
[]
(t2/select-fn->fn :path :source :model/PythonLibrary))
(defn top-level-fields-metadata
(mu/defn top-level-fields-metadata
"The export metadata columns of the active top-level Fields of the Table with `table-id`, in database order."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select [:model/Field :id :name :base_type :effective_type :semantic_type :database_type :database_position]
:table_id table-id
:active true

View File

@@ -0,0 +1,24 @@
(ns metabase-enterprise.transforms-python.schema
"Malli schemas for the transforms-python module."
(:require
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::python-library
"A PythonLibrary as selected from the app DB: every column of `:python_library`."
[:map {:closed true}
[:id ms/PositiveInt]
[:path :string]
[:source [:maybe [:or :keyword :string]]]
[:created_at ms/TemporalInstant]
[:updated_at ms/TemporalInstant]
[:entity_id :string]])
(mr/def ::python-library.update
"What an update (or insert) of a PythonLibrary accepts: every column of `:python_library` except `id`, all optional."
[:map {:closed true}
[:path {:optional true} [:maybe :string]]
[:source {:optional true} [:maybe [:or :keyword :string]]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
[:entity_id {:optional true} [:maybe :string]]])

View File

@@ -2,24 +2,26 @@
"Application database queries for the upload-management module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn attached-dwh-database-id
(mu/defn attached-dwh-database-id
"The ID of the attached data warehouse Database, or nil."
[]
(t2/select-one-fn :id :model/Database :is_attached_dwh true))
(defn non-upload-tables-for-database
(mu/defn non-upload-tables-for-database
"The active Tables of the Database with `database-id` that were not uploaded."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select :model/Table :db_id database-id :active true :is_upload false))
(defn upload-tables
(mu/defn upload-tables
"The active uploaded Tables, ordered by name."
[]
(t2/select :model/Table :active true :is_upload true {:order-by [[:name :asc]]}))
(defn table
(mu/defn table
"The Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one :model/Table table-id))

View File

@@ -3,14 +3,14 @@
(:require
[clojure.test :refer :all]
[metabase-enterprise.semantic-search.test-util :as semantic.tu]
[metabase.metabot.db :as metabot.db]
[metabase.metabot.tools.search :as search]
[metabase.permissions.core :as perms]
[metabase.search.core :as search-core]
[metabase.search.engine :as search.engine]
[metabase.search.ingestion :as search.ingestion]
[metabase.search.test-util :as search.tu]
[metabase.test :as mt]
[toucan2.core :as t2]))
[metabase.test :as mt]))
(use-fixtures :once #'semantic.tu/once-fixture)
@@ -109,9 +109,9 @@
(testing "search with metabot verified-or-curated content flag"
(let [metabot {:entity_id "test-bot"
:use_verified_content true}]
(with-redefs [t2/select-one (fn [model & _]
(is (= :model/Metabot model) "Should query for Metabot model")
metabot)
(with-redefs [metabot.db/metabot-by-entity-id (fn [entity-id]
(is (= "test-bot" entity-id) "Should look up the Metabot")
metabot)
search-core/search (fn [context]
;; use_verified_content now drives the curated filter, not :verified
(is (true? (:curated? context)))

View File

@@ -377,7 +377,8 @@
(is (some #(str/includes? % "collection") (keys files-after-export)))
(is (some #(str/includes? % "card") (keys files-after-export))))
(t2/delete! :model/RemoteSyncTask :id (:id export-task))
(let [import-task (t2/with-connection [_conn (app-db/app-db) (t2/insert-returning-instance! :model/RemoteSyncTask {:sync_task_type "import" :initiated_by (mt/user->id :rasta)})])
(let [import-task (t2/with-connection [_conn (app-db/app-db)]
(t2/insert-returning-instance! :model/RemoteSyncTask {:sync_task_type "import" :initiated_by (mt/user->id :rasta)}))
import-result (impl/import! (source.p/snapshot mock-main) (:id import-task))]
(remote-sync.task/complete-sync-task! (:id import-task))
(is (= :success (:status import-result)))

View File

@@ -12,8 +12,8 @@
[metabase.analytics-interface.core :as analytics]
[metabase.app-db.core :as mdb]
[metabase.search.appdb.core :as appdb]
[metabase.search.db :as search.db]
[metabase.search.engine :as search.engine]
[metabase.search.in-place.legacy :as in-place.legacy]
[metabase.search.in-place.scoring :as in-place.scoring]
[metabase.search.settings :as search.settings]
[metabase.test :as mt])
@@ -131,7 +131,7 @@
(fn [result _]
{:result (dissoc result :score)
:score (:score result)})
in-place.legacy/results
search.db/in-place-search-reducible
(fn [_]
(reset! legacy-called? true)
(reify clojure.lang.IReduceInit

View File

@@ -91,8 +91,8 @@
analytics
analytics-interface
api
api-scope
api-keys
api-scope
appearance
audit-app
auth-identity
@@ -120,16 +120,19 @@
explorations
formatter
geojson
indexed-entities
initialization-status
interestingness
internal-stats
llm
login-history
mcp
measures
metabot
mq
notification
oauth-server
osi
permissions
premium-features
public-sharing
@@ -139,6 +142,7 @@
sample-data
search
secrets
segments
server
session
settings
@@ -158,7 +162,8 @@
util
version
view-log
warehouse-schema})
warehouse-schema
xrays})
(defn- affected-modules
"Set of modules that are direct or indirect dependents of `modules`, and thus are affected by changes to them.

View File

@@ -2,24 +2,29 @@
"Application database queries for the bigquery-cloud-sdk driver. Every function here is a direct Toucan 2 call with
no additional logic, so the rest of the driver never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[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]}
[toucan2.core :as t2]))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database database-id))
(defn update-database-details!
"Set the details of the Database with `database-id`."
[database-id details]
(mu/defn update-database-details!
"Set the details of the Database with `database-id`, returning the number updated."
[database-id :- ::lib.schema.id/database
details :- ::warehouses.schema/database.details]
(t2/update! :model/Database database-id {:details details}))
(defn set-table-schemas!
(mu/defn set-table-schemas!
"Set the schema of the Tables of the Database with `database-id` that do not have `schema` yet, overwriting any
stale schema they already have."
[database-id schema]
stale schema they already have, returning the number updated."
[database-id :- ::lib.schema.id/database
schema :- :string]
(t2/query-one {:update (t2/table-name :model/Table)
:set {:schema schema}
:where [:and
@@ -28,7 +33,8 @@
[:= :schema nil]
[:not= :schema schema]]]}))
(defn active-partitioned-field-exists?
(mu/defn active-partitioned-field-exists?
"Whether the Table with `table-id` has an active database-partitioned Field named `field-name`."
[table-id 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))

View File

@@ -2,232 +2,241 @@
"Application database queries for the actions module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
(:require
[metabase.actions.schema :as actions.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn database-for-action
(mu/defn database-for-action
"The Database of the model Card of the Action with `action-id`, or nil."
[action-id]
[action-id :- ::lib.schema.id/action]
(t2/select-one :model/Database {:select [:db.*]
:from :action
:join [[:report_card :card] [:= :card.id :action.model_id]
[:metabase_database :db] [:= :db.id :card.database_id]]
:where [:= :action.id action-id]}))
(defn table-database-id
(mu/defn table-database-id
"The Database id of the Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one-fn :db_id [:model/Table :db_id] table-id))
(defn card-query
(mu/defn card-query
"The query of the Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one-fn :dataset_query :model/Card :id card-id))
(defn card
(mu/defn card
"The Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one :model/Card :id card-id))
(defn cards
(mu/defn cards
"The Cards with `card-ids`."
[card-ids]
[card-ids :- [:set ::lib.schema.id/card]]
(t2/select :model/Card :id [:in card-ids]))
(defn cards-by-id
(mu/defn cards-by-id
"A map of Card id to Card for the Cards with `card-ids`."
[card-ids]
[card-ids :- [:sequential ::lib.schema.id/card]]
(t2/select-pk->fn identity :model/Card :id [:in card-ids]))
(defn card-type
(mu/defn card-type
"The type of the Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one-fn :type [:model/Card :type :card_schema] :id card-id))
(defn table
(mu/defn table
"The Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one :model/Table :id table-id))
(defn tables
(mu/defn tables
"The Tables with `table-ids`."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/select :model/Table :id [:in table-ids]))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database :id database-id))
(defn dashcard-in-dashboard
(mu/defn dashcard-in-dashboard
"The DashboardCard with `dashcard-id` on the Dashboard with `dashboard-id`, or nil."
[dashcard-id dashboard-id]
[dashcard-id :- ::lib.schema.id/dashcard
dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-one :model/DashboardCard :id dashcard-id :dashboard_id dashboard-id))
(defn dashcard-dashboard-id
(mu/defn dashcard-dashboard-id
"The `:dashboard_id` of the DashboardCard with `dashcard-id`, or nil."
[dashcard-id]
[dashcard-id :- ::lib.schema.id/dashcard]
(t2/select-one-fn :dashboard_id [:model/DashboardCard :dashboard_id] dashcard-id))
(defn dashcard-action-id
(mu/defn dashcard-action-id
"The Action id of the DashboardCard with `dashcard-id`, or nil."
[dashcard-id]
[dashcard-id :- ::lib.schema.id/dashcard]
(t2/select-one-fn :action_id :model/DashboardCard :id dashcard-id))
(defn delete-dashcards-for-action!
"Delete the DashboardCards of the Action with `action-id`."
[action-id]
(mu/defn delete-dashcards-for-action!
"Delete the DashboardCards of the Action with `action-id`, returning the number deleted."
[action-id :- ::lib.schema.id/action]
(t2/delete! :model/DashboardCard :action_id action-id))
(defn insert-action!
(mu/defn insert-action!
"Insert the Action `row` and return the inserted instance."
[row]
[row :- ::actions.schema/action.for-insert]
(t2/insert-returning-instance! :model/Action row))
(defn update-action!
"Apply `changes` to the Action with `action-id`."
[action-id changes]
(mu/defn update-action!
"Apply `changes` to the Action with `action-id`, returning the number updated."
[action-id :- ::lib.schema.id/action
changes :- ::actions.schema/action.for-update]
(t2/update! :model/Action action-id changes))
(defn insert-query-action!
"Insert the QueryAction `action`."
[action]
(mu/defn insert-query-action!
"Insert the QueryAction `action`, returning the number inserted."
[action :- ::actions.schema/query-action.update]
(t2/insert! :model/QueryAction action))
(defn insert-http-action!
"Insert the HTTPAction `action`."
[action]
(mu/defn insert-http-action!
"Insert the HTTPAction `action`, returning the number inserted."
[action :- ::actions.schema/httpaction.update]
(t2/insert! :model/HTTPAction action))
(defn insert-implicit-action!
"Insert the ImplicitAction `action`."
[action]
(mu/defn insert-implicit-action!
"Insert the ImplicitAction `action`, returning the number inserted."
[action :- ::actions.schema/implicit-action.update]
(t2/insert! :model/ImplicitAction action))
(defn update-query-action!
"Apply `changes` to the QueryAction with `action-id`."
[action-id changes]
(mu/defn update-query-action!
"Apply `changes` to the QueryAction with `action-id`, returning the number updated."
[action-id :- ::lib.schema.id/action
changes :- ::actions.schema/query-action.update]
(t2/update! :model/QueryAction action-id changes))
(defn update-http-action!
"Apply `changes` to the HTTPAction with `action-id`."
[action-id changes]
(mu/defn update-http-action!
"Apply `changes` to the HTTPAction with `action-id`, returning the number updated."
[action-id :- ::lib.schema.id/action
changes :- ::actions.schema/httpaction.update]
(t2/update! :model/HTTPAction action-id changes))
(defn update-implicit-action!
"Apply `changes` to the ImplicitAction with `action-id`."
[action-id changes]
(mu/defn update-implicit-action!
"Apply `changes` to the ImplicitAction with `action-id`, returning the number updated."
[action-id :- ::lib.schema.id/action
changes :- ::actions.schema/implicit-action.update]
(t2/update! :model/ImplicitAction action-id changes))
(defn delete-query-action!
"Delete the QueryAction of the Action with `action-id`."
[action-id]
(mu/defn delete-query-action!
"Delete the QueryAction of the Action with `action-id`, returning the number deleted."
[action-id :- ::lib.schema.id/action]
(t2/delete! :model/QueryAction :action_id action-id))
(defn delete-http-action!
"Delete the HTTPAction of the Action with `action-id`."
[action-id]
(mu/defn delete-http-action!
"Delete the HTTPAction of the Action with `action-id`, returning the number deleted."
[action-id :- ::lib.schema.id/action]
(t2/delete! :model/HTTPAction :action_id action-id))
(defn delete-implicit-action!
"Delete the ImplicitAction of the Action with `action-id`."
[action-id]
(mu/defn delete-implicit-action!
"Delete the ImplicitAction of the Action with `action-id`, returning the number deleted."
[action-id :- ::lib.schema.id/action]
(t2/delete! :model/ImplicitAction :action_id action-id))
(defn query-actions
(mu/defn query-actions
"The QueryActions of the Actions with `action-ids`."
[action-ids]
[action-ids :- [:sequential ::lib.schema.id/action]]
(t2/select :model/QueryAction :action_id [:in action-ids]))
(defn query-action
(mu/defn query-action
"The QueryAction of the Action with `action-id`, or nil."
[action-id]
[action-id :- ::lib.schema.id/action]
(t2/select-one :model/QueryAction :action_id action-id))
(defn http-actions
(mu/defn http-actions
"The HTTPActions of the Actions with `action-ids`."
[action-ids]
[action-ids :- [:sequential ::lib.schema.id/action]]
(t2/select :model/HTTPAction :action_id [:in action-ids]))
(defn implicit-actions
(mu/defn implicit-actions
"The ImplicitActions of the Actions with `action-ids`."
[action-ids]
[action-ids :- [:sequential ::lib.schema.id/action]]
(t2/select :model/ImplicitAction :action_id [:in action-ids]))
(defn actions-with-id
(mu/defn actions-with-id
"The Actions with `action-id` (usually a single Action, since ids are unique)."
[action-id]
[action-id :- [:maybe ::lib.schema.id/action]]
(t2/select :model/Action :id action-id))
(defn actions-with-ids
(mu/defn actions-with-ids
"The Actions whose `:id` is in `action-ids`."
[action-ids]
[action-ids :- [:sequential ::lib.schema.id/action]]
(t2/select :model/Action :id [:in action-ids]))
(defn unarchived-action-with-id
(mu/defn unarchived-action-with-id
"The unarchived Actions with `action-id` (usually a single Action, since ids are unique)."
[action-id]
[action-id :- [:maybe ::lib.schema.id/action]]
(t2/select :model/Action :id action-id :archived false))
(defn action-with-entity-id
(mu/defn action-with-entity-id
"The Actions with `entity-id` (usually a single Action, since entity ids are unique)."
[entity-id]
[entity-id :- :string]
(t2/select :model/Action :entity_id entity-id))
(defn actions-of-type
(mu/defn actions-of-type
"The Actions of `action-type`."
[action-type]
[action-type :- :keyword]
(t2/select :model/Action :type action-type))
(defn unarchived-actions-for-models
(mu/defn unarchived-actions-for-models
"The unarchived Actions whose `:model_id` is in `model-ids`."
[model-ids]
[model-ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Action :model_id [:in model-ids] :archived false))
(defn unarchived-non-http-actions-for-model
(mu/defn unarchived-non-http-actions-for-model
"The unarchived, non-HTTP Actions of the model Card with `model-id`."
[model-id]
[model-id :- ms/PositiveInt]
(t2/select :model/Action :model_id model-id :archived false :type [:not= "http"]))
(defn unarchived-non-http-actions-for-models
(mu/defn unarchived-non-http-actions-for-models
"The unarchived, non-HTTP Actions whose `:model_id` is in `model-ids`."
[model-ids]
[model-ids :- [:set ms/PositiveInt]]
(t2/select :model/Action :model_id [:in model-ids] :archived false :type [:not= "http"]))
(defn fields-for-parameters
(mu/defn fields-for-parameters
"The id, base type, display name, and description of the Fields with `field-ids`."
[field-ids]
[field-ids :- [:set ::lib.schema.id/field]]
(t2/select [:model/Field :id :base_type :display_name :description] :id [:in field-ids]))
(defn action-database-settings
(mu/defn action-database-settings
"The id and Database settings of the Actions with `action-ids`."
[action-ids]
[action-ids :- [:sequential ::lib.schema.id/action]]
(t2/query {:select [:action.id :db.settings]
:from :action
:join [[:report_card :card] [:= :card.id :action.model_id]
[:metabase_database :db] [:= :db.id :card.database_id]]
:where [:in :action.id action-ids]}))
(defn card-scope-columns
(mu/defn card-scope-columns
"The query, Collection id, Database id, and display of the Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one [:model/Card :dataset_query :collection_id :database_id :display] card-id))
(defn dashboard-collection-id
(mu/defn dashboard-collection-id
"The Collection id of the Dashboard with `dashboard-id`, or nil."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-one-fn :collection_id [:model/Dashboard :collection_id] dashboard-id))
(defn destination-database-exists-for-router?
(mu/defn destination-database-exists-for-router?
"Whether the Database with `database-id` has routing destinations."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Database :router_database_id database-id))
(defn writable-table-exists?
(mu/defn writable-table-exists?
"Whether the Database with `database-id` has a writable Table."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Table :db_id database-id :is_writable true))
(defn table-with-unknown-writability-exists?
(mu/defn table-with-unknown-writability-exists?
"Whether the Database with `database-id` has a Table whose writability is unknown."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/exists? :model/Table :db_id database-id :is_writable nil))

View File

@@ -177,3 +177,51 @@
(mr/def ::action.for-update
"Schema for updating an Action (REST API or internally)."
(action-schema :update))
(mr/def ::httpaction
"A HTTPAction as selected from the app DB: every column of `:http_action`."
[:map {:closed true}
[:action_id ::lib.schema.id/action]
[:template ::http-action.template]
[:response_handle [:maybe :string]]
[:error_handle [:maybe :string]]])
(mr/def ::httpaction.update
"What an update (or insert) of a HTTPAction accepts: every column of `:http_action` except `id`, all optional."
[:map {:closed true}
[:action_id {:optional true} [:maybe ::lib.schema.id/action]]
[:template {:optional true} [:maybe ::http-action.template]]
[:response_handle {:optional true} [:maybe :string]]
[:error_handle {:optional true} [:maybe :string]]])
(mr/def ::implicit-action.row
"A ImplicitAction as selected from the app DB: every column of `:implicit_action`."
[:map {:closed true}
[:action_id ::lib.schema.id/action]
[:kind [:or :keyword :string]]])
(mr/def ::implicit-action.update
"What an update (or insert) of a ImplicitAction accepts: every column of `:implicit_action` except `id`, all optional."
[:map {:closed true}
[:action_id {:optional true} [:maybe ::lib.schema.id/action]]
[:kind {:optional true} [:maybe [:or :keyword :string]]]])
(mr/def ::query-action.dataset-query
"The `:dataset_query` column of a QueryAction, decoded."
:map)
(mr/def ::query-action.row
"A QueryAction as selected from the app DB: every column of `:query_action`."
[:map {:closed true}
[:action_id ::lib.schema.id/action]
[:database_id ::lib.schema.id/database]
[:dataset_query ::query-action.dataset-query]
[:legacy_query [:maybe :string]]])
(mr/def ::query-action.update
"What an update (or insert) of a QueryAction accepts: every column of `:query_action` except `id`, all optional."
[:map {:closed true}
[:action_id {:optional true} [:maybe ::lib.schema.id/action]]
[:database_id {:optional true} [:maybe ::lib.schema.id/database]]
[:dataset_query {:optional true} [:maybe ::query-action.dataset-query]]
[:legacy_query {:optional true} [:maybe :string]]])

View File

@@ -3,9 +3,11 @@
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
(:require
[metabase.collections.models.collection :as collection]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn unarchived-models-visible-to-user
(mu/defn unarchived-models-visible-to-user
"The unarchived model Cards in Collections the current user can read."
[]
(t2/select :model/Card {:where [:and
@@ -13,22 +15,24 @@
[:= :archived false]
(collection/visible-collection-filter-clause)]}))
(defn public-actions
(mu/defn public-actions
"The name, id, public uuid, and model id of the unarchived Actions that are publicly shared."
[]
(t2/select [:model/Action :name :id :public_uuid :model_id], :public_uuid [:not= nil], :archived false))
(defn delete-action!
(mu/defn delete-action!
"Delete the Action with `action-id`."
[action-id]
[action-id :- ::lib.schema.id/action]
(t2/delete! :model/Action :id action-id))
(defn database
(mu/defn database
"The Database with `database-id`, or nil."
[database-id]
[database-id :- ::lib.schema.id/database]
(t2/select-one :model/Database :id database-id))
(defn set-action-public-uuid!
(mu/defn set-action-public-uuid!
"Set the public uuid of the Action with `action-id` and the User who made it public."
[action-id public-uuid made-public-by-id]
[action-id :- ::lib.schema.id/action
public-uuid :- [:maybe :string]
made-public-by-id :- [:maybe ::lib.schema.id/user]]
(t2/update! :model/Action action-id {:public_uuid public-uuid, :made_public_by_id made-public-by-id}))

View File

@@ -2,13 +2,17 @@
"Application database queries for the activity feed module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
(:require
[metabase.activity-feed.schema :as activity-feed.schema]
[metabase.collections.models.collection :as collection]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn recent-cards
(mu/defn recent-cards
"The recently viewed Cards with `ids`, with their Collection and Dashboard names."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select [:model/Card
:id :name :collection_id :description :display
:dataset_query :type :archived :card_schema
@@ -18,9 +22,9 @@
:left-join [:collection [:= :collection.id :report_card.collection_id]
[:report_dashboard :dashboard] [:= :dashboard.id :report_card.dashboard_id]]}))
(defn recent-dashboards
(mu/defn recent-dashboards
"The recently viewed Dashboards with `ids`, with their Collection names."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select [:model/Dashboard
:id :name :collection_id :description
:archived
@@ -28,9 +32,9 @@
{:where [:in :report_dashboard.id ids]
:left-join [:collection [:= :collection.id :report_dashboard.collection_id]]}))
(defn recent-tables
(mu/defn recent-tables
"The recently viewed Tables with `ids`, with their Database names and sync status."
[ids]
[ids :- [:sequential ms/PositiveInt]]
(t2/select [:model/Table
:id :name :db_id :active
:display_name [:metabase_database.initial_sync_status :initial-sync-status]
@@ -39,10 +43,10 @@
{:where [:in :metabase_table.id ids]
:left-join [:metabase_database [:= :metabase_database.id :metabase_table.db_id]]}))
(defn recent-dashboard-and-table-views
(mu/defn recent-dashboard-and-table-views
"Up to `limit` most recently viewed unarchived, active Dashboards and Tables with their view counts and last
viewer."
[limit]
[limit :- ms/PositiveInt]
(t2/select [:model/RecentViews
[[:min :recent_views.user_id] :user_id]
:model
@@ -66,9 +70,9 @@
[:= :model "table"]
[:= :t.id :model_id]]]}))
(defn recent-card-runs
(mu/defn recent-card-runs
"Up to `limit` most recently run question Cards with their run counts and last runner."
[limit]
[limit :- ms/PositiveInt]
(t2/select [:model/QueryExecution
[:%min.executor_id :user_id]
[:query_execution.card_id :model_id]
@@ -80,70 +84,75 @@
:order-by [[:max_ts :desc]]
:limit limit}))
(defn card-exists?
(mu/defn card-exists?
"Whether a Card with `id` exists."
[id]
[id :- ::lib.schema.id/card]
(t2/exists? :model/Card :id id))
(defn dashboard-exists?
(mu/defn dashboard-exists?
"Whether a Dashboard with `id` exists."
[id]
[id :- ::lib.schema.id/dashboard]
(t2/exists? :model/Dashboard :id id))
(defn table-exists?
(mu/defn table-exists?
"Whether a Table with `id` exists."
[id]
[id :- ::lib.schema.id/table]
(t2/exists? :model/Table :id id))
(defn collection-exists?
(mu/defn collection-exists?
"Whether a Collection with `id` exists."
[id]
[id :- ::lib.schema.id/collection]
(t2/exists? :model/Collection :id id))
(defn document-exists?
(mu/defn document-exists?
"Whether a Document with `id` exists."
[id]
[id :- ms/PositiveInt]
(t2/exists? :model/Document :id id))
(defn card
(mu/defn card
"The Card with `id`, or nil."
[id]
[id :- ::lib.schema.id/card]
(t2/select-one :model/Card :id id))
(defn table
(mu/defn table
"The Table with `id`, or nil."
[id]
[id :- ::lib.schema.id/table]
(t2/select-one :model/Table :id id))
(defn collection
(mu/defn collection
"The Collection with `id`, or nil."
[id]
[id :- ::lib.schema.id/collection]
(t2/select-one :model/Collection :id id))
(defn document
(mu/defn document
"The Document with `id`, or nil."
[id]
[id :- ms/PositiveInt]
(t2/select-one :model/Document :id id))
(defn dashboard
(mu/defn dashboard
"The Dashboard with `dashboard-id`, or nil."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-one :model/Dashboard :id dashboard-id))
(defn card-document-id
(mu/defn card-document-id
"The Document id of the Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one-fn :document_id :model/Card :id card-id))
(defn recent-views-for-user-context
(mu/defn recent-views-for-user-context
"The RecentViews of the User with `user-id` in `context`, newest first."
[user-id context]
[user-id :- ::lib.schema.id/user
context :- [:or :keyword :string]]
(t2/select :model/RecentViews :user_id user-id :context context {:order-by [[:timestamp :desc]]}))
(defn recent-view-ids-to-prune
(mu/defn recent-view-ids-to-prune
"The ids of the RecentViews of the User with `user-id` for `db-model` in `context` beyond the newest `keep` of
them, restricted to the Cards of `card-type` when non-nil."
[db-model user-id context card-type keep]
[db-model :- :string
user-id :- ::lib.schema.id/user
context :- :string
card-type :- [:maybe :string]
keep :- ms/IntGreaterThanOrEqualToZero]
(t2/select-fn-set :id
:model/RecentViews
{:select [:rv.id]
@@ -163,19 +172,20 @@
:limit 100000
:offset keep}))
(defn insert-recent-views!
(mu/defn insert-recent-views!
"Insert the RecentViews `rows`."
[rows]
[rows :- [:sequential ::activity-feed.schema/recent-views.update]]
(t2/insert! :model/RecentViews rows))
(defn delete-recent-views!
(mu/defn delete-recent-views!
"Delete the RecentViews with `ids`."
[ids]
[ids :- [:set ms/PositiveInt]]
(t2/delete! :model/RecentViews :id [:in ids]))
(defn most-recently-viewed-dashboard-id
(mu/defn most-recently-viewed-dashboard-id
"The id of the unarchived Dashboard the User with `user-id` viewed most recently after `since`, or nil."
[user-id since]
[user-id :- ::lib.schema.id/user
since :- ms/TemporalInstant]
(t2/select-one-fn
:model_id
:model/RecentViews
@@ -188,9 +198,9 @@
:left-join [[:report_dashboard :d]
[:= :recent_views.model_id :d.id]]}))
(defn cards-for-recent-views
(mu/defn cards-for-recent-views
"The Cards with `card-ids` with their Dashboard, Collection, and moderation status."
[card-ids]
[card-ids :- [:sequential ::lib.schema.id/card]]
(t2/select :model/Card
{:select [:card.name
:card.description
@@ -224,9 +234,9 @@
[:report_dashboard :dashboard]
[:= :dashboard.id :card.dashboard_id]]}))
(defn dashboards-for-recent-views
(mu/defn dashboards-for-recent-views
"The Dashboards with `dashboard-ids` with their Collection and moderation status."
[dashboard-ids]
[dashboard-ids :- [:sequential ::lib.schema.id/dashboard]]
(t2/select :model/Dashboard
{:select [:dash.id
:dash.name
@@ -249,9 +259,9 @@
[:= :c.id :dash.collection_id]
[:= :c.archived false]]]}))
(defn unarchived-collections-with-details
(mu/defn unarchived-collections-with-details
"The unarchived Collections with `collection-ids`, with their location, type, and authority level."
[collection-ids]
[collection-ids :- [:sequential ::lib.schema.id/collection]]
(t2/select :model/Collection
{:select [:id :name :description :authority_level
:archived :location :type]
@@ -259,9 +269,9 @@
[:in :id collection-ids]
[:= :archived false]]}))
(defn visible-tables-for-recent-views
(mu/defn visible-tables-for-recent-views
"The non-hidden Tables with `table-ids` with their Database name and sync status."
[table-ids]
[table-ids :- [:sequential ::lib.schema.id/table]]
(t2/select :model/Table
{:select [:t.id :t.name :t.description
:t.display_name :t.active :t.visibility_type :t.schema
@@ -277,11 +287,15 @@
:left-join [[:metabase_database :db]
[:= :db.id :t.db_id]]}))
(defn recent-views-with-card-type
(mu/defn recent-views-with-card-type
"The RecentViews of the User with `user-id` in `contexts`, newest first, with the type of the viewed Card. Narrowed
to `db-models` and to the Cards of `card-types` when given; excludes trashed and namespaced Collections, exploration
Documents, and, when `selections?`, the instance analytics Collection."
[user-id contexts db-models card-types selections?]
[user-id :- ::lib.schema.id/user
contexts :- [:sequential :string]
db-models :- [:maybe [:sequential :string]]
card-types :- [:sequential :string]
selections? :- :boolean]
(t2/select :model/RecentViews
{:select [:rv.* [:rc.type :card_type]]
:from [[:recent_views :rv]]
@@ -325,9 +339,9 @@
[:= :doc.id :rv.model_id]]]
:order-by [[:rv.timestamp :desc]]}))
(defn documents-for-recent-views
(mu/defn documents-for-recent-views
"The Documents with `document-ids` with their Collection."
[document-ids]
[document-ids :- [:sequential ms/PositiveInt]]
(t2/select :model/Document
{:select [:d.id
:d.name

View File

@@ -0,0 +1,25 @@
(ns metabase.activity-feed.schema
"Malli schemas for the activity-feed module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::recent-views
"A RecentViews as selected from the app DB: every column of `:recent_views`."
[:map {:closed true}
[:id ms/PositiveInt]
[:user_id ::lib.schema.id/user]
[:model [:or :keyword :string]]
[:model_id [:maybe :int]]
[:timestamp ms/TemporalInstant]
[:context [:or :keyword :string]]])
(mr/def ::recent-views.update
"What an update (or insert) of a RecentViews accepts: every column of `:recent_views` except `id`, all optional."
[:map {:closed true}
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:model {:optional true} [:maybe [:or :keyword :string]]]
[:model_id {:optional true} [:maybe :int]]
[:timestamp {:optional true} [:maybe ms/TemporalInstant]]
[:context {:optional true} [:maybe [:or :keyword :string]]]])

View File

@@ -2,71 +2,78 @@
"Application database queries for the agent API module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module only touches `toucan2.core` for hydration."
(:require
[malli.util :as mut]
[metabase.dashboards.schema :as dashboards.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util :as u]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn collection-breadcrumb-columns
(mu/defn collection-breadcrumb-columns
"The id, name, location, owner, namespace, and archival of the Collection with `collection-id`, or nil."
[collection-id]
[collection-id :- ::lib.schema.id/collection]
(t2/select-one [:model/Collection :id :name :location :personal_owner_id :namespace :archived_directly]
collection-id))
(defn card
(mu/defn card
"The Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one :model/Card :id card-id))
(defn dashboard
(mu/defn dashboard
"The Dashboard with `dashboard-id`, or nil."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-one :model/Dashboard :id dashboard-id))
(defn dashboard-tab-names
(mu/defn dashboard-tab-names
"The id and name of the DashboardTabs of the Dashboard with `dashboard-id`, in position order."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select [:model/DashboardTab :id :name] :dashboard_id dashboard-id {:order-by [[:position :asc] [:id :asc]]}))
(defn dashboard-tab-ids
(mu/defn dashboard-tab-ids
"The ids of the DashboardTabs of the Dashboard with `dashboard-id`, in position order."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-pks-vec :model/DashboardTab :dashboard_id dashboard-id {:order-by [[:position :asc] [:id :asc]]}))
(defn insert-dashboard!
(mu/defn insert-dashboard!
"Insert the Dashboard `row` and return the inserted instance."
[row]
[row :- ::dashboards.schema/dashboard.update]
(t2/insert-returning-instance! :model/Dashboard row))
(defn insert-dashcard!
(mu/defn insert-dashcard!
"Insert the DashboardCard `row` and return the inserted instance."
[row]
[row :- ::dashboards.schema/dashboard-card.update]
(t2/insert-returning-instance! :model/DashboardCard row))
(defn dashcard-ids-in-layout-order
(mu/defn dashcard-ids-in-layout-order
"The ids of the DashboardCards of the Dashboard with `dashboard-id`, in row then column order."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-fn-vec :id :model/DashboardCard :dashboard_id dashboard-id {:order-by [[:row :asc] [:col :asc]]}))
(defn dashcards
(mu/defn dashcards
"The DashboardCards of the Dashboard with `dashboard-id`."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/select :model/DashboardCard :dashboard_id dashboard-id))
(defn dashcard-in-dashboard
(mu/defn dashcard-in-dashboard
"The DashboardCard with `dashcard-id` on the Dashboard with `dashboard-id`, or nil."
[dashcard-id dashboard-id]
[dashcard-id :- ::lib.schema.id/dashcard
dashboard-id :- ::lib.schema.id/dashboard]
(t2/select-one :model/DashboardCard :id dashcard-id :dashboard_id dashboard-id))
(defn update-dashcard!
"Apply `changes` to the DashboardCard with `dashcard-id`."
[dashcard-id changes]
(mu/defn update-dashcard!
"Apply `changes` to the DashboardCard with `dashcard-id`, returning the number updated."
[dashcard-id :- ::lib.schema.id/dashcard
changes :- (mut/select-keys ::dashboards.schema/dashboard-card.update [:visualization_settings :row :col])]
(t2/update! :model/DashboardCard dashcard-id changes))
(defn update-dashboard!
"Apply `changes` to the Dashboard with `dashboard-id`."
[dashboard-id changes]
(mu/defn update-dashboard!
"Apply `changes` to the Dashboard with `dashboard-id`, returning the number updated."
[dashboard-id :- ::lib.schema.id/dashboard
changes :- (mut/select-keys ::dashboards.schema/dashboard.update [:name :description :collection_id :archived :archived_directly])]
(t2/update! :model/Dashboard dashboard-id changes))
(defn active-user-by-email
(mu/defn active-user-by-email
"The active User with `email`, compared case-insensitively, or nil."
[email]
[email :- :string]
(t2/select-one :model/User :%lower.email (u/lower-case-en email) :is_active true))

View File

@@ -0,0 +1,33 @@
(ns metabase.agent-api.schema
"Malli schemas for the agent-api module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::agent-api-call-log
"A AgentApiCallLog as selected from the app DB: every column of `:agent_api_call_log`."
[:map {:closed true}
[:id ms/PositiveInt]
[:created_at ms/TemporalInstant]
[:user_id [:maybe ::lib.schema.id/user]]
[:tenant_id [:maybe ms/PositiveInt]]
[:client_name [:maybe :string]]
[:operation [:maybe :string]]
[:status [:maybe [:or :keyword :string]]]
[:duration_ms [:maybe :int]]
[:ip_address [:maybe :string]]
[:error_message [:maybe :string]]])
(mr/def ::agent-api-call-log.update
"What an update (or insert) of a AgentApiCallLog accepts: every column of `:agent_api_call_log` except `id`, all optional."
[:map {:closed true}
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:tenant_id {:optional true} [:maybe ms/PositiveInt]]
[:client_name {:optional true} [:maybe :string]]
[:operation {:optional true} [:maybe :string]]
[:status {:optional true} [:maybe [:or :keyword :string]]]
[:duration_ms {:optional true} [:maybe :int]]
[:ip_address {:optional true} [:maybe :string]]
[:error_message {:optional true} [:maybe :string]]])

View File

@@ -4,72 +4,75 @@
(:require
[clojure.string :as str]
[metabase.app-db.core :as app-db]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.models.interface :as mi]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn first-user-date-joined
(mu/defn first-user-date-joined
"The earliest join date among all Users, or nil."
[]
(t2/select-one-fn :min [:model/User [:%min.date_joined :min]]))
(defn sample-database-exists?
(mu/defn sample-database-exists?
"Whether a sample Database exists."
[]
(t2/exists? :model/Database, :is_sample true))
(defn sample-database-id
(mu/defn sample-database-id
"The id of the sample Database, or nil."
[]
(t2/select-one-pk :model/Database :is_sample true))
(defn personal-user-stats-columns
(mu/defn personal-user-stats-columns
"The active, superuser, last login, and SSO source of every personal User."
[]
(t2/select [:model/User :is_active :is_superuser :last_login :sso_source] :type :personal))
(defn document-archived-flags
(mu/defn document-archived-flags
"The archived flag of every Document."
[]
(t2/select [:model/Document :archived]))
(defn collection-by-type
(mu/defn collection-by-type
"The id and location of a Collection of `collection-type`, or nil."
[collection-type]
[collection-type :- :string]
(t2/select-one [:model/Collection :id :location] :type collection-type))
(defn descendant-collection-ids
(mu/defn descendant-collection-ids
"The ids of the Collections whose location starts with `location-prefix`, or nil."
[location-prefix]
[location-prefix :- :string]
(t2/select-pks-set :model/Collection :location [:like (str location-prefix "%")]))
(defn published-table-count-in-collections
(mu/defn published-table-count-in-collections
"The number of published Tables in the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/count :model/Table {:where [:and
[:= :is_published true]
[:in :collection_id collection-ids]]}))
(defn unarchived-metric-count-in-collections
(mu/defn unarchived-metric-count-in-collections
"The number of unarchived metric Cards in the Collections with `collection-ids`."
[collection-ids]
[collection-ids :- [:set ::lib.schema.id/collection]]
(t2/count :model/Card {:where [:and
[:= :type "metric"]
[:= :archived false]
[:in :collection_id collection-ids]]}))
(defn permissions-group-count
(mu/defn permissions-group-count
"The number of PermissionsGroups."
[]
(t2/count :model/PermissionsGroup))
(defn dashboard-stats-columns
(mu/defn dashboard-stats-columns
"The creator, public uuid, parameters, and embedding columns of the non-internal Dashboards."
[]
(t2/select [:model/Dashboard :creator_id :public_uuid :parameters :enable_embedding :embedding_params]
{:where (mi/exclude-internal-content-hsql :model/Dashboard)}))
(defn dashcards-of-dashboards
(mu/defn dashcards-of-dashboards
"The DashboardCards of the non-internal Dashboards."
[]
(t2/query {:select :dc.*
@@ -84,33 +87,36 @@
:where [(if alerts? :not= :=) :pulse.alert_condition nil]}
left-join? (assoc :left-join [:pulse [:= :pulse.id :pulse_id]]))))
(defn pulse-channel-frequencies-by-column
(mu/defn pulse-channel-frequencies-by-column
"The distinct `column` values (as `:k`) and their `:count` among the PulseChannels of alerts when `alerts?`, or of
pulses otherwise."
[column alerts?]
[column :- :keyword
alerts? :- :boolean]
(notification-frequencies-by-column* :model/PulseChannel column alerts? true))
(defn pulse-frequencies-by-column
(mu/defn pulse-frequencies-by-column
"The distinct `column` values (as `:k`) and their `:count` among the Pulses of alerts when `alerts?`, or of pulses
otherwise."
[column alerts?]
[column :- :keyword
alerts? :- :boolean]
(notification-frequencies-by-column* :model/Pulse column alerts? false))
(defn pulse-card-frequencies-by-column
(mu/defn pulse-card-frequencies-by-column
"The distinct `column` values (as `:k`) and their `:count` among the PulseCards of alerts when `alerts?`, or of
pulses otherwise."
[column alerts?]
[column :- :keyword
alerts? :- :boolean]
(notification-frequencies-by-column* :model/PulseCard column alerts? true))
(defn pulse-count
(mu/defn pulse-count
"The number of Pulses that are not alerts."
[]
(t2/count :model/Pulse :alert_condition nil))
(defn notification-xls-or-csv-card-count
(mu/defn notification-xls-or-csv-card-count
"The `:count` of Notifications with at least one PulseCard with `:include_xls` or `:include_csv`, of alerts when
`alerts?`, or of pulses otherwise."
[alerts?]
[alerts? :- :boolean]
(t2/query-one {:select [[[::h2x/distinct-count :pulse.id] :count]]
:from [:pulse]
:left-join [:pulse_card [:= :pulse.id :pulse_card.pulse_id]]
@@ -120,38 +126,38 @@
[:= :pulse_card.include_xls true]]
[(if alerts? :not= :=) :alert_condition nil]]}))
(defn alert-count
(mu/defn alert-count
"The number of Pulses that are alerts."
[]
(t2/count :model/Pulse :alert_condition [:not= nil]))
(defn first-time-only-alert-count
(mu/defn first-time-only-alert-count
"The number of alert Pulses that fire only once."
[]
(t2/count :model/Pulse :alert_condition [:not= nil], :alert_first_only true))
(defn above-goal-alert-count
(mu/defn above-goal-alert-count
"The number of alert Pulses that fire above their goal."
[]
(t2/count :model/Pulse :alert_condition [:not= nil], :alert_above_goal true))
(defn collection-count
(mu/defn collection-count
"The number of non-internal Collections."
[]
(t2/count :model/Collection {:where (mi/exclude-internal-content-hsql :model/Collection)}))
(defn card-collection-ids
(mu/defn card-collection-ids
"The Collection id and schema of the non-internal Cards."
[]
(t2/select [:model/Card :collection_id :card_schema] {:where [:and (mi/exclude-internal-content-hsql :model/Card)]}))
(defn database-stats-columns
(mu/defn database-stats-columns
"The sync, engine, and DBMS version of the non-internal Databases."
[]
(t2/select [:model/Database :is_full_sync :engine :dbms_version]
{:where (mi/exclude-internal-content-hsql :model/Database)}))
(defn table-database-and-schema
(mu/defn table-database-and-schema
"The Database id and schema of the Tables of the non-internal Databases."
[]
(t2/query {:select [:t.db_id :t.schema]
@@ -159,7 +165,7 @@
:join [[(t2/table-name :model/Database) :d] [:= :d.id :t.db_id]]
:where (mi/exclude-internal-content-hsql :model/Database :table-alias :d)}))
(defn field-table-ids
(mu/defn field-table-ids
"The Table id of the Fields of the non-internal Databases."
[]
(t2/query {:select [:f.table_id]
@@ -168,12 +174,12 @@
[(t2/table-name :model/Database) :d] [:= :d.id :t.db_id]]
:where (mi/exclude-internal-content-hsql :model/Database :table-alias :d)}))
(defn segment-count
(mu/defn segment-count
"The number of Segments."
[]
(t2/count :model/Segment))
(defn unarchived-metric-card-count
(mu/defn unarchived-metric-card-count
"The number of unarchived metric Cards."
[]
(t2/count :model/Card :type :metric :archived false))
@@ -230,149 +236,151 @@
")"
"SELECT q1.*, q2.* FROM query_stats_1 q1, query_stats_2 q2;"])))
(defn execution-metrics
(mu/defn execution-metrics
"The execution statistics over the last 30 days of QueryExecutions."
[]
(first (t2/query (execution-metrics-sql))))
(defn query-cache-stats
(mu/defn query-cache-stats
"The average result `:length` and `:count` of the QueryCache entries."
[]
(t2/select-one [:model/QueryCache [[:avg [:length :results]] :length] [:%count.* :count]]))
(defn user-count-joined-before
(mu/defn user-count-joined-before
"The number (up to `limit`) of non-internal Users who joined at or before `joined-before`."
[joined-before limit]
[joined-before :- ms/TemporalInstant
limit :- ms/PositiveInt]
(t2/count :model/User {:where [:and
[:<= :date_joined joined-before]
(mi/exclude-internal-content-hsql :model/User)]
:limit limit}))
(defn query-execution-ids-excluding-database
(mu/defn query-execution-ids-excluding-database
"Up to `limit` ids of QueryExecutions not run against the Database with `database-id`."
[database-id limit]
[database-id :- [:maybe ::lib.schema.id/database]
limit :- ms/PositiveInt]
(t2/select-fn-set :id :model/QueryExecution
{:where [:or
[:not= :database_id database-id]
[:= :database_id nil]]
:limit limit}))
(defn transform-count
(mu/defn transform-count
"The number of Transforms."
[]
(t2/count :model/Transform))
(defn transform-run-count-since
(mu/defn transform-run-count-since
"The number of TransformRuns started at or after `since`."
[since]
[since :- ms/TemporalInstant]
(t2/count :model/TransformRun :start_time [:>= since]))
(defn unarchived-model-count
(mu/defn unarchived-model-count
"The number of unarchived model Cards."
[]
(t2/count :model/Card :type :model :archived false))
(defn new-embedded-dashboard-count-since
(mu/defn new-embedded-dashboard-count-since
"The number of unarchived Dashboards with embedding enabled created at or after `since`."
[since]
[since :- ms/TemporalInstant]
(t2/count :model/Dashboard :enable_embedding true :archived false :created_at [:>= since]))
(defn new-active-user-count-since
(mu/defn new-active-user-count-since
"The number of active Users who joined at or after `since`."
[since]
[since :- ms/TemporalInstant]
(t2/count :model/User :is_active true :date_joined [:>= since]))
(defn unarchived-pivot-table-count
(mu/defn unarchived-pivot-table-count
"The number of unarchived pivot table Cards."
[]
(t2/count :model/Card :display :pivot :archived false))
(defn query-execution-count-since
(mu/defn query-execution-count-since
"The number of QueryExecutions started at or after `since`."
[since]
[since :- ms/TemporalInstant]
(t2/count :model/QueryExecution :started_at [:>= since]))
(defn new-scim-user-count-since
(mu/defn new-scim-user-count-since
"The number of active SCIM-provisioned Users who joined at or after `since`."
[since]
[since :- ms/TemporalInstant]
(t2/count :model/User :sso_source :scim :is_active true :date_joined [:>= since]))
(defn database-engines-among
(mu/defn database-engines-among
"The set of engines of the Databases whose engine is one of `engine-names`."
[engine-names]
[engine-names :- [:sequential :string]]
(t2/select-fn-set :engine :model/Database {:where [:in :engine engine-names]}))
(defn embedded-dashboard-exists?
(mu/defn embedded-dashboard-exists?
"Whether a Dashboard with embedding enabled exists."
[]
(t2/exists? :model/Dashboard :enable_embedding true))
(defn embedded-card-exists?
(mu/defn embedded-card-exists?
"Whether a Card with embedding enabled exists."
[]
(t2/exists? :model/Card :enable_embedding true))
(defn public-dashboard-exists?
(mu/defn public-dashboard-exists?
"Whether a publicly shared Dashboard exists."
[]
(t2/exists? :model/Dashboard :public_uuid [:not= nil]))
(defn public-card-exists?
(mu/defn public-card-exists?
"Whether a publicly shared Card exists."
[]
(t2/exists? :model/Card :public_uuid [:not= nil]))
(defn custom-viz-plugin-exists?
(mu/defn custom-viz-plugin-exists?
"Whether a CustomVizPlugin exists."
[]
(t2/exists? :model/CustomVizPlugin))
(defn uploads-database-exists?
(mu/defn uploads-database-exists?
"Whether a Database with uploads enabled exists."
[]
(t2/exists? :model/Database :uploads_enabled true))
(defn official-collection-exists?
(mu/defn official-collection-exists?
"Whether an official Collection exists."
[]
(t2/exists? :model/Collection :authority_level "official"))
(defn cache-config-exists?
(mu/defn cache-config-exists?
"Whether any CacheConfig exists."
[]
(t2/exists? :model/CacheConfig))
(defn preemptive-cache-config-exists?
(mu/defn preemptive-cache-config-exists?
"Whether a CacheConfig that refreshes automatically exists."
[]
(t2/exists? :model/CacheConfig :refresh_automatically true))
(defn database-router-exists?
(mu/defn database-router-exists?
"Whether a DatabaseRouter exists."
[]
(t2/exists? :model/DatabaseRouter))
(defn moderation-review-exists?
(mu/defn moderation-review-exists?
"Whether a ModerationReview exists."
[]
(t2/exists? :model/ModerationReview))
(defn filtered-pulse-exists?
(mu/defn filtered-pulse-exists?
"Whether a Pulse with parameters exists."
[]
(t2/exists? :model/Pulse {:where [:not= :parameters "[]"]}))
(defn upload-table-exists?
(mu/defn upload-table-exists?
"Whether an uploaded Table exists."
[]
(t2/exists? :model/Table :is_upload true))
(defn snippet-collection-exists?
(mu/defn snippet-collection-exists?
"Whether a snippet Collection exists."
[]
(t2/exists? :model/Collection :namespace "snippets"))
(defn starburst-database-details
(mu/defn starburst-database-details
"The connection details of the Starburst Databases."
[]
(t2/select-fn-set :details :model/Database :engine "starburst"))

View File

@@ -2,16 +2,19 @@
"Application database queries for the API module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn entity-exists?
(mu/defn entity-exists?
"Whether a row of `entity` matching the key-value `conditions` exists."
[entity & conditions]
[entity :- :keyword & conditions :- [:* :some]]
(apply t2/exists? entity conditions))
(defn entity-by-id
(mu/defn entity-by-id
"The `entity` row with `id` also matching the key-value `conditions`, or nil."
[entity id & conditions]
[entity :- :keyword id :- [:or ms/PositiveInt :string] & conditions :- [:* :some]]
(apply t2/select-one entity :id id conditions))
(defn- shift-positions-after!
@@ -32,74 +35,102 @@
:collection_position [:between lower upper]}
{:collection_position [plus-or-minus :collection_position 1]}))
(defn shift-card-positions-after!
(mu/defn shift-card-positions-after!
"Add or subtract (`plus-or-minus`) one from the collection position of the Cards in the Collection with
`collection-id` positioned after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-after! 'Card collection-id position plus-or-minus))
(defn shift-dashboard-positions-after!
(mu/defn shift-dashboard-positions-after!
"Add or subtract (`plus-or-minus`) one from the collection position of the Dashboards in the Collection with
`collection-id` positioned after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-after! 'Dashboard collection-id position plus-or-minus))
(defn shift-pulse-positions-after!
(mu/defn shift-pulse-positions-after!
"Add or subtract (`plus-or-minus`) one from the collection position of the Pulses in the Collection with
`collection-id` positioned after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-after! 'Pulse collection-id position plus-or-minus))
(defn shift-document-positions-after!
(mu/defn shift-document-positions-after!
"Add or subtract (`plus-or-minus`) one from the collection position of the Documents in the Collection with
`collection-id` positioned after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-after! 'Document collection-id position plus-or-minus))
(defn shift-card-positions-from!
(mu/defn shift-card-positions-from!
"Add or subtract (`plus-or-minus`) one from the collection position of the Cards in the Collection with
`collection-id` positioned at or after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-from! 'Card collection-id position plus-or-minus))
(defn shift-dashboard-positions-from!
(mu/defn shift-dashboard-positions-from!
"Add or subtract (`plus-or-minus`) one from the collection position of the Dashboards in the Collection with
`collection-id` positioned at or after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-from! 'Dashboard collection-id position plus-or-minus))
(defn shift-pulse-positions-from!
(mu/defn shift-pulse-positions-from!
"Add or subtract (`plus-or-minus`) one from the collection position of the Pulses in the Collection with
`collection-id` positioned at or after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-from! 'Pulse collection-id position plus-or-minus))
(defn shift-document-positions-from!
(mu/defn shift-document-positions-from!
"Add or subtract (`plus-or-minus`) one from the collection position of the Documents in the Collection with
`collection-id` positioned at or after `position`."
[collection-id position plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
position :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-from! 'Document collection-id position plus-or-minus))
(defn shift-card-positions-between!
(mu/defn shift-card-positions-between!
"Add or subtract (`plus-or-minus`) one from the collection position of the Cards in the Collection with
`collection-id` positioned between `lower` and `upper` inclusive."
[collection-id lower upper plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
lower :- ms/PositiveInt
upper :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-between! 'Card collection-id lower upper plus-or-minus))
(defn shift-dashboard-positions-between!
(mu/defn shift-dashboard-positions-between!
"Add or subtract (`plus-or-minus`) one from the collection position of the Dashboards in the Collection with
`collection-id` positioned between `lower` and `upper` inclusive."
[collection-id lower upper plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
lower :- ms/PositiveInt
upper :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-between! 'Dashboard collection-id lower upper plus-or-minus))
(defn shift-pulse-positions-between!
(mu/defn shift-pulse-positions-between!
"Add or subtract (`plus-or-minus`) one from the collection position of the Pulses in the Collection with
`collection-id` positioned between `lower` and `upper` inclusive."
[collection-id lower upper plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
lower :- ms/PositiveInt
upper :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-between! 'Pulse collection-id lower upper plus-or-minus))
(defn shift-document-positions-between!
(mu/defn shift-document-positions-between!
"Add or subtract (`plus-or-minus`) one from the collection position of the Documents in the Collection with
`collection-id` positioned between `lower` and `upper` inclusive."
[collection-id lower upper plus-or-minus]
[collection-id :- [:maybe ::lib.schema.id/collection]
lower :- ms/PositiveInt
upper :- ms/PositiveInt
plus-or-minus :- [:enum :+ :-]]
(shift-positions-between! 'Document collection-id lower upper plus-or-minus))

View File

@@ -2,41 +2,53 @@
"Application database queries for the API keys module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
(:require
[malli.util :as mut]
[metabase.api-keys.schema :as api-keys.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.users.schema :as users.schema]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn unscoped-api-key-count
(mu/defn unscoped-api-key-count
"The number of ApiKeys without a scope."
[]
(t2/count :model/ApiKey :scope nil))
(defn unscoped-api-keys
(mu/defn unscoped-api-keys
"The ApiKeys without a scope."
[]
(t2/select :model/ApiKey :scope nil))
(defn api-key
(mu/defn api-key
"The ApiKey with `id`, or nil."
[id]
[id :- ms/PositiveInt]
(t2/select-one :model/ApiKey id))
(defn save-api-key!
"Save the changes made to the ApiKey instance `api-key`."
[api-key]
(def ^:private ApiKeyWithGroupId
"An ApiKey instance possibly carrying the `::api-keys/group-id` the before-update hook consumes."
(mut/merge ::api-keys.schema/api-key
[:map
[:metabase.api-keys.core/group-id {:optional true} [:maybe ms/PositiveInt]]]))
(mu/defn save-api-key!
"Save the changes made to the ApiKey instance `api-key` and return it."
[api-key :- ApiKeyWithGroupId]
(t2/save! api-key))
(defn api-key-exists?
(mu/defn api-key-exists?
"Whether an ApiKey with `id` exists."
[id]
[id :- ms/PositiveInt]
(t2/exists? :model/ApiKey id))
(defn delete-api-key!
(mu/defn delete-api-key!
"Delete the ApiKey with `id`."
[id]
[id :- ms/PositiveInt]
(t2/delete! :model/ApiKey id))
(defn api-key-groups
(mu/defn api-key-groups
"The group name, group id, and api key id of the PermissionsGroups of the ApiKeys with `api-key-ids`."
[api-key-ids]
[api-key-ids :- [:sequential ms/PositiveInt]]
(t2/query {:select [[:pg.name :group-name]
[:pg.id :group-id]
[:api_key.id :api-key-id]]
@@ -45,42 +57,44 @@
:api_key [:= :api_key.user_id :pgm.user_id]]
:where [:in :api_key.id api-key-ids]}))
(defn rename-api-key-user!
(mu/defn rename-api-key-user!
"Set the first name (and clear the last name) of the api-key User with `user-id`."
[user-id first-name]
[user-id :- ::lib.schema.id/user
first-name :- :string]
(t2/update! :model/User :id user-id, :type :api-key, {:first_name first-name, :last_name ""}))
(defn user-type
(mu/defn user-type
"The `:type` of the User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-fn :type :model/User :id user-id))
(defn deactivate-api-key-user!
"Deactivate the api-key User with `user-id`."
[user-id]
(mu/defn deactivate-api-key-user!
"Deactivate the api-key User with `user-id` (nil for keys without a user, e.g. SCIM keys, which updates nothing)."
[user-id :- [:maybe ::lib.schema.id/user]]
(t2/update! :model/User user-id, :type :api-key, {:is_active false}))
(defn api-key-prefix-exists?
(mu/defn api-key-prefix-exists?
"Whether an ApiKey with `prefix` exists."
[prefix]
[prefix :- :string]
(t2/exists? :model/ApiKey :key_prefix prefix))
(defn api-key-name-exists?
(mu/defn api-key-name-exists?
"Whether an ApiKey named `key-name` exists."
[key-name]
[key-name :- :string]
(t2/exists? :model/ApiKey :name key-name))
(defn insert-user!
(mu/defn insert-user!
"Insert the User `row` and return its id."
[row]
[row :- ::users.schema/user.update]
(t2/insert-returning-pk! :model/User row))
(defn insert-api-key!
(mu/defn insert-api-key!
"Insert the ApiKey `row` and return the inserted instance."
[row]
[row :- ::api-keys.schema/api-key.create]
(t2/insert-returning-instance! :model/ApiKey row))
(defn update-api-key!
(mu/defn update-api-key!
"Apply `changes` to the ApiKey with `id`."
[id changes]
[id :- ms/PositiveInt
changes :- (mut/select-keys ::api-keys.schema/api-key.update [:key :key_prefix :updated_by_id])]
(t2/update! :model/ApiKey :id id changes))

View File

@@ -3,6 +3,9 @@
[clojure.math :as math]
[clojure.string :as str]
[malli.core :as mc]
[malli.util :as mut]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.users.schema :as users.schema]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]
[metabase.util.secret]))
@@ -102,7 +105,9 @@
[:name [:ref ::name]]
[:updated_by_id pos-int?]
[:scope {:optional true} [:maybe [:ref ::scope]]]
[:single_collection_id {:optional true} [:maybe pos-int?]]])
[:single_collection_id {:optional true} [:maybe pos-int?]]
;; added by the model's after-select hook
[:masked_key {:optional true} :string]])
(defn- insert-schema [map-schema]
(into [:map]
@@ -128,3 +133,24 @@
(mr/def ::api-key.update
(update-schema ::api-key))
(mr/def ::api-key.create
"What an insert of a ApiKey accepts: every column of `:api_key` except `id`, all optional, plus `:metabase.api-keys.core/unhashed-key` consumed by the model's hooks."
[:map {:closed true}
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:key {:optional true} [:maybe :string]]
[:key_prefix {:optional true} [:maybe :string]]
[:creator_id {:optional true} [:maybe ::lib.schema.id/user]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
[:name {:optional true} [:maybe :string]]
[:updated_by_id {:optional true} [:maybe ms/PositiveInt]]
[:scope {:optional true} [:maybe [:or :keyword :string]]]
[:metabase.api-keys.core/unhashed-key {:optional true} ::key.unhashed-or-secret]])
(mr/def ::api-key.inserted
"What inserting an ApiKey returns: the selected row plus the `:group` and `:updated_by` hydrated by the after-insert hook."
(mut/merge ::api-key
[:map
[:group {:optional true} [:map {:closed true} [:id [:maybe pos-int?]] [:name [:maybe :string]]]]
[:updated_by {:optional true} ::users.schema/user]]))

View File

@@ -8,29 +8,34 @@
(:require
[honey.sql :as sql]
[metabase.util.honey-sql-2 :as h2x]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn current-timestamp-string
(mu/defn current-timestamp-string
"The application DB's own current timestamp, as a string, for app DB type `db-type`."
^String [db-type]
^String [db-type :- [:enum :h2 :mysql :postgres]]
;; for MySQL, cast(current_timestamp AS char); for H2 & Postgres, cast(current_timestamp AS text)
(let [cast-form (h2x/cast (if (= db-type :mysql) :char :text) (h2x/current-datetime-honeysql-form db-type))]
(:timestamp (t2/query-one {:select [[cast-form :timestamp]]}))))
;;; ------------------------------------------------ Liquibase ------------------------------------------------
(defn changelog-by-id
(mu/defn changelog-by-id
"The Liquibase changelog row with `changelog-id` in the app DB of type `db-type`, or nil."
[db-type changelog-id]
[db-type :- [:enum :h2 :mysql :postgres]
changelog-id :- :string]
(let [table-name (case db-type
(:postgres :h2) "databasechangelog"
:mysql "DATABASECHANGELOG")]
(t2/query-one [(format "select * from %s where id = ?" table-name) changelog-id])))
(defn changelog-ids
(mu/defn changelog-ids
"The ids among `changelog-ids` still present in the Liquibase changelog table `changelog-table-name`, read on
`conn`."
[conn changelog-table-name changelog-ids]
[conn :- (ms/InstanceOfClass java.sql.Connection)
changelog-table-name :- :string
changelog-ids :- [:set :string]]
(map :id (t2/query conn (sql/format {:select [:id]
:from [(keyword changelog-table-name)]
:where [:in :id changelog-ids]}))))
@@ -40,61 +45,75 @@
(def ^:private unmigrated-settings-where
[:and [:= :value_with_aad nil] [:not= :value nil] [:not= :value ""]])
(defn setting-value
(mu/defn setting-value
"The legacy `value` of the setting row with `setting-key`, or nil."
[setting-key]
[setting-key :- :string]
(t2/select-one-fn :value :setting :key setting-key))
(defn settings
(mu/defn settings
"Every setting row, raw."
[]
(t2/select :setting))
(defn reducible-setting-values-with-aad
(mu/defn reducible-setting-values-with-aad
"A reducible of the key and `value_with_aad` of every setting row that has one."
[]
(t2/reducible-select [:setting :key :value_with_aad] {:where [:!= :value_with_aad nil]}))
(defn unmigrated-settings?
(mu/defn unmigrated-settings?
"Whether any setting row has a non-blank `value` but no `value_with_aad`."
[]
(t2/exists? :setting {:where unmigrated-settings-where}))
(defn unmigrated-settings
(mu/defn unmigrated-settings
"Every setting row with a non-blank `value` but no `value_with_aad`, locked for update."
[]
(t2/select :setting {:where unmigrated-settings-where, :for :update}))
(defn insert-setting!
"Insert a setting row for `setting-key` holding `value` and `value-with-aad`."
[setting-key value value-with-aad]
(mu/defn insert-setting!
"Insert a setting row for `setting-key` holding `value` and `value-with-aad`, returning the number inserted (as a
one-element sequence -- `t2/query` on an INSERT, unlike `t2/insert!`, does not unwrap it)."
[setting-key :- :string
value :- [:maybe :string]
value-with-aad :- [:maybe :string]]
(t2/query {:insert-into :setting
:values [{:key setting-key, :value value, :value_with_aad value-with-aad}]}))
(defn update-setting-values!
(mu/defn update-setting-values!
"Set the columns in `changes` -- `:value` and/or `:value_with_aad`, nil writing NULL -- of the setting row with
`setting-key`."
[setting-key changes]
`setting-key`, returning the number updated (as a one-element sequence -- `t2/query` on an UPDATE, unlike
`t2/update!`, does not unwrap it)."
[setting-key :- :string
changes :- [:map {:closed true}
[:value {:optional true} [:maybe :string]]
[:value_with_aad {:optional true} [:maybe :string]]]]
(t2/query {:update :setting
:set (select-keys changes [:value :value_with_aad])
:where [:= :key setting-key]}))
(defn delete-setting!
"Delete the setting row with `setting-key`."
[setting-key]
(mu/defn delete-setting!
"Delete the setting row with `setting-key`, returning the number deleted (as a one-element sequence -- `t2/query`
on a DELETE, unlike `t2/delete!`, does not unwrap it)."
[setting-key :- :string]
(t2/query {:delete-from :setting, :where [:= :key setting-key]}))
(defn reducible-column-values
(mu/defn reducible-column-values
"A reducible of the id and `column` value of every row of `table`."
[table column]
[table :- :keyword
column :- :keyword]
(t2/reducible-select [table :id [column :value]]))
(defn update-column-value!
"Set `column` of the row of `table` with `id` to `value`."
[table column id value]
(mu/defn update-column-value!
"Set `column` of the row of `table` with `id` to `value`, returning the number updated (as a one-element
sequence -- `t2/query` on an UPDATE, unlike `t2/update!`, does not unwrap it)."
[table :- :keyword
column :- :keyword
id :- ms/PositiveInt
value :- [:maybe [:or :string bytes?]]]
(t2/query {:update table, :set {column value}, :where [:= :id id]}))
(defn delete-query-cache!
"Delete every cached query result."
(mu/defn delete-query-cache!
"Delete every cached query result, returning the number deleted (as a one-element sequence -- `t2/query` on a
DELETE, unlike `t2/delete!`, does not unwrap it)."
[]
(t2/query {:delete-from :query_cache}))

View File

@@ -2,9 +2,11 @@
"Application database queries for the appearance module. Every function here is a direct Toucan 2 call with no
additional logic, so the rest of the module never talks to `toucan2.core` itself."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[toucan2.core :as t2]))
(defn unarchived-dashboard-exists?
(mu/defn unarchived-dashboard-exists?
"Whether an unarchived Dashboard with `dashboard-id` exists."
[dashboard-id]
[dashboard-id :- ::lib.schema.id/dashboard]
(t2/exists? :model/Dashboard :id dashboard-id :archived false))

View File

@@ -2,41 +2,49 @@
"Application database queries for the audit app module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
(:require
[metabase.audit-app.schema :as audit-app.schema]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn cards
"The Cards with `card-ids`."
[card-ids]
(mu/defn cards
"The Cards with `card-ids` (nil entries, e.g. from virtual dashcards, are ignored)."
[card-ids :- [:sequential [:maybe ::lib.schema.id/card]]]
(t2/select :model/Card :id [:in card-ids]))
(defn audit-log-topic-exists?
(mu/defn audit-log-topic-exists?
"Whether an AuditLog entry with `topic` exists."
[topic]
[topic :- [:or :keyword :string]]
(t2/exists? :model/AuditLog :topic topic))
(defn collection-with-entity-id
(mu/defn collection-with-entity-id
"The Collection with `entity-id`, or nil."
[entity-id]
[entity-id :- :string]
(t2/select-one :model/Collection :entity_id entity-id))
(defn dashboard-with-entity-id
(mu/defn dashboard-with-entity-id
"The Dashboard with `entity-id`, or nil."
[entity-id]
[entity-id :- :string]
(t2/select-one :model/Dashboard :entity_id entity-id))
(defn card-name-and-description
(mu/defn card-name-and-description
"The name and description of the Card with `card-id`, or nil."
[card-id]
[card-id :- ::lib.schema.id/card]
(t2/select-one [:model/Card :name :description :card_schema], :id card-id))
(defn table-database-id
(mu/defn table-database-id
"The Database id of the Table with `table-id`, or nil."
[table-id]
[table-id :- ::lib.schema.id/table]
(t2/select-one-fn :db_id :model/Table, :id table-id))
(defn insert-audit-log!
(mu/defn insert-audit-log!
"Insert an AuditLog entry."
[topic details model-name model-id user-id]
[topic :- :keyword
details :- [:maybe ::audit-app.schema/audit-log.details]
model-name :- [:maybe :string]
model-id :- [:maybe ms/PositiveInt]
user-id :- [:maybe ::lib.schema.id/user]]
(t2/insert! :model/AuditLog
:topic topic
:details details
@@ -44,9 +52,12 @@
:model_id model-id
:user_id user-id))
(defn delete-oldest-by-id-subquery!
(mu/defn delete-oldest-by-id-subquery!
"Delete up to `batch-size` of the `table` rows whose `time-column` is at or before `cutoff`, lowest ids first."
[table time-column cutoff batch-size]
[table :- :keyword
time-column :- :keyword
cutoff :- ms/TemporalInstant
batch-size :- ms/PositiveInt]
(t2/query-one {:delete-from table
:where [:in
:id
@@ -56,9 +67,12 @@
:order-by [[:id :asc]]
:limit batch-size}]}))
(defn delete-oldest-with-limit!
(mu/defn delete-oldest-with-limit!
"Delete up to `batch-size` of the `table` rows whose `time-column` is at or before `cutoff`."
[table time-column cutoff batch-size]
[table :- :keyword
time-column :- :keyword
cutoff :- ms/TemporalInstant
batch-size :- ms/PositiveInt]
(t2/query-one {:delete-from table
:where [:<= time-column cutoff]
:limit batch-size}))

View File

@@ -0,0 +1,33 @@
(ns metabase.audit-app.schema
"Malli schemas for the audit-app module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::audit-log.details
"The `:details` column of a AuditLog, decoded."
:map)
(mr/def ::audit-log
"A AuditLog as selected from the app DB: every column of `:audit_log`."
[:map {:closed true}
[:id ms/PositiveInt]
[:topic [:or :keyword :string]]
[:timestamp ms/TemporalInstant]
[:end_timestamp [:maybe ms/TemporalInstant]]
[:user_id [:maybe ::lib.schema.id/user]]
[:model [:maybe [:or :keyword :string]]]
[:model_id [:maybe :int]]
[:details ::audit-log.details]])
(mr/def ::audit-log.update
"What an update (or insert) of a AuditLog accepts: every column of `:audit_log` except `id`, all optional."
[:map {:closed true}
[:topic {:optional true} [:maybe [:or :keyword :string]]]
[:timestamp {:optional true} [:maybe ms/TemporalInstant]]
[:end_timestamp {:optional true} [:maybe ms/TemporalInstant]]
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:model {:optional true} [:maybe [:or :keyword :string]]]
[:model_id {:optional true} [:maybe :int]]
[:details {:optional true} [:maybe ::audit-log.details]]])

View File

@@ -2,100 +2,128 @@
"Application database queries for the auth identity module. Every function here is a direct Toucan 2 call with no
additional logic, so no other namespace in the module runs a query itself (model definitions still use `toucan2.core`)."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.users.schema :as users.schema]
[metabase.util :as u]
[metabase.util.malli :as mu]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
(defn auth-identity
(def ^:private AuthIdentityRow
"The writable columns of an AuthIdentity row (excluding `:id`, `:created_at`, `:updated_at`)."
[:map {:closed true}
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:provider {:optional true} [:maybe [:or :keyword :string]]]
[:credentials {:optional true} [:maybe :map]]
[:metadata {:optional true} [:maybe :map]]
[:provider_id {:optional true} [:maybe :string]]
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]])
(mu/defn auth-identity
"The AuthIdentity of the User with `user-id` at `provider`, or nil."
[user-id provider]
[user-id :- ::lib.schema.id/user
provider :- :string]
(t2/select-one :model/AuthIdentity :user_id user-id :provider provider))
(defn auth-identity-id
(mu/defn auth-identity-id
"The id of the AuthIdentity of the User with `user-id` at `provider`, or nil."
[user-id provider]
[user-id :- ::lib.schema.id/user
provider :- :string]
(t2/select-one-pk :model/AuthIdentity :user_id user-id :provider provider))
(defn auth-identity-expiry
(mu/defn auth-identity-expiry
"The id and expiry of the AuthIdentity of the User with `user-id` at `provider`, or nil."
[user-id provider]
[user-id :- ::lib.schema.id/user
provider :- :string]
(t2/select-one [:model/AuthIdentity :id :expires_at] :user_id user-id :provider provider))
(defn auth-identity-exists?
(mu/defn auth-identity-exists?
"Whether the User with `user-id` has an AuthIdentity at `provider`."
[user-id provider]
[user-id :- ::lib.schema.id/user
provider :- :string]
(t2/exists? :model/AuthIdentity :user_id user-id :provider provider))
(defn insert-auth-identity!
"Insert the AuthIdentity `row`."
[row]
(mu/defn insert-auth-identity!
"Insert the AuthIdentity `row`, returning the number inserted."
[row :- AuthIdentityRow]
(t2/insert! :model/AuthIdentity row))
(defn update-auth-identity!
"Apply `changes` to the AuthIdentity with `id`."
[id changes]
(mu/defn update-auth-identity!
"Apply `changes` to the AuthIdentity with `id`, returning the number updated."
[id :- ms/PositiveInt
changes :- AuthIdentityRow]
(t2/update! :model/AuthIdentity id changes))
(defn touch-auth-identity!
"Set `last_used_at` of the AuthIdentity with `id` to now."
[id]
(mu/defn touch-auth-identity!
"Set `last_used_at` of the AuthIdentity with `id` to now, returning the number updated. `id` may be nil (no
matching AuthIdentity), which updates nothing."
[id :- [:maybe ms/PositiveInt]]
(t2/update! :model/AuthIdentity id {:last_used_at :%now}))
(defn delete-auth-identities!
"Delete the AuthIdentities of the User with `user-id` at `provider`."
[user-id provider]
(mu/defn delete-auth-identities!
"Delete the AuthIdentities of the User with `user-id` at `provider`, returning the number deleted."
[user-id :- ::lib.schema.id/user
provider :- :string]
(t2/delete! :model/AuthIdentity :user_id user-id :provider provider))
(defn delete-sessions-for-user!
"Delete every Session of the User with `user-id`. Duplicates `metabase.session.db/delete-sessions-for-user!`;
can't delegate to it because the `session` module already depends on `auth-identity`, so the reverse dependency
would be a module cycle."
[user-id]
(mu/defn delete-sessions-for-user!
"Delete every Session of the User with `user-id`, returning the number deleted. Duplicates
`metabase.session.db/delete-sessions-for-user!`; can't delegate to it because the `session` module already depends
on `auth-identity`, so the reverse dependency would be a module cycle."
[user-id :- ::lib.schema.id/user]
(t2/delete! :model/Session :user_id user-id))
(defn user
(mu/defn user
"The User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one :model/User user-id))
(defn user-by-email
(mu/defn user-by-email
"The User whose email matches `email` case-insensitively, or nil."
[email]
[email :- :string]
(t2/select-one :model/User :%lower.email (u/lower-case-en email)))
(defn user-login-columns
(mu/defn user-login-columns
"The id, active flag, last login, and tenant id of the User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one [:model/User :id :is_active :last_login :tenant_id] :id user-id))
(defn user-login-columns-by-email
(mu/defn user-login-columns-by-email
"The id, active flag, last login, and tenant id of the User whose email matches `email` case-insensitively, or
nil."
[email]
[email :- :string]
(t2/select-one [:model/User :id :is_active :last_login :tenant_id] :%lower.email (u/lower-case-en email)))
(defn user-login-status
(mu/defn user-login-status
"The id, active flag, and last login of the User with `user-id`, or nil."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one [:model/User :id :is_active :last_login] user-id))
(defn user-active?
(mu/defn user-active?
"Whether the User with `user-id` is active."
[user-id]
[user-id :- ::lib.schema.id/user]
(t2/select-one-fn :is_active :model/User :id user-id))
(defn update-user!
"Apply `changes` to the User with `user-id`."
[user-id changes]
(mu/defn update-user!
"Apply `changes` to the User with `user-id`, returning the number updated."
[user-id :- ::lib.schema.id/user
changes :- ::users.schema/user.update]
(t2/update! :model/User user-id changes))
(defn insert-user-returning-login-columns!
(mu/defn insert-user-returning-login-columns!
"Insert the User `row` and return its id, last login, active flag, and tenant id."
[row]
[row :- ::users.schema/user.update]
(t2/insert-returning-instance! [:model/User :id :last_login :is_active :tenant_id] row))
(defn insert-session!
(mu/defn insert-session!
"Insert a Session and return the inserted instance."
[session-id user-id auth-identity-id session-key expires-at]
[session-id :- :string
user-id :- ::lib.schema.id/user
auth-identity-id :- [:maybe ms/PositiveInt]
session-key :- :string
expires-at :- [:maybe ms/TemporalInstant]]
(t2/insert-returning-instance! :model/Session
;; Without setting the ID here we can't return an instance on MySQL
:id session-id

View File

@@ -0,0 +1,43 @@
(ns metabase.auth-identity.schema
"Malli schemas for the auth-identity module."
(:require
[metabase.lib.schema.id :as lib.schema.id]
[metabase.util.malli.registry :as mr]
[metabase.util.malli.schema :as ms]))
(mr/def ::auth-identity.credentials
"The `:credentials` column of a AuthIdentity, decoded."
:map)
(mr/def ::auth-identity.metadata
"The `:metadata` column of a AuthIdentity, decoded."
:map)
(mr/def ::auth-identity
"A AuthIdentity as selected from the app DB: every column of `:auth_identity`."
[:map {:closed true}
[:id ms/PositiveInt]
[:user_id ::lib.schema.id/user]
[:provider [:or :keyword :string]]
[:credentials [:maybe ::auth-identity.credentials]]
[:metadata [:maybe ::auth-identity.metadata]]
[:provider_id [:maybe :string]]
[:last_used_at [:maybe ms/TemporalInstant]]
[:expires_at [:maybe ms/TemporalInstant]]
[:created_at ms/TemporalInstant]
[:updated_at ms/TemporalInstant]
[:confirmed_at [:maybe ms/TemporalInstant]]])
(mr/def ::auth-identity.update
"What an update (or insert) of a AuthIdentity accepts: every column of `:auth_identity` except `id`, all optional."
[:map {:closed true}
[:user_id {:optional true} [:maybe ::lib.schema.id/user]]
[:provider {:optional true} [:maybe [:or :keyword :string]]]
[:credentials {:optional true} [:maybe ::auth-identity.credentials]]
[:metadata {:optional true} [:maybe ::auth-identity.metadata]]
[:provider_id {:optional true} [:maybe :string]]
[:last_used_at {:optional true} [:maybe ms/TemporalInstant]]
[:expires_at {:optional true} [:maybe ms/TemporalInstant]]
[:created_at {:optional true} [:maybe ms/TemporalInstant]]
[:updated_at {:optional true} [:maybe ms/TemporalInstant]]
[:confirmed_at {:optional true} [:maybe ms/TemporalInstant]]])

Some files were not shown because too many files have changed in this diff Show More