Repository: tobymao/sqlglot

Issue:
bigquery whitespace characters need to be escaped in string and bytes literals
Looks like newlines are handled but the remaining characters are not escaped correctly.

**Fully reproducible code snippet**

```
In [22]: import sqlglot as sg

In [23]: sg.__version__
Out[23]: '18.0.0'

In [24]: sql = sg.parse_one("SELECT '\a\b\f\n\r\t\v'", read="bigquery").sql("bigquery")

In [25]: !bq shell <<< "$sql"
Welcome to BigQuery! (Type help for more information.)
ibis-gbq> Error in query string: Error processing job 'ibis-gbq:bqjob_r1fef3289cccddd3_0000018a987c16e2_1': Syntax error: Unclosed string literal at [1:8]
ibis-gbq> Goodbye.
```

**Official Documentation**

https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences



Discussion:
Hey @cpcloud, I tried to reproduce this but the string I get back from the SQL generator seems valid in BQ (screenshot attached):

```python
>>> import sqlglot as sg
>>>
>>> sg.__version__
'18.5.1.dev10'
>>>
>>> sql = sg.parse_one("SELECT '\a\b\f\n\r\t\v'", read="bigquery").sql("bigquery")
>>> print(sql)
SELECT '
       \n
        '
>>> sql
"SELECT '\x07\x08\x0c\\n\r\t\x0b'"
```

Am I missing something here?

<img width="1151" alt="Screenshot 2023-09-15 at 2 13 12 PM" src="https://github.com/tobymao/sqlglot/assets/46752250/05ef679e-6b69-408f-834a-ca57995f7cae">
```
>>> sql
```

shows the result of `__repr__`

while `print` shows the result of `__str__` which is what is going to be passed to Python APIs as well as command line tools:

```
>>> class Demo:
...     def __str__(self):
...         print("__str__ called")
...         return "Demo"
...     def __repr__(self):
...         print("__repr__ called")
...         return "Demo"
...
>>> d = Demo()
>>> d
__repr__ called
Demo
>>> print(d)
__str__ called
Demo
```

You have to test with the result of `__str__`
Ah right, makes sense. I will have a look :)

Resolving patch:
diff --git a/sqlglot/dialects/bigquery.py b/sqlglot/dialects/bigquery.py
--- a/sqlglot/dialects/bigquery.py
+++ b/sqlglot/dialects/bigquery.py
@@ -413,7 +413,6 @@ class Generator(generator.Generator):
         TABLE_HINTS = False
         LIMIT_FETCH = "LIMIT"
         RENAME_TABLE_WITH_DB = False
-        ESCAPE_LINE_BREAK = True
         NVL2_SUPPORTED = False
 
         TRANSFORMS = {
@@ -514,6 +513,18 @@ class Generator(generator.Generator):
             exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED,
         }
 
+        UNESCAPED_SEQUENCE_TABLE = str.maketrans(  # type: ignore
+            {
+                "\a": "\\a",
+                "\b": "\\b",
+                "\f": "\\f",
+                "\n": "\\n",
+                "\r": "\\r",
+                "\t": "\\t",
+                "\v": "\\v",
+            }
+        )
+
         # from: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#reserved_keywords
         RESERVED_KEYWORDS = {
             *generator.Generator.RESERVED_KEYWORDS,
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -308,6 +308,8 @@ class Generator:
         exp.Paren,
     )
 
+    UNESCAPED_SEQUENCE_TABLE = None  # type: ignore
+
     SENTINEL_LINE_BREAK = "__SQLGLOT__LB__"
 
     # Autofilled
@@ -320,7 +322,6 @@ class Generator:
     STRICT_STRING_CONCAT = False
     NORMALIZE_FUNCTIONS: bool | str = "upper"
     NULL_ORDERING = "nulls_are_small"
-    ESCAPE_LINE_BREAK = False
 
     can_identify: t.Callable[[str, str | bool], bool]
 
@@ -1594,8 +1595,8 @@ def literal_sql(self, expression: exp.Literal) -> str:
 
     def escape_str(self, text: str) -> str:
         text = text.replace(self.QUOTE_END, self._escaped_quote_end)
-        if self.ESCAPE_LINE_BREAK:
-            text = text.replace("\n", "\\n")
+        if self.UNESCAPED_SEQUENCE_TABLE:
+            text = text.translate(self.UNESCAPED_SEQUENCE_TABLE)
         elif self.pretty:
             text = text.replace("\n", self.SENTINEL_LINE_BREAK)
         return text


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
@@ -9,36 +9,6 @@ class TestBigQuery(Validator):
     maxDiff = None
 
     def test_bigquery(self):
-        self.validate_identity("SELECT * FROM tbl FOR SYSTEM_TIME AS OF z")
-        self.validate_identity(
-            """SELECT JSON '"foo"' AS json_data""",
-            """SELECT PARSE_JSON('"foo"') AS json_data""",
-        )
-
-        self.validate_all(
-            """SELECT
-  `u`.`harness_user_email` AS `harness_user_email`,
-  `d`.`harness_user_id` AS `harness_user_id`,
-  `harness_account_id` AS `harness_account_id`
-FROM `analytics_staging`.`stg_mongodb__users` AS `u`, UNNEST(`u`.`harness_cluster_details`) AS `d`, UNNEST(`d`.`harness_account_ids`) AS `harness_account_id`
-WHERE
-  NOT `harness_account_id` IS NULL""",
-            read={
-                "": """
-                SELECT
-                  "u"."harness_user_email" AS "harness_user_email",
-                  "_q_0"."d"."harness_user_id" AS "harness_user_id",
-                  "_q_1"."harness_account_id" AS "harness_account_id"
-                FROM
-                  "analytics_staging"."stg_mongodb__users" AS "u",
-                  UNNEST("u"."harness_cluster_details") AS "_q_0"("d"),
-                  UNNEST("_q_0"."d"."harness_account_ids") AS "_q_1"("harness_account_id")
-                WHERE
-                  NOT "_q_1"."harness_account_id" IS NULL
-                """
-            },
-            pretty=True,
-        )
         with self.assertRaises(TokenError):
             transpile("'\\'", read="bigquery")
 
@@ -63,6 +33,8 @@ def test_bigquery(self):
         with self.assertRaises(ParseError):
             transpile("DATE_ADD(x, day)", read="bigquery")
 
+        self.validate_identity(r"SELECT '\n\r\a\v\f\t'")
+        self.validate_identity("SELECT * FROM tbl FOR SYSTEM_TIME AS OF z")
         self.validate_identity("STRING_AGG(DISTINCT a ORDER BY b DESC, c DESC LIMIT 10)")
         self.validate_identity("SELECT PARSE_TIMESTAMP('%c', 'Thu Dec 25 07:30:00 2008', 'UTC')")
         self.validate_identity("SELECT ANY_VALUE(fruit HAVING MAX sold) FROM fruits")
@@ -111,6 +83,7 @@ def test_bigquery(self):
         self.validate_identity("COMMIT TRANSACTION")
         self.validate_identity("ROLLBACK TRANSACTION")
         self.validate_identity("CAST(x AS BIGNUMERIC)")
+        self.validate_identity("SELECT y + 1 FROM x GROUP BY y + 1 ORDER BY 1")
         self.validate_identity(
             "DATE(CAST('2016-12-25 05:30:00+07' AS DATETIME), 'America/Los_Angeles')"
         )
@@ -132,6 +105,18 @@ def test_bigquery(self):
         self.validate_identity(
             "SELECT LAST_VALUE(a IGNORE NULLS) OVER y FROM x WINDOW y AS (PARTITION BY CATEGORY)",
         )
+        self.validate_identity(
+            "SELECT y + 1 z FROM x GROUP BY y + 1 ORDER BY z",
+            "SELECT y + 1 AS z FROM x GROUP BY z ORDER BY z",
+        )
+        self.validate_identity(
+            "SELECT y + 1 z FROM x GROUP BY y + 1",
+            "SELECT y + 1 AS z FROM x GROUP BY y + 1",
+        )
+        self.validate_identity(
+            """SELECT JSON '"foo"' AS json_data""",
+            """SELECT PARSE_JSON('"foo"') AS json_data""",
+        )
 
         self.validate_all("SELECT SPLIT(foo)", write={"bigquery": "SELECT SPLIT(foo, ',')"})
         self.validate_all("SELECT 1 AS hash", write={"bigquery": "SELECT 1 AS `hash`"})
@@ -453,7 +438,6 @@ def test_bigquery(self):
             "SELECT ARRAY(SELECT * FROM foo JOIN bla ON x = y)",
             write={"bigquery": "SELECT ARRAY(SELECT * FROM foo JOIN bla ON x = y)"},
         )
-
         self.validate_all(
             "x IS unknown",
             write={
@@ -682,16 +666,30 @@ def test_bigquery(self):
                 "spark": "TO_JSON(x)",
             },
         )
-
-        self.validate_identity(
-            "SELECT y + 1 z FROM x GROUP BY y + 1 ORDER BY z",
-            "SELECT y + 1 AS z FROM x GROUP BY z ORDER BY z",
-        )
-        self.validate_identity(
-            "SELECT y + 1 z FROM x GROUP BY y + 1",
-            "SELECT y + 1 AS z FROM x GROUP BY y + 1",
+        self.validate_all(
+            """SELECT
+  `u`.`harness_user_email` AS `harness_user_email`,
+  `d`.`harness_user_id` AS `harness_user_id`,
+  `harness_account_id` AS `harness_account_id`
+FROM `analytics_staging`.`stg_mongodb__users` AS `u`, UNNEST(`u`.`harness_cluster_details`) AS `d`, UNNEST(`d`.`harness_account_ids`) AS `harness_account_id`
+WHERE
+  NOT `harness_account_id` IS NULL""",
+            read={
+                "": """
+                SELECT
+                  "u"."harness_user_email" AS "harness_user_email",
+                  "_q_0"."d"."harness_user_id" AS "harness_user_id",
+                  "_q_1"."harness_account_id" AS "harness_account_id"
+                FROM
+                  "analytics_staging"."stg_mongodb__users" AS "u",
+                  UNNEST("u"."harness_cluster_details") AS "_q_0"("d"),
+                  UNNEST("_q_0"."d"."harness_account_ids") AS "_q_1"("harness_account_id")
+                WHERE
+                  NOT "_q_1"."harness_account_id" IS NULL
+                """
+            },
+            pretty=True,
         )
-        self.validate_identity("SELECT y + 1 FROM x GROUP BY y + 1 ORDER BY 1")
 
     def test_user_defined_functions(self):
         self.validate_identity(