Repository: dbt-labs/dbt-core

Issue:
Regression: dbt does not allow integer dataset names on BigQuery
### Describe the bug
BigQuery allows stringy dataset names that contain only digits, but dbt fails at startup if such a dataset name is provided. Is this a function of dbt's new NativeEnvironment rendering for jinja?

### Steps To Reproduce
Use a profile like:
```
    dev:
      type: bigquery
      method: service-account
      project: '...'
      dataset: "1991"    # This is the important part
      threads: 1
      keyfile: ...
      location: US
      timeout_seconds: 1
      retries: 3
```

A `dbt run` will fail indicating that the provided profile is invalid:
```
Credentials in profile "bq", target "dev" invalid: 1991 is not of type 'string'
```

### Expected behavior
dbt should create models in a dataset named `1991`

### dbt version
```
dev/octavius-catto
```


Discussion:
>Is this a function of dbt's new NativeEnvironment rendering for jinja?

Yes, almost certainly. I can think of two ways to approach this:
 - figure out how to tell the renderer that "this value should come out as a string". I'm not sure if that's hard or not, but I bet it isn't simple. This probably involves something neat with native_concat and marker types/custom filters?
 - figure out how to tell the validator "if you expected a string, call `str` on it". That's actually kind of tricky in the general case, because we don't really control validation, so we'd probably need to separately descend in, comparing it to the dataclass being serialized, and mutate the source dict before we get to the validator. Hideous!

Resolving patch:
diff --git a/core/dbt/clients/jinja.py b/core/dbt/clients/jinja.py
--- a/core/dbt/clients/jinja.py
+++ b/core/dbt/clients/jinja.py
@@ -8,7 +8,7 @@
 from contextlib import contextmanager
 from itertools import chain, islice
 from typing import (
-    List, Union, Set, Optional, Dict, Any, Iterator, Type, NoReturn
+    List, Union, Set, Optional, Dict, Any, Iterator, Type, NoReturn, Tuple
 )
 
 import jinja2
@@ -104,6 +104,13 @@ class NativeSandboxEnvironment(MacroFuzzEnvironment):
     code_generator_class = jinja2.nativetypes.NativeCodeGenerator
 
 
+class TextMarker(str):
+    """A special native-env marker that indicates that a value is text and is
+    not to be evaluated. Use this to prevent your numbery-strings from becoming
+    numbers!
+    """
+
+
 def quoted_native_concat(nodes):
     """This is almost native_concat from the NativeTemplate, except in the
     special case of a single argument that is a quoted string and returns a
@@ -116,6 +123,8 @@ def quoted_native_concat(nodes):
 
     if len(head) == 1:
         raw = head[0]
+        if isinstance(raw, TextMarker):
+            return str(raw)
     else:
         raw = "".join([str(v) for v in chain(head, nodes)])
 
@@ -124,10 +133,7 @@ def quoted_native_concat(nodes):
     except (ValueError, SyntaxError, MemoryError):
         return raw
 
-    if len(head) == 1 and len(raw) > 2 and isinstance(result, str):
-        return _requote_result(raw, result)
-    else:
-        return result
+    return result
 
 
 class NativeSandboxTemplate(jinja2.nativetypes.NativeTemplate):  # mypy: ignore
@@ -423,12 +429,18 @@ def get_environment(
     args['extensions'].append(DocumentationExtension)
 
     env_cls: Type[jinja2.Environment]
+    text_filter: Type
     if native:
         env_cls = NativeSandboxEnvironment
+        text_filter = TextMarker
     else:
         env_cls = MacroFuzzEnvironment
+        text_filter = str
+
+    env = env_cls(**args)
+    env.filters['as_text'] = text_filter
 
-    return env_cls(**args)
+    return env
 
 
 @contextmanager
@@ -541,9 +553,15 @@ def add_rendered_test_kwargs(
     """
     looks_like_func = r'^\s*(env_var|ref|var|source|doc)\s*\(.+\)\s*$'
 
-    # we never care about the keypath
-    def _convert_function(value: Any, _: Any) -> Any:
+    def _convert_function(
+        value: Any, keypath: Tuple[Union[str, int], ...]
+    ) -> Any:
         if isinstance(value, str):
+            if keypath == ('column_name',):
+                # special case: Don't render column names as native, make them
+                # be strings
+                return value
+
             if re.match(looks_like_func, value) is not None:
                 # curly braces to make rendering happy
                 value = f'{{{{ {value} }}}}'


Test patch:
diff --git a/test/integration/042_sources_test/models/schema.yml b/test/integration/042_sources_test/models/schema.yml
--- a/test/integration/042_sources_test/models/schema.yml
+++ b/test/integration/042_sources_test/models/schema.yml
@@ -22,7 +22,7 @@ sources:
     tables:
       - name: test_table
         identifier: source
-        loaded_at_field: "{{ var('test_loaded_at') }}"
+        loaded_at_field: "{{ var('test_loaded_at') | as_text }}"
         freshness:
           error_after: {count: 18, period: hour}
         tags:
@@ -63,7 +63,7 @@ sources:
               - id_column
       - name: disabled_test_table
         freshness: null
-        loaded_at_field: "{{ var('test_loaded_at') }}"
+        loaded_at_field: "{{ var('test_loaded_at') | as_text }}"
   - name: other_source
     schema: "{{ var('test_run_schema') }}"
     quoting:
diff --git a/test/unit/test_jinja.py b/test/unit/test_jinja.py
--- a/test/unit/test_jinja.py
+++ b/test/unit/test_jinja.py
@@ -1,5 +1,6 @@
 import unittest
 
+from dbt.clients.jinja import get_rendered
 from dbt.clients.jinja import get_template
 from dbt.clients.jinja import extract_toplevel_blocks
 from dbt.exceptions import CompilationException
@@ -13,6 +14,36 @@ def test_do(self):
         mod = template.make_module()
         self.assertEqual(mod.my_dict, {'a': 1})
 
+    def test_regular_render(self):
+        s = '{{ "some_value" }}'
+        value = get_rendered(s, {}, native=False)
+        assert value == 'some_value'
+        s = '{{ 1991 }}'
+        value = get_rendered(s, {}, native=False)
+        assert value == '1991'
+
+        s = '{{ "some_value" | as_text }}'
+        value = get_rendered(s, {}, native=True)
+        assert value == 'some_value'
+        s = '{{ 1991 | as_text }}'
+        value = get_rendered(s, {}, native=True)
+        assert value == '1991'
+
+    def test_native_render(self):
+        s = '{{ "some_value" }}'
+        value = get_rendered(s, {}, native=True)
+        assert value == 'some_value'
+        s = '{{ 1991 }}'
+        value = get_rendered(s, {}, native=True)
+        assert value == 1991
+
+        s = '{{ "some_value" | as_text }}'
+        value = get_rendered(s, {}, native=True)
+        assert value == 'some_value'
+        s = '{{ 1991 | as_text }}'
+        value = get_rendered(s, {}, native=True)
+        assert value == '1991'
+
 
 class TestBlockLexer(unittest.TestCase):
     def test_basic(self):
@@ -380,3 +411,5 @@ def test_if_endfor_newlines(self):
 hi
 {% endmaterialization %}
 '''
+
+