Repository: alanmcruickshank/sqlfluff

Issue:
Bogus capitalization warning involving SAFE_CAST, NULL, and AS
Input #1 :
```
SELECT
    SAFE_CAST(NULL AS STRING) AS age_label
FROM
    `table`
```

Input #2:
```
SELECT
    NULL AS age_label
FROM
    `table`
```

Either of these files results in a similar warning:
```
$ sqlfluff lint --exclude-rules L001 --dialect bigquery test2.sql
== [test2.sql] FAIL
L:   2 | P:  34 | L014 | Inconsistent capitalisation of unquoted identifiers.
```

I'm not sure if the `SAFE_CAST` is important or not, but am including it in case it's a different parsing pattern, etc.

@alanmcruickshank: I'd be happy to work on some tickets if you'd like to suggest any (this one or others)! February was busy for me, but I'm starting to have some discretionary time again.

Discussion:

Patch:
diff --git a/src/sqlfluff/dialects/dialect_ansi.py b/src/sqlfluff/dialects/dialect_ansi.py
index 5834c196..4432b171 100644
--- a/src/sqlfluff/dialects/dialect_ansi.py
+++ b/src/sqlfluff/dialects/dialect_ansi.py
@@ -89,7 +89,7 @@ ansi_dialect.add(
     # also use a regex to explicitly exclude disallowed keywords.
     NakedIdentifierSegment=ReSegment.make(
         r"[A-Z0-9_]*[A-Z][A-Z0-9_]*", name='identifier', type='naked_identifier',
-        _anti_template=r"^(SELECT|JOIN|ON|USING|CROSS|INNER|LEFT|RIGHT|OUTER|INTERVAL|CASE|FULL)$"),
+        _anti_template=r"^(SELECT|JOIN|ON|USING|CROSS|INNER|LEFT|RIGHT|OUTER|INTERVAL|CASE|FULL|NULL)$"),
     FunctionNameSegment=ReSegment.make(r"[A-Z][A-Z0-9_]*", name='function_name', type='function_name'),
     # Maybe data types should be more restrictive?
     DatatypeIdentifierSegment=ReSegment.make(r"[A-Z][A-Z0-9_]*", name='data_type_identifier', type='data_type_identifier'),
@@ -206,7 +206,10 @@ ansi_dialect.add(
     LiteralGrammar=OneOf(
         Ref('QuotedLiteralSegment'), Ref('NumericLiteralSegment'),
         Ref('BooleanLiteralGrammar'), Ref('QualifiedNumericLiteralSegment'),
-        Ref('IntervalLiteralSegment')
+        Ref('IntervalLiteralSegment'),
+        # NB: Null is included in the literals, because it is a keyword which
+        # can otherwise be easily mistaken for an identifier.
+        Ref('NullKeywordSegment')
     ),
 )
 


Test patch:
diff --git a/test/rules_std_test.py b/test/rules_std_test.py
index 8acee7a1..c5ef40b0 100644
--- a/test/rules_std_test.py
+++ b/test/rules_std_test.py
@@ -84,6 +84,9 @@ def assert_rule_pass_in_sql(code, sql, configs=None):
     # Test that fixes are consistent
     ('L014', 'fail', 'SELECT a,   B', 'SELECT a,   b', None),
     ('L014', 'fail', 'SELECT B,   a', 'SELECT B,   A', None),
+    # Test that NULL is classed as a keyword and not an identifier
+    ('L014', 'pass', 'SELECT NULL,   a', None, None),
+    ('L010', 'fail', 'SELECT null,   a', 'SELECT NULL,   a', None),
     # Test that we don't fail * operators in brackets
     ('L006', 'pass', 'SELECT COUNT(*) FROM tbl\n', None, None),
     # Long lines (with config override)