Skip to content
Snippets Groups Projects
Commit 36ec742e authored by Allen Gilliland's avatar Allen Gilliland
Browse files

remove admin SQL Query backend files and api endpoints.

parent a765e1d0
Branches
Tags
No related merge requests found
Showing with 28 additions and 889 deletions
(ns metabase.api.qs
"/api/qs endpoints."
(:require [clojure.data.csv :as csv]
[compojure.core :refer [defroutes GET POST]]
[metabase.api.common :refer :all]
[metabase.db :refer :all]
[metabase.driver :as driver]
(metabase.models [database :refer [Database]]
[hydrate :refer :all]
[query-execution :refer [QueryExecution all-fields build-response]])
[metabase.util :refer [contains-many? now-iso8601]]))
(defendpoint POST "/"
"Create a new `Query`, and start it asynchronously."
[:as {{:keys [timezone database sql] :as body} :body}]
{database [Required Integer]
sql [Required NonEmptyString]} ; TODO - check timezone
(read-check Database database)
(let [dataset-query {:type "native"
:database database
:native {:query sql
:timezone timezone}}
options {:executed_by *current-user-id*
:synchronously false
:cache_result true}]
(driver/dataset-query dataset-query options)))
(defendpoint GET "/:uuid"
"Fetch the results of a `Query` with UUID."
[uuid]
(let-404 [query-execution (sel :one all-fields :uuid uuid)]
(build-response query-execution)))
(defendpoint GET "/:uuid/csv"
"Fetch the results of a `Query` with UUID as CSV."
[uuid]
(let-404 [{{:keys [columns rows]} :result_data} (sel :one all-fields :uuid uuid)]
{:status 200
:body (with-out-str
(csv/write-csv *out* (into [columns] rows)))
:headers {"Content-Type" "text/csv"
"Content-Disposition" (str "attachment; filename=\"query_result_" (now-iso8601) ".csv\"")}}))
(define-routes)
(ns metabase.api.query
"/api/query endpoints."
(:require [clojure.data.csv :as csv]
[korma.core :refer [where subselect fields order limit]]
[compojure.core :refer [defroutes GET PUT POST DELETE]]
[medley.core :refer :all]
[metabase.api.common :refer :all]
[metabase.db :refer :all]
[metabase.driver :as driver]
(metabase.models [common :as common]
[hydrate :refer :all]
[database :refer [Database databases-for-org]]
[org :refer [Org]]
[query :refer [Query]]
[query-execution :refer [QueryExecution all-fields]])
[metabase.util :as util]))
(defendpoint GET "/form_input"
"Values of options for the create/edit `Query` UI."
[org]
{org Required}
(read-check Org org)
{:permissions common/permissions
:timezones common/timezones
:databases (databases-for-org org)})
(defendpoint GET "/"
"Fetch all `Querys` for an `Org`. With filter option F (default `all`), return all queries;
with `mine`, only return queries created by the current user."
[org f]
{org Required, f FilterOptionAllOrMine}
(read-check Org org)
(let [all? (or (not f) (= f :all))]
(-> (sel :many Query
(where {:database_id [in (subselect Database (fields :id) (where {:organization_id org}))]})
(where (or {:creator_id *current-user-id*}
(when all?
{:public_perms [> common/perms-none]})))
(order :name :ASC))
(hydrate :creator :database))))
(defn query-clone
"Create a new query by cloning an existing query. Returns a 403 if user doesn't have acces to read query."
[query-id]
{:pre [(integer? query-id)]}
(let-400 [{:keys [name] :as query} (sel :one Query :id query-id)]
(read-check query)
(->> (-> query
(select-keys [:type :details :database_id])
(assoc :name (str name " CLONED")
:public_perms common/perms-none
:creator_id *current-user-id*))
(mapply ins Query))))
(defn query-create
"Create a new query from user posted data."
[{:keys [name sql timezone public_perms database]
:or {name (str "New Query: " (java.util.Date.))
public_perms common/perms-none}}]
(check database [400 "'database' is a required param."]
sql [400 "'sql' is a required param."])
(read-check Database database)
(ins Query
:type "rawsql"
:name name
:details {:sql sql
:timezone timezone}
:public_perms public_perms
:creator_id *current-user-id*
:database_id database))
(defendpoint POST "/"
"Clone or create a `Query` (`clone`, if passed, should be the ID of the `Query` to clone)."
[:as {{:keys [clone] :as body} :body}]
{clone Integer}
(if clone
(query-clone clone)
(query-create body)))
(defendpoint GET "/:id"
"Fetch a `Query`."
[id]
(->404 (sel :one Query :id id)
read-check
(hydrate :creator :database :can_read :can_write)))
(defendpoint PUT "/:id"
"Update a `Query`."
[id :as {{:keys [name public_perms details timezone database] :as body} :body}]
{database [Required Dict]
name NonEmptyString
public_perms PublicPerms}
(read-check Database (:id database))
(let-404 [query (sel :one Query :id id)]
(write-check query)
(-> (util/select-non-nil-keys body :name :public_perms :details)
(assoc :version (:version query) ; don't increment this here. That happens on pre-update
:database_id (:id database)) ; you can change the DB of a Query why??
(#(mapply upd Query id %)))
(-> (sel :one Query :id id)
(hydrate :creator :database))))
(defendpoint DELETE "/:id"
"Delete a `Query`."
[id]
(write-check Query id)
(del Query :id id))
(defendpoint POST "/:id"
"Execute a `Query` asynchronously."
[id]
(let-404 [{database-id :database_id
{:keys [sql timezone]} :details :as query} (sel :one Query :id id)]
(read-check query)
(let [dataset-query {:type :native
:database database-id
:native {:query sql
:timezone timezone}}
options {:executed_by *current-user-id*
:saved_query query
:synchronously false
:cache_result true}]
(driver/dataset-query dataset-query options))))
(defendpoint GET "/:id/results"
"Fetch the most recent results of a `Query`."
[id]
(read-check Query id)
(sel :many QueryExecution :query_id id (order :finished_at :DESC) (limit 10)))
(defendpoint GET "/:id/csv"
"Fetch the most recent results of a `Query` as CSV."
[id]
(let-404 [{{:keys [columns rows]} :result_data :as query-execution} (sel :one all-fields, :query_id id, (order :started_at :DESC))]
(let-404 [{{:keys [can_read name]} :query} (hydrate query-execution :query)]
(check-403 @can_read)
{:status 200
:body (with-out-str (csv/write-csv *out* (into [columns] rows)))
:headers {"Content-Type" "text/csv"
"Content-Disposition" (str "attachment; filename=\"" name ".csv\"")}})))
(define-routes)
(ns metabase.api.result
"/api/result/* endpoints representing saved Query executions."
(:require [clojure.data.csv :as csv]
[korma.core :refer [where subselect fields]]
[compojure.core :refer [defroutes GET PUT POST DELETE]]
[medley.core :refer [mapply]]
(metabase.api [common :refer :all])
[metabase.db :refer :all]
(metabase.models [database :refer [Database databases-for-org]]
[hydrate :refer :all]
[org :refer [Org]]
[query-execution :refer [QueryExecution all-fields build-response]])))
(defendpoint GET "/:id"
"Returns the basic information about a given query result."
[id]
(let-404 [{:keys [query_id] :as query-execution} (sel :one QueryExecution :id id)]
;; NOTE - this endpoint requires there to be a saved query associated with this execution
(check-404 query_id)
(let-404 [{{can_read :can_read} :query} (hydrate query-execution :query)]
(check-403 @can_read)
query-execution)))
(defendpoint GET "/:id/response"
"Returns the actual data response for a given query result (as if the query was just executed)."
[id]
(let-404 [{:keys [query_id] :as query-execution} (sel :one all-fields :id id)]
;; NOTE - this endpoint requires there to be a saved query associated with this execution
(check-404 query_id)
(let-404 [{{can_read :can_read} :query} (hydrate query-execution :query)]
(check-403 @can_read)
(build-response query-execution))))
(defendpoint GET "/:id/csv"
"Returns the data response for a given query result as a CSV file."
[id]
(let-404 [{:keys [result_data query_id] :as query-execution} (sel :one all-fields :id id)]
;; NOTE - this endpoint requires there to be a saved query associated with this execution
(check-404 query_id)
(let-404 [{{can_read :can_read name :name} :query} (hydrate query-execution :query)]
(check-403 @can_read)
{:status 200
:body (with-out-str (csv/write-csv *out* (into [(:columns result_data)] (:rows result_data))))
:headers {"Content-Type" "text/csv"
"Content-Disposition" (str "attachment; filename=\"" name ".csv\"")}})))
(define-routes)
......@@ -5,9 +5,6 @@
[dash :as dash]
[notify :as notify]
[org :as org]
[qs :as qs]
[query :as query]
[result :as result]
[session :as session]
[setting :as setting]
[setup :as setup]
......@@ -42,9 +39,6 @@
(context "/meta/table" [] (+auth table/routes))
(context "/notify" [] (+apikey notify/routes))
(context "/org" [] (+auth org/routes))
(context "/qs" [] (+auth qs/routes))
(context "/query" [] (+auth query/routes))
(context "/result" [] (+auth result/routes))
(context "/session" [] session/routes)
(context "/setting" [] (+auth setting/routes))
(context "/setup" [] setup/routes)
......
......@@ -2,12 +2,10 @@
(:require clojure.java.classpath
[clojure.tools.logging :as log]
[clojure.tools.namespace.find :as ns-find]
[cheshire.core :as cheshire]
[medley.core :refer :all]
[metabase.db :refer [exists? ins sel upd]]
(metabase.driver [interface :as i]
[query-processor :as qp]
[result :as result])
[query-processor :as qp])
(metabase.models [database :refer [Database]]
[query-execution :refer [QueryExecution]])
[metabase.util :as u]))
......@@ -139,14 +137,6 @@
;; ## Query Execution Stuff
(defn- execute-query
"Process and run a query and return results."
[{:keys [type] :as query}]
(case (keyword type)
:native (process-query query)
:query (process-query query)
:result (result/process-and-run query)))
(defn dataset-query
"Process and run a json based dataset query and return results.
......@@ -160,21 +150,16 @@
Possible caller-options include:
:executed_by [int] (user_id of caller)
:saved_query [{}] (dictionary representing Query model)
:synchronously [true|false] (default true)
:cache_result [true|false] (default false)"
{:arglists '([query caller-options])}
[query {:keys [executed_by synchronously saved_query]
:or {synchronously true}
:as caller-options}]
:executed_by [int] (user_id of caller)"
{:arglists '([query options])}
[query {:keys [executed_by]
:as options}]
{:pre [(integer? executed_by)]}
(let [options (merge {:cache_result false} caller-options)
query-execution {:uuid (.toString (java.util.UUID/randomUUID))
(let [query-execution {:uuid (.toString (java.util.UUID/randomUUID))
:executor_id executed_by
:json_query query
:query_id (:id saved_query)
:version (get saved_query :version 0)
:query_id nil
:version 0
:status :starting
:error ""
:started_at (u/new-sql-timestamp)
......@@ -185,35 +170,18 @@
:result_data "{}"
:raw_query ""
:additional_info ""}]
(if synchronously
(-dataset-query query options query-execution)
;; TODO - this is untested/used. determine proper way to place execution on background thread
(let [query-execution (-> (save-query-execution query-execution)
;; this is a bit lame, but to avoid having the delay fns in the dictionary we are
;; trimming them right here. maybe there is a better way?
(select-keys [:id :uuid :executor :query_id :version :status :started_at]))]
;; run the query, but do it on another thread
(future (-dataset-query query options query-execution))
;; this ensures the currently saved query-execution is what gets returned
query-execution))))
(defn -dataset-query
"Execute a query and record the outcome. Entire execution is wrapped in a try-catch to prevent Exceptions
from leaking outside the function call."
[query options query-execution]
(let [query-execution (assoc query-execution :start_time_millis (System/currentTimeMillis))]
(try
(let [query-result (execute-query query)]
(when-not (contains? query-result :status)
(throw (Exception. "invalid response from database driver. no :status provided")))
(when (= :failed (:status query-result))
(throw (Exception. ^String (get query-result :error "general error"))))
(query-complete query-execution query-result (:cache_result options)))
(catch Exception ex
(log/warn ex)
(.printStackTrace ex)
(query-fail query-execution (.getMessage ex))))))
(let [query-execution (assoc query-execution :start_time_millis (System/currentTimeMillis))]
(try
(let [query-result (process-query query)]
(when-not (contains? query-result :status)
(throw (Exception. "invalid response from database driver. no :status provided")))
(when (= :failed (:status query-result))
(throw (Exception. ^String (get query-result :error "general error"))))
(query-complete query-execution query-result))
(catch Exception ex
(log/warn ex)
(.printStackTrace ex)
(query-fail query-execution (.getMessage ex)))))))
(defn query-fail
"Save QueryExecution state and construct a failed query response"
......@@ -236,16 +204,13 @@
(defn query-complete
"Save QueryExecution state and construct a completed (successful) query response"
[query-execution query-result cache-result]
[query-execution query-result]
;; record our query execution and format response
(-> (u/assoc* query-execution
:status :completed
:finished_at (u/new-sql-timestamp)
:running_time (- (System/currentTimeMillis) (:start_time_millis <>))
:result_rows (get query-result :row_count 0)
:result_data (if cache-result
(cheshire/generate-string (:data query-result))
"{}"))
:result_rows (get query-result :row_count 0))
(dissoc :start_time_millis)
(save-query-execution)
;; at this point we've saved and we just need to massage things into our final response format
......
(ns metabase.driver.result
"The `result` query processor."
(:require [clojure.tools.logging :as log]
[korma.core :as korma]
[metabase.api.common :refer :all]
[metabase.db :refer [sel]]
[metabase.models.query-execution :refer [QueryExecution all-fields build-response]]))
(defn process-and-run [{:keys [result] :as query}]
(log/debug "RESULT QUERY: " query)
(if-let [query-execution (sel :one all-fields :query_id (:query_id result) (korma/order :started_at :DESC))]
(build-response query-execution)
{:status :failed
:error (str "Failed to looked result for query: " (:query_id result))}))
......@@ -19,8 +19,7 @@
:can_write (delay (org-can-write organization_id))))
(defmethod pre-cascade-delete Database [_ {:keys [id] :as database}]
(cascade-delete 'metabase.models.table/Table :db_id id)
(cascade-delete 'metabase.models.query/Query :database_id id))
(cascade-delete 'metabase.models.table/Table :db_id id))
(defn databases-for-org
"Selects the ID and NAME for all databases available to the given org-id."
......
(ns metabase.models.query
(:require [korma.core :refer :all]
[metabase.api.common :refer [check]]
[metabase.db :refer :all]
(metabase.models [common :refer :all]
[user :refer [User]]
[database :refer [Database]])
[metabase.util :as u]))
(defentity Query
(table :query_query)
(types {:details :json})
timestamped
(assoc :hydration-keys #{:query}))
;; default fields to return for `sel` Query
(defmethod default-fields Query [_]
[:id
:created_at
:updated_at
:name
:type
:details
:version
:public_perms
:creator_id
:database_id])
(defmethod pre-insert Query [_ query]
(let [defaults {:version 1}]
(merge defaults query)))
(defmethod pre-update Query [_ {:keys [version] :as query}]
(-> query
(u/select-non-nil-keys :name :database_id :public_perms :details)
(assoc :version (+ 1 version))))
(defmethod post-select Query [_ {:keys [creator_id database_id] :as query}]
(-> query
(u/assoc* :creator (delay (check creator_id 500 "Can't get creator: Query doesn't have a :creator_id.")
(sel :one User :id creator_id))
:database (delay (check database_id 500 "Can't get database: Query doesn't have a :database_id.")
(sel :one Database :id database_id))
:organization_id (delay (:organization_id @(:database <>))))
assoc-permissions-sets))
(defmethod pre-cascade-delete Query [_ {:keys [id]}]
(cascade-delete 'metabase.models.query-execution/QueryExecution :query_id id))
......@@ -3,8 +3,7 @@
[metabase.api.common :refer [check]]
[metabase.db :refer :all]
(metabase.models [common :refer :all]
[database :refer [Database]]
[query :refer [Query]])))
[database :refer [Database]])))
(defentity QueryExecution
......@@ -13,22 +12,6 @@
:result_data :json
:status :keyword}))
(def all-fields [QueryExecution
:id
:uuid
:version
:json_query
:raw_query
:status
:started_at
:finished_at
:running_time
:error
:result_file
:result_rows
:result_data
:query_id])
;; default fields to return for `sel QueryExecution
;; specifically excludes stored data columns
(defmethod default-fields QueryExecution [_]
......@@ -41,26 +24,9 @@
:started_at
:finished_at
:running_time
:result_rows
:query_id])
:error
:result_rows])
(defmethod post-select QueryExecution [_ {:keys [query_id result_rows] :as query-execution}]
(defmethod post-select QueryExecution [_ {:keys [result_rows] :as query-execution}]
(assoc query-execution
:row_count (or result_rows 0) ; sadly we have 2 ways to reference the row count :(
:query (delay (check query_id 500 "Can't get execution: QueryExecution doesn't have a :query_id.")
(sel :one Query :id query_id))))
(defn build-response
"Build a query response from a QueryExecution record."
[{{:keys [cols columns rows data]
:or {cols []
columns []}} :result_data :as query-execution}]
(let [rows (or rows data [])]
(-> (select-keys query-execution [:id :uuid :status])
(assoc :data {:rows rows
:cols cols
:columns columns}
:row_count (count rows))
(cond->
(= :failed (:status query-execution)) (assoc :error (:error query-execution)
:sql_error (:error query-execution))))))
:row_count (or result_rows 0))) ; sadly we have 2 ways to reference the row count :(
(ns metabase.task.execute-queries
(:require (metabase [db :refer :all]
[driver :as driver]
[task :refer :all])
[metabase.models.query :refer [Query]]))
(defn execute-queries
"Execute all `Querys` in the database, one-at-a-time."
[]
(->> (sel :many Query)
(map (fn [{database-id :database_id
creator-id :creator_id
{:keys [sql timezone]} :details :as query}]
(let [dataset-query {:type :native ; TODO: this code looks too much like the code in POST /api/query/:id
:database database-id ; it would make me happier if there was a nice way to de-duplicate it
:native {:query sql
:timezone timezone}}
options {:executed_by creator-id ; HACK: Technically, creator *isn't* executing this `Query`, but this is a required field
:saved_query query
:synchronously true ; probably makes sense to run these one-at-a-time to avoid putting too much stress on the DB
:cache_result true}]
(driver/dataset-query dataset-query options))))
dorun))
(add-hook! #'nightly-tasks-hook execute-queries)
......@@ -36,7 +36,6 @@
:row_count 0
:result_rows 0
:status "failed"
:query_id nil
:version 0
:json_query $
:started_at $
......
(ns metabase.api.qs-test
"Tests for /api/qs endpoints."
(:require [expectations :refer :all]
[korma.core :refer :all]
[metabase.db :refer :all]
(metabase.models [query-execution :refer [QueryExecution]])
[metabase.test.util :refer [match-$ random-name expect-eval-actual-first]]
[metabase.test-data :refer :all]))
;; ## Helper Fns
(defn create-query []
((user->client :rasta) :post 200 "qs" {:database (:id @test-db)
:sql "SELECT COUNT(*) FROM CATEGORIES;"}))
(defn fetch-query
([uuid]
(fetch-query uuid 500))
([uuid maxwait-ms]
(let [{:keys [status] :as result} ((user->client :rasta) :get (format "qs/%s" uuid))]
(if (or (not= status "starting")
(<= maxwait-ms 0))
result
(do (Thread/sleep 50)
(recur uuid (- maxwait-ms 50)))))))
;; ## POST /api/qs
;; Test that we can create a Query
(expect-eval-actual-first
(match-$ (sel :one QueryExecution (order :id :desc))
{:id $
:uuid $
:query_id nil
:version 0
:started_at $})
(-> (create-query)
(dissoc :status))) ; status is a race condition and can be either 'running' or 'completed'
;; ## GET /api/qs/:uuid
;; Can we fetch the results of a Query ?
(expect-eval-actual-first
(match-$ (sel :one QueryExecution (order :id :desc))
{:id $
:uuid $
:status "completed"
:data {:columns ["count(*)"]
:cols [{:base_type "IntegerField"
:name "count(*)"}]
:rows [[75]]}
:row_count 1})
(let [{uuid :uuid} (create-query)]
(fetch-query uuid)))
;; ## GET /api/qs/:uuid/csv
(expect-eval-actual-first
"count(*)\n75\n"
(let [{uuid :uuid} (create-query)]
(Thread/sleep 50)
((user->client :rasta) :get 200 (format "qs/%s/csv" uuid))))
(ns metabase.api.query-test
"Tests for /api/query endpoints."
(:require [expectations :refer :all]
[korma.core :refer :all]
[metabase.db :refer :all]
[metabase.driver.mongo.test-data :as mongo-test-data]
(metabase.models [database :refer [Database]]
[query :refer [Query]]
[query-execution :refer [QueryExecution]])
[metabase.test.util :refer [match-$ random-name expect-eval-actual-first]]
[metabase.test.data.datasets :as datasets]
[metabase.test-data :refer :all]))
;; ## Helper Fns
(defn create-query [& {:as kwargs}]
((user->client :rasta) :post 200 "query" (merge {:database (:id @test-db)
:sql "SELECT COUNT(*) FROM VENUES;"}
kwargs)))
;; ## GET /api/query/form_input
(expect-eval-actual-first
{:databases (filter identity
[(datasets/when-testing-dataset :mongo
(match-$ @mongo-test-data/mongo-test-db
{:id $
:name "Mongo Test"}))
(datasets/when-testing-dataset :generic-sql
(match-$ @test-db
{:id $
:name "Test Database"}))])
:timezones ["GMT"
"UTC"
"US/Alaska"
"US/Arizona"
"US/Central"
"US/Eastern"
"US/Hawaii"
"US/Mountain"
"US/Pacific"
"America/Costa_Rica"]
:permissions [{:name "None", :id 0}
{:name "Read Only", :id 1}
{:name "Read & Write", :id 2}]}
(do
;; delete any other rando test DBs made by other tests
(cascade-delete Database :organization_id @org-id :id [not-in (set (filter identity
[(datasets/when-testing-dataset :generic-sql
@db-id)
(datasets/when-testing-dataset :mongo
@mongo-test-data/mongo-test-db-id)]))])
((user->client :rasta) :get 200 "query/form_input" :org @org-id)))
;; ## POST /api/query (create)
;; Check that we can save a Query
(expect-eval-actual-first
(match-$ (sel :one Query (order :id :DESC))
{:database_id (:id @test-db)
:name $
:type "rawsql"
:creator_id (user->id :rasta)
:updated_at $
:details {:timezone nil
:sql "SELECT COUNT(*) FROM VENUES;"}
:id $
:version $
:public_perms 0
:created_at $})
(create-query))
;; ## GET /api/query/:id
;; Check that we can fetch details for a Query
(expect-eval-actual-first
(match-$ (sel :one Query (order :id :DESC))
{:id $
:name $
:type "rawsql"
:creator_id (user->id :rasta)
:updated_at $
:details {:timezone nil
:sql "SELECT COUNT(*) FROM VENUES;"}
:database_id (:id @test-db)
:database (match-$ @test-db
{:created_at $
:engine "h2"
:id $
:details $
:updated_at $
:name "Test Database"
:organization_id @org-id
:description nil})
:creator (match-$ (fetch-user :rasta)
{:common_name "Rasta Toucan"
:date_joined $
:last_name "Toucan"
:id $
:is_superuser false
:last_login $
:first_name "Rasta"
:email "rasta@metabase.com"})
:can_read true
:can_write true
:version $
:public_perms 0
:created_at $})
(let [{id :id} (create-query)]
((user->client :rasta) :get 200 (format "query/%d" id))))
;; ## PUT /api/query/:id
;; Check that we can update a Query
(expect-eval-actual-first
[{:name "My Awesome Query"
:version 2}
{:name "My Awesome Query 2"
:version 3}]
(let [{:keys [id name database_id]} (create-query)
get-query-name-and-version (fn [] (sel :one :fields [Query :name :version] :id id))]
[(do ((user->client :rasta) :put 200 (format "query/%d" id) {:name "My Awesome Query"
:database {:id database_id}})
(get-query-name-and-version))
(do ((user->client :rasta) :put 200 (format "query/%d" id) {:name "My Awesome Query 2"
:database {:id database_id}})
(get-query-name-and-version))]))
;; ## DELETE /api/query/:id
;; Check that we can delete a Query
(let [query-name (random-name)
get-query-name (fn [] (sel :one :field [Query :name] :name query-name))]
(expect-eval-actual-first
[query-name
nil]
(let [{id :id} (create-query :name query-name)]
[(get-query-name)
(do ((user->client :rasta) :delete 204 (format "query/%d" id))
(get-query-name))])))
;; ## POST /api/query (clone)
;; Can we clone a Query?
(let [query-name (random-name)]
(expect-eval-actual-first
(match-$ (sel :one Query :name (format "%s CLONED" query-name))
{:database_id (:id @test-db)
:name $
:type "rawsql"
:creator_id (user->id :crowberto)
:updated_at $
:details {:timezone nil
:sql "SELECT COUNT(*) FROM VENUES;"}
:id $
:version 1
:public_perms 0
:created_at $})
;; Clone Query with a different User than the one that created it
(let [{id :id} (create-query :name query-name)]
((user->client :crowberto) :post 200 "query" {:clone id}))))
;; ## POST /api/query/:id & GET /api/query/:id/results
;; Can we execute a Query (i.e., create a new QueryExecution) ?
(expect-eval-actual-first
(let [{query-id :id :as query} (sel :one Query (order :id :DESC))
query-execution (sel :one QueryExecution :query_id query-id (order :id :DESC))]
[(match-$ query-execution
{:id $
:uuid $
:query_id query-id
:version 1
:status "starting"
:started_at $})
[(match-$ query-execution
{:query_id query-id
:raw_query ""
:result_rows 1
:finished_at $
:started_at $
:json_query {:native {:timezone nil
:query "SELECT COUNT(*) FROM VENUES;"}
:database (:id @test-db)
:type "native"}
:status "completed"
:id $
:uuid $
:row_count 1
:running_time $
:version 1})]])
(let [{id :id} (create-query)]
[;; POST /query/:id should create a new QueryExecution
((user->client :rasta) :post 200 (format "query/%d" id))
;; GET /query/:id/results should return array of QueryExecutions for the Query (e.g., the one we just created)
(do
;; wait 100ms for QueryExecution to complete. If it takes longer than that, it's probably brokesies
(Thread/sleep 100)
((user->client :rasta) :get 200 (format "query/%d/results" id)))]))
;; ## GET /api/query
;; Fetch Queries for the current Org
(expect-eval-actual-first
(let [[query-1 query-2] (sel :many Query :database_id (:id @test-db) (order :name :ASC))
rasta (match-$ (fetch-user :rasta)
{:common_name "Rasta Toucan"
:date_joined $
:last_name "Toucan"
:id $
:is_superuser false
:last_login $
:first_name "Rasta"
:email "rasta@metabase.com"})
db (match-$ @test-db
{:created_at $
:engine "h2"
:id $
:details $
:updated_at $
:name "Test Database"
:organization_id @org-id
:description nil})]
[(match-$ query-1
{:creator rasta
:database_id (:id @test-db)
:name $
:type "rawsql"
:creator_id (user->id :rasta)
:updated_at $
:details {:timezone nil
:sql "SELECT COUNT(*) FROM VENUES;"}
:id $
:database db
:version 1
:public_perms 0
:created_at $})
(match-$ query-2
{:creator rasta
:database_id (:id @test-db)
:name $
:type "rawsql"
:creator_id (user->id :rasta)
:updated_at $
:details {:timezone nil
:sql "SELECT COUNT(*) FROM VENUES;"}
:id $
:database db
:version 1
:public_perms 0
:created_at $})])
(do (cascade-delete Query :database_id (:id @test-db))
(create-query)
(create-query)
((user->client :rasta) :get 200 "query" :org @org-id)))
;; ## POST /api/query/:id/csv
(expect-eval-actual-first
"count(*)\n100\n"
(let [{query-id :id} (create-query)]
((user->client :rasta) :post 200 (format "query/%d" query-id))
;; 50ms should be long enough for query to finish
(Thread/sleep 50)
((user->client :rasta) :get 200 (format "query/%d/csv" query-id))))
(ns metabase.api.result-test
"Tests for /api/result endpoints."
(:require [expectations :refer :all]
[korma.core :refer :all]
[metabase.db :refer :all]
(metabase.models [query-execution :refer [QueryExecution]])
[metabase.api.query-test :refer [create-query]]
[metabase.test.util :refer [match-$ random-name expect-eval-actual-first]]
[metabase.test-data :refer :all]))
(defn create-and-execute-query
"Create a `Query`, run it + wait for it to finish, and return the resulting `QueryExecution`."
[]
(let [{query-id :id} (create-query)
query-execution ((user->client :rasta) :post 200 (format "query/%d" query-id))]
(Thread/sleep 200) ; Give it 200ms to finish
query-execution))
;; ## GET /result/:id
;; Check that we can fetch the QueryResult object that gets created when we call POST /api/query/:id
(expect-eval-actual-first
(match-$ (sel :one QueryExecution (order :id :DESC))
{:query_id $
:raw_query ""
:result_rows 1
:finished_at $
:started_at $
:json_query {:native {:timezone nil,
:query "SELECT COUNT(*) FROM VENUES;"}
:database (:id @test-db)
:type "native"}
:status "completed"
:id $
:uuid $
:row_count 1
:running_time $
:version 1})
(let [{execution-id :id} (create-and-execute-query)]
((user->client :rasta) :get 200 (format "result/%d" execution-id))))
;; ## GET /result/:id/response
;; Can we get the results of a QueryExecution?
(expect-eval-actual-first
(match-$ (sel :one QueryExecution (order :id :DESC))
{:id $
:uuid $
:status "completed"
:data {:columns ["count(*)"]
:cols [{:base_type "IntegerField", :name "count(*)"}]
:rows [[100]]}
:row_count 1})
(let [{execution-id :id} (create-and-execute-query)]
((user->client :rasta) :get 200 (format "result/%d/response" execution-id))))
;; ## GET /result/:id/csv
(expect-eval-actual-first
"count(*)\n100\n"
(let [{execution-id :id} (create-and-execute-query)]
((user->client :rasta) :get 200 (format "result/%d/csv" execution-id))))
(ns metabase.driver.result-test
(:require [clojure.data.json :as json]
[expectations :refer :all]
[metabase.db :refer :all]
[metabase.driver.result :as result]
(metabase.models [query :refer [Query]]
[query-execution :refer [QueryExecution]])
[metabase.test-data :refer [db-id user->id]]
[metabase.test.util :refer [match-$ random-name expect-eval-actual-first]]
[metabase.util :as util]))
; insert a Query with a QueryExecution
(let [addtl-info (random-name)]
(expect-eval-actual-first
(match-$ (sel :one QueryExecution :additional_info addtl-info)
{:id $
:uuid $
:status :completed
:row_count 1
:data {:rows [[100]]
:columns ["count"]
:cols [{:base_type "IntegerField"
:special_type "number"
:name "count"
:id nil
:table_id nil
:description nil}]}})
(let [{query-id :id} (ins Query
:type "rawsql"
:name (random-name)
:details {:sql "select 100"}
:creator_id (user->id :rasta)
:public_perms 0
:database_id @db-id)]
;; Add a dummy QueryExecution for our Query so that we have something to pull the cached result from
(ins QueryExecution
:uuid (.toString (java.util.UUID/randomUUID))
:executor_id (user->id :rasta)
:json_query {}
:query_id query-id
:version 1
:status "completed"
:error ""
:started_at (util/new-sql-timestamp)
:finished_at (util/new-sql-timestamp)
:running_time 0
:result_rows 1
:result_file ""
:result_data (json/write-str {:rows [[100]]
:cols [{:base_type "IntegerField"
:special_type "number"
:name "count"
:id nil
:table_id nil
:description nil}]
:columns ["count"]})
:raw_query ""
:additional_info addtl-info)
(result/process-and-run {:type :result
:database @db-id
:result {:query_id query-id}}))))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment