| # 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, | |
| SessionComponentsExportable, | |
| SessionExtensionComponents, | |
| SessionPlannerExportable, | |
| ) | |
| 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. | |
| See :ref:`io_custom_table_provider` for registering one, and | |
| :ref:`extension_providers` for writing one. | |
| Args: | |
| session: See | |
| :py:class:`~datafusion.user_defined.LogicalExtensionCodecExportable`. | |
| For this getter it is a session when the provider is registered | |
| through :py:meth:`SessionContext.register_table` and the host's | |
| logical codec when it goes through | |
| :py:meth:`datafusion.catalog.Schema.register_table`, so | |
| duck-typing it is not optional. | |
| """ | |
| 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. | |
| Each entry is applied as though passed to :py:meth:`set`, so the same | |
| keys are rejected. See :ref:`configuration`. | |
| Args: | |
| config_options: Options to apply, keyed by fully qualified name. | |
| Raises: | |
| ValueError: If a key names no known option, or a value does not | |
| parse as that option's declared type. Which of several bad | |
| entries is reported is not defined. | |
| Example usage: | |
| >>> from datafusion import SessionConfig, SessionContext | |
| >>> config = SessionConfig( | |
| ... config_options={ | |
| ... "datafusion.execution.batch_size": "1024", | |
| ... "datafusion.execution.target_partitions": "8", | |
| ... } | |
| ... ) | |
| >>> ctx = SessionContext(config.with_information_schema(True)) | |
| >>> ctx.sql( | |
| ... "select value from information_schema.df_settings where name in" | |
| ... " ('datafusion.execution.batch_size'," | |
| ... " 'datafusion.execution.target_partitions') order by name" | |
| ... ).collect()[0]["value"].to_pylist() | |
| ['1024', '8'] | |
| """ | |
| 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 by its fully qualified key. | |
| Not every key that ``information_schema.df_settings`` lists can be set | |
| here: the ``datafusion.runtime.*`` entries come from the runtime | |
| environment rather than from the session config. See | |
| :ref:`configuration`. | |
| Args: | |
| key: Option key including its namespace, such as | |
| ``datafusion.execution.batch_size``. | |
| value: Option value as a string, parsed according to the type the | |
| option declares. | |
| Returns: | |
| This :py:class:`SessionConfig`, modified in place, so that calls | |
| chain. | |
| Raises: | |
| ValueError: If ``key`` names no known option, or if ``value`` does | |
| not parse as that option's declared type. | |
| Example usage: | |
| >>> from datafusion import SessionConfig, SessionContext | |
| >>> config = SessionConfig().set("datafusion.execution.batch_size", "1024") | |
| >>> ctx = SessionContext(config.with_information_schema(True)) | |
| >>> ctx.sql( | |
| ... "select value from information_schema.df_settings" | |
| ... " where name = 'datafusion.execution.batch_size'" | |
| ... ).collect()[0]["value"][0] | |
| <pyarrow.StringScalar: '1024'> | |
| """ | |
| 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. | |
| **A context is a handle on a session, not the session itself.** The | |
| ``with_*`` methods — :py:meth:`with_logical_extension_codec`, | |
| :py:meth:`with_physical_extension_codec`, | |
| :py:meth:`with_python_udf_inlining`, and :py:meth:`with_extensions` — | |
| return a new context wrapping the *same* underlying session. Only the | |
| Python-side codec settings differ; catalogs, tables, registered functions, | |
| and configuration are the one shared session, so a registration through | |
| either handle is visible to both. | |
| A few things therefore belong to the session rather than to a handle, and | |
| take effect even if the handle that set them is discarded: the query | |
| planner (see :py:meth:`set_query_planner`), and the rebuild of an installed | |
| foreign planner that follows installing a codec. :ref:`extension_sessions` | |
| in the online documentation works through when that matters. | |
| **Keep a context alive for as long as anything derived from it is in use.** | |
| A :py:class:`~datafusion.DataFrame`, logical plan, or exported capsule does | |
| not extend the session's lifetime. Once the last context on a session is | |
| collected, any operation that reaches an extension codec fails with | |
| ``TaskContextProvider went out of scope over FFI boundary``. | |
| """ | |
| 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, in the same way | |
| :meth:`~SessionContext.add_physical_optimizer_rule` installs a rule. | |
| Returns nothing, because the planner lives in the session rather than | |
| in a handle on it — installing one is visible to every context sharing | |
| that session, including ones an earlier ``with_*`` call returned. See | |
| :py:class:`SessionContext`. | |
| 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; the rebuild that | |
| follows a later codec install does not reach the fallback inside one. | |
| See :ref:`planner_codec_rebinding`, or prefer | |
| :py:meth:`with_extensions`, which cannot capture a partial chain. | |
| Args: | |
| planner: Object exposing ``__datafusion_query_planner__`` (see | |
| :py:class:`~datafusion.extensions.QueryPlannerExportable`) or a raw | |
| ``datafusion_query_planner`` PyCapsule. | |
| Raises: | |
| ValueError: If the capsule is not named ``datafusion_query_planner``. | |
| Examples: | |
| A session exports its own planner, which is what you capture to | |
| wrap: | |
| >>> from datafusion import SessionContext | |
| >>> ctx = SessionContext() | |
| >>> fallback = ctx.__datafusion_query_planner__() | |
| >>> type(fallback).__name__ | |
| 'PyCapsule' | |
| Skipped here (needs a built extension library): | |
| >>> from my_extension import DistributedQueryPlanner # doctest: +SKIP | |
| >>> ctx.set_query_planner( | |
| ... DistributedQueryPlanner(fallback=fallback) | |
| ... ) # doctest: +SKIP | |
| >>> ctx.sql("SELECT * FROM remote_table").collect() # doctest: +SKIP | |
| """ | |
| self.ctx.set_query_planner(planner) | |
| def with_extensions( | |
| self, *extensions: SessionComponentsExportable | SessionPlannerExportable | |
| ) -> SessionContext: | |
| """Create a new session context with the given extension bundles. | |
| This is the preferred way to install extension codecs and query | |
| planners, because it removes the ordering question that installing them | |
| by hand creates. | |
| Each argument is called twice, in two phases: | |
| 1. ``__datafusion_session_components__(ctx)`` on every extension, then | |
| all the returned codecs are installed at once. | |
| 2. ``__datafusion_session_planner__(ctx, fallback)`` on every | |
| extension, **in argument order**, each handed the planner built so | |
| far and the context carrying every bundle's codecs. The last | |
| extension listed ends up outermost. | |
| An extension implements either hook or both. Return ``None`` from the | |
| planner hook to contribute no planner; see | |
| :py:class:`~datafusion.extensions.SessionPlannerExportable`. | |
| Nothing is written to the session until every hook has returned and | |
| every capsule has been validated, so a hook that raises leaves the | |
| session as it was. A hook that *mutates* the context it is handed — | |
| registering a table, say — is not rolled back, which is why bundle | |
| objects must be configuration-only. | |
| Shares its session with this context — see :py:class:`SessionContext`. | |
| See :ref:`extension_bundles` in the online documentation for why the | |
| phases are split, how to contribute a bundle's two halves at different | |
| positions, and a worked Rust implementation. | |
| Args: | |
| extensions: Extension bundles to install. Order is irrelevant for | |
| codecs and significant for planners, which nest in this order | |
| with the last one outermost. Passing none installs nothing and | |
| returns a handle on this session, so a caller assembling the | |
| list at runtime need not special-case it being empty. | |
| Returns: | |
| A new context with all extension components installed. | |
| Raises: | |
| TypeError: If an argument implements neither hook, if a hook | |
| returns the wrong type, or if a codec is contributed as a bare | |
| ``PyCapsule`` rather than an object exposing the getter. | |
| ValueError: If two codecs claim the same id, or a getter returns a | |
| capsule of the wrong kind. See | |
| :py:meth:`with_logical_extension_codec` for how ids are | |
| assigned. | |
| Examples: | |
| The returned handle is a different object sharing one session, and | |
| an empty call is legal: | |
| >>> from datafusion import SessionContext | |
| >>> ctx = SessionContext() | |
| >>> derived = ctx.with_extensions() | |
| >>> derived is ctx | |
| False | |
| >>> ctx.from_pydict({"a": [1, 2]}, name="t") # doctest: +ELLIPSIS | |
| DataFrame()... | |
| >>> derived.table_exist("t") | |
| True | |
| >>> derived.logical_extension_codec_ids() | |
| [] | |
| A runnable multi-bundle example, showing what a bundle returns and | |
| how its codec ids accumulate, is in :ref:`extension_bundles`. | |
| Real usage. Skipped here (needs a built extension library); run | |
| verbatim by ``test_with_extensions_docstring_example_still_runs``. | |
| >>> 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] | |
| """ | |
| for extension in extensions: | |
| if not isinstance( | |
| extension, (SessionComponentsExportable, SessionPlannerExportable) | |
| ): | |
| msg = ( | |
| "Extension implements neither " | |
| "__datafusion_session_components__ nor " | |
| f"__datafusion_session_planner__: {extension!r}" | |
| ) | |
| raise TypeError(msg) | |
| # Phase one: collect every bundle's codecs. Components are bound | |
| # 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. | |
| logical_codecs: list[LogicalExtensionCodecExportable] = [] | |
| physical_codecs: list[PhysicalExtensionCodecExportable] = [] | |
| for extension in extensions: | |
| if not isinstance(extension, SessionComponentsExportable): | |
| continue | |
| components = extension.__datafusion_session_components__(self) | |
| if not isinstance(components, SessionExtensionComponents): | |
| msg = ( | |
| "__datafusion_session_components__ must return " | |
| "SessionExtensionComponents, got " | |
| f"{type(components).__name__} from {extension!r}" | |
| ) | |
| raise TypeError(msg) | |
| logical_codecs.extend(components.logical_extension_codecs) | |
| physical_codecs.extend(components.physical_extension_codecs) | |
| # Writes nothing: the chains belong to the new handle, so a failure | |
| # above or below leaves this context as it was. | |
| new = SessionContext.__new__(SessionContext) | |
| new.ctx = self.ctx._install_extension_codecs(logical_codecs, physical_codecs) | |
| # Phase two: nest the planners, outermost last. Each hook runs against | |
| # `new`, which carries the final chains, so a planner captured here | |
| # never sees a partial codec set. `planner` stays None when no bundle | |
| # supplies one, which leaves an already-installed planner in place | |
| # rather than wrapping the session's default in an FFI hop. | |
| planner: _PyCapsule | None = None | |
| for extension in extensions: | |
| if not isinstance(extension, SessionPlannerExportable): | |
| continue | |
| fallback = ( | |
| planner | |
| if planner is not None | |
| else new.ctx.__datafusion_query_planner__() | |
| ) | |
| supplied = extension.__datafusion_session_planner__(new, fallback) | |
| if supplied is None: | |
| continue | |
| planner = new.ctx._export_query_planner(supplied) | |
| # Rebinding the session's planner is a side effect on state shared with | |
| # every other handle, so do not pay it for a call that installs nothing | |
| # -- the same guard `with_python_udf_inlining` carries. With no codec | |
| # installed the chains the planner would be rebuilt against are the ones | |
| # it already holds, so the rebuild is unobservable except in the one case | |
| # where it does harm: a planner sitting on some *other* handle's codecs | |
| # gets dragged onto this handle's, silently undoing that install. | |
| if planner is not None or logical_codecs or physical_codecs: | |
| new.ctx._install_extension_planner(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, partition: int) -> RecordBatchStream: | |
| """Execute a single partition of ``plan`` and stream its batches. | |
| Args: | |
| plan: The physical plan to execute. | |
| partition: Index of the partition to execute, in | |
| ``range(plan.partition_count)``. | |
| Returns: | |
| A stream over the record batches that partition produces. | |
| Raises: | |
| ValueError: If ``partition`` is not a valid index for ``plan``. | |
| OverflowError: If ``partition`` is negative, or too large to fit a | |
| platform-sized unsigned integer. | |
| Example usage: | |
| >>> import pyarrow as pa | |
| >>> from datafusion import SessionContext | |
| >>> ctx = SessionContext() | |
| >>> ctx.register_record_batches( | |
| ... "t", [[pa.record_batch({"a": [1, 2]})], [pa.record_batch({"a": [3]})]] | |
| ... ) | |
| >>> plan = ctx.sql("select a from t").execution_plan() | |
| >>> plan.partition_count | |
| 2 | |
| >>> sum( | |
| ... batch.to_pyarrow().num_rows | |
| ... for p in range(plan.partition_count) | |
| ... for batch in ctx.execute(plan, p) | |
| ... ) | |
| 3 | |
| """ | |
| return RecordBatchStream(self.ctx.execute(plan._raw_plan, partition)) | |
| @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 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 — see :py:class:`SessionContext` — 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 host's codec. A context already carries one, so the | |
| argument is ignored. See | |
| :py:class:`~datafusion.user_defined.LogicalExtensionCodecExportable` | |
| for what an extension library does with it. | |
| Args: | |
| session: Accepted and ignored. | |
| Returns: | |
| A ``datafusion_logical_extension_codec`` PyCapsule. | |
| Examples: | |
| >>> from datafusion import SessionContext | |
| >>> type(SessionContext().__datafusion_logical_extension_codec__()).__name__ | |
| 'PyCapsule' | |
| """ | |
| 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. | |
| This is how you capture the planner a session already has in order to | |
| wrap it. Capture it *before* installing the new one, since | |
| :py:meth:`set_query_planner` replaces rather than layers. | |
| Args: | |
| session: Accepted and ignored. See | |
| :meth:`__datafusion_logical_extension_codec__`. | |
| Returns: | |
| A ``datafusion_query_planner`` PyCapsule wrapping the session's | |
| current planner, exported for a foreign planner to delegate to. | |
| Examples: | |
| >>> from datafusion import SessionContext | |
| >>> ctx = SessionContext() | |
| >>> fallback = ctx.__datafusion_query_planner__() | |
| >>> type(fallback).__name__ | |
| 'PyCapsule' | |
| Both spellings work, since the argument is ignored: | |
| >>> type(ctx.__datafusion_query_planner__(ctx)).__name__ | |
| 'PyCapsule' | |
| """ | |
| 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. | |
| Shares its session with this context — see :py:class:`SessionContext`. | |
| See :ref:`extension_codec_ids` in the online documentation for how ids | |
| are assigned and what an extension codec has to implement, and | |
| :py:meth:`with_extensions` for installing a library's codecs and planner | |
| together. | |
| Args: | |
| codec: Object implementing ``__datafusion_logical_extension_codec__`` | |
| (see | |
| :py:class:`~datafusion.user_defined.LogicalExtensionCodecExportable`), | |
| or a raw ``datafusion_logical_extension_codec`` PyCapsule. | |
| codec_id: Overrides the id the codec's payloads are tagged with. | |
| Normally unnecessary. Pass it when installing two instances of | |
| one class, which otherwise claim the same id, and when | |
| installing a bare ``PyCapsule``: a capsule has no class to take | |
| an id from, so it is given a random ``anon:`` id that differs on | |
| every install, and plans it encodes can never be decoded | |
| elsewhere. | |
| Returns: | |
| A new context carrying this codec in addition to any already | |
| installed. | |
| Raises: | |
| ValueError: If the resolved id is already installed on this session. | |
| Examples: | |
| A context exports its own codec, which stands in here for a real | |
| library's: | |
| >>> from datafusion import SessionContext | |
| >>> host = SessionContext() | |
| >>> capsule = host.__datafusion_logical_extension_codec__() | |
| >>> ctx = SessionContext().with_logical_extension_codec( | |
| ... capsule, codec_id="my_library.Codec" | |
| ... ) | |
| >>> ctx.logical_extension_codec_ids() | |
| ['my_library.Codec'] | |
| Without ``codec_id`` a bare capsule gets an anonymous id, which is | |
| fine only if its plans never leave this session: | |
| >>> ctx = SessionContext().with_logical_extension_codec(capsule) | |
| >>> ctx.logical_extension_codec_ids()[0].startswith("anon:") | |
| True | |
| """ | |
| 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. | |
| Returns: | |
| The installed codec ids, in install order. Empty if none are | |
| installed. | |
| Examples: | |
| >>> from datafusion import SessionContext | |
| >>> host = SessionContext() | |
| >>> host.logical_extension_codec_ids() | |
| [] | |
| >>> capsule = host.__datafusion_logical_extension_codec__() | |
| >>> ctx = SessionContext().with_logical_extension_codec( | |
| ... capsule, codec_id="my_library.Codec" | |
| ... ) | |
| >>> ctx.logical_extension_codec_ids() | |
| ['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``. | |
| Args: | |
| session: Accepted and ignored. | |
| Returns: | |
| A ``datafusion_physical_extension_codec`` PyCapsule. | |
| Examples: | |
| >>> from datafusion import SessionContext | |
| >>> ctx = SessionContext() | |
| >>> type(ctx.__datafusion_physical_extension_codec__()).__name__ | |
| 'PyCapsule' | |
| """ | |
| 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`. | |
| Returns: | |
| The installed codec ids, in install order. Empty if none are | |
| installed. | |
| Examples: | |
| >>> from datafusion import SessionContext | |
| >>> host = SessionContext() | |
| >>> host.physical_extension_codec_ids() | |
| [] | |
| >>> capsule = host.__datafusion_physical_extension_codec__() | |
| >>> ctx = SessionContext().with_physical_extension_codec( | |
| ... capsule, codec_id="my_library.PhysicalCodec" | |
| ... ) | |
| >>> ctx.physical_extension_codec_ids() | |
| ['my_library.PhysicalCodec'] | |
| """ | |
| 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. | |
| Args: | |
| codec: As :py:meth:`with_logical_extension_codec`, for the physical | |
| getter. | |
| codec_id: As :py:meth:`with_logical_extension_codec`. | |
| Returns: | |
| A new context carrying this codec in addition to any already | |
| installed. | |
| Raises: | |
| ValueError: If the resolved id is already installed on this session. | |
| Examples: | |
| >>> from datafusion import SessionContext | |
| >>> host = SessionContext() | |
| >>> capsule = host.__datafusion_physical_extension_codec__() | |
| >>> ctx = SessionContext().with_physical_extension_codec( | |
| ... capsule, codec_id="my_library.PhysicalCodec" | |
| ... ) | |
| >>> ctx.physical_extension_codec_ids() | |
| ['my_library.PhysicalCodec'] | |
| """ | |
| 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. | |
| 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. | |
| Shares its session with this context — see | |
| :py:class:`SessionContext`. The original context's own codec | |
| settings are unchanged. | |
| Args: | |
| enabled: Whether to embed Python UDFs in serialized | |
| expressions. Keyword-only and required, so callers must | |
| pick a mode explicitly. Fresh sessions behave as | |
| ``enabled=True`` until this method overrides the toggle. | |
| Returns: | |
| A new :class:`SessionContext` with the toggle applied. | |
| 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 |