Repository: reata/sqllineage

Issue:
Dialect='tsql' should return warning when no semicolons are detected
The issue is related to #384 and #159. It's common (although frustrating) practice to **exclude** semicolons in TSQL queries. For example:

```sql
-- sample_query.sql

SELECT CUST_ID, CUST_NAME, CUST_CITY, CUST_COUNTRY
INTO #TEMP_CUSTOMERS
FROM T_CUSTOMERS
WHERE CUST_COUNTRY = 'USA'

SELECT CUST.*
    , SALES.CUM_SALES
INTO #TEMP_FINAL_RESULTS
FROM #TEMP_CUSTOMERS CUST AS CUST
LEFT JOIN T_SALES AS SALES
ON CUST.CUST_ID = SALES.CUST_ID
WHERE SALES.CUST_ID IS NULL

SELECT *
FROM #TEMP_FINAL_RESULTS

```

This causes the `LineageRunner()` to parse the first statement and skip the remaining statements.

```python
from sqllineage.runner import LineageRunner
import csv
import pandas as pd

# read text from sample_query.sql
sql_script = open('sample_query.sql', 'r').read()

# parse sql_script with LineageRunner
parsed_results = LineageRunner(sql_script, dialect="tsql")

# write parsed_results to data frame
df = pd.DataFrame(parsed_results.get_column_lineage())

df.columns = ['Source', 'Target']

print(df)
```

The output is:

Source                |                  Target
----------|----------
`<default>.t_customers.cust_city`  |   `<default>.#temp_customers.cust_city`
`<default>.t_customers.cust_country` | `<default>.#temp_customers.cust_country`
`<default>.t_customers.cust_id`      | `<default>.#temp_customers.cust_id`
`<default>.t_customers.cust_name`   |  `<default>.#temp_customers.cust_name`

The proposal is to return a warning message when `dialect='tsql'`:

```python
parsed_results = LineageRunner(sql_script, dialect="tsql")

"""Warning: No semicolons detected. Consider adding a semicolon after each statement."""
```

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/reata/sqllineage/pull/431?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#431](https://app.codecov.io/gh/reata/sqllineage/pull/431?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (0162194) into [master](https://app.codecov.io/gh/reata/sqllineage/commit/3c70f2956d7bae3284123c3320c9dcbd9152d5a4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3c70f29) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master     #431   +/-   ##
=======================================
  Coverage   99.54%   99.54%           
=======================================
  Files          41       41           
  Lines        1987     1991    +4     
=======================================
+ Hits         1978     1982    +4     
  Misses          9        9           
```


| [Files Changed](https://app.codecov.io/gh/reata/sqllineage/pull/431?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [sqllineage/core/parser/sqlfluff/analyzer.py](https://app.codecov.io/gh/reata/sqllineage/pull/431?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3FsbGluZWFnZS9jb3JlL3BhcnNlci9zcWxmbHVmZi9hbmFseXplci5weQ==) | `100.00% <100.00%> (ø)` | |



Patch:
diff --git a/sqllineage/core/parser/sqlfluff/analyzer.py b/sqllineage/core/parser/sqlfluff/analyzer.py
index 10808e4..5bd42f8 100644
--- a/sqllineage/core/parser/sqlfluff/analyzer.py
+++ b/sqllineage/core/parser/sqlfluff/analyzer.py
@@ -1,3 +1,5 @@
+import warnings
+
 from sqlfluff.core import Linter, SQLLexError, SQLParseError
 
 from sqllineage.core.analyzer import LineageAnalyzer
@@ -39,7 +41,15 @@ class SqlFluffLineageAnalyzer(LineageAnalyzer):
                 statement_segment = top_segment.segments[0]
                 break
             elif top_segment.type == "batch":
-                statement_segment = top_segment.get_child("statement").segments[0]
+                statements = top_segment.get_children("statement")
+                if len(statements) > 1:
+                    warnings.warn(
+                        "SQL statements is not split by semicolon. "
+                        "SQLLineage is not guaranteed to generate correct result under this circumstances.",
+                        SyntaxWarning,
+                        stacklevel=2,
+                    )
+                statement_segment = statements[0].segments[0]
                 break
         if statement_segment is None:
             raise UnsupportedStatementException(


Test patch:
diff --git a/tests/core/test_exception.py b/tests/core/test_exception.py
index 9edf144..49f4b85 100644
--- a/tests/core/test_exception.py
+++ b/tests/core/test_exception.py
@@ -48,3 +48,14 @@ def test_deprecated_warning_in_sqlparse():
         LineageRunner("SELECT * FROM DUAL", dialect="non-validating")._eval()
         assert len(w) == 1
         assert issubclass(w[0].category, DeprecationWarning)
+
+
+def test_syntax_warning_no_semicolon_in_tsql():
+    with warnings.catch_warnings(record=True) as w:
+        LineageRunner(
+            """SELECT * FROM foo
+SELECT * FROM bar""",
+            dialect="tsql",
+        )._eval()
+        assert len(w) == 1
+        assert issubclass(w[0].category, SyntaxWarning)