blob: ac453bb72c62885ea60cbb3db3fb40618b7914b2 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
""":py:class:`SessionContext` — entry point for running DataFusion queries.
A :py:class:`SessionContext` holds registered tables, catalogs, and
configuration for the current session. It is the first object most programs
create: from it you register data, run SQL strings
(:py:meth:`SessionContext.sql`), read files
(:py:meth:`SessionContext.read_csv`,
:py:meth:`SessionContext.read_parquet`, ...), and construct
:py:class:`~datafusion.dataframe.DataFrame` objects in memory
(:py:meth:`SessionContext.from_pydict`,
:py:meth:`SessionContext.from_arrow`).
Session behavior (memory limits, batch size, configured optimizer passes,
...) is controlled by :py:class:`SessionConfig` and
:py:class:`RuntimeEnvBuilder`; SQL dialect limits are controlled by
:py:class:`SQLOptions`.
Examples:
>>> ctx = dfn.SessionContext()
>>> df = ctx.from_pydict({"a": [1, 2, 3]})
>>> ctx.sql("SELECT 1 AS n").to_pydict()
{'n': [1]}
See :ref:`user_guide_concepts` in the online documentation for the broader
execution model.
"""
from __future__ import annotations
import uuid
import warnings
from typing import TYPE_CHECKING, Any, Protocol
try:
from warnings import deprecated # Python 3.13+
except ImportError:
from typing_extensions import deprecated # Python 3.12
from urllib.parse import urlparse
import pyarrow as pa
from datafusion.catalog import (
Catalog,
CatalogList,
CatalogProviderExportable,
CatalogProviderList,
CatalogProviderListExportable,
TableProviderFactory,
TableProviderFactoryExportable,
)
from datafusion.dataframe import DataFrame
from datafusion.expr import sort_list_to_raw_sort_list
from datafusion.extensions import (
QueryPlannerExportable,
SessionExtensionComponents,
SessionExtensionExportable,
)
from datafusion.options import (
DEFAULT_MAX_INFER_SCHEMA,
CsvReadOptions,
_convert_table_partition_cols,
)
from datafusion.record_batch import RecordBatchStream
from ._internal import RuntimeEnvBuilder as RuntimeEnvBuilderInternal
from ._internal import SessionConfig as SessionConfigInternal
from ._internal import SessionContext as SessionContextInternal
from ._internal import SQLOptions as SQLOptionsInternal
from ._internal import expr as expr_internal
if TYPE_CHECKING:
import pathlib
from collections.abc import Iterable, Sequence
import pandas as pd
import polars as pl # type: ignore[import]
from _typeshed import CapsuleType as _PyCapsule
from datafusion.catalog import CatalogProvider, Table
from datafusion.common import DFSchema
from datafusion.expr import Expr, SortKey
from datafusion.plan import ExecutionPlan, LogicalPlan
from datafusion.user_defined import (
AggregateUDF,
LogicalExtensionCodecExportable,
PhysicalExtensionCodecExportable,
ScalarUDF,
TableFunction,
WindowUDF,
)
class ArrowStreamExportable(Protocol):
"""Type hint for object exporting Arrow C Stream via Arrow PyCapsule Interface.
https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
"""
def __arrow_c_stream__( # noqa: D105
self, requested_schema: object | None = None
) -> object: ...
class ArrowArrayExportable(Protocol):
"""Type hint for object exporting Arrow C Array via Arrow PyCapsule Interface.
https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
"""
def __arrow_c_array__( # noqa: D105
self, requested_schema: object | None = None
) -> tuple[object, object]: ...
class TableProviderExportable(Protocol):
"""Type hint for object that has __datafusion_table_provider__ PyCapsule.
https://datafusion.apache.org/python/user-guide/io/table_provider.html
"""
def __datafusion_table_provider__(self, session: Any) -> object: ... # noqa: D105
class PhysicalOptimizerRuleExportable(Protocol):
"""Type hint for object that has __datafusion_physical_optimizer_rule__ PyCapsule.
The method returns a PyCapsule wrapping an ``FFI_PhysicalOptimizerRule``,
typically produced by a separate compiled extension.
"""
def __datafusion_physical_optimizer_rule__(self) -> object: ... # noqa: D105
class SessionConfig:
"""Session configuration options."""
def __init__(self, config_options: dict[str, str] | None = None) -> None:
"""Create a new :py:class:`SessionConfig` with the given configuration options.
Args:
config_options: Configuration options.
"""
self.config_internal = SessionConfigInternal(config_options)
def with_create_default_catalog_and_schema(
self, enabled: bool = True
) -> SessionConfig:
"""Control if the default catalog and schema will be automatically created.
Args:
enabled: Whether the default catalog and schema will be
automatically created.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = (
self.config_internal.with_create_default_catalog_and_schema(enabled)
)
return self
def with_default_catalog_and_schema(
self, catalog: str, schema: str
) -> SessionConfig:
"""Select a name for the default catalog and schema.
Args:
catalog: Catalog name.
schema: Schema name.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_default_catalog_and_schema(
catalog, schema
)
return self
def with_information_schema(self, enabled: bool = True) -> SessionConfig:
"""Enable or disable the inclusion of ``information_schema`` virtual tables.
Args:
enabled: Whether to include ``information_schema`` virtual tables.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_information_schema(enabled)
return self
def with_batch_size(self, batch_size: int) -> SessionConfig:
"""Customize batch size.
Args:
batch_size: Batch size.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_batch_size(batch_size)
return self
def with_target_partitions(self, target_partitions: int) -> SessionConfig:
"""Customize the number of target partitions for query execution.
Increasing partitions can increase concurrency.
Args:
target_partitions: Number of target partitions.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_target_partitions(
target_partitions
)
return self
def with_repartition_aggregations(self, enabled: bool = True) -> SessionConfig:
"""Enable or disable the use of repartitioning for aggregations.
Enabling this improves parallelism.
Args:
enabled: Whether to use repartitioning for aggregations.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_repartition_aggregations(
enabled
)
return self
def with_repartition_joins(self, enabled: bool = True) -> SessionConfig:
"""Enable or disable the use of repartitioning for joins to improve parallelism.
Args:
enabled: Whether to use repartitioning for joins.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_repartition_joins(enabled)
return self
def with_repartition_windows(self, enabled: bool = True) -> SessionConfig:
"""Enable or disable the use of repartitioning for window functions.
This may improve parallelism.
Args:
enabled: Whether to use repartitioning for window functions.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_repartition_windows(enabled)
return self
def with_repartition_sorts(self, enabled: bool = True) -> SessionConfig:
"""Enable or disable the use of repartitioning for window functions.
This may improve parallelism.
Args:
enabled: Whether to use repartitioning for window functions.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_repartition_sorts(enabled)
return self
def with_repartition_file_scans(self, enabled: bool = True) -> SessionConfig:
"""Enable or disable the use of repartitioning for file scans.
Args:
enabled: Whether to use repartitioning for file scans.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_repartition_file_scans(enabled)
return self
def with_repartition_file_min_size(self, size: int) -> SessionConfig:
"""Set minimum file range size for repartitioning scans.
Args:
size: Minimum file range size.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_repartition_file_min_size(size)
return self
def with_parquet_pruning(self, enabled: bool = True) -> SessionConfig:
"""Enable or disable the use of pruning predicate for parquet readers.
Pruning predicates will enable the reader to skip row groups.
Args:
enabled: Whether to use pruning predicate for parquet readers.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_parquet_pruning(enabled)
return self
def set(self, key: str, value: str) -> SessionConfig:
"""Set a configuration option.
Args:
key: Option key.
value: Option value.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.set(key, value)
return self
def with_extension(self, extension: Any) -> SessionConfig:
"""Create a new configuration using an extension.
Args:
extension: A custom configuration extension object. These are
shared from another DataFusion extension library.
Returns:
A new :py:class:`SessionConfig` object with the updated setting.
"""
self.config_internal = self.config_internal.with_extension(extension)
return self
class RuntimeEnvBuilder:
"""Runtime configuration options."""
def __init__(self) -> None:
"""Create a new :py:class:`RuntimeEnvBuilder` with default values."""
self.config_internal = RuntimeEnvBuilderInternal()
def with_disk_manager_disabled(self) -> RuntimeEnvBuilder:
"""Disable the disk manager, attempts to create temporary files will error.
Returns:
A new :py:class:`RuntimeEnvBuilder` object with the updated setting.
"""
self.config_internal = self.config_internal.with_disk_manager_disabled()
return self
def with_disk_manager_os(self) -> RuntimeEnvBuilder:
"""Use the operating system's temporary directory for disk manager.
Returns:
A new :py:class:`RuntimeEnvBuilder` object with the updated setting.
"""
self.config_internal = self.config_internal.with_disk_manager_os()
return self
def with_disk_manager_specified(
self, *paths: str | pathlib.Path
) -> RuntimeEnvBuilder:
"""Use the specified paths for the disk manager's temporary files.
Args:
paths: Paths to use for the disk manager's temporary files.
Returns:
A new :py:class:`RuntimeEnvBuilder` object with the updated setting.
"""
paths_list = [str(p) for p in paths]
self.config_internal = self.config_internal.with_disk_manager_specified(
paths_list
)
return self
def with_unbounded_memory_pool(self) -> RuntimeEnvBuilder:
"""Use an unbounded memory pool.
Returns:
A new :py:class:`RuntimeEnvBuilder` object with the updated setting.
"""
self.config_internal = self.config_internal.with_unbounded_memory_pool()
return self
def with_fair_spill_pool(self, size: int) -> RuntimeEnvBuilder:
"""Use a fair spill pool with the specified size.
This pool works best when you know beforehand the query has multiple spillable
operators that will likely all need to spill. Sometimes it will cause spills
even when there was sufficient memory (reserved for other operators) to avoid
doing so::
┌───────────────────────z──────────────────────z───────────────┐
│ z z │
│ z z │
│ Spillable z Unspillable z Free │
│ Memory z Memory z Memory │
│ z z │
│ z z │
└───────────────────────z──────────────────────z───────────────┘
Args:
size: Size of the memory pool in bytes.
Returns:
A new :py:class:`RuntimeEnvBuilder` object with the updated setting.
Examples:
>>> config = dfn.RuntimeEnvBuilder().with_fair_spill_pool(1024)
"""
self.config_internal = self.config_internal.with_fair_spill_pool(size)
return self
def with_greedy_memory_pool(self, size: int) -> RuntimeEnvBuilder:
"""Use a greedy memory pool with the specified size.
This pool works well for queries that do not need to spill or have a single
spillable operator. See :py:func:`with_fair_spill_pool` if there are
multiple spillable operators that all will spill.
Args:
size: Size of the memory pool in bytes.
Returns:
A new :py:class:`RuntimeEnvBuilder` object with the updated setting.
Examples:
>>> config = dfn.RuntimeEnvBuilder().with_greedy_memory_pool(1024)
"""
self.config_internal = self.config_internal.with_greedy_memory_pool(size)
return self
def with_temp_file_path(self, path: str | pathlib.Path) -> RuntimeEnvBuilder:
"""Use the specified path to create any needed temporary files.
Args:
path: Path to use for temporary files.
Returns:
A new :py:class:`RuntimeEnvBuilder` object with the updated setting.
Examples:
>>> config = dfn.RuntimeEnvBuilder().with_temp_file_path("/tmp")
"""
self.config_internal = self.config_internal.with_temp_file_path(str(path))
return self
class SQLOptions:
"""Options to be used when performing SQL queries."""
def __init__(self) -> None:
"""Create a new :py:class:`SQLOptions` with default values.
The default values are:
- DDL commands are allowed
- DML commands are allowed
- Statements are allowed
"""
self.options_internal = SQLOptionsInternal()
def with_allow_ddl(self, allow: bool = True) -> SQLOptions:
"""Should DDL (Data Definition Language) commands be run?
Examples of DDL commands include ``CREATE TABLE`` and ``DROP TABLE``.
Args:
allow: Allow DDL commands to be run.
Returns:
A new :py:class:`SQLOptions` object with the updated setting.
Examples:
>>> options = dfn.SQLOptions().with_allow_ddl(True)
"""
self.options_internal = self.options_internal.with_allow_ddl(allow)
return self
def with_allow_dml(self, allow: bool = True) -> SQLOptions:
"""Should DML (Data Manipulation Language) commands be run?
Examples of DML commands include ``INSERT INTO`` and ``DELETE``.
Args:
allow: Allow DML commands to be run.
Returns:
A new :py:class:`SQLOptions` object with the updated setting.
Examples:
>>> options = dfn.SQLOptions().with_allow_dml(True)
"""
self.options_internal = self.options_internal.with_allow_dml(allow)
return self
def with_allow_statements(self, allow: bool = True) -> SQLOptions:
"""Should statements such as ``SET VARIABLE`` and ``BEGIN TRANSACTION`` be run?
Args:
allow: Allow statements to be run.
Returns:
A new :py:class:SQLOptions` object with the updated setting.
Examples:
>>> options = dfn.SQLOptions().with_allow_statements(True)
"""
self.options_internal = self.options_internal.with_allow_statements(allow)
return self
class SessionContext:
"""This is the main interface for executing queries and creating DataFrames.
See :ref:`user_guide_concepts` in the online documentation for more information.
"""
def __init__(
self,
config: SessionConfig | None = None,
runtime: RuntimeEnvBuilder | None = None,
) -> None:
"""Main interface for executing queries with DataFusion.
Maintains the state of the connection between a user and an instance
of the connection between a user and an instance of the DataFusion
engine.
Args:
config: Session configuration options.
runtime: Runtime configuration options.
Example usage:
The following example demonstrates how to use the context to execute
a query against a CSV data source using the :py:class:`DataFrame` API::
from datafusion import SessionContext
ctx = SessionContext()
df = ctx.read_csv("data.csv")
"""
config = config.config_internal if config is not None else None
runtime = runtime.config_internal if runtime is not None else None
self.ctx = SessionContextInternal(config, runtime)
def __repr__(self) -> str:
"""Print a string representation of the Session Context."""
return self.ctx.__repr__()
@classmethod
def global_ctx(cls) -> SessionContext:
"""Retrieve the global context as a `SessionContext` wrapper.
Returns:
A `SessionContext` object that wraps the global `SessionContextInternal`.
"""
internal_ctx = SessionContextInternal.global_ctx()
wrapper = cls()
wrapper.ctx = internal_ctx
return wrapper
def enable_url_table(self) -> SessionContext:
"""Control if local files can be queried as tables.
Returns:
A new :py:class:`SessionContext` object with url table enabled.
"""
klass = self.__class__
obj = klass.__new__(klass)
obj.ctx = self.ctx.enable_url_table()
return obj
def register_object_store(
self, schema: str, store: Any, host: str | None = None
) -> None:
"""Add a new object store into the session.
Args:
schema: The data source schema.
store: The :py:class:`~datafusion.object_store.ObjectStore` to register.
host: URL for the host.
"""
self.ctx.register_object_store(schema, store, host)
def deregister_object_store(self, schema: str, host: str | None = None) -> None:
"""Remove an object store from the session.
Args:
schema: The data source schema (e.g. ``"s3://"``).
host: URL for the host (e.g. bucket name).
"""
self.ctx.deregister_object_store(schema, host)
def _register_object_store_for_path(
self, path: str | pathlib.Path, store: Any
) -> None:
"""Parse a URL path and register the given object store for its scheme and host.
This is a convenience helper used by methods like
:py:meth:`register_parquet` and :py:meth:`read_parquet` to
automatically register an object store when an ``object_store``
parameter is provided.
Args:
path: A URL-style path (e.g. ``"s3://bucket/key.parquet"`` or
``"file:///tmp/data.parquet"``).
store: An object store instance to register.
Raises:
ValueError: If the path does not contain a URL scheme, or if
a non-file scheme is missing a host/bucket component.
"""
parsed = urlparse(str(path))
if not parsed.scheme:
msg = (
f"Cannot determine object store URL from path {path!r}. "
"The path must use a URL scheme (e.g. 's3://bucket/key')."
)
raise ValueError(msg)
# file:// URLs typically have an empty netloc (e.g. file:///tmp/a.parquet)
# For other schemes (s3, gs, az, https) the netloc (bucket/host) is required.
if parsed.scheme != "file" and not parsed.netloc:
msg = (
f"Cannot determine object store URL from path {path!r}. "
"The path must include a host or bucket "
"(e.g. 's3://bucket/key')."
)
raise ValueError(msg)
scheme = f"{parsed.scheme}://"
host = parsed.netloc or None
self.register_object_store(scheme, store, host=host)
def register_listing_table(
self,
name: str,
path: str | pathlib.Path,
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
file_extension: str = ".parquet",
schema: pa.Schema | None = None,
file_sort_order: Sequence[Sequence[SortKey]] | None = None,
) -> None:
"""Register multiple files as a single table.
Registers a :py:class:`~datafusion.catalog.Table` that can assemble multiple
files from locations in an :py:class:`~datafusion.object_store.ObjectStore`
instance.
Args:
name: Name of the resultant table.
path: Path to the file to register.
table_partition_cols: Partition columns.
file_extension: File extension of the provided table.
schema: The data source schema.
file_sort_order: Sort order for the file. Each sort key can be
specified as a column name (``str``), an expression
(``Expr``), or a ``SortExpr``.
"""
if table_partition_cols is None:
table_partition_cols = []
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
self.ctx.register_listing_table(
name,
path,
table_partition_cols,
file_extension,
schema,
self._convert_file_sort_order(file_sort_order),
)
def sql(
self,
query: str,
options: SQLOptions | None = None,
param_values: dict[str, Any] | None = None,
**named_params: Any,
) -> DataFrame:
"""Create a :py:class:`~datafusion.DataFrame` from SQL query text.
See the online documentation for a description of how to perform
parameterized substitution via either the ``param_values`` option
or passing in ``named_params``.
Note: This API implements DDL statements such as ``CREATE TABLE`` and
``CREATE VIEW`` and DML statements such as ``INSERT INTO`` with in-memory
default implementation.See
:py:func:`~datafusion.context.SessionContext.sql_with_options`.
Args:
query: SQL query text.
options: If provided, the query will be validated against these options.
param_values: Provides substitution of scalar values in the query
after parsing.
named_params: Provides string or DataFrame substitution in the query string.
Returns:
DataFrame representation of the SQL query.
"""
def value_to_scalar(value: Any) -> pa.Scalar:
if isinstance(value, pa.Scalar):
return value
return pa.scalar(value)
def value_to_string(value: Any) -> str:
if isinstance(value, DataFrame):
view_name = str(uuid.uuid4()).replace("-", "_")
view_name = f"view_{view_name}"
view = value.df.into_view(temporary=True)
self.ctx.register_table(view_name, view)
return view_name
return str(value)
param_values = (
{name: value_to_scalar(value) for (name, value) in param_values.items()}
if param_values is not None
else {}
)
param_strings = (
{name: value_to_string(value) for (name, value) in named_params.items()}
if named_params is not None
else {}
)
options_raw = options.options_internal if options is not None else None
return DataFrame(
self.ctx.sql_with_options(
query,
options=options_raw,
param_values=param_values,
param_strings=param_strings,
)
)
def sql_with_options(
self,
query: str,
options: SQLOptions,
param_values: dict[str, Any] | None = None,
**named_params: Any,
) -> DataFrame:
"""Create a :py:class:`~datafusion.dataframe.DataFrame` from SQL query text.
This function will first validate that the query is allowed by the
provided options.
Args:
query: SQL query text.
options: SQL options.
param_values: Provides substitution of scalar values in the query
after parsing.
named_params: Provides string or DataFrame substitution in the query string.
Returns:
DataFrame representation of the SQL query.
"""
return self.sql(
query, options=options, param_values=param_values, **named_params
)
def create_dataframe(
self,
partitions: list[list[pa.RecordBatch]],
name: str | None = None,
schema: pa.Schema | None = None,
) -> DataFrame:
"""Create and return a dataframe using the provided partitions.
Args:
partitions: :py:class:`pa.RecordBatch` partitions to register.
name: Resultant dataframe name.
schema: Schema for the partitions.
Returns:
DataFrame representation of the SQL query.
"""
return DataFrame(self.ctx.create_dataframe(partitions, name, schema))
def create_dataframe_from_logical_plan(self, plan: LogicalPlan) -> DataFrame:
"""Create a :py:class:`~datafusion.dataframe.DataFrame` from an existing plan.
Args:
plan: Logical plan.
Returns:
DataFrame representation of the logical plan.
"""
return DataFrame(self.ctx.create_dataframe_from_logical_plan(plan._raw_plan))
def from_pylist(
self, data: list[dict[str, Any]], name: str | None = None
) -> DataFrame:
"""Create a :py:class:`~datafusion.dataframe.DataFrame` from a list.
Args:
data: List of dictionaries.
name: Name of the DataFrame.
Returns:
DataFrame representation of the list of dictionaries.
"""
return DataFrame(self.ctx.from_pylist(data, name))
def from_pydict(
self, data: dict[str, list[Any]], name: str | None = None
) -> DataFrame:
"""Create a :py:class:`~datafusion.dataframe.DataFrame` from a dictionary.
Args:
data: Dictionary of lists.
name: Name of the DataFrame.
Returns:
DataFrame representation of the dictionary of lists.
"""
return DataFrame(self.ctx.from_pydict(data, name))
def from_arrow(
self,
data: ArrowStreamExportable | ArrowArrayExportable,
name: str | None = None,
) -> DataFrame:
"""Create a :py:class:`~datafusion.dataframe.DataFrame` from an Arrow source.
The Arrow data source can be any object that implements either
``__arrow_c_stream__`` or ``__arrow_c_array__``. For the latter, it must return
a struct array.
Arrow data can be Polars, Pandas, Pyarrow etc.
Args:
data: Arrow data source.
name: Name of the DataFrame.
Returns:
DataFrame representation of the Arrow table.
"""
return DataFrame(self.ctx.from_arrow(data, name))
def from_pandas(self, data: pd.DataFrame, name: str | None = None) -> DataFrame:
"""Create a :py:class:`~datafusion.dataframe.DataFrame` from a Pandas DataFrame.
Args:
data: Pandas DataFrame.
name: Name of the DataFrame.
Returns:
DataFrame representation of the Pandas DataFrame.
"""
return DataFrame(self.ctx.from_pandas(data, name))
def from_polars(self, data: pl.DataFrame, name: str | None = None) -> DataFrame:
"""Create a :py:class:`~datafusion.dataframe.DataFrame` from a Polars DataFrame.
Args:
data: Polars DataFrame.
name: Name of the DataFrame.
Returns:
DataFrame representation of the Polars DataFrame.
"""
return DataFrame(self.ctx.from_polars(data, name))
# https://github.com/apache/datafusion-python/pull/1016#discussion_r1983239116
# is the discussion on how we arrived at adding register_view
def register_view(self, name: str, df: DataFrame) -> None:
"""Register a :py:class:`~datafusion.dataframe.DataFrame` as a view.
Args:
name (str): The name to register the view under.
df (DataFrame): The DataFrame to be converted into a view and registered.
"""
view = df.into_view()
self.ctx.register_table(name, view)
def register_table(
self,
name: str,
table: Table | TableProviderExportable | DataFrame | pa.dataset.Dataset,
) -> None:
"""Register a :py:class:`~datafusion.Table` with this context.
The registered table can be referenced from SQL statements executed against
this context.
Args:
name: Name of the resultant table.
table: Any object that can be converted into a :class:`Table`.
"""
self.ctx.register_table(name, table)
def deregister_table(self, name: str) -> None:
"""Remove a table from the session."""
self.ctx.deregister_table(name)
def register_table_factory(
self,
format: str,
factory: TableProviderFactory | TableProviderFactoryExportable,
) -> None:
"""Register a :py:class:`~datafusion.TableProviderFactoryExportable`.
The registered factory can be referenced from SQL DDL statements executed
against this context.
Args:
format: The value to be used in `STORED AS ${format}` clause.
factory: A PyCapsule that implements :class:`TableProviderFactoryExportable`
"""
self.ctx.register_table_factory(format, factory)
def catalog_names(self) -> set[str]:
"""Returns the list of catalogs in this context."""
return self.ctx.catalog_names()
def register_catalog_provider_list(
self,
provider: CatalogProviderListExportable | CatalogProviderList | CatalogList,
) -> None:
"""Register a catalog provider list."""
if isinstance(provider, CatalogList):
self.ctx.register_catalog_provider_list(provider.catalog)
else:
self.ctx.register_catalog_provider_list(provider)
def register_catalog_provider(
self, name: str, provider: CatalogProviderExportable | CatalogProvider | Catalog
) -> None:
"""Register a catalog provider."""
if isinstance(provider, Catalog):
self.ctx.register_catalog_provider(name, provider.catalog)
else:
self.ctx.register_catalog_provider(name, provider)
@deprecated("Use register_table() instead.")
def register_table_provider(
self,
name: str,
provider: Table | TableProviderExportable | DataFrame | pa.dataset.Dataset,
) -> None:
"""Register a table provider.
Deprecated: use :meth:`register_table` instead.
"""
self.register_table(name, provider)
def register_udtf(self, func: TableFunction) -> None:
"""Register a user defined table function."""
self.ctx.register_udtf(func._udtf)
def register_batch(self, name: str, batch: pa.RecordBatch) -> None:
"""Register a single :py:class:`pa.RecordBatch` as a table.
Args:
name: Name of the resultant table.
batch: Record batch to register as a table.
Examples:
>>> ctx = dfn.SessionContext()
>>> batch = pa.RecordBatch.from_pydict({"a": [1, 2, 3]})
>>> ctx.register_batch("batch_tbl", batch)
>>> ctx.sql("SELECT * FROM batch_tbl").collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
1,
2,
3
]
"""
self.ctx.register_batch(name, batch)
def deregister_udtf(self, name: str) -> None:
"""Remove a user-defined table function from the session.
Args:
name: Name of the UDTF to deregister.
"""
self.ctx.deregister_udtf(name)
def register_record_batches(
self, name: str, partitions: list[list[pa.RecordBatch]]
) -> None:
"""Register record batches as a table.
This function will convert the provided partitions into a table and
register it into the session using the given name.
Args:
name: Name of the resultant table.
partitions: Record batches to register as a table.
"""
self.ctx.register_record_batches(name, partitions)
def read_batch(self, batch: pa.RecordBatch) -> DataFrame:
"""Return a :py:class:`~datafusion.DataFrame` reading a single batch.
Convenience wrapper around :py:meth:`read_batches` for the single-batch
case. Unlike :py:meth:`register_batch`, this does not register the
batch as a named table; it returns an anonymous
:py:class:`~datafusion.DataFrame` directly.
Args:
batch: Record batch to wrap as a DataFrame.
Examples:
>>> ctx = dfn.SessionContext()
>>> batch = pa.RecordBatch.from_pydict({"a": [1, 2, 3]})
>>> ctx.read_batch(batch).to_pydict()
{'a': [1, 2, 3]}
"""
return self.read_batches([batch])
def read_batches(self, batches: Iterable[pa.RecordBatch]) -> DataFrame:
"""Return a :py:class:`~datafusion.DataFrame` reading the given batches.
All batches must share the same schema. Any iterable of
:py:class:`pa.RecordBatch` is accepted (list, tuple, generator);
it is materialized into a list before being handed to the
underlying Rust binding. Unlike :py:meth:`register_record_batches`,
this does not register the batches as a named table; it returns
an anonymous :py:class:`~datafusion.DataFrame` directly.
Args:
batches: Record batches to wrap as a DataFrame.
Examples:
>>> ctx = dfn.SessionContext()
>>> b1 = pa.RecordBatch.from_pydict({"a": [1, 2]})
>>> b2 = pa.RecordBatch.from_pydict({"a": [3, 4]})
>>> ctx.read_batches([b1, b2]).to_pydict()
{'a': [1, 2, 3, 4]}
A generator works too:
>>> ctx.read_batches(b for b in [b1, b2]).to_pydict()
{'a': [1, 2, 3, 4]}
"""
return DataFrame(self.ctx.read_batches(list(batches)))
def register_parquet(
self,
name: str,
path: str | pathlib.Path,
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
parquet_pruning: bool = True,
file_extension: str = ".parquet",
skip_metadata: bool = True,
schema: pa.Schema | None = None,
file_sort_order: Sequence[Sequence[SortKey]] | None = None,
object_store: Any | None = None,
) -> None:
"""Register a Parquet file as a table.
The registered table can be referenced from SQL statement executed
against this context.
Args:
name: Name of the table to register.
path: Path to the Parquet file.
table_partition_cols: Partition columns.
parquet_pruning: Whether the parquet reader should use the
predicate to prune row groups.
file_extension: File extension; only files with this extension are
selected for data input.
skip_metadata: Whether the parquet reader should skip any metadata
that may be in the file schema. This can help avoid schema
conflicts due to metadata.
schema: The data source schema.
file_sort_order: Sort order for the file. Each sort key can be
specified as a column name (``str``), an expression
(``Expr``), or a ``SortExpr``.
object_store: A pre-configured object store instance (e.g.
:py:class:`~datafusion.object_store.AmazonS3`,
:py:class:`~datafusion.object_store.GoogleCloud`,
:py:class:`~datafusion.object_store.MicrosoftAzure`) to use
for accessing the file. When provided, the store is
automatically registered for the URL scheme and host parsed
from ``path``, removing the need to call
:py:meth:`register_object_store` separately. This is
especially useful in multi-threaded environments where
setting credentials via ``os.environ`` is not thread-safe.
Examples:
Register a local Parquet file:
>>> import datafusion
>>> ctx = datafusion.SessionContext()
>>> ctx.register_parquet("my_table", "data.parquet") # doctest: +SKIP
Register from S3 with inline credentials (thread-safe):
>>> from datafusion.object_store import AmazonS3 # doctest: +SKIP
>>> store = AmazonS3(
... bucket_name="my-bucket",
... region="us-east-1",
... access_key_id="...",
... secret_access_key="...",
... ) # doctest: +SKIP
>>> ctx.register_parquet(
... "my_table",
... "s3://my-bucket/data.parquet",
... object_store=store,
... ) # doctest: +SKIP
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if table_partition_cols is None:
table_partition_cols = []
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
self.ctx.register_parquet(
name,
path,
table_partition_cols,
parquet_pruning,
file_extension,
skip_metadata,
schema,
self._convert_file_sort_order(file_sort_order),
)
def register_csv(
self,
name: str,
path: str | pathlib.Path | list[str | pathlib.Path],
schema: pa.Schema | None = None,
has_header: bool = True,
delimiter: str = ",",
schema_infer_max_records: int = DEFAULT_MAX_INFER_SCHEMA,
file_extension: str = ".csv",
file_compression_type: str | None = None,
options: CsvReadOptions | None = None,
object_store: Any | None = None,
) -> None:
"""Register a CSV file as a table.
The registered table can be referenced from SQL statement executed against.
Args:
name: Name of the table to register.
path: Path to the CSV file. It also accepts a list of Paths.
schema: An optional schema representing the CSV file. If None, the
CSV reader will try to infer it based on data in file.
has_header: Whether the CSV file have a header. If schema inference
is run on a file with no headers, default column names are
created.
delimiter: An optional column delimiter.
schema_infer_max_records: Maximum number of rows to read from CSV
files for schema inference if needed.
file_extension: File extension; only files with this extension are
selected for data input.
file_compression_type: File compression type.
options: Set advanced options for CSV reading. This cannot be
combined with any of the other options in this method.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
"""
if object_store is not None:
# For list paths, register from the first entry
register_path = path[0] if isinstance(path, list) else path
self._register_object_store_for_path(register_path, object_store)
if options is not None and (
schema is not None
or not has_header
or delimiter != ","
or schema_infer_max_records != DEFAULT_MAX_INFER_SCHEMA
or file_extension != ".csv"
or file_compression_type is not None
):
message = (
"Combining CsvReadOptions parameter with additional options "
"is not supported. Use CsvReadOptions to set parameters."
)
warnings.warn(
message,
category=UserWarning,
stacklevel=2,
)
options = (
options
if options is not None
else CsvReadOptions(
schema=schema,
has_header=has_header,
delimiter=delimiter,
schema_infer_max_records=schema_infer_max_records,
file_extension=file_extension,
file_compression_type=file_compression_type,
)
)
self.ctx.register_csv(
name,
path,
options.to_inner(),
)
def register_json(
self,
name: str,
path: str | pathlib.Path,
schema: pa.Schema | None = None,
schema_infer_max_records: int = 1000,
file_extension: str = ".json",
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
file_compression_type: str | None = None,
object_store: Any | None = None,
) -> None:
"""Register a JSON file as a table.
The registered table can be referenced from SQL statement executed
against this context.
Args:
name: Name of the table to register.
path: Path to the JSON file.
schema: The data source schema.
schema_infer_max_records: Maximum number of rows to read from JSON
files for schema inference if needed.
file_extension: File extension; only files with this extension are
selected for data input.
table_partition_cols: Partition columns.
file_compression_type: File compression type.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if table_partition_cols is None:
table_partition_cols = []
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
self.ctx.register_json(
name,
path,
schema,
schema_infer_max_records,
file_extension,
table_partition_cols,
file_compression_type,
)
def register_avro(
self,
name: str,
path: str | pathlib.Path,
schema: pa.Schema | None = None,
file_extension: str = ".avro",
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
object_store: Any | None = None,
) -> None:
"""Register an Avro file as a table.
The registered table can be referenced from SQL statement executed against
this context.
Args:
name: Name of the table to register.
path: Path to the Avro file.
schema: The data source schema.
file_extension: File extension to select.
table_partition_cols: Partition columns.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if table_partition_cols is None:
table_partition_cols = []
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
self.ctx.register_avro(name, path, schema, file_extension, table_partition_cols)
def register_arrow(
self,
name: str,
path: str | pathlib.Path,
schema: pa.Schema | None = None,
file_extension: str = ".arrow",
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
object_store: Any | None = None,
) -> None:
"""Register an Arrow IPC file as a table.
The registered table can be referenced from SQL statements executed
against this context.
Args:
name: Name of the table to register.
path: Path to the Arrow IPC file.
schema: The data source schema.
file_extension: File extension to select.
table_partition_cols: Partition columns.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
Examples:
>>> import tempfile, os
>>> ctx = dfn.SessionContext()
>>> table = pa.table({"x": [10, 20, 30]})
>>> with tempfile.TemporaryDirectory() as tmpdir:
... path = os.path.join(tmpdir, "data.arrow")
... with pa.ipc.new_file(path, table.schema) as writer:
... writer.write_table(table)
... ctx.register_arrow("arrow_tbl", path)
... ctx.sql("SELECT * FROM arrow_tbl").collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
10,
20,
30
]
Provide an explicit ``schema`` to override schema inference:
>>> with tempfile.TemporaryDirectory() as tmpdir:
... path = os.path.join(tmpdir, "data.arrow")
... with pa.ipc.new_file(path, table.schema) as writer:
... writer.write_table(table)
... ctx.register_arrow(
... "arrow_schema",
... path,
... schema=pa.schema([("x", pa.int64())]),
... )
... ctx.sql("SELECT * FROM arrow_schema").collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
10,
20,
30
]
Use ``file_extension`` to read files with a non-default extension:
>>> with tempfile.TemporaryDirectory() as tmpdir:
... path = os.path.join(tmpdir, "data.ipc")
... with pa.ipc.new_file(path, table.schema) as writer:
... writer.write_table(table)
... ctx.register_arrow(
... "arrow_ipc", path, file_extension=".ipc"
... )
... ctx.sql("SELECT * FROM arrow_ipc").collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
10,
20,
30
]
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if table_partition_cols is None:
table_partition_cols = []
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
self.ctx.register_arrow(
name, path, schema, file_extension, table_partition_cols
)
def register_dataset(self, name: str, dataset: pa.dataset.Dataset) -> None:
"""Register a :py:class:`pa.dataset.Dataset` as a table.
Args:
name: Name of the table to register.
dataset: PyArrow dataset.
"""
self.ctx.register_dataset(name, dataset)
def register_udf(self, udf: ScalarUDF) -> None:
"""Register a user-defined function (UDF) with the context."""
self.ctx.register_udf(udf._udf)
def deregister_udf(self, name: str) -> None:
"""Remove a user-defined scalar function from the session.
Args:
name: Name of the UDF to deregister.
"""
self.ctx.deregister_udf(name)
def register_udaf(self, udaf: AggregateUDF) -> None:
"""Register a user-defined aggregation function (UDAF) with the context."""
self.ctx.register_udaf(udaf._udaf)
def enable_spark_functions(self) -> None:
"""Register all Spark-compatible functions for SQL access.
Registers every UDF/UDAF/UDWF from the ``datafusion-spark`` crate,
overriding any DataFusion built-ins of the same name with their
Spark-semantics version (e.g. ``substring`` becomes 1-indexed,
``concat`` propagates NULL, ``round`` uses HALF_UP rounding).
For DataFrame use, import the typed wrappers from
:py:mod:`datafusion.functions.spark` directly; this method is only
needed for SQL queries.
Examples:
>>> ctx = dfn.SessionContext()
>>> ctx.enable_spark_functions()
>>> ctx.sql(
... "SELECT sha2('hello', 256) AS h"
... ).collect_column("h")[0].as_py()
'2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
"""
self.ctx.enable_spark_functions()
def deregister_udaf(self, name: str) -> None:
"""Remove a user-defined aggregate function from the session.
Args:
name: Name of the UDAF to deregister.
"""
self.ctx.deregister_udaf(name)
def register_udwf(self, udwf: WindowUDF) -> None:
"""Register a user-defined window function (UDWF) with the context."""
self.ctx.register_udwf(udwf._udwf)
def deregister_udwf(self, name: str) -> None:
"""Remove a user-defined window function from the session.
Args:
name: Name of the UDWF to deregister.
"""
self.ctx.deregister_udwf(name)
def udf(self, name: str) -> ScalarUDF:
"""Look up a registered scalar UDF by name.
Returns the same ``ScalarUDF`` wrapper that :py:meth:`register_udf`
accepts, so it can be invoked as an expression in the DataFrame API
or re-registered into a different :py:class:`SessionContext`.
Built-in scalar functions from the session's function registry are
also looked up.
Args:
name: Name of the registered scalar UDF.
Raises:
KeyError: If no scalar UDF is registered under ``name``.
Examples:
Register a UDF, then look it up by name and use it in the
DataFrame API:
>>> ctx = dfn.SessionContext()
>>> nullcheck = dfn.udf(
... lambda x: x.is_null(),
... [pa.int64()],
... pa.bool_(),
... volatility="immutable",
... name="nullcheck",
... )
>>> ctx.register_udf(nullcheck)
>>> fn = ctx.udf("nullcheck")
>>> df = ctx.from_pydict({"a": [1, None, 3]})
>>> df.select(fn(col("a")).alias("is_null")).to_pydict()
{'is_null': [False, True, False]}
Late-binding: the function name can come from configuration
rather than an imported symbol, which is useful when the set
of UDFs is plugin-driven or chosen at runtime:
>>> config = {"null_check": "nullcheck"}
>>> fn = ctx.udf(config["null_check"])
>>> df.select(fn(col("a")).alias("is_null")).to_pydict()
{'is_null': [False, True, False]}
"""
from datafusion.user_defined import ScalarUDF as _ScalarUDF # noqa: PLC0415
return _ScalarUDF._from_internal(self.ctx.udf(name))
def udaf(self, name: str) -> AggregateUDF:
"""Look up a registered aggregate UDF by name.
Returns the same ``AggregateUDF`` wrapper that :py:meth:`register_udaf`
accepts. Built-in aggregate functions such as ``sum`` or ``avg`` are
also discoverable through this lookup. See :py:meth:`udf` for a worked
late-binding example; the pattern is identical for aggregates.
Args:
name: Name of the registered aggregate UDF.
Raises:
KeyError: If no aggregate UDF is registered under ``name``.
Examples:
Look up a built-in aggregate by name and use it in
:py:meth:`~datafusion.DataFrame.aggregate`:
>>> ctx = dfn.SessionContext()
>>> sum_fn = ctx.udaf("sum")
>>> df = ctx.from_pydict({"a": [1, 2, 3]})
>>> df.aggregate([], [sum_fn(col("a")).alias("total")]).to_pydict()
{'total': [6]}
"""
from datafusion.user_defined import ( # noqa: PLC0415
AggregateUDF as _AggregateUDF,
)
return _AggregateUDF._from_internal(self.ctx.udaf(name))
def udwf(self, name: str) -> WindowUDF:
"""Look up a registered window UDF by name.
Returns the same ``WindowUDF`` wrapper that :py:meth:`register_udwf`
accepts. Built-in window functions such as ``row_number`` or ``rank``
are also discoverable through this lookup. See :py:meth:`udf` for a
worked late-binding example; the pattern is identical for window
functions.
Args:
name: Name of the registered window UDF.
Raises:
KeyError: If no window UDF is registered under ``name``.
Examples:
Look up a built-in window function by name and use it in
``select``:
>>> ctx = dfn.SessionContext()
>>> rn = ctx.udwf("row_number")
>>> df = ctx.from_pydict({"a": [10, 20, 30]})
>>> df.select(col("a"), rn().alias("rn")).to_pydict()
{'a': [10, 20, 30], 'rn': [1, 2, 3]}
"""
from datafusion.user_defined import WindowUDF as _WindowUDF # noqa: PLC0415
return _WindowUDF._from_internal(self.ctx.udwf(name))
def udfs(self) -> list[str]:
"""Return the sorted names of all registered scalar UDFs.
Includes both user-registered and built-in scalar functions. Pair
with :py:meth:`udf` to drive discovery, validation, or config-based
dispatch.
Examples:
>>> ctx = dfn.SessionContext()
>>> "abs" in ctx.udfs()
True
"""
return self.ctx.udfs()
def udafs(self) -> list[str]:
"""Return the sorted names of all registered aggregate UDFs.
Examples:
>>> ctx = dfn.SessionContext()
>>> "sum" in ctx.udafs()
True
"""
return self.ctx.udafs()
def udwfs(self) -> list[str]:
"""Return the sorted names of all registered window UDFs.
Examples:
>>> ctx = dfn.SessionContext()
>>> "row_number" in ctx.udwfs()
True
"""
return self.ctx.udwfs()
def catalog(self, name: str = "datafusion") -> Catalog:
"""Retrieve a catalog by name."""
return Catalog(self.ctx.catalog(name))
def table(self, name: str) -> DataFrame:
"""Retrieve a previously registered table by name."""
return DataFrame(self.ctx.table(name))
def table_exist(self, name: str) -> bool:
"""Return whether a table with the given name exists."""
return self.ctx.table_exist(name)
def empty_table(self) -> DataFrame:
"""Create an empty :py:class:`~datafusion.dataframe.DataFrame`."""
return DataFrame(self.ctx.empty_table())
def session_id(self) -> str:
"""Return an id that uniquely identifies this :py:class:`SessionContext`."""
return self.ctx.session_id()
def session_start_time(self) -> str:
"""Return the session start time as an RFC 3339 formatted string.
Examples:
>>> ctx = SessionContext()
>>> ctx.session_start_time() # doctest: +SKIP
'2026-01-01T12:34:56.123456789+00:00'
"""
return self.ctx.session_start_time()
def enable_ident_normalization(self) -> bool:
"""Return whether identifier normalization (lowercasing) is enabled.
Examples:
>>> ctx = SessionContext()
>>> ctx.enable_ident_normalization()
True
"""
return self.ctx.enable_ident_normalization()
def copied_config(self) -> SessionConfig:
"""Return a copy of the active :py:class:`SessionConfig`.
Mutating the returned config does not affect this context; use
the result when you need a starting point for a new context or
want to inspect the current settings independent of further
changes here.
Examples:
>>> ctx = SessionContext(SessionConfig().with_batch_size(1024))
>>> isinstance(ctx.copied_config(), SessionConfig)
True
"""
config = SessionConfig()
config.config_internal = self.ctx.copied_config()
return config
@staticmethod
def parse_capacity_limit(config_name: str, limit: str) -> int:
"""Parse a size string into a byte count.
Accepts strings like ``"100M"``, ``"1.5G"``, or ``"512K"``.
``"0"`` is accepted and returns 0. ``config_name`` is used purely
for error messages and identifies which configuration setting the
limit belongs to. Use this helper when constructing a
:py:class:`RuntimeEnvBuilder` from a human-friendly size string.
Examples:
>>> SessionContext.parse_capacity_limit(
... "datafusion.runtime.memory_limit", "1M"
... )
1048576
>>> SessionContext.parse_capacity_limit(
... "datafusion.runtime.memory_limit", "0"
... )
0
"""
return SessionContextInternal.parse_capacity_limit(config_name, limit)
def parse_sql_expr(self, sql: str, schema: DFSchema) -> Expr:
"""Parse a SQL expression string into a logical expression.
Args:
sql: SQL expression string.
schema: Schema to use for resolving column references.
Returns:
Parsed expression.
Examples:
>>> from datafusion.common import DFSchema
>>> ctx = SessionContext()
>>> schema = DFSchema.empty()
>>> ctx.parse_sql_expr("1 + 2", schema=schema)
Expr(Int64(1) + Int64(2))
"""
from datafusion.expr import Expr # noqa: PLC0415
return Expr(self.ctx.parse_sql_expr(sql, schema))
def execute_logical_plan(self, plan: LogicalPlan) -> DataFrame:
"""Execute a :py:class:`~datafusion.plan.LogicalPlan` and return a DataFrame.
Args:
plan: Logical plan to execute.
Returns:
DataFrame resulting from the execution.
Examples:
>>> ctx = SessionContext()
>>> df = ctx.from_pydict({"a": [1, 2, 3]})
>>> plan = df.logical_plan()
>>> df2 = ctx.execute_logical_plan(plan)
>>> df2.collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
1,
2,
3
]
"""
return DataFrame(self.ctx.execute_logical_plan(plan._raw_plan))
def refresh_catalogs(self) -> None:
"""Refresh catalog metadata.
Examples:
>>> ctx = SessionContext()
>>> ctx.refresh_catalogs()
"""
self.ctx.refresh_catalogs()
def remove_optimizer_rule(self, name: str) -> bool:
"""Remove an optimizer rule by name.
Args:
name: Name of the optimizer rule to remove.
Returns:
True if a rule with the given name was found and removed.
Examples:
>>> ctx = SessionContext()
>>> ctx.remove_optimizer_rule("nonexistent_rule")
False
"""
return self.ctx.remove_optimizer_rule(name)
def add_physical_optimizer_rule(
self, rule: PhysicalOptimizerRuleExportable
) -> None:
"""Append a user-defined physical optimizer rule to the session.
The rule is imported via its ``__datafusion_physical_optimizer_rule__``
PyCapsule, typically produced by a separate compiled extension. The
underlying :class:`SessionState` is rebuilt from its current state
with the new rule appended, so previously registered tables, UDFs,
and catalogs are preserved.
Args:
rule: Object exposing ``__datafusion_physical_optimizer_rule__``,
a :class:`PhysicalOptimizerRuleExportable`.
Examples:
>>> from datafusion import SessionContext
>>> ctx = SessionContext()
>>> from my_extension import MyPhysicalOptimizerRule # doctest: +SKIP
>>> rule = MyPhysicalOptimizerRule() # doctest: +SKIP
>>> ctx.add_physical_optimizer_rule(rule) # doctest: +SKIP
"""
self.ctx.add_physical_optimizer_rule(rule)
def set_query_planner(self, planner: QueryPlannerExportable | _PyCapsule) -> None:
"""Install a custom query planner on this session.
The planner is imported through its ``__datafusion_query_planner__``
PyCapsule and installed on this context, in the same way
:meth:`~SessionContext.add_physical_optimizer_rule` installs a rule.
The query planner is part of the session state, so it applies to this
context and to every context sharing its session — including ones
already returned by
:meth:`~SessionContext.with_logical_extension_codec` and friends.
A session holds exactly one planner, so calling this again replaces the
previous one rather than layering. To chain planners, have the new
planner wrap the capsule from
:meth:`~SessionContext.__datafusion_query_planner__`, captured
*before* the new planner is installed.
Install any extension codecs before a layered planner. Installing a
codec afterwards rebuilds the installed planner against it, but not the
fallback inside it, which keeps the codecs it was imported with. Note
also that the planner is built against the codecs of the context this
method is called on, so installing the same planner again on a different
handle rebinds the session's planner to *that* handle's codecs. See the
FFI extensions guide for the full multi-library registration recipe.
Args:
planner: Object exposing ``__datafusion_query_planner__`` (see
:class:`QueryPlannerExportable`) or a raw
``datafusion_query_planner`` PyCapsule.
Examples:
>>> from my_extension import DistributedQueryPlanner # doctest: +SKIP
>>> ctx = SessionContext()
>>> ctx.set_query_planner(DistributedQueryPlanner()) # doctest: +SKIP
>>> ctx.sql("SELECT * FROM remote_table").collect() # doctest: +SKIP
Layer a planner on top of the one already installed by capturing
the existing planner first:
>>> fallback = ctx.__datafusion_query_planner__() # doctest: +SKIP
>>> ctx.set_query_planner(
... DistributedQueryPlanner(fallback=fallback)
... ) # doctest: +SKIP
"""
self.ctx.set_query_planner(planner)
def with_extensions(
self, *extensions: SessionExtensionExportable
) -> SessionContext:
"""Create a new session context with the given extension bundles.
This is the preferred way to install FFI extensions that need a
task-context provider (extension codecs and query planners). Each
extension's ``__datafusion_session_extension__`` method is called with
this context so it can bind its components to the session they will
run on, then all components are installed in one step. This avoids the
pitfalls of chaining :py:meth:`with_logical_extension_codec`,
:py:meth:`with_physical_extension_codec`, and
:py:meth:`set_query_planner` by hand, where the codecs a planner was
built against can end up stale.
Codecs compose with the existing chain and with each other: extensions
are processed left to right and their codecs are appended to the chain
in that order. Decoding routes by codec id, so the order matters only
for encoding. At most one extension may supply a query planner. If none
does, an existing FFI planner is rebound to the final codec chains.
Each codec is named after its exporting class, as
:py:meth:`with_logical_extension_codec` describes. A codec handed over
as a bare ``PyCapsule`` has no class to take a name from, so it is
named after the extension that contributed it — the extension's import
path is library-owned and stable across processes, so plans it writes
stay decodable elsewhere. Declare ``__datafusion_codec_id__`` on the
extension to pin that name against a later class rename, or on the
object handed over to name a codec directly.
Like the individual ``with_*`` methods, the returned context shares its
session with this one: catalogs, tables, registered functions, and
configuration are the one session, so a registration on either side is
visible to both, and the planner is installed on that shared session
even if the returned context is discarded. Only the Python-side codec
chains are specific to the returned handle.
No state is written until every extension has run and every capsule has
been validated, so an extension that raises or returns invalid
components leaves the session as it was. The exception is an extension
that mutates the context it is handed — registering a table, say —
which is not rolled back. Extension factories should treat that context
as configuration-only.
The session owns the installed components' task-context providers, and
dependent objects do not extend its lifetime. Keep a context on the
session alive for as long as DataFrames or plans derived from it are in
use; FFI operations after the last one is collected raise an error.
Args:
extensions: Extension bundles to install, in the order their
codecs join the chain.
Returns:
A new context with all extension components installed.
Raises:
TypeError: If an argument does not implement the protocol or
returns something other than a
:py:class:`SessionExtensionComponents`.
ValueError: If no extensions are given, more than one extension
supplies a query planner, or two codecs claim the same id. An
extension that contributes two instances of one codec class,
or two bare capsules of the same kind, must declare
``__datafusion_codec_id__`` on at least one of them; the
collision is refused rather than resolved by position, because
a positional id would break stored plans the first time the
extension reordered what it returns.
Examples:
The example is skipped here because it needs a built FFI
extension library, which this package does not ship. It is run
verbatim against a real one by
``test_with_extensions_docstring_example_still_runs`` in
``examples/datafusion-ffi-query-planner-example``, so it cannot
drift from the API.
>>> from my_extension import DistributedEngineExtension # doctest: +SKIP
>>> ctx = SessionContext().with_extensions(
... DistributedEngineExtension("scheduler:50050")
... ) # doctest: +SKIP
>>> batches = ctx.sql("SELECT 1 AS n").collect() # doctest: +SKIP
>>> batches[0].column(0).to_pylist() # doctest: +SKIP
[1]
"""
if not extensions:
msg = "with_extensions requires at least one extension"
raise ValueError(msg)
for extension in extensions:
if not isinstance(extension, SessionExtensionExportable):
msg = (
"Extension does not implement __datafusion_session_extension__: "
f"{extension!r}"
)
raise TypeError(msg)
# Bind every component against this context, not a context derived from
# it. There is one `Arc<SessionContext>` per session, so a component
# bound here holds a task-context provider that the returned handle
# keeps alive, and `_install_extensions` writes the final state through
# that same session.
#
# Each codec is paired with the extension that contributed it. A codec
# handed over as a bare capsule has no class to take an id from, so it
# is named after that extension rather than randomized.
logical_codecs: list[
tuple[LogicalExtensionCodecExportable | _PyCapsule, object]
] = []
physical_codecs: list[
tuple[PhysicalExtensionCodecExportable | _PyCapsule, object]
] = []
planner: QueryPlannerExportable | _PyCapsule | None = None
for extension in extensions:
components = extension.__datafusion_session_extension__(self)
if not isinstance(components, SessionExtensionComponents):
msg = (
"__datafusion_session_extension__ must return "
"SessionExtensionComponents, got "
f"{type(components).__name__} from {extension!r}"
)
raise TypeError(msg)
logical_codecs.extend(
(codec, extension) for codec in components.logical_extension_codecs
)
physical_codecs.extend(
(codec, extension) for codec in components.physical_extension_codecs
)
if components.query_planner is not None:
if planner is not None:
msg = (
"Multiple extensions supplied a query planner; a "
"session context has exactly one. Layer planners "
"explicitly instead."
)
raise ValueError(msg)
planner = components.query_planner
new = SessionContext.__new__(SessionContext)
new.ctx = self.ctx._install_extensions(logical_codecs, physical_codecs, planner)
return new
def table_provider(self, name: str) -> Table:
"""Return the :py:class:`~datafusion.catalog.Table` for the given table name.
Args:
name: Name of the table.
Returns:
The table provider.
Raises:
KeyError: If the table is not found.
Examples:
>>> import pyarrow as pa
>>> ctx = SessionContext()
>>> batch = pa.RecordBatch.from_pydict({"x": [1, 2]})
>>> ctx.register_record_batches("my_table", [[batch]])
>>> tbl = ctx.table_provider("my_table")
>>> tbl.schema
x: int64
"""
from datafusion.catalog import Table # noqa: PLC0415
return Table(self.ctx.table_provider(name))
def read_json(
self,
path: str | pathlib.Path,
schema: pa.Schema | None = None,
schema_infer_max_records: int = 1000,
file_extension: str = ".json",
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
file_compression_type: str | None = None,
object_store: Any | None = None,
) -> DataFrame:
"""Read a line-delimited JSON data source.
Args:
path: Path to the JSON file.
schema: The data source schema.
schema_infer_max_records: Maximum number of rows to read from JSON
files for schema inference if needed.
file_extension: File extension; only files with this extension are
selected for data input.
table_partition_cols: Partition columns.
file_compression_type: File compression type.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
Returns:
DataFrame representation of the read JSON files.
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if table_partition_cols is None:
table_partition_cols = []
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
return DataFrame(
self.ctx.read_json(
path,
schema,
schema_infer_max_records,
file_extension,
table_partition_cols,
file_compression_type,
)
)
def read_csv(
self,
path: str | pathlib.Path | list[str] | list[pathlib.Path],
schema: pa.Schema | None = None,
has_header: bool = True,
delimiter: str = ",",
schema_infer_max_records: int = DEFAULT_MAX_INFER_SCHEMA,
file_extension: str = ".csv",
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
file_compression_type: str | None = None,
options: CsvReadOptions | None = None,
object_store: Any | None = None,
) -> DataFrame:
"""Read a CSV data source.
Args:
path: Path to the CSV file
schema: An optional schema representing the CSV files. If None, the
CSV reader will try to infer it based on data in file.
has_header: Whether the CSV file have a header. If schema inference
is run on a file with no headers, default column names are
created.
delimiter: An optional column delimiter.
schema_infer_max_records: Maximum number of rows to read from CSV
files for schema inference if needed.
file_extension: File extension; only files with this extension are
selected for data input.
table_partition_cols: Partition columns.
file_compression_type: File compression type.
options: Set advanced options for CSV reading. This cannot be
combined with any of the other options in this method.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
Returns:
DataFrame representation of the read CSV files
"""
if object_store is not None:
register_path = path[0] if isinstance(path, list) else path
self._register_object_store_for_path(register_path, object_store)
if options is not None and (
schema is not None
or not has_header
or delimiter != ","
or schema_infer_max_records != DEFAULT_MAX_INFER_SCHEMA
or file_extension != ".csv"
or table_partition_cols is not None
or file_compression_type is not None
):
message = (
"Combining CsvReadOptions parameter with additional options "
"is not supported. Use CsvReadOptions to set parameters."
)
warnings.warn(
message,
category=UserWarning,
stacklevel=2,
)
options = (
options
if options is not None
else CsvReadOptions(
schema=schema,
has_header=has_header,
delimiter=delimiter,
schema_infer_max_records=schema_infer_max_records,
file_extension=file_extension,
table_partition_cols=table_partition_cols,
file_compression_type=file_compression_type,
)
)
return DataFrame(
self.ctx.read_csv(
path,
options.to_inner(),
)
)
def read_parquet(
self,
path: str | pathlib.Path,
table_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
parquet_pruning: bool = True,
file_extension: str = ".parquet",
skip_metadata: bool = True,
schema: pa.Schema | None = None,
file_sort_order: Sequence[Sequence[SortKey]] | None = None,
object_store: Any | None = None,
) -> DataFrame:
"""Read a Parquet source into a :py:class:`~datafusion.dataframe.Dataframe`.
Args:
path: Path to the Parquet file.
table_partition_cols: Partition columns.
parquet_pruning: Whether the parquet reader should use the predicate
to prune row groups.
file_extension: File extension; only files with this extension are
selected for data input.
skip_metadata: Whether the parquet reader should skip any metadata
that may be in the file schema. This can help avoid schema
conflicts due to metadata.
schema: An optional schema representing the parquet files. If None,
the parquet reader will try to infer it based on data in the
file.
file_sort_order: Sort order for the file. Each sort key can be
specified as a column name (``str``), an expression
(``Expr``), or a ``SortExpr``.
object_store: A pre-configured object store instance (e.g.
:py:class:`~datafusion.object_store.AmazonS3`,
:py:class:`~datafusion.object_store.GoogleCloud`,
:py:class:`~datafusion.object_store.MicrosoftAzure`) to use
for accessing the file. When provided, the store is
automatically registered for the URL scheme and host parsed
from ``path``, removing the need to call
:py:meth:`register_object_store` separately. This is
especially useful in multi-threaded environments where
setting credentials via ``os.environ`` is not thread-safe.
Returns:
DataFrame representation of the read Parquet files
Examples:
Read a local Parquet file:
>>> import datafusion
>>> ctx = datafusion.SessionContext()
>>> df = ctx.read_parquet("data.parquet") # doctest: +SKIP
Read from S3 with inline credentials (thread-safe):
>>> from datafusion.object_store import AmazonS3 # doctest: +SKIP
>>> store = AmazonS3(
... bucket_name="my-bucket",
... region="us-east-1",
... access_key_id="...",
... secret_access_key="...",
... ) # doctest: +SKIP
>>> df = ctx.read_parquet(
... "s3://my-bucket/data.parquet",
... object_store=store,
... ) # doctest: +SKIP
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if table_partition_cols is None:
table_partition_cols = []
table_partition_cols = _convert_table_partition_cols(table_partition_cols)
file_sort_order = self._convert_file_sort_order(file_sort_order)
return DataFrame(
self.ctx.read_parquet(
path,
table_partition_cols,
parquet_pruning,
file_extension,
skip_metadata,
schema,
file_sort_order,
)
)
def read_avro(
self,
path: str | pathlib.Path,
schema: pa.Schema | None = None,
file_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
file_extension: str = ".avro",
object_store: Any | None = None,
) -> DataFrame:
"""Create a :py:class:`DataFrame` for reading Avro data source.
Args:
path: Path to the Avro file.
schema: The data source schema.
file_partition_cols: Partition columns.
file_extension: File extension to select.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
Returns:
DataFrame representation of the read Avro file
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if file_partition_cols is None:
file_partition_cols = []
file_partition_cols = _convert_table_partition_cols(file_partition_cols)
return DataFrame(
self.ctx.read_avro(path, schema, file_partition_cols, file_extension)
)
def read_arrow(
self,
path: str | pathlib.Path,
schema: pa.Schema | None = None,
file_extension: str = ".arrow",
file_partition_cols: list[tuple[str, str | pa.DataType]] | None = None,
object_store: Any | None = None,
) -> DataFrame:
"""Create a :py:class:`DataFrame` for reading an Arrow IPC data source.
Args:
path: Path to the Arrow IPC file.
schema: The data source schema.
file_extension: File extension to select.
file_partition_cols: Partition columns.
object_store: A pre-configured object store instance to use for
accessing the file. When provided, the store is automatically
registered for the URL scheme and host parsed from ``path``.
Returns:
DataFrame representation of the read Arrow IPC file.
Examples:
>>> import tempfile, os
>>> ctx = dfn.SessionContext()
>>> table = pa.table({"a": [1, 2, 3]})
>>> with tempfile.TemporaryDirectory() as tmpdir:
... path = os.path.join(tmpdir, "data.arrow")
... with pa.ipc.new_file(path, table.schema) as writer:
... writer.write_table(table)
... df = ctx.read_arrow(path)
... df.collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
1,
2,
3
]
Provide an explicit ``schema`` to override schema inference:
>>> with tempfile.TemporaryDirectory() as tmpdir:
... path = os.path.join(tmpdir, "data.arrow")
... with pa.ipc.new_file(path, table.schema) as writer:
... writer.write_table(table)
... df = ctx.read_arrow(path, schema=pa.schema([("a", pa.int64())]))
... df.collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
1,
2,
3
]
Use ``file_extension`` to read files with a non-default extension:
>>> with tempfile.TemporaryDirectory() as tmpdir:
... path = os.path.join(tmpdir, "data.ipc")
... with pa.ipc.new_file(path, table.schema) as writer:
... writer.write_table(table)
... df = ctx.read_arrow(path, file_extension=".ipc")
... df.collect()[0].column(0)
<pyarrow.lib.Int64Array object at ...>
[
1,
2,
3
]
"""
if object_store is not None:
self._register_object_store_for_path(path, object_store)
if file_partition_cols is None:
file_partition_cols = []
file_partition_cols = _convert_table_partition_cols(file_partition_cols)
return DataFrame(
self.ctx.read_arrow(path, schema, file_extension, file_partition_cols)
)
def read_empty(self) -> DataFrame:
"""Create an empty :py:class:`DataFrame` with no columns or rows.
See Also:
This is an alias for :meth:`empty_table`.
"""
return self.empty_table()
def read_table(
self, table: Table | TableProviderExportable | DataFrame | pa.dataset.Dataset
) -> DataFrame:
"""Creates a :py:class:`~datafusion.dataframe.DataFrame` from a table."""
return DataFrame(self.ctx.read_table(table))
def execute(self, plan: ExecutionPlan, partitions: int) -> RecordBatchStream:
"""Execute the ``plan`` and return the results."""
return RecordBatchStream(self.ctx.execute(plan._raw_plan, partitions))
@staticmethod
def _convert_file_sort_order(
file_sort_order: Sequence[Sequence[SortKey]] | None,
) -> list[list[expr_internal.SortExpr]] | None:
"""Convert nested ``SortKey`` sequences into raw sort expressions.
Each ``SortKey`` can be a column name string, an ``Expr``, or a
``SortExpr`` and will be converted using
:func:`datafusion.expr.sort_list_to_raw_sort_list`.
"""
# Convert each ``SortKey`` in the provided sort order to the low-level
# representation expected by the Rust bindings.
return (
[sort_list_to_raw_sort_list(f) for f in file_sort_order]
if file_sort_order is not None
else None
)
@staticmethod
def _convert_table_partition_cols(
table_partition_cols: list[tuple[str, str | pa.DataType]],
) -> list[tuple[str, pa.DataType]]:
warn = False
converted_table_partition_cols = []
for col, data_type in table_partition_cols:
if isinstance(data_type, str):
warn = True
if data_type == "string":
converted_data_type = pa.string()
elif data_type == "int":
converted_data_type = pa.int32()
else:
message = (
f"Unsupported literal data type '{data_type}' for partition "
"column. Supported types are 'string' and 'int'"
)
raise ValueError(message)
else:
converted_data_type = data_type
converted_table_partition_cols.append((col, converted_data_type))
if warn:
message = (
"using literals for table_partition_cols data types is deprecated,"
"use pyarrow types instead"
)
warnings.warn(
message,
category=DeprecationWarning,
stacklevel=2,
)
return converted_table_partition_cols
def __datafusion_task_context_provider__(self) -> Any:
"""Access the PyCapsule FFI_TaskContextProvider."""
return self.ctx.__datafusion_task_context_provider__()
@property
def __datafusion_codec_id__(self) -> str:
"""Identity this context carries when installed as an extension codec.
A context can be installed on another session as an extension codec,
which tags the payloads it writes with this string. It is unique per
session, so two contexts can be installed on one session and a plan
written through one will not be decoded by the other.
Contexts derived from the same session — including the ones returned by
:py:meth:`with_logical_extension_codec`,
:py:meth:`with_python_udf_inlining`, and :py:meth:`with_extensions` —
report the same id, so only one of them can be installed on a given
session. That is the intended answer: they are one session, so their
payloads would be indistinguishable on decode.
Examples:
>>> from datafusion import SessionContext
>>> ctx = SessionContext()
>>> ctx.__datafusion_codec_id__.startswith("session:")
True
>>> ctx.__datafusion_codec_id__ == SessionContext().__datafusion_codec_id__
False
"""
return self.ctx.__datafusion_codec_id__
def __datafusion_logical_extension_codec__(self, session: Any = None) -> Any:
"""Access the PyCapsule FFI_LogicalExtensionCodec.
``session`` is accepted so a context satisfies the same protocol an
extension library implements, where the argument is how the library
reaches the session it is being installed on. A context already is one,
so the argument is ignored.
"""
return self.ctx.__datafusion_logical_extension_codec__(session)
def __datafusion_query_planner__(self, session: Any = None) -> Any:
"""Access the ``FFI_QueryPlanner`` PyCapsule for the current planner.
See :meth:`__datafusion_logical_extension_codec__` for ``session``.
"""
return self.ctx.__datafusion_query_planner__(session)
def with_logical_extension_codec(
self,
codec: LogicalExtensionCodecExportable | _PyCapsule,
codec_id: str | None = None,
) -> SessionContext:
"""Create a new session context with an additional logical codec.
Only FFI codecs are supported. Pass any object implementing
``__datafusion_logical_extension_codec__`` (see
:py:class:`~datafusion.user_defined.LogicalExtensionCodecExportable`).
Codecs compose: each call appends the codec rather than replacing
codecs installed earlier, so one session can carry codecs from several
independent libraries and the order they are installed in does not
affect decoding.
A serialized plan records which codec wrote each payload, as a short id
taken from the codec's class. ``codec_id`` overrides that id and is
normally unnecessary. Pass it when installing from a bare ``PyCapsule``,
which has no class to take an id from, or when installing two instances
of one class, which otherwise claim the same id and raise ``ValueError``.
The returned context shares its session state with the original, so a
later registration on either is visible to both, and an installed query
planner is rebound on the shared session even if the returned context is
discarded.
See :ref:`ffi` in the online documentation for how ids are assigned,
what an extension codec has to implement, and a worked multi-library
registration recipe.
Examples:
>>> from datafusion import SessionContext
>>> ctx = SessionContext()
>>> ctx = ctx.with_logical_extension_codec(
... my_library.Codec()
... ) # doctest: +SKIP
Installing from a bare capsule, pinning the id so encoded
plans remain decodable on another session:
>>> ctx = ctx.with_logical_extension_codec(
... capsule, codec_id="my_library.Codec"
... ) # doctest: +SKIP
"""
new_internal = self.ctx.with_logical_extension_codec(codec, codec_id)
new = SessionContext.__new__(SessionContext)
new.ctx = new_internal
return new
def logical_extension_codec_ids(self) -> list[str]:
"""List the logical extension codecs installed on this session.
Returns the identity of each installed codec, in install order. Those
identities are what encoding stamps onto a payload and what decoding
dispatches on, so this is how to check which library owns a plan and
whether a session is able to decode one.
DataFusion's own default codec is not listed. It handles whatever no
installed codec claims, and it carries no identity to list.
Examples:
>>> from datafusion import SessionContext
>>> ctx = SessionContext()
>>> ctx.logical_extension_codec_ids()
[]
>>> ctx = ctx.with_logical_extension_codec(
... my_library.Codec()
... ) # doctest: +SKIP
>>> ctx.logical_extension_codec_ids() # doctest: +SKIP
['my_library.Codec']
"""
return self.ctx.logical_extension_codec_ids()
def __datafusion_physical_extension_codec__(self, session: Any = None) -> Any:
"""Access the PyCapsule FFI_PhysicalExtensionCodec.
See :meth:`__datafusion_logical_extension_codec__` for ``session``.
"""
return self.ctx.__datafusion_physical_extension_codec__(session)
def physical_extension_codec_ids(self) -> list[str]:
"""List the physical extension codecs installed on this session.
See :py:meth:`logical_extension_codec_ids`.
Examples:
>>> from datafusion import SessionContext
>>> ctx = SessionContext()
>>> ctx.physical_extension_codec_ids()
[]
"""
return self.ctx.physical_extension_codec_ids()
def with_physical_extension_codec(
self,
codec: PhysicalExtensionCodecExportable | _PyCapsule,
codec_id: str | None = None,
) -> SessionContext:
"""Create a new session context with an additional physical codec.
Only FFI codecs are supported. Pass any object implementing
``__datafusion_physical_extension_codec__`` (see
:py:class:`~datafusion.user_defined.PhysicalExtensionCodecExportable`).
Composes and assigns an id exactly as
:py:meth:`with_logical_extension_codec` does, including when to pass
``codec_id`` and what the returned context shares. See that method.
Examples:
>>> from datafusion import SessionContext
>>> ctx = SessionContext()
>>> ctx = ctx.with_physical_extension_codec(
... my_library.PhysicalCodec()
... ) # doctest: +SKIP
>>> ctx = ctx.with_physical_extension_codec(
... capsule, codec_id="my_library.PhysicalCodec"
... ) # doctest: +SKIP
"""
new_internal = self.ctx.with_physical_extension_codec(codec, codec_id)
new = SessionContext.__new__(SessionContext)
new.ctx = new_internal
return new
def with_python_udf_inlining(self, *, enabled: bool) -> SessionContext:
"""Control whether Python UDFs are embedded in serialized expressions.
``enabled`` is keyword-only and required: callers must pick a
mode explicitly. Fresh sessions inline UDFs (``enabled=True``
behavior) until this method overrides the toggle.
With ``enabled=True``, serialized expressions carry the Python
code for any scalar, aggregate, or window UDFs they reference.
The receiver rebuilds the UDFs from those bytes and does not
need to register them first.
With ``enabled=False``, serialized expressions store only the
UDF names. This has two uses:
* **Cross-language portability.** The bytes can be decoded by a
non-Python receiver, which must already have UDFs registered
under matching names.
* **Safer deserialization.** :meth:`Expr.from_bytes` will refuse
to rebuild Python UDFs rather than call ``cloudpickle.loads``
on untrusted input.
The setting affects :meth:`Expr.to_bytes` and
:meth:`Expr.from_bytes` whenever this session is passed as the
``ctx`` argument. :func:`pickle.dumps` and :func:`pickle.loads`
do not pass a context, so to apply the setting through pickle,
register this session with
:func:`datafusion.ipc.set_sender_ctx` on the sender and
:func:`datafusion.ipc.set_worker_ctx` on the receiver.
.. warning:: Security
This setting narrows only :meth:`Expr.from_bytes`. Calling
:func:`pickle.loads` on untrusted bytes remains unsafe
regardless of the toggle.
Returns a new :class:`SessionContext` with the toggle applied;
the original context's own codec settings are unchanged. The
returned context shares its session state with the original, so
a later registration on either is visible to both. If a custom
query planner is installed, it is rebuilt against the new codecs
on the shared session, so the original context plans with them
too. This happens on the shared session, so it takes effect even
if the returned context is discarded.
Examples:
>>> import pyarrow as pa
>>> from datafusion import SessionContext, Expr, col, udf
>>> ctx = SessionContext()
>>> identity = udf(lambda a: a, [pa.int64()], pa.int64(),
... volatility="immutable", name="identity_demo")
>>> ctx.register_udf(identity)
>>> blob = identity(col("x")).to_bytes(ctx)
>>> strict = SessionContext().with_python_udf_inlining(enabled=False)
>>> try:
... Expr.from_bytes(blob, strict)
... except Exception as e:
... print("Refusing to deserialize" in str(e))
True
"""
new_internal = self.ctx.with_python_udf_inlining(enabled)
new = SessionContext.__new__(SessionContext)
new.ctx = new_internal
return new