diff --git a/bin/build-driver.sh b/bin/build-driver.sh index 688d93f5eb341f7fd90e9166fdacbbe5ed9d6017..445e1bf832dfe5314d5248e2d780d04e67ad10aa 100755 --- a/bin/build-driver.sh +++ b/bin/build-driver.sh @@ -48,23 +48,26 @@ fi # Calculate a checksum of all the driver source files. If we've already built the driver and the checksum is the same # there's no need to build the driver a second time calculate_checksum() { - find "$driver_project_dir" -name '*.clj' -or -name '*.yaml' | sort | cat | $md5_command + find "$driver_project_dir" -name '*.clj' -or -name '*.yaml' | sort | xargs cat | $md5_command } # Check whether the saved checksum for the driver sources from the last build is the same as the current one. If so, # we don't need to build again. checksum_is_same() { - result="" if [ -f "$checksum_file" ]; then old_checksum=`cat "$checksum_file"` - if [ "$(calculate_checksum)" == "$old_checksum" ]; then + current_checksum=`calculate_checksum` + echo "Checksum of source files for previous build: $old_checksum" + echo "Current checksum of source files: $current_checksum" + if [ "$current_checksum" == "$old_checksum" ]; then # Make sure the target driver JAR actually exists as well! if [ -f "$target_jar" ]; then - result="$driver driver source unchanged since last build. Skipping re-build." + echo "$driver driver source unchanged since last build. Skipping re-build." + return 0 fi fi fi - echo "$result" + return 1 } ######################################## BUILDING THE DRIVER ######################################## @@ -228,9 +231,11 @@ mkdir -p resources/modules if [ $# -eq 2 ]; then $2 # Build driver if checksum has changed -elif [ ! "$(checksum_is_same)" ]; then +elif ! checksum_is_same; then + echo "Checksum has changed." build_driver || retry_clean_build # Either way, always copy the target uberjar to the dest location else + echo "Checksum is unchanged." (copy_target_to_dest && verify_build) || retry_clean_build fi diff --git a/docs/api-documentation.md b/docs/api-documentation.md index 0c459fa33d57b798b5ed6742a6a7ea1ec5c861f2..327e5b1fc63967830904949d1acb5ee7abbc6eba 100644 --- a/docs/api-documentation.md +++ b/docs/api-documentation.md @@ -1,4 +1,4 @@ -# API Documentation for Metabase v0.30.0-snapshot +# API Documentation for Metabase v0.32.2 ## `GET /api/activity/` @@ -16,7 +16,7 @@ Delete an Alert. (DEPRECATED -- don't delete a Alert anymore -- archive it inste ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/alert/` @@ -34,7 +34,7 @@ Fetch all questions for the given question (`Card`) id ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/alert/` @@ -53,7 +53,7 @@ Create a new Alert. * **`alert_above_goal`** value may be nil, or if non-nil, value must be a boolean. -* **`new-alert-request-body`** +* **`new-alert-request-body`** ## `PUT /api/alert/:id` @@ -62,7 +62,7 @@ Update a `Alert` with ID. ##### PARAMS: -* **`id`** +* **`id`** * **`alert_condition`** value may be nil, or if non-nil, value must be one of: `goal`, `rows`. @@ -76,7 +76,7 @@ Update a `Alert` with ID. * **`archived`** value may be nil, or if non-nil, value must be a boolean. -* **`alert-updates`** +* **`alert-updates`** ## `PUT /api/alert/:id/unsubscribe` @@ -85,7 +85,7 @@ Unsubscribes a user from the given alert ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/automagic-dashboards/:entity/:entity-id-or-query` @@ -96,7 +96,7 @@ Return an automagic dashboard for entity `entity` with id `ìd`. * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`show`** invalid show value @@ -111,7 +111,7 @@ Return an automagic dashboard analyzing cell in automagic dashboard for entity * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`cell-query`** value couldn't be parsed as base64 encoded JSON @@ -128,7 +128,7 @@ Return an automagic comparison dashboard for cell in automagic dashboard for ent * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`cell-query`** value couldn't be parsed as base64 encoded JSON @@ -136,7 +136,7 @@ Return an automagic comparison dashboard for cell in automagic dashboard for ent * **`comparison-entity`** Invalid comparison entity type. Can only be one of "table", "segment", or "adhoc" -* **`comparison-entity-id-or-query`** +* **`comparison-entity-id-or-query`** ## `GET /api/automagic-dashboards/:entity/:entity-id-or-query/cell/:cell-query/rule/:prefix/:rule` @@ -148,7 +148,7 @@ Return an automagic dashboard analyzing cell in question with id `id` defined b * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`cell-query`** value couldn't be parsed as base64 encoded JSON @@ -169,7 +169,7 @@ Return an automagic comparison dashboard for cell in automagic dashboard for ent * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`cell-query`** value couldn't be parsed as base64 encoded JSON @@ -181,7 +181,7 @@ Return an automagic comparison dashboard for cell in automagic dashboard for ent * **`comparison-entity`** Invalid comparison entity type. Can only be one of "table", "segment", or "adhoc" -* **`comparison-entity-id-or-query`** +* **`comparison-entity-id-or-query`** ## `GET /api/automagic-dashboards/:entity/:entity-id-or-query/compare/:comparison-entity/:comparison-entity-id-or-query` @@ -193,13 +193,13 @@ Return an automagic comparison dashboard for entity `entity` with id `ìd` compa * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`show`** invalid show value * **`comparison-entity`** Invalid comparison entity type. Can only be one of "table", "segment", or "adhoc" -* **`comparison-entity-id-or-query`** +* **`comparison-entity-id-or-query`** ## `GET /api/automagic-dashboards/:entity/:entity-id-or-query/rule/:prefix/:rule` @@ -210,7 +210,7 @@ Return an automagic dashboard for entity `entity` with id `ìd` using rule `rule * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`prefix`** invalid value for prefix @@ -228,7 +228,7 @@ Return an automagic comparison dashboard for entity `entity` with id `ìd` using * **`entity`** Invalid entity type -* **`entity-id-or-query`** +* **`entity-id-or-query`** * **`prefix`** invalid value for prefix @@ -238,7 +238,7 @@ Return an automagic comparison dashboard for entity `entity` with id `ìd` using * **`comparison-entity`** Invalid comparison entity type. Can only be one of "table", "segment", or "adhoc" -* **`comparison-entity-id-or-query`** +* **`comparison-entity-id-or-query`** ## `GET /api/automagic-dashboards/database/:id/candidates` @@ -247,7 +247,7 @@ Return a list of candidates for automagic dashboards orderd by interestingness. ##### PARAMS: -* **`id`** +* **`id`** ## `DELETE /api/card/:card-id/favorite` @@ -256,7 +256,7 @@ Unfavorite a Card. ##### PARAMS: -* **`card-id`** +* **`card-id`** ## `DELETE /api/card/:card-id/public_link` @@ -267,7 +267,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`card-id`** +* **`card-id`** ## `DELETE /api/card/:id` @@ -276,7 +276,7 @@ Delete a Card. (DEPRECATED -- don't delete a Card anymore -- archive it instead. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/card/` @@ -298,7 +298,7 @@ Get `Card` with ID. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/card/:id/related` @@ -307,7 +307,7 @@ Return related entities. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/card/embeddable` @@ -345,7 +345,7 @@ Create a new `Card`. * **`name`** value must be a non-blank string. -* **`dataset_query`** +* **`dataset_query`** * **`display`** value must be a non-blank string. @@ -356,7 +356,7 @@ Favorite a Card. ##### PARAMS: -* **`card-id`** +* **`card-id`** ## `POST /api/card/:card-id/public_link` @@ -369,7 +369,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`card-id`** +* **`card-id`** ## `POST /api/card/:card-id/query` @@ -378,9 +378,9 @@ Run the query associated with a Card. ##### PARAMS: -* **`card-id`** +* **`card-id`** -* **`parameters`** +* **`parameters`** * **`ignore_cache`** value may be nil, or if non-nil, value must be a boolean. @@ -392,12 +392,16 @@ Run the query associated with a Card, and return its results as a file in the sp ##### PARAMS: -* **`card-id`** +* **`card-id`** * **`export-format`** value must be one of: `csv`, `json`, `xlsx`. * **`parameters`** value may be nil, or if non-nil, value must be a valid JSON string. +* **`respond`** + +* **`raise`** + ## `POST /api/card/collections` @@ -417,7 +421,7 @@ Return related entities for an ad-hoc query. ##### PARAMS: -* **`query`** +* **`query`** ## `PUT /api/card/:id` @@ -442,7 +446,7 @@ Update a `Card`. * **`collection_id`** value may be nil, or if non-nil, value must be an integer greater than zero. -* **`card-updates`** +* **`card-updates`** * **`name`** value may be nil, or if non-nil, value must be a non-blank string. @@ -450,7 +454,7 @@ Update a `Card`. * **`dataset_query`** value may be nil, or if non-nil, value must be a map. -* **`id`** +* **`id`** * **`display`** value may be nil, or if non-nil, value must be a non-blank string. @@ -474,7 +478,7 @@ Fetch a specific Collection with standard details added ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/collection/:id/items` @@ -486,7 +490,7 @@ Fetch a specific Collection's items with the following options: ##### PARAMS: -* **`id`** +* **`id`** * **`model`** value may be nil, or if non-nil, value must be one of: `card`, `collection`, `dashboard`, `pulse`. @@ -546,7 +550,7 @@ Modify an existing Collection, including archiving or unarchiving it, or moving ##### PARAMS: -* **`id`** +* **`id`** * **`name`** value may be nil, or if non-nil, value must be a non-blank string. @@ -558,7 +562,7 @@ Modify an existing Collection, including archiving or unarchiving it, or moving * **`parent_id`** value may be nil, or if non-nil, value must be an integer greater than zero. -* **`collection-updates`** +* **`collection-updates`** ## `PUT /api/collection/graph` @@ -580,25 +584,25 @@ You must be a superuser to do this. ##### PARAMS: -* **`dashboard-id`** +* **`dashboard-id`** ## `DELETE /api/dashboard/:id` -Delete a `Dashboard`. +Delete a Dashboard. ##### PARAMS: -* **`id`** +* **`id`** ## `DELETE /api/dashboard/:id/cards` -Remove a `DashboardCard` from a `Dashboard`. +Remove a DashboardCard from a Dashboard. ##### PARAMS: -* **`id`** +* **`id`** * **`dashcardId`** value must be a valid integer greater than zero. @@ -609,7 +613,7 @@ Unfavorite a Dashboard. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/dashboard/` @@ -627,11 +631,11 @@ Get `Dashboards`. With filter option `f` (default `all`), restrict results as fo ## `GET /api/dashboard/:id` -Get `Dashboard` with ID. +Get Dashboard with `id`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/dashboard/:id/related` @@ -640,16 +644,16 @@ Return related entities. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/dashboard/:id/revisions` -Fetch `Revisions` for `Dashboard` with ID. +Fetch Revisions for Dashboard with `id`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/dashboard/embeddable` @@ -670,7 +674,7 @@ You must be a superuser to do this. ## `POST /api/dashboard/` -Create a new `Dashboard`. +Create a new Dashboard. ##### PARAMS: @@ -684,7 +688,7 @@ Create a new `Dashboard`. * **`collection_position`** value may be nil, or if non-nil, value must be an integer greater than zero. -* **`dashboard`** +* **`dashboard`** ## `POST /api/dashboard/:dashboard-id/public_link` @@ -697,24 +701,43 @@ You must be a superuser to do this. ##### PARAMS: -* **`dashboard-id`** +* **`dashboard-id`** + + +## `POST /api/dashboard/:from-dashboard-id/copy` + +Copy a Dashboard. + +##### PARAMS: + +* **`from-dashboard-id`** + +* **`name`** value may be nil, or if non-nil, value must be a non-blank string. + +* **`description`** value may be nil, or if non-nil, value must be a string. + +* **`collection_id`** value may be nil, or if non-nil, value must be an integer greater than zero. + +* **`collection_position`** value may be nil, or if non-nil, value must be an integer greater than zero. + +* **`dashboard`** ## `POST /api/dashboard/:id/cards` -Add a `Card` to a `Dashboard`. +Add a Card to a Dashboard. ##### PARAMS: -* **`id`** +* **`id`** * **`cardId`** value may be nil, or if non-nil, value must be an integer greater than zero. * **`parameter_mappings`** value must be an array. Each value must be a map. -* **`series`** +* **`series`** -* **`dashboard-card`** +* **`dashboard-card`** ## `POST /api/dashboard/:id/favorite` @@ -723,16 +746,16 @@ Favorite a Dashboard. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/dashboard/:id/revert` -Revert a `Dashboard` to a prior `Revision`. +Revert a Dashboard to a prior Revision. ##### PARAMS: -* **`id`** +* **`id`** * **`revision_id`** value must be an integer greater than zero. @@ -743,7 +766,7 @@ Save a denormalized description of dashboard. ##### PARAMS: -* **`dashboard`** +* **`dashboard`** ## `POST /api/dashboard/save/collection/:parent-collection-id` @@ -752,14 +775,14 @@ Save a denormalized description of dashboard into collection with ID `:parent-co ##### PARAMS: -* **`parent-collection-id`** +* **`parent-collection-id`** -* **`dashboard`** +* **`dashboard`** ## `PUT /api/dashboard/:id` -Update a `Dashboard`. +Update a Dashboard. Usually, you just need write permissions for this Dashboard to do this (which means you have appropriate permissions for the Cards belonging to this Dashboard), but to change the value of `enable_embedding` you must be a @@ -783,7 +806,7 @@ Update a `Dashboard`. * **`collection_id`** value may be nil, or if non-nil, value must be an integer greater than zero. -* **`dash-updates`** +* **`dash-updates`** * **`name`** value may be nil, or if non-nil, value must be a non-blank string. @@ -791,14 +814,14 @@ Update a `Dashboard`. * **`embedding_params`** value may be nil, or if non-nil, value must be a valid embedding params map. -* **`id`** +* **`id`** * **`position`** value may be nil, or if non-nil, value must be an integer greater than zero. ## `PUT /api/dashboard/:id/cards` -Update `Cards` on a `Dashboard`. Request body should have the form: +Update Cards on a Dashboard. Request body should have the form: {:cards [{:id ... :sizeX ... @@ -810,9 +833,9 @@ Update `Cards` on a `Dashboard`. Request body should have the form: ##### PARAMS: -* **`id`** +* **`id`** -* **`cards`** +* **`cards`** ## `DELETE /api/database/:id` @@ -821,7 +844,7 @@ Delete a `Database`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/database/` @@ -843,7 +866,7 @@ Get `Database` with ID. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/database/:id/autocomplete_suggestions` @@ -857,7 +880,7 @@ Return a list of autocomplete suggestions for a given PREFIX. ##### PARAMS: -* **`id`** +* **`id`** * **`prefix`** value must be a non-blank string. @@ -868,7 +891,7 @@ Get a list of all `Fields` in `Database`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/database/:id/idfields` @@ -877,7 +900,7 @@ Get a list of all primary key `Fields` for `Database`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/database/:id/metadata` @@ -887,7 +910,7 @@ Get metadata about a `Database`, including all of its `Tables` and `Fields`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/database/:id/schema/:schema` @@ -896,9 +919,9 @@ Returns a list of tables for the given database `id` and `schema` ##### PARAMS: -* **`id`** +* **`id`** -* **`schema`** +* **`schema`** ## `GET /api/database/:id/schemas` @@ -907,7 +930,7 @@ Returns a list of all the schemas found for the database `id` ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/database/:virtual-db/metadata` @@ -945,7 +968,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/database/:id/rescan_values` @@ -956,7 +979,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/database/:id/sync` @@ -965,7 +988,7 @@ Update the metadata for this `Database`. This happens asynchronously. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/database/:id/sync_schema` @@ -976,7 +999,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/database/sample_dataset` @@ -1019,13 +1042,13 @@ You must be a superuser to do this. * **`caveats`** value may be nil, or if non-nil, value must be a string. -* **`is_full_sync`** +* **`is_full_sync`** * **`details`** value may be nil, or if non-nil, value must be a map. -* **`id`** +* **`id`** -* **`is_on_demand`** +* **`is_on_demand`** ## `POST /api/dataset/` @@ -1036,7 +1059,7 @@ Execute a query and retrieve the results in the usual format. * **`database`** value must be an integer. -* **`query`** +* **`query`** ## `POST /api/dataset/:export-format` @@ -1049,6 +1072,10 @@ Execute a query and download the result data as a file in the specified format. * **`query`** value must be a valid JSON string. +* **`respond`** + +* **`raise`** + ## `POST /api/dataset/duration` @@ -1056,9 +1083,9 @@ Get historical query execution duration. ##### PARAMS: -* **`database`** +* **`database`** -* **`query`** +* **`query`** ## `DELETE /api/email/` @@ -1077,7 +1104,7 @@ You must be a superuser to do this. ## `PUT /api/email/` -Update multiple `Settings` values. You must be a superuser to do this. +Update multiple email Settings. You must be a superuser to do this. You must be a superuser to do this. @@ -1096,7 +1123,7 @@ Fetch a Card via a JSON Web Token signed with the `embedding-secret-key`. ##### PARAMS: -* **`token`** +* **`token`** ## `GET /api/embed/card/:token/field/:field-id/remapping/:remapped-id` @@ -1106,11 +1133,11 @@ Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/: ##### PARAMS: -* **`token`** +* **`token`** -* **`field-id`** +* **`field-id`** -* **`remapped-id`** +* **`remapped-id`** * **`value`** value must be a non-blank string. @@ -1121,11 +1148,11 @@ Search for values of a Field that is referenced by an embedded Card. ##### PARAMS: -* **`token`** +* **`token`** -* **`field-id`** +* **`field-id`** -* **`search-field-id`** +* **`search-field-id`** * **`value`** value must be a non-blank string. @@ -1138,9 +1165,9 @@ Fetch FieldValues for a Field that is referenced by an embedded Card. ##### PARAMS: -* **`token`** +* **`token`** -* **`field-id`** +* **`field-id`** ## `GET /api/embed/card/:token/query` @@ -1154,11 +1181,11 @@ Fetch the results of running a Card using a JSON Web Token signed with the `embe ##### PARAMS: -* **`token`** +* **`token`** -* **`&`** +* **`&`** -* **`query-params`** +* **`query-params`** ## `GET /api/embed/card/:token/query/:export-format` @@ -1167,13 +1194,15 @@ Like `GET /api/embed/card/query`, but returns the results as a file in the speci ##### PARAMS: -* **`token`** +* **`token`** * **`export-format`** value must be one of: `csv`, `json`, `xlsx`. -* **`&`** +* **`query-params`** + +* **`respond`** -* **`query-params`** +* **`raise`** ## `GET /api/embed/dashboard/:token` @@ -1186,44 +1215,47 @@ Fetch a Dashboard via a JSON Web Token signed with the `embedding-secret-key`. ##### PARAMS: -* **`token`** +* **`token`** ## `GET /api/embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id` -Fetch the results of running a Card belonging to a Dashboard using a JSON Web Token signed with the `embedding-secret-key` +Fetch the results of running a Card belonging to a Dashboard using a JSON Web Token signed with the + `embedding-secret-key` ##### PARAMS: -* **`token`** +* **`token`** -* **`dashcard-id`** +* **`dashcard-id`** -* **`card-id`** +* **`card-id`** -* **`&`** +* **`&`** -* **`query-params`** +* **`query-params`** ## `GET /api/embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id/:export-format` -Fetch the results of running a Card belonging to a Dashboard using a JSON Web Token signed with the `embedding-secret-key` - return the data in one of the export formats +Fetch the results of running a Card belonging to a Dashboard using a JSON Web Token signed with the + `embedding-secret-key` return the data in one of the export formats ##### PARAMS: -* **`token`** +* **`token`** * **`export-format`** value must be one of: `csv`, `json`, `xlsx`. -* **`dashcard-id`** +* **`dashcard-id`** -* **`card-id`** +* **`card-id`** -* **`&`** +* **`query-params`** -* **`query-params`** +* **`respond`** + +* **`raise`** ## `GET /api/embed/dashboard/:token/field/:field-id/remapping/:remapped-id` @@ -1233,11 +1265,11 @@ Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/: ##### PARAMS: -* **`token`** +* **`token`** -* **`field-id`** +* **`field-id`** -* **`remapped-id`** +* **`remapped-id`** * **`value`** value must be a non-blank string. @@ -1248,11 +1280,11 @@ Search for values of a Field that is referenced by a Card in an embedded Dashboa ##### PARAMS: -* **`token`** +* **`token`** -* **`field-id`** +* **`field-id`** -* **`search-field-id`** +* **`search-field-id`** * **`value`** value must be a non-blank string. @@ -1265,9 +1297,9 @@ Fetch FieldValues for a Field that is used as a param in an embedded Dashboard. ##### PARAMS: -* **`token`** +* **`token`** -* **`field-id`** +* **`field-id`** ## `DELETE /api/field/:id/dimension` @@ -1276,7 +1308,7 @@ Remove the dimension associated to field at ID ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/field/:id` @@ -1285,7 +1317,7 @@ Get `Field` with ID. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/field/:id/related` @@ -1294,7 +1326,7 @@ Return related entities. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/field/:id/remapping/:remapped-id` @@ -1303,22 +1335,23 @@ Fetch remapped Field values. ##### PARAMS: -* **`id`** +* **`id`** -* **`remapped-id`** +* **`remapped-id`** -* **`value`** +* **`value`** ## `GET /api/field/:id/search/:search-id` -Search for values of a Field that match values of another Field when breaking out by the +Search for values of a Field with `search-id` that start with `value`. See docstring for + `metabase.api.field/search-values` for a more detailed explanation. ##### PARAMS: -* **`id`** +* **`id`** -* **`search-id`** +* **`search-id`** * **`value`** value must be a non-blank string. @@ -1331,7 +1364,7 @@ Get the count and distinct count of `Field` with ID. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/field/:id/values` @@ -1341,7 +1374,7 @@ If a Field's value of `has_field_values` is `list`, return a list of all the dis ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/field/field-literal%2C:field-name%2Ctype%2F:field-type/values` @@ -1351,7 +1384,7 @@ Implementation of the field values endpoint for fields in the Saved Questions 'v ##### PARAMS: -* **`_`** +* **`_`** ## `POST /api/field/:id/dimension` @@ -1360,7 +1393,7 @@ Sets the dimension for the given field at ID ##### PARAMS: -* **`id`** +* **`id`** * **`type`** value must be one of: `external`, `internal`. @@ -1378,7 +1411,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/field/:id/rescan_values` @@ -1390,7 +1423,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/field/:id/values` @@ -1400,7 +1433,7 @@ Update the fields values and human-readable values for a `Field` whose special t ##### PARAMS: -* **`id`** +* **`id`** * **`value-pairs`** value must be an array. Each value must be an array. @@ -1423,11 +1456,13 @@ Update `Field` with ID. * **`has_field_values`** value may be nil, or if non-nil, value must be one of: `auto-list`, `list`, `none`, `search`. +* **`settings`** value may be nil, or if non-nil, value must be a map. + * **`caveats`** value may be nil, or if non-nil, value must be a non-blank string. * **`fk_target_field_id`** value may be nil, or if non-nil, value must be an integer greater than zero. -* **`id`** +* **`id`** ## `GET /api/geojson/:key` @@ -1453,13 +1488,11 @@ You must be a superuser to do this. ## `DELETE /api/metric/:id` -Delete a `Metric`. - -You must be a superuser to do this. +Archive a Metric. (DEPRECATED -- Just pass updated value of `:archived` to the `PUT` endpoint instead.) ##### PARAMS: -* **`id`** +* **`id`** * **`revision_message`** value must be a non-blank string. @@ -1470,18 +1503,16 @@ Fetch *all* `Metrics`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/metric/:id` Fetch `Metric` with ID. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/metric/:id/related` @@ -1490,31 +1521,27 @@ Return related entities. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/metric/:id/revisions` Fetch `Revisions` for `Metric` with ID. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/metric/` Create a new `Metric`. -You must be a superuser to do this. - ##### PARAMS: * **`name`** value must be a non-blank string. -* **`description`** +* **`description`** value may be nil, or if non-nil, value must be a string. * **`table_id`** value must be an integer greater than zero. @@ -1525,11 +1552,9 @@ You must be a superuser to do this. Revert a `Metric` to a prior `Revision`. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`id`** * **`revision_id`** value must be an integer greater than zero. @@ -1538,18 +1563,28 @@ You must be a superuser to do this. Update a `Metric` with ID. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`points_of_interest`** value may be nil, or if non-nil, value must be a string. -* **`definition`** value must be a map. +* **`description`** value may be nil, or if non-nil, value must be a string. -* **`name`** value must be a non-blank string. +* **`archived`** value may be nil, or if non-nil, value must be a boolean. + +* **`definition`** value may be nil, or if non-nil, value must be a map. * **`revision_message`** value must be a non-blank string. +* **`show_in_getting_started`** value may be nil, or if non-nil, value must be a boolean. + +* **`name`** value may be nil, or if non-nil, value must be a non-blank string. + +* **`caveats`** value may be nil, or if non-nil, value must be a string. + +* **`id`** + +* **`how_is_this_calculated`** value may be nil, or if non-nil, value must be a string. + ## `PUT /api/metric/:id/important_fields` @@ -1560,7 +1595,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** * **`important_field_ids`** value must be an array. Each value must be an integer greater than zero. @@ -1572,11 +1607,11 @@ Notification about a potential schema change to one of our `Databases`. ##### PARAMS: -* **`id`** +* **`id`** -* **`table_id`** +* **`table_id`** -* **`table_name`** +* **`table_name`** ## `DELETE /api/permissions/group/:group-id` @@ -1587,7 +1622,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`group-id`** +* **`group-id`** ## `DELETE /api/permissions/membership/:id` @@ -1598,7 +1633,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/permissions/graph` @@ -1623,7 +1658,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/permissions/membership` @@ -1687,7 +1722,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`group-id`** +* **`group-id`** * **`name`** value must be a non-blank string. @@ -1698,7 +1733,7 @@ Fetch a Card you're considering embedding by passing a JWT TOKEN. ##### PARAMS: -* **`token`** +* **`token`** ## `GET /api/preview-embed/card/:token/query` @@ -1707,20 +1742,20 @@ Fetch the query results for a Card you're considering embedding by passing a JWT ##### PARAMS: -* **`token`** +* **`token`** -* **`&`** +* **`&`** -* **`query-params`** +* **`query-params`** ## `GET /api/preview-embed/dashboard/:token` -Fetch a Dashboard you're considering embedding by passing a JWT TOKEN. +Fetch a Dashboard you're considering embedding by passing a JWT TOKEN. ##### PARAMS: -* **`token`** +* **`token`** ## `GET /api/preview-embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id` @@ -1729,15 +1764,15 @@ Fetch the results of running a Card belonging to a Dashboard you're considering ##### PARAMS: -* **`token`** +* **`token`** -* **`dashcard-id`** +* **`dashcard-id`** -* **`card-id`** +* **`card-id`** -* **`&`** +* **`&`** -* **`query-params`** +* **`query-params`** ## `GET /api/public/card/:uuid` @@ -1747,7 +1782,7 @@ Fetch a publicly-accessible Card an return query results as well as `:card` info ##### PARAMS: -* **`uuid`** +* **`uuid`** ## `GET /api/public/card/:uuid/field/:field-id/remapping/:remapped-id` @@ -1757,11 +1792,11 @@ Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/: ##### PARAMS: -* **`uuid`** +* **`uuid`** -* **`field-id`** +* **`field-id`** -* **`remapped-id`** +* **`remapped-id`** * **`value`** value must be a non-blank string. @@ -1772,11 +1807,11 @@ Search for values of a Field that is referenced by a public Card. ##### PARAMS: -* **`uuid`** +* **`uuid`** -* **`field-id`** +* **`field-id`** -* **`search-field-id`** +* **`search-field-id`** * **`value`** value must be a non-blank string. @@ -1789,9 +1824,9 @@ Fetch FieldValues for a Field that is referenced by a public Card. ##### PARAMS: -* **`uuid`** +* **`uuid`** -* **`field-id`** +* **`field-id`** ## `GET /api/public/card/:uuid/query` @@ -1801,7 +1836,7 @@ Fetch a publicly-accessible Card an return query results as well as `:card` info ##### PARAMS: -* **`uuid`** +* **`uuid`** * **`parameters`** value may be nil, or if non-nil, value must be a valid JSON string. @@ -1813,12 +1848,16 @@ Fetch a publicly-accessible Card and return query results in the specified forma ##### PARAMS: -* **`uuid`** +* **`uuid`** * **`export-format`** value must be one of: `csv`, `json`, `xlsx`. * **`parameters`** value may be nil, or if non-nil, value must be a valid JSON string. +* **`respond`** + +* **`raise`** + ## `GET /api/public/dashboard/:uuid` @@ -1826,7 +1865,7 @@ Fetch a publicly-accessible Dashboard. Does not require auth credentials. Public ##### PARAMS: -* **`uuid`** +* **`uuid`** ## `GET /api/public/dashboard/:uuid/card/:card-id` @@ -1836,9 +1875,9 @@ Fetch the results for a Card in a publicly-accessible Dashboard. Does not requir ##### PARAMS: -* **`uuid`** +* **`uuid`** -* **`card-id`** +* **`card-id`** * **`parameters`** value may be nil, or if non-nil, value must be a valid JSON string. @@ -1850,11 +1889,11 @@ Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/: ##### PARAMS: -* **`uuid`** +* **`uuid`** -* **`field-id`** +* **`field-id`** -* **`remapped-id`** +* **`remapped-id`** * **`value`** value must be a non-blank string. @@ -1865,11 +1904,11 @@ Search for values of a Field that is referenced by a Card in a public Dashboard. ##### PARAMS: -* **`uuid`** +* **`uuid`** -* **`field-id`** +* **`field-id`** -* **`search-field-id`** +* **`search-field-id`** * **`value`** value must be a non-blank string. @@ -1882,9 +1921,9 @@ Fetch FieldValues for a Field that is referenced by a Card in a public Dashboard ##### PARAMS: -* **`uuid`** +* **`uuid`** -* **`field-id`** +* **`field-id`** ## `GET /api/public/oembed` @@ -1908,7 +1947,7 @@ Delete a Pulse. (DEPRECATED -- don't delete a Pulse anymore -- archive it instea ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/pulse/` @@ -1926,7 +1965,7 @@ Fetch `Pulse` with ID. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/pulse/form_input` @@ -1940,7 +1979,7 @@ Get HTML rendering of a Card with `id`. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/pulse/preview_card_info/:id` @@ -1949,7 +1988,7 @@ Get JSON object containing HTML rendering of a Card with `id` and other informat ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/pulse/preview_card_png/:id` @@ -1958,7 +1997,7 @@ Get PNG rendering of a Card with `id`. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/pulse/` @@ -2005,7 +2044,7 @@ Update a Pulse with `id`. ##### PARAMS: -* **`id`** +* **`id`** * **`name`** value may be nil, or if non-nil, value must be a non-blank string. @@ -2019,7 +2058,7 @@ Update a Pulse with `id`. * **`archived`** value may be nil, or if non-nil, value must be a boolean. -* **`pulse-updates`** +* **`pulse-updates`** ## `GET /api/revision/` @@ -2059,13 +2098,11 @@ Search Cards, Dashboards, Collections and Pulses for the substring `q`. ## `DELETE /api/segment/:id` -Delete a `Segment`. - -You must be a superuser to do this. +Archive a Segment. (DEPRECATED -- Just pass updated value of `:archived` to the `PUT` endpoint instead.) ##### PARAMS: -* **`id`** +* **`id`** * **`revision_message`** value must be a non-blank string. @@ -2079,11 +2116,9 @@ Fetch *all* `Segments`. Fetch `Segment` with ID. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/segment/:id/related` @@ -2092,31 +2127,27 @@ Return related entities. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/segment/:id/revisions` Fetch `Revisions` for `Segment` with ID. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/segment/` Create a new `Segment`. -You must be a superuser to do this. - ##### PARAMS: * **`name`** value must be a non-blank string. -* **`description`** +* **`description`** value may be nil, or if non-nil, value must be a string. * **`table_id`** value must be an integer greater than zero. @@ -2127,11 +2158,9 @@ You must be a superuser to do this. Revert a `Segement` to a prior `Revision`. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`id`** * **`revision_id`** value must be an integer greater than zero. @@ -2140,18 +2169,26 @@ You must be a superuser to do this. Update a `Segment` with ID. -You must be a superuser to do this. - ##### PARAMS: -* **`id`** +* **`points_of_interest`** value may be nil, or if non-nil, value must be a string. -* **`name`** value must be a non-blank string. +* **`description`** value may be nil, or if non-nil, value must be a string. -* **`definition`** value must be a map. +* **`archived`** value may be nil, or if non-nil, value must be a boolean. + +* **`definition`** value may be nil, or if non-nil, value must be a map. * **`revision_message`** value must be a non-blank string. +* **`show_in_getting_started`** value may be nil, or if non-nil, value must be a boolean. + +* **`name`** value may be nil, or if non-nil, value must be a non-blank string. + +* **`caveats`** value may be nil, or if non-nil, value must be a string. + +* **`id`** + ## `DELETE /api/session/` @@ -2159,7 +2196,7 @@ Logout. ##### PARAMS: -* **`session_id`** value must be a non-blank string. +* **`metabase-session-id`** ## `GET /api/session/password_reset_token_valid` @@ -2186,7 +2223,9 @@ Login. * **`password`** value must be a non-blank string. -* **`remote-address`** +* **`remote-address`** + +* **`request`** ## `POST /api/session/forgot_password` @@ -2195,11 +2234,11 @@ Send a reset email when user has forgotten their password. ##### PARAMS: -* **`server-name`** +* **`server-name`** * **`email`** value must be a valid email address. -* **`remote-address`** +* **`remote-address`** ## `POST /api/session/google_auth` @@ -2210,7 +2249,9 @@ Login with Google Auth. * **`token`** value must be a non-blank string. -* **`remote-address`** +* **`remote-address`** + +* **`request`** ## `POST /api/session/reset_password` @@ -2223,6 +2264,8 @@ Reset password with a reset token. * **`password`** Insufficient password strength +* **`request`** + ## `GET /api/setting/` @@ -2250,7 +2293,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`settings`** +* **`settings`** ## `PUT /api/setting/:key` @@ -2264,7 +2307,7 @@ You must be a superuser to do this. * **`key`** value must be a non-blank string. -* **`value`** +* **`value`** ## `GET /api/setup/admin_checklist` @@ -2281,7 +2324,7 @@ Special endpoint for creating the first user during setup. ##### PARAMS: -* **`engine`** +* **`engine`** * **`schedules`** value may be nil, or if non-nil, value must be a valid map of schedule maps for a DB. @@ -2291,19 +2334,21 @@ Special endpoint for creating the first user during setup. * **`first_name`** value must be a non-blank string. +* **`request`** + * **`password`** Insufficient password strength -* **`name`** +* **`name`** -* **`is_full_sync`** +* **`is_full_sync`** * **`site_name`** value must be a non-blank string. * **`token`** Token does not match the setup token. -* **`details`** +* **`details`** -* **`is_on_demand`** +* **`is_on_demand`** * **`last_name`** value must be a non-blank string. @@ -2316,7 +2361,7 @@ Validate that we can connect to a database given a set of details. * **`engine`** value must be a valid database engine. -* **`details`** +* **`details`** * **`token`** Token does not match the setup token. @@ -2333,7 +2378,7 @@ You must be a superuser to do this. * **`metabot-enabled`** value must be a boolean. -* **`slack-settings`** +* **`slack-settings`** ## `GET /api/table/` @@ -2347,7 +2392,7 @@ Get `Table` with ID. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/table/:id/fks` @@ -2356,7 +2401,7 @@ Get all foreign keys whose destination is a `Field` that belongs to this `Table` ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/table/:id/query_metadata` @@ -2369,7 +2414,7 @@ Get metadata about a `Table` useful for running queries. ##### PARAMS: -* **`id`** +* **`id`** * **`include_sensitive_fields`** value may be nil, or if non-nil, value must be a valid boolean string ('true' or 'false'). @@ -2380,7 +2425,7 @@ Return related entities. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/table/card__:id/fks` @@ -2395,7 +2440,7 @@ Return metadata for the 'virtual' table for a Card. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/table/:id/discard_values` @@ -2407,7 +2452,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `POST /api/table/:id/rescan_values` @@ -2419,7 +2464,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `PUT /api/table/:id` @@ -2428,7 +2473,7 @@ Update `Table` with ID. ##### PARAMS: -* **`id`** +* **`id`** * **`display_name`** value may be nil, or if non-nil, value must be a non-blank string. @@ -2445,6 +2490,35 @@ Update `Table` with ID. * **`show_in_getting_started`** value may be nil, or if non-nil, value must be a boolean. +## `GET /api/task/` + +Fetch a list of recent tasks stored as Task History + +You must be a superuser to do this. + +##### PARAMS: + +* **`limit`** value may be nil, or if non-nil, value must be a valid integer greater than zero. + +* **`offset`** value may be nil, or if non-nil, value must be a valid integer greater than or equal to zero. + + +## `GET /api/task/:id` + +Get `TaskHistory` entry with ID. + +##### PARAMS: + +* **`id`** + + +## `GET /api/task/info` + +Return raw data about all scheduled tasks (i.e., Quartz Jobs and Triggers). + +You must be a superuser to do this. + + ## `GET /api/tiles/:zoom/:x/:y/:lat-field-id/:lon-field-id/:lat-col-idx/:lon-col-idx/` This endpoints provides an image with the appropriate pins rendered given a MBQL QUERY (passed as a GET query @@ -2479,7 +2553,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/user/` @@ -2499,7 +2573,7 @@ Fetch a `User`. You must be fetching yourself *or* be a superuser. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/user/current` @@ -2521,7 +2595,9 @@ You must be a superuser to do this. * **`email`** value must be a valid email address. -* **`password`** +* **`password`** + +* **`group_ids`** value may be nil, or if non-nil, value must be an array. Each value must be an integer greater than zero. * **`login_attributes`** value may be nil, or if non-nil, value must be a map with each value either a string or number. @@ -2534,7 +2610,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `PUT /api/user/:id` @@ -2543,7 +2619,7 @@ Update an existing, active `User`. ##### PARAMS: -* **`id`** +* **`id`** * **`email`** value may be nil, or if non-nil, value must be a valid email address. @@ -2551,7 +2627,9 @@ Update an existing, active `User`. * **`last_name`** value may be nil, or if non-nil, value must be a non-blank string. -* **`is_superuser`** +* **`group_ids`** value may be nil, or if non-nil, value must be an array. Each value must be an integer greater than zero. + +* **`is_superuser`** value may be nil, or if non-nil, value must be a boolean. * **`login_attributes`** value may be nil, or if non-nil, value must be a map with each value either a string or number. @@ -2562,11 +2640,11 @@ Update a user's password. ##### PARAMS: -* **`id`** +* **`id`** * **`password`** Insufficient password strength -* **`old_password`** +* **`old_password`** ## `PUT /api/user/:id/qbnewb` @@ -2575,7 +2653,7 @@ Indicate that a user has been informed about the vast intricacies of 'the' Query ##### PARAMS: -* **`id`** +* **`id`** ## `PUT /api/user/:id/reactivate` @@ -2586,7 +2664,7 @@ You must be a superuser to do this. ##### PARAMS: -* **`id`** +* **`id`** ## `GET /api/util/logs` @@ -2598,7 +2676,7 @@ You must be a superuser to do this. ## `GET /api/util/random_token` -Return a cryptographically secure random 32-byte token, encoded as a hexidecimal string. +Return a cryptographically secure random 32-byte token, encoded as a hexadecimal string. Intended for use when creating a value for `embedding-secret-key`. @@ -2616,4 +2694,4 @@ Endpoint that checks if the supplied password meets the currently configured pas ##### PARAMS: -* **`password`** Insufficient password strength +* **`password`** Insufficient password strength \ No newline at end of file diff --git a/modules/drivers/snowflake/project.clj b/modules/drivers/snowflake/project.clj index 4dfb565206bd2439b89e50eea9d8679e5adeac5d..47a23e171bbd0c3a87cdfec8e19b21d839f81b32 100644 --- a/modules/drivers/snowflake/project.clj +++ b/modules/drivers/snowflake/project.clj @@ -1,8 +1,8 @@ -(defproject metabase/snowflake-driver "1.0.0-SNAPSHOT-3.6.20" +(defproject metabase/snowflake-driver "1.0.0-SNAPSHOT-3.6.27" :min-lein-version "2.5.0" :dependencies - [[net.snowflake/snowflake-jdbc "3.6.21"]] + [[net.snowflake/snowflake-jdbc "3.6.27"]] :profiles {:provided diff --git a/modules/drivers/snowflake/resources/metabase-plugin.yaml b/modules/drivers/snowflake/resources/metabase-plugin.yaml index bbfdd6e483de07569b76e0101f860bdd144104db..2d9b314b873a482445cd7517f0c041d72d1a68d1 100644 --- a/modules/drivers/snowflake/resources/metabase-plugin.yaml +++ b/modules/drivers/snowflake/resources/metabase-plugin.yaml @@ -1,6 +1,6 @@ info: name: Metabase Snowflake Driver - version: 1.0.0-SNAPSHOT-3.6.20 + version: 1.0.0-SNAPSHOT-3.6.27 description: Allows Metabase to connect to Snowflake databases. driver: name: snowflake @@ -33,6 +33,7 @@ driver: - name: role display-name: Role placeholder: my_role + - additional-options connection-properties-include-tunnel-config: true init: - step: load-namespace diff --git a/modules/drivers/snowflake/src/metabase/driver/snowflake.clj b/modules/drivers/snowflake/src/metabase/driver/snowflake.clj index 6917bb01b601f8aa838ceab4316c219c69a6f363..8133fce538f2e8542dbf3041d3f396550b05f75f 100644 --- a/modules/drivers/snowflake/src/metabase/driver/snowflake.clj +++ b/modules/drivers/snowflake/src/metabase/driver/snowflake.clj @@ -13,6 +13,7 @@ [common :as driver.common] [sql-jdbc :as sql-jdbc]] [metabase.driver.sql-jdbc + [common :as sql-jdbc.common] [connection :as sql-jdbc.conn] [execute :as sql-jdbc.execute] [sync :as sql-jdbc.sync]] @@ -39,22 +40,26 @@ account)] ;; it appears to be the case that their JDBC driver ignores `db` -- see my bug report at ;; https://support.snowflake.net/s/question/0D50Z00008WTOMCSA5/ - (merge {:classname "net.snowflake.client.jdbc.SnowflakeDriver" - :subprotocol "snowflake" - :subname (str "//" host ".snowflakecomputing.com/") - :client_metadata_request_use_connection_ctx true - :ssl true - ;; other SESSION parameters - ;; use the same week start we use for all the other drivers - :week_start 7 - ;; not 100% sure why we need to do this but if we don't set the connection to UTC our report timezone - ;; stuff doesn't work, even though we ultimately override this when we set the session timezone - :timezone "UTC"} - (-> opts - ;; original version of the Snowflake driver incorrectly used `dbname` in the details fields instead of - ;; `db`. If we run across `dbname`, correct our behavior - (set/rename-keys {:dbname :db}) - (dissoc :host :port :timezone))))) + (-> (merge {:classname "net.snowflake.client.jdbc.SnowflakeDriver" + :subprotocol "snowflake" + :subname (str "//" host ".snowflakecomputing.com/") + :client_metadata_request_use_connection_ctx true + :ssl true + ;; keep open connections open indefinitely instead of closing them. See #9674 and + ;; https://docs.snowflake.net/manuals/sql-reference/parameters.html#client-session-keep-alive + :client_session_keep_alive true + ;; other SESSION parameters + ;; use the same week start we use for all the other drivers + :week_start 7 + ;; not 100% sure why we need to do this but if we don't set the connection to UTC our report timezone + ;; stuff doesn't work, even though we ultimately override this when we set the session timezone + :timezone "UTC"} + (-> opts + ;; original version of the Snowflake driver incorrectly used `dbname` in the details fields instead of + ;; `db`. If we run across `dbname`, correct our behavior + (set/rename-keys {:dbname :db}) + (dissoc :host :port :timezone))) + (sql-jdbc.common/handle-additional-options opts)))) (defmethod sql-jdbc.sync/database-type->base-type :snowflake [_ base-type] ({:NUMBER :type/Number diff --git a/resources/log4j.properties b/resources/log4j.properties index 2ba0837e28f20c90d51b51f835bac05bc2b04fdf..ca4192baa2fe05391a98311d8c2e128a32fa04c8 100644 --- a/resources/log4j.properties +++ b/resources/log4j.properties @@ -15,6 +15,7 @@ log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d [%t] %-5p%c - %m%n # customizations to logging by package + log4j.logger.metabase.driver=INFO log4j.logger.metabase.plugins=DEBUG log4j.logger.metabase.middleware=DEBUG @@ -23,12 +24,15 @@ log4j.logger.metabase.query-processor.permissions=INFO log4j.logger.metabase.query-processor=INFO log4j.logger.metabase.sync=DEBUG log4j.logger.metabase.models.field-values=INFO -# NOCOMMIT + +# TODO - we can dial these back a bit once we are satisfied the async stuff isn't so new (0.33.0+) +log4j.logger.metabase.async.api-response=DEBUG log4j.logger.metabase.async.semaphore-channel=DEBUG log4j.logger.metabase.async.util=DEBUG log4j.logger.metabase.middleware.async=DEBUG log4j.logger.metabase.query-processor.async=DEBUG -log4j.logger.metabase.async.api-response=DEBUG + log4j.logger.metabase=INFO + # c3p0 connection pools tend to log useless warnings way too often; only log actual errors log4j.logger.com.mchange=ERROR diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj index bb6f2b99d1c240289819e12cbcb54a9106ba266a..ff60886538d06e2ba3dafd33dd95eb66040ed8be 100644 --- a/src/metabase/api/dashboard.clj +++ b/src/metabase/api/dashboard.clj @@ -217,14 +217,14 @@ :collection_id collection_id :collection_position collection_position} dashboard (db/transaction - ;; Adding a new dashboard at `collection_position` could cause other dashboards in this collection to change - ;; position, check that and fix up if needed - (api/maybe-reconcile-collection-position! dashboard-data) - ;; Ok, now save the Dashboard - (u/prog1 (db/insert! Dashboard dashboard-data) - ;; Get cards from existing dashboard and associate to copied dashboard - (doseq [card (:ordered_cards existing-dashboard)] - (api/check-500 (dashboard/add-dashcard! <> (:card_id card) card)))))] + ;; Adding a new dashboard at `collection_position` could cause other dashboards in this + ;; collection to change position, check that and fix up if needed + (api/maybe-reconcile-collection-position! dashboard-data) + ;; Ok, now save the Dashboard + (u/prog1 (db/insert! Dashboard dashboard-data) + ;; Get cards from existing dashboard and associate to copied dashboard + (doseq [card (:ordered_cards existing-dashboard)] + (api/check-500 (dashboard/add-dashcard! <> (:card_id card) card)))))] (events/publish-event! :dashboard-create dashboard))) @@ -233,13 +233,7 @@ (api/defendpoint GET "/:id" "Get `Dashboard` with ID." [id] - (u/prog1 (-> (Dashboard id) - api/check-404 - (hydrate [:ordered_cards :card :series] :can_write) - api/read-check - api/check-not-archived - hide-unreadable-cards - add-query-average-durations) + (u/prog1 (get-dashboard id) (events/publish-event! :dashboard-read (assoc <> :actor_id api/*current-user-id*)))) diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj index 28024af1e50772a08a9c0a8599544788bab5092c..4bfa5f7e343e268bce132facc48187926ba86fe0 100644 --- a/src/metabase/api/dataset.clj +++ b/src/metabase/api/dataset.clj @@ -47,7 +47,7 @@ (let [source-card-id (query->source-card-id query) options {:executed-by api/*current-user-id*, :context :ad-hoc, :card-id source-card-id, :nested? (boolean source-card-id)}] - (qp.async/process-query-and-save-with-max! query options))) + (qp.async/process-query-and-save-with-max-results-constraints! query options))) ;;; ----------------------------------- Downloading Query Results in Other Formats ----------------------------------- diff --git a/src/metabase/async/api_response.clj b/src/metabase/async/api_response.clj index 0f6b1340ef375554eda7e23bff8311a15c777579..881d192a9ff3f1b743e203247dd59821a2b34db4 100644 --- a/src/metabase/async/api_response.clj +++ b/src/metabase/async/api_response.clj @@ -1,4 +1,11 @@ (ns metabase.async.api-response + "Handle ring response maps that contain a core.async chan in the :body key: + + {:status 200 + :body (a/chan)} + + and send strings (presumibly \n) as heartbeats to the client until the real results (a seq) is received, then stream + that to the client." (:require [cheshire.core :as json] [clojure.core.async :as a] [clojure.java.io :as io] @@ -19,6 +26,7 @@ (def ^:private keepalive-interval-ms "Interval between sending newline characters to keep Heroku from terminating requests like queries that take a long time to complete." + ;; 1 second (* 1 1000)) (def ^:private absolute-max-keepalive-ms @@ -29,18 +37,16 @@ ;; 4 hours (* 4 60 60 1000)) -;; Handle ring response maps that contain a core.async chan in the :body key: -;; -;; {:status 200 -;; :body (a/chan)} -;; -;; and send strings (presumibly \n) as heartbeats to the client until the real results (a seq) is received, then -;; stream that to the client -(defn- write-keepalive-character [^Writer out] + +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | Writing Results of Async Keep-alive Channel | +;;; +----------------------------------------------------------------------------------------------------------------+ + +(defn- write-keepalive-character! [^Writer out] (try - ;; a newline padding character as it's harmless and will allow us to check if the client - ;; is connected. If sending this character fails because the connection is closed, the - ;; chan will then close. Newlines are no-ops when reading JSON which this depends upon. + ;; a newline padding character as it's harmless and will allow us to check if the client is connected. If sending + ;; this character fails because the connection is closed, the chan will then close. Newlines are no-ops when + ;; reading JSON which this depends upon. (.write out (str \newline)) (.flush out) true @@ -52,7 +58,7 @@ false))) ;; `chunkk` named as such to avoid conflict with `clojure.core/chunk` -(defn- write-response-chunk [chunkk, ^Writer out] +(defn- write-response-chunk! [chunkk, ^Writer out] (cond ;; An error has occurred, let the user know (instance? Throwable chunkk) @@ -65,12 +71,16 @@ :else (log/error (trs "Unexpected output in async API response") (class chunkk)))) -(defn- write-channel-to-output-stream [chan, ^Writer out] +(defn- write-chan-vals-to-writer! + "Write whatever val(s) come into `chan` onto the Writer wrapping our OutputStream. Vals should be either + `::keepalive`, meaning we should write a keepalive newline character to the Writer, or some other value, which is + the actual response we've been waiting for (at this point we can close both the Writer and the channel)." + [chan, ^Writer out] (a/go-loop [chunkk (a/<! chan)] (cond - (= chunkk ::keepalive) ;; keepalive chunkk - (if (write-keepalive-character out) + (= chunkk ::keepalive) + (if (write-keepalive-character! out) (recur (a/<! chan)) (do (a/close! chan) @@ -86,7 +96,7 @@ (future (try ;; chunkk *might* be `nil` if the channel already go closed. - (write-response-chunk chunkk out) + (write-response-chunk! chunkk out) (finally ;; should already be closed, but just to be safe (a/close! chan) @@ -94,15 +104,12 @@ (.close out)))))) nil) - -(extend-protocol ring.protocols/StreamableResponseBody - ManyToManyChannel - (write-body-to-stream [chan _ ^OutputStream output-stream] - (log/debug (u/format-color 'green (trs "starting streaming response"))) - (write-channel-to-output-stream chan (io/writer output-stream)))) +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | Async Keep-alive Channel | +;;; +----------------------------------------------------------------------------------------------------------------+ -(defn- start-async-keepalive-loop +(defn- start-async-keepalive-loop! "Starts a go-loop that will send `::keepalive` messages to `output-chan` every second until `input-chan` either produces a response or one of the two channels is closed. If `output-chan` is closed (because there's no longer anywhere to write to -- the connection was canceled), closes `input-chan`; this can and is used by producers such as @@ -155,19 +162,32 @@ (a/close! output-chan) (a/close! input-chan)))))))) -(defn- async-keepalive-chan [input-chan] +(defn- async-keepalive-channel + "Given a core.async channel `input-chan` which will (presumably) eventually receive an asynchronous result, return a + new channel 'wrapping' the original that will write keepalive bytes until the actual result is obtained." + [input-chan] ;; Output chan only needs to hold on to the last message it got, for example no point in writing multiple `\n` ;; characters if the consumer didn't get a chance to consume them, and no point writing `\n` before writing the ;; actual response - (let [output-chan (a/chan (a/sliding-buffer 1))] - (start-async-keepalive-loop input-chan output-chan) - output-chan)) + (u/prog1 (a/chan (a/sliding-buffer 1)) + (start-async-keepalive-loop! input-chan <>))) + + +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | Telling Ring & Compojure how to handle core.async channel API responses | +;;; +----------------------------------------------------------------------------------------------------------------+ + +;; Synchronous Compojure endpoint (e.g. `defendpoint`) responses go directly to here. Async endpoint +;; (`defendpoint-async`) responses go to Sendable and then to here. So technically this affects both sync & async. -(defn- async-keepalive-response [input-chan] - (assoc (response/response (async-keepalive-chan input-chan)) - :content-type "applicaton/json; charset=utf-8")) +(extend-protocol ring.protocols/StreamableResponseBody + ManyToManyChannel + (write-body-to-stream [chan _ ^OutputStream output-stream] + (log/debug (u/format-color 'green (trs "starting streaming response"))) + (write-chan-vals-to-writer! (async-keepalive-channel chan) (io/writer output-stream)))) (extend-protocol Sendable ManyToManyChannel (send* [input-chan _ respond _] - (respond (async-keepalive-response input-chan)))) + (respond (assoc (response/response input-chan) + :content-type "applicaton/json; charset=utf-8")))) diff --git a/src/metabase/mbql/schema.clj b/src/metabase/mbql/schema.clj index bdb8c9e4f37254859052b53f1b34f9a2008a317f..c7284b06701f09c44b345357c76f8e8a4672f260 100644 --- a/src/metabase/mbql/schema.clj +++ b/src/metabase/mbql/schema.clj @@ -658,7 +658,7 @@ :question :xlsx-download)) -;; TODO - this schema is somewhat misleading because if you use a function like `qp/process-query-and-save-with-max!` +;; TODO - this schema is somewhat misleading because if you use a function like `qp/process-query-and-save-with-max-results-constraints!` ;; some of these keys (e.g. `:context`) are in fact required (def Info "Schema for query `:info` dictionary, which is used for informational purposes to record information about how a query diff --git a/src/metabase/middleware/json.clj b/src/metabase/middleware/json.clj index 34b067d88eb5ad76db9e8af85900a5440e732289..e032187c790bcaf6119bce6497005bd36bb5a661 100644 --- a/src/metabase/middleware/json.clj +++ b/src/metabase/middleware/json.clj @@ -62,29 +62,6 @@ (respond ring.json/default-malformed-response)) (handler request respond raise)))) -#_(defn check-application-type-headers - "We don't support API requests with any type of content encoding other than JSON so let's be nice and make that - explicit. Added benefit is that it reduces CSRF surface because POSTing a form with JSON content encoding isn't so - easy to do." - [handler] - (fn - [{:keys [request-method body], {:strs [content-type]} :headers, :as request} respond raise] - ;; GET or DELETE requests with no body we can go ahead and proceed without Content-Type headers, since they - ;; generally don't have bodies. - ;; - ;; POST/PUT requests always require Content-Type: application/json. GET/DELETE requests that specify any other - ;; content type aren't allowed. - (if (or (and (#{:get :delete} request-method) - (nil? content-type)) - (#'ring.json/json-request? request)) - (handler request respond raise) - (respond - {:status 400 - :headers {"Content-Type" "text/plain"} - :body (str (tru "Metabase only supports JSON requests.") - " " - (tru "Make sure you set a 'Content-Type: application/json' header."))})))) - ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Streaming JSON Responses | diff --git a/src/metabase/middleware/session.clj b/src/metabase/middleware/session.clj index 9ccb9fc5bde409e83bac86a33368400a76e84688..640f41499a6812a98a6864286c6a3563e0558ae8 100644 --- a/src/metabase/middleware/session.clj +++ b/src/metabase/middleware/session.clj @@ -83,7 +83,6 @@ (merge {:same-site :lax :http-only true - :path "/api" :max-age (config/config-int :max-session-age)} ;; If the authentication request request was made over HTTPS (hopefully always except for local dev instances) ;; add `Secure` attribute so the cookie is only sent over HTTPS. diff --git a/src/metabase/models/task_history.clj b/src/metabase/models/task_history.clj index 7fe870220b19271775b2473490e7817db4f723ce..0e058eccb492207df056c5ce257054c2f3de488a 100644 --- a/src/metabase/models/task_history.clj +++ b/src/metabase/models/task_history.clj @@ -1,8 +1,10 @@ (ns metabase.models.task-history - (:require [metabase.models.interface :as i] + (:require [clojure.tools.logging :as log] + [metabase.models.interface :as i] [metabase.util :as u] [metabase.util [date :as du] + [i18n :refer [trs]] [schema :as su]] [schema.core :as s] [toucan @@ -20,9 +22,9 @@ ;; the date that task finished, it deletes everything after that. As we continue to add TaskHistory entries, this ;; ensures we'll have a good amount of history for debugging/troubleshooting, but not grow too large and fill the ;; disk. - (when-let [clean-before-date (db/select-one-field :ended_at TaskHistory {:limit 1 - :offset num-rows-to-keep - :order-by [[:ended_at :desc]]})] + (when-let [clean-before-date (db/select-one-field :ended_at TaskHistory {:limit 1 + :offset num-rows-to-keep + :order-by [[:ended_at :desc]]})] (db/simple-delete! TaskHistory :ended_at [:<= clean-before-date]))) (u/strict-extend (class TaskHistory) @@ -36,7 +38,7 @@ (s/defn all "Return all TaskHistory entries, applying `limit` and `offset` if not nil" - [limit :- (s/maybe su/IntGreaterThanZero) + [limit :- (s/maybe su/IntGreaterThanZero) offset :- (s/maybe su/IntGreaterThanOrEqualToZero)] (db/select TaskHistory (merge {:order-by [[:ended_at :desc]]} (when limit @@ -58,11 +60,14 @@ (defn- save-task-history! [start-time-ms info] (let [end-time-ms (System/currentTimeMillis) duration-ms (- end-time-ms start-time-ms)] - (db/insert! TaskHistory - (assoc info - :started_at (du/->Timestamp start-time-ms) - :ended_at (du/->Timestamp end-time-ms) - :duration duration-ms)))) + (try + (db/insert! TaskHistory + (assoc info + :started_at (du/->Timestamp start-time-ms) + :ended_at (du/->Timestamp end-time-ms) + :duration duration-ms)) + (catch Throwable e + (log/warn e (trs "Error saving task history")))))) (s/defn do-with-task-history "Impl for `with-task-history` macro; see documentation below." @@ -85,6 +90,7 @@ "Execute `body`, recording a TaskHistory entry when the task completes; if it failed to complete, records an entry containing information about the Exception. `info` should contain at least a name for the task (conventionally lisp-cased) as `:task`; see the `TaskHistoryInfo` schema in this namespace for other optional keys. + (with-task-history {:task \"send-pulses\"} ...)" {:style/indent 1} diff --git a/src/metabase/plugins.clj b/src/metabase/plugins.clj index c1d0cd099511921cd15f04d45c731d99bf1f03ad..6ad8c4e30e2f864b3d423efcebeb748eedbcee78 100644 --- a/src/metabase/plugins.clj +++ b/src/metabase/plugins.clj @@ -54,7 +54,7 @@ (when (io/resource "modules") (let [plugins-path (plugins-dir)] (files/with-open-path-to-resource [modules-path "modules"] - (files/copy-files-if-not-exists! modules-path plugins-path))))) + (files/copy-files! modules-path plugins-path))))) ;;; +----------------------------------------------------------------------------------------------------------------+ diff --git a/src/metabase/plugins/files.clj b/src/metabase/plugins/files.clj index 20e070519c1e163d17b188231eac47bf789ebc28..dace8e0a9e688c63dcd186946ed1054933aeba7c 100644 --- a/src/metabase/plugins/files.clj +++ b/src/metabase/plugins/files.clj @@ -7,14 +7,15 @@ *file-manipulation* functions for the sorts of operations the plugin system needs to perform." (:require [clojure.java.io :as io] [clojure.string :as str] + [clojure.tools.logging :as log] [metabase.util :as u] [metabase.util [date :as du] [i18n :refer [trs]]]) (:import java.io.FileNotFoundException java.net.URL - [java.nio.file CopyOption Files FileSystem FileSystems LinkOption OpenOption Path] - java.nio.file.attribute.FileAttribute + [java.nio.file CopyOption Files FileSystem FileSystems LinkOption OpenOption Path StandardCopyOption] + [java.nio.file.attribute FileAttribute FileTime] java.util.Collections)) ;;; --------------------------------------------------- Path Utils --------------------------------------------------- @@ -69,20 +70,25 @@ ;;; ------------------------------------------------- Copying Stuff -------------------------------------------------- -(defn- copy! [^Path source, ^Path dest] - (du/profile (trs "Extract file {0} -> {1}" source dest) - (Files/copy source dest (u/varargs CopyOption)))) +(defn- last-modified-time ^FileTime [^Path path] + (Files/getLastModifiedTime path (u/varargs LinkOption))) -(defn- copy-if-not-exists! [^Path source, ^Path dest] - (when-not (exists? dest) - (copy! source dest))) +(defn- copy-file! [^Path source, ^Path dest] + (when (or (not (exists? dest)) + (pos? (.compareTo (last-modified-time source) (last-modified-time dest)))) + (du/profile (trs "Extract file {0} -> {1}" source dest) + (Files/copy source dest (u/varargs CopyOption [StandardCopyOption/REPLACE_EXISTING]))))) -(defn copy-files-if-not-exists! - "Copy all files in `source-dir` to `dest-dir`; skip files if a file of the same name already exists in `dest-dir`." +(defn copy-files! + "Copy all files in `source-dir` to `dest-dir`. Overwrites existing files if last modified date is older than that of + the source file." [^Path source-dir, ^Path dest-dir] (doseq [^Path source (files-seq source-dir) :let [target (append-to-path dest-dir (str (.getFileName source)))]] - (copy-if-not-exists! source target))) + (try + (copy-file! source target) + (catch Throwable e + (log/error e (trs "Failed to copy file")))))) ;;; ------------------------------------------ Opening filesystems for URLs ------------------------------------------ diff --git a/src/metabase/pulse.clj b/src/metabase/pulse.clj index 03972a4f517a6ba904437da73bc406e9a19b7114..1e4331c3ff70d8472ff76783cd3b0c5495981829 100644 --- a/src/metabase/pulse.clj +++ b/src/metabase/pulse.clj @@ -33,10 +33,11 @@ (when-let [card (Card :id card-id, :archived false)] (let [{:keys [creator_id dataset_query]} card] {:card card - :result (qp/process-query-and-save-with-max! dataset_query (merge {:executed-by creator_id, - :context :pulse, - :card-id card-id} - options))})) + :result (qp/process-query-and-save-with-max-results-constraints! dataset_query + (merge {:executed-by creator_id, + :context :pulse, + :card-id card-id} + options))})) (catch Throwable t (log/warn t (trs "Error running query for Card {0}" card-id))))) diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj index fd64e52000b294e59ee98689760fe28332641ace..4794a3fbead20f13003a7fa24845a68537be8ba6 100644 --- a/src/metabase/query_processor.clj +++ b/src/metabase/query_processor.clj @@ -395,7 +395,7 @@ {:max-results-bare-rows max-results}) m)) -(s/defn process-query-and-save-with-max! +(s/defn process-query-and-save-with-max-results-constraints! "Same as `process-query-and-save-execution!` but will include the default max rows returned as a constraint. (This function is ulitmately what powers most API endpoints that run queries, including `POST /api/dataset`.)" {:style/indent 1} diff --git a/src/metabase/query_processor/async.clj b/src/metabase/query_processor/async.clj index b829c6c3d7c29eb6d145b7d78139daab8c0dcb27..48ae943491025b9d24beec4979e573231807c856 100644 --- a/src/metabase/query_processor/async.clj +++ b/src/metabase/query_processor/async.clj @@ -70,12 +70,12 @@ [query options] (do-async (:database query) qp/process-query-and-save-execution! query options)) -(defn process-query-and-save-with-max! - "Async version of `metabase.query-processor/process-query-and-save-with-max!`. Runs query asynchronously, and returns - a `core.async` channel that can be used to fetch the results once the query finishes running. Closing the channel - will cancel the query." +(defn process-query-and-save-with-max-results-constraints! + "Async version of `metabase.query-processor/process-query-and-save-with-max-results-constraints!`. Runs query + asynchronously, and returns a `core.async` channel that can be used to fetch the results once the query finishes + running. Closing the channel will cancel the query." [query options] - (do-async (:database query) qp/process-query-and-save-with-max! query options)) + (do-async (:database query) qp/process-query-and-save-with-max-results-constraints! query options)) (defn process-query-without-save! "Async version of `metabase.query-processor/process-query-without-save!`. Runs query asynchronously, and returns a diff --git a/src/metabase/server.clj b/src/metabase/server.clj index 03dfdc75a0adc8f493d352279143f2e80682120b..71c19a5442f6465a6d87e7a46f7482b2b5d62636 100644 --- a/src/metabase/server.clj +++ b/src/metabase/server.clj @@ -56,9 +56,13 @@ (.setHandler (#'ring-jetty/async-proxy-handler handler ;; if any API endpoint functions aren't at the very least returning a channel to fetch the results - ;; later after 30 seconds we're in serious trouble. Kill the request. + ;; later after 10 minutes we're in serious trouble. (Almost everything 'slow' should be returning a + ;; channel before then, but some things like CSV downloads don't currently return channels at this + ;; time) + ;; + ;; TODO - I suppose the default value should be moved to the `metabase.config` namespace? (or (config/config-int :mb-jetty-async-response-timeout) - (* 30 1000)))))) + (* 10 60 1000)))))) (defn start-web-server! "Start the embedded Jetty web server. Returns `:started` if a new server was started; `nil` if there was already a diff --git a/src/metabase/sync/util.clj b/src/metabase/sync/util.clj index 89260d16608eb99ff5664a49b2b3cfa63b13f27c..12a561fe521e0c2c622684d4e0cb8171480179c5 100644 --- a/src/metabase/sync/util.clj +++ b/src/metabase/sync/util.clj @@ -411,23 +411,26 @@ [task-name :- su/NonBlankString database :- i/DatabaseInstance {:keys [start-time end-time]} :- SyncOperationOrStepRunMetadata] - {:task task-name - :db_id (u/get-id database) + {:task task-name + :db_id (u/get-id database) :started_at (du/->Timestamp start-time) - :ended_at (du/->Timestamp end-time) - :duration (du/calculate-duration start-time end-time)}) + :ended_at (du/->Timestamp end-time) + :duration (du/calculate-duration start-time end-time)}) (s/defn ^:private store-sync-summary! [operation :- s/Str database :- i/DatabaseInstance {:keys [steps] :as sync-md} :- SyncOperationMetadata] - (db/insert-many! TaskHistory - (cons (create-task-history operation database sync-md) - (for [[step-name step-info] steps - :let [task-details (dissoc step-info :start-time :end-time :log-summary-fn)]] - (assoc (create-task-history step-name database step-info) - :task_details (when (seq task-details) - task-details)))))) + (try + (db/insert-many! TaskHistory + (cons (create-task-history operation database sync-md) + (for [[step-name step-info] steps + :let [task-details (dissoc step-info :start-time :end-time :log-summary-fn)]] + (assoc (create-task-history step-name database step-info) + :task_details (when (seq task-details) + task-details))))) + (catch Throwable e + (log/warn e (trs "Error saving task history"))))) (s/defn run-sync-operation "Run `sync-steps` and log a summary message" diff --git a/src/metabase/task.clj b/src/metabase/task.clj index 485cad0e734e695f9d4d38e05b4e89f6362c8db6..0bf425489f5cd0e767f02624d81393f4f1065c4c 100644 --- a/src/metabase/task.clj +++ b/src/metabase/task.clj @@ -53,9 +53,11 @@ {:arglists '([job-name-string])} keyword) -(defn- find-and-load-tasks! +(defn- find-and-load-task-namespaces! "Search Classpath for namespaces that start with `metabase.tasks.`, then `require` them so initialization can happen." [] + ;; make sure current thread is using canonical MB classloader + (classloader/the-classloader) ;; first, load all the task namespaces (doseq [ns-symb @u/metabase-namespace-symbols :when (.startsWith (name ns-symb) "metabase.task.")] @@ -63,8 +65,11 @@ (log/debug (trs "Loading tasks namespace:") (u/format-color 'blue ns-symb)) (require ns-symb) (catch Throwable e - (log/error e (trs "Error loading tasks namespace {0}" ns-symb))))) - ;; next, call all implementations of `init!` + (log/error e (trs "Error loading tasks namespace {0}" ns-symb)))))) + +(defn- init-tasks! + "Call all implementations of `init!`" + [] (doseq [[k f] (methods init!)] (try ;; don't bother logging namespace for now, maybe in the future if there's tasks of the same name in multiple @@ -97,33 +102,8 @@ ;;; | Quartz Scheduler Class Load Helper | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; Custom `ClassLoadHelper` implementation that makes sure to require the namespaces that tasks live in (to make sure -;; record types are loaded) and that uses our canonical ClassLoader. - -(defn- task-class-name->namespace-str - "Determine the namespace we need to load for one of our tasks. - - (task-class-name->namespace-str \"metabase.task.upgrade_checks.CheckForNewVersions\") - ;; -> \"metabase.task.upgrade-checks\"" - [class-name] - (-> class-name - (str/replace \_ \-) - (str/replace #"\.\w+$" ""))) - -(defn- require-task-namespace - "Since Metabase tasks are defined in Clojure-land we need to make sure we `require` the namespaces where they are - defined before we try to load the task classes." - [class-name] - ;; call `the-classloader` to force side-effects of making it the current thread context classloader - (classloader/the-classloader) - ;; only try to `require` metabase.task classes; don't do this for other stuff that gets shuffled thru here like - ;; Quartz classes - (when (str/starts-with? class-name "metabase.task.") - (require (symbol (task-class-name->namespace-str class-name))))) - (defn- load-class ^Class [^String class-name] - (require-task-namespace class-name) - (.loadClass (classloader/the-classloader) class-name)) + (Class/forName class-name true (classloader/the-classloader))) (defrecord ^:private ClassLoadHelper [] org.quartz.spi.ClassLoadHelper @@ -158,8 +138,9 @@ (set-jdbc-backend-properties!) (let [new-scheduler (qs/initialize)] (when (compare-and-set! quartz-scheduler nil new-scheduler) + (find-and-load-task-namespaces!) (qs/start new-scheduler) - (find-and-load-tasks!))))) + (init-tasks!))))) (defn stop-scheduler! "Stop our Quartzite scheduler and shutdown any running executions." diff --git a/src/metabase/task/task_history_cleanup.clj b/src/metabase/task/task_history_cleanup.clj index 45a5f6dfb77041185b4ce432e3de60d1e7993927..53762c570292f0fa84e5bf76a537e69182b52205 100644 --- a/src/metabase/task/task_history_cleanup.clj +++ b/src/metabase/task/task_history_cleanup.clj @@ -9,7 +9,7 @@ [metabase.util.i18n :refer [trs]])) (def ^:private history-rows-to-keep - "Maximum number of TaskHistory rows. This is not a `const` so that we can redef it in tests" + "Maximum number of TaskHistory rows." 100000) (defn- task-history-cleanup! [] diff --git a/test/metabase/async/api_response_test.clj b/test/metabase/async/api_response_test.clj index fa4415fcf9adcff3d5247765a5d80daae377d370..d62b3228ecdcb28f4790ae33417c5d7dbb650373 100644 --- a/test/metabase/async/api_response_test.clj +++ b/test/metabase/async/api_response_test.clj @@ -1,7 +1,12 @@ (ns metabase.async.api-response-test (:require [cheshire.core :as json] + [clj-http.client :as client] [clojure.core.async :as a] + [compojure.core :as compojure] [expectations :refer [expect]] + [metabase + [server :as server] + [util :as u]] [metabase.async.api-response :as async-response] [metabase.test.util.async :as tu.async] [ring.core.protocols :as ring.protocols]) @@ -13,7 +18,7 @@ ;;; +----------------------------------------------------------------------------------------------------------------+ -;;; | New Tests | +;;; | Tests to make sure channels do the right thing | ;;; +----------------------------------------------------------------------------------------------------------------+ (defn- do-with-response [input-chan f] @@ -25,8 +30,16 @@ (a/close! os-closed-chan) (let [^Closeable this this] (proxy-super close))))] - (let [{output-chan :body, :as response} (#'async-response/async-keepalive-response input-chan)] - (ring.protocols/write-body-to-stream output-chan response os) + ;; normally `write-body-to-stream` will create the `output-chan`, however we want to do it ourselves so we can + ;; truly enjoy the magical output channel slash see when it gets closed. Create it now... + (let [output-chan (#'async-response/async-keepalive-channel input-chan) + response {:status 200 + :headers {} + :body input-chan + :content-type "applicaton/json; charset=utf-8"}] + ;; and keep it from getting [re]created. + (with-redefs [async-response/async-keepalive-channel identity] + (ring.protocols/write-body-to-stream output-chan response os)) (try (f {:os os, :output-chan output-chan, :os-closed-chan os-closed-chan}) (finally @@ -198,3 +211,59 @@ (with-response [{:keys [output-chan os-closed-chan]} input-chan] (wait-for-close os-closed-chan) (wait-for-close output-chan))))) + + +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | Tests to make sure keepalive bytes actually get written | +;;; +----------------------------------------------------------------------------------------------------------------+ + +(defn- do-with-temp-server [handler f] + (let [port (+ 60000 (rand-int 5000)) + server (server/create-server handler {:port port})] + (try + (.start server) + (f port) + (finally + (.stop server))))) + +(defmacro ^:private with-temp-server + "Spin up a Jetty server with `handler` with a random port between 60000 and 65000; bind the random port to `port`, and + execute body. Shuts down server when finished." + [[port-binding handler] & body] + `(do-with-temp-server ~handler (fn [~port-binding] ~@body))) + +(defn- num-keepalive-chars-in-response + "Make a request to `handler` and count the number of newline keepalive chars in the response." + [handler] + (with-redefs [async-response/keepalive-interval-ms 50] + (with-temp-server [port handler] + (let [{response :body} (client/get (format "http://localhost:%d/" port))] + (count (re-seq #"\n" response)))))) + +(defn- output-chan-with-delayed-result + "Returns an output channel that receives a 'DONE' value after 400ms. " + [] + (u/prog1 (a/chan 1) + (a/go + (a/<! (a/timeout 400)) + (a/>! <> "DONE")))) + +;; confirm that some newlines were written as part of the response for an async API response +(defn- async-handler [_ respond _] + (respond {:status 200, :headers {"Content-Type" "text/plain"}, :body (output-chan-with-delayed-result)})) + +(expect pos? (num-keepalive-chars-in-response async-handler)) + +;; make sure newlines are written for sync-style compojure endpoints (e.g. `defendpoint`) +(def ^:private compojure-sync-handler + (compojure/routes + (compojure/GET "/" [_] (output-chan-with-delayed-result)))) + +(expect pos? (num-keepalive-chars-in-response compojure-sync-handler)) + +;; ...and for true async compojure endpoints (e.g. `defendpoint-async`) +(def ^:private compojure-async-handler + (compojure/routes + (compojure/GET "/" [] (fn [_ respond _] (respond (output-chan-with-delayed-result)))))) + +(expect pos? (num-keepalive-chars-in-response compojure-async-handler)) diff --git a/test/metabase/query_processor_test/constraints_test.clj b/test/metabase/query_processor_test/constraints_test.clj index 02978fc1c17af92a3c4d9ab41dda943fe1e90a85..a25d8922a3a85f0b98242c0efe508a6f2369495b 100644 --- a/test/metabase/query_processor_test/constraints_test.clj +++ b/test/metabase/query_processor_test/constraints_test.clj @@ -29,8 +29,8 @@ :native (native-query) :constraints {:max-results 5}}))) -;; does it also work when running via `process-query-and-save-with-max!`, the function that powers endpoints like -;; `POST /api/dataset`? +;; does it also work when running via `process-query-and-save-with-max-results-constraints!`, the function that powers +;; endpoints like `POST /api/dataset`? (qp.test/expect-with-non-timeseries-dbs [["Red Medicine"] ["Stout Burgers & Beers"] @@ -38,7 +38,7 @@ ["Wurstküche"] ["Brite Spot Family Restaurant"]] (qp.test/rows - (qp/process-query-and-save-with-max! + (qp/process-query-and-save-with-max-results-constraints! {:database (data/id) :type :native :native (native-query) diff --git a/test/metabase/task_test.clj b/test/metabase/task_test.clj deleted file mode 100644 index e7f97fd0486c6bc1b9095ec5d930c3b71024eed9..0000000000000000000000000000000000000000 --- a/test/metabase/task_test.clj +++ /dev/null @@ -1,7 +0,0 @@ -(ns metabase.task-test - (:require [expectations :refer [expect]] - [metabase.task :as task])) - -(expect - "metabase.task.upgrade-checks" - (#'task/task-class-name->namespace-str "metabase.task.upgrade_checks.CheckForNewVersions"))