Repository: dbt-labs/dbt-core

Issue:
Defined dict with malformed result from `fromyaml()` passed into `log()` crashes dbt
### Describe the bug
dbt crashes attempting to compile when I inadvertently call `log()` with a non-string - this in turn leads the RPC server to not report an error and, in dbt Cloud, manifests as an infinite spinning compiling status.

```
[2021-06-16 18:27:42.179948] ERROR: werkzeug: Error on request:
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/werkzeug/serving.py", line 323, in run_wsgi
    execute(self.server.app)
  File "/usr/local/lib/python3.8/dist-packages/werkzeug/serving.py", line 312, in execute
    application_iter = app(environ, start_response)
  File "/usr/local/lib/python3.8/dist-packages/werkzeug/middleware/dispatcher.py", line 66, in __call__
    return app(environ, start_response)
  File "/usr/local/lib/python3.8/dist-packages/werkzeug/wrappers/base_request.py", line 238, in application
    resp = f(*args[:-2] + (request,))
  File "/usr/local/lib/python3.8/dist-packages/dbt/task/rpc/server.py", line 156, in handle_jsonrpc_request
    json_data = json.dumps(
  File "/usr/lib/python3.8/json/__init__.py", line 234, in dumps
    return cls(
  File "/usr/lib/python3.8/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.8/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/usr/local/lib/python3.8/dist-packages/dbt/utils.py", line 332, in default
    return super().default(obj)
  File "/usr/local/lib/python3.8/dist-packages/dbt/utils.py", line 320, in default
    if hasattr(obj, 'to_dict'):
  File "/usr/local/lib/python3.8/dist-packages/dbt/clients/jinja.py", line 441, in __getattr__
    return self.__class__(hint=self.hint, name=self.name)
  File "/usr/local/lib/python3.8/dist-packages/dbt/clients/jinja.py", line 418, in __init__
    super().__init__(hint=hint, name=name)
RecursionError: maximum recursion depth exceeded while calling a Python object
```

### Steps To Reproduce
Case 1:  Create a model file with (only):
```
{%- set yaml_metadata -%}
foo: 'foo_value'
bar:
    -this
        -bad 
            'one':'two'
        -notbad
            'three': 'four'
{%- endset -%}
{% set metadata_dict = fromyaml(yaml_metadata) %}
{{ log(metadata_dict['foo'],true) }}
```
in the dbt Cloud IDE and save.

### Expected behavior
Expect dbt not to crash.

**The output of `dbt --version`:**
```
0.19.1
```



Discussion:
Thanks for opening @barberscott!

I managed an even simpler reproduction case. Create and save a model file containing just:
```sql
{{ log(none['foo']) }}
```

In response to a `status` request, the server will return:
```
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or
	there is an error in the application.</p>
```
With the same stacktrace:
```
{"timestamp": "2021-06-17T19:52:53.044580Z", "message": "Error on request:\nTraceback (most recent call last):\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/werkzeug/serving.py\", line 323, in run_wsgi\n    execute(self.server.app)\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/werkzeug/serving.py\", line 312, in execute\n    application_iter = app(environ, start_response)\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/werkzeug/middleware/dispatcher.py\", line 66, in __call__\n    return app(environ, start_response)\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/werkzeug/wrappers/base_request.py\", line 238, in application\n    resp = f(*args[:-2] + (request,))\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/dbt/task/rpc/server.py\", line 156, in handle_jsonrpc_request\n    json_data = json.dumps(\n  File \"/usr/local/Cellar/python@3.8/3.8.10/Frameworks/Python.framework/Versions/3.8/lib/python3.8/json/__init__.py\", line 234, in dumps\n    return cls(\n  File \"/usr/local/Cellar/python@3.8/3.8.10/Frameworks/Python.framework/Versions/3.8/lib/python3.8/json/encoder.py\", line 199, in encode\n    chunks = self.iterencode(o, _one_shot=True)\n  File \"/usr/local/Cellar/python@3.8/3.8.10/Frameworks/Python.framework/Versions/3.8/lib/python3.8/json/encoder.py\", line 257, in iterencode\n    return _iterencode(o, 0)\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/dbt/utils.py\", line 332, in default\n    return super().default(obj)\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/dbt/utils.py\", line 320, in default\n    if hasattr(obj, 'to_dict'):\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/dbt/clients/jinja.py\", line 441, in __getattr__\n    return self.__class__(hint=self.hint, name=self.name)\n  File \"/usr/local/Cellar/dbt/0.19.1_1/libexec/lib/python3.8/site-packages/dbt/clients/jinja.py\", line 418, in __init__\n    super().__init__(hint=hint, name=name)\nRecursionError: maximum recursion depth exceeded while calling a Python object", "channel": "werkzeug", "level": 14, "levelname": "ERROR", "thread_name": "Thread-2", "process": 730, "extra": {"stack_info": null, "context": "server", "run_state": "internal"}}
```

Interestingly, this:
- Doesn't raise an error in CLI parsing / compilation
- Doesn't raise an error if the problematic text is passed to the `compile_sql` RPC method (base64: `e3sgbG9nKG5vbmVbJ2ZvbyddKSB9fQ==`), rather than being saved in the project

The error is cropping up when trying to create a Jinja2 Undefined object:
https://github.com/fishtown-analytics/dbt/blob/1edb8a60c8867c58e8d6078010aeceb0a6810a92/core/dbt/clients/jinja.py#L415-L425

That's as far as I've gotten. Worthy of further investigation for sure.

Resolving patch:
diff --git a/core/dbt/utils.py b/core/dbt/utils.py
--- a/core/dbt/utils.py
+++ b/core/dbt/utils.py
@@ -6,6 +6,7 @@
 import functools
 import hashlib
 import itertools
+import jinja2
 import json
 import os
 from contextlib import contextmanager
@@ -306,14 +307,16 @@ def timestring() -> str:
 
 class JSONEncoder(json.JSONEncoder):
     """A 'custom' json encoder that does normal json encoder things, but also
-    handles `Decimal`s. Naturally, this can lose precision because they get
-    converted to floats.
+    handles `Decimal`s. and `Undefined`s. Decimals can lose precision because
+    they get converted to floats. Undefined's are serialized to an empty string
     """
     def default(self, obj):
         if isinstance(obj, DECIMALS):
             return float(obj)
         if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
             return obj.isoformat()
+        if isinstance(obj, jinja2.Undefined):
+            return ""
         if hasattr(obj, 'to_dict'):
             # if we have a to_dict we should try to serialize the result of
             # that!


Test patch:
diff --git a/test/integration/100_rpc_test/rpc_serialize_test_models/test_serialize.sql b/test/integration/100_rpc_test/rpc_serialize_test_models/test_serialize.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/100_rpc_test/rpc_serialize_test_models/test_serialize.sql
@@ -0,0 +1,8 @@
+
+-- see https://github.com/dbt-labs/dbt/issues/3464
+-- Make sure that Undefined values are serialized correctly
+-- in RPC responses
+{{ log(none['foo']) }}
+
+select 1 as id
+
diff --git a/test/integration/100_rpc_test/test_execute_fetch_and_serialize.py b/test/integration/100_rpc_test/test_execute_fetch_and_serialize.py
--- a/test/integration/100_rpc_test/test_execute_fetch_and_serialize.py
+++ b/test/integration/100_rpc_test/test_execute_fetch_and_serialize.py
@@ -17,6 +17,7 @@ def models(self):
     def project_config(self):
         return {
             'config-version': 2,
+            'source-paths': ['rpc_serialize_test_models'],
             'macro-paths': ['macros'],
         }