Repository: AfterShip/clickhouse-sql-parser
Pull request #161 (2025-05-11 20:09:57)

[ Bug Fix ] Preserve DISTINCT keyword when it appears right after SELECT
## Description
This PR fixes a bug in the SQL parser where the `DISTINCT` keyword was being lost during parsing and reconstruction when it appears right after the `SELECT` keyword. For example:

```sql
-- Before: This would lose the DISTINCT keyword
SELECT DISTINCT record_id FROM records
-- Would become:
SELECT record_id FROM records

-- After: The DISTINCT keyword is preserved
SELECT DISTINCT record_id FROM records
-- Remains as:
SELECT DISTINCT record_id FROM records
```

Note that this fix only affects the case where `DISTINCT` appears right after `SELECT`. The parser already correctly handles `DISTINCT` in other contexts, such as in aggregate functions:

```sql
SELECT count(DISTINCT(RECORD_ID)) FROM RECORD_TABLE
```

## Changes
- Added `HasDistinct` field to `SelectQuery` struct
- Modified `parseSelectStmt` to store the DISTINCT information
- Updated `String()` method to include DISTINCT in the output
- Added test cases to verify the fix

## Testing
Added test cases to verify that:
1. `SELECT DISTINCT record_id FROM records` preserves the DISTINCT keyword
2. `SELECT count(DISTINCT(RECORD_ID)) FROM RECORD_TABLE` continues to work as before

coveralls: ## Pull Request Test Coverage Report for [Build 14959292031](https://coveralls.io/builds/73611903)


### Details

* **5** of **5**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.01%**) to **62.818%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/73611903/badge)](https://coveralls.io/builds/73611903) |
| :-- | --: |
| Change from base [Build 14919903301](https://coveralls.io/builds/73586752): |  0.01% |
| Covered Lines: | 7057 |
| Relevant Lines: | 11234 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/parser/ast.go b/parser/ast.go
index c762616..0196511 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -5694,6 +5694,7 @@ type SelectQuery struct {
 	StatementEnd  Pos
 	With          *WithClause
 	Top           *TopClause
+	HasDistinct   bool
 	SelectItems   []*SelectItem
 	From          *FromClause
 	ArrayJoin     *ArrayJoinClause
@@ -5735,6 +5736,9 @@ func (s *SelectQuery) String() string { // nolint: funlen
 		builder.WriteString(" ")
 	}
 	builder.WriteString("SELECT ")
+	if s.HasDistinct {
+		builder.WriteString("DISTINCT ")
+	}
 	if s.Top != nil {
 		builder.WriteString(s.Top.String())
 		builder.WriteString(" ")
diff --git a/parser/parser_query.go b/parser/parser_query.go
index 52b7e22..1b01498 100644
--- a/parser/parser_query.go
+++ b/parser/parser_query.go
@@ -834,7 +834,7 @@ func (p *Parser) parseSelectStmt(pos Pos) (*SelectQuery, error) { // nolint: fun
 		return nil, err
 	}
 	// DISTINCT?
-	_ = p.tryConsumeKeywords(KeywordDistinct)
+	hasDistinct := p.tryConsumeKeywords(KeywordDistinct)
 
 	top, err := p.tryParseTopClause(p.Pos())
 	if err != nil {
@@ -961,6 +961,7 @@ func (p *Parser) parseSelectStmt(pos Pos) (*SelectQuery, error) { // nolint: fun
 		SelectPos:    pos,
 		StatementEnd: statementEnd,
 		Top:          top,
+		HasDistinct:  hasDistinct,
 		SelectItems:  selectItems,
 		From:         from,
 		ArrayJoin:    arrayJoin,


Test patch:
diff --git a/parser/testdata/ddl/output/bug_001.sql.golden.json b/parser/testdata/ddl/output/bug_001.sql.golden.json
index b1390c6..91dc86d 100644
--- a/parser/testdata/ddl/output/bug_001.sql.golden.json
+++ b/parser/testdata/ddl/output/bug_001.sql.golden.json
@@ -57,6 +57,7 @@
         "StatementEnd": 635,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json b/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json
index d3295f9..1a6589e 100644
--- a/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json
+++ b/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json
@@ -82,6 +82,7 @@
         "StatementEnd": 101,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json
index a452379..64e8469 100644
--- a/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json
+++ b/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json
@@ -254,6 +254,7 @@
         "StatementEnd": 537,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json
index 3ca4ad1..265cf9e 100644
--- a/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json
+++ b/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json
@@ -180,6 +180,7 @@
         "StatementEnd": 355,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json
index f2439a2..3a67b47 100644
--- a/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json
+++ b/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json
@@ -151,6 +151,7 @@
         "StatementEnd": 460,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
@@ -245,6 +246,7 @@
                     "StatementEnd": 433,
                     "With": null,
                     "Top": null,
+                    "HasDistinct": false,
                     "SelectItems": [
                       {
                         "Expr": {
diff --git a/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json
index 0755290..71cd813 100644
--- a/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json
+++ b/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json
@@ -147,6 +147,7 @@
         "StatementEnd": 302,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json b/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json
index e8534bc..5d98e14 100644
--- a/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json
+++ b/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json
@@ -254,6 +254,7 @@
         "StatementEnd": 559,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json b/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json
index 49b6526..2c6d458 100644
--- a/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json
+++ b/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json
@@ -103,6 +103,7 @@
         "StatementEnd": 135,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
@@ -233,6 +234,7 @@
         "StatementEnd": 259,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_or_replace.sql.golden.json b/parser/testdata/ddl/output/create_or_replace.sql.golden.json
index 14510d5..b22714e 100644
--- a/parser/testdata/ddl/output/create_or_replace.sql.golden.json
+++ b/parser/testdata/ddl/output/create_or_replace.sql.golden.json
@@ -413,6 +413,7 @@
         "StatementEnd": 508,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_view_basic.sql.golden.json b/parser/testdata/ddl/output/create_view_basic.sql.golden.json
index 877a010..e11f041 100644
--- a/parser/testdata/ddl/output/create_view_basic.sql.golden.json
+++ b/parser/testdata/ddl/output/create_view_basic.sql.golden.json
@@ -90,6 +90,7 @@
         "StatementEnd": 104,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json b/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json
index dfcdb65..4b92b23 100644
--- a/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json
+++ b/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json
@@ -41,6 +41,7 @@
         "StatementEnd": 199,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/dml/output/insert_with_select.sql.golden.json b/parser/testdata/dml/output/insert_with_select.sql.golden.json
index 392c864..d30a816 100644
--- a/parser/testdata/dml/output/insert_with_select.sql.golden.json
+++ b/parser/testdata/dml/output/insert_with_select.sql.golden.json
@@ -24,6 +24,7 @@
       "StatementEnd": 103,
       "With": null,
       "Top": null,
+      "HasDistinct": false,
       "SelectItems": [
         {
           "Expr": {
diff --git a/parser/testdata/query/format/select_with_distinct_keyword.sql b/parser/testdata/query/format/select_with_distinct_keyword.sql
new file mode 100644
index 0000000..fcc818c
--- /dev/null
+++ b/parser/testdata/query/format/select_with_distinct_keyword.sql
@@ -0,0 +1,5 @@
+-- Origin SQL:
+SELECT DISTINCT record_id FROM records 
+
+-- Format SQL:
+SELECT DISTINCT record_id FROM records;
diff --git a/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json b/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json
index f290a1d..53235fa 100644
--- a/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json
+++ b/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 34,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -95,6 +96,7 @@
     "StatementEnd": 336,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/query_with_expr_compare.sql.golden.json b/parser/testdata/query/output/query_with_expr_compare.sql.golden.json
index 13209c4..f7e2cb6 100644
--- a/parser/testdata/query/output/query_with_expr_compare.sql.golden.json
+++ b/parser/testdata/query/output/query_with_expr_compare.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 225,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -108,6 +109,7 @@
               "StatementEnd": 121,
               "With": null,
               "Top": null,
+              "HasDistinct": false,
               "SelectItems": [
                 {
                   "Expr": {
diff --git a/parser/testdata/query/output/select_cast.sql.golden.json b/parser/testdata/query/output/select_cast.sql.golden.json
index 4f79f73..8057c4f 100644
--- a/parser/testdata/query/output/select_cast.sql.golden.json
+++ b/parser/testdata/query/output/select_cast.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 34,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -56,6 +57,7 @@
     "StatementEnd": 70,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -105,6 +107,7 @@
     "StatementEnd": 102,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -164,6 +167,7 @@
     "StatementEnd": 130,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_column_alias_string.sql.golden.json b/parser/testdata/query/output/select_column_alias_string.sql.golden.json
index 30279be..27e4665 100644
--- a/parser/testdata/query/output/select_column_alias_string.sql.golden.json
+++ b/parser/testdata/query/output/select_column_alias_string.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 23,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -42,6 +43,7 @@
     "StatementEnd": 52,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_expr.sql.golden.json b/parser/testdata/query/output/select_expr.sql.golden.json
index 4f0c3fe..41311a4 100644
--- a/parser/testdata/query/output/select_expr.sql.golden.json
+++ b/parser/testdata/query/output/select_expr.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 10,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json b/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json
index 69c9609..11b9cf0 100644
--- a/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json
+++ b/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 35,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -95,6 +96,7 @@
     "StatementEnd": 73,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -197,6 +199,7 @@
     "StatementEnd": 133,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json b/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json
index 2c64363..2f5fb62 100644
--- a/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json
+++ b/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 47,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple.sql.golden.json b/parser/testdata/query/output/select_simple.sql.golden.json
index 7cbc54b..20517a1 100644
--- a/parser/testdata/query/output/select_simple.sql.golden.json
+++ b/parser/testdata/query/output/select_simple.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 277,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_field_alias.sql.golden.json b/parser/testdata/query/output/select_simple_field_alias.sql.golden.json
index 4d8f856..131376b 100644
--- a/parser/testdata/query/output/select_simple_field_alias.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_field_alias.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 48,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json b/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json
index 0734767..cb2906b 100644
--- a/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 66,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json b/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json
index 29b6a99..7b809ca 100644
--- a/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json
@@ -60,6 +60,7 @@
             "StatementEnd": 58,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -135,6 +136,7 @@
       ]
     },
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json b/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json
index 8ea3165..f61fe09 100644
--- a/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 86,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json b/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json
index 01ee86e..b4e1f1e 100644
--- a/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 133,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json b/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json
index 5a57bb5..680ad1f 100644
--- a/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 112,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json b/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json
index caf8dc8..e41cb9e 100644
--- a/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json
@@ -14,6 +14,7 @@
       },
       "WithTies": false
     },
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json b/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json
index 8f4c0ab..760b8fc 100644
--- a/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json
+++ b/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json
@@ -19,6 +19,7 @@
             "StatementEnd": 35,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -84,6 +85,7 @@
             "StatementEnd": 68,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -139,6 +141,7 @@
       ]
     },
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json b/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json
index 1703ae8..43595d1 100644
--- a/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json
+++ b/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 36,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_table_function_with_query.sql.golden.json b/parser/testdata/query/output/select_table_function_with_query.sql.golden.json
index ea48f92..57ed3ee 100644
--- a/parser/testdata/query/output/select_table_function_with_query.sql.golden.json
+++ b/parser/testdata/query/output/select_table_function_with_query.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 125,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -23,6 +24,7 @@
             "StatementEnd": 20,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -119,6 +121,7 @@
                                 "StatementEnd": 105,
                                 "With": null,
                                 "Top": null,
+                                "HasDistinct": false,
                                 "SelectItems": [
                                   {
                                     "Expr": {
diff --git a/parser/testdata/query/output/select_when_condition.sql.golden.json b/parser/testdata/query/output/select_when_condition.sql.golden.json
index 2083a77..53e0cb0 100644
--- a/parser/testdata/query/output/select_when_condition.sql.golden.json
+++ b/parser/testdata/query/output/select_when_condition.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 0,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_distinct.sql.golden.json b/parser/testdata/query/output/select_with_distinct.sql.golden.json
index 4228d75..2bc8d9c 100644
--- a/parser/testdata/query/output/select_with_distinct.sql.golden.json
+++ b/parser/testdata/query/output/select_with_distinct.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 51,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_distinct_keyword.sql.golden.json b/parser/testdata/query/output/select_with_distinct_keyword.sql.golden.json
new file mode 100644
index 0000000..39d0f86
--- /dev/null
+++ b/parser/testdata/query/output/select_with_distinct_keyword.sql.golden.json
@@ -0,0 +1,59 @@
+[
+  {
+    "SelectPos": 0,
+    "StatementEnd": 38,
+    "With": null,
+    "Top": null,
+    "HasDistinct": true,
+    "SelectItems": [
+      {
+        "Expr": {
+          "Name": "record_id",
+          "QuoteType": 1,
+          "NamePos": 16,
+          "NameEnd": 25
+        },
+        "Modifiers": [],
+        "Alias": null
+      }
+    ],
+    "From": {
+      "FromPos": 26,
+      "Expr": {
+        "Table": {
+          "TablePos": 31,
+          "TableEnd": 38,
+          "Alias": null,
+          "Expr": {
+            "Database": null,
+            "Table": {
+              "Name": "records",
+              "QuoteType": 1,
+              "NamePos": 31,
+              "NameEnd": 38
+            }
+          },
+          "HasFinal": false
+        },
+        "StatementEnd": 38,
+        "SampleRatio": null,
+        "HasFinal": false
+      }
+    },
+    "ArrayJoin": null,
+    "Window": null,
+    "Prewhere": null,
+    "Where": null,
+    "GroupBy": null,
+    "WithTotal": false,
+    "Having": null,
+    "OrderBy": null,
+    "LimitBy": null,
+    "Limit": null,
+    "Settings": null,
+    "Format": null,
+    "UnionAll": null,
+    "UnionDistinct": null,
+    "Except": null
+  }
+]
\ No newline at end of file
diff --git a/parser/testdata/query/output/select_with_group_by.sql.golden.json b/parser/testdata/query/output/select_with_group_by.sql.golden.json
index f870eb4..5b6dff7 100644
--- a/parser/testdata/query/output/select_with_group_by.sql.golden.json
+++ b/parser/testdata/query/output/select_with_group_by.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 171,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -222,6 +223,7 @@
     "StatementEnd": 264,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_join_only.sql.golden.json b/parser/testdata/query/output/select_with_join_only.sql.golden.json
index 94b4ea9..81afdc0 100644
--- a/parser/testdata/query/output/select_with_join_only.sql.golden.json
+++ b/parser/testdata/query/output/select_with_join_only.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 17,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json b/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json
index 1baf028..aa91755 100644
--- a/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json
+++ b/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 181,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_left_join.sql.golden.json b/parser/testdata/query/output/select_with_left_join.sql.golden.json
index 2a1f5d8..513d030 100644
--- a/parser/testdata/query/output/select_with_left_join.sql.golden.json
+++ b/parser/testdata/query/output/select_with_left_join.sql.golden.json
@@ -19,6 +19,7 @@
             "StatementEnd": 54,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -67,6 +68,7 @@
             "StatementEnd": 98,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -105,6 +107,7 @@
       ]
     },
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json b/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json
index 4115fa7..7a79c43 100644
--- a/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json
+++ b/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 60,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_multi_join.sql.golden.json b/parser/testdata/query/output/select_with_multi_join.sql.golden.json
index e9fffb9..053010b 100644
--- a/parser/testdata/query/output/select_with_multi_join.sql.golden.json
+++ b/parser/testdata/query/output/select_with_multi_join.sql.golden.json
@@ -19,6 +19,7 @@
             "StatementEnd": 41,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -66,6 +67,7 @@
             "StatementEnd": 81,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -113,6 +115,7 @@
             "StatementEnd": 121,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -150,6 +153,7 @@
       ]
     },
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json b/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json
index 54e418a..57d041d 100644
--- a/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json
+++ b/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 61,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_multi_union.sql.golden.json b/parser/testdata/query/output/select_with_multi_union.sql.golden.json
index 51b7fe9..6009a9c 100644
--- a/parser/testdata/query/output/select_with_multi_union.sql.golden.json
+++ b/parser/testdata/query/output/select_with_multi_union.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 14,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -39,6 +40,7 @@
       "StatementEnd": 39,
       "With": null,
       "Top": null,
+      "HasDistinct": false,
       "SelectItems": [
         {
           "Expr": {
@@ -74,6 +76,7 @@
         "StatementEnd": 64,
         "With": null,
         "Top": null,
+        "HasDistinct": false,
         "SelectItems": [
           {
             "Expr": {
diff --git a/parser/testdata/query/output/select_with_number_field.sql.golden.json b/parser/testdata/query/output/select_with_number_field.sql.golden.json
index 27387eb..61c9f79 100644
--- a/parser/testdata/query/output/select_with_number_field.sql.golden.json
+++ b/parser/testdata/query/output/select_with_number_field.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 53,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_placeholder.sql.golden.json b/parser/testdata/query/output/select_with_placeholder.sql.golden.json
index 219c174..381facb 100644
--- a/parser/testdata/query/output/select_with_placeholder.sql.golden.json
+++ b/parser/testdata/query/output/select_with_placeholder.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 28,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_query_parameter.sql.golden.json b/parser/testdata/query/output/select_with_query_parameter.sql.golden.json
index 0a2fc5a..c9255e1 100644
--- a/parser/testdata/query/output/select_with_query_parameter.sql.golden.json
+++ b/parser/testdata/query/output/select_with_query_parameter.sql.golden.json
@@ -170,6 +170,7 @@
     "StatementEnd": 218,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -314,6 +315,7 @@
     "StatementEnd": 283,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_string_expr.sql.golden.json b/parser/testdata/query/output/select_with_string_expr.sql.golden.json
index 927c682..c560c0c 100644
--- a/parser/testdata/query/output/select_with_string_expr.sql.golden.json
+++ b/parser/testdata/query/output/select_with_string_expr.sql.golden.json
@@ -19,6 +19,7 @@
             "StatementEnd": 28,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -57,6 +58,7 @@
       ]
     },
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_union_distinct.sql.golden.json b/parser/testdata/query/output/select_with_union_distinct.sql.golden.json
index 6129d74..efeff52 100644
--- a/parser/testdata/query/output/select_with_union_distinct.sql.golden.json
+++ b/parser/testdata/query/output/select_with_union_distinct.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 43,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
@@ -62,6 +63,7 @@
       "StatementEnd": 121,
       "With": null,
       "Top": null,
+      "HasDistinct": false,
       "SelectItems": [
         {
           "Expr": {
diff --git a/parser/testdata/query/output/select_with_variable.sql.golden.json b/parser/testdata/query/output/select_with_variable.sql.golden.json
index 37e3ab1..21beaae 100644
--- a/parser/testdata/query/output/select_with_variable.sql.golden.json
+++ b/parser/testdata/query/output/select_with_variable.sql.golden.json
@@ -19,6 +19,7 @@
             "StatementEnd": 27,
             "With": null,
             "Top": null,
+            "HasDistinct": false,
             "SelectItems": [
               {
                 "Expr": {
@@ -57,6 +58,7 @@
       ]
     },
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/output/select_with_window_function.sql.golden.json b/parser/testdata/query/output/select_with_window_function.sql.golden.json
index b4e6f17..e418104 100644
--- a/parser/testdata/query/output/select_with_window_function.sql.golden.json
+++ b/parser/testdata/query/output/select_with_window_function.sql.golden.json
@@ -4,6 +4,7 @@
     "StatementEnd": 348,
     "With": null,
     "Top": null,
+    "HasDistinct": false,
     "SelectItems": [
       {
         "Expr": {
diff --git a/parser/testdata/query/select_with_distinct_keyword.sql b/parser/testdata/query/select_with_distinct_keyword.sql
new file mode 100644
index 0000000..34376b6
--- /dev/null
+++ b/parser/testdata/query/select_with_distinct_keyword.sql
@@ -0,0 +1,1 @@
+SELECT DISTINCT record_id FROM records 
\ No newline at end of file