|
22 | 22 | #include "google/cloud/internal/getenv.h" |
23 | 23 | #include <array> |
24 | 24 | #include <atomic> |
| 25 | +#include <cctype> |
25 | 26 | #include <cstdint> |
26 | 27 | #include <random> |
27 | 28 | #include <sstream> |
@@ -1511,4 +1512,267 @@ StatusRecord NormalizeOAuthMechanism(Section& section) { |
1511 | 1512 | return StatusRecord::Ok(); |
1512 | 1513 | } |
1513 | 1514 | #endif // _WIN32 |
| 1515 | + |
| 1516 | +namespace { |
| 1517 | + |
| 1518 | +std::string_view TrimWhitespace(std::string_view sv) { |
| 1519 | + while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.front()))) { |
| 1520 | + sv.remove_prefix(1); |
| 1521 | + } |
| 1522 | + while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.back()))) { |
| 1523 | + sv.remove_suffix(1); |
| 1524 | + } |
| 1525 | + return sv; |
| 1526 | +} |
| 1527 | + |
| 1528 | +bool ExtractQuotedLiteral(std::string_view sv, std::string& out_literal) { |
| 1529 | + sv = TrimWhitespace(sv); |
| 1530 | + if (sv.size() >= 2) { |
| 1531 | + char quote = sv.front(); |
| 1532 | + if ((quote == '\'' || quote == '"') && sv.back() == quote) { |
| 1533 | + out_literal = std::string(sv.substr(1, sv.size() - 2)); |
| 1534 | + return true; |
| 1535 | + } |
| 1536 | + } |
| 1537 | + return false; |
| 1538 | +} |
| 1539 | + |
| 1540 | +bool StartsWithIgnoreCase(std::string_view sv, std::string_view prefix) { |
| 1541 | + if (sv.size() < prefix.size()) return false; |
| 1542 | + for (size_t i = 0; i < prefix.size(); ++i) { |
| 1543 | + if (std::tolower(static_cast<unsigned char>(sv[i])) != |
| 1544 | + std::tolower(static_cast<unsigned char>(prefix[i]))) { |
| 1545 | + return false; |
| 1546 | + } |
| 1547 | + } |
| 1548 | + return true; |
| 1549 | +} |
| 1550 | + |
| 1551 | +std::string ProcessEscapeContent(std::string_view content) { |
| 1552 | + content = TrimWhitespace(content); |
| 1553 | + if (content.empty()) return "{}"; |
| 1554 | + |
| 1555 | + // Check for {ts '...'} / {TS '...'} |
| 1556 | + if (StartsWithIgnoreCase(content, "ts") && |
| 1557 | + (content.size() == 2 || |
| 1558 | + std::isspace(static_cast<unsigned char>(content[2])))) { |
| 1559 | + std::string_view rest = TrimWhitespace(content.substr(2)); |
| 1560 | + std::string literal; |
| 1561 | + if (ExtractQuotedLiteral(rest, literal)) { |
| 1562 | + return "TIMESTAMP '" + literal + "'"; |
| 1563 | + } |
| 1564 | + } |
| 1565 | + |
| 1566 | + // Check for {d '...'} / {D '...'} |
| 1567 | + if (StartsWithIgnoreCase(content, "d") && |
| 1568 | + (content.size() == 1 || |
| 1569 | + std::isspace(static_cast<unsigned char>(content[1])))) { |
| 1570 | + std::string_view rest = TrimWhitespace(content.substr(1)); |
| 1571 | + std::string literal; |
| 1572 | + if (ExtractQuotedLiteral(rest, literal)) { |
| 1573 | + return "DATE '" + literal + "'"; |
| 1574 | + } |
| 1575 | + } |
| 1576 | + |
| 1577 | + // Check for {t '...'} / {T '...'} |
| 1578 | + if (StartsWithIgnoreCase(content, "t") && |
| 1579 | + (content.size() == 1 || |
| 1580 | + std::isspace(static_cast<unsigned char>(content[1])))) { |
| 1581 | + std::string_view rest = TrimWhitespace(content.substr(1)); |
| 1582 | + std::string literal; |
| 1583 | + if (ExtractQuotedLiteral(rest, literal)) { |
| 1584 | + return "TIME '" + literal + "'"; |
| 1585 | + } |
| 1586 | + } |
| 1587 | + |
| 1588 | + // Check for {escape '...'} |
| 1589 | + if (StartsWithIgnoreCase(content, "escape") && |
| 1590 | + (content.size() == 6 || |
| 1591 | + std::isspace(static_cast<unsigned char>(content[6])))) { |
| 1592 | + std::string_view rest = TrimWhitespace(content.substr(6)); |
| 1593 | + std::string literal; |
| 1594 | + if (ExtractQuotedLiteral(rest, literal)) { |
| 1595 | + return "ESCAPE '" + literal + "'"; |
| 1596 | + } |
| 1597 | + } |
| 1598 | + |
| 1599 | + // Check for {guid '...'} |
| 1600 | + if (StartsWithIgnoreCase(content, "guid") && |
| 1601 | + (content.size() == 4 || |
| 1602 | + std::isspace(static_cast<unsigned char>(content[4])))) { |
| 1603 | + std::string_view rest = TrimWhitespace(content.substr(4)); |
| 1604 | + std::string literal; |
| 1605 | + if (ExtractQuotedLiteral(rest, literal)) { |
| 1606 | + return "'" + literal + "'"; |
| 1607 | + } |
| 1608 | + } |
| 1609 | + |
| 1610 | + // Check for {oj ...} -> outer join |
| 1611 | + if (StartsWithIgnoreCase(content, "oj") && |
| 1612 | + (content.size() == 2 || |
| 1613 | + std::isspace(static_cast<unsigned char>(content[2])))) { |
| 1614 | + std::string_view rest = TrimWhitespace(content.substr(2)); |
| 1615 | + return std::string(rest); |
| 1616 | + } |
| 1617 | + |
| 1618 | + // Check for {fn ...} -> scalar function |
| 1619 | + if (StartsWithIgnoreCase(content, "fn") && |
| 1620 | + (content.size() == 2 || |
| 1621 | + std::isspace(static_cast<unsigned char>(content[2])))) { |
| 1622 | + std::string_view rest = TrimWhitespace(content.substr(2)); |
| 1623 | + return std::string(rest); |
| 1624 | + } |
| 1625 | + |
| 1626 | + // If none matched, return original braced text |
| 1627 | + return "{" + std::string(content) + "}"; |
| 1628 | +} |
| 1629 | + |
| 1630 | +} // namespace |
| 1631 | + |
| 1632 | +std::string TranslateOdbcEscapeSequences(std::string const& sql) { |
| 1633 | + // Fast path: if there are no braces, no ODBC escape sequence can be present. |
| 1634 | + if (sql.find('{') == std::string::npos) { |
| 1635 | + return sql; |
| 1636 | + } |
| 1637 | + |
| 1638 | + std::string current = sql; |
| 1639 | + constexpr int kMaxPasses = 10; |
| 1640 | + for (int pass = 0; pass < kMaxPasses; ++pass) { |
| 1641 | + std::string result; |
| 1642 | + result.reserve(current.size()); |
| 1643 | + bool changed = false; |
| 1644 | + |
| 1645 | + size_t i = 0; |
| 1646 | + size_t const n = current.size(); |
| 1647 | + |
| 1648 | + while (i < n) { |
| 1649 | + char c = current[i]; |
| 1650 | + |
| 1651 | + // Check for single-line comment: -- or # |
| 1652 | + if ((c == '-' && i + 1 < n && current[i + 1] == '-') || c == '#') { |
| 1653 | + size_t comment_end = current.find('\n', i); |
| 1654 | + if (comment_end == std::string::npos) { |
| 1655 | + result.append(current, i, n - i); |
| 1656 | + break; |
| 1657 | + } |
| 1658 | + result.append(current, i, comment_end - i + 1); |
| 1659 | + i = comment_end + 1; |
| 1660 | + continue; |
| 1661 | + } |
| 1662 | + |
| 1663 | + // Check for multi-line comment: /* ... */ |
| 1664 | + if (c == '/' && i + 1 < n && current[i + 1] == '*') { |
| 1665 | + size_t comment_end = current.find("*/", i + 2); |
| 1666 | + if (comment_end == std::string::npos) { |
| 1667 | + result.append(current, i, n - i); |
| 1668 | + break; |
| 1669 | + } |
| 1670 | + result.append(current, i, comment_end + 2 - i); |
| 1671 | + i = comment_end + 2; |
| 1672 | + continue; |
| 1673 | + } |
| 1674 | + |
| 1675 | + // Check for string literals and quoted identifiers: '...', "...", `...` |
| 1676 | + if (c == '\'' || c == '"' || c == '`') { |
| 1677 | + char const quote_char = c; |
| 1678 | + result.push_back(c); |
| 1679 | + ++i; |
| 1680 | + while (i < n) { |
| 1681 | + char sc = current[i]; |
| 1682 | + result.push_back(sc); |
| 1683 | + if (sc == quote_char) { |
| 1684 | + if (i + 1 < n && current[i + 1] == quote_char) { |
| 1685 | + // Escaped quote (e.g. '') |
| 1686 | + ++i; |
| 1687 | + result.push_back(current[i]); |
| 1688 | + ++i; |
| 1689 | + } else { |
| 1690 | + ++i; |
| 1691 | + break; |
| 1692 | + } |
| 1693 | + } else if (sc == '\\' && i + 1 < n && current[i + 1] == '\\') { |
| 1694 | + ++i; |
| 1695 | + result.push_back(current[i]); |
| 1696 | + ++i; |
| 1697 | + } else { |
| 1698 | + ++i; |
| 1699 | + } |
| 1700 | + } |
| 1701 | + continue; |
| 1702 | + } |
| 1703 | + |
| 1704 | + // Check for opening brace `{` |
| 1705 | + if (c == '{') { |
| 1706 | + // Find matching `}` while respecting quotes inside |
| 1707 | + size_t start_brace = i; |
| 1708 | + size_t j = i + 1; |
| 1709 | + int brace_depth = 1; |
| 1710 | + bool matched = false; |
| 1711 | + |
| 1712 | + while (j < n && brace_depth > 0) { |
| 1713 | + char jc = current[j]; |
| 1714 | + if (jc == '\'' || jc == '"' || jc == '`') { |
| 1715 | + char const quote_char = jc; |
| 1716 | + ++j; |
| 1717 | + while (j < n) { |
| 1718 | + if (current[j] == quote_char) { |
| 1719 | + if (j + 1 < n && current[j + 1] == quote_char) { |
| 1720 | + j += 2; |
| 1721 | + } else { |
| 1722 | + ++j; |
| 1723 | + break; |
| 1724 | + } |
| 1725 | + } else if (current[j] == '\\' && j + 1 < n && |
| 1726 | + current[j + 1] == '\\') { |
| 1727 | + j += 2; |
| 1728 | + } else { |
| 1729 | + ++j; |
| 1730 | + } |
| 1731 | + } |
| 1732 | + } else if (jc == '{') { |
| 1733 | + ++brace_depth; |
| 1734 | + ++j; |
| 1735 | + } else if (jc == '}') { |
| 1736 | + --brace_depth; |
| 1737 | + if (brace_depth == 0) { |
| 1738 | + matched = true; |
| 1739 | + break; |
| 1740 | + } |
| 1741 | + ++j; |
| 1742 | + } else { |
| 1743 | + ++j; |
| 1744 | + } |
| 1745 | + } |
| 1746 | + |
| 1747 | + if (matched) { |
| 1748 | + std::string_view const inner = std::string_view{current}.substr( |
| 1749 | + start_brace + 1, j - start_brace - 1); |
| 1750 | + std::string replaced = ProcessEscapeContent(inner); |
| 1751 | + if (replaced != current.substr(start_brace, j - start_brace + 1)) { |
| 1752 | + changed = true; |
| 1753 | + } |
| 1754 | + result.append(replaced); |
| 1755 | + i = j + 1; |
| 1756 | + continue; |
| 1757 | + } |
| 1758 | + |
| 1759 | + // No matching brace found, output '{' |
| 1760 | + result.push_back(c); |
| 1761 | + ++i; |
| 1762 | + continue; |
| 1763 | + } |
| 1764 | + |
| 1765 | + result.push_back(c); |
| 1766 | + ++i; |
| 1767 | + } |
| 1768 | + |
| 1769 | + if (!changed) { |
| 1770 | + return result; |
| 1771 | + } |
| 1772 | + current = std::move(result); |
| 1773 | + } |
| 1774 | + |
| 1775 | + return current; |
| 1776 | +} |
| 1777 | + |
1514 | 1778 | } // namespace google::cloud::odbc_bq_driver_internal |
0 commit comments