Skip to content

Commit 0501480

Browse files
committed
fix: handling timestamp escape clause
1 parent 24af46e commit 0501480

7 files changed

Lines changed: 374 additions & 5 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
build/
22
build-perf/
3+
build-main/
4+
build-perf-main/
35
cmake-out/
46
cmake-build-debug/
57
build-out/

google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.cc

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "google/cloud/odbc/bq_driver/internal/odbc_sql_type_info.h"
2222
#include "google/cloud/odbc/bq_driver/internal/odbc_transactions.h"
2323
#include "google/cloud/odbc/bq_driver/internal/trace_utils.h"
24+
#include "google/cloud/odbc/bq_driver/internal/utils.h"
2425
#include "google/cloud/odbc/internal/status_record_or.h"
2526

2627
namespace google::cloud::odbc_bq_driver_internal {
@@ -213,8 +214,14 @@ StatusRecord StatementHandle::PrepareQuery(std::string const& query) {
213214
}
214215
ConnectionHandle& conn_handle = *GetConnectionHandle();
215216

217+
std::string processed_query = query;
218+
auto noscan_attr = GetAttribute(SQL_ATTR_NOSCAN);
219+
if (!noscan_attr || *noscan_attr != SQL_NOSCAN_ON) {
220+
processed_query = TranslateOdbcEscapeSequences(query);
221+
}
222+
216223
Job req;
217-
req.configuration.query.query = query;
224+
req.configuration.query.query = processed_query;
218225
req.configuration.query.use_query_cache = conn_handle.GetDsn().is_query_cache;
219226
req.configuration.dry_run = true;
220227
req.configuration.query.use_legacy_sql =
@@ -233,7 +240,7 @@ StatusRecord StatementHandle::PrepareQuery(std::string const& query) {
233240
// to be used during table creation. Subsequent operations on the table will
234241
// automatically use the KMS key without the application sending it.
235242
std::string kms_key_name = conn_handle.GetDsn().kms_key_name;
236-
if (IsInsertQuery(query) || IsSelectQuery(query)) {
243+
if (IsInsertQuery(processed_query) || IsSelectQuery(processed_query)) {
237244
if (!kms_key_name.empty()) {
238245
req.configuration.query.destination_encryption_configuration
239246
.kms_key_name = kms_key_name;
@@ -243,8 +250,8 @@ StatusRecord StatementHandle::PrepareQuery(std::string const& query) {
243250
if (!conn_handle.GetDsn().is_bq_legacy_sql) {
244251
// Detect POSITIONAL (`?`) and NAMED (`[:@]\w+`) parameter markers using
245252
// RE2 instead of a manual character scan.
246-
bool has_positional = re2::RE2::PartialMatch(query, R"(\?)");
247-
bool has_named = re2::RE2::PartialMatch(query, R"([:@]\w+)");
253+
bool has_positional = re2::RE2::PartialMatch(processed_query, R"(\?)");
254+
bool has_named = re2::RE2::PartialMatch(processed_query, R"([:@]\w+)");
248255
if (has_positional) {
249256
req.configuration.query.parameter_mode = "POSITIONAL";
250257
}
@@ -330,7 +337,7 @@ StatusRecord StatementHandle::PrepareQuery(std::string const& query) {
330337
conn_handle.SetSessionId(response->statistics.session_info.session_id);
331338
}
332339

333-
query_str_ = query;
340+
query_str_ = processed_query;
334341
prepared_job_ = *response;
335342
return StatusRecord::Ok();
336343
}
@@ -551,6 +558,10 @@ StatusRecord StatementHandle::PopulateIpd(DescriptorHandle& handle,
551558
void StatementHandle::CloseCursor() {
552559
ResultSet result_set;
553560
result_set_ = result_set;
561+
#if (!defined(_WIN32) || defined(_WIN64)) && !defined(NO_ARROW)
562+
ClearReadRowsStream();
563+
ClearReadRowsIterator();
564+
#endif
554565
if (StatementPrepared()) {
555566
SetStmtState(StmtStates::kStatementPrepared);
556567
} else {

google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class StatementHandle : public Handle {
141141
StreamRange<::google::cloud::bigquery::storage::v1::ReadRowsResponse>
142142
stream_range) {
143143
read_rows_stream_ = std::move(stream_range);
144+
read_rows_iterator_.reset();
144145
}
145146

146147
std::optional<

google/cloud/odbc/bq_driver/internal/utils.cc

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "google/cloud/internal/getenv.h"
2323
#include <array>
2424
#include <atomic>
25+
#include <cctype>
2526
#include <cstdint>
2627
#include <random>
2728
#include <sstream>
@@ -1511,4 +1512,267 @@ StatusRecord NormalizeOAuthMechanism(Section& section) {
15111512
return StatusRecord::Ok();
15121513
}
15131514
#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+
15141778
} // namespace google::cloud::odbc_bq_driver_internal

google/cloud/odbc/bq_driver/internal/utils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@ odbc_internal::StatusRecordOr<SQLUINTEGER> ParseStringToInteger(
486486
std::string const& input);
487487

488488
std::string GetLocationfromPSC(std::string const& psc);
489+
490+
std::string TranslateOdbcEscapeSequences(std::string const& sql);
489491
} // namespace google::cloud::odbc_bq_driver_internal
490492

491493
#endif // CPP_BIGQUERY_ODBC_GOOGLE_CLOUD_ODBC_BQ_DRIVER_INTERNAL_UTILS_H

0 commit comments

Comments
 (0)