Repository: sqlfluff/sqlfluff

Issue:
LT02 violation in jinja blocks with joins
### Search before asking

- [X] I searched the [issues](https://github.com/sqlfluff/sqlfluff/issues) and found no similar issues.


### What Happened

Hello, dear sqlfluff team.

When you're trying to put the join condition inside jinja template linter violates LT02 on the table name **before** templated block. Same behaviour for the union statement. Works as expected if you're putting and statement inside such a block.

### Expected Behaviour

Linter shouldn't violate LT02 when founds join statement inside jinja template block

### Observed Behaviour

Expected line break and indent of 8 spaces before | 'cte1'. [layout.indent]

### How to reproduce

```
with
cte1 as (
    select
        col1,
        col2
    from {{ ref('table_1') }}
    where col2 = 'val'
        {% if is_incremental() %}
            and updated_at >= '{{ max_updated_at }}'::timestamp
        {% endif %}
),

final as (
    select
        conn.col1
    from cte1 conn --LT02 | Expected line break and indent of 8 spaces before | 'cte1'. [layout.indent]
    {% if is_incremental() %}
        left join {{ this }} orig
        on orig.col1 = conn.col1
            and orig.col2 = conn.col2
        where orig.col1 is null
    {% endif %}

)

select *
from final
```

### Dialect

postgres

### Version

2.3.2

### Configuration

```
[sqlfluff]
templater = jinja
dialect = postgres
exclude_rules = AL02, AL04, AL05, RF01, AL07, ST07, ST06, CV03, CV06, CV08, TQ01, RF05, CV10, ST09
max_line_length = 140

[sqlfluff:indentation]
indented_using_on = False
allow_implicit_indents = True
indent_unit = space
tab_space_size = 4

[sqlfluff:rules:layout.indent]
operator_new_lines = after

[sqlfluff:rules:aliasing.table]
aliasing = implicit

[sqlfluff:rules:aliasing.expression]
allow_scalar = True

[sqlfluff:rules:layout.long_lines]
ignore_comment_lines = True
ignore_comment_clauses = True

[sqlfluff:rules:layout.operators]
operator_new_lines = after

[sqlfluff:rules:capitalisation.identifiers]
extended_capitalisation_policy = lower

[sqlfluff:rules:capitalisation.literals]
extended_capitalisation_policy = consistent

[sqlfluff:rules:ambiguous.column_references]
group_by_and_order_by_style = explicit

[sqlfluff:rules:references.keywords]
ignore_words = account

[sqlfluff:layout:type:comma]
line_position = trailing

[sqlfluff:templater:dbt]
project_dir = .
profiles_dir = .
profile = some
target = some

[sqlfluff:templater:jinja]
apply_dbt_builtins = True
load_macros_from_path = macros

[sqlfluff:templater:jinja:macros]
dbt_ref = {% macro ref(model_ref) %}{{model_ref}}{% endmacro %}
dbt_source = {% macro source(source_name, table) %}{{source_name}}_{{table}}{% endmacro %}
dbt_config = {% macro config() %}{% for k in kwargs %}{% endfor %}{% endmacro %}
dbt_var = {% macro var(variable, default='') %}item{% endmacro %}
dbt_env_var = {% macro env_var(variable, default='') %}item{% endmacro %}
dbt_is_incremental = {% macro is_incremental() %}True{% endmacro %}
dbt_relation_exists = {% macro relation_exists(model_name, object_schema=none, object_name=none) %}False{% endmacro %}
dbt_execute = {% macro execute() %}True{% endmacro %}
```

### Are you willing to work on and submit a PR to address the issue?

- [ ] 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:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/63209039/badge)](https://coveralls.io/builds/63209039)

coverage: 99.93% (-0.07%) from 100.0% when pulling **8ad3d69d024f8c41de7b716af3ec4b9f7bf954f7 on ac/indent** into **c117afa07e256cc8261d18b77404c25e9f497066 on main**.

github-actions[bot]: # Coverage Results ⚠️
```
Name                                    Stmts   Miss  Cover   Missing
---------------------------------------------------------------------
src/sqlfluff/core/rules/base.py           469      1    99%   237
src/sqlfluff/rules/layout/LT09.py         143     11    92%   43-57, 392-398
src/sqlfluff/utils/reflow/reindent.py     702      1    99%   361
---------------------------------------------------------------------
TOTAL                                   16729     13    99%

216 files skipped due to complete coverage.
Coverage failure: total of 99 is less than fail-under=100

```


Patch:
diff --git a/src/sqlfluff/core/parser/grammar/sequence.py b/src/sqlfluff/core/parser/grammar/sequence.py
index 0db87c480..03238c564 100644
--- a/src/sqlfluff/core/parser/grammar/sequence.py
+++ b/src/sqlfluff/core/parser/grammar/sequence.py
@@ -3,7 +3,7 @@
 # NOTE: We rename the typing.Sequence here so it doesn't collide
 # with the grammar class that we're defining.
 from os import getenv
-from typing import Optional, Set, Tuple, Type, Union
+from typing import Optional, Set, Tuple, Type, Union, cast
 from typing import Sequence as SequenceType
 
 from sqlfluff.core.parser.context import ParseContext
@@ -24,6 +24,7 @@ from sqlfluff.core.parser.segments import (
     BaseSegment,
     Indent,
     MetaSegment,
+    TemplateSegment,
     UnparsableSegment,
 )
 from sqlfluff.core.parser.types import ParseMode, SimpleHintType
@@ -31,13 +32,49 @@ from sqlfluff.core.slice_helpers import is_zero_slice
 
 
 def _flush_metas(
-    pre_nc_idx: int, post_nc_idx: int, meta_buffer: SequenceType[Type["MetaSegment"]]
+    pre_nc_idx: int,
+    post_nc_idx: int,
+    meta_buffer: SequenceType[Type["MetaSegment"]],
+    segments: SequenceType[BaseSegment],
 ) -> Tuple[Tuple[int, Type[MetaSegment]], ...]:
-    # Flush any metas...
+    """Position any new meta segments relative to the non code section.
+
+    It's important that we position the new meta segments appropriately
+    around any templated sections and any whitespace so that indentation
+    behaviour works as expected.
+
+    There are four valid locations (which may overlap).
+    1. Before any non-code
+    2. Before the first block templated section (if it's a block opener).
+    3. After the last block templated section (if it's a block closer).
+    4. After any non code.
+
+    If all the metas have a positive indent value then they should go in
+    position 1 or 3, otherwise we're in position 2 or 4. Within each of
+    those scenarios it depends on whether an appropriate block end exists.
+    """
     if all(m.indent_val >= 0 for m in meta_buffer):
-        meta_idx = pre_nc_idx
+        for _idx in range(post_nc_idx, pre_nc_idx, -1):
+            if segments[_idx - 1].is_type("placeholder"):
+                _seg = cast(TemplateSegment, segments[_idx - 1])
+                if _seg.block_type == "block_end":
+                    meta_idx = _idx
+                else:
+                    meta_idx = pre_nc_idx
+                break
+        else:
+            meta_idx = pre_nc_idx
     else:
-        meta_idx = post_nc_idx
+        for _idx in range(pre_nc_idx, post_nc_idx):
+            if segments[_idx].is_type("placeholder"):
+                _seg = cast(TemplateSegment, segments[_idx])
+                if _seg.block_type == "block_start":
+                    meta_idx = _idx
+                else:
+                    meta_idx = post_nc_idx
+                break
+        else:
+            meta_idx = post_nc_idx
     return tuple((meta_idx, meta) for meta in meta_buffer)
 
 
@@ -270,7 +307,7 @@ class Sequence(BaseGrammar):
                 )
 
             # Flush any metas...
-            insert_segments += _flush_metas(matched_idx, _idx, meta_buffer)
+            insert_segments += _flush_metas(matched_idx, _idx, meta_buffer, segments)
             meta_buffer = []
 
             # Otherwise we _do_ have a match. Update the position.
diff --git a/src/sqlfluff/core/rules/base.py b/src/sqlfluff/core/rules/base.py
index 6fb12b829..c2ac109b3 100644
--- a/src/sqlfluff/core/rules/base.py
+++ b/src/sqlfluff/core/rules/base.py
@@ -118,8 +118,6 @@ class LintResult:
         self.anchor = anchor
         # Fixes might be blank
         self.fixes = fixes or []
-        # When instantiating the result, we filter any fixes which are "trivial".
-        self.fixes = [f for f in self.fixes if not f.is_trivial()]
         # Memory is passed back in the linting result
         self.memory = memory
         # store a description_override for later
@@ -220,24 +218,17 @@ class LintFix:
             # can't guarantee with edits.
         self.source = [seg for seg in source if seg.pos_marker] if source else []
 
-    def is_trivial(self) -> bool:
-        """Return true if the fix is trivial.
-
-        Trivial edits are:
-        - Anything of zero length.
-        - Any edits which result in themselves.
-
-        Removing these makes the routines which process fixes much faster.
-        """
+        # On creation of the fix we'll also validate the edits are non-trivial.
         if self.edit_type in ("create_before", "create_after"):
-            if isinstance(self.edit, BaseSegment):
-                if len(self.edit.raw) == 0:  # pragma: no cover TODO?
-                    return True
-            elif self.edit and all(len(elem.raw) == 0 for elem in self.edit):
-                return True
-        elif self.edit_type == "replace" and self.edit == self.anchor:
-            return True  # pragma: no cover TODO?
-        return False
+            assert self.edit, "A create fix must have an edit."
+            # They should all have a non-zero raw.
+            assert all(
+                seg.raw for seg in self.edit
+            ), f"Invalid edit found: {self.edit}."
+        elif self.edit_type == "replace":
+            assert (
+                self.edit != self.anchor
+            ), "Fix created which replaces segment with itself."
 
     def is_just_source_edit(self) -> bool:
         """Return whether this a valid source only edit."""
diff --git a/src/sqlfluff/dialects/dialect_ansi.py b/src/sqlfluff/dialects/dialect_ansi.py
index 519eba97a..06e50deb4 100644
--- a/src/sqlfluff/dialects/dialect_ansi.py
+++ b/src/sqlfluff/dialects/dialect_ansi.py
@@ -1665,9 +1665,7 @@ class SelectClauseSegment(BaseSegment):
             Ref("SelectClauseElementSegment"),
             allow_trailing=True,
         ),
-        # NB: The Dedent for the indent above lives in the
-        # SelectStatementSegment so that it sits in the right
-        # place corresponding to the whitespace.
+        Dedent,
         terminators=[Ref("SelectClauseTerminatorGrammar")],
         parse_mode=ParseMode.GREEDY_ONCE_STARTED,
     )
@@ -2564,9 +2562,6 @@ class UnorderedSelectStatementSegment(BaseSegment):
 
     match_grammar: Matchable = Sequence(
         Ref("SelectClauseSegment"),
-        # Dedent for the indent in the select clause.
-        # It's here so that it can come AFTER any whitespace.
-        Dedent,
         Ref("FromClauseSegment", optional=True),
         Ref("WhereClauseSegment", optional=True),
         Ref("GroupByClauseSegment", optional=True),
diff --git a/src/sqlfluff/dialects/dialect_exasol.py b/src/sqlfluff/dialects/dialect_exasol.py
index 8e20a0c71..6e8eae07a 100644
--- a/src/sqlfluff/dialects/dialect_exasol.py
+++ b/src/sqlfluff/dialects/dialect_exasol.py
@@ -304,9 +304,6 @@ class UnorderedSelectStatementSegment(BaseSegment):
 
     match_grammar = Sequence(
         Ref("SelectClauseSegment"),
-        #     # Dedent for the indent in the select clause.
-        #     # It's here so that it can come AFTER any whitespace.
-        Dedent,
         Ref("FromClauseSegment", optional=True),
         Ref("ReferencingClauseSegment", optional=True),
         Ref("WhereClauseSegment", optional=True),
@@ -376,9 +373,7 @@ class SelectClauseSegment(BaseSegment):
         Ref("WithInvalidForeignKeySegment", optional=True),
         Ref("WithInvalidUniquePKSegment", optional=True),
         Ref("IntoTableSegment", optional=True),
-        # NB: The Dedent for the indent above lives in the
-        # SelectStatementSegment so that it sits in the right
-        # place corresponding to the whitespace.
+        Dedent,
         terminators=[Ref("SelectClauseTerminatorGrammar")],
         parse_mode=ParseMode.GREEDY_ONCE_STARTED,
     )
diff --git a/src/sqlfluff/dialects/dialect_postgres.py b/src/sqlfluff/dialects/dialect_postgres.py
index 8467b084e..01b009e58 100644
--- a/src/sqlfluff/dialects/dialect_postgres.py
+++ b/src/sqlfluff/dialects/dialect_postgres.py
@@ -1422,9 +1422,7 @@ class SelectClauseSegment(ansi.SelectClauseSegment):
             optional=True,
             allow_trailing=True,
         ),
-        # NB: The Dedent for the indent above lives in the
-        # SelectStatementSegment so that it sits in the right
-        # place corresponding to the whitespace.
+        Dedent,
         terminators=[
             "INTO",
             "FROM",
diff --git a/src/sqlfluff/dialects/dialect_sparksql.py b/src/sqlfluff/dialects/dialect_sparksql.py
index 8de0ad478..7553a9a08 100644
--- a/src/sqlfluff/dialects/dialect_sparksql.py
+++ b/src/sqlfluff/dialects/dialect_sparksql.py
@@ -3236,9 +3236,7 @@ class SelectClauseSegment(BaseSegment):
                 ),
             ),
         ),
-        # NB: The Dedent for the indent above lives in the
-        # SelectStatementSegment so that it sits in the right
-        # place corresponding to the whitespace.
+        Dedent,
         terminators=[
             "FROM",
             "WHERE",
diff --git a/src/sqlfluff/dialects/dialect_sqlite.py b/src/sqlfluff/dialects/dialect_sqlite.py
index 219448583..6dfc26fee 100644
--- a/src/sqlfluff/dialects/dialect_sqlite.py
+++ b/src/sqlfluff/dialects/dialect_sqlite.py
@@ -9,7 +9,6 @@ from sqlfluff.core.parser import (
     Anything,
     BaseSegment,
     Bracketed,
-    Dedent,
     Delimited,
     LiteralSegment,
     Matchable,
@@ -474,9 +473,6 @@ class UnorderedSelectStatementSegment(BaseSegment):
 
     match_grammar = Sequence(
         Ref("SelectClauseSegment"),
-        # Dedent for the indent in the select clause.
-        # It's here so that it can come AFTER any whitespace.
-        Dedent,
         Ref("FromClauseSegment", optional=True),
         Ref("WhereClauseSegment", optional=True),
         Ref("GroupByClauseSegment", optional=True),
diff --git a/src/sqlfluff/dialects/dialect_trino.py b/src/sqlfluff/dialects/dialect_trino.py
index feab35a68..063dabff8 100644
--- a/src/sqlfluff/dialects/dialect_trino.py
+++ b/src/sqlfluff/dialects/dialect_trino.py
@@ -9,7 +9,6 @@ from sqlfluff.core.parser import (
     Anything,
     BaseSegment,
     Bracketed,
-    Dedent,
     Delimited,
     LiteralSegment,
     Matchable,
@@ -227,9 +226,6 @@ class UnorderedSelectStatementSegment(ansi.UnorderedSelectStatementSegment):
 
     match_grammar: Matchable = Sequence(
         Ref("SelectClauseSegment"),
-        # Dedent for the indent in the select clause.
-        # It's here so that it can come AFTER any whitespace.
-        Dedent,
         Ref("FromClauseSegment", optional=True),
         Ref("WhereClauseSegment", optional=True),
         Ref("GroupByClauseSegment", optional=True),
diff --git a/src/sqlfluff/dialects/dialect_tsql.py b/src/sqlfluff/dialects/dialect_tsql.py
index 9ebc037b6..fd0b8249e 100644
--- a/src/sqlfluff/dialects/dialect_tsql.py
+++ b/src/sqlfluff/dialects/dialect_tsql.py
@@ -776,9 +776,7 @@ class SelectClauseSegment(BaseSegment):
         Indent,
         # NOTE: Don't allow trailing.
         Delimited(Ref("SelectClauseElementSegment")),
-        # NB: The Dedent for the indent above lives in the
-        # SelectStatementSegment so that it sits in the right
-        # place corresponding to the whitespace.
+        Dedent,
         # NOTE: In TSQL - this grammar is NOT greedy.
     )
 
@@ -797,9 +795,6 @@ class UnorderedSelectStatementSegment(BaseSegment):
     type = "select_statement"
     match_grammar = Sequence(
         Ref("SelectClauseSegment"),
-        # Dedent for the indent in the select clause.
-        # It's here so that it can come AFTER any whitespace.
-        Dedent,
         Ref("IntoTableSegment", optional=True),
         Ref("FromClauseSegment", optional=True),
         Ref("WhereClauseSegment", optional=True),
diff --git a/src/sqlfluff/rules/layout/LT09.py b/src/sqlfluff/rules/layout/LT09.py
index e93c37af4..e5b58c399 100644
--- a/src/sqlfluff/rules/layout/LT09.py
+++ b/src/sqlfluff/rules/layout/LT09.py
@@ -260,6 +260,9 @@ class Rule_LT09(BaseRule):
         # We'll add it to the right section at the end, once we know
         # what to add.
         initial_deletes = [target_seg]
+        # If there's whitespace before it, delete that too.
+        if select_children[target_idx - 1].is_type("whitespace"):
+            initial_deletes.append(select_children[target_idx - 1])
 
         # Do we have a modifier?
         modifier: Optional[Segments]
@@ -340,20 +343,6 @@ class Rule_LT09(BaseRule):
                         if delete_last_newline:
                             fixes.append(LintFix.delete(next_segment))
 
-                elif next_segment.is_type("dedent"):
-                    # Again let's strip back the whitespace, but simpler
-                    # as don't need to worry about new line so just break
-                    # if see non-whitespace
-                    to_delete = select_children.reversed().select(
-                        loop_while=sp.is_type("whitespace"),
-                        start_seg=select_children[select_clause_idx - 1],
-                    )
-                    if to_delete:
-                        # If we deleted a newline, create a newline.
-                        add_newline = any(
-                            seg for seg in to_delete if seg.is_type("newline")
-                        )
-
                 elif next_segment.is_type("whitespace"):
                     # The select_clause has stuff after (most likely a comment)
                     # Delete the whitespace immediately after the select clause
diff --git a/src/sqlfluff/utils/reflow/elements.py b/src/sqlfluff/utils/reflow/elements.py
index 311954351..2309772d9 100644
--- a/src/sqlfluff/utils/reflow/elements.py
+++ b/src/sqlfluff/utils/reflow/elements.py
@@ -531,15 +531,20 @@ class ReflowPoint(ReflowElement):
         else:
             # There isn't currently a newline.
             new_newline = NewlineSegment()
+            new_segs: List[RawSegment]
             # Check for whitespace
             ws_seg = None
             for seg in self.segments[::-1]:
                 if seg.is_type("whitespace"):
                     ws_seg = seg
             if not ws_seg:
+                # Work out the new segments. Always a newline, only whitespace if
+                # there's a non zero indent.
+                new_segs = [new_newline] + (
+                    [WhitespaceSegment(desired_indent)] if desired_indent else []
+                )
                 # There isn't a whitespace segment either. We need to insert one.
                 # Do we have an anchor?
-                new_indent = WhitespaceSegment(desired_indent)
                 if not before and not after:  # pragma: no cover
                     raise NotImplementedError(
                         "Not set up to handle empty points in this "
@@ -554,10 +559,7 @@ class ReflowPoint(ReflowElement):
                         if before.is_type("placeholder")
                         else before.raw
                     )
-                    fix = LintFix.create_before(
-                        before,
-                        [new_newline, new_indent],
-                    )
+                    fix = LintFix.create_before(before, new_segs)
                     description = description or (
                         "Expected line break and "
                         f"{_indent_description(desired_indent)} "
@@ -570,23 +572,19 @@ class ReflowPoint(ReflowElement):
                         if after.is_type("placeholder")
                         else after.raw
                     )
-                    fix = LintFix.create_after(
-                        after,
-                        [new_newline, new_indent],
-                    )
+                    fix = LintFix.create_after(after, new_segs)
                     description = description or (
                         "Expected line break and "
                         f"{_indent_description(desired_indent)} "
                         f"after {after_raw!r}."
                     )
-                new_point = ReflowPoint((new_newline, new_indent))
+                new_point = ReflowPoint(tuple(new_segs))
                 anchor = before
             else:
                 # There is whitespace. Coerce it to the right indent and add
                 # a newline _before_. In the edge case that we're coercing to
                 # _no indent_, edit existing indent to be the newline and leave
                 # it there.
-                new_segs: List[RawSegment]
                 if desired_indent == "":
                     new_segs = [new_newline]
                 else:
diff --git a/src/sqlfluff/utils/reflow/reindent.py b/src/sqlfluff/utils/reflow/reindent.py
index c2ede8040..a93de10d8 100644
--- a/src/sqlfluff/utils/reflow/reindent.py
+++ b/src/sqlfluff/utils/reflow/reindent.py
@@ -336,16 +336,32 @@ def _revise_templated_lines(lines: List[_IndentLine], elements: ReflowSequenceTy
                 # strangely.
                 continue
 
-            for seg in elements[first_point_idx].segments[::-1]:
-                if seg.is_type("indent"):
-                    # If it's the one straight away, after a block_end or
-                    # block_mid, skip it. We know this because it will have
-                    # block_uuid.
-                    if cast(Indent, seg).block_uuid:
-                        continue
-                    # Minus because we're going backward.
-                    indent_balance -= cast(Indent, seg).indent_val
-                    steps.add(indent_balance)
+            for i in range(first_point_idx, 0, -1):
+                if isinstance(elements[i], ReflowPoint):
+                    for seg in elements[i].segments[::-1]:
+                        if seg.is_type("indent"):
+                            # If it's the one straight away, after a block_end or
+                            # block_mid, skip it. We know this because it will have
+                            # block_uuid.
+                            if cast(Indent, seg).block_uuid:
+                                continue
+                            # Minus because we're going backward.
+                            indent_balance -= cast(Indent, seg).indent_val
+                            steps.add(indent_balance)
+                # if it's anything other than a blank placeholder, break.
+                # NOTE: We still need the forward version of this.
+                elif not elements[i].segments[0].is_type("placeholder"):
+                    break
+                elif cast(TemplateSegment, elements[i].segments[0]).block_type not in (
+                    "block_start",
+                    "block_end",
+                    "skipped_source",
+                    "block_mid",
+                ):
+                    # Recreating this condition is hard, but we shouldn't allow any
+                    # rendered content here.
+                    break  # pragma: no cover
+
             # Run forward through the post point.
             indent_balance = line.initial_indent_balance
             last_point_idx = line.indent_points[-1].idx
@@ -499,10 +515,8 @@ def _revise_templated_lines(lines: List[_IndentLine], elements: ReflowSequenceTy
                     net_balance += ip.indent_impulse
 
         # Evaluate options.
-        # NOTE: We don't use the _last_ option, because it tends to be trailing
-        # and have strange effects.
         reflow_logger.debug("    Options: %s", options)
-        overlap = set.intersection(*options[:-1])
+        overlap = set.intersection(*options)
         reflow_logger.debug("    Simple Overlap: %s", overlap)
         # Remove any options above the limit option.
         # We minus one from the limit, because if it comes into effect
@@ -1843,12 +1857,10 @@ def _fix_long_line_with_comment(
         prev_elems = []
         anchor = first_seg
     else:
-        new_point = ReflowPoint(
-            (
-                NewlineSegment(),
-                WhitespaceSegment(current_indent),
-            )
-        )
+        new_segments: Tuple[RawSegment, ...] = (NewlineSegment(),)
+        if current_indent:
+            new_segments += (WhitespaceSegment(current_indent),)
+        new_point = ReflowPoint(new_segments)
         prev_elems = elements[: last_indent_idx + 1]
         anchor = elements[last_indent_idx + 1].segments[0]
 


Test patch:
diff --git a/test/dialects/ansi_test.py b/test/dialects/ansi_test.py
index 933cdfc37..2a7d45cde 100644
--- a/test/dialects/ansi_test.py
+++ b/test/dialects/ansi_test.py
@@ -196,18 +196,18 @@ def test__dialect__ansi_is_whitespace():
         (
             "select field_1 from my_table as alias_1",
             True,
-            (1, 5, 8, 11, 15, 16, 17, 18, 19),
+            (1, 4, 8, 11, 15, 16, 17, 18, 19),
         ),
-        ("select field_1 from my_table as alias_1", False, (1, 5, 8, 11, 15, 16, 17)),
+        ("select field_1 from my_table as alias_1", False, (1, 4, 8, 11, 15, 16, 17)),
         (
             "select field_1 from my_table as alias_1 join foo using (field_1)",
             True,
-            (1, 5, 8, 11, 15, 17, 18, 20, 24, 25, 27, 30, 32, 34, 35, 36, 37),
+            (1, 4, 8, 11, 15, 17, 18, 20, 24, 25, 27, 30, 32, 34, 35, 36, 37),
         ),
         (
             "select field_1 from my_table as alias_1 join foo using (field_1)",
             False,
-            (1, 5, 8, 11, 15, 17, 19, 23, 24, 26, 29, 31, 33, 34, 35),
+            (1, 4, 8, 11, 15, 17, 19, 23, 24, 26, 29, 31, 33, 34, 35),
         ),
     ],
 )
diff --git a/test/fixtures/rules/std_rule_cases/LT02-indent.yml b/test/fixtures/rules/std_rule_cases/LT02-indent.yml
index ddbe6d6de..42db13309 100644
--- a/test/fixtures/rules/std_rule_cases/LT02-indent.yml
+++ b/test/fixtures/rules/std_rule_cases/LT02-indent.yml
@@ -2128,6 +2128,15 @@ test_pass_implicit_where:
     indentation:
       allow_implicit_indents: true
 
+test_pass_templated_join:
+  # See: https://github.com/sqlfluff/sqlfluff/issues/5290
+  pass_str: |
+    select *
+    from a
+    {% if True %}
+        left join b using(x)
+    {% endif %}
+
 test_whitespace_control_issue_5277:
   # https://github.com/sqlfluff/sqlfluff/issues/5277
   fail_str: |
@@ -2152,3 +2161,29 @@ test_whitespace_control_issue_5277:
             {%- endif %}
     )
     select * from a
+
+test_inconsistent_indent:
+  # In specific circumstances the indentation algorithm can behave
+  # unexpectedly, this is a test case to catch one, where the
+  # fix was unexpected.
+  # https://github.com/sqlfluff/sqlfluff/issues/5277
+  fail_str: |
+    WITH x AS (
+        SELECT
+            o.p AS p
+            {% for action in ["a", "b"] %}
+                , n.campaign_count_{{ action }}
+        {% endfor %}
+        FROM o
+    )
+    SELECT * FROM x
+  fix_str: |
+    WITH x AS (
+        SELECT
+            o.p AS p
+            {% for action in ["a", "b"] %}
+                , n.campaign_count_{{ action }}
+            {% endfor %}
+        FROM o
+    )
+    SELECT * FROM x
diff --git a/test/fixtures/templater/jinja_l_metas/001.yml b/test/fixtures/templater/jinja_l_metas/001.yml
index 2bf923877..0ca3ff31e 100644
--- a/test/fixtures/templater/jinja_l_metas/001.yml
+++ b/test/fixtures/templater/jinja_l_metas/001.yml
@@ -27,9 +27,9 @@ file:
               - select_clause_element:
                   column_reference:
                     naked_identifier: SNAPSHOT_DATE
+              - dedent: ''
             - newline: "\n"
             - whitespace: '    '
-            - dedent: ''
             - from_clause:
               - keyword: FROM
               - whitespace: ' '
@@ -70,8 +70,8 @@ file:
               - naked_identifier: cust
               - dot: .
               - naked_identifier: SNAPSHOT_DATE
+          - dedent: ''
           newline: "\n"
-          dedent: ''
           from_clause:
             keyword: FROM
             whitespace: ' '
diff --git a/test/fixtures/templater/jinja_l_metas/002.yml b/test/fixtures/templater/jinja_l_metas/002.yml
index 8f4f0b10a..0b72dfe08 100644
--- a/test/fixtures/templater/jinja_l_metas/002.yml
+++ b/test/fixtures/templater/jinja_l_metas/002.yml
@@ -88,12 +88,12 @@ file:
       - select_clause_element:
           column_reference:
             naked_identifier: campaign_count_c
+      - dedent: ''
     - newline: "\n"
     - whitespace: '    '
     - dedent: ''
     - placeholder: '{% endfor %}'
     - newline: "\n"
-    - dedent: ''
     - from_clause:
       - keyword: FROM
       - newline: "\n"
diff --git a/test/fixtures/templater/jinja_l_metas/003.yml b/test/fixtures/templater/jinja_l_metas/003.yml
index 50bfe46fa..f8a11152f 100644
--- a/test/fixtures/templater/jinja_l_metas/003.yml
+++ b/test/fixtures/templater/jinja_l_metas/003.yml
@@ -8,7 +8,7 @@ file:
           whitespace: '  '
           select_clause_element:
             numeric_literal: '1'
-        dedent: ""
+          dedent: ""
   - newline: "\n"
   - whitespace: '  '
   - placeholder: '{% if false %}'
diff --git a/test/fixtures/templater/jinja_l_metas/004.yml b/test/fixtures/templater/jinja_l_metas/004.yml
index 5aa06542d..ae5f7f56e 100644
--- a/test/fixtures/templater/jinja_l_metas/004.yml
+++ b/test/fixtures/templater/jinja_l_metas/004.yml
@@ -18,7 +18,7 @@ file:
           - binary_operator: +
           - whitespace: ' '
           - numeric_literal: '2'
-      dedent: ""
+        dedent: ""
   newline: "\n"
   whitespace: '  '
   dedent: ''
diff --git a/test/fixtures/templater/jinja_l_metas/005.yml b/test/fixtures/templater/jinja_l_metas/005.yml
index 78b339df8..644bb651f 100644
--- a/test/fixtures/templater/jinja_l_metas/005.yml
+++ b/test/fixtures/templater/jinja_l_metas/005.yml
@@ -49,5 +49,5 @@ file:
       - whitespace: '  '
       - select_clause_element:
           numeric_literal: '4'
-      dedent: ""
+      - dedent: ""
   end_of_file: ""
diff --git a/test/fixtures/templater/jinja_l_metas/006.yml b/test/fixtures/templater/jinja_l_metas/006.yml
index 489d6bb62..20dc94d30 100644
--- a/test/fixtures/templater/jinja_l_metas/006.yml
+++ b/test/fixtures/templater/jinja_l_metas/006.yml
@@ -15,7 +15,7 @@ file:
             - binary_operator: +
             - whitespace: ' '
             - numeric_literal: '1'
-        dedent: ""
+          dedent: ""
   - placeholder: "\n"
   - dedent: ""
   - placeholder: "{%- endif %}"
diff --git a/test/fixtures/templater/jinja_l_metas/007.yml b/test/fixtures/templater/jinja_l_metas/007.yml
index bb417b035..a67cc44bf 100644
--- a/test/fixtures/templater/jinja_l_metas/007.yml
+++ b/test/fixtures/templater/jinja_l_metas/007.yml
@@ -7,10 +7,10 @@ file:
         whitespace: ' '
         select_clause_element:
           numeric_literal: '1'
+        dedent: ""
     - whitespace: ' '
     - placeholder: '{{ " + 2" if false }}'
     - whitespace: ' '
-    - dedent: ""
     - from_clause:
       - keyword: FROM
       - whitespace: ' '
diff --git a/test/fixtures/templater/jinja_l_metas/008.yml b/test/fixtures/templater/jinja_l_metas/008.yml
index d2b8de517..ba6658edc 100644
--- a/test/fixtures/templater/jinja_l_metas/008.yml
+++ b/test/fixtures/templater/jinja_l_metas/008.yml
@@ -13,8 +13,8 @@ file:
               wildcard_expression:
                 wildcard_identifier:
                   star: '*'
+            dedent: ""
           newline: "\n"
-          dedent: ""
           from_clause:
             keyword: FROM
             whitespace: ' '
@@ -45,8 +45,8 @@ file:
               wildcard_expression:
                 wildcard_identifier:
                   star: '*'
+            dedent: ""
           newline: "\n"
-          dedent: ""
           from_clause:
             keyword: FROM
             whitespace: ' '
diff --git a/test/fixtures/templater/jinja_l_metas/009.yml b/test/fixtures/templater/jinja_l_metas/009.yml
index 24aa86d44..d40e2ed4a 100644
--- a/test/fixtures/templater/jinja_l_metas/009.yml
+++ b/test/fixtures/templater/jinja_l_metas/009.yml
@@ -16,8 +16,8 @@ file:
       - comma: ','
       - select_clause_element:
           numeric_literal: '2'
+      - dedent: ""
     - newline: "\n"
-    - dedent: ""
     - from_clause:
       - keyword: FROM
       - whitespace: ' '
diff --git a/test/fixtures/templater/jinja_l_metas/010.yml b/test/fixtures/templater/jinja_l_metas/010.yml
index bd7d91dad..101f234bb 100644
--- a/test/fixtures/templater/jinja_l_metas/010.yml
+++ b/test/fixtures/templater/jinja_l_metas/010.yml
@@ -16,7 +16,7 @@ file:
           whitespace: ' '
           select_clause_element:
             numeric_literal: '2'
-        dedent: ''
+          dedent: ''
   - statement_terminator: ;
   - newline: "\n"
   - newline: "\n"
diff --git a/test/utils/reflow/depthmap_test.py b/test/utils/reflow/depthmap_test.py
index 34ff905b3..bcc6c1f46 100644
--- a/test/utils/reflow/depthmap_test.py
+++ b/test/utils/reflow/depthmap_test.py
@@ -26,7 +26,7 @@ def test_reflow_depthmap_from_parent(default_config):
         4,
         4,
         5,
-        3,
+        4,
         1,
     ]
     # Check they all share the same first three hash and
diff --git a/test/utils/reflow/sequence_test.py b/test/utils/reflow/sequence_test.py
index 242017dea..1094c10cf 100644
--- a/test/utils/reflow/sequence_test.py
+++ b/test/utils/reflow/sequence_test.py
@@ -221,12 +221,15 @@ def test_reflow_sequence_from_around_target_non_raw(default_config, caplog):
 @pytest.mark.parametrize(
     "raw_sql,filter,delete_indices,edit_indices",
     [
+        # NOTE: These tests rely on the position of code *and non code* elements
+        # in the parsed sequence, so may need to be altered if the parse structure
+        # changes.
         ("SELECT      \n   4", "all", [2], []),
         ("SELECT \n 4, \n 6", "all", [2, 7], []),
         ("SELECT \n 4, \n 6  ", "all", [2, 7, 12], []),
-        ("SELECT \n 4, 5,  6   ,    7 \n 6  ", "newline", [2, 16, 21], []),
+        ("SELECT \n 4, 5,  6   ,    7 \n 6  ", "newline", [2, 17, 21], []),
         ("SELECT \n 4, 5,  6   ,    7 \n 6  ", "inline", [12], [10, 14]),
-        ("SELECT \n 4, 5,  6    ,    7 \n 6  ", "all", [2, 12, 16, 21], [10, 14]),
+        ("SELECT \n 4, 5,  6    ,    7 \n 6  ", "all", [2, 12, 17, 21], [10, 14]),
     ],
 )
 def test_reflow_sequence_respace_filter(