Repository: sqlfluff/sqlfluff

Issue:
add ability to render the compiled sql
### Search before asking

- [X] I searched the [issues](https://github.com/sqlfluff/sqlfluff/issues) and found no similar issues.


### Description

It would be nice to see the compiled sql in which any templates are rendered. I would be happy to work on this but it may be a struggle and would need some guidance.

### Use case

 It would help debug linting errors around jinja templates.
It would also make it easier to copy and use the query in the bigquery ui, for example. We process our queries through Airflow so currently I can start a dag run and look at the rendered template to get this effect. That's not very efficient though :)


### Dialect

We use bigquery but this could apply to all dialects.

### Are you willing to work on and submit a PR to address the issue?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/sqlfluff/sqlfluff/blob/main/CODE_OF_CONDUCT.md)



Discussion:
If you're interested in contributing this feature, I suggest starting by looking at the existing `parse` command, implemented [here](https://github.com/sqlfluff/sqlfluff/blob/main/src/sqlfluff/cli/commands.py#L938). I think this new command will be pretty similar except for the output format.

I think `sqlfluff render` is a good name for it. 👍
If you have questions, feel free to drop them here or ask in the "contributing" Slack channel.

Patch:
diff --git a/src/sqlfluff/cli/commands.py b/src/sqlfluff/cli/commands.py
--- a/src/sqlfluff/cli/commands.py
+++ b/src/sqlfluff/cli/commands.py
@@ -1116,6 +1116,68 @@ def parse(
         sys.exit(EXIT_SUCCESS)
 
 
+@cli.command()
+@common_options
+@core_options
+@click.argument("path", nargs=1, type=click.Path(allow_dash=True))
+def render(
+    path: str,
+    bench: bool,
+    logger: Optional[logging.Logger] = None,
+    extra_config_path: Optional[str] = None,
+    ignore_local_config: bool = False,
+    **kwargs,
+) -> None:
+    """Render SQL files and just spit out the result.
+
+    PATH is the path to a sql file. This should be either a single file
+    file ('path/to/file.sql') or a single ('-') character to indicate reading
+    from *stdin*.
+    """
+    c = get_config(
+        extra_config_path, ignore_local_config, require_dialect=False, **kwargs
+    )
+    # We don't want anything else to be logged if we want json or yaml output
+    # unless we're writing to a file.
+    output_stream = make_output_stream(c, None, None)
+    lnt, formatter = get_linter_and_formatter(c, output_stream)
+    verbose = c.get("verbose")
+
+    progress_bar_configuration.disable_progress_bar = True
+
+    formatter.dispatch_config(lnt)
+
+    # Set up logging.
+    set_logging_level(
+        verbosity=verbose,
+        formatter=formatter,
+        logger=logger,
+        stderr_output=False,
+    )
+
+    # handle stdin if specified via lone '-'
+    with PathAndUserErrorHandler(formatter, path):
+        if "-" == path:
+            raw_sql = sys.stdin.read()
+            fname = "stdin"
+            file_config = lnt.config
+        else:
+            raw_sql, file_config, _ = lnt.load_raw_file_and_config(path, lnt.config)
+            fname = path
+
+    # Get file specific config
+    file_config.process_raw_file_for_config(raw_sql)
+    rendered = lnt.render_string(raw_sql, fname, file_config, "utf8")
+
+    if rendered.templater_violations:
+        for v in rendered.templater_violations:
+            click.echo(formatter.format_violation(v))
+        sys.exit(EXIT_FAIL)
+    else:
+        click.echo(rendered.templated_file.templated_str)
+        sys.exit(EXIT_SUCCESS)
+
+
 # This "__main__" handler allows invoking SQLFluff using "python -m", which
 # simplifies the use of cProfile, e.g.:
 # python -m cProfile -s cumtime -m sqlfluff.cli.commands lint slow_file.sql
diff --git a/src/sqlfluff/core/linter/linter.py b/src/sqlfluff/core/linter/linter.py
--- a/src/sqlfluff/core/linter/linter.py
+++ b/src/sqlfluff/core/linter/linter.py
@@ -110,7 +110,7 @@ def rule_tuples(self) -> List[RuleTuple]:
     # These are the building blocks of the linting process.
 
     @staticmethod
-    def _load_raw_file_and_config(
+    def load_raw_file_and_config(
         fname: str, root_config: FluffConfig
     ) -> Tuple[str, FluffConfig, str]:
         """Load a raw file and the associated config."""
@@ -837,7 +837,7 @@ def render_string(
     def render_file(self, fname: str, root_config: FluffConfig) -> RenderedFile:
         """Load and render a file with relevant config."""
         # Load the raw file.
-        raw_file, config, encoding = self._load_raw_file_and_config(fname, root_config)
+        raw_file, config, encoding = self.load_raw_file_and_config(fname, root_config)
         # Render the file
         return self.render_string(raw_file, fname, config, encoding)
 
@@ -1211,7 +1211,7 @@ def parse_path(
                 self.formatter.dispatch_path(path)
             # Load the file with the config and yield the result.
             try:
-                raw_file, config, encoding = self._load_raw_file_and_config(
+                raw_file, config, encoding = self.load_raw_file_and_config(
                     fname, self.config
                 )
             except SQLFluffSkipFile as s:


Test patch:
diff --git a/test/cli/commands_test.py b/test/cli/commands_test.py
--- a/test/cli/commands_test.py
+++ b/test/cli/commands_test.py
@@ -31,6 +31,7 @@
     parse,
     dialects,
     get_config,
+    render,
 )
 from sqlfluff.core.rules import BaseRule, LintFix, LintResult
 from sqlfluff.core.parser.segments.raw import CommentSegment
@@ -250,6 +251,15 @@ def test__cli__command_lint_stdin(command):
     invoke_assert_code(args=[lint, ("--dialect=ansi",) + command], cli_input=sql)
 
 
+def test__cli__command_render_stdin():
+    """Check render on a simple script using stdin."""
+    with open("test/fixtures/cli/passing_a.sql") as test_file:
+        sql = test_file.read()
+    result = invoke_assert_code(args=[render, ("--dialect=ansi", "-")], cli_input=sql)
+    # Check we get back out the same file we input.
+    assert result.output.startswith(sql)
+
+
 @pytest.mark.parametrize(
     "command",
     [
@@ -263,6 +273,13 @@ def test__cli__command_lint_stdin(command):
                 "L051",
             ],
         ),
+        # Basic render
+        (
+            render,
+            [
+                "test/fixtures/cli/passing_b.sql",
+            ],
+        ),
         # Original tests from test__cli__command_lint
         (lint, ["-n", "test/fixtures/cli/passing_a.sql"]),
         (lint, ["-n", "-v", "test/fixtures/cli/passing_a.sql"]),
@@ -474,8 +491,15 @@ def test__cli__command_lint_parse(command):
         (
             (
                 lint,
-                ["test/fixtures/cli/unknown_jinja_tag/test.sql", "-vvvvvvv"],
-                "y",
+                ["test/fixtures/cli/unknown_jinja_tag/test.sql"],
+            ),
+            1,
+        ),
+        # Test render fail
+        (
+            (
+                render,
+                ["test/fixtures/cli/fail_many.sql"],
             ),
             1,
         ),
@@ -1790,3 +1814,41 @@ def test__cli__multiple_files__fix_multiple_errors_show_errors():
 
     # Assert that they are sorted in alphabetical order
     assert unfix_err_log.index(indent_pass_msg) < unfix_err_log.index(multi_fail_msg)
+
+
+def test__cli__render_fail():
+    """Basic how render fails."""
+    expected_render_output = (
+        "L:   3 | P:   8 |  TMP | Undefined jinja template " "variable: 'something'"
+    )
+
+    result = invoke_assert_code(
+        ret_code=1,
+        args=[
+            render,
+            [
+                "test/fixtures/cli/fail_many.sql",
+            ],
+        ],
+    )
+    # Check whole output. The replace command just accounts for
+    # cross platform testing.
+    assert result.output.replace("\\", "/").startswith(expected_render_output)
+
+
+def test__cli__render_pass():
+    """Basic how render works."""
+    expected_render_output = "SELECT 56 FROM sch1.tbl2"
+
+    result = invoke_assert_code(
+        ret_code=0,
+        args=[
+            render,
+            [
+                "test/fixtures/templater/jinja_a/jinja.sql",
+            ],
+        ],
+    )
+    # Check whole output. The replace command just accounts for
+    # cross platform testing.
+    assert result.output.replace("\\", "/").startswith(expected_render_output)
diff --git a/test/core/linter_test.py b/test/core/linter_test.py
--- a/test/core/linter_test.py
+++ b/test/core/linter_test.py
@@ -90,7 +90,7 @@ def test__linter__skip_large_bytes(filesize, raises_skip):
     # First check the function directly
     if raises_skip:
         with pytest.raises(SQLFluffSkipFile) as excinfo:
-            Linter._load_raw_file_and_config(
+            Linter.load_raw_file_and_config(
                 "test/fixtures/linter/indentation_errors.sql", config
             )
         assert "Skipping" in str(excinfo.value)