Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3d93746
ci: Unpin (some) dependencies
FBruzzesi Oct 9, 2025
5d2600a
ci(typing): Re-pin `duckdb==1.4.1`
dangotbanned Oct 9, 2025
875ec84
chore(typing): Kinda fix `lit`, `lambda_expr`
dangotbanned Oct 9, 2025
49e96f1
fix: Always use `fetch_arrow_table`
dangotbanned Oct 9, 2025
d4df1c0
fix(typing): Get `pyright` happy with `DuckDBPyType`
dangotbanned Oct 9, 2025
d110f24
ci(typing): Let `mypy` follow `duckdb` imports
dangotbanned Oct 9, 2025
2e91048
ci(typing): Disable `no-any-return` for `_duckdb`
dangotbanned Oct 9, 2025
d83ffc7
fix: Version branch deprecated `duckdb.typing` module
dangotbanned Oct 9, 2025
6c65d72
semi bump `sqlframe`
dangotbanned Oct 9, 2025
e2d2013
Merge remote-tracking branch 'upstream/main' into fix-duckdb-1-4-1-ty…
dangotbanned Oct 9, 2025
98880d0
fix: Exclude `decimal` from `nested`
dangotbanned Oct 9, 2025
54f5030
fix: Exclude `decimal` from `nested`
dangotbanned Oct 9, 2025
9c3469c
Merge branch 'fix-duckdb-1-4-1-typing' of https://github.com/narwhals…
dangotbanned Oct 9, 2025
863901c
refactor: Use the aliases from (d83ffc7adba619684bc8ef4cf7cc065504044…
dangotbanned Oct 9, 2025
98d23e9
Update pyproject.toml
dangotbanned Oct 10, 2025
9a9314a
Merge branch 'main' into fix-duckdb-1-4-1-typing
dangotbanned Oct 10, 2025
3cf4ee4
children children children
dangotbanned Oct 10, 2025
ef2d4eb
make `{Array,Enum,Decimal}Type` aliases
dangotbanned Oct 10, 2025
9bb26cf
refactor: Un-inline an import
dangotbanned Oct 10, 2025
b4e4980
rework into singular `is_dtype`, add docs
dangotbanned Oct 10, 2025
7b43748
excuse me, what?
dangotbanned Oct 10, 2025
64f6b4f
fix: It found a bug!!!
dangotbanned Oct 10, 2025
ed6f6b2
fix(typing): Align `lambda_expr` to `cpp` implementation
dangotbanned Oct 11, 2025
eb8e329
chore: Remove outdated comment
dangotbanned Oct 11, 2025
52e0955
Merge remote-tracking branch 'upstream/main' into fix-duckdb-1-4-1-ty…
dangotbanned Oct 11, 2025
e1fe121
revert: raising `UnsupportedDTypeError`
dangotbanned Oct 11, 2025
fd9de79
ci: Install `ibis` from git
dangotbanned Oct 12, 2025
f84344c
fix: mkdocs as well
dangotbanned Oct 12, 2025
7246499
Merge remote-tracking branch 'upstream/main' into fix-duckdb-1-4-1-ty…
dangotbanned Oct 12, 2025
aa9e43a
ci: maybe fix
dangotbanned Oct 12, 2025
4637e0a
why do we have a group named extra
dangotbanned Oct 12, 2025
5e2f7da
cov: leave todo for (#3197)
dangotbanned Oct 12, 2025
a58e4ce
test: fix `ibis`, `sqlframe` xpass
dangotbanned Oct 12, 2025
c7f474b
Merge branch 'main' into fix-duckdb-1-4-1-typing
dangotbanned Oct 14, 2025
cdecbc8
Merge branch 'main' into fix-duckdb-1-4-1-typing
dangotbanned Oct 15, 2025
a98a3d9
ci: Bump ibis
dangotbanned Oct 15, 2025
1409514
test: remove workarounds
dangotbanned Oct 15, 2025
fd5c275
Merge remote-tracking branch 'upstream/main' into fix-duckdb-1-4-1-ty…
dangotbanned Oct 15, 2025
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
12 changes: 4 additions & 8 deletions narwhals/_duckdb/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def to_narwhals(
if self._version is Version.V1:
from narwhals.stable.v1 import DataFrame as DataFrameV1

return DataFrameV1(self, level="interchange") # type: ignore[no-any-return]
return DataFrameV1(self, level="interchange")
return self._version.lazyframe(self, level="lazy")

def __narwhals_dataframe__(self) -> Self: # pragma: no cover
Expand All @@ -116,7 +116,7 @@ def __narwhals_lazyframe__(self) -> Self:
return self

def __native_namespace__(self) -> ModuleType:
return get_duckdb() # type: ignore[no-any-return]
return get_duckdb()

def __narwhals_namespace__(self) -> DuckDBNamespace:
from narwhals._duckdb.namespace import DuckDBNamespace
Expand All @@ -138,12 +138,8 @@ def collect(
if backend is None or backend is Implementation.PYARROW:
from narwhals._arrow.dataframe import ArrowDataFrame

if self._backend_version < (1, 4):
ret = self.native.arrow()
else: # pragma: no cover
ret = self.native.fetch_arrow_table()
return ArrowDataFrame(
ret,
self.native.fetch_arrow_table(),
validate_backend_version=True,
version=self._version,
validate_column_names=True,
Expand Down Expand Up @@ -260,7 +256,7 @@ def to_pandas(self) -> pd.DataFrame:

def to_arrow(self) -> pa.Table:
# only if version is v1, keep around for backcompat
return self.lazy().collect(Implementation.PYARROW).native # type: ignore[no-any-return]
return self.lazy().collect(Implementation.PYARROW).native

def _with_version(self, version: Version) -> Self:
return self.__class__(self.native, version=version)
Expand Down
3 changes: 2 additions & 1 deletion narwhals/_duckdb/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ def is_finite(self) -> Self:
return self._with_elementwise(lambda expr: F("isfinite", expr))

def is_in(self, other: Sequence[Any]) -> Self:
return self._with_elementwise(lambda expr: F("contains", lit(other), expr))
other_ = tuple(other) if not isinstance(other, (tuple, list)) else other
return self._with_elementwise(lambda expr: F("contains", lit(other_), expr))

def fill_null(
self,
Expand Down
5 changes: 2 additions & 3 deletions narwhals/_duckdb/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from narwhals._utils import Version
from narwhals.typing import ConcatMethod, IntoDType, NonNestedLiteral

BIGINT = duckdb_dtypes.BIGINT
VARCHAR = duckdb_dtypes.VARCHAR


Expand Down Expand Up @@ -123,9 +124,7 @@ def mean_horizontal(self, *exprs: DuckDBExpr) -> DuckDBExpr:
def func(cols: Iterable[Expression]) -> Expression:
cols = tuple(cols)
total = reduce(operator.add, (CoalesceOperator(col, lit(0)) for col in cols))
count = reduce(
operator.add, (col.isnotnull().cast(duckdb_dtypes.BIGINT) for col in cols)
)
count = reduce(operator.add, (col.isnotnull().cast(BIGINT) for col in cols))
return total / count

return self._expr._from_elementwise_horizontal_op(func, *exprs)
Expand Down
2 changes: 1 addition & 1 deletion narwhals/_duckdb/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __narwhals_series__(self) -> Self:
return self

def __native_namespace__(self) -> ModuleType:
return get_duckdb() # type: ignore[no-any-return]
return get_duckdb()

@property
def dtype(self) -> DType:
Expand Down
138 changes: 135 additions & 3 deletions narwhals/_duckdb/typing.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
from __future__ import annotations

from typing import TYPE_CHECKING, TypedDict
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Literal, Protocol, TypedDict, Union, overload

import duckdb
from duckdb import Expression

from narwhals._typing_compat import TypeVar

if TYPE_CHECKING:
from collections.abc import Sequence
import uuid

import numpy as np
import pandas as pd
from duckdb import DuckDBPyConnection
from typing_extensions import TypeAlias, TypeIs

from narwhals.typing import Into1DArray, PythonLiteral

from duckdb import Expression

__all__ = [
"BaseType",
"IntoColumnExpr",
"WindowExpressionKwargs",
"has_children",
"is_dtype",
]

IntoDuckDBLiteral: TypeAlias = """
PythonLiteral
| dict[Any, Any]
| uuid.UUID
| bytearray
| memoryview
| Into1DArray
| pd.api.typing.NaTType
| pd.api.typing.NAType
| np.ma.MaskedArray
| duckdb.Value
"""


class WindowExpressionKwargs(TypedDict, total=False):
Expand All @@ -16,3 +49,102 @@ class WindowExpressionKwargs(TypedDict, total=False):
descending: Sequence[bool]
nulls_last: Sequence[bool]
ignore_nulls: bool


_Children_co = TypeVar(
"_Children_co",
covariant=True,
bound=Sequence[tuple[str, Any]],
default=Sequence[tuple[str, Any]],
)
DTypeT_co = TypeVar("DTypeT_co", covariant=True, bound="BaseType", default="BaseType")
_Child: TypeAlias = tuple[Literal["child"], DTypeT_co]
_Size: TypeAlias = tuple[Literal["size"], int]
_ID_co = TypeVar("_ID_co", bound=str, default=str, covariant=True)
_Array: TypeAlias = Literal["array"]
_Struct: TypeAlias = Literal["struct"]
_List: TypeAlias = Literal["list"]
_Enum: TypeAlias = Literal["enum"]
_Decimal: TypeAlias = Literal["decimal"]
_TimestampTZ: TypeAlias = Literal["timestamp with time zone"]
IntoColumnExpr: TypeAlias = Union[str, Expression]
"""A column name, or the result of calling `duckdb.ColumnExpression`."""


class BaseType(Protocol[_ID_co]):
"""Structural equivalent to [`DuckDBPyType`].

Excludes attributes which are unsafe to use on most types.

[`DuckDBPyType`]: https://github.com/duckdb/duckdb-python/blob/df7789cbd31b2d2b8d03d012f14331bc3297fb2d/_duckdb-stubs/_sqltypes.pyi#L35-L75
"""

def __eq__(self, other: object) -> bool: ...
def __hash__(self) -> int: ...
@overload
def __init__(self, type_str: str, connection: DuckDBPyConnection) -> None: ...
@overload
def __init__(self, obj: object) -> None: ...
@property
def id(self) -> _ID_co: ...


def has_children(
dtype: BaseType | _ParentType[_ID_co, _Children_co],
) -> TypeIs[_ParentType[_ID_co, _Children_co]]:
"""Return True if `dtype.children` can be accessed safely.

`_hasattr_static` returns True on *any* [`DuckDBPyType`], so the only way to be sure is by forcing an exception.

[`DuckDBPyType`]: https://github.com/duckdb/duckdb-python/blob/df7789cbd31b2d2b8d03d012f14331bc3297fb2d/_duckdb-stubs/_sqltypes.pyi#L35-L75
"""
try:
return hasattr(dtype, "children")
except duckdb.InvalidInputException:
return False


@overload
def is_dtype(obj: BaseType, type_id: _Array, /) -> TypeIs[ArrayType]: ...
@overload
def is_dtype(obj: BaseType, type_id: _Struct, /) -> TypeIs[StructType]: ...
@overload
def is_dtype(obj: BaseType, type_id: _List, /) -> TypeIs[ListType]: ...
@overload
def is_dtype(obj: BaseType, type_id: _Enum, /) -> TypeIs[EnumType]: ...
@overload
def is_dtype(obj: BaseType, type_id: _Decimal, /) -> TypeIs[DecimalType]: ...
@overload
def is_dtype(
obj: BaseType, type_id: _TimestampTZ, /
) -> TypeIs[BaseType[_TimestampTZ]]: ...
def is_dtype(
obj: BaseType, type_id: _Array | _Struct | _List | _Enum | _Decimal | _TimestampTZ, /
) -> bool:
"""Return True if `obj` is the [`DuckDBPyType`] corresponding with `type_id`.

[`DuckDBPyType`]: https://github.com/duckdb/duckdb-python/blob/df7789cbd31b2d2b8d03d012f14331bc3297fb2d/_duckdb-stubs/_sqltypes.pyi#L35-L75
"""
return obj.id == type_id


class _ParentType(BaseType[_ID_co], Protocol[_ID_co, _Children_co]):
@property
def children(self) -> _Children_co: ...


ArrayType: TypeAlias = _ParentType[_Array, tuple[_Child[DTypeT_co], _Size]]
EnumType: TypeAlias = _ParentType[_Enum, tuple[tuple[Literal["values"], list[str]]]]
DecimalType: TypeAlias = _ParentType[
_Decimal, tuple[tuple[Literal["precision"], int], tuple[Literal["scale"], int]]
]


class ListType(_ParentType[_List, tuple[_Child[DTypeT_co]]], Protocol[DTypeT_co]):
@property
def child(self) -> DTypeT_co: ...


class StructType(_ParentType[_Struct, Sequence[tuple[str, BaseType]]], Protocol):
def __getattr__(self, name: str) -> BaseType: ...
def __getitem__(self, name: str) -> BaseType: ...
Loading
Loading