Repository: getredash/redash

Issue:
Scheduled query not working in the latest preview Docker Image (redash/redash:preview)
### Issue Summary

Scheduled query not working in the latest preview Docker Image (redash/redash:preview).

### Steps to Reproduce

`redash_scheduled_worker_1` has error:

```
[2021-03-27 07:27:06,319][PID:520][ERROR][rq.worker] AttributeError: 'NoneType' object has no attribute 'id'
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/rq/worker.py", line 934, in perform_job
    rv = job.perform()
  File "/usr/local/lib/python3.7/site-packages/rq/job.py", line 686, in perform
    self._result = self._execute()
  File "/usr/local/lib/python3.7/site-packages/rq/job.py", line 692, in _execute
    return self.func(*self.args, **self.kwargs)
  File "/app/redash/tasks/queries/execution.py", line 288, in execute_query
    scheduled_query_id is not None,
  File "/app/redash/tasks/queries/execution.py", line 175, in __init__
    models.scheduled_queries_executions.update(self.query_model.id)
AttributeError: 'NoneType' object has no attribute 'id'
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/rq/worker.py", line 934, in perform_job
    rv = job.perform()
  File "/usr/local/lib/python3.7/site-packages/rq/job.py", line 686, in perform
    self._result = self._execute()
  File "/usr/local/lib/python3.7/site-packages/rq/job.py", line 692, in _execute
    return self.func(*self.args, **self.kwargs)
  File "/app/redash/tasks/queries/execution.py", line 288, in execute_query
    scheduled_query_id is not None,
  File "/app/redash/tasks/queries/execution.py", line 175, in __init__
    models.scheduled_queries_executions.update(self.query_model.id)
AttributeError: 'NoneType' object has no attribute 'id'
```

If I revert this commit (9fdf1f341d02d903f045ea65d130a3cc93884299), it works again.

### Technical details:

* Redash Version: Version: 9.0.0-beta (44178d99)
* Browser/OS: Chrome
* How did you install Redash: Docker



Discussion:

Resolving patch:
diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py
--- a/redash/handlers/query_results.py
+++ b/redash/handlers/query_results.py
@@ -60,7 +60,9 @@ def error_response(message, http_status=400):
 }
 
 
-def run_query(query, parameters, data_source, query_id, should_apply_auto_limit, max_age=0):
+def run_query(
+    query, parameters, data_source, query_id, should_apply_auto_limit, max_age=0
+):
     if data_source.paused:
         if data_source.pause_reason:
             message = "{} is paused ({}). Please try later.".format(
@@ -76,7 +78,9 @@ def run_query(query, parameters, data_source, query_id, should_apply_auto_limit,
     except (InvalidParameterError, QueryDetachedFromDataSourceError) as e:
         abort(400, message=str(e))
 
-    query_text = data_source.query_runner.apply_auto_limit(query.text, should_apply_auto_limit)
+    query_text = data_source.query_runner.apply_auto_limit(
+        query.text, should_apply_auto_limit
+    )
 
     if query.missing_params:
         return error_response(
@@ -118,7 +122,7 @@ def run_query(query, parameters, data_source, query_id, should_apply_auto_limit,
                 "Username": repr(current_user)
                 if current_user.is_api_user()
                 else current_user.email,
-                "Query ID": query_id,
+                "query_id": query_id,
             },
         )
         return serialize_job(job)
@@ -195,7 +199,12 @@ def post(self):
             return error_messages["no_permission"]
 
         return run_query(
-            parameterized_query, parameters, data_source, query_id, should_apply_auto_limit, max_age
+            parameterized_query,
+            parameters,
+            data_source,
+            query_id,
+            should_apply_auto_limit,
+            max_age,
         )
 
 
@@ -392,10 +401,10 @@ def get(self, query_id=None, query_result_id=None, filetype="json"):
                 self.record_event(event)
 
             response_builders = {
-                'json': self.make_json_response,
-                'xlsx': self.make_excel_response,
-                'csv': self.make_csv_response,
-                'tsv': self.make_tsv_response
+                "json": self.make_json_response,
+                "xlsx": self.make_excel_response,
+                "csv": self.make_csv_response,
+                "tsv": self.make_tsv_response,
             }
             response = response_builders[filetype](query_result)
 
@@ -426,12 +435,16 @@ def make_json_response(query_result):
     @staticmethod
     def make_csv_response(query_result):
         headers = {"Content-Type": "text/csv; charset=UTF-8"}
-        return make_response(serialize_query_result_to_dsv(query_result, ","), 200, headers)
+        return make_response(
+            serialize_query_result_to_dsv(query_result, ","), 200, headers
+        )
 
     @staticmethod
     def make_tsv_response(query_result):
         headers = {"Content-Type": "text/tab-separated-values; charset=UTF-8"}
-        return make_response(serialize_query_result_to_dsv(query_result, "\t"), 200, headers)
+        return make_response(
+            serialize_query_result_to_dsv(query_result, "\t"), 200, headers
+        )
 
     @staticmethod
     def make_excel_response(query_result):
diff --git a/redash/tasks/queries/execution.py b/redash/tasks/queries/execution.py
--- a/redash/tasks/queries/execution.py
+++ b/redash/tasks/queries/execution.py
@@ -95,7 +95,7 @@ def enqueue_query(
                         "data_source_id": data_source.id,
                         "org_id": data_source.org_id,
                         "scheduled": scheduled_query_id is not None,
-                        "query_id": metadata.get("query_id", metadata.get("Query ID")),
+                        "query_id": metadata.get("query_id"),
                         "user_id": user_id,
                     },
                 }
@@ -159,7 +159,7 @@ def __init__(
         self.metadata = metadata
         self.data_source = self._load_data_source()
         self.query_id = metadata.get("query_id")
-        self.user = _resolve_user(user_id, is_api_key, metadata.get("Query ID"))
+        self.user = _resolve_user(user_id, is_api_key, metadata.get("query_id"))
         self.query_model = (
             models.Query.query.get(self.query_id)
             if self.query_id and self.query_id != "adhoc"
@@ -259,7 +259,7 @@ def _log_progress(self, state):
             self.data_source.id,
             self.job.id,
             self.metadata.get("Queue", "unknown"),
-            self.metadata.get("Query ID", "unknown"),
+            self.metadata.get("query_id", "unknown"),
             self.metadata.get("Username", "unknown"),
         )
 
diff --git a/redash/tasks/queries/maintenance.py b/redash/tasks/queries/maintenance.py
--- a/redash/tasks/queries/maintenance.py
+++ b/redash/tasks/queries/maintenance.py
@@ -78,7 +78,9 @@ class RefreshQueriesError(Exception):
 
 def _apply_auto_limit(query_text, query):
     should_apply_auto_limit = query.options.get("apply_auto_limit", False)
-    return query.data_source.query_runner.apply_auto_limit(query_text, should_apply_auto_limit)
+    return query.data_source.query_runner.apply_auto_limit(
+        query_text, should_apply_auto_limit
+    )
 
 
 def refresh_queries():
@@ -96,7 +98,7 @@ def refresh_queries():
                 query.data_source,
                 query.user_id,
                 scheduled_query=query,
-                metadata={"Query ID": query.id, "Username": "Scheduled"},
+                metadata={"query_id": query.id, "Username": "Scheduled"},
             )
             enqueued.append(query)
         except Exception as e:


Test patch:
diff --git a/tests/tasks/test_queries.py b/tests/tasks/test_queries.py
--- a/tests/tasks/test_queries.py
+++ b/tests/tasks/test_queries.py
@@ -48,7 +48,7 @@ def test_multiple_enqueue_of_same_query(self, enqueue, _):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
             enqueue_query(
                 query.query_text,
@@ -56,7 +56,7 @@ def test_multiple_enqueue_of_same_query(self, enqueue, _):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
             enqueue_query(
                 query.query_text,
@@ -64,7 +64,7 @@ def test_multiple_enqueue_of_same_query(self, enqueue, _):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
 
         self.assertEqual(1, enqueue.call_count)
@@ -79,7 +79,7 @@ def test_multiple_enqueue_of_expired_job(self, enqueue, fetch_job):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
 
             # "expire" the previous job
@@ -91,7 +91,7 @@ def test_multiple_enqueue_of_expired_job(self, enqueue, fetch_job):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
 
         self.assertEqual(2, enqueue.call_count)
@@ -106,7 +106,7 @@ def test_reenqueue_during_job_cancellation(self, enqueue, my_fetch_job):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
 
             # "cancel" the previous job
@@ -123,7 +123,7 @@ def cancel_job(*args, **kwargs):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
 
         self.assertEqual(2, enqueue.call_count)
@@ -139,7 +139,7 @@ def test_limits_query_time(self, _, enqueue, __):
                 query.user_id,
                 False,
                 query,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
 
         _, kwargs = enqueue.call_args
@@ -155,7 +155,7 @@ def test_multiple_enqueue_of_different_query(self, enqueue, _):
                 query.user_id,
                 False,
                 None,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
             enqueue_query(
                 query.query_text + "2",
@@ -163,7 +163,7 @@ def test_multiple_enqueue_of_different_query(self, enqueue, _):
                 query.user_id,
                 False,
                 None,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
             enqueue_query(
                 query.query_text + "3",
@@ -171,7 +171,7 @@ def test_multiple_enqueue_of_different_query(self, enqueue, _):
                 query.user_id,
                 False,
                 None,
-                {"Username": "Arik", "Query ID": query.id},
+                {"Username": "Arik", "query_id": query.id},
             )
 
         self.assertEqual(3, enqueue.call_count)
diff --git a/tests/tasks/test_refresh_queries.py b/tests/tasks/test_refresh_queries.py
--- a/tests/tasks/test_refresh_queries.py
+++ b/tests/tasks/test_refresh_queries.py
@@ -14,8 +14,9 @@ def test_enqueues_outdated_queries_for_sqlquery(self):
         """
         query1 = self.factory.create_query(options={"apply_auto_limit": True})
         query2 = self.factory.create_query(
-            query_text="select 42;", data_source=self.factory.create_data_source(),
-            options={"apply_auto_limit": True}
+            query_text="select 42;",
+            data_source=self.factory.create_data_source(),
+            options={"apply_auto_limit": True},
         )
         oq = staticmethod(lambda: [query1, query2])
         with patch(ENQUEUE_QUERY) as add_job_mock, patch.object(
@@ -30,14 +31,14 @@ def test_enqueues_outdated_queries_for_sqlquery(self):
                         query1.data_source,
                         query1.user_id,
                         scheduled_query=query1,
-                        metadata=ANY,
+                        metadata={"query_id": query1.id, "Username": "Scheduled"},
                     ),
                     call(
                         "select 42 LIMIT 1000",
                         query2.data_source,
                         query2.user_id,
                         scheduled_query=query2,
-                        metadata=ANY,
+                        metadata={"query_id": query2.id, "Username": "Scheduled"},
                     ),
                 ],
                 any_order=True,
@@ -51,7 +52,9 @@ def test_enqueues_outdated_queries_for_non_sqlquery(self):
         ds = self.factory.create_data_source(
             group=self.factory.org.default_group, type="prometheus"
         )
-        query1 = self.factory.create_query(data_source=ds, options={"apply_auto_limit": True})
+        query1 = self.factory.create_query(
+            data_source=ds, options={"apply_auto_limit": True}
+        )
         query2 = self.factory.create_query(
             query_text="select 42;", data_source=ds, options={"apply_auto_limit": True}
         )
@@ -68,14 +71,14 @@ def test_enqueues_outdated_queries_for_non_sqlquery(self):
                         query1.data_source,
                         query1.user_id,
                         scheduled_query=query1,
-                        metadata=ANY,
-                        ),
+                        metadata={"query_id": query1.id, "Username": "Scheduled"},
+                    ),
                     call(
                         query2.query_text,
                         query2.data_source,
                         query2.user_id,
                         scheduled_query=query2,
-                        metadata=ANY,
+                        metadata={"query_id": query2.id, "Username": "Scheduled"},
                     ),
                 ],
                 any_order=True,
@@ -106,7 +109,9 @@ def test_doesnt_enqueue_outdated_queries_for_paused_data_source_for_sqlquery(sel
                     metadata=ANY,
                 )
 
-    def test_doesnt_enqueue_outdated_queries_for_paused_data_source_for_non_sqlquery(self):
+    def test_doesnt_enqueue_outdated_queries_for_paused_data_source_for_non_sqlquery(
+        self,
+    ):
         """
         refresh_queries() does not launch execution tasks for queries whose
         data source is paused.
@@ -114,7 +119,9 @@ def test_doesnt_enqueue_outdated_queries_for_paused_data_source_for_non_sqlquery
         ds = self.factory.create_data_source(
             group=self.factory.org.default_group, type="prometheus"
         )
-        query = self.factory.create_query(data_source=ds, options={"apply_auto_limit": True})
+        query = self.factory.create_query(
+            data_source=ds, options={"apply_auto_limit": True}
+        )
         oq = staticmethod(lambda: [query])
         query.data_source.pause()
         with patch.object(Query, "outdated_queries", oq):
@@ -132,7 +139,7 @@ def test_doesnt_enqueue_outdated_queries_for_paused_data_source_for_non_sqlquery
                     query.user_id,
                     scheduled_query=query,
                     metadata=ANY,
-                    )
+                )
 
     def test_enqueues_parameterized_queries_for_sqlquery(self):
         """
@@ -150,7 +157,7 @@ def test_enqueues_parameterized_queries_for_sqlquery(self):
                         "title": "n",
                     }
                 ],
-                "apply_auto_limit": True
+                "apply_auto_limit": True,
             },
         )
         oq = staticmethod(lambda: [query])
@@ -185,8 +192,7 @@ def test_enqueues_parameterized_queries_for_non_sqlquery(self):
                         "title": "n",
                     }
                 ],
-                "apply_auto_limit": True
-
+                "apply_auto_limit": True,
             },
             data_source=ds,
         )
@@ -219,7 +225,7 @@ def test_doesnt_enqueue_parameterized_queries_with_invalid_parameters(self):
                         "title": "n",
                     }
                 ],
-                "apply_auto_limit": True
+                "apply_auto_limit": True,
             },
         )
         oq = staticmethod(lambda: [query])
@@ -230,7 +236,7 @@ def test_doesnt_enqueue_parameterized_queries_with_invalid_parameters(self):
             add_job_mock.assert_not_called()
 
     def test_doesnt_enqueue_parameterized_queries_with_dropdown_queries_that_are_detached_from_data_source(
-        self
+        self,
     ):
         """
         Scheduled queries with a dropdown parameter which points to a query that is detached from its data source are skipped.
@@ -247,7 +253,7 @@ def test_doesnt_enqueue_parameterized_queries_with_dropdown_queries_that_are_det
                         "title": "n",
                     }
                 ],
-                "apply_auto_limit": True
+                "apply_auto_limit": True,
             },
         )
 
diff --git a/tests/tasks/test_worker.py b/tests/tasks/test_worker.py
--- a/tests/tasks/test_worker.py
+++ b/tests/tasks/test_worker.py
@@ -29,7 +29,7 @@ def test_worker_records_success_metrics(self, incr):
                 query.user_id,
                 False,
                 None,
-                {"Username": "Patrick", "Query ID": query.id},
+                {"Username": "Patrick", "query_id": query.id},
             )
 
             Worker(["queries"]).work(max_jobs=1)
@@ -38,7 +38,7 @@ def test_worker_records_success_metrics(self, incr):
             call("rq.jobs.running.queries"),
             call("rq.jobs.started.queries"),
             call("rq.jobs.running.queries", -1, 1),
-            call("rq.jobs.finished.queries")
+            call("rq.jobs.finished.queries"),
         ]
         incr.assert_has_calls(calls)
 
@@ -56,7 +56,7 @@ def test_worker_records_failure_metrics(self, _, incr):
                 query.user_id,
                 False,
                 None,
-                {"Username": "Patrick", "Query ID": query.id},
+                {"Username": "Patrick", "query_id": query.id},
             )
             job.set_status(JobStatus.FAILED)
 
@@ -66,7 +66,7 @@ def test_worker_records_failure_metrics(self, _, incr):
             call("rq.jobs.running.queries"),
             call("rq.jobs.started.queries"),
             call("rq.jobs.running.queries", -1, 1),
-            call("rq.jobs.failed.queries")
+            call("rq.jobs.failed.queries"),
         ]
         incr.assert_has_calls(calls)
 
@@ -88,7 +88,7 @@ def test_enqueue_query_records_created_metric(self, incr):
                 query.user_id,
                 False,
                 None,
-                {"Username": "Patrick", "Query ID": query.id},
+                {"Username": "Patrick", "query_id": query.id},
             )
 
         incr.assert_called_with("rq.jobs.created.queries")