Skip to content
Merged
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
102 changes: 53 additions & 49 deletions duckdb/_dbapi_type_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
- DuckDB Type System: https://duckdb.org/docs/sql/data_types/overview
"""

from duckdb import sqltypes
import collections.abc

from duckdb.sqltypes import DuckDBPyType


class DBAPITypeObject:
Expand All @@ -35,36 +37,40 @@ class DBAPITypeObject:
It allows checking whether a specific DuckDB type belongs to a broader
category like STRING, NUMBER, DATETIME, etc.

The type object supports equality comparison with DuckDBPyType instances,
returning True if the type belongs to this category.
A category is a set of type ids, as DuckDBPyType.id gives them. A DuckDBPyType
belongs to the category when its id is in the set, whatever its parameters
(such as the width and scale of a DECIMAL) or its alias (such as JSON, a VARCHAR).

Args:
types: A list of DuckDBPyType instances that belong to this type category.
type_ids: Lowercase type ids of the types that belong to this category.

Example:
>>> string_types = DBAPITypeObject([sqltypes.VARCHAR, sqltypes.CHAR])
>>> result = sqltypes.VARCHAR == string_types # True
>>> result = sqltypes.INTEGER == string_types # False
>>> string_types = DBAPITypeObject({"varchar"})
>>> result = duckdb.sqltypes.VARCHAR == string_types # True
>>> result = duckdb.sqltypes.INTEGER == string_types # False

Note:
This follows the DB API 2.0 specification where type objects are compared
using equality operators rather than isinstance() checks.
"""

def __init__(self, types: list[sqltypes.DuckDBPyType]) -> None:
def __init__(self, type_ids: collections.abc.Iterable[str]) -> None:
"""Initialize a DB API type object.

Args:
types: List of DuckDB types that belong to this category.
type_ids: Lowercase type ids, as DuckDBPyType.id gives them, of the types that belong to this category.
"""
self.types = types
if isinstance(type_ids, str):
msg = "type_ids must be a collection of type ids, not a single str"
raise TypeError(msg)
self.type_ids = frozenset(type_ids)

def __eq__(self, other: object) -> bool:
"""Check if a DuckDB type belongs to this type category.

This method implements the DB API 2.0 type checking mechanism.
It returns True if the other object is a DuckDBPyType that
is contained in this type category.
It returns True if the other object is a DuckDBPyType whose id
is in this type category.

Args:
other: The object to compare, typically a DuckDBPyType instance.
Expand All @@ -73,34 +79,32 @@ def __eq__(self, other: object) -> bool:
True if other is a DuckDBPyType in this category, False otherwise.

Example:
>>> NUMBER == sqltypes.INTEGER # True
>>> NUMBER == sqltypes.VARCHAR # False
>>> NUMBER == duckdb.sqltypes.INTEGER # True
>>> NUMBER == duckdb.sqltypes.VARCHAR # False
"""
if isinstance(other, sqltypes.DuckDBPyType):
return other in self.types
return False
return isinstance(other, DuckDBPyType) and other.id in self.type_ids

def __repr__(self) -> str:
"""Return a string representation of this type object.

Returns:
A string showing the type object and its contained DuckDB types.
A string listing the type ids of this category, uppercase and sorted.

Example:
>>> repr(STRING)
'<DBAPITypeObject [VARCHAR]>'
"""
return f"<DBAPITypeObject [{','.join(str(x) for x in self.types)}]>"
return f"<DBAPITypeObject [{','.join(sorted(type_id.upper() for type_id in self.type_ids))}]>"


# Define the standard DB API 2.0 type objects for DuckDB

STRING = DBAPITypeObject([sqltypes.VARCHAR])
STRING = DBAPITypeObject({"varchar"})
"""
STRING type object for text-based database columns.

This type object represents all string/text types in DuckDB. Currently includes:
- VARCHAR: Variable-length character strings
- VARCHAR: Variable-length character strings, and the types that alias it, such as JSON

Use this to check if a column contains textual data that should be handled
as Python strings.
Expand All @@ -113,22 +117,22 @@ def __repr__(self) -> str:
"""

NUMBER = DBAPITypeObject(
[
sqltypes.TINYINT,
sqltypes.UTINYINT,
sqltypes.SMALLINT,
sqltypes.USMALLINT,
sqltypes.INTEGER,
sqltypes.UINTEGER,
sqltypes.BIGINT,
sqltypes.UBIGINT,
sqltypes.HUGEINT,
sqltypes.UHUGEINT,
sqltypes.DuckDBPyType("BIGNUM"),
sqltypes.DuckDBPyType("DECIMAL"),
sqltypes.FLOAT,
sqltypes.DOUBLE,
]
{
"tinyint",
"utinyint",
"smallint",
"usmallint",
"integer",
"uinteger",
"bigint",
"ubigint",
"hugeint",
"uhugeint",
"bignum",
"decimal",
"float",
"double",
}
)
"""
NUMBER type object for numeric database columns.
Expand All @@ -144,7 +148,7 @@ def __repr__(self) -> str:

Decimal Types:
- BIGNUM: Arbitrary precision integers
- DECIMAL: Fixed-point decimal numbers
- DECIMAL: Fixed-point decimal numbers, of every width and scale

Floating Point Types:
- FLOAT: 32-bit floating point
Expand All @@ -161,16 +165,16 @@ def __repr__(self) -> str:
"""

DATETIME = DBAPITypeObject(
[
sqltypes.DATE,
sqltypes.TIME,
sqltypes.TIME_TZ,
sqltypes.TIMESTAMP,
sqltypes.TIMESTAMP_TZ,
sqltypes.TIMESTAMP_NS,
sqltypes.TIMESTAMP_MS,
sqltypes.TIMESTAMP_S,
]
{
"date",
"time",
"time with time zone",
"timestamp",
"timestamp with time zone",
"timestamp_ns",
"timestamp_ms",
"timestamp_s",
}
)
"""
DATETIME type object for date and time database columns.
Expand Down Expand Up @@ -201,7 +205,7 @@ def __repr__(self) -> str:
>>> cursor.description[2][1] == DATETIME # Check if third column is date/time
"""

BINARY = DBAPITypeObject([sqltypes.BLOB])
BINARY = DBAPITypeObject({"blob"})
"""
BINARY type object for binary data database columns.

Expand Down
4 changes: 4 additions & 0 deletions src/include/duckdb_python/pyconnection/pyconnection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ struct DefaultConnectionHolder {
public:
std::shared_ptr<DuckDBPyConnection> Get();
void Set(std::shared_ptr<DuckDBPyConnection> conn);
//! Once the module's cleanup ran and the interpreter is finalizing, the holder neither opens nor keeps a
//! connection, because nothing would close it
void Release();

private:
std::shared_ptr<DuckDBPyConnection> connection;
bool released = false;
mutex l;
};

Expand Down
75 changes: 67 additions & 8 deletions src/pyconnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2155,18 +2155,77 @@ void DuckDBPyConnection::LoadExtension(const string &extension) {
ExtensionHelper::LoadExternalExtension(*connection.context, extension_opts);
}

// sys.is_finalizing is asked because Py_IsFinalizing is public only from Python 3.13; without sys the interpreter is
// finalizing.
static bool InterpreterIsFinalizing() {
PyObject *is_finalizing = PySys_GetObject("is_finalizing");
if (!is_finalizing) {
return true;
}
PyObject *result = PyObject_CallNoArgs(is_finalizing);
if (!result) {
PyErr_Clear();
return true;
}
const int truth = PyObject_IsTrue(result);
Py_DECREF(result);
if (truth < 0) {
PyErr_Clear();
return true;
}
return truth != 0;
}

// The mutex guards only reads and swaps of the pointer: opening or destroying a connection releases the GIL and can run
// Python code that fetches the default connection again, so both happen after the mutex is released.
std::shared_ptr<DuckDBPyConnection> DefaultConnectionHolder::Get() {
lock_guard<mutex> guard(l);
if (!connection || connection->con.ConnectionIsClosed()) {
nb::dict config_dict;
connection = DuckDBPyConnection::Connect(nb::str(":memory:"), false, config_dict);
bool was_released;
{
lock_guard<mutex> guard(l);
if (connection && !connection->con.ConnectionIsClosed()) {
return connection;
}
was_released = released;
}
if (was_released && InterpreterIsFinalizing()) {
throw ConnectionException("the default connection is not available during interpreter shutdown");
}
nb::dict config_dict;
auto opened = DuckDBPyConnection::Connect(nb::str(":memory:"), false, config_dict);
std::shared_ptr<DuckDBPyConnection> replaced;
{
lock_guard<mutex> guard(l);
if (connection && !connection->con.ConnectionIsClosed()) {
return connection;
}
replaced = std::move(connection);
connection = opened;
}
return connection;
return opened;
}

void DefaultConnectionHolder::Set(std::shared_ptr<DuckDBPyConnection> conn) {
lock_guard<mutex> guard(l);
connection = conn;
bool was_released;
{
lock_guard<mutex> guard(l);
was_released = released;
}
if (was_released && InterpreterIsFinalizing()) {
return;
}
{
lock_guard<mutex> guard(l);
connection.swap(conn);
}
}

void DefaultConnectionHolder::Release() {
std::shared_ptr<DuckDBPyConnection> released_connection;
{
lock_guard<mutex> guard(l);
released = true;
connection.swap(released_connection);
}
}

void DuckDBPyConnection::Cursors::AddCursor(std::shared_ptr<DuckDBPyConnection> conn) {
Expand Down Expand Up @@ -2519,7 +2578,7 @@ void DuckDBPyConnection::Exit(DuckDBPyConnection &self, const nb::object &exc_ty
}

void DuckDBPyConnection::Cleanup() {
GetModuleState().default_connection.Set(nullptr);
GetModuleState().default_connection.Release();
GetModuleState().import_cache.reset();
GetModuleState().consumed_arrow_readers.reset();
}
Expand Down
13 changes: 13 additions & 0 deletions src/typing/pytype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,19 @@ void DuckDBPyType::Initialize(nb::handle &m) {
nb::is_operator());
type_module.def("__eq__", &DuckDBPyType::EqualsString, "Compare two types for equality", nb::arg("other"),
nb::is_operator());
// Binds every other operand in nanobind's strict first pass, because its implicit conversion pass prints to
// stderr each time the conversion to DuckDBPyType fails; registered after the str overload, so a plain str
// operand is compared by its text and never parsed.
type_module.def(
"__eq__",
[](const DuckDBPyType &self, const nb::object &other) -> nb::object {
std::unique_ptr<DuckDBPyType> converted;
if (!DuckDBPyType::TryConvert(other, converted)) {
return nb::borrow(nb::handle(Py_NotImplemented));
}
return nb::bool_(self.Equals(*converted));
},
"Compare two types for equality", nb::arg("other").none(), nb::is_operator());
type_module.def("__hash__", [](const DuckDBPyType &type) {
auto s = type.ToString();
return nb::hash(nb::str(s.c_str(), s.size()));
Expand Down
Loading
Loading