Skip to content

Commit e5590a7

Browse files
Merge branch 'main' into sshetkar-SNOW-2171791-add-platform-telemetry
2 parents c9577da + d89ebee commit e5590a7

File tree

10 files changed

+180
-142
lines changed

10 files changed

+180
-142
lines changed

DESCRIPTION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
99
# Release Notes
1010
- v3.16.1(TBD)
1111
- Added in-band OCSP exception telemetry.
12+
- Added `APPLICATION_PATH` within `CLIENT_ENVIRONMENT` to distinguish between multiple scripts using the PythonConnector in the same environment.
13+
- Disabled token caching for OAuth Client Credentials authentication
1214
- Added in-band HTTP exception telemetry.
15+
- Fixed a bug where timezoned timestamps fetched as pandas.DataFrame or pyarrow.Table would overflow for the sake of unnecessary precision. In the case where an overflow cannot be prevented a clear error will be raised now.
16+
- Fix OAuth authenticator values.
1317

1418
- v3.16.0(July 04,2025)
1519
- Bumped numpy dependency from <2.1.0 to <=2.2.4.

src/snowflake/connector/_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import string
44
from enum import Enum
5+
from inspect import stack
56
from random import choice
67
from threading import Timer
78
from uuid import UUID
@@ -86,3 +87,12 @@ def __init__(self, interval, function, args=None, kwargs=None):
8687
def run(self):
8788
super().run()
8889
self.executed = True
90+
91+
92+
def get_application_path() -> str:
93+
"""Get the path of the application script using the connector."""
94+
try:
95+
outermost_frame = stack()[-1]
96+
return outermost_frame.filename
97+
except Exception:
98+
return "unknown"

src/snowflake/connector/auth/_auth.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
load_pem_private_key,
1818
)
1919

20+
from .._utils import get_application_path
2021
from ..compat import urlencode
2122
from ..constants import (
2223
DAY_IN_SECONDS,
@@ -112,6 +113,7 @@ def base_auth_data(
112113
"LOGIN_NAME": user,
113114
"CLIENT_ENVIRONMENT": {
114115
"APPLICATION": application,
116+
"APPLICATION_PATH": get_application_path(),
115117
"OS": OPERATING_SYSTEM,
116118
"OS_VERSION": PLATFORM,
117119
"PYTHON_VERSION": PYTHON_VERSION,

src/snowflake/connector/auth/oauth_credentials.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from typing import TYPE_CHECKING, Any
99

1010
from ..constants import OAUTH_TYPE_CLIENT_CREDENTIALS
11-
from ..token_cache import TokenCache
1211
from ._oauth_base import AuthByOAuthBase
1312

1413
if TYPE_CHECKING:
@@ -27,8 +26,6 @@ def __init__(
2726
client_secret: str,
2827
token_request_url: str,
2928
scope: str,
30-
token_cache: TokenCache | None = None,
31-
refresh_token_enabled: bool = False,
3229
connection: SnowflakeConnection | None = None,
3330
**kwargs,
3431
) -> None:
@@ -38,8 +35,8 @@ def __init__(
3835
client_secret=client_secret,
3936
token_request_url=token_request_url,
4037
scope=scope,
41-
token_cache=token_cache,
42-
refresh_token_enabled=refresh_token_enabled,
38+
token_cache=None,
39+
refresh_token_enabled=False,
4340
**kwargs,
4441
)
4542
self._application = application

src/snowflake/connector/connection.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@ def _get_private_bytes_from_file(
377377
str,
378378
# SNOW-2096721: External (Spark) session ID
379379
),
380+
"unsafe_file_write": (
381+
False,
382+
bool,
383+
), # SNOW-1944208: add unsafe write flag
380384
_VARIABLE_NAME_SERVER_DOP_CAP_FOR_FILE_TRANSFER: (
381385
_DEFAULT_VALUE_SERVER_DOP_CAP_FOR_FILE_TRANSFER, # default value
382386
int, # type
@@ -1272,12 +1276,6 @@ def __open_connection(self):
12721276
host=self.host, port=self.port
12731277
),
12741278
scope=self._oauth_scope,
1275-
token_cache=(
1276-
auth.get_token_cache()
1277-
if self._client_store_temporary_credential
1278-
else None
1279-
),
1280-
refresh_token_enabled=self._oauth_enable_refresh_tokens,
12811279
connection=self,
12821280
)
12831281
elif self._authenticator == USR_PWD_MFA_AUTHENTICATOR:
@@ -1422,11 +1420,6 @@ def __config(self, **kwargs):
14221420
if "host" not in kwargs:
14231421
self._host = construct_hostname(kwargs.get("region"), self._account)
14241422

1425-
if "unsafe_file_write" in kwargs:
1426-
self._unsafe_file_write = kwargs["unsafe_file_write"]
1427-
else:
1428-
self._unsafe_file_write = False
1429-
14301423
logger.info(
14311424
f"Connecting to {_DOMAIN_NAME_MAP.get(extract_top_level_domain_from_hostname(self._host), 'GLOBAL')} Snowflake domain"
14321425
)

src/snowflake/connector/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,5 +440,5 @@ class IterUnit(Enum):
440440
)
441441

442442
_OAUTH_DEFAULT_SCOPE = "session:role:{role}"
443-
OAUTH_TYPE_AUTHORIZATION_CODE = "authorization_code"
444-
OAUTH_TYPE_CLIENT_CREDENTIALS = "client_credentials"
443+
OAUTH_TYPE_AUTHORIZATION_CODE = "oauth_authorization_code"
444+
OAUTH_TYPE_CLIENT_CREDENTIALS = "oauth_client_credentials"

src/snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp

Lines changed: 86 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,45 @@ void CArrowTableIterator::convertTimeColumn_nanoarrow(
600600
ArrowArrayMove(newArray, columnArray->array);
601601
}
602602

603+
/**
604+
* Helper function to detect nanosecond timestamp overflow and determine if
605+
* downscaling to microseconds is needed.
606+
* @param columnArray The Arrow array containing the timestamp data
607+
* @param epochArray The Arrow array containing epoch values
608+
* @param fractionArray The Arrow array containing fraction values
609+
* @return true if overflow was detected and downscaling to microseconds is
610+
* safe, false otherwise
611+
* @throws std::overflow_error if overflow is detected but downscaling would
612+
* lose precision
613+
*/
614+
static bool _checkNanosecondTimestampOverflowAndDownscale(
615+
ArrowArrayView* columnArray, ArrowArrayView* epochArray,
616+
ArrowArrayView* fractionArray) {
617+
int powTenSB4 = sf::internal::powTenSB4[9];
618+
for (int64_t rowIdx = 0; rowIdx < columnArray->array->length; rowIdx++) {
619+
if (!ArrowArrayViewIsNull(columnArray, rowIdx)) {
620+
int64_t epoch = ArrowArrayViewGetIntUnsafe(epochArray, rowIdx);
621+
int64_t fraction = ArrowArrayViewGetIntUnsafe(fractionArray, rowIdx);
622+
if (epoch > (INT64_MAX / powTenSB4) || epoch < (INT64_MIN / powTenSB4)) {
623+
if (fraction % 1000 != 0) {
624+
std::string errorInfo = Logger::formatString(
625+
"The total number of nanoseconds %d%d overflows int64 range. "
626+
"If you use a timestamp with "
627+
"the nanosecond part over 6-digits in the Snowflake database, "
628+
"the timestamp must be "
629+
"between '1677-09-21 00:12:43.145224192' and '2262-04-11 "
630+
"23:47:16.854775807' to not overflow.",
631+
epoch, fraction);
632+
throw std::overflow_error(errorInfo.c_str());
633+
} else {
634+
return true; // Safe to downscale
635+
}
636+
}
637+
}
638+
}
639+
return false;
640+
}
641+
603642
void CArrowTableIterator::convertTimestampColumn_nanoarrow(
604643
ArrowSchemaView* field, ArrowArrayView* columnArray, const int scale,
605644
const std::string timezone) {
@@ -614,11 +653,11 @@ void CArrowTableIterator::convertTimestampColumn_nanoarrow(
614653
newSchema->flags &=
615654
(field->schema->flags & ARROW_FLAG_NULLABLE); // map to nullable()
616655

617-
// calculate has_overflow_to_downscale
656+
// Find epoch and fraction arrays for overflow detection
657+
ArrowArrayView* epochArray = nullptr;
658+
ArrowArrayView* fractionArray = nullptr;
618659
bool has_overflow_to_downscale = false;
619660
if (scale > 6 && field->type == NANOARROW_TYPE_STRUCT) {
620-
ArrowArrayView* epochArray;
621-
ArrowArrayView* fractionArray;
622661
for (int64_t i = 0; i < field->schema->n_children; i++) {
623662
ArrowSchema* c_schema = field->schema->children[i];
624663
if (std::strcmp(c_schema->name, internal::FIELD_NAME_EPOCH.c_str()) ==
@@ -631,30 +670,8 @@ void CArrowTableIterator::convertTimestampColumn_nanoarrow(
631670
// do nothing
632671
}
633672
}
634-
635-
int powTenSB4 = sf::internal::powTenSB4[9];
636-
for (int64_t rowIdx = 0; rowIdx < columnArray->array->length; rowIdx++) {
637-
if (!ArrowArrayViewIsNull(columnArray, rowIdx)) {
638-
int64_t epoch = ArrowArrayViewGetIntUnsafe(epochArray, rowIdx);
639-
int64_t fraction = ArrowArrayViewGetIntUnsafe(fractionArray, rowIdx);
640-
if (epoch > (INT64_MAX / powTenSB4) ||
641-
epoch < (INT64_MIN / powTenSB4)) {
642-
if (fraction % 1000 != 0) {
643-
std::string errorInfo = Logger::formatString(
644-
"The total number of nanoseconds %d%d overflows int64 range. "
645-
"If you use a timestamp with "
646-
"the nanosecond part over 6-digits in the Snowflake database, "
647-
"the timestamp must be "
648-
"between '1677-09-21 00:12:43.145224192' and '2262-04-11 "
649-
"23:47:16.854775807' to not overflow.",
650-
epoch, fraction);
651-
throw std::overflow_error(errorInfo.c_str());
652-
} else {
653-
has_overflow_to_downscale = true;
654-
}
655-
}
656-
}
657-
}
673+
has_overflow_to_downscale = _checkNanosecondTimestampOverflowAndDownscale(
674+
columnArray, epochArray, fractionArray);
658675
}
659676

660677
if (scale <= 6) {
@@ -855,6 +872,29 @@ void CArrowTableIterator::convertTimestampTZColumn_nanoarrow(
855872
ArrowSchemaInit(newSchema);
856873
newSchema->flags &=
857874
(field->schema->flags & ARROW_FLAG_NULLABLE); // map to nullable()
875+
876+
// Find epoch and fraction arrays
877+
ArrowArrayView* epochArray = nullptr;
878+
ArrowArrayView* fractionArray = nullptr;
879+
for (int64_t i = 0; i < field->schema->n_children; i++) {
880+
ArrowSchema* c_schema = field->schema->children[i];
881+
if (std::strcmp(c_schema->name, internal::FIELD_NAME_EPOCH.c_str()) == 0) {
882+
epochArray = columnArray->children[i];
883+
} else if (std::strcmp(c_schema->name,
884+
internal::FIELD_NAME_FRACTION.c_str()) == 0) {
885+
fractionArray = columnArray->children[i];
886+
} else {
887+
// do nothing
888+
}
889+
}
890+
891+
// Check for timestamp overflow and determine if downscaling is needed
892+
bool has_overflow_to_downscale = false;
893+
if (scale > 6 && byteLength == 16) {
894+
has_overflow_to_downscale = _checkNanosecondTimestampOverflowAndDownscale(
895+
columnArray, epochArray, fractionArray);
896+
}
897+
858898
auto timeunit = NANOARROW_TIME_UNIT_SECOND;
859899
if (scale == 0) {
860900
timeunit = NANOARROW_TIME_UNIT_SECOND;
@@ -863,7 +903,9 @@ void CArrowTableIterator::convertTimestampTZColumn_nanoarrow(
863903
} else if (scale <= 6) {
864904
timeunit = NANOARROW_TIME_UNIT_MICRO;
865905
} else {
866-
timeunit = NANOARROW_TIME_UNIT_NANO;
906+
// Use microsecond precision if we detected overflow, otherwise nanosecond
907+
timeunit = has_overflow_to_downscale ? NANOARROW_TIME_UNIT_MICRO
908+
: NANOARROW_TIME_UNIT_NANO;
867909
}
868910

869911
if (!timezone.empty()) {
@@ -893,20 +935,6 @@ void CArrowTableIterator::convertTimestampTZColumn_nanoarrow(
893935
"from schema : %s, error code: %d",
894936
ArrowErrorMessage(&error), returnCode);
895937

896-
ArrowArrayView* epochArray;
897-
ArrowArrayView* fractionArray;
898-
for (int64_t i = 0; i < field->schema->n_children; i++) {
899-
ArrowSchema* c_schema = field->schema->children[i];
900-
if (std::strcmp(c_schema->name, internal::FIELD_NAME_EPOCH.c_str()) == 0) {
901-
epochArray = columnArray->children[i];
902-
} else if (std::strcmp(c_schema->name,
903-
internal::FIELD_NAME_FRACTION.c_str()) == 0) {
904-
fractionArray = columnArray->children[i];
905-
} else {
906-
// do nothing
907-
}
908-
}
909-
910938
for (int64_t rowIdx = 0; rowIdx < columnArray->array->length; rowIdx++) {
911939
if (!ArrowArrayViewIsNull(columnArray, rowIdx)) {
912940
if (byteLength == 8) {
@@ -920,8 +948,14 @@ void CArrowTableIterator::convertTimestampTZColumn_nanoarrow(
920948
returnCode = ArrowArrayAppendInt(
921949
newArray, epoch * sf::internal::powTenSB4[6 - scale]);
922950
} else {
923-
returnCode = ArrowArrayAppendInt(
924-
newArray, epoch * sf::internal::powTenSB4[9 - scale]);
951+
// Handle overflow by falling back to microsecond precision
952+
if (has_overflow_to_downscale) {
953+
returnCode = ArrowArrayAppendInt(
954+
newArray, epoch * sf::internal::powTenSB4[6]);
955+
} else {
956+
returnCode = ArrowArrayAppendInt(
957+
newArray, epoch * sf::internal::powTenSB4[9 - scale]);
958+
}
925959
}
926960
SF_CHECK_ARROW_RC(returnCode,
927961
"[Snowflake Exception] error appending int to "
@@ -941,8 +975,14 @@ void CArrowTableIterator::convertTimestampTZColumn_nanoarrow(
941975
newArray, epoch * sf::internal::powTenSB4[6] +
942976
fraction / sf::internal::powTenSB4[3]);
943977
} else {
944-
returnCode = ArrowArrayAppendInt(
945-
newArray, epoch * sf::internal::powTenSB4[9] + fraction);
978+
// Handle overflow by falling back to microsecond precision
979+
if (has_overflow_to_downscale) {
980+
returnCode = ArrowArrayAppendInt(
981+
newArray, epoch * sf::internal::powTenSB4[6] + fraction / 1000);
982+
} else {
983+
returnCode = ArrowArrayAppendInt(
984+
newArray, epoch * sf::internal::powTenSB4[9] + fraction);
985+
}
946986
}
947987
SF_CHECK_ARROW_RC(returnCode,
948988
"[Snowflake Exception] error appending int to "

test/integ/pandas_it/test_arrow_pandas.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -438,40 +438,67 @@ def test_timestampntz(conn_cnx, scale):
438438
[
439439
"'1400-01-01 01:02:03.123456789'::timestamp as low_ts",
440440
"'9999-01-01 01:02:03.123456789789'::timestamp as high_ts",
441+
"convert_timezone('UTC', '1400-01-01 01:02:03.123456789') as low_ts",
442+
"convert_timezone('UTC', '9999-01-01 01:02:03.123456789789') as high_ts",
441443
],
442444
)
443-
def test_timestampntz_raises_overflow(conn_cnx, timestamp_str):
445+
def test_timestamp_raises_overflow(conn_cnx, timestamp_str):
444446
with conn_cnx() as conn:
445447
r = conn.cursor().execute(f"select {timestamp_str}")
446448
with pytest.raises(OverflowError, match="overflows int64 range."):
447449
r.fetch_arrow_all()
448450

449451

450-
def test_timestampntz_down_scale(conn_cnx):
452+
def test_timestamp_down_scale(conn_cnx):
451453
with conn_cnx() as conn:
452454
r = conn.cursor().execute(
453-
"select '1400-01-01 01:02:03.123456'::timestamp as low_ts, '9999-01-01 01:02:03.123456'::timestamp as high_ts"
455+
"""select '1400-01-01 01:02:03.123456'::timestamp as low_ntz,
456+
'9999-01-01 01:02:03.123456'::timestamp as high_ntz,
457+
convert_timezone('UTC', '1400-01-01 01:02:03.123456') as low_tz,
458+
convert_timezone('UTC', '9999-01-01 01:02:03.123456') as high_tz
459+
"""
454460
)
455461
table = r.fetch_arrow_all()
456-
lower_dt = table[0][0].as_py() # type: datetime
462+
lower_ntz = table[0][0].as_py() # type: datetime
457463
assert (
458-
lower_dt.year,
459-
lower_dt.month,
460-
lower_dt.day,
461-
lower_dt.hour,
462-
lower_dt.minute,
463-
lower_dt.second,
464-
lower_dt.microsecond,
464+
lower_ntz.year,
465+
lower_ntz.month,
466+
lower_ntz.day,
467+
lower_ntz.hour,
468+
lower_ntz.minute,
469+
lower_ntz.second,
470+
lower_ntz.microsecond,
465471
) == (1400, 1, 1, 1, 2, 3, 123456)
466-
higher_dt = table[1][0].as_py()
472+
higher_ntz = table[1][0].as_py() # type: datetime
467473
assert (
468-
higher_dt.year,
469-
higher_dt.month,
470-
higher_dt.day,
471-
higher_dt.hour,
472-
higher_dt.minute,
473-
higher_dt.second,
474-
higher_dt.microsecond,
474+
higher_ntz.year,
475+
higher_ntz.month,
476+
higher_ntz.day,
477+
higher_ntz.hour,
478+
higher_ntz.minute,
479+
higher_ntz.second,
480+
higher_ntz.microsecond,
481+
) == (9999, 1, 1, 1, 2, 3, 123456)
482+
483+
lower_tz = table[2][0].as_py() # type: datetime
484+
assert (
485+
lower_tz.year,
486+
lower_tz.month,
487+
lower_tz.day,
488+
lower_tz.hour,
489+
lower_tz.minute,
490+
lower_tz.second,
491+
lower_tz.microsecond,
492+
) == (1400, 1, 1, 1, 2, 3, 123456)
493+
higher_tz = table[3][0].as_py() # type: datetime
494+
assert (
495+
higher_tz.year,
496+
higher_tz.month,
497+
higher_tz.day,
498+
higher_tz.hour,
499+
higher_tz.minute,
500+
higher_tz.second,
501+
higher_tz.microsecond,
475502
) == (9999, 1, 1, 1, 2, 3, 123456)
476503

477504

0 commit comments

Comments
 (0)