narwhals-datafusion 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- narwhals_datafusion/__init__.py +28 -0
- narwhals_datafusion/dataframe.py +438 -0
- narwhals_datafusion/expr.py +477 -0
- narwhals_datafusion/expr_dt.py +130 -0
- narwhals_datafusion/expr_list.py +50 -0
- narwhals_datafusion/expr_str.py +82 -0
- narwhals_datafusion/expr_struct.py +17 -0
- narwhals_datafusion/extra_functions.py +37 -0
- narwhals_datafusion/group_by.py +35 -0
- narwhals_datafusion/namespace.py +253 -0
- narwhals_datafusion/py.typed +0 -0
- narwhals_datafusion/selectors.py +34 -0
- narwhals_datafusion/testing.py +37 -0
- narwhals_datafusion/utils.py +260 -0
- narwhals_datafusion-0.1.0.dist-info/METADATA +149 -0
- narwhals_datafusion-0.1.0.dist-info/RECORD +19 -0
- narwhals_datafusion-0.1.0.dist-info/WHEEL +4 -0
- narwhals_datafusion-0.1.0.dist-info/entry_points.txt +2 -0
- narwhals_datafusion-0.1.0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Apache DataFusion backend for Narwhals, registered via the `narwhals.plugins` entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import datafusion
|
|
9
|
+
from narwhals._utils import Version
|
|
10
|
+
from typing_extensions import TypeIs
|
|
11
|
+
|
|
12
|
+
from narwhals_datafusion.namespace import DataFusionNamespace
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
NATIVE_PACKAGE = "datafusion"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __narwhals_namespace__(version: Version) -> DataFusionNamespace: # noqa: N807
|
|
20
|
+
from narwhals_datafusion.namespace import DataFusionNamespace
|
|
21
|
+
|
|
22
|
+
return DataFusionNamespace(version=version)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_native(native_object: object) -> TypeIs[datafusion.DataFrame]:
|
|
26
|
+
import datafusion
|
|
27
|
+
|
|
28
|
+
return isinstance(native_object, datafusion.DataFrame)
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import reduce
|
|
4
|
+
from operator import and_
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import pyarrow as pa
|
|
9
|
+
from datafusion import functions as F
|
|
10
|
+
from narwhals._sql.dataframe import SQLLazyFrame
|
|
11
|
+
from narwhals._utils import (
|
|
12
|
+
Implementation,
|
|
13
|
+
Version,
|
|
14
|
+
extend_bool,
|
|
15
|
+
generate_temporary_column_name,
|
|
16
|
+
not_implemented,
|
|
17
|
+
parse_columns_to_drop,
|
|
18
|
+
)
|
|
19
|
+
from narwhals.exceptions import InvalidOperationError
|
|
20
|
+
|
|
21
|
+
from narwhals_datafusion.utils import (
|
|
22
|
+
BACKEND_VERSION,
|
|
23
|
+
catch_datafusion_exception,
|
|
24
|
+
col,
|
|
25
|
+
evaluate_exprs_and_aliases,
|
|
26
|
+
lit,
|
|
27
|
+
native_to_narwhals_dtype,
|
|
28
|
+
sort_expr,
|
|
29
|
+
window_expression,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
34
|
+
from io import BytesIO
|
|
35
|
+
from types import ModuleType
|
|
36
|
+
|
|
37
|
+
import datafusion
|
|
38
|
+
from datafusion.expr import Expr
|
|
39
|
+
from narwhals._compliant.typing import CompliantDataFrameAny
|
|
40
|
+
from narwhals._typing import _EagerAllowedImpl
|
|
41
|
+
from narwhals._utils import _LimitedContext
|
|
42
|
+
from narwhals.dataframe import LazyFrame
|
|
43
|
+
from narwhals.dtypes import DType
|
|
44
|
+
from narwhals.typing import JoinStrategy, UniqueKeepStrategy
|
|
45
|
+
from typing_extensions import Self, TypeIs
|
|
46
|
+
|
|
47
|
+
from narwhals_datafusion.expr import DataFusionExpr
|
|
48
|
+
from narwhals_datafusion.group_by import DataFusionGroupBy
|
|
49
|
+
from narwhals_datafusion.namespace import DataFusionNamespace
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DataFusionLazyFrame(
|
|
53
|
+
SQLLazyFrame["DataFusionExpr", "datafusion.DataFrame", "LazyFrame[datafusion.DataFrame]"]
|
|
54
|
+
):
|
|
55
|
+
_implementation = Implementation.UNKNOWN
|
|
56
|
+
|
|
57
|
+
def __init__(self, df: datafusion.DataFrame, *, version: Version) -> None:
|
|
58
|
+
self._native_frame: datafusion.DataFrame = df
|
|
59
|
+
self._version = version
|
|
60
|
+
self._cached_native_schema: pa.Schema | None = None
|
|
61
|
+
self._cached_columns: list[str] | None = None
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def _backend_version(self) -> tuple[int, ...]:
|
|
65
|
+
return BACKEND_VERSION
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _is_native(obj: datafusion.DataFrame | Any) -> TypeIs[datafusion.DataFrame]:
|
|
69
|
+
import datafusion
|
|
70
|
+
|
|
71
|
+
return isinstance(obj, datafusion.DataFrame)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_native(cls, data: datafusion.DataFrame, /, *, context: _LimitedContext) -> Self:
|
|
75
|
+
return cls(data, version=context._version)
|
|
76
|
+
|
|
77
|
+
def to_narwhals(self) -> LazyFrame[datafusion.DataFrame]:
|
|
78
|
+
return self._version.lazyframe(self, level="lazy")
|
|
79
|
+
|
|
80
|
+
def __narwhals_lazyframe__(self) -> Self:
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __native_namespace__(self) -> ModuleType:
|
|
84
|
+
import datafusion
|
|
85
|
+
|
|
86
|
+
return datafusion
|
|
87
|
+
|
|
88
|
+
def __narwhals_namespace__(self) -> DataFusionNamespace:
|
|
89
|
+
from narwhals_datafusion.namespace import DataFusionNamespace
|
|
90
|
+
|
|
91
|
+
return DataFusionNamespace(version=self._version)
|
|
92
|
+
|
|
93
|
+
def _iter_columns(self) -> Iterator[Expr]:
|
|
94
|
+
for name in self.columns:
|
|
95
|
+
yield col(name)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def _native_schema(self) -> pa.Schema:
|
|
99
|
+
if self._cached_native_schema is None:
|
|
100
|
+
self._cached_native_schema = self.native.schema()
|
|
101
|
+
return self._cached_native_schema
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def schema(self) -> dict[str, DType]:
|
|
105
|
+
schema = self._native_schema
|
|
106
|
+
return {field.name: native_to_narwhals_dtype(field.type, self._version) for field in schema}
|
|
107
|
+
|
|
108
|
+
def collect_schema(self) -> dict[str, DType]:
|
|
109
|
+
return self.schema
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def columns(self) -> list[str]:
|
|
113
|
+
if self._cached_columns is None:
|
|
114
|
+
self._cached_columns = list(self._native_schema.names)
|
|
115
|
+
return self._cached_columns
|
|
116
|
+
|
|
117
|
+
def _with_version(self, version: Version) -> Self:
|
|
118
|
+
return self.__class__(self.native, version=version)
|
|
119
|
+
|
|
120
|
+
def _with_native(self, df: datafusion.DataFrame) -> Self:
|
|
121
|
+
return self.__class__(df, version=self._version)
|
|
122
|
+
|
|
123
|
+
def collect(self, backend: _EagerAllowedImpl | None, **kwargs: Any) -> CompliantDataFrameAny:
|
|
124
|
+
if backend is None or backend is Implementation.PYARROW:
|
|
125
|
+
from narwhals._arrow.dataframe import ArrowDataFrame
|
|
126
|
+
|
|
127
|
+
return ArrowDataFrame(
|
|
128
|
+
self.native.to_arrow_table(),
|
|
129
|
+
validate_backend_version=True,
|
|
130
|
+
version=self._version,
|
|
131
|
+
validate_column_names=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if backend is Implementation.PANDAS:
|
|
135
|
+
from narwhals._pandas_like.dataframe import PandasLikeDataFrame
|
|
136
|
+
|
|
137
|
+
return PandasLikeDataFrame(
|
|
138
|
+
self.native.to_pandas(),
|
|
139
|
+
implementation=Implementation.PANDAS,
|
|
140
|
+
validate_backend_version=True,
|
|
141
|
+
version=self._version,
|
|
142
|
+
validate_column_names=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if backend is Implementation.POLARS:
|
|
146
|
+
from narwhals._polars.dataframe import PolarsDataFrame
|
|
147
|
+
|
|
148
|
+
return PolarsDataFrame(
|
|
149
|
+
self.native.to_polars(),
|
|
150
|
+
validate_backend_version=True,
|
|
151
|
+
version=self._version,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
msg = f"Unsupported `backend` value: {backend}" # pragma: no cover
|
|
155
|
+
raise ValueError(msg) # pragma: no cover
|
|
156
|
+
|
|
157
|
+
def head(self, n: int) -> Self:
|
|
158
|
+
return self._with_native(self.native.limit(n))
|
|
159
|
+
|
|
160
|
+
def simple_select(self, *column_names: str) -> Self:
|
|
161
|
+
return self._with_native(self.native.select(*(col(c) for c in column_names)))
|
|
162
|
+
|
|
163
|
+
def aggregate(self, *exprs: DataFusionExpr) -> Self:
|
|
164
|
+
selection = [value.alias(name) for name, value in evaluate_exprs_and_aliases(self, *exprs)]
|
|
165
|
+
try:
|
|
166
|
+
return self._with_native(self.native.aggregate([], selection))
|
|
167
|
+
except Exception as e: # noqa: BLE001
|
|
168
|
+
raise catch_datafusion_exception(e, self) from None
|
|
169
|
+
|
|
170
|
+
def select(self, *exprs: DataFusionExpr) -> Self:
|
|
171
|
+
selection = [value.alias(name) for name, value in evaluate_exprs_and_aliases(self, *exprs)]
|
|
172
|
+
try:
|
|
173
|
+
return self._with_native(self.native.select(*selection))
|
|
174
|
+
except Exception as e: # noqa: BLE001
|
|
175
|
+
raise catch_datafusion_exception(e, self) from None
|
|
176
|
+
|
|
177
|
+
def with_columns(self, *exprs: DataFusionExpr) -> Self:
|
|
178
|
+
new_columns_map = dict(evaluate_exprs_and_aliases(self, *exprs))
|
|
179
|
+
result = [
|
|
180
|
+
new_columns_map.pop(name).alias(name) if name in new_columns_map else col(name)
|
|
181
|
+
for name in self.columns
|
|
182
|
+
]
|
|
183
|
+
result.extend(value.alias(name) for name, value in new_columns_map.items())
|
|
184
|
+
try:
|
|
185
|
+
return self._with_native(self.native.select(*result))
|
|
186
|
+
except Exception as e: # noqa: BLE001
|
|
187
|
+
raise catch_datafusion_exception(e, self) from None
|
|
188
|
+
|
|
189
|
+
def _filter(self, predicate: DataFusionExpr) -> Self:
|
|
190
|
+
mask = predicate(self)[0]
|
|
191
|
+
try:
|
|
192
|
+
return self._with_native(self.native.filter(mask))
|
|
193
|
+
except Exception as e:
|
|
194
|
+
raise catch_datafusion_exception(e, self) from e
|
|
195
|
+
|
|
196
|
+
def drop(self, columns: Sequence[str], *, strict: bool) -> Self:
|
|
197
|
+
columns_to_drop = parse_columns_to_drop(self, columns, strict=strict)
|
|
198
|
+
selection = (name for name in self.columns if name not in columns_to_drop)
|
|
199
|
+
return self.simple_select(*selection)
|
|
200
|
+
|
|
201
|
+
def drop_nulls(self, subset: Sequence[str] | None) -> Self:
|
|
202
|
+
subset_ = subset if subset is not None else self.columns
|
|
203
|
+
keep_condition = reduce(and_, (col(name).is_not_null() for name in subset_))
|
|
204
|
+
return self._with_native(self.native.filter(keep_condition))
|
|
205
|
+
|
|
206
|
+
def rename(self, mapping: Mapping[str, str]) -> Self:
|
|
207
|
+
selection = (
|
|
208
|
+
col(name).alias(mapping[name]) if name in mapping else col(name)
|
|
209
|
+
for name in self.columns
|
|
210
|
+
)
|
|
211
|
+
return self._with_native(self.native.select(*selection))
|
|
212
|
+
|
|
213
|
+
def group_by(
|
|
214
|
+
self, keys: Sequence[str] | Sequence[DataFusionExpr], *, drop_null_keys: bool
|
|
215
|
+
) -> DataFusionGroupBy:
|
|
216
|
+
from narwhals_datafusion.group_by import DataFusionGroupBy
|
|
217
|
+
|
|
218
|
+
return DataFusionGroupBy(self, keys, drop_null_keys=drop_null_keys)
|
|
219
|
+
|
|
220
|
+
def sort(self, *by: str, descending: bool | Sequence[bool], nulls_last: bool) -> Self:
|
|
221
|
+
descending_flags = extend_bool(descending, len(by))
|
|
222
|
+
keys = [
|
|
223
|
+
sort_expr(name, descending=desc, nulls_last=nulls_last)
|
|
224
|
+
for name, desc in zip(by, descending_flags, strict=True)
|
|
225
|
+
]
|
|
226
|
+
return self._with_native(self.native.sort(*keys))
|
|
227
|
+
|
|
228
|
+
def top_k(self, k: int, *, by: Iterable[str], reverse: bool | Sequence[bool]) -> Self:
|
|
229
|
+
by = list(by)
|
|
230
|
+
if isinstance(reverse, bool):
|
|
231
|
+
descending = extend_bool(not reverse, len(by))
|
|
232
|
+
else:
|
|
233
|
+
descending = tuple(not rev for rev in reverse)
|
|
234
|
+
tmp_name = generate_temporary_column_name(8, self.columns, prefix="top_k_")
|
|
235
|
+
rank = window_expression(
|
|
236
|
+
F.row_number(),
|
|
237
|
+
order_by=by,
|
|
238
|
+
descending=descending,
|
|
239
|
+
nulls_last=extend_bool(True, len(by)),
|
|
240
|
+
)
|
|
241
|
+
with_rank = self.native.select(*(col(name) for name in self.columns), rank.alias(tmp_name))
|
|
242
|
+
return self._with_native(with_rank.filter(col(tmp_name) <= lit(k))).drop(
|
|
243
|
+
[tmp_name], strict=False
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def unique(
|
|
247
|
+
self,
|
|
248
|
+
subset: Sequence[str] | None,
|
|
249
|
+
*,
|
|
250
|
+
keep: UniqueKeepStrategy,
|
|
251
|
+
order_by: Sequence[str] | None,
|
|
252
|
+
) -> Self:
|
|
253
|
+
subset_ = subset or self.columns
|
|
254
|
+
if error := self._check_columns_exist(subset_):
|
|
255
|
+
raise error
|
|
256
|
+
tmp_name = generate_temporary_column_name(8, self.columns, prefix="row_index_")
|
|
257
|
+
flags = extend_bool(True, len(order_by)) if order_by and keep == "last" else None
|
|
258
|
+
if keep == "none":
|
|
259
|
+
# The group-size count must ignore `order_by`: with ORDER BY the
|
|
260
|
+
# window defaults to a cumulative frame, turning this into a
|
|
261
|
+
# running count that keeps the first row of every duplicated group.
|
|
262
|
+
expr = window_expression(F.count_star(), subset_)
|
|
263
|
+
else:
|
|
264
|
+
expr = window_expression(
|
|
265
|
+
F.row_number(),
|
|
266
|
+
subset_,
|
|
267
|
+
order_by or (),
|
|
268
|
+
descending=flags,
|
|
269
|
+
nulls_last=flags,
|
|
270
|
+
)
|
|
271
|
+
with_marker = self.native.select(
|
|
272
|
+
*(col(name) for name in self.columns), expr.alias(tmp_name)
|
|
273
|
+
)
|
|
274
|
+
return self._with_native(with_marker.filter(col(tmp_name) == lit(1))).drop(
|
|
275
|
+
[tmp_name], strict=False
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def join(
|
|
279
|
+
self,
|
|
280
|
+
other: Self,
|
|
281
|
+
*,
|
|
282
|
+
how: JoinStrategy,
|
|
283
|
+
left_on: Sequence[str] | None,
|
|
284
|
+
right_on: Sequence[str] | None,
|
|
285
|
+
suffix: str,
|
|
286
|
+
) -> Self:
|
|
287
|
+
left_columns = self.columns
|
|
288
|
+
right_columns = other.columns
|
|
289
|
+
|
|
290
|
+
if how in {"semi", "anti"}:
|
|
291
|
+
assert left_on is not None # noqa: S101
|
|
292
|
+
assert right_on is not None # noqa: S101
|
|
293
|
+
joined = self.native.join(
|
|
294
|
+
other.native,
|
|
295
|
+
left_on=list(left_on),
|
|
296
|
+
right_on=list(right_on),
|
|
297
|
+
how=how,
|
|
298
|
+
)
|
|
299
|
+
# semi/anti joins keep left columns only.
|
|
300
|
+
return self._with_native(joined.select(*(col(c) for c in left_columns)))
|
|
301
|
+
|
|
302
|
+
# Rename every right-hand column to a collision-free temporary name so
|
|
303
|
+
# references stay unambiguous, then re-select with narwhals' suffix rules.
|
|
304
|
+
tmp_names = {
|
|
305
|
+
name: generate_temporary_column_name(
|
|
306
|
+
8, [*left_columns, *right_columns], prefix=f"join_{i}_"
|
|
307
|
+
)
|
|
308
|
+
for i, name in enumerate(right_columns)
|
|
309
|
+
}
|
|
310
|
+
rhs = other.native.select(*(col(name).alias(tmp_names[name]) for name in right_columns))
|
|
311
|
+
|
|
312
|
+
if how == "cross":
|
|
313
|
+
joined = self.native.join_on(rhs, lit(True), how="inner")
|
|
314
|
+
else:
|
|
315
|
+
assert left_on is not None # noqa: S101
|
|
316
|
+
assert right_on is not None # noqa: S101
|
|
317
|
+
joined = self.native.join(
|
|
318
|
+
rhs,
|
|
319
|
+
left_on=list(left_on),
|
|
320
|
+
right_on=[tmp_names[name] for name in right_on],
|
|
321
|
+
how="full" if how == "full" else how,
|
|
322
|
+
coalesce_duplicate_keys=False,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
selection = [col(name) for name in left_columns]
|
|
326
|
+
for name in right_columns:
|
|
327
|
+
in_left = name in left_columns
|
|
328
|
+
renamed = col(tmp_names[name])
|
|
329
|
+
if how == "full":
|
|
330
|
+
if in_left:
|
|
331
|
+
selection.append(renamed.alias(f"{name}{suffix}"))
|
|
332
|
+
else:
|
|
333
|
+
selection.append(renamed.alias(name))
|
|
334
|
+
elif right_on is not None and name in right_on:
|
|
335
|
+
continue # key columns are kept from the left side only
|
|
336
|
+
elif in_left:
|
|
337
|
+
selection.append(renamed.alias(f"{name}{suffix}"))
|
|
338
|
+
else:
|
|
339
|
+
selection.append(renamed.alias(name))
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
return self._with_native(joined.select(*selection))
|
|
343
|
+
except Exception as e: # noqa: BLE001
|
|
344
|
+
raise catch_datafusion_exception(e, self) from None
|
|
345
|
+
|
|
346
|
+
def explode(self, columns: Sequence[str]) -> Self:
|
|
347
|
+
dtypes = self._version.dtypes
|
|
348
|
+
schema = self.collect_schema()
|
|
349
|
+
for name in columns:
|
|
350
|
+
dtype = schema[name]
|
|
351
|
+
if dtype != dtypes.List:
|
|
352
|
+
msg = f"`explode` operation not supported for dtype `{dtype}`, expected List type"
|
|
353
|
+
raise InvalidOperationError(msg)
|
|
354
|
+
|
|
355
|
+
if len(columns) != 1:
|
|
356
|
+
msg = (
|
|
357
|
+
"Exploding on multiple columns is not supported with the DataFusion backend since "
|
|
358
|
+
"we cannot guarantee that the exploded columns have matching element counts."
|
|
359
|
+
)
|
|
360
|
+
raise NotImplementedError(msg)
|
|
361
|
+
|
|
362
|
+
name = columns[0]
|
|
363
|
+
original_columns = self.columns
|
|
364
|
+
inner_type = self._native_schema.field(name).type.value_type
|
|
365
|
+
|
|
366
|
+
# datafusion's `unnest_columns` drops rows with empty lists and keeps
|
|
367
|
+
# nulls only via `preserve_nulls`; to match polars, route both empty
|
|
368
|
+
# and null lists through a literal-null branch and union positionally.
|
|
369
|
+
not_null_condition = col(name).is_not_null() & (F.array_length(col(name)) > lit(0))
|
|
370
|
+
non_null_rel = (
|
|
371
|
+
self.native.filter(not_null_condition)
|
|
372
|
+
.unnest_columns(name)
|
|
373
|
+
.select(*(col(c) for c in original_columns))
|
|
374
|
+
)
|
|
375
|
+
null_rel = self.native.filter(~not_null_condition).select(
|
|
376
|
+
*(
|
|
377
|
+
lit(None).cast(inner_type).alias(c) if c == name else col(c)
|
|
378
|
+
for c in original_columns
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
return self._with_native(non_null_rel.union(null_rel))
|
|
382
|
+
|
|
383
|
+
def unpivot(
|
|
384
|
+
self,
|
|
385
|
+
on: Sequence[str] | None,
|
|
386
|
+
index: Sequence[str] | None,
|
|
387
|
+
variable_name: str,
|
|
388
|
+
value_name: str,
|
|
389
|
+
) -> Self:
|
|
390
|
+
index_ = [] if index is None else list(index)
|
|
391
|
+
on_ = [c for c in self.columns if c not in index_] if on is None else list(on)
|
|
392
|
+
|
|
393
|
+
# `union` requires matching schemas, so promote mixed numeric value
|
|
394
|
+
# columns to a common type up front.
|
|
395
|
+
schema = self._native_schema
|
|
396
|
+
value_types = {schema.field(name).type for name in on_}
|
|
397
|
+
target: pa.DataType | None = None
|
|
398
|
+
if len(value_types) > 1:
|
|
399
|
+
if all(pa.types.is_integer(tp) for tp in value_types):
|
|
400
|
+
target = pa.int64()
|
|
401
|
+
elif all(pa.types.is_integer(tp) or pa.types.is_floating(tp) for tp in value_types):
|
|
402
|
+
target = pa.float64()
|
|
403
|
+
|
|
404
|
+
def value_expr(name: str) -> Expr:
|
|
405
|
+
expr = col(name)
|
|
406
|
+
return expr.cast(target) if target is not None else expr
|
|
407
|
+
|
|
408
|
+
# No native unpivot: build one projection per value column and union.
|
|
409
|
+
frames = [
|
|
410
|
+
self.native.select(
|
|
411
|
+
*(col(c) for c in index_),
|
|
412
|
+
lit(name).alias(variable_name),
|
|
413
|
+
value_expr(name).alias(value_name),
|
|
414
|
+
)
|
|
415
|
+
for name in on_
|
|
416
|
+
]
|
|
417
|
+
native = reduce(lambda left, right: left.union(right), frames)
|
|
418
|
+
return self._with_native(native)
|
|
419
|
+
|
|
420
|
+
def with_row_index(self, name: str, order_by: Sequence[str]) -> Self:
|
|
421
|
+
if not order_by:
|
|
422
|
+
msg = "Must pass `order_by` to `with_row_index` for the DataFusion backend"
|
|
423
|
+
raise TypeError(msg)
|
|
424
|
+
expr = (window_expression(F.row_number(), order_by=order_by) - lit(1)).alias(name)
|
|
425
|
+
return self._with_native(self.native.select(expr, *(col(c) for c in self.columns)))
|
|
426
|
+
|
|
427
|
+
def sink_parquet(self, file: str | Path | BytesIO) -> None:
|
|
428
|
+
if not isinstance(file, (str, Path)):
|
|
429
|
+
# `write_parquet` takes a path; `str(buffer)` would silently write
|
|
430
|
+
# to a file named after the object's repr.
|
|
431
|
+
msg = (
|
|
432
|
+
"`sink_parquet` to a file-like object is not supported for the "
|
|
433
|
+
"DataFusion backend; pass a file path instead."
|
|
434
|
+
)
|
|
435
|
+
raise NotImplementedError(msg)
|
|
436
|
+
self.native.write_parquet(str(file))
|
|
437
|
+
|
|
438
|
+
join_asof = not_implemented()
|