Skip to content
Snippets Groups Projects
Commit c52ec7ca authored by Cam Saül's avatar Cam Saül
Browse files

Basic HoneySQL DB primitives :honey_pot:

parent 0887a747
No related branches found
No related tags found
No related merge requests found
Showing
with 508 additions and 122 deletions
......@@ -47,6 +47,7 @@
[compojure "1.5.0"] ; HTTP Routing library built on Ring
[environ "1.0.2"] ; easy environment management
[hiccup "1.0.5"] ; HTML templating
[honeysql "0.6.3"] ; Transform Clojure data structures to SQL
[korma "0.4.2"] ; SQL generation
[log4j/log4j "1.2.17" ; logging framework
:exclusions [javax.mail/mail
......
......@@ -18,6 +18,7 @@ log4j.appender.metabase=metabase.logger.Appender
# customizations to logging by package
log4j.logger.metabase=INFO
log4j.logger.metabase.db=DEBUG
log4j.logger.metabase.driver=DEBUG
log4j.logger.metabase.query-processor=DEBUG
log4j.logger.metabase.sync-database=DEBUG
......@@ -5,6 +5,8 @@
(clojure [set :as set]
[string :as s]
[walk :as walk])
(honeysql [core :as hsql]
[helpers :as h])
(korma [core :as k]
[db :as kdb])
[medley.core :as m]
......@@ -12,7 +14,8 @@
[metabase.config :as config]
[metabase.db.internal :as i]
[metabase.models.interface :as models]
[metabase.util :as u])
[metabase.util :as u]
metabase.util.honeysql-extensions) ; this needs to be loaded so the `:h2` quoting style gets added
(:import java.io.StringWriter
java.sql.Connection
liquibase.Liquibase
......@@ -39,6 +42,10 @@
;; if we don't have an absolute path then make sure we start from "user.dir"
[(System/getProperty "user.dir") "/" db-file-name options]))))))
(def ^:const ^clojure.lang.Keyword db-type
"The type of backing DB used to run Metabase. `:h2`, `:mysql`, or `:postgres`."
(config/config-kw :mb-db-type))
(defn parse-connection-string
"Parse a DB connection URI like `postgres://cam@localhost.com:5432/cams_cool_db?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory` and return a broken-out map."
[uri]
......@@ -58,7 +65,7 @@
(e.g., to use the Generic SQL driver functions on the Metabase DB itself)."
(delay (or (when-let [uri (config/config-str :mb-db-connection-uri)]
(parse-connection-string uri))
(case (config/config-kw :mb-db-type)
(case db-type
:h2 {:type :h2
:db @db-file}
:mysql {:type :mysql
......@@ -191,11 +198,11 @@
(apply setup-db args)))
;; # ---------------------------------------- UTILITY FUNCTIONS ----------------------------------------
;; # ---------------------------------------- OLD UTILITY FUNCTIONS ----------------------------------------
;; ## UPD
(defn upd
(defn ^:deprecated upd
"Wrapper around `korma.core/update` that updates a single row by its id value and
automatically passes &rest KWARGS to `korma.core/set-fields`.
......@@ -212,7 +219,7 @@
(models/post-update obj))
(> rows-affected 0)))
(defn upd-non-nil-keys
(defn ^:deprecated upd-non-nil-keys
"Calls `upd`, but filters out KWARGS with `nil` values."
{:style/indent 2}
[entity entity-id & {:as kwargs}]
......@@ -222,7 +229,7 @@
;; ## DEL
(defn del
(defn ^:deprecated del
"Wrapper around `korma.core/delete` that makes it easier to delete a row given a single PK value.
Returns a `204 (No Content)` response dictionary."
[entity & {:as kwargs}]
......@@ -233,12 +240,12 @@
;; ## SEL
(def ^:dynamic *sel-disable-logging*
(def ^:dynamic *disable-db-logging*
"Should we disable logging for the `sel` macro? Normally `false`, but bind this to `true` to keep logging from getting too noisy during
operations that require a lot of DB access, like the sync process."
false)
(defmacro sel
(defmacro ^:deprecated sel
"Wrapper for korma `select` that calls `post-select` on results and provides a few other conveniences.
ONE-OR-MANY tells `sel` how many objects to fetch and is either `:one` or `:many`.
......@@ -317,7 +324,7 @@
;; ## INS
(defn ins
(defn ^:deprecated ins
"Wrapper around `korma.core/insert` that renames the `:scope_identity()` keyword in output to `:id`
and automatically passes &rest KWARGS to `korma.core/values`.
......@@ -345,7 +352,7 @@
;; ## CASADE-DELETE
(defn -cascade-delete
(defn ^:deprecated -cascade-delete
"Internal implementation of `cascade-delete`. Don't use this directly!"
[entity f]
(let [entity (i/entity->korma entity)
......@@ -355,10 +362,380 @@
(del entity :id (:id obj))))))
{:status 204, :body nil})
(defmacro cascade-delete
(defmacro ^:deprecated cascade-delete
"Do a cascading delete of object(s). For each matching object, the `pre-cascade-delete` multimethod is called,
which should delete any objects related the object about to be deleted.
Like `del`, this returns a 204/nil reponse so it can be used directly in an API endpoint."
[entity & kwargs]
`(-cascade-delete ~entity (i/sel-fn ~@kwargs)))
;;; +------------------------------------------------------------------------------------------------------------------------+
;;; | NEW HONEY-SQL BASED DB UTIL FUNCTIONS |
;;; +------------------------------------------------------------------------------------------------------------------------+
;; THIS DEPRECATES THE *ENTIRE* `metabase.db.internal` namespace. Yay!
(defn- entity-symb->ns
"Return the namespace symbol where we'd expect to find an entity symbol.
(entity-symb->ns 'CardFavorite) -> 'metabase.models.card-favorite"
[symb]
{:pre [(symbol? symb)]}
(symbol (str "metabase.models." (s/lower-case (s/replace (name symb) #"([a-z])([A-Z])" "$1-$2")))))
(defn- resolve-entity-from-symbol
"Resolve the entity associated with SYMB, calling `require` on its namespace if needed.
(resolve-entity-from-symbol 'CardFavorite) -> metabase.models.card-favorite/CardFavorite"
[symb]
(let [entity-ns (entity-symb->ns symb)]
@(try (ns-resolve entity-ns symb)
(catch Throwable _
(require entity-ns)
(ns-resolve entity-ns symb)))))
(defn resolve-entity
"Resolve a model entity *if* it's quoted. This also unwraps entities when they're inside vectores.
(resolve-entity Database) -> #'metabase.models.database/Database
(resolve-entity [Database :name]) -> #'metabase.models.database/Database
(resolve-entity 'Database) -> #'metabase.models.database/Database"
[entity]
{:post [(:metabase.models.interface/entity %)]}
(cond
(:metabase.models.interface/entity entity) entity
(vector? entity) (resolve-entity (first entity))
(symbol? entity) (resolve-entity-from-symbol entity)
:else (throw (Exception. (str "Invalid entity:" entity)))))
(defn- db-connection
"Get a JDBC connection spec for the Metabase DB."
[]
(setup-db-if-needed)
(or korma.db/*current-conn*
(korma.db/get-connection (or korma.db/*current-db* @korma.db/_default))))
(def ^:private ^:const quoting-style
"Style of `:quoting` that should be passed to HoneySQL `format`."
(case db-type
:h2 :h2
:mysql :mysql
:postgres :ansi))
(defn- honeysql->sql
"Compile HONEYSQL-FORM to SQL.
This returns a vector with the SQL string as its first item and prepared statement params as the remaining items."
[honeysql-form]
(u/prog1 (hsql/format honeysql-form :quoting quoting-style)
(when-not *disable-db-logging*
(log/debug (str "DB Call: " (first <>))))))
(defn query
"Compile HONEYSQL-FROM and call `jdbc/query` against the Metabase database.
Options are passed along to `jdbc/query`."
[honeysql-form & options]
(apply jdbc/query (db-connection) (honeysql->sql honeysql-form) options))
(defn entity->table-name
"Get the keyword table name associated with an ENTITY, which can be anything that can be passed to `resolve-entity`.
(entity->table-name 'CardFavorite) -> :report_cardfavorite"
^clojure.lang.Keyword [entity]
(keyword (:table (resolve-entity entity))))
(defn- entity->fields
"Get the fields that should be used in a query, destructuring ENTITY if it's wrapped in a vector, otherwise calling `default-fields`.
This will return `nil` if the entity isn't wrapped in a vector and uses the default implementation of `default-fields`.
(entity->fields 'User) -> [:id :email :date_joined :first_name :last_name :last_login :is_superuser :is_qbnewb]
(entity->fields ['User :first_name :last_name]) -> [:first_name :last_name]
(entity->fields 'Database) -> nil"
[entity]
(if (vector? entity)
(rest entity)
(models/default-fields (resolve-entity entity))))
(defn qualify
"Qualify a FIELD-NAME name with the name its ENTITY. This is necessary for disambiguating fields for HoneySQL queries that contain joins.
(qualify 'CardFavorite :id) -> :report_cardfavorite.id"
^clojure.lang.Keyword [entity field-name]
(hsql/qualify (entity->table-name entity) field-name))
(defn simple-select
"Select objects from the database. Like `select`, but doesn't offer as many conveniences, so you should use that instead.
This calls `post-select` on the results.
(simple-select 'User {:where [:= :id 1]})"
{:style/indent 1}
[entity honeysql-form]
(let [entity (resolve-entity entity)]
(vec (for [object (query (merge {:select (or (models/default-fields entity)
[:*])
:from [(entity->table-name entity)]}
honeysql-form))]
(models/do-post-select entity object)))))
(defn simple-select-one
"Select a single object from the database. Like `select-one`, but doesn't offer as many conveniences, so prefer that instead.
(simple-select-one 'User (h/where [:= :first-name \"Cam\"]))"
([entity]
(simple-select-one entity {}))
([entity honeysql-form]
(first (simple-select entity (h/limit honeysql-form 1)))))
(defn execute!
"Compile HONEYSQL-FORM and call `jdbc/execute!` against the Metabase DB.
OPTIONS are passed directly to `jdbc/execute!` and can be things like `:multi?` (default `false`) or `:transaction?` (default `true`)."
[honeysql-form & options]
(apply jdbc/execute! (db-connection) (honeysql->sql honeysql-form) options))
(defn- where
"Generate a HoneySQL `where` form using key-value args.
(where {} :a :b) -> (h/where {} [:= :a :b])
(where {} :a [:!= b]) -> (h/where {} [:!= :a :b])"
{:style/indent 1}
([honeysql-form]
honeysql-form) ; no-op
([honeysql-form m]
(m/mapply where honeysql-form m))
([honeysql-form k v]
(h/merge-where honeysql-form (if (vector? v)
(let [[f v] v] ; e.g. :id [:!= 1] -> [:!= :id 1]
(assert (keyword? f))
[f k v])
[:= k v])))
([honeysql-form k v & more]
(apply where (where honeysql-form k v) more)))
(defn- where+
"Generate a HoneySQL form, converting pairs of arguments with keywords into a `where` clause, and merging other HoneySQL clauses in as-is.
Meant for internal use by functions like `select`. (So called because it handles `where` *plus* other clauses).
(where+ {} [:id 1 {:limit 10}]) -> {:where [:= :id 1], :limit 10}"
[honeysql-form options]
(loop [honeysql-form honeysql-form, [first-option & [second-option & more, :as butfirst]] options]
(cond
(keyword? first-option) (recur (where honeysql-form first-option second-option) more)
first-option (recur (merge honeysql-form first-option) butfirst)
:else honeysql-form)))
;;; ## UPDATE!
(defn update!
"Update a single row in the database. Returns `true` if a row was affected, `false` otherwise.
Accepts either a single map of updates to make or kwargs. ENTITY is automatically resolved,
and `pre-update` is called on KVS before the object is inserted into the database.
(db/update! 'Label 11 :name \"ToucanFriendly\")
(db/update! 'Label 11 {:name \"ToucanFriendly\"})"
{:style/indent 2}
([entity honeysql-form]
(let [entity (resolve-entity entity)]
(not= [0] (execute! (merge (h/update (entity->table-name entity))
honeysql-form)))))
([entity id kvs]
{:pre [(integer? id) (map? kvs)]}
(let [entity (resolve-entity entity)
kvs (-> (models/do-pre-update entity (assoc kvs :id id))
(dissoc :id))]
(update! entity (-> (h/sset {} kvs)
(where :id id)))))
([entity id k v & more]
(update! entity id (apply array-map k v more))))
(defn update-non-nil-keys!
"Like `update!`, but filters out KVS with `nil` values."
{:style/indent 2}
([entity id kvs]
(update! entity id (m/filter-vals (complement nil?) kvs)))
([entity id k v & more]
(update-non-nil-keys! entity id (apply array-map k v more))))
;;; ## DELETE!
(defn delete!
"Delete an object or objects from the Metabase DB matching certain constraints. Returns `true` if something was deleted, `false` otherwise.
(delete! 'Label) ; delete all Labels
(delete! Label :name \"Cam\") ; delete labels where :name == \"Cam\"
(delete! Label {:name \"Cam\"}) ; for flexibility either a single map or kwargs are accepted"
{:style/indent 1}
([entity]
(delete! entity {}))
([entity kvs]
(let [entity (resolve-entity entity)]
(not= [0] (execute! (-> (h/delete-from (entity->table-name entity))
(where kvs))))))
([entity k v & more]
(delete! entity (apply array-map k v more))))
;;; ## INSERT!
(defn insert!
"Insert a new object into the Database. Resolves ENTITY, and calls its `pre-insert` method on OBJECT to prepare it before insertion;
after insertion, it calls `post-insert` on the newly created object and returns it.
For flexibility, `insert!` OBJECT can be either a single map or individual kwargs:
(insert! Label {:name \"Toucan Unfriendly\"})
(insert! 'Label :name \"Toucan Friendly\")"
{:style/indent 1}
([entity object]
(let [entity (resolve-entity entity)
object (models/do-pre-insert entity object)
id (first (vals (first (jdbc/insert! (db-connection) (entity->table-name entity) object))))]
(some-> id entity models/post-insert)))
([entity k v & more]
(insert! entity (apply array-map k v more))))
;;; ## EXISTS?
;; The new implementation of exists? is commented out for the time being until we re-work existing uses
#_(defn exists?
"Easy way to see if something exists in the DB.
(db/exists? User :id 100)
NOTE: This only works for objects that have an `:id` field."
[entity & kvs]
(boolean (select-one entity (apply where (h/select :id) kvs))))
;;; ## SELECT
;; All of the following functions are based off of the old `sel` macro and can do things like select certain fields by wrapping ENTITY in a vector
;; and automatically convert kv-args to a `where` clause
(defn select-one
"Select a single object from the database.
(select-one ['Database :name] :id 1) -> {:name \"Sample Dataset\"}"
{:style/indent 1}
[entity & options]
(let [fields (entity->fields entity)]
(simple-select-one entity (where+ {:select (or fields [:*])} options))))
(defn select-one-field
"Select a single FIELD of a single object from the database.
(select-one-field :name 'Database :id 1) -> \"Sample Dataset\""
{:style/indent 2}
[field entity & options]
{:pre [(keyword? field)]}
(field (apply select-one [entity field] options)))
(defn select-one-id
"Select the `:id` of a single object from the database.
(select-one-id 'Database :name \"Sample Dataset\") -> 1"
[entity & options]
(apply select-one-field :id entity options))
(defn select-one-count
"Select the count of objects matching some condition.
;; Get all Databases whose name is non-nil
(select-one-count 'Database :name [:not= nil]) -> 12"
[entity & options]
(:count (apply select-one [entity [:%count.* :count]] options)))
(defn select
"Select objects from the database.
(select 'Database :name [:not= nil] {:limit 2}) -> [...]"
{:style/indent 1}
[entity & options]
(let [fields (entity->fields entity)]
(simple-select entity (where+ {:select (or fields [:*])} options))))
(defn select-field
"Select values of a single field for multiple objects. These are returned as a set.
(select-field :name 'Database) -> #{\"Sample Dataset\", \"test-data\"}"
{:style/indent 2}
[field entity & options]
{:pre [(keyword? field)]}
(set (map field (apply select [entity field] options))))
(defn select-ids
"Select IDs for multiple objects. These are returned as a set.
(select-ids 'Table :db_id 1) -> #{1 2 3 4}"
{:style/indent 1}
[entity & options]
(apply select-field :id entity options))
(defn select-field->obj
"Select objects from the database, and return them as a map of FIELD to the objects themselves.
(select-field->obj :name 'Database) -> {\"Sample Dataset\" {...}, \"test-data\" {...}}"
{:style/indent 2}
[field entity & options]
{:pre [(keyword? field)]}
(into {} (for [result (apply select entity options)]
{(field result) result})))
(defn select-id->obj
"Select objects from the database, and return them as a map of their `:id` to the objects themselves.
(select-id->obj 'Database) -> {1 {...}, 2 {...}}"
[entity & options]
(apply select-field->obj :id entity options))
(defn select-field->field
"Select fields K and V from objects in the database, and return them as a map from K to V.
(select-field->field :id :name 'Database) -> {1 \"Sample Dataset\", 2 \"test-data\"}"
[k v entity & options]
{:style/indent 3}
{:pre [(keyword? k) (keyword? v)]}
(into {} (for [result (apply select [entity k v] options)]
{(k result) (v result)})))
(defn select-field->id
"Select FIELD and `:id` from objects in the database, and return them as a map from FIELD to `:id`.
(select-field->id :name 'Database) -> {\"Sample Dataset\" 1, \"test-data\" 2}"
{:style/indent 2}
[field entity & options]
(apply select-field->field field :id entity options))
(defn select-id->field
"Select FIELD and `:id` from objects in the database, and return them as a map from `:id` to FIELD.
(select-id->field :name 'Database) -> {1 \"Sample Dataset\", 2 \"test-data\"}"
{:style/indent 2}
[field entity & options]
(apply select-field->field :id field entity options))
;;; ## CASADE-DELETE
(defn cascade-delete!
"Do a cascading delete of object(s). For each matching object, the `pre-cascade-delete` multimethod is called,
which should delete any objects related the object about to be deleted.
Returns a 204/nil reponse so it can be used directly in an API endpoint."
{:style/indent 1}
([entity id]
(cascade-delete! entity :id id))
([entity k v & more]
(let [entity (resolve-entity entity)]
(doseq [object (apply select entity k v more)]
(models/pre-cascade-delete object)
(delete! entity :id (:id object))))
{:status 204, :body nil}))
(ns metabase.db.internal
(ns ^:deprecated metabase.db.internal
"Internal functions and macros used by the public-facing functions in `metabase.db`."
(:require [clojure.string :as s]
[clojure.tools.logging :as log]
......@@ -9,7 +9,7 @@
(declare entity->korma)
(defn- pull-kwargs
(defn- ^:deprecated pull-kwargs
"Where FORMS is a sequence like `[:id 1 (limit 3)]`, return a map of kwarg pairs and sequence of remaining forms.
(pull-kwargs [:id 1 (limit 3) (order :id :ASC]) -> [{:id 1} [(limit 3) (order :id ASC)]"
......@@ -21,20 +21,20 @@
(recur (assoc kwargs-acc arg1 (first rest-args)) forms-acc (rest rest-args))
(recur kwargs-acc (conj forms-acc arg1) rest-args)))))
(defn sel-apply-kwargs
(defn ^:deprecated sel-apply-kwargs
"Pull kwargs from forms and add korma `where` form if applicable."
[forms]
(let [[kwargs-map forms] (pull-kwargs forms)]
(if-not (empty? kwargs-map) (conj forms `(k/where ~kwargs-map))
forms)))
(defn destructure-entity
(defn ^:deprecated destructure-entity
"Take an ENTITY of the form `entity` or `[entity & field-keys]` and return a pair like `[entity field-keys]`."
[entity]
(if-not (vector? entity) [entity nil]
[(first entity) (vec (rest entity))]))
(def ^{:arglists '([entity])} entity->korma
(def ^{:arglists '([entity])} ^:deprecated entity->korma
"Convert an ENTITY argument to `sel` into the form we should pass to korma `select` and to various multi-methods such as
`post-select`.
......@@ -64,7 +64,7 @@
;;; Low-level sel implementation
(defmacro sel-fn
(defmacro ^:deprecated sel-fn
"Part of the internal implementation for `sel`, don't call this directly!"
[& forms]
(let [forms (sel-apply-kwargs forms)
......@@ -76,7 +76,7 @@
:else `[(fn [~entity]
~query) ~(str query)]))))
(defn sel-exec
(defn ^:deprecated sel-exec
"Part of the internal implementation for `sel`, don't call this directly!
Execute the korma form generated by the `sel` macro and process the results."
[entity [select-fn log-str]]
......@@ -87,7 +87,7 @@
;; Log if applicable
(future
(when (config/config-bool :mb-db-logging)
(when-not @(resolve 'metabase.db/*sel-disable-logging*)
(when-not @(resolve 'metabase.db/*disable-db-logging*)
(log/debug "DB CALL:" (:name entity)
(or (:fields entity+fields) "*")
(s/replace log-str #"korma.core/" "")))))
......@@ -95,14 +95,14 @@
(for [obj (k/exec (select-fn entity+fields))]
(models/do-post-select entity obj))))
(defmacro sel*
(defmacro ^:deprecated sel*
"Part of the internal implementation for `sel`, don't call this directly!"
[entity & forms]
`(sel-exec ~entity (sel-fn ~@forms)))
;;; :field
(defmacro sel:field
(defmacro ^:deprecated sel:field
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field ...)` forms."
[[entity field] & forms]
......@@ -111,7 +111,7 @@
;;; :id
(defmacro sel:id
(defmacro ^:deprecated sel:id
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :id ...)` forms."
[entity & forms]
......@@ -119,14 +119,14 @@
;;; :fields
(defn sel:fields*
(defn ^:deprecated sel:fields*
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :fields ...)` forms."
[fields results]
(for [result results]
(select-keys result fields)))
(defmacro sel:fields
(defmacro ^:deprecated sel:fields
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :fields ...)` forms."
[[entity & fields] & forms]
......@@ -135,7 +135,7 @@
;;; :id->fields
(defn sel:id->fields*
(defn ^:deprecated sel:id->fields*
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :id->fields ...)` forms."
[fields results]
......@@ -143,7 +143,7 @@
(map (u/rpartial select-keys fields))
(zipmap (map :id results))))
(defmacro sel:id->fields
(defmacro ^:deprecated sel:id->fields
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :id->fields ...)` forms."
[[entity & fields] & forms]
......@@ -152,14 +152,14 @@
;;; :field->field
(defn sel:field->field*
(defn ^:deprecated sel:field->field*
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field->field ...)` forms."
[f1 f2 results]
(into {} (for [result results]
{(f1 result) (f2 result)})))
(defmacro sel:field->field
(defmacro ^:deprecated sel:field->field
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field->field ...)` forms."
[[entity f1 f2] & forms]
......@@ -169,14 +169,14 @@
;;; :field->fields
(defn sel:field->fields*
(defn ^:deprecated sel:field->fields*
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field->fields ...)` forms."
[key-field other-fields results]
(into {} (for [result results]
{(key-field result) (select-keys result other-fields)})))
(defmacro sel:field->fields
(defmacro ^:deprecated sel:field->fields
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field->fields ...)` forms."
[[entity key-field & other-fields] & forms]
......@@ -186,7 +186,7 @@
;;; : id->field
(defmacro sel:id->field
(defmacro ^:deprecated sel:id->field
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :id->field ...)` forms."
[[entity field] & forms]
......@@ -194,7 +194,7 @@
;;; :field->id
(defmacro sel:field->id
(defmacro ^:deprecated sel:field->id
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field->id ...)` forms."
[[entity field] & forms]
......@@ -202,14 +202,14 @@
;;; :field->obj
(defn sel:field->obj*
(defn ^:deprecated sel:field->obj*
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field->obj ...)` forms."
[field results]
(into {} (for [result results]
{(field result) result})))
(defmacro sel:field->obj
(defmacro ^:deprecated sel:field->obj
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel ... :field->obj ...)` forms."
[[entity field] & forms]
......@@ -217,13 +217,13 @@
;;; :one & :many
(defmacro sel:one
(defmacro ^:deprecated sel:one
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel :one ...)` forms."
[& args]
`(first (metabase.db/sel ~@args (k/limit 1))))
(defmacro sel:many
(defmacro ^:deprecated sel:many
"Part of the internal implementation for `sel`, don't call this directly!
Handle `(sel :many ...)` forms."
[& args]
......
......@@ -189,7 +189,7 @@
;; NOTE: we only handle active Tables/Fields and we skip any FK relationships (they can safely populate later)
(defmigration create-raw-tables
(when (= 0 (:cnt (first (k/select RawTable (k/aggregate (count :*) :cnt)))))
(binding [db/*sel-disable-logging* true]
(binding [db/*disable-db-logging* true]
(kdb/transaction
(doseq [{database-id :id, :keys [name engine]} (db/sel :many Database)]
(when-let [tables (not-empty (db/sel :many Table :db_id database-id, :active true))]
......
......@@ -31,7 +31,7 @@
;; if this database is already being synced then bail now
(when-not (contains? @currently-syncing-dbs database-id)
(binding [qp/*disable-qp-logging* true
db/*sel-disable-logging* true]
db/*disable-db-logging* true]
(let [db-driver (driver/engine->driver (:engine database))
full-sync? (if-not (nil? full-sync?)
full-sync?
......@@ -72,7 +72,7 @@
(events/publish-event :database-sync-begin {:database_id (:id database) :custom_id tracking-hash})
(binding [qp/*disable-qp-logging* true
db/*sel-disable-logging* true]
db/*disable-db-logging* true]
;; start with capturing a full introspection of the database
(introspect/introspect-database-and-update-raw-tables! driver database)
......@@ -95,7 +95,7 @@
(log/info (u/format-color 'magenta "Syncing table '%s' from %s database '%s'..." (:display_name table) (name driver) (:name database)))
(binding [qp/*disable-qp-logging* true
db/*sel-disable-logging* true]
db/*disable-db-logging* true]
;; if the Table has a RawTable backing it then do an introspection and sync
(when-let [raw-tbl (db/sel :one raw-table/RawTable :id (:raw_table_id table))]
(introspect/introspect-raw-table-and-update! driver database raw-tbl)
......
(ns metabase.util.honeysql-extensions
"Tweaks an utils for HoneySQL."
(:require [clojure.string :as s]
[honeysql.format :as hformat]))
;; Add an `:h2` quote style that uppercases the identifier
(let [quote-fns @(resolve 'honeysql.format/quote-fns)
ansi-quote-fn (:ansi quote-fns)]
(intern 'honeysql.format 'quote-fns
(assoc quote-fns :h2 (comp s/upper-case ansi-quote-fn))))
......@@ -5,8 +5,7 @@
; ported from https://stackoverflow.com/questions/8870261/how-to-split-text-without-spaces-into-list-of-words/11642687#11642687
(def ^:const ^:private special-words [
"checkins"])
(def ^:const ^:private special-words ["checkins"])
; # Build a cost dictionary, assuming Zipf's law and cost = -math.log(probability).
(def ^:private words (concat special-words (s/split-lines (slurp (io/file (io/resource "words-by-frequency.txt"))))))
......
(ns metabase.api.card-test
"Tests for /api/card endpoints."
(:require [expectations :refer :all]
[metabase.db :refer :all]
[metabase.db :as db]
[metabase.http-client :refer :all, :as http]
[metabase.middleware :as middleware]
(metabase.models [card :refer [Card]]
......@@ -207,9 +207,9 @@
(expect-with-temp [Card [{card-id :id, original-name :name}]]
[original-name
updated-name]
[(sel :one :field [Card :name] :id card-id)
[(db/sel :one :field [Card :name] :id card-id)
(do ((user->client :rasta) :put 200 (str "card/" card-id) {:name updated-name})
(sel :one :field [Card :name] :id card-id))]))
(db/sel :one :field [Card :name] :id card-id))]))
(defmacro ^:private with-temp-card {:style/indent 1} [binding & body]
......
(ns metabase.api.database-test
(:require [expectations :refer :all]
[korma.core :as k]
[metabase.db :refer :all]
[metabase.driver :as driver]
(metabase [db :as db]
[driver :as driver])
(metabase.models [database :refer [Database]]
[field :refer [Field]]
[table :refer [Table]])
......@@ -79,7 +79,7 @@
;; Check that we can create a Database
(let [db-name (random-name)]
(expect-eval-actual-first
(match-$ (sel :one Database :name db-name)
(match-$ (db/sel :one Database :name db-name)
{:created_at $
:engine "postgres" ; string because it's coming back from API instead of DB
:id $
......@@ -97,7 +97,7 @@
;; Check that we can delete a Database
(expect-let [db-name (random-name)
{db-id :id} (create-db db-name)
sel-db-name (fn [] (sel :one :field [Database :name] :id db-id))]
sel-db-name (fn [] (db/sel :one :field [Database :name] :id db-id))]
[db-name
nil]
[(sel-db-name)
......@@ -108,7 +108,7 @@
;; Check that we can update fields in a Database
(expect-let [[old-name new-name] (repeatedly 2 random-name)
{db-id :id} (create-db old-name)
sel-db (fn [] (sel :one :fields [Database :name :engine :details :is_full_sync] :id db-id))]
sel-db (fn [] (db/sel :one :fields [Database :name :engine :details :is_full_sync] :id db-id))]
[{:details {:host "localhost", :port 5432, :dbname "fakedb", :user "cam", :ssl true}
:engine :postgres
:name old-name
......@@ -146,7 +146,7 @@
:description nil
:features (mapv name (driver/features (driver/engine->driver engine)))})))
;; (?) I don't remember why we have to do this for postgres but not any other of the bonus drivers
(match-$ (sel :one Database :name db-name)
(match-$ (db/sel :one Database :name db-name)
{:created_at $
:engine "postgres"
:id $
......@@ -159,10 +159,10 @@
:features (mapv name (driver/features (driver/engine->driver :postgres)))}))))
(do
;; Delete all the randomly created Databases we've made so far
(cascade-delete Database :id [not-in (set (filter identity
(for [engine datasets/all-valid-engines]
(datasets/when-testing-engine engine
(:id (get-or-create-test-data-db! (driver/engine->driver engine)))))))])
(db/cascade-delete Database :id [not-in (set (filter identity
(for [engine datasets/all-valid-engines]
(datasets/when-testing-engine engine
(:id (get-or-create-test-data-db! (driver/engine->driver engine)))))))])
;; Add an extra DB so we have something to fetch besides the Test DB
(create-db db-name)
;; Now hit the endpoint
......@@ -171,7 +171,7 @@
;; GET /api/databases (include tables)
(let [db-name (str "A" (random-name))] ; make sure this name comes before "test-data"
(expect-eval-actual-first
(set (concat [(match-$ (sel :one Database :name db-name)
(set (concat [(match-$ (db/sel :one Database :name db-name)
{:created_at $
:engine "postgres"
:id $
......@@ -196,13 +196,13 @@
:is_full_sync true
:organization_id nil
:description nil
:tables (->> (sel :many Table :db_id (:id database))
:tables (->> (db/sel :many Table :db_id (:id database))
(mapv table-details)
(sort-by :name))
:features (mapv name (driver/features (driver/engine->driver engine)))})))))))
(do
;; Delete all the randomly created Databases we've made so far
(cascade-delete Database :id [not-in (set (filter identity
(db/cascade-delete Database :id [not-in (set (filter identity
(for [engine datasets/all-valid-engines]
(datasets/when-testing-engine engine
(:id (get-or-create-test-data-db! (driver/engine->driver engine)))))))])
......
......@@ -2,9 +2,9 @@
"Unit tests for /api/dataset endpoints."
(:require [expectations :refer :all]
[metabase.api.dataset :refer [dataset-query-api-constraints]]
[metabase.db :refer :all]
[metabase.models.card :refer [Card]]
[metabase.models.query-execution :refer [QueryExecution]]
[metabase.db :as db]
(metabase.models [card :refer [Card]]
[query-execution :refer [QueryExecution]])
[metabase.query-processor.expand :as ql]
[metabase.test.data.users :refer :all]
[metabase.test.data :refer :all]
......@@ -89,7 +89,7 @@
(query checkins
(ql/aggregation (ql/count)))))]
[(format-response result)
(format-response (sel :one QueryExecution :uuid (:uuid result)))]))
(format-response (db/sel :one QueryExecution :uuid (:uuid result)))]))
;; Even if a query fails we still expect a 200 response from the api
(expect
......@@ -121,7 +121,7 @@
:type "native"
:native {:query "foobar"}})]
[(format-response result)
(format-response (sel :one QueryExecution :uuid (:uuid result)))]))
(format-response (db/sel :one QueryExecution :uuid (:uuid result)))]))
;; GET /api/dataset/card/:id
......@@ -186,4 +186,4 @@
[(-> result
(update :card remove-ids-and-boolean-timestamps)
(update :result format-response))
(format-response (sel :one QueryExecution :uuid (get-in result [:result :uuid])))])))
(format-response (db/sel :one QueryExecution :uuid (get-in result [:result :uuid])))])))
......@@ -2,7 +2,7 @@
"Tests for /api/session"
(:require [cemerick.friend.credentials :as creds]
[expectations :refer :all]
[metabase.db :refer :all]
[metabase.db :as db]
[metabase.http-client :refer :all]
(metabase.models [session :refer [Session]]
[user :refer [User]])
......@@ -13,8 +13,8 @@
;; ## POST /api/session
;; Test that we can login
(expect-eval-actual-first
(sel :one :fields [Session :id] :user_id (user->id :rasta))
(do (del Session :user_id (user->id :rasta)) ; delete all other sessions for the bird first
(db/sel :one :fields [Session :id] :user_id (user->id :rasta))
(do (db/del Session :user_id (user->id :rasta)) ; delete all other sessions for the bird first
(client :post 200 "session" (user->credentials :rasta))))
;; Test for required params
......@@ -62,10 +62,10 @@
(expect
true
(let [reset-fields-set? (fn []
(let [{:keys [reset_token reset_triggered]} (sel :one :fields [User :reset_token :reset_triggered] :id (user->id :rasta))]
(let [{:keys [reset_token reset_triggered]} (db/sel :one :fields [User :reset_token :reset_triggered] :id (user->id :rasta))]
(boolean (and reset_token reset_triggered))))]
;; make sure user is starting with no values
(upd User (user->id :rasta) :reset_token nil :reset_triggered nil)
(db/upd User (user->id :rasta) :reset_token nil :reset_triggered nil)
(assert (not (reset-fields-set?)))
;; issue reset request (token & timestamp should be saved)
((user->client :rasta) :post 200 "session/forgot_password" {:email (:email (user->credentials :rasta))})
......@@ -91,7 +91,7 @@
:new "whateverUP12!!"}
{:keys [email id]} (create-user :password (:old password), :last_name user-last-name, :reset_triggered (System/currentTimeMillis))
token (str id "_" (java.util.UUID/randomUUID))
_ (upd User id :reset_token token)
_ (db/upd User id :reset_token token)
creds {:old {:password (:old password)
:email email}
:new {:password (:new password)
......@@ -108,18 +108,18 @@
;; New creds *should* work
(client :post 200 "session" (:new creds))
;; Double check that reset token was cleared
(sel :one :fields [User :reset_token :reset_triggered] :id id)))
(db/sel :one :fields [User :reset_token :reset_triggered] :id id)))
;; Check that password reset returns a valid session token
(let [user-last-name (random-name)]
(expect-eval-actual-first
(let [id (sel :one :id User, :last_name user-last-name)
session-id (sel :one :id Session, :user_id id)]
(let [id (db/sel :one :id User, :last_name user-last-name)
session-id (db/sel :one :id Session, :user_id id)]
{:success true
:session_id session-id})
(let [{:keys [email id]} (create-user :password "password", :last_name user-last-name, :reset_triggered (System/currentTimeMillis))
token (str id "_" (java.util.UUID/randomUUID))
_ (upd User id :reset_token token)]
_ (db/upd User id :reset_token token)]
;; run the password reset
(client :post 200 "session/reset_password" {:token token
:password "whateverUP12!!"}))))
......@@ -144,7 +144,7 @@
;; Test that an expired token doesn't work
(expect {:errors {:password "Invalid reset token"}}
(let [token (str (user->id :rasta) "_" (java.util.UUID/randomUUID))]
(upd User (user->id :rasta) :reset_token token, :reset_triggered 0)
(db/upd User (user->id :rasta) :reset_token token, :reset_triggered 0)
(client :post 400 "session/reset_password" {:token token
:password "whateverUP12!!"})))
......@@ -154,7 +154,7 @@
;; Check that a valid, unexpired token returns true
(expect {:valid true}
(let [token (str (user->id :rasta) "_" (java.util.UUID/randomUUID))]
(upd User (user->id :rasta) :reset_token token, :reset_triggered (dec (System/currentTimeMillis)))
(db/upd User (user->id :rasta) :reset_token token, :reset_triggered (dec (System/currentTimeMillis)))
(client :get 200 "session/password_reset_token_valid", :token token)))
;; Check than an made-up token returns false
......@@ -164,7 +164,7 @@
;; Check that an expired but valid token returns false
(expect {:valid false}
(let [token (str (user->id :rasta) "_" (java.util.UUID/randomUUID))]
(upd User (user->id :rasta) :reset_token token, :reset_triggered 0)
(db/upd User (user->id :rasta) :reset_token token, :reset_triggered 0)
(client :get 200 "session/password_reset_token_valid", :token token)))
......
(ns metabase.api.setup-test
"Tests for /api/setup endpoints."
(:require [expectations :refer :all]
[metabase.db :refer :all]
[metabase.db :as db]
[metabase.http-client :as http]
(metabase.models [session :refer [Session]]
[setting :as setting]
......@@ -15,9 +15,9 @@
;; Check that we can create a new superuser via setup-token
(let [user-name (random-name)]
(expect-eval-actual-first
[(match-$ (->> (sel :one User :email (str user-name "@metabase.com"))
[(match-$ (->> (db/sel :one User :email (str user-name "@metabase.com"))
(:id)
(sel :one Session :user_id))
(db/sel :one Session :user_id))
{:id $id})
(str user-name "@metabase.com")]
(let [resp (http/client :post 200 "setup" {:token (setup/token-create)
......
(ns metabase.api.table-test
"Tests for /api/table endpoints."
(:require [expectations :refer :all]
[metabase.db :refer :all]
(metabase [http-client :as http]
(metabase [db :as db]
[driver :as driver]
[http-client :as http]
[middleware :as middleware])
(metabase.models [field :refer [Field]]
[table :refer [Table]])
......@@ -36,7 +37,7 @@
:is_full_sync true
:organization_id nil
:description nil
:features (mapv name (metabase.driver/features (metabase.driver/engine->driver :h2)))}))
:features (mapv name (driver/features (driver/engine->driver :h2)))}))
;; ## GET /api/table
......@@ -218,7 +219,7 @@
;;; GET api/table/:id/query_metadata?include_sensitive_fields
;;; Make sure that getting the User table *does* include info about the password field, but not actual values themselves
(expect
(match-$ (sel :one Table :id (id :users))
(match-$ (db/sel :one Table :id (id :users))
{:description nil
:entity_type nil
:visibility_type nil
......@@ -226,7 +227,7 @@
:schema "PUBLIC"
:name "USERS"
:display_name "Users"
:fields [(match-$ (sel :one Field :id (id :users :id))
:fields [(match-$ (db/sel :one Field :id (id :users :id))
{:description nil
:table_id (id :users)
:special_type "id"
......@@ -246,7 +247,7 @@
:parent_id nil
:raw_column_id $
:last_analyzed $})
(match-$ (sel :one Field :id (id :users :last_login))
(match-$ (db/sel :one Field :id (id :users :last_login))
{:description nil
:table_id (id :users)
:special_type nil
......@@ -266,7 +267,7 @@
:parent_id nil
:raw_column_id $
:last_analyzed $})
(match-$ (sel :one Field :id (id :users :name))
(match-$ (db/sel :one Field :id (id :users :name))
{:description nil
:table_id (id :users)
:special_type "name"
......@@ -286,7 +287,7 @@
:parent_id nil
:raw_column_id $
:last_analyzed $})
(match-$ (sel :one Field :table_id (id :users) :name "PASSWORD")
(match-$ (db/sel :one Field :table_id (id :users) :name "PASSWORD")
{:description nil
:table_id (id :users)
:special_type "category"
......@@ -438,7 +439,7 @@
(expect-eval-actual-first
(match-$ (let [table (Table (id :users))]
;; reset Table back to its original state
(upd Table (id :users) :display_name "Users" :entity_type nil :visibility_type nil :description nil)
(db/upd Table (id :users) :display_name "Users" :entity_type nil :visibility_type nil :description nil)
table)
{:description "What a nice table!"
:entity_type "person"
......@@ -454,7 +455,7 @@
:id $
:engine "h2"
:created_at $
:features (mapv name (metabase.driver/features (metabase.driver/engine->driver :h2)))})
:features (mapv name (driver/features (driver/engine->driver :h2)))})
:schema "PUBLIC"
:name "USERS"
:rows 15
......@@ -477,8 +478,8 @@
;; ## GET /api/table/:id/fks
;; We expect a single FK from CHECKINS.USER_ID -> USERS.ID
(expect
(let [checkins-user-field (sel :one Field :table_id (id :checkins) :name "USER_ID")
users-id-field (sel :one Field :table_id (id :users) :name "ID")]
(let [checkins-user-field (db/sel :one Field :table_id (id :checkins) :name "USER_ID")
users-id-field (db/sel :one Field :table_id (id :users) :name "ID")]
[{:origin_id (:id checkins-user-field)
:destination_id (:id users-id-field)
:relationship "Mt1"
......@@ -557,15 +558,15 @@
;; ## POST /api/table/:id/reorder
(expect-eval-actual-first
{:result "success"}
(let [categories-id-field (sel :one Field :table_id (id :categories) :name "ID")
categories-name-field (sel :one Field :table_id (id :categories) :name "NAME")
(let [categories-id-field (db/sel :one Field :table_id (id :categories) :name "ID")
categories-name-field (db/sel :one Field :table_id (id :categories) :name "NAME")
api-response ((user->client :crowberto) :post 200 (format "table/%d/reorder" (id :categories))
{:new_order [(:id categories-name-field) (:id categories-id-field)]})]
;; check the modified values (have to do it here because the api response tells us nothing)
(assert (= 0 (:position (sel :one :fields [Field :position] :id (:id categories-name-field)))))
(assert (= 1 (:position (sel :one :fields [Field :position] :id (:id categories-id-field)))))
(assert (= 0 (:position (db/sel :one :fields [Field :position] :id (:id categories-name-field)))))
(assert (= 1 (:position (db/sel :one :fields [Field :position] :id (:id categories-id-field)))))
;; put the values back to their previous state
(upd Field (:id categories-name-field) :position 0)
(upd Field (:id categories-id-field) :position 0)
(db/upd Field (:id categories-name-field) :position 0)
(db/upd Field (:id categories-id-field) :position 0)
;; return our origin api response for validation
api-response))
......@@ -2,8 +2,8 @@
"Tests for /api/user endpoints."
(:require [expectations :refer :all]
[korma.core :as k]
[metabase.db :refer :all]
(metabase [http-client :as http]
(metabase [db :as db]
[http-client :as http]
[middleware :as middleware])
(metabase.models [session :refer [Session]]
[user :refer [User]])
......@@ -61,7 +61,7 @@
(do
;; Delete all the other random Users we've created so far
(let [user-ids (set (map user->id [:crowberto :rasta :lucky :trashbird]))]
(cascade-delete User :id [not-in user-ids]))
(db/cascade-delete User :id [not-in user-ids]))
;; Now do the request
(set ((user->client :rasta) :get 200 "user")))) ; as a set since we don't know what order the results will come back in
......@@ -70,7 +70,7 @@
;; Test that we can create a new User
(let [rand-name (random-name)]
(expect-eval-actual-first
(match-$ (sel :one User :first_name rand-name)
(match-$ (db/sel :one User :first_name rand-name)
{:id $
:email $
:first_name rand-name
......@@ -85,7 +85,7 @@
;; Test that reactivating a disabled account works
(let [rand-name (random-name)]
(expect-eval-actual-first
(match-$ (sel :one User :first_name rand-name :is_active true)
(match-$ (db/sel :one User :first_name rand-name :is_active true)
{:id $
:email $
:first_name rand-name
......@@ -97,7 +97,7 @@
:is_qbnewb true})
(when-let [user (create-user-api rand-name)]
;; create a random user then set them to :inactive
(upd User (:id user)
(db/upd User (:id user)
:is_active false
:is_superuser true)
;; then try creating the same user again
......@@ -186,7 +186,7 @@
(expect-let [{old-first :first_name, last-name :last_name, old-email :email, id :id, :as user} (create-user)
new-first (random-name)
new-email (.toLowerCase ^String (str new-first "@metabase.com"))
fetch-user (fn [] (sel :one :fields [User :first_name :last_name :is_superuser :email] :id id))]
fetch-user (fn [] (db/sel :one :fields [User :first_name :last_name :is_superuser :email] :id id))]
[{:email old-email
:is_superuser false
:last_name last-name
......@@ -201,7 +201,7 @@
(fetch-user))])
;; Test that a normal user cannot change the :is_superuser flag for themselves
(expect-let [fetch-user (fn [] (sel :one :fields [User :first_name :last_name :is_superuser :email] :id (user->id :rasta)))]
(expect-let [fetch-user (fn [] (db/sel :one :fields [User :first_name :last_name :is_superuser :email] :id (user->id :rasta)))]
[(fetch-user)]
[(do ((user->client :rasta) :put 200 (str "user/" (user->id :rasta)) (-> (fetch-user)
(assoc :is_superuser true)))
......@@ -220,7 +220,7 @@
;; Test that a User can change their password
(expect-let [creds {:email "abc@metabase.com"
:password "def"}
{:keys [id password]} (ins User
{:keys [id password]} (db/ins User
:first_name "test"
:last_name "user"
:email "abc@metabase.com"
......@@ -231,7 +231,7 @@
(metabase.http-client/client creds :put 200 (format "user/%d/password" id) {:password "abc123!!DEF"
:old_password (:password creds)})
;; now simply grab the lastest pass from the db and compare to the one we have from before reset
(not= password (sel :one :field [User :password] :email (:email creds)))))
(not= password (db/sel :one :field [User :password] :email (:email creds)))))
;; Check that a non-superuser CANNOT update someone else's password
(expect "You don't have permissions to do that."
......@@ -260,7 +260,7 @@
(let [creds {:email "def@metabase.com"
:password "def123"}]
[(metabase.http-client/client creds :put 200 (format "user/%d/qbnewb" id))
(sel :one :field [User :is_qbnewb] :id id)])))
(db/sel :one :field [User :is_qbnewb] :id id)])))
;; Check that a non-superuser CANNOT update someone else's password
......
(ns metabase.db.metadata-queries-test
(:require [expectations :refer :all]
[metabase.db :refer :all]
[metabase.db :as db]
[metabase.db.metadata-queries :refer :all]
[metabase.models.field :refer [Field]]
[metabase.test.data :refer :all]))
(defn- fetch-field [table-kw field-kw]
(sel :one Field :id (id table-kw field-kw)))
(db/sel :one Field :id (id table-kw field-kw)))
;; ### FIELD-DISTINCT-COUNT
(expect 100
......
(ns metabase.driver.generic-sql-test
(:require [expectations :refer :all]
[metabase.db :refer :all]
[metabase.driver :as driver]
(metabase [db :as db]
[driver :as driver])
[metabase.driver.generic-sql :refer :all]
(metabase.models [field :refer [Field]]
[table :refer [Table]])
[table :refer [Table], :as table])
[metabase.test.data :refer :all]
[metabase.test.util :refer [resolve-private-fns]]
[metabase.models.table :as table])
[metabase.test.util :refer [resolve-private-fns]])
(:import metabase.driver.h2.H2Driver))
(def users-table
(delay (sel :one Table :name "USERS")))
(delay (db/sel :one Table :name "USERS")))
(def venues-table
(delay (Table (id :venues))))
......
(ns metabase.models.common-test
(:require [expectations :refer :all]
[metabase.db :refer :all]
[metabase.models.common :refer [name->human-readable-name]]
[metabase.test.data :refer :all]))
......
(ns metabase.models.field-test
(:require [expectations :refer :all]
[metabase.db :refer :all]
(metabase.models [field :refer :all]
[field-values :refer :all])
[metabase.test.data :refer :all]))
......
......@@ -4,7 +4,7 @@
(:require [clojure.math.numeric-tower :as math]
[clojure.set :as set]
[expectations :refer :all]
(metabase [db :refer :all]
(metabase [db :as db]
[driver :as driver])
(metabase.models [field :refer [Field]]
[table :refer [Table]])
......@@ -943,9 +943,9 @@
(ql/limit 1))
:data :cols set))]
[(get-col-names)
(do (upd Field (id :venues :price) :visibility_type :details-only)
(do (db/upd Field (id :venues :price) :visibility_type :details-only)
(get-col-names))
(do (upd Field (id :venues :price) :visibility_type :normal)
(do (db/upd Field (id :venues :price) :visibility_type :normal)
(get-col-names))]))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment