Skip to content

Commit beab721

Browse files
committed
Give the two FFI example crates a runnable entry point (#1727)
1 parent 878e5d3 commit beab721

6 files changed

Lines changed: 148 additions & 0 deletions

File tree

‎examples/datafusion-ffi-example/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
# DataFusion Python FFI provider example
2121

22+
**What this is:** A testbed that exports table providers, catalogs, functions, and codecs to Python.
23+
**If you are learning the protocol:** Read the [Extension Guide](https://datafusion.apache.org/python/extension-guide/index.html).
24+
**To run the demo:** `uv run python examples/datafusion-ffi-example/run_demo.py` (after `uv run maturin develop` in this directory).
25+
2226
This crate is the **provider library** in the three-library query-planning example. It exports table providers, functions, and the logical and physical codecs needed to serialize objects owned by this library. The companion planner is in [`../datafusion-ffi-query-planner-example`](../datafusion-ffi-query-planner-example/).
2327

2428
The example intentionally uses separate `cdylib` crates for these roles:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import subprocess
2+
import sys
3+
from pathlib import Path
4+
5+
6+
def test_run_demo():
7+
script = Path(__file__).parent.parent.parent / "run_demo.py"
8+
result = subprocess.run(
9+
[sys.executable, str(script)],
10+
capture_output=True,
11+
text=True,
12+
check=True,
13+
)
14+
assert "1. table provider" in result.stdout
15+
assert "2. functions" in result.stdout
16+
assert "3. catalog provider" in result.stdout
17+
assert "4. config extension" in result.stdout
18+
assert "5. codec round-trip" in result.stdout
19+
assert "6. the same bytes decoded a second time" in result.stdout
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
DataFusion Python FFI provider example.
3+
4+
Walks the conformance matrix in numbered sections: table provider, functions,
5+
catalog provider, config extension, codec round-trip, then the same bytes
6+
decoded a second time.
7+
"""
8+
import sys
9+
10+
try:
11+
from datafusion_ffi_example import (
12+
IsNullUDF,
13+
MyCatalogProvider,
14+
MyConfig,
15+
MyLogicalExtensionCodec,
16+
MyTableProvider,
17+
)
18+
except ImportError:
19+
sys.exit("build the extension first:\n uv run maturin develop\nSee README.md.")
20+
21+
from datafusion import LogicalPlan, SessionConfig, SessionContext, udf
22+
23+
24+
25+
26+
print("1. table provider")
27+
ctx = SessionContext()
28+
ctx.register_table("numbers", MyTableProvider(1, 6, 1))
29+
ctx.sql('SELECT "A" FROM numbers').show()
30+
31+
print("\n2. functions")
32+
ctx.register_udf(udf(IsNullUDF()))
33+
ctx.sql('SELECT "A", my_custom_is_null("A") AS is_null FROM numbers').show()
34+
35+
print("\n3. catalog provider")
36+
ctx.register_catalog_provider("ffi_catalog", MyCatalogProvider())
37+
ctx.sql("SELECT * FROM ffi_catalog.my_schema.my_table").show()
38+
39+
print("\n4. config extension")
40+
config = MyConfig()
41+
config = SessionConfig({"datafusion.catalog.information_schema": "true"}).with_extension(config)
42+
config.set("my_config.baz_count", "42")
43+
ctx2 = SessionContext(config)
44+
ctx2.sql("SHOW my_config.baz_count;").show()
45+
46+
print("\n5. codec round-trip")
47+
codec = MyLogicalExtensionCodec()
48+
ctx3 = SessionContext().with_logical_extension_codec(codec)
49+
ctx3.register_table("numbers", MyTableProvider(1, 4, 1))
50+
plan = ctx3.sql('SELECT "A" FROM numbers').logical_plan()
51+
blob = plan.to_bytes(ctx3)
52+
restored = LogicalPlan.from_bytes(ctx3, blob)
53+
ctx3.create_dataframe_from_logical_plan(restored).show()
54+
55+
print("\n6. the same bytes decoded a second time")
56+
restored2 = LogicalPlan.from_bytes(ctx3, blob)
57+
ctx3.create_dataframe_from_logical_plan(restored2).show()

‎examples/datafusion-ffi-query-planner-example/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
# DataFusion Python FFI query planner example
2121

22+
**What this is:** A query planner extension that adds a configurable global limit to query plans.
23+
**If you are learning the protocol:** Read the [Extension Guide](https://datafusion.apache.org/python/extension-guide/index.html).
24+
**To run the demo:** `uv run python examples/datafusion-ffi-query-planner-example/run_demo.py` (after `uv run maturin develop` in this directory).
25+
2226
This crate is an independent query-planner Python extension. Together with [`../datafusion-ffi-example`](../datafusion-ffi-example/) it demonstrates a real three-library plan exchange:
2327

2428
- **A — `datafusion-python`:** owns the session and final execution.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import subprocess
2+
import sys
3+
from pathlib import Path
4+
5+
6+
def test_run_demo():
7+
script = Path(__file__).parent.parent.parent / "run_demo.py"
8+
result = subprocess.run(
9+
[sys.executable, str(script)],
10+
capture_output=True,
11+
text=True,
12+
check=True,
13+
)
14+
assert "1. logical plan" in result.stdout
15+
assert "2. physical plan returned" in result.stdout
16+
assert "3. effect of SET ffi_query_planner.max_rows" in result.stdout
17+
assert "4. two planners nesting" in result.stdout
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
DataFusion Python FFI query planner example.
3+
4+
Prints plans, showing the logical plan handed to the planner, the physical
5+
plan returned, the effect of SET ffi_query_planner.max_rows, and two
6+
planners nesting.
7+
"""
8+
import sys
9+
10+
try:
11+
from datafusion_ffi_query_planner_example import (
12+
MyPlannerConfig,
13+
MyQueryPlanner,
14+
)
15+
except ImportError:
16+
sys.exit("build the extension first:\n uv run maturin develop\nSee README.md.")
17+
18+
from datafusion import SessionConfig, SessionContext
19+
20+
21+
22+
23+
print("1. logical plan")
24+
config = SessionConfig().with_extension(MyPlannerConfig(max_rows=5))
25+
ctx = SessionContext(config)
26+
27+
ctx.sql("CREATE TABLE t AS SELECT * FROM (VALUES (1), (2), (3), (4), (5), (6), (7)) AS t(a)")
28+
df = ctx.sql("SELECT * FROM t")
29+
print(df.logical_plan().display_indent())
30+
31+
print("\n2. physical plan returned")
32+
planner = MyQueryPlanner()
33+
ctx.set_query_planner(planner)
34+
35+
plan = df.execution_plan()
36+
print(plan.display_indent())
37+
38+
print("\n3. effect of SET ffi_query_planner.max_rows")
39+
ctx.sql("SET ffi_query_planner.max_rows = 2").collect()
40+
plan2 = df.execution_plan()
41+
print(plan2.display_indent())
42+
43+
print("\n4. two planners nesting")
44+
outer_planner = MyQueryPlanner(fallback=planner)
45+
ctx.set_query_planner(outer_planner)
46+
plan3 = df.execution_plan()
47+
print(plan3.display_indent())

0 commit comments

Comments
 (0)