Repository: tobymao/sqlglot

Issue:
tsql: table hint "...WITH (HOLDLOCK)..." causing parse error
HOLDLOCK is a table hint and is causing a parse error in the statement below. HOLDLOCK is one of many table hints that applies to many statement types [[DELETE](https://learn.microsoft.com/en-us/sql/t-sql/statements/delete-transact-sql?view=sql-server-ver16) [INSERT](https://learn.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=sql-server-ver16) [SELECT](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver16) [UPDATE](https://learn.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-ver16) [MERGE](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16)]. 

It would be good to parse `WITH  ( <table_hint> [ [ , ] ...n ] )` [ tsql table hints](https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table?view=sql-server-ver16&viewFallbackFrom=azure-sqldw-latest)

```python
sql = """
MERGE mytable WITH (HOLDLOCK) AS T
USING mytable_merge AS S
ON (T.user_id = S.user_id)
WHEN NOT MATCHED BY TARGET
THEN INSERT(c1, c2);
"""
import sqlglot
sqlglot.parse(sql, read="tsql")
```
Produces ParseError `ParseError: Expected table name but got . Line 2, Col: 32`

where 
```python
sql = """
MERGE mytable AS T
USING mytable_merge AS S
ON (T.user_id = S.user_id)
WHEN NOT MATCHED BY TARGET
THEN INSERT(c1, c2);
"""
import sqlglot
sqlglot.parse(sql, read="tsql")
```
Produces the AST.

**Official Documentation**
The `WITH (HOLDLOCK)` is [mentioned here in the MERGE docs](https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16#concurrency-considerations-for-merge)

`HOLDLOCK` is a [table hint ](https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table?view=sql-server-ver16)

> HOLDLOCK
Equivalent to SERIALIZABLE. For more information, see SERIALIZABLE later in this article. HOLDLOCK applies only to the table or view for which it is specified and only for the duration of the transaction defined by the statement that it is used in. HOLDLOCK can't be used in a SELECT statement that includes the FOR BROWSE option.

Table Hints:
> WITH  ( <table_hint> [ [ , ] ...n ] )

> <table_hint> ::=
{ NOEXPAND [ , INDEX ( <index_value> [ , ...n ] ) | INDEX = ( <index_value> ) ]
  | INDEX ( <index_value> [ , ...n ] ) | INDEX = ( <index_value> )
  | FORCESEEK [ ( <index_value> ( <index_column_name> [ , ... ] ) ) ]
  | FORCESCAN
  | HOLDLOCK
  | NOLOCK
  | NOWAIT
  | PAGLOCK
  | READCOMMITTED
  | READCOMMITTEDLOCK
  | READPAST
  | READUNCOMMITTED
  | REPEATABLEREAD
  | ROWLOCK
  | SERIALIZABLE
  | SNAPSHOT
  | SPATIAL_WINDOW_MAX_CELLS = <integer_value>
  | TABLOCK
  | TABLOCKX
  | UPDLOCK
  | XLOCK
}

> <table_hint_limited> ::=
{
    KEEPIDENTITY
  | KEEPDEFAULTS
  | HOLDLOCK
  | IGNORE_CONSTRAINTS
  | IGNORE_TRIGGERS
  | NOLOCK
  | NOWAIT
  | PAGLOCK
  | READCOMMITTED
  | READCOMMITTEDLOCK
  | READPAST
  | REPEATABLEREAD
  | ROWLOCK
  | SERIALIZABLE
  | SNAPSHOT
  | TABLOCK
  | TABLOCKX
  | UPDLOCK
  | XLOCK
}


Comments:

Patch:
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 1c837610..ac0fc706 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -2528,10 +2528,21 @@ class Generator:
         return f"WHEN {matched}{source}{condition} THEN {then}"
 
     def merge_sql(self, expression: exp.Merge) -> str:
-        this = self.sql(expression, "this")
+        table = expression.this
+        table_alias = ""
+
+        hints = table.args.get("hints")
+        if hints and table.alias and isinstance(hints[0], exp.WithTableHint):
+            # T-SQL syntax is MERGE ... <target_table> [WITH (<merge_hint>)] [[AS] table_alias]
+            table = table.copy()
+            table_alias = f" AS {self.sql(table.args['alias'].pop())}"
+
+        this = self.sql(table)
         using = f"USING {self.sql(expression, 'using')}"
         on = f"ON {self.sql(expression, 'on')}"
-        return f"MERGE INTO {this} {using} {on} {self.expressions(expression, sep=' ')}"
+        expressions = self.expressions(expression, sep=" ")
+
+        return f"MERGE INTO {this}{table_alias} {using} {on} {expressions}"
 
     def tochar_sql(self, expression: exp.ToChar) -> str:
         if expression.args.get("format"):
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 10fecc9a..4ab48548 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -4620,6 +4620,9 @@ class Parser(metaclass=_Parser):
         self._match(TokenType.INTO)
         target = self._parse_table()
 
+        if target and self._match(TokenType.ALIAS, advance=False):
+            target.set("alias", self._parse_table_alias())
+
         self._match(TokenType.USING)
         using = self._parse_table()
 


Test patch:
diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index 8b919d48..a3fba58f 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -17,6 +17,11 @@ class TestTSQL(Validator):
                 "spark": "DROP TABLE IF EXISTS TempTableName",
             },
         )
+
+        self.validate_identity(
+            "MERGE INTO mytable WITH (HOLDLOCK) AS T USING mytable_merge AS S "
+            "ON (T.user_id = S.user_id) WHEN NOT MATCHED THEN INSERT (c1, c2) VALUES (S.c1, S.c2)"
+        )
         self.validate_identity("UPDATE x SET y = 1 OUTPUT x.a, x.b INTO @y FROM y")
         self.validate_identity("UPDATE x SET y = 1 OUTPUT x.a, x.b FROM y")
         self.validate_identity("INSERT INTO x (y) OUTPUT x.a, x.b INTO l SELECT * FROM z")