diff --git a/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py b/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py index 90907c6a779e..c00aaf245ee2 100644 --- a/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py +++ b/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py @@ -382,10 +382,13 @@ def ensure_where_clause(sql): return sql + " WHERE 1=1" +_QUOTED_IDENTIFIER_RE = re.compile(r"\A`(\\.|[^`\\])*`\Z", re.DOTALL) + + def escape_name(name): """ - Apply backticks to the name that either contain '-' or - ' ', or is a Cloud Spanner's reserved keyword. + Apply backticks to the name if it is not a valid regular ASCII identifier, + or if it is a Cloud Spanner's reserved keyword. :type name: str :param name: Name to escape. @@ -393,6 +396,24 @@ def escape_name(name): :rtype: str :returns: Name escaped if it has to be escaped. """ - if "-" in name or " " in name or name.upper() in SPANNER_RESERVED_KEYWORDS: - return "`" + name + "`" + if not name: + return name + + if _QUOTED_IDENTIFIER_RE.match(name): + return name + + if "." in name: + parts = name.split(".") + return ".".join(escape_name(part) for part in parts) + + is_valid_regular_identifier = ( + (name[0].isalpha() or name[0] == "_") + and all(c.isalnum() or c == "_" for c in name) + and name.isascii() + ) + + if not is_valid_regular_identifier or name.upper() in SPANNER_RESERVED_KEYWORDS: + escaped = name.replace("\\", "\\\\").replace("`", "\\`") + return f"`{escaped}`" + return name diff --git a/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py b/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py index 64000a0ae12d..29a08edb4264 100644 --- a/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py +++ b/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py @@ -403,6 +403,24 @@ def test_escape_name(self): ("with space", "`with space`"), ("name", "name"), ("", ""), + ("col`; DROP TABLE t; -- x", "`col\\`; DROP TABLE t; -- x`"), + ("table`name", "`table\\`name`"), + ("`", "`\\``"), + ("col/*comment*/name", "`col/*comment*/name`"), + ("123column", "`123column`"), + ("col;select", "`col;select`"), + ("col\nname", "`col\nname`"), + ("test\\", "`test\\\\`"), + ("my_schema.my_table", "my_schema.my_table"), + ("my-schema.my-table", "`my-schema`.`my-table`"), + ("`my_table`", "`my_table`"), + ("`my_schema`.`my_table`", "`my_schema`.`my_table`"), + ("`table.with.dots`", "`table.with.dots`"), + ("`col\\`; DROP TABLE users; --`", "`col\\`; DROP TABLE users; --`"), + ( + "`col\\\\`; DROP TABLE users; --`", + "`\\`col\\\\\\\\\\`; DROP TABLE users; --\\``", + ), ) for name, want in cases: with self.subTest(name=name):