Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion benchmarks/requirements-bench.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,5 @@ pygments==2.20.0
pytest==9.1.1
pytest-codspeed==5.0.3
python-dateutil==2.9.0.post0
pytz==2026.2
rich==15.0.0
six==1.17.0
2 changes: 1 addition & 1 deletion benchmarks/test_fetch_perf.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def run() -> None:


# small-N *_gate variants: at range(2048) the measured region is ~60% SQL compile + engine, ~40% fetch, so these
# catch a fixed-cost regression (not a pure per-row one). Plus expensive scalar types (timestamptz pytz-per-row,
# catch a fixed-cost regression (not a pure per-row one). Plus expensive scalar types (timestamptz zoneinfo per row,
# blob, null-heavy), a heterogeneous per-cell-dispatch row, and the batched fetchmany loop.


Expand Down
3 changes: 0 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,6 @@ test = [ # dependencies used for running tests
"py4j",
"pyotp",
"pyspark",
"pytz",
"requests",
"urllib3",
"fsspec>=2022.11.0; sys_platform != 'win32' or platform_machine != 'ARM64'",
Expand All @@ -316,7 +315,6 @@ bench = [ # Pinned deps for the benchmark suite (see benchmarks/README.md). Mini
"pytest",
"pytest_codspeed",
"polars>=1.33.0",
"pytz",
"numpy<2; ( sys_platform != 'win32' or platform_machine != 'ARM64' ) and python_version < '3.12'",
"numpy>=2; ( sys_platform != 'win32' or platform_machine != 'ARM64' ) and python_version >= '3.12'",
"numpy>=2.3; sys_platform == 'win32' and platform_machine == 'ARM64'",
Expand All @@ -331,7 +329,6 @@ scripts = [ # dependencies used for running scripts
"pandas",
"polars>=1.33.0",
"pyarrow; sys_platform != 'win32' or platform_machine != 'ARM64'",
"pytz"
]
pypi = [ # dependencies used by the pypi cleanup script
"pyotp>=2.9.0",
Expand Down
38 changes: 23 additions & 15 deletions scripts/cache_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@
"type": "attribute",
"full_path": "datetime.timezone",
"name": "timezone",
"children": []
"children": [
"datetime.timezone.utc"
]
},
"decimal": {
"type": "module",
Expand Down Expand Up @@ -582,20 +584,6 @@
"name": "Value",
"children": []
},
"pytz": {
"type": "module",
"full_path": "pytz",
"name": "pytz",
"children": [
"pytz.timezone"
]
},
"pytz.timezone": {
"type": "attribute",
"full_path": "pytz.timezone",
"name": "timezone",
"children": []
},
"types": {
"type": "module",
"full_path": "types",
Expand Down Expand Up @@ -858,5 +846,25 @@
"full_path": "pandas.arrays.StringArray",
"name": "StringArray",
"children": []
},
"datetime.timezone.utc": {
"type": "attribute",
"full_path": "datetime.timezone.utc",
"name": "utc",
"children": []
},
"zoneinfo": {
"type": "module",
"full_path": "zoneinfo",
"name": "zoneinfo",
"children": [
"zoneinfo.ZoneInfo"
]
},
"zoneinfo.ZoneInfo": {
"type": "attribute",
"full_path": "zoneinfo.ZoneInfo",
"name": "ZoneInfo",
"children": []
}
}
5 changes: 3 additions & 2 deletions scripts/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
datetime.datetime.max
datetime.datetime.combine
datetime.timezone
datetime.timezone.utc

import decimal

Expand Down Expand Up @@ -122,9 +123,9 @@
duckdb.filesystem.ModifiedMemoryFileSystem
duckdb.Value

import pytz
import zoneinfo

pytz.timezone
zoneinfo.ZoneInfo

import types

Expand Down
16 changes: 14 additions & 2 deletions src/include/duckdb_python/import_cache/modules/datetime_module.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@

namespace duckdb {

struct DatetimeTimezoneCacheItem : public PythonImportCacheItem {

public:
DatetimeTimezoneCacheItem(optional_ptr<PythonImportCacheItem> parent)
: PythonImportCacheItem("timezone", parent), utc("utc", this) {
}
~DatetimeTimezoneCacheItem() override {
}

PythonImportCacheItem utc;
};

struct DatetimeDatetimeCacheItem : public PythonImportCacheItem {

public:
Expand Down Expand Up @@ -55,7 +67,7 @@ struct DatetimeCacheItem : public PythonImportCacheItem {
public:
DatetimeCacheItem()
: PythonImportCacheItem("datetime"), date(this), time("time", this), timedelta("timedelta", this),
datetime(this), timezone("timezone", this) {
datetime(this), timezone(this) {
}
~DatetimeCacheItem() override {
}
Expand All @@ -64,7 +76,7 @@ struct DatetimeCacheItem : public PythonImportCacheItem {
PythonImportCacheItem time;
PythonImportCacheItem timedelta;
DatetimeDatetimeCacheItem datetime;
PythonImportCacheItem timezone;
DatetimeTimezoneCacheItem timezone;
};

} // namespace duckdb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//===----------------------------------------------------------------------===//
// DuckDB
//
// duckdb_python/import_cache/modules/pytz_module.hpp
// duckdb_python/import_cache/modules/zoneinfo_module.hpp
//
//
//===----------------------------------------------------------------------===//
Expand All @@ -20,18 +20,18 @@

namespace duckdb {

struct PytzCacheItem : public PythonImportCacheItem {
struct ZoneinfoCacheItem : public PythonImportCacheItem {

public:
static constexpr const char *Name = "pytz";
static constexpr const char *Name = "zoneinfo";

public:
PytzCacheItem() : PythonImportCacheItem("pytz"), timezone("timezone", this) {
ZoneinfoCacheItem() : PythonImportCacheItem("zoneinfo"), ZoneInfo("ZoneInfo", this) {
}
~PytzCacheItem() override {
~ZoneinfoCacheItem() override {
}

PythonImportCacheItem timezone;
PythonImportCacheItem ZoneInfo;
};

} // namespace duckdb
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ struct PythonImportCache {
PathlibCacheItem pathlib;
PolarsCacheItem polars;
DuckdbCacheItem duckdb;
PytzCacheItem pytz;
TypesCacheItem types;
TypingCacheItem typing;
UuidCacheItem uuid;
CollectionsCacheItem collections;
ZoneinfoCacheItem zoneinfo;

public:
nb::handle AddCache(nb::object item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
#include "duckdb_python/import_cache/modules/pathlib_module.hpp"
#include "duckdb_python/import_cache/modules/polars_module.hpp"
#include "duckdb_python/import_cache/modules/duckdb_module.hpp"
#include "duckdb_python/import_cache/modules/pytz_module.hpp"
#include "duckdb_python/import_cache/modules/types_module.hpp"
#include "duckdb_python/import_cache/modules/typing_module.hpp"
#include "duckdb_python/import_cache/modules/uuid_module.hpp"
#include "duckdb_python/import_cache/modules/collections_module.hpp"
#include "duckdb_python/import_cache/modules/collections_module.hpp"
#include "duckdb_python/import_cache/modules/zoneinfo_module.hpp"
28 changes: 23 additions & 5 deletions src/native/python_objects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -661,11 +661,29 @@ nb::object PythonObject::FromValue(const Value &val, const LogicalType &type,
return nb::str(fallback_str.c_str(), fallback_str.size());
}
if (type.id() == LogicalTypeId::TIMESTAMP_TZ || type.id() == LogicalTypeId::TIMESTAMP_TZ_NS) {
// We have to add the timezone info
auto tz_utc = import_cache.pytz.timezone()("UTC");
auto timestamp_utc = tz_utc.attr("localize")(py_timestamp);
auto tz_info = import_cache.pytz.timezone()(client_properties.time_zone);
return timestamp_utc.attr("astimezone")(tz_info);
nb::object zone;
try {
zone = import_cache.zoneinfo.ZoneInfo()(client_properties.time_zone);
} catch (nb::python_error &e) {
if (!e.matches(PyExc_KeyError)) {
throw;
}
throw InvalidInputException(
"Time zone '%s' is not available to Python's zoneinfo. Install the 'tzdata' "
"package, or SET TimeZone to a name the IANA database has.",
client_properties.time_zone);
}
auto timestamp_utc = py_timestamp.attr("replace")(nb::arg("tzinfo") = import_cache.datetime.timezone.utc());
try {
return timestamp_utc.attr("astimezone")(zone);
} catch (nb::python_error &e) {
// The local wall time can leave Python's year range while the UTC instant is inside it.
if (!e.matches(PyExc_OverflowError)) {
throw;
}
auto fallback_str = val.ToString();
return nb::str(fallback_str.c_str(), fallback_str.size());
}
}
return py_timestamp;
}
Expand Down
6 changes: 5 additions & 1 deletion tests/fast/api/test_cursor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# simple DB API testcase

import datetime
import zoneinfo

import pytest

import duckdb
Expand Down Expand Up @@ -93,7 +96,8 @@ def test_cursor_timezone(self):
# Because the 'timezone' setting was not explicitly set for the connection
# the setting of the DBConfig is used instead
res = con1.execute("SELECT make_timestamptz(2000,01,20,03,30,59)").fetchone()
assert str(res) == "(datetime.datetime(2000, 1, 20, 3, 30, 59, tzinfo=<UTC>),)"
assert res == (datetime.datetime(2000, 1, 20, 3, 30, 59, tzinfo=zoneinfo.ZoneInfo("UTC")),)
assert res[0].tzinfo.key == "UTC"

def test_cursor_closed(self):
con = duckdb.connect(":memory:")
Expand Down
8 changes: 4 additions & 4 deletions tests/fast/api/test_native_tz.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,23 @@ def test_native_python_timestamp_timezone(self, duckdb_cursor):
res = duckdb_cursor.execute(f"select TimeRecStart as tz from '{filename}'").fetchone()
assert res[0].hour == 14
assert res[0].minute == 52
assert res[0].tzinfo.zone == "America/Los_Angeles"
assert res[0].tzinfo.key == "America/Los_Angeles"

res = duckdb_cursor.execute(f"select TimeRecStart as tz from '{filename}'").fetchall()[0]
assert res[0].hour == 14
assert res[0].minute == 52
assert res[0].tzinfo.zone == "America/Los_Angeles"
assert res[0].tzinfo.key == "America/Los_Angeles"

res = duckdb_cursor.execute(f"select TimeRecStart as tz from '{filename}'").fetchmany(1)[0]
assert res[0].hour == 14
assert res[0].minute == 52
assert res[0].tzinfo.zone == "America/Los_Angeles"
assert res[0].tzinfo.key == "America/Los_Angeles"

duckdb_cursor.execute("SET timezone='UTC';")
res = duckdb_cursor.execute(f"select TimeRecStart as tz from '{filename}'").fetchone()
assert res[0].hour == 21
assert res[0].minute == 52
assert res[0].tzinfo.zone == "UTC"
assert res[0].tzinfo.key == "UTC"

def test_native_python_time_timezone(self, duckdb_cursor):
res = duckdb_cursor.execute(f"select TimeRecStart::TIMETZ as tz from '{filename}'").fetchone()
Expand Down
9 changes: 4 additions & 5 deletions tests/fast/arrow/test_arrow_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import decimal

import pytest
import pytz

pa = pytest.importorskip("pyarrow")

Expand Down Expand Up @@ -324,10 +323,10 @@ def test_struct_of_duration(self, duckdb_cursor, constructor, unit, expected, co
@pytest.mark.parametrize(
("constructor", "unit", "expected"),
[
(pa_timestamp(), "ms", datetime.datetime(1970, 1, 1, 0, 2, 11, 72000, tzinfo=pytz.utc)),
(pa_timestamp(), "s", datetime.datetime(1970, 1, 2, 12, 24, 32, 0, tzinfo=pytz.utc)),
(pa_timestamp(), "ns", datetime.datetime(1970, 1, 1, 0, 0, 0, 131, tzinfo=pytz.utc)),
(pa_timestamp(), "us", datetime.datetime(1970, 1, 1, 0, 0, 0, 131072, tzinfo=pytz.utc)),
(pa_timestamp(), "ms", datetime.datetime(1970, 1, 1, 0, 2, 11, 72000, tzinfo=datetime.timezone.utc)),
(pa_timestamp(), "s", datetime.datetime(1970, 1, 2, 12, 24, 32, 0, tzinfo=datetime.timezone.utc)),
(pa_timestamp(), "ns", datetime.datetime(1970, 1, 1, 0, 0, 0, 131, tzinfo=datetime.timezone.utc)),
(pa_timestamp(), "us", datetime.datetime(1970, 1, 1, 0, 0, 0, 131072, tzinfo=datetime.timezone.utc)),
],
)
def test_struct_of_timestamp_tz(self, duckdb_cursor, constructor, unit, expected, col1_null, col2_null):
Expand Down
3 changes: 1 addition & 2 deletions tests/fast/arrow/test_timestamp_timezone.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime

import pytest
import pytz

import duckdb

Expand All @@ -21,7 +20,7 @@ def generate_table(current_time, precision, timezone):
class TestArrowTimestampsTimezone:
def test_timestamp_timezone(self, duckdb_cursor):
precisions = ["us", "s", "ns", "ms"]
current_time = datetime.datetime(2017, 11, 28, 23, 55, 59, tzinfo=pytz.UTC)
current_time = datetime.datetime(2017, 11, 28, 23, 55, 59, tzinfo=datetime.timezone.utc)
con = duckdb.connect()
con.execute("SET TimeZone = 'UTC'")
for precision in precisions:
Expand Down
6 changes: 3 additions & 3 deletions tests/fast/pandas/test_append_df.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import enum
import math
import pathlib
import zoneinfo
from decimal import Decimal

import pytest
import pytz

import duckdb
import numpy as np
Expand Down Expand Up @@ -416,7 +416,7 @@ def test_naive_datetime_into_timestamptz_reads_in_the_session_time_zone(self, du
duckdb_cursor.execute("CREATE TABLE t (a TIMESTAMPTZ)")
duckdb_cursor.append("t", pd.DataFrame({"a": _outside_the_sample(AWARE, naive)}))
values = _column(duckdb_cursor)
assert (values[0], values[-1]) == (AWARE, pytz.timezone(zone).localize(NAIVE))
assert (values[0], values[-1]) == (AWARE, NAIVE.replace(tzinfo=zoneinfo.ZoneInfo(zone)))

@pytest.mark.parametrize(
"zone",
Expand All @@ -439,7 +439,7 @@ def test_date_into_timestamptz_reads_as_midnight_in_the_session_time_zone(self,
duckdb_cursor.execute("CREATE TABLE t (a TIMESTAMPTZ)")
duckdb_cursor.append("t", pd.DataFrame({"a": _outside_the_sample(AWARE, datetime.date(2024, 5, 6))}))
values = _column(duckdb_cursor)
assert (values[0], values[-1]) == (AWARE, pytz.timezone(zone).localize(datetime.datetime(2024, 5, 6)))
assert (values[0], values[-1]) == (AWARE, datetime.datetime(2024, 5, 6, tzinfo=zoneinfo.ZoneInfo(zone)))

@pytest.mark.xfail(strict=True, reason=NOT_YET_DECLARED)
def test_by_name_declares_each_column_from_its_name(self, duckdb_cursor):
Expand Down
Loading
Loading