Repository: tobymao/sqlglot

Issue:
Unable to resolve columns when using BigQuery's alternate table identifier quoting
When qualifying columns in a BigQuery select statement, BigQuery allows `.`-delimited database objects notation wrapped in backticks to qualify columns in a `SELECT` clause, similar to what was described in #3083.

These two SQL statements are valid and identical in BigQuery:
```sql
SELECT `my_column` FROM `my_db.my_table`
SELECT `my_db.my_table`.`my_column` FROM `my_db.my_table`
```

**Fully reproducible code snippet**
Please include a fully reproducible code snippet or the input sql, dialect, and expected output.

```python
from sqlglot import MappingSchema, parse_one
from sqlglot.optimizer import traverse_scope
from sqlglot.optimizer.qualify import qualify

import sqlglot

print(sqlglot.__version__)

mapping_schema = MappingSchema(
    {"my_catalog": {"my_db": {"my_table": {"my_column": "INTEGER"}}}},
    dialect="bigquery"
)


queries = [
    # this case succeeds
    "SELECT `my_column` FROM `my_db.my_table`",
    # this case fails
    "SELECT `my_db.my_table`.`my_column` FROM `my_db.my_table`",
]

for i, sql in enumerate(queries, start=1):
    print("-" * 80)
    print(sql)
    try:
        expression = parse_one(sql, read="bigquery")
        for scope in traverse_scope(expression):
            print("unqualified sources: ", scope.sources)
            print("unqualified columns: ", scope.columns)
        qualified_expression = qualify(
            expression,
            dialect="bigquery",
            schema=mapping_schema,
            catalog="my_catalog",
            validate_qualify_columns=True,
            infer_schema=True,
        )
        for scope in traverse_scope(qualified_expression):
            print("qualified sources: ", scope.sources)
            print("qualified columns: ", scope.columns)
    except Exception as e:
        print(f"Parse Error in sql {i}: {e}")
```


### Output for first case (passing case)
```
SELECT `my_column` FROM `my_db.my_table`
unqualified sources:  {'my_table': Table(
  this=Identifier(this=my_table, quoted=True),
  db=Identifier(this=my_db, quoted=True))}
unqualified columns:  [Column(
  this=Identifier(this=my_column, quoted=True))]
qualified sources:  {'my_table': Table(
  this=Identifier(this=my_table, quoted=True, _type=DataType(this=Type.UNKNOWN)),
  db=Identifier(this=my_db, quoted=True, _type=DataType(this=Type.UNKNOWN)),
  catalog=Identifier(this=my_catalog, quoted=True, _type=DataType(this=Type.UNKNOWN)),
  alias=TableAlias(
    this=Identifier(this=my_table, quoted=True, _type=DataType(this=Type.UNKNOWN)),
    _type=DataType(this=Type.UNKNOWN)),
  _type=DataType(this=Type.UNKNOWN))}
qualified columns:  [Column(
  this=Identifier(this=my_column, quoted=True),
  table=Identifier(this=my_table, quoted=True),
  _type=DataType(this=Type.INT, nested=False))]
```

### Output for second case (failing case)
I expected `qualify()` to not raise an exception and to have similar output to the previous statement.
```
SELECT `my_db.my_table`.`my_column` FROM `my_db.my_table`
unqualified sources:  {'my_table': Table(
  this=Identifier(this=my_table, quoted=True),
  db=Identifier(this=my_db, quoted=True))}
unqualified columns:  [Column(
  this=Identifier(this=my_column, quoted=True),
  table=Identifier(this=my_db.my_table, quoted=True))]
Parse Error in sql 2: Column '"my_db.my_table"."my_column"' could not be resolved for table: 'my_db.my_table'
```

**Official Documentation**
Please include links to official SQL documentation related to your issue.
This [article](https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical) the best I could find but I could not find an example that illustrates this particular failing case. 

Here is an example query demonstrating this type of column qualification:
```sql
SELECT 
  `census_bureau_usa.population_by_zip_2010`.`geo_id`
FROM `bigquery-public-data`.`census_bureau_usa.population_by_zip_2010` LIMIT 1000;
```
<img width="727" alt="image" src="https://github.com/tobymao/sqlglot/assets/357102/eb1ef284-7cfd-4417-a01c-88640bcf7e59">






Discussion:
We'll take a look shortly, thanks
> We'll take a look shortly, thanks

Thanks, @georgesittas. I'm happy to work on it but could use some guidance if possible so I can better understand SQLGlot.

Resolving patch:
diff --git a/sqlglot/dialects/bigquery.py b/sqlglot/dialects/bigquery.py
--- a/sqlglot/dialects/bigquery.py
+++ b/sqlglot/dialects/bigquery.py
@@ -474,11 +474,31 @@ def _parse_table_parts(
                 if rest and this:
                     this = exp.Dot.build([this, *rest])  # type: ignore
 
-                table = exp.Table(this=this, db=db, catalog=catalog)
+                table = exp.Table(
+                    this=this, db=db, catalog=catalog, pivots=table.args.get("pivots")
+                )
                 table.meta["quoted_table"] = True
 
             return table
 
+        def _parse_column(self) -> t.Optional[exp.Expression]:
+            column = super()._parse_column()
+            if isinstance(column, exp.Column):
+                parts = column.parts
+                if any("." in p.name for p in parts):
+                    catalog, db, table, this, *rest = (
+                        exp.to_identifier(p, quoted=True)
+                        for p in split_num_words(".".join(p.name for p in parts), ".", 4)
+                    )
+
+                    if rest and this:
+                        this = exp.Dot.build([this, *rest])  # type: ignore
+
+                    column = exp.Column(this=this, table=table, db=db, catalog=catalog)
+                    column.meta["quoted_column"] = True
+
+            return column
+
         @t.overload
         def _parse_json_object(self, agg: Lit[False]) -> exp.JSONObject: ...
 
@@ -781,6 +801,16 @@ class Generator(generator.Generator):
             "within",
         }
 
+        def column_parts(self, expression: exp.Column) -> str:
+            if expression.meta.get("quoted_column"):
+                # If a column reference is of the form `dataset.table`.name, we need
+                # to preserve the quoted table path, otherwise the reference breaks
+                table_parts = ".".join(p.name for p in expression.parts[:-1])
+                table_path = self.sql(exp.Identifier(this=table_parts, quoted=True))
+                return f"{table_path}.{self.sql(expression, 'this')}"
+
+            return super().column_parts(expression)
+
         def table_parts(self, expression: exp.Table) -> str:
             # Depending on the context, `x.y` may not resolve to the same data source as `x`.`y`, so
             # we need to make sure the correct quoting is used in each case.
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -778,14 +778,8 @@ def characterset_sql(self, expression: exp.CharacterSet) -> str:
         default = "DEFAULT " if expression.args.get("default") else ""
         return f"{default}CHARACTER SET={self.sql(expression, 'this')}"
 
-    def column_sql(self, expression: exp.Column) -> str:
-        join_mark = " (+)" if expression.args.get("join_mark") else ""
-
-        if join_mark and not self.COLUMN_JOIN_MARKS_SUPPORTED:
-            join_mark = ""
-            self.unsupported("Outer join syntax using the (+) operator is not supported.")
-
-        column = ".".join(
+    def column_parts(self, expression: exp.Column) -> str:
+        return ".".join(
             self.sql(part)
             for part in (
                 expression.args.get("catalog"),
@@ -796,7 +790,14 @@ def column_sql(self, expression: exp.Column) -> str:
             if part
         )
 
-        return f"{column}{join_mark}"
+    def column_sql(self, expression: exp.Column) -> str:
+        join_mark = " (+)" if expression.args.get("join_mark") else ""
+
+        if join_mark and not self.COLUMN_JOIN_MARKS_SUPPORTED:
+            join_mark = ""
+            self.unsupported("Outer join syntax using the (+) operator is not supported.")
+
+        return f"{self.column_parts(expression)}{join_mark}"
 
     def columnposition_sql(self, expression: exp.ColumnPosition) -> str:
         this = self.sql(expression, "this")


Test patch:
diff --git a/tests/dialects/test_bigquery.py b/tests/dialects/test_bigquery.py
--- a/tests/dialects/test_bigquery.py
+++ b/tests/dialects/test_bigquery.py
@@ -57,6 +57,9 @@ def test_bigquery(self):
         self.assertEqual(exp.to_table("`x.y.z`", dialect="bigquery").sql("bigquery"), "`x.y.z`")
         self.assertEqual(exp.to_table("`x`.`y`", dialect="bigquery").sql("bigquery"), "`x`.`y`")
 
+        column = self.validate_identity("SELECT `db.t`.`c` FROM `db.t`").selects[0]
+        self.assertEqual(len(column.parts), 3)
+
         select_with_quoted_udf = self.validate_identity("SELECT `p.d.UdF`(data) FROM `p.d.t`")
         self.assertEqual(select_with_quoted_udf.selects[0].name, "p.d.UdF")
 
diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py
--- a/tests/test_optimizer.py
+++ b/tests/test_optimizer.py
@@ -228,6 +228,17 @@ def test_normalize(self):
 
     @patch("sqlglot.generator.logger")
     def test_qualify_columns(self, logger):
+        self.assertEqual(
+            optimizer.qualify.qualify(
+                parse_one(
+                    "SELECT `my_db.my_table`.`my_column` FROM `my_db.my_table`",
+                    read="bigquery",
+                ),
+                dialect="bigquery",
+            ).sql(dialect="bigquery"),
+            "SELECT `my_table`.`my_column` AS `my_column` FROM `my_db.my_table` AS `my_table`",
+        )
+
         self.assertEqual(
             optimizer.qualify_columns.qualify_columns(
                 parse_one(