Repository: sqlfluff/sqlfluff

Issue:
"sqlfluff fix" improperly removes indentation of "DISTINCT <column list>" in query
Input:
```
SELECT
    -- The GDPR safe_user table contains duplicates, so we use DISTINCT.
    DISTINCT user_id
FROM
    safe_user
```

Result when processed with `sqlfluff fix --rules L003 test.sql`:
```
SELECT
-- The GDPR safe_user table contains duplicates, so we use DISTINCT.
DISTINCT user_id
FROM
    safe_user
```

I would prefer the line `DISTINCT user_id` to remain indented. I think one contibuting factor to this behavior is the oddity we've discussed elsewhere, that `DISTINCT` is a record-level, not column-level concept. Another reasonable output for `sqlfluff fix` could be to move `DISTINCT` up with the `SELECT, e.g.:

```
SELECT DISTINCT
    user_id
FROM
    safe_user
```

This may even be _better_ since it emphasizes the actual behavior of the query. Maybe this would be considered a different fix than `L003`, though?

Discussion:

Patch:
diff --git a/src/sqlfluff/core/parser/segments/base.py b/src/sqlfluff/core/parser/segments/base.py
index d4bf8fd01..700805b61 100644
--- a/src/sqlfluff/core/parser/segments/base.py
+++ b/src/sqlfluff/core/parser/segments/base.py
@@ -12,7 +12,7 @@ from io import StringIO
 import copy
 from benchit import BenchIt
 from cached_property import cached_property
-from typing import Optional, List, Tuple, NamedTuple, Iterator
+from typing import Any, Callable, Optional, List, Tuple, NamedTuple, Iterator
 import logging
 
 from sqlfluff.core.string_helpers import (
@@ -661,6 +661,28 @@ class BaseSegment:
                 buff.append(seg)
         return buff
 
+    def select_children(
+        self,
+        start_seg: Optional["BaseSegment"] = None,
+        stop_seg: Optional["BaseSegment"] = None,
+        select_if: Optional[Callable[["BaseSegment"], Any]] = None,
+        loop_while: Optional[Callable[["BaseSegment"], Any]] = None,
+    ):
+        """Retrieve subset of children based on range and filters.
+
+        Often useful by linter rules when generating fixes, e.g. to find
+        whitespace segments between two already known segments.
+        """
+        start_index = self.segments.index(start_seg) if start_seg else -1
+        stop_index = self.segments.index(stop_seg) if stop_seg else len(self.segments)
+        buff = []
+        for seg in self.segments[start_index + 1 : stop_index]:
+            if loop_while and not loop_while(seg):
+                break
+            if not select_if or select_if(seg):
+                buff.append(seg)
+        return buff
+
     def recursive_crawl(self, *seg_type):
         """Recursively crawl for segments of a given type.
 
diff --git a/src/sqlfluff/core/rules/std/L041.py b/src/sqlfluff/core/rules/std/L041.py
new file mode 100644
index 000000000..076ef03c9
--- /dev/null
+++ b/src/sqlfluff/core/rules/std/L041.py
@@ -0,0 +1,78 @@
+"""Implementation of Rule L040."""
+
+from sqlfluff.core.rules.base import BaseCrawler, LintFix, LintResult
+from sqlfluff.core.rules.doc_decorators import document_fix_compatible
+
+
+@document_fix_compatible
+class Rule_L041(BaseCrawler):
+    """SELECT clause modifiers such as DISTINCT must be on the same line as SELECT.
+
+    | **Anti-pattern**
+
+    .. code-block:: sql
+
+        select
+            distinct a,
+            b
+        from x
+
+
+    | **Best practice**
+
+    .. code-block:: sql
+
+        select distinct
+            a,
+            b
+        from x
+
+    """
+
+    def _eval(self, segment, **kwargs):
+        """Select clause modifiers must appear on same line as SELECT."""
+        if segment.is_type("select_clause"):
+            # Does the select clause have modifiers?
+            select_modifier = segment.get_child("select_clause_modifier")
+            if not select_modifier:
+                return None  # No. We're done.
+            select_modifier_idx = segment.segments.index(select_modifier)
+
+            # Does the select clause contain a newline?
+            newline = segment.get_child("newline")
+            if not newline:
+                return None  # No. We're done.
+            newline_idx = segment.segments.index(newline)
+
+            # Is there a newline before the select modifier?
+            if newline_idx > select_modifier_idx:
+                return None  # No, we're done.
+
+            # Yes to all the above. We found an issue.
+
+            # E.g.: " DISTINCT\n"
+            replace_newline_with = [
+                self.make_whitespace(raw=" ", pos_marker=newline.pos_marker),
+                select_modifier,
+                self.make_newline(pos_marker=newline.pos_marker),
+            ]
+            fixes = [
+                # E.g. "\n" -> " DISTINCT\n.
+                LintFix("edit", newline, replace_newline_with),
+                # E.g. "DISTINCT" -> X
+                LintFix("delete", select_modifier),
+            ]
+
+            # E.g. " " after "DISTINCT"
+            ws_to_delete = segment.select_children(
+                start_seg=select_modifier,
+                select_if=lambda s: s.is_type("whitespace"),
+                loop_while=lambda s: s.is_type("whitespace") or s.is_meta,
+            )
+
+            # E.g. " " -> X
+            fixes += [LintFix("delete", ws) for ws in ws_to_delete]
+            return LintResult(
+                anchor=segment,
+                fixes=fixes,
+            )


Test patch:
diff --git a/test/core/rules/test_cases/L041.yml b/test/core/rules/test_cases/L041.yml
new file mode 100644
index 000000000..53a01910e
--- /dev/null
+++ b/test/core/rules/test_cases/L041.yml
@@ -0,0 +1,32 @@
+rule: L041
+
+test_1:
+  fail_str: |
+    SELECT
+        DISTINCT user_id,
+        list_id
+    FROM
+        safe_user
+  fix_str: |
+    SELECT DISTINCT
+        user_id,
+        list_id
+    FROM
+        safe_user
+
+test_2:
+  fail_str: |
+    SELECT
+        -- The table contains duplicates, so we use DISTINCT.
+        DISTINCT user_id
+    FROM
+        safe_user
+  fix_str: |
+    SELECT DISTINCT
+        -- The table contains duplicates, so we use DISTINCT.
+        user_id
+    FROM
+        safe_user
+
+test_3:
+  pass_str: SELECT DISTINCT user_id FROM safe_user