batchcorder-datafusion 55.0.0__cp310-abi3-win_amd64.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.
- batchcorder_datafusion/__init__.py +36 -0
- batchcorder_datafusion/_batchcorder_datafusion.pyd +4 -0
- batchcorder_datafusion/_batchcorder_datafusion.pyi +35 -0
- batchcorder_datafusion/_hosts.py +168 -0
- batchcorder_datafusion/py.typed +0 -0
- batchcorder_datafusion-55.0.0.dist-info/METADATA +92 -0
- batchcorder_datafusion-55.0.0.dist-info/RECORD +9 -0
- batchcorder_datafusion-55.0.0.dist-info/WHEEL +4 -0
- batchcorder_datafusion-55.0.0.dist-info/sboms/batchcorder-datafusion-py.cyclonedx.json +10041 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataFusion table provider that tees one read-once Arrow stream to every scan.
|
|
3
|
+
|
|
4
|
+
Register a :class:`TeeTableProvider` with a ``SessionContext`` from a
|
|
5
|
+
DataFusion host (datafusion-python or xorq-datafusion) and every scan of the table in a query replays the same
|
|
6
|
+
upstream stream, which is read only once. With the default
|
|
7
|
+
``bounding="sealed"``, each planned scan reserves a reader at planning time,
|
|
8
|
+
and each batch is evicted as soon as every scan has read past it.
|
|
9
|
+
|
|
10
|
+
Example::
|
|
11
|
+
|
|
12
|
+
import pyarrow as pa
|
|
13
|
+
from datafusion import SessionContext
|
|
14
|
+
from batchcorder_datafusion import TeeTableProvider
|
|
15
|
+
|
|
16
|
+
ctx = SessionContext()
|
|
17
|
+
ctx.register_table("t", TeeTableProvider(reader))
|
|
18
|
+
ctx.sql("select * from t a join t b using (k)").collect()
|
|
19
|
+
|
|
20
|
+
A sealed provider serves one query. Register a new provider per query, or
|
|
21
|
+
pass ``bounding="unbounded"`` for a table that is queried repeatedly.
|
|
22
|
+
|
|
23
|
+
The host must be built on the same DataFusion major as this wheel.
|
|
24
|
+
Registering the provider with any other host raises :class:`RuntimeError`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from importlib.metadata import version
|
|
30
|
+
|
|
31
|
+
from ._batchcorder_datafusion import TeeTableProvider
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["TeeTableProvider"]
|
|
35
|
+
|
|
36
|
+
__version__: str = version("batchcorder-datafusion")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import pyarrow as pa
|
|
4
|
+
|
|
5
|
+
__all__ = ["TeeTableProvider"]
|
|
6
|
+
|
|
7
|
+
@typing.final
|
|
8
|
+
class TeeTableProvider:
|
|
9
|
+
def __new__(
|
|
10
|
+
cls,
|
|
11
|
+
reader: typing.Any,
|
|
12
|
+
*,
|
|
13
|
+
memory_capacity: int | None = None,
|
|
14
|
+
disk_path: str | None = None,
|
|
15
|
+
disk_capacity: int | None = None,
|
|
16
|
+
write_policy: str = "on_insertion",
|
|
17
|
+
bounding: str = "sealed",
|
|
18
|
+
) -> TeeTableProvider: ...
|
|
19
|
+
def __datafusion_table_provider__(self, session: typing.Any) -> typing.Any: ...
|
|
20
|
+
@property
|
|
21
|
+
def schema(self) -> pa.Schema: ...
|
|
22
|
+
@property
|
|
23
|
+
def scans_planned(self) -> int: ...
|
|
24
|
+
@property
|
|
25
|
+
def scans_executed(self) -> int: ...
|
|
26
|
+
@property
|
|
27
|
+
def ingested_count(self) -> int: ...
|
|
28
|
+
@property
|
|
29
|
+
def evicted_count(self) -> int: ...
|
|
30
|
+
@property
|
|
31
|
+
def upstream_exhausted(self) -> bool: ...
|
|
32
|
+
@property
|
|
33
|
+
def is_sealed(self) -> bool: ...
|
|
34
|
+
def seal(self) -> None: ...
|
|
35
|
+
def close(self) -> None: ...
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Check that a DataFusion host is built on the same DataFusion major as this wheel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# The provider crosses into the host through datafusion-ffi, whose structs are
|
|
16
|
+
# only compatible within one DataFusion major. Each entry maps a host
|
|
17
|
+
# distribution to the half-open range [low, high) of its versions built on the
|
|
18
|
+
# same DataFusion major as this wheel (datafusion-ffi in Cargo.toml).
|
|
19
|
+
#
|
|
20
|
+
# Keep this table and the compatibility table in README.md in sync.
|
|
21
|
+
SUPPORTED_HOSTS: dict[str, tuple[tuple[int, ...], tuple[int, ...]]] = {
|
|
22
|
+
"datafusion": ((55,), (56,)),
|
|
23
|
+
"xorq-datafusion": ((0, 3), (0, 4)),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Top-level Python package of each host -> its distribution. A session class
|
|
27
|
+
# belongs to a host when its module is in the package, or when the package
|
|
28
|
+
# exports it (see `_host_distribution`).
|
|
29
|
+
HOST_PACKAGES: dict[str, str] = {
|
|
30
|
+
"datafusion": "datafusion",
|
|
31
|
+
"xorq_datafusion": "xorq-datafusion",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
DATAFUSION_MAJOR = 55
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _release(text: str) -> tuple[int, ...]:
|
|
38
|
+
"""
|
|
39
|
+
Return the leading numeric release segment of a version string.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
text
|
|
44
|
+
A version such as ``"55.0.0"`` or ``"0.3.0rc1"``.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
tuple[int, ...]
|
|
49
|
+
The release numbers, e.g. ``(55, 0, 0)`` or ``(0, 3, 0)``.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
match = re.match(r"\d+(?:\.\d+)*", text)
|
|
53
|
+
if match is None:
|
|
54
|
+
return ()
|
|
55
|
+
return tuple(int(part) for part in match.group().split("."))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _format(release: tuple[int, ...]) -> str:
|
|
59
|
+
return ".".join(map(str, release))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _exports(package: str, cls: type) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Tell whether ``package`` or one of its imported submodules exports ``cls``.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
package
|
|
69
|
+
A host's top-level Python package.
|
|
70
|
+
cls
|
|
71
|
+
The class of the session the host passed.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
bool
|
|
76
|
+
``True`` if an imported module of ``package`` has ``cls`` as an
|
|
77
|
+
attribute.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
prefix = package + "."
|
|
81
|
+
# Only modules already imported: the host imported its own package to
|
|
82
|
+
# build the session, and importing one here could have side effects.
|
|
83
|
+
for name, module in list(sys.modules.items()):
|
|
84
|
+
if module is None or not (name == package or name.startswith(prefix)):
|
|
85
|
+
continue
|
|
86
|
+
if any(value is cls for value in vars(module).values()):
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _host_distribution(session: object) -> str | None:
|
|
92
|
+
"""
|
|
93
|
+
Return the distribution of the host that ``session`` belongs to.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
session
|
|
98
|
+
The ``session`` argument the host passed to
|
|
99
|
+
``__datafusion_table_provider__``.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
str | None
|
|
104
|
+
The host's distribution, or ``None`` for an unknown host.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
cls = type(session)
|
|
108
|
+
dist = HOST_PACKAGES.get(cls.__module__.partition(".")[0])
|
|
109
|
+
if dist is not None or cls.__module__ in sys.modules:
|
|
110
|
+
return dist
|
|
111
|
+
# A native (PyO3) class reports the module name given at build time,
|
|
112
|
+
# which need not be a real module: xorq-datafusion's SessionContext says
|
|
113
|
+
# `let`. Find the host package that exports the class itself. A class
|
|
114
|
+
# from a real module (a capsule is `builtins.PyCapsule`, which hosts
|
|
115
|
+
# re-export) is never looked up this way.
|
|
116
|
+
for package, dist in HOST_PACKAGES.items():
|
|
117
|
+
if _exports(package, cls):
|
|
118
|
+
return dist
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def check_host(
|
|
123
|
+
session: object,
|
|
124
|
+
get_version: Callable[[str], str] = version,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Raise if ``session`` belongs to a host built on another DataFusion major.
|
|
128
|
+
|
|
129
|
+
Called by ``TeeTableProvider.__datafusion_table_provider__`` before any
|
|
130
|
+
FFI struct of the host is touched. The host is identified by the package
|
|
131
|
+
of the session's class, or by the host package that exports that class.
|
|
132
|
+
A codec capsule, or a session from an unknown host, cannot be identified
|
|
133
|
+
here; the provider then checks the datafusion-ffi version of the host's
|
|
134
|
+
codec instead.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
session
|
|
139
|
+
The ``session`` argument the host passed to
|
|
140
|
+
``__datafusion_table_provider__``.
|
|
141
|
+
get_version
|
|
142
|
+
Callable mapping a distribution name to its installed version and
|
|
143
|
+
raising :class:`importlib.metadata.PackageNotFoundError` when it is
|
|
144
|
+
not installed. Defaults to :func:`importlib.metadata.version`.
|
|
145
|
+
|
|
146
|
+
Raises
|
|
147
|
+
------
|
|
148
|
+
RuntimeError
|
|
149
|
+
If the host's installed version is outside its supported range.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
dist = _host_distribution(session)
|
|
153
|
+
if dist is None:
|
|
154
|
+
return
|
|
155
|
+
try:
|
|
156
|
+
installed = get_version(dist)
|
|
157
|
+
except PackageNotFoundError:
|
|
158
|
+
return
|
|
159
|
+
low, high = SUPPORTED_HOSTS[dist]
|
|
160
|
+
if low <= _release(installed) < high:
|
|
161
|
+
return
|
|
162
|
+
raise RuntimeError(
|
|
163
|
+
f"TeeTableProvider cannot be registered with {dist} {installed}: "
|
|
164
|
+
f"batchcorder-datafusion is built on DataFusion {DATAFUSION_MAJOR} and "
|
|
165
|
+
f"needs {dist}>={_format(low)},<{_format(high)}. Install the "
|
|
166
|
+
f"batchcorder-datafusion release built on the same DataFusion major "
|
|
167
|
+
f"as {dist} {installed}, or a {dist} in that range."
|
|
168
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: batchcorder-datafusion
|
|
3
|
+
Version: 55.0.0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Dist: pyarrow>=18
|
|
14
|
+
Summary: DataFusion table provider that tees one read-once Arrow stream to every scan in a query
|
|
15
|
+
Author: Daniel Mesejo
|
|
16
|
+
License: Apache-2.0
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
19
|
+
Project-URL: Homepage, https://github.com/mesejo/batchcorder
|
|
20
|
+
Project-URL: Repository, https://github.com/mesejo/batchcorder
|
|
21
|
+
|
|
22
|
+
# batchcorder-datafusion
|
|
23
|
+
|
|
24
|
+
A DataFusion table provider that tees one read-once Arrow stream to every
|
|
25
|
+
scan in a query.
|
|
26
|
+
|
|
27
|
+
Register a `TeeTableProvider` with a DataFusion `SessionContext`, and every
|
|
28
|
+
scan of the table in a query (self-joins, CTEs referenced twice, subqueries)
|
|
29
|
+
replays the same upstream stream, which is read only once. With the
|
|
30
|
+
default `bounding="sealed"`, each planned scan reserves a reader at planning
|
|
31
|
+
time, and each batch is evicted as soon as every scan has read past it.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
The provider plugs into a DataFusion host through `datafusion-ffi`. It does
|
|
36
|
+
not install a host: install the one you use, in a version from the
|
|
37
|
+
compatibility table below.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install batchcorder-datafusion "datafusion>=55,<56" # datafusion-python
|
|
41
|
+
pip install batchcorder-datafusion "xorq-datafusion>=0.3,<0.4" # xorq-datafusion
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import pyarrow as pa
|
|
48
|
+
from datafusion import SessionContext
|
|
49
|
+
|
|
50
|
+
from batchcorder_datafusion import TeeTableProvider
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
schema = pa.schema([("k", pa.int64())])
|
|
54
|
+
reader = pa.RecordBatchReader.from_batches(
|
|
55
|
+
schema, [pa.record_batch([[1, 2, 3]], schema=schema)]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
ctx = SessionContext()
|
|
59
|
+
ctx.register_table("t", TeeTableProvider(reader))
|
|
60
|
+
ctx.sql("select * from t a join t b using (k)").collect()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The upstream can be any object that implements the Arrow PyCapsule stream
|
|
64
|
+
interface (`__arrow_c_stream__`).
|
|
65
|
+
|
|
66
|
+
A sealed provider serves one query. Register a new provider per query, or
|
|
67
|
+
pass `bounding="unbounded"` for a table that is queried repeatedly.
|
|
68
|
+
|
|
69
|
+
### Do not pass a `batchcorder.StreamCache`
|
|
70
|
+
|
|
71
|
+
The provider keeps its own cache. Passing a `batchcorder` reader caches the
|
|
72
|
+
data twice. Pass the original upstream reader, unless you also need to replay
|
|
73
|
+
the stream outside DataFusion.
|
|
74
|
+
|
|
75
|
+
## Compatibility
|
|
76
|
+
|
|
77
|
+
`datafusion-ffi` structs are only compatible within one DataFusion major, so
|
|
78
|
+
the provider and the host must be built on the same one. The major version of
|
|
79
|
+
`batchcorder-datafusion` is that DataFusion major.
|
|
80
|
+
|
|
81
|
+
| batchcorder-datafusion | DataFusion | datafusion (datafusion-python) | xorq-datafusion |
|
|
82
|
+
|------------------------|------------|--------------------------------|-----------------|
|
|
83
|
+
| 55.x | 55 | `>=55,<56` | `>=0.3,<0.4` |
|
|
84
|
+
|
|
85
|
+
Registering a `TeeTableProvider` with a host outside these ranges raises
|
|
86
|
+
`RuntimeError`, naming the host version found and the range needed, before any
|
|
87
|
+
data crosses the FFI boundary.
|
|
88
|
+
|
|
89
|
+
`batchcorder-datafusion` does not depend on the `batchcorder` package. The
|
|
90
|
+
cache is compiled into its extension, and data enters through the Arrow C
|
|
91
|
+
stream interface.
|
|
92
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
batchcorder_datafusion/__init__.py,sha256=iP9G6JsrPgzSa4pG-1qNRNSz360d0Ds40dQosinSiAg,1293
|
|
2
|
+
batchcorder_datafusion/_batchcorder_datafusion.pyd,sha256=9V5k6wFh4bCdMhsWvCA8qLmVvv9Bq7dS0hkRIkZieGk,61756416
|
|
3
|
+
batchcorder_datafusion/_batchcorder_datafusion.pyi,sha256=Rw2X2PUdtQVfQ9E3Z6HlL9yCs7Yc4bshPtvhlCCNpcg,975
|
|
4
|
+
batchcorder_datafusion/_hosts.py,sha256=d7k1tnuA7E0v4faZbECWpsUHc-GQhHhQ2Zvwi4I7yCA,5508
|
|
5
|
+
batchcorder_datafusion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
batchcorder_datafusion-55.0.0.dist-info/METADATA,sha256=qqChFaj322NtiuVsZHPfvNA4IZzJ-kA5yF200fxWXMs,3590
|
|
7
|
+
batchcorder_datafusion-55.0.0.dist-info/WHEEL,sha256=TzjyIyBUptJHnlQyOymQYLWjtXe7MGI0m3QvmcG7PpI,96
|
|
8
|
+
batchcorder_datafusion-55.0.0.dist-info/sboms/batchcorder-datafusion-py.cyclonedx.json,sha256=_ZRMrakGPQW12EGJ3W2AtExVvjI28SD4Kda5ImubapY,345548
|
|
9
|
+
batchcorder_datafusion-55.0.0.dist-info/RECORD,,
|