Repository: sqlfluff/sqlfluff

Issue:
Add PascalCase capitalisation_policy
[L014](https://docs.sqlfluff.com/en/stable/rules.html#sqlfluff.core.rules.std.Rule_L014) (Inconsistent capitalisation of unquoted identifiers) needs a PascalCase capitalisation_policy option, implemented as follows:

- Unquoted identifier does not contain non-alphanumeric characters
- Unquoted identifier begins with a capital letter

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bee43f58c..27af0fea3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### Added
-
+- `pascal` (PascalCase) `capitalisation_policy` option for L014 (unquoted identifiers)
+- `only_aliases` configuration option for L014 (unquoted identifers)
 - Dialects now have more advanced dependency options to allow less repetition
   between related dialects. The methods `get_segment` and `get_grammar` can be
   used on unexpanded grammars to access elements of the parent grammars.
@@ -16,7 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 - Add support for `ALTER USER` commands in Snowflake dialect.
 - Added describe statement to ANSI dialect
-
+- Renamed `capitalisation_policy` to `extended_capitalisation_policy` for L014 to reflect the fact that it now
+  accepts more options (`pascal`) than regular `capitalisation_policy` still used by L010 and others.
+- Replaced `only_aliases` config with `unquoted_identifiers_policy` and added it to rule L014 in addition to L029.
 - Renamed the BaseCrawler class to BaseRule. This is the base class for all
   rules. This is a breaking change for any custom rules that have been added
   via plugins or by forking the SQLFluff repo.
@@ -29,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   only imported as needed at runtime. All dialects should now be accessed
   using the selector methods in `sqlfluff.core.dialects` rather than importing
   from `sqlfluff.dialects` directly.
-  
+
 ## [0.4.1] - 2021-02-25
 ### Added
 
diff --git a/src/sqlfluff/core/default_config.cfg b/src/sqlfluff/core/default_config.cfg
index 669a11ad6..b83f9195f 100644
--- a/src/sqlfluff/core/default_config.cfg
+++ b/src/sqlfluff/core/default_config.cfg
@@ -40,7 +40,7 @@ indent_unit = space
 comma_style = trailing
 allow_scalar = True
 single_table_references = consistent
-only_aliases = True
+unquoted_identifiers_policy = all
 
 # Some rules have their own specific config.
 [sqlfluff:rules:L003]
@@ -50,11 +50,14 @@ lint_templated_tokens = True
 capitalisation_policy = consistent
 
 [sqlfluff:rules:L014]  # Unquoted identifiers
-capitalisation_policy = consistent
+extended_capitalisation_policy = consistent
 
 [sqlfluff:rules:L016]
 ignore_comment_lines = False
 
+[sqlfluff:rules:L029]  # Keyword identifiers
+unquoted_identifiers_policy = aliases
+
 [sqlfluff:rules:L030]  # Function names
 capitalisation_policy = consistent
 
diff --git a/src/sqlfluff/core/rules/config_info.py b/src/sqlfluff/core/rules/config_info.py
index 70d4044dd..701c5516e 100644
--- a/src/sqlfluff/core/rules/config_info.py
+++ b/src/sqlfluff/core/rules/config_info.py
@@ -42,17 +42,20 @@ STANDARD_CONFIG_INFO_DICT = {
         "validation": ["consistent", "qualified", "unqualified"],
         "definition": "The expectation for references in single-table select",
     },
-    "only_aliases": {
-        "validation": [True, False],
-        "definition": (
-            "Whether or not to flags violations for only alias expressions "
-            "or all unquoted identifiers"
-        ),
+    "unquoted_identifiers_policy": {
+        "validation": ["all", "aliases", "column_aliases"],
+        "definition": "Types of unquoted identifiers to flag violations for",
     },
     "capitalisation_policy": {
         "validation": ["consistent", "upper", "lower", "capitalise"],
         "definition": "The capitalisation policy to enforce",
     },
+    "extended_capitalisation_policy": {
+        "validation": ["consistent", "upper", "lower", "pascal", "capitalise"],
+        "definition": (
+            "The capitalisation policy to enforce, extended with PascalCase. This is separate from capitalisation_policy as it should not be applied to keywords."
+        ),
+    },
     "lint_templated_tokens": {
         "validation": [True, False],
         "definition": (
diff --git a/src/sqlfluff/core/rules/std/L010.py b/src/sqlfluff/core/rules/std/L010.py
index dabeb2a40..cb9444076 100644
--- a/src/sqlfluff/core/rules/std/L010.py
+++ b/src/sqlfluff/core/rules/std/L010.py
@@ -1,7 +1,9 @@
 """Implementation of Rule L010."""
 
+import re
 from typing import Tuple, List
 from sqlfluff.core.rules.base import BaseRule, LintResult, LintFix
+from sqlfluff.core.rules.config_info import get_config_info
 from sqlfluff.core.rules.doc_decorators import (
     document_fix_compatible,
     document_configuration,
@@ -45,99 +47,134 @@ class Rule_L010(BaseRule):
     ]
     config_keywords = ["capitalisation_policy"]
 
-    def _eval(self, segment, memory, **kwargs):
+    def _eval(self, segment, memory, parent_stack, **kwargs):
         """Inconsistent capitalisation of keywords.
 
-        We use the `memory` feature here to keep track of
-        what we've seen in the past.
+        We use the `memory` feature here to keep track of cases known to be
+        INconsistent with what we've seen so far as well as the top choice
+        for what the possible case is.
 
         """
-        cases_seen = memory.get("cases_seen", set())
-
-        if ("type", segment.type) in self._target_elems or (
-            "name",
-            segment.name,
-        ) in self._target_elems:
-            raw = segment.raw
-            uc = raw.upper()
-            lc = raw.lower()
-            cap = raw.capitalize()
-            seen_case = None
-            if uc == lc:
-                # Caseless
-                pass
-            elif raw == uc:
-                seen_case = "upper"
-            elif raw == lc:
-                seen_case = "lower"
-            elif raw == cap:
-                seen_case = "capitalise"
-            else:
-                seen_case = "inconsistent"
-
-            # NOTE: We'll only add to cases_seen if we DON'T
-            # also raise an error, so that we can focus in.
-
-            def make_replacement(seg, policy):
-                """Make a replacement segment, based on seen capitalisation."""
-                if policy == "lower":
-                    new_raw = seg.raw.lower()
-                elif policy == "upper":
-                    new_raw = seg.raw.upper()
-                elif policy == "capitalise":
-                    new_raw = seg.raw.capitalize()
-                elif policy == "consistent":
-                    # The only case we DON'T allow here is "inconsistent",
-                    # because it doesn't actually help us.
-                    filtered_cases_seen = [c for c in cases_seen if c != "inconsistent"]
-                    if filtered_cases_seen:
-                        # Get an element from what we've already seen.
-                        return make_replacement(seg, list(filtered_cases_seen)[0])
-                    else:
-                        # If we haven't seen anything yet, then let's default
-                        # to upper
-                        return make_replacement(seg, "upper")
-                # Make a new class and return it.
-                return seg.__class__(raw=new_raw, pos_marker=seg.pos_marker)
-
-            if not seen_case:
-                # Skip this if we haven't seen anything good.
-                # No need to update memory
-                return LintResult(memory=memory)
-            elif (
-                # Are we required to be consistent? (and this is inconsistent?)
-                (
-                    self.capitalisation_policy == "consistent"
-                    and (
-                        # Either because we've seen multiple
-                        (cases_seen and seen_case not in cases_seen)
-                        # Or just because this one is inconsistent internally
-                        or seen_case == "inconsistent"
-                    )
+        # Skip if not an element of the specified type/name
+        if (("type", segment.type) not in self._target_elems) and (
+            ("name", segment.name) not in self._target_elems
+        ):
+            return LintResult(memory=memory)
+
+        # Get the capitalisation policy configuration
+        cap_policy_name = next(
+            k for k in self.config_keywords if k.endswith("capitalisation_policy")
+        )
+        cap_policy = getattr(self, cap_policy_name)
+        cap_policy_opts = [
+            opt
+            for opt in get_config_info()[cap_policy_name]["validation"]
+            if opt != "consistent"
+        ]
+        self.logger.debug(
+            f"Selected '{cap_policy_name}': '{cap_policy}' from options "
+            f"{cap_policy_opts}"
+        )
+
+        refuted_cases = memory.get("refuted_cases", set())
+
+        # Which cases are definitely inconsistent with the segment?
+        if segment.raw[0] != segment.raw[0].upper():
+            refuted_cases.update(["upper", "capitalise", "pascal"])
+            if segment.raw != segment.raw.lower():
+                refuted_cases.update(["lower"])
+        else:
+            refuted_cases.update(["lower"])
+            if segment.raw != segment.raw.upper():
+                refuted_cases.update(["upper"])
+            if segment.raw != segment.raw.capitalize():
+                refuted_cases.update(["capitalise"])
+            if not segment.raw.isalnum():
+                refuted_cases.update(["pascal"])
+
+        # Update the memory
+        memory["refuted_cases"] = refuted_cases
+
+        self.logger.debug(
+            f"Refuted cases after segment '{segment.raw}': {refuted_cases}"
+        )
+
+        # Skip if no inconsistencies, otherwise compute a concrete policy
+        # to convert to.
+        if cap_policy == "consistent":
+            possible_cases = [c for c in cap_policy_opts if c not in refuted_cases]
+            self.logger.debug(
+                f"Possible cases after segment '{segment.raw}': {possible_cases}"
+            )
+            if possible_cases:
+                # Save the latest possible case and skip
+                memory["latest_possible_case"] = possible_cases[0]
+                self.logger.debug(
+                    f"Consistent capitalization, returning with memory: {memory}"
                 )
-                # Are we just required to be specific?
-                # Policy is either upper, lower or capitalize
-                or (
-                    self.capitalisation_policy != "consistent"
-                    and seen_case != self.capitalisation_policy
+                return LintResult(memory=memory)
+            else:
+                concrete_policy = memory.get("latest_possible_case", "upper")
+                self.logger.debug(
+                    f"Getting concrete policy '{concrete_policy}' from memory"
                 )
-            ):
-                return LintResult(
-                    anchor=segment,
-                    fixes=[
-                        LintFix(
-                            "edit",
-                            segment,
-                            make_replacement(segment, self.capitalisation_policy),
-                        )
-                    ],
-                    memory=memory,
+        else:
+            if cap_policy not in refuted_cases:
+                # Skip
+                self.logger.debug(
+                    f"Consistent capitalization {cap_policy}, returning with "
+                    f"memory: {memory}"
                 )
-            else:
-                # Update memory and carry on
-                cases_seen.add(seen_case)
-                memory["cases_seen"] = cases_seen
                 return LintResult(memory=memory)
+            else:
+                concrete_policy = cap_policy
+                self.logger.debug(
+                    f"Setting concrete policy '{concrete_policy}' from cap_policy"
+                )
 
-        # If it's not a keyword just carry on
-        return LintResult(memory=memory)
+        # We need to change the segment to match the concrete policy
+        if concrete_policy in ["upper", "lower", "capitalise"]:
+            if "pascal" in cap_policy_opts and segment.raw[0].isupper():
+                # Insert undescores in transitions between lower and upper
+                fixed_raw = re.sub("(?<=[a-z0-9])(?=[A-Z])", "_", segment.raw)
+                self.logger.debug(f"Inserted underscores: {fixed_raw}")
+            else:
+                fixed_raw = segment.raw
+
+            if concrete_policy == "upper":
+                fixed_raw = fixed_raw.upper()
+            elif concrete_policy == "lower":
+                fixed_raw = fixed_raw.lower()
+            elif concrete_policy == "capitalise":
+                fixed_raw = fixed_raw.capitalize()
+        elif concrete_policy == "pascal":
+            fixed_raw = re.sub(
+                "([^a-zA-Z0-9]+|^)([a-zA-Z0-9])([a-zA-Z0-9]*)",
+                lambda match: match.group(2).upper() + match.group(3).lower(),
+                segment.raw,
+            )
+
+        if fixed_raw == segment.raw:
+            # No need to fix
+            self.logger.debug(
+                f"Capitalisation of segment '{segment.raw}' already OK with policy "
+                f"'{concrete_policy}', returning with memory {memory}"
+            )
+            return LintResult(memory=memory)
+        else:
+            # Return the fixed segment
+            self.logger.debug(
+                f"INCONSISTENT Capitalisation of segment '{segment.raw}', fixing to "
+                f"'{fixed_raw}' and returning with memory {memory}"
+            )
+            return LintResult(
+                anchor=segment,
+                fixes=[
+                    LintFix(
+                        "edit",
+                        segment,
+                        segment.__class__(raw=fixed_raw, pos_marker=segment.pos_marker),
+                    )
+                ],
+                memory=memory,
+            )
diff --git a/src/sqlfluff/core/rules/std/L014.py b/src/sqlfluff/core/rules/std/L014.py
index b57fd842c..cd371996d 100644
--- a/src/sqlfluff/core/rules/std/L014.py
+++ b/src/sqlfluff/core/rules/std/L014.py
@@ -2,6 +2,7 @@
 
 from typing import Tuple, List
 
+from sqlfluff.core.rules.base import LintResult
 from sqlfluff.core.rules.doc_decorators import (
     document_configuration,
     document_fix_compatible,
@@ -9,6 +10,21 @@ from sqlfluff.core.rules.doc_decorators import (
 from sqlfluff.core.rules.std.L010 import Rule_L010
 
 
+def unquoted_ids_policy_applicable(policy, parent_stack):
+    """Does `unquoted_identifiers_policy` apply to this segment?"""
+    if policy == "all":
+        return True
+    is_alias = parent_stack and parent_stack[-1].is_type(
+        "alias_expression", "column_definition", "with_compound_statement"
+    )
+    if policy == "aliases" and is_alias:
+        return True
+    is_inside_from = any(p.is_type("from_clause") for p in parent_stack)
+    if policy == "column_aliases" and is_alias and not is_inside_from:
+        return True
+    return False
+
+
 @document_configuration
 @document_fix_compatible
 class Rule_L014(Rule_L010):
@@ -18,3 +34,12 @@ class Rule_L014(Rule_L010):
     """
 
     _target_elems: List[Tuple[str, str]] = [("name", "naked_identifier")]
+    config_keywords = ["extended_capitalisation_policy", "unquoted_identifiers_policy"]
+
+    def _eval(self, segment, memory, parent_stack, **kwargs):
+        if unquoted_ids_policy_applicable(
+            self.unquoted_identifiers_policy, parent_stack
+        ):
+            return super()._eval(segment, memory, parent_stack, **kwargs)
+        else:
+            return LintResult(memory=memory)
diff --git a/src/sqlfluff/core/rules/std/L029.py b/src/sqlfluff/core/rules/std/L029.py
index 04df3bb26..3f6657d50 100644
--- a/src/sqlfluff/core/rules/std/L029.py
+++ b/src/sqlfluff/core/rules/std/L029.py
@@ -3,6 +3,7 @@
 
 from sqlfluff.core.rules.base import BaseRule, LintResult
 from sqlfluff.core.rules.doc_decorators import document_configuration
+from sqlfluff.core.rules.std.L014 import unquoted_ids_policy_applicable
 
 
 @document_configuration
@@ -29,21 +30,17 @@ class Rule_L029(BaseRule):
 
     """
 
-    config_keywords = ["only_aliases"]
+    config_keywords = ["unquoted_identifiers_policy"]
 
     def _eval(self, segment, dialect, parent_stack, **kwargs):
         """Keywords should not be used as identifiers."""
-        if segment.name == "naked_identifier":
-            # If self.only_aliases is true, we're a bit pickier here
-            if self.only_aliases:
-                # Aliases are ok (either directly, or in column definitions or in with statements)
-                if parent_stack[-1].is_type(
-                    "alias_expression", "column_definition", "with_compound_statement"
-                ):
-                    pass
-                # All other references may not be at the discretion of the developer, so leave them out
-                else:
-                    return None
-            # Actually lint
-            if segment.raw.upper() in dialect.sets("unreserved_keywords"):
-                return LintResult(anchor=segment)
+        if (
+            segment.name == "naked_identifier"
+            and unquoted_ids_policy_applicable(
+                self.unquoted_identifiers_policy, parent_stack
+            )
+            and (segment.raw.upper() in dialect.sets("unreserved_keywords"))
+        ):
+            return LintResult(anchor=segment)
+        else:
+            return None


Test patch:
diff --git a/test/api/simple_test.py b/test/api/simple_test.py
index 352bb7b0a..b80e78816 100644
--- a/test/api/simple_test.py
+++ b/test/api/simple_test.py
@@ -38,6 +38,12 @@ lint_result = [
         "line_pos": 12,
         "description": "Column expression without alias. Use explicit `AS` clause.",
     },
+    {
+        "code": "L010",
+        "line_no": 1,
+        "line_pos": 20,
+        "description": "Inconsistent capitalisation of keywords.",
+    },
     {
         "code": "L039",
         "description": "Unnecessary whitespace found.",
@@ -56,6 +62,12 @@ lint_result = [
         "line_no": 1,
         "line_pos": 27,
     },
+    {
+        "code": "L010",
+        "line_no": 1,
+        "line_pos": 29,
+        "description": "Inconsistent capitalisation of keywords.",
+    },
     {
         "code": "L009",
         "line_no": 1,
diff --git a/test/cli/commands_test.py b/test/cli/commands_test.py
index 175abbbc6..ba93c71e3 100644
--- a/test/cli/commands_test.py
+++ b/test/cli/commands_test.py
@@ -421,7 +421,13 @@ def test__cli__command_parse_serialize_from_stdin(serialize):
                             "line_no": 1,
                             "line_pos": 1,
                             "description": "Inconsistent capitalisation of keywords.",
-                        }
+                        },
+                        {
+                            "code": "L010",
+                            "line_no": 1,
+                            "line_pos": 10,
+                            "description": "Inconsistent capitalisation of keywords.",
+                        },
                     ],
                 }
             ],
diff --git a/test/core/rules/std_test.py b/test/core/rules/std_test.py
index 4094e7554..9fe2382d5 100644
--- a/test/core/rules/std_test.py
+++ b/test/core/rules/std_test.py
@@ -189,9 +189,9 @@ def test__rules__std_L003_process_raw_stack(generate_test_segments):
         {"comma_style": "blah"},
         {"allow_scalar": "blah"},
         {"single_table_references": "blah"},
-        {"only_aliases": "blah"},
+        {"unquoted_identifiers_policy": "blah"},
         {"L010": {"capitalisation_policy": "blah"}},
-        {"L014": {"capitalisation_policy": "blah"}},
+        {"L014": {"extended_capitalisation_policy": "blah"}},
         {"L030": {"capitalisation_policy": "blah"}},
     ],
 )
@@ -223,10 +223,10 @@ def test_rules_configs_are_dynamically_documented():
     class RuleWithConfig(BaseRule):
         """A new rule with configuration."""
 
-        config_keywords = ["comma_style", "only_aliases"]
+        config_keywords = ["comma_style", "lint_templated_tokens"]
 
     assert "comma_style" in RuleWithConfig.__doc__
-    assert "only_aliases" in RuleWithConfig.__doc__
+    assert "lint_templated_tokens" in RuleWithConfig.__doc__
 
     @document_configuration
     class RuleWithoutConfig(BaseRule):
diff --git a/test/core/rules/test_cases/L014.yml b/test/core/rules/test_cases/L014.yml
index 1702a7be7..82822f8bb 100644
--- a/test/core/rules/test_cases/L014.yml
+++ b/test/core/rules/test_cases/L014.yml
@@ -1,20 +1,199 @@
 rule: L014
 
-test_1:
+test_consistency_1:
   pass_str: SELECT a, b
 
-test_2:
+test_consistency_2:
   pass_str: SELECT A, B
 
-test_3:
+test_consistency_3:
   # Test that fixes are consistent
   fail_str: SELECT a,   B
   fix_str: SELECT a,   b
 
-test_4:
+test_consistency_4:
   fail_str: SELECT B,   a
   fix_str: SELECT B,   A
 
-test_5:
+test_consistency_5:
   # Test that NULL is classed as a keyword and not an identifier
   pass_str: SELECT NULL,   a
+
+test_consistency_6:
+  # Single-letter ambiguity: Upper vs Capitalise
+  pass_str: SELECT A, Boo
+
+test_consistency_7:
+  # Single-word ambiguity: Pascal vs Capitalise
+  pass_str: SELECT Apple, Banana_split
+
+test_consistency_8:
+  # Single-word ambiguity: Pascal vs Capitalise
+  pass_str: SELECT AppleFritter, Banana
+
+# PascalCase tests are based on this comment by @alanmcruickshank:
+# https://github.com/sqlfluff/sqlfluff/issues/820#issuecomment-787050507
+test_pascal_1:
+  pass_str: SELECT PascalCase
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_2:
+  pass_str: SELECT Pascalcase
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_3:
+  fail_str: SELECT pascalCase
+  fix_str: SELECT Pascalcase
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_4:
+  pass_str: SELECT PasCalCaSe 
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_5:
+  pass_str: SELECT PAscalcase 
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_6:
+  pass_str: SELECT PASCALCASE 
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_7:
+  fail_str: SELECT pascalcase
+  fix_str: SELECT Pascalcase
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_8:
+  fail_str: SELECT pascal_case
+  fix_str: SELECT PascalCase
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_9:
+  fail_str: SELECT PASCAL_CASE
+  fix_str: SELECT PascalCase
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+
+test_pascal_10:
+  fail_str: SELECT PasCalCase
+  fix_str: SELECT pas_cal_case
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: lower
+
+test_pascal_11:
+  fail_str: SELECT PascalCaseNAME
+  fix_str: SELECT PASCAL_CASE_NAME
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: upper
+
+test_pascal_12:
+  # Pascal vs Capitalise
+  fail_str: SELECT AppleFritter, Banana_split
+  fix_str: SELECT AppleFritter, BananaSplit
+
+test_only_aliases_1:
+  pass_str: SELECT a,   B
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_2:
+  pass_str: SELECT B,   a
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_3:
+  pass_str: SELECT PASCAL_CASE
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: pascal
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_4:
+  pass_str: SELECT UPPER_CASE AS low_case, PascalCase AS low_case
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_5:
+  fail_str: SELECT UPPER_CASE AS PascalCase, PascalCase AS lower_case
+  fix_str: SELECT UPPER_CASE AS PascalCase, PascalCase AS LowerCase
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_6:
+  fail_str: SELECT UPPER_CASE AS PascalCase, PascalCase AS lower_case
+  fix_str: SELECT UPPER_CASE AS PASCAL_CASE, PascalCase AS LOWER_CASE
+  configs:
+    rules:
+      L014:
+        extended_capitalisation_policy: upper
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_7:
+  fail_str: SELECT UPPER_CASE AS low_case, PascalCase AS low_case FROM UPPER_CASE AS UPPER_CASE
+  fix_str: SELECT UPPER_CASE AS low_case, PascalCase AS low_case FROM UPPER_CASE AS upper_case
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_8:
+  fail_str: SELECT UPPER_CASE AS PascalCase, PascalCase AS lower_case FROM lower_case AS lower_case
+  fix_str: SELECT UPPER_CASE AS PascalCase, PascalCase AS LowerCase FROM lower_case AS LowerCase
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: aliases
+
+test_only_aliases_9:
+  pass_str: SELECT UPPER_CASE AS low_case, PascalCase AS low_case FROM UPPER_CASE AS UPPER_CASE
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: column_aliases
+
+test_only_aliases_10:
+  fail_str: SELECT UPPER_CASE AS PascalCase, PascalCase AS lower_case FROM lower_case AS lower_case
+  fix_str: SELECT UPPER_CASE AS PascalCase, PascalCase AS LowerCase FROM lower_case AS lower_case
+  configs:
+    rules:
+      L014:
+        unquoted_identifiers_policy: column_aliases
diff --git a/test/core/rules/test_cases/L029.yml b/test/core/rules/test_cases/L029.yml
index b311d20e1..7aba07819 100644
--- a/test/core/rules/test_cases/L029.yml
+++ b/test/core/rules/test_cases/L029.yml
@@ -10,13 +10,29 @@ test_3:
   fail_str: SELECT 1 as parameter
 
 test_4:
+  fail_str: SELECT x FROM tbl AS parameter
+
+test_5:
   # should pass on default config as not alias
   pass_str: SELECT parameter
 
-test_5:
+test_6:
   fail_str: SELECT parameter
+  configs:
+    rules:
+      L029:
+        unquoted_identifiers_policy: all
+
+test_7:
+  pass_str: SELECT x FROM tbl AS parameter
+  configs:
+    rules:
+      L029:
+        unquoted_identifiers_policy: column_aliases
 
+test_8:
+  fail_str: SELECT x AS date FROM tbl AS parameter
   configs:
     rules:
       L029:
-        only_aliases: false
+        unquoted_identifiers_policy: column_aliases