Repository: sqlfluff/sqlfluff

Issue:
Simple API raises the first parsing violation instead of "all of the violations"
This came up in https://github.com/sqlfluff/sqlfluff/pull/541/files#r526472027

Where the "simple api" `parse(` function only raises the first violation thrown by the function.

To avoid throwing information away, we could define an Exception which takes a list of violations so the API consumer can use the validation errors.

As a reference, in Scala cats there is this structure called "Validated" which accumulates errors https://typelevel.org/cats/datatypes/validated.html

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a177ec28..dc29c7e50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 Contributors:
 - [@GitHub-Username](Link to GitHub profile) ([#PR-Number](Link to PR))
 
+### Changed
+- Better exception handling for the simple parsing API (`sqlfluff.parse`)
+  which now raises an exception which holds all potential parsing issues
+  and prints nicely with more than one issue.
+
 ## [0.6.0a1] - 2021-05-15
 ### Added
 - Lint and fix parallelism using `--parallel` CLI argument
diff --git a/src/sqlfluff/api/__init__.py b/src/sqlfluff/api/__init__.py
index 682cb33f3..ad8beb1bc 100644
--- a/src/sqlfluff/api/__init__.py
+++ b/src/sqlfluff/api/__init__.py
@@ -3,5 +3,5 @@
 # flake8: noqa: F401
 
 # Expose the simple api
-from sqlfluff.api.simple import lint, fix, parse
+from sqlfluff.api.simple import lint, fix, parse, APIParsingError
 from sqlfluff.api.info import list_rules, list_dialects
diff --git a/src/sqlfluff/api/simple.py b/src/sqlfluff/api/simple.py
index 9eb015c3e..152d26c05 100644
--- a/src/sqlfluff/api/simple.py
+++ b/src/sqlfluff/api/simple.py
@@ -3,6 +3,17 @@
 from sqlfluff.core import Linter
 
 
+class APIParsingError(ValueError):
+    """An exception which holds a set of violations."""
+
+    def __init__(self, violations, **kwargs):
+        self.violations = violations
+        self.msg = f"Found {len(violations)} issues while parsing string."
+        for viol in violations:
+            self.msg += f"\n{viol!s}"
+        super().__init__(self.msg, **kwargs)
+
+
 def _unify_str_or_file(sql):
     """Unify string and files in the same format."""
     if not isinstance(sql, str):
@@ -73,7 +84,7 @@ def parse(sql, dialect="ansi"):
     sql = _unify_str_or_file(sql)
     linter = Linter(dialect=dialect)
     parsed = linter.parse_string(sql)
-    # If we encounter any parsing errors, raise the first one we find.
+    # If we encounter any parsing errors, raise them in a combined issue.
     if parsed.violations:
-        raise parsed.violations[0]
+        raise APIParsingError(parsed.violations)
     return parsed
diff --git a/src/sqlfluff/core/linter/__init__.py b/src/sqlfluff/core/linter/__init__.py
index 21e935a16..88a8c2d6e 100644
--- a/src/sqlfluff/core/linter/__init__.py
+++ b/src/sqlfluff/core/linter/__init__.py
@@ -982,10 +982,11 @@ class Linter:
                     # so that we can use the common interface
                     violations.append(
                         SQLParseError(
-                            "Found unparsable section: {0!r}".format(
+                            "Line {0[0]}, Position {0[1]}: Found unparsable section: {1!r}".format(
+                                unparsable.pos_marker.working_loc,
                                 unparsable.raw
                                 if len(unparsable.raw) < 40
-                                else unparsable.raw[:40] + "..."
+                                else unparsable.raw[:40] + "...",
                             ),
                             segment=unparsable,
                         )


Test patch:
diff --git a/test/api/simple_test.py b/test/api/simple_test.py
index 5fe52c3f3..856149361 100644
--- a/test/api/simple_test.py
+++ b/test/api/simple_test.py
@@ -1,6 +1,7 @@
 """Tests for simple use cases of the public api."""
 
 import io
+import pytest
 
 import sqlfluff
 from sqlfluff.core.linter import ParsedString
@@ -156,3 +157,22 @@ def test__api__parse_string():
         tbl_ref.raw for tbl_ref in parsed.tree.recursive_crawl("table_reference")
     ]
     assert tbl_refs == ["myTable"]
+
+
+def test__api__parse_fail():
+    """Basic failure mode of parse functionality."""
+    try:
+        sqlfluff.parse("Select (1 + 2 +++) FROM mytable as blah blah")
+        pytest.fail("sqlfluff.parse should have raised an exception.")
+    except Exception as err:
+        # Check it's the right kind of exception
+        assert isinstance(err, sqlfluff.api.APIParsingError)
+        # Check there are two violations in there.
+        assert len(err.violations) == 2
+        # Check it prints nicely.
+        assert (
+            str(err)
+            == """Found 2 issues while parsing string.
+Line 1, Position 14: Found unparsable section: ' +++'
+Line 1, Position 41: Found unparsable section: 'blah'"""
+        )