sqlfluff / sqlfluff
Showing 1 of 2 files from the diff.

@@ -1,7 +1,8 @@
Loading
1 1
"""Implementation of Rule L015."""
2 2
from typing import Optional
3 3
4 -
from sqlfluff.core.rules import BaseRule, LintResult, RuleContext
4 +
from sqlfluff.core.parser import KeywordSegment, WhitespaceSegment
5 +
from sqlfluff.core.rules import BaseRule, LintFix, LintResult, RuleContext
5 6
from sqlfluff.core.rules.crawlers import SegmentSeekerCrawler
6 7
from sqlfluff.core.rules.doc_decorators import document_fix_compatible, document_groups
7 8
from sqlfluff.utils.functional import sp, FunctionalContext
@@ -35,46 +36,78 @@
Loading
35 36
    """
36 37
37 38
    groups = ("all", "core")
38 -
    crawl_behaviour = SegmentSeekerCrawler({"select_clause"})
39 +
    crawl_behaviour = SegmentSeekerCrawler({"select_clause", "function"})
39 40
40 41
    def _eval(self, context: RuleContext) -> Optional[LintResult]:
41 42
        """Looking for DISTINCT before a bracket.
42 43
43 44
        Look for DISTINCT keyword immediately followed by open parenthesis.
44 45
        """
45 -
        # We trigger on `select_clause` and look for `select_clause_modifier`
46 -
        assert context.segment.is_type("select_clause")
46 +
        seq = None
47 +
        anchor = None
47 48
        children = FunctionalContext(context).segment.children()
48 -
        modifier = children.select(sp.is_type("select_clause_modifier"))
49 -
        first_element = children.select(sp.is_type("select_clause_element")).first()
50 -
        if not modifier or not first_element:
51 -
            return None
52 -
        # is the first element only an expression with only brackets?
53 -
        expression = (
54 -
            first_element.children(sp.is_type("expression")).first() or first_element
55 -
        )
56 -
        bracketed = expression.children(sp.is_type("bracketed")).first()
57 -
        if bracketed:
58 -
            # If there's nothing else in the expression, remove the brackets.
59 -
            if len(expression[0].segments) == 1:
60 -
                # Remove the brackets and strip any meta segments.
61 -
                anchor = bracketed.get()
62 -
                assert anchor
63 -
                seq = ReflowSequence.from_around_target(
64 -
                    anchor,
65 -
                    context.parent_stack[0],
66 -
                    config=context.config,
67 -
                    sides="before",
68 -
                ).replace(anchor, self.filter_meta(anchor.segments)[1:-1])
69 -
            # Otherwise, still make sure there's a space after the DISTINCT.
70 -
            else:
71 -
                anchor = modifier[0]
72 -
                seq = ReflowSequence.from_around_target(
73 -
                    modifier[0],
74 -
                    context.parent_stack[0],
75 -
                    config=context.config,
76 -
                    sides="after",
77 -
                )
49 +
        if context.segment.is_type("select_clause"):
50 +
            # Look for `select_clause_modifier`
51 +
            modifier = children.select(sp.is_type("select_clause_modifier"))
52 +
            first_element = children.select(sp.is_type("select_clause_element")).first()
53 +
            expression = (
54 +
                first_element.children(sp.is_type("expression")).first()
55 +
                or first_element
56 +
            )
57 +
            bracketed = expression.children(sp.is_type("bracketed")).first()
58 +
            # is the first element only an expression with only brackets?
59 +
            if modifier and bracketed:
60 +
                # If there's nothing else in the expression, remove the brackets.
61 +
                if len(expression[0].segments) == 1:
62 +
                    anchor, seq = self._remove_unneeded_brackets(context, bracketed)
63 +
                # Otherwise, still make sure there's a space after the DISTINCT.
64 +
                else:
65 +
                    anchor = modifier[0]
66 +
                    seq = ReflowSequence.from_around_target(
67 +
                        modifier[0],
68 +
                        context.parent_stack[0],
69 +
                        config=context.config,
70 +
                        sides="after",
71 +
                    )
72 +
        elif context.segment.is_type("function"):
73 +
            # Look for a function call DISTINCT() whose parent is an expression
74 +
            # with a single child.
75 +
            anchor = context.parent_stack[-1]
76 +
            if not anchor.is_type("expression") or len(anchor.segments) != 1:
77 +
                return None
78 +
            function_name = children.select(sp.is_type("function_name")).first()
79 +
            bracketed = children.first(sp.is_type("bracketed"))
80 +
            if (
81 +
                not function_name
82 +
                or function_name[0].raw_upper != "DISTINCT"
83 +
                or not bracketed
84 +
            ):
85 +
                return None
86 +
            # Using ReflowSequence here creates an unneeded space between CONCAT
87 +
            # and "(" in the test case test_fail_distinct_concat_inside_count:
88 +
            #    SELECT COUNT(DISTINCT(CONCAT(col1, '-', col2, '-', col3)))
89 +
            #
90 +
            # seq = ReflowSequence.from_around_target(
91 +
            #     anchor,
92 +
            #     context.parent_stack[0],
93 +
            #     config=context.config,
94 +
            # ).replace(
95 +
            #     anchor,
96 +
            #     (KeywordSegment("DISTINCT"), WhitespaceSegment())
97 +
            #     + self.filter_meta(bracketed[0].segments)[1:-1],
98 +
            # )
99 +
            # Do this until we have a fix for the above.
100 +
            return LintResult(
101 +
                anchor=anchor,
102 +
                fixes=[
103 +
                    LintFix.replace(
104 +
                        anchor,
105 +
                        (KeywordSegment("DISTINCT"), WhitespaceSegment())
106 +
                        + self.filter_meta(bracketed[0].segments)[1:-1],
107 +
                    )
108 +
                ],
109 +
            )
110 +
        if seq and anchor:
78 111
            # Get modifications.
79 112
            fixes = seq.respace().get_fixes()
80 113
            if fixes:
@@ -83,3 +116,15 @@
Loading
83 116
                    fixes=fixes,
84 117
                )
85 118
        return None
119 +
120 +
    def _remove_unneeded_brackets(self, context, bracketed):
121 +
        # Remove the brackets and strip any meta segments.
122 +
        anchor = bracketed.get()
123 +
        assert anchor
124 +
        seq = ReflowSequence.from_around_target(
125 +
            anchor,
126 +
            context.parent_stack[0],
127 +
            config=context.config,
128 +
            sides="before",
129 +
        ).replace(anchor, self.filter_meta(anchor.segments)[1:-1])
130 +
        return anchor, seq
Files Coverage
src/sqlfluff 100.00%
Project Totals (195 files) 100.00%
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading