Skip to content

Commit 6c6411d

Browse files
committed
[SNOW-2464063] bugfix: Fix string representation for year-month subtypes
1 parent 6f48c9e commit 6c6411d

File tree

7 files changed

+74
-12
lines changed

7 files changed

+74
-12
lines changed

src/snowflake/connector/arrow_context.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,14 @@ def DECFLOAT_to_decimal(self, exponent: int, significand: bytes) -> decimal.Deci
165165
def DECFLOAT_to_numpy_float64(self, exponent: int, significand: bytes) -> float64:
166166
return numpy.float64(self.DECFLOAT_to_decimal(exponent, significand))
167167

168-
def INTERVAL_YEAR_MONTH_to_str(self, months: int) -> str:
169-
return interval_year_month_to_string(months)
170-
171-
def INTERVAL_YEAR_MONTH_to_numpy_timedelta(self, months: int) -> timedelta64:
168+
def INTERVAL_YEAR_MONTH_to_str(self, months: int, scale: int) -> str:
169+
return interval_year_month_to_string(months, scale)
170+
171+
def INTERVAL_YEAR_MONTH_to_numpy_timedelta(
172+
self, months: int, scale: int
173+
) -> timedelta64:
174+
if scale == 1: # interval year
175+
return numpy.timedelta64(months // 12, "Y")
172176
return numpy.timedelta64(months, "M")
173177

174178
def INTERVAL_DAY_TIME_int_to_numpy_timedelta(self, nanos: int) -> timedelta64:

src/snowflake/connector/converter.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,8 @@ def _BOOLEAN_to_python(
357357
return lambda value: value in ("1", "TRUE")
358358

359359
def _INTERVAL_YEAR_MONTH_to_python(self, ctx: dict[str, Any]) -> Callable:
360-
return lambda v: interval_year_month_to_string(int(v))
360+
scale = ctx["scale"]
361+
return lambda v: interval_year_month_to_string(int(v), scale)
361362

362363
def _INTERVAL_YEAR_MONTH_numpy_to_python(self, ctx: dict[str, Any]) -> Callable:
363364
return lambda v: numpy.timedelta64(int(v), "M")
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
#!/usr/bin/env python
22

33

4-
def interval_year_month_to_string(interval: int) -> str:
4+
def interval_year_month_to_string(interval: int, scale: int) -> str:
55
"""Convert a year-month interval to a string.
66
77
Args:
8-
interval: The year-month interval.
8+
interval: The year-month interval value in months.
9+
scale: The scale of the interval which represents subtype as follows:
10+
0: INTERVAL YEAR TO MONTH
11+
1: INTERVAL YEAR
12+
2: INTERVAL MONTH
913
1014
Returns:
1115
The string representation of the interval.
1216
"""
1317
sign = "+" if interval >= 0 else "-"
1418
interval = abs(interval)
19+
if scale == 2: # INTERVAL MONTH
20+
return f"{sign}{interval}"
1521
years = interval // 12
22+
if scale == 1: # INTERVAL YEAR
23+
return f"{sign}{years}"
24+
# INTERVAL YEAR TO MONTH
1625
months = interval % 12
1726
return f"{sign}{years}-{months:02}"

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,8 +481,21 @@ std::shared_ptr<sf::IColumnConverter> getConverterFromSchema(
481481
}
482482

483483
case SnowflakeType::Type::INTERVAL_YEAR_MONTH: {
484+
struct ArrowStringView scaleString = ArrowCharView(nullptr);
485+
int scale = 9;
486+
if (metadata != nullptr) {
487+
returnCode = ArrowMetadataGetValue(metadata, ArrowCharView("scale"),
488+
&scaleString);
489+
SF_CHECK_ARROW_RC_AND_RETURN(
490+
returnCode, nullptr,
491+
"[Snowflake Exception] error getting 'scale' from "
492+
"Arrow metadata, error code: %d",
493+
returnCode);
494+
scale =
495+
std::stoi(std::string(scaleString.data, scaleString.size_bytes));
496+
}
484497
converter = std::make_shared<sf::IntervalYearMonthConverter>(
485-
array, context, useNumpy);
498+
array, context, useNumpy, scale);
486499
break;
487500
}
488501

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ static constexpr char INTERVAL_YEAR_MONTH_TO_STR[] =
2525

2626
IntervalYearMonthConverter::IntervalYearMonthConverter(ArrowArrayView* array,
2727
PyObject* context,
28-
bool useNumpy)
29-
: m_array(array), m_context(context) {
28+
bool useNumpy, int scale)
29+
: m_array(array), m_context(context), m_scale(scale) {
3030
m_method = useNumpy ? INTERVAL_YEAR_MONTH_TO_NUMPY_TIMEDELTA
3131
: INTERVAL_YEAR_MONTH_TO_STR;
3232
}
@@ -36,7 +36,7 @@ PyObject* IntervalYearMonthConverter::toPyObject(int64_t rowIndex) const {
3636
Py_RETURN_NONE;
3737
}
3838
int64_t val = ArrowArrayViewGetIntUnsafe(m_array, rowIndex);
39-
return PyObject_CallMethod(m_context, m_method, "L", val);
39+
return PyObject_CallMethod(m_context, m_method, "Li", val, m_scale);
4040
}
4141

4242
IntervalDayTimeConverterInt::IntervalDayTimeConverterInt(ArrowArrayView* array,

src/snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ namespace sf {
1212
class IntervalYearMonthConverter : public IColumnConverter {
1313
public:
1414
explicit IntervalYearMonthConverter(ArrowArrayView* array, PyObject* context,
15-
bool useNumpy);
15+
bool useNumpy, int scale);
1616
virtual ~IntervalYearMonthConverter() = default;
1717

1818
PyObject* toPyObject(int64_t rowIndex) const override;
1919

2020
private:
2121
ArrowArrayView* m_array;
2222
PyObject* m_context;
23+
int m_scale;
2324
const char* m_method;
2425
};
2526

test/integ/test_interval_types.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,40 @@ def test_select_year_month_interval(conn_cnx, use_numpy, result_format):
4343
assert result == expected
4444

4545

46+
@pytest.mark.parametrize("use_numpy", [True, False])
47+
@pytest.mark.parametrize("result_format", ["json", "arrow"])
48+
@pytest.mark.parametrize("datatype", ["YEAR", "MONTH"])
49+
def test_select_year_month_interval_subtypes(
50+
conn_cnx, use_numpy, result_format, datatype
51+
):
52+
cases = ["0", "1", "-1", "999999999", "-999999999"]
53+
if use_numpy:
54+
expected = [numpy.timedelta64(int(c), datatype[0]) for c in cases]
55+
else:
56+
expected = ["+0", "+1", "-1", "+999999999", "-999999999"]
57+
58+
table = "test_year_month_interval_subtypes"
59+
values = "(" + "),(".join([f"'{c}'" for c in cases]) + ")"
60+
with conn_cnx(numpy=use_numpy) as conn:
61+
cursor = conn.cursor()
62+
cursor.execute(
63+
f"alter session set python_connector_query_result_format='{result_format}'"
64+
)
65+
66+
cursor.execute("alter session set feature_interval_types=enabled")
67+
cursor.execute(f"create or replace table {table} (c1 interval {datatype})")
68+
cursor.execute(f"insert into {table} values {values}")
69+
result = cursor.execute(f"select * from {table}").fetchall()
70+
# Validate column metadata.
71+
type_code = cursor._description[0].type_code
72+
assert (
73+
constants.FIELD_ID_TO_NAME[type_code] == "INTERVAL_YEAR_MONTH"
74+
), f"invalid column type: {type_code}"
75+
# Validate column values.
76+
result = [r[0] for r in result]
77+
assert result == expected
78+
79+
4680
@pytest.mark.parametrize("use_numpy", [True, False])
4781
@pytest.mark.parametrize("result_format", ["json", "arrow"])
4882
def test_select_day_time_interval(conn_cnx, use_numpy, result_format):

0 commit comments

Comments
 (0)