From 7f276516a644cde908cf74795251746d482d1829 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Wed, 8 May 2024 17:07:20 -0600 Subject: [PATCH 001/181] Generate changelog (#673) --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a05429f4..863b142dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,33 @@ # DataFusion Python Changelog +## [37.1.0](https://github.com/apache/arrow-datafusion-python/tree/37.1.0) (2024-05-08) + +**Implemented enhancements:** + +- feat: add execute_stream and execute_stream_partitioned [#610](https://github.com/apache/arrow-datafusion-python/pull/610) (mesejo) + +**Documentation updates:** + +- docs: update docs CI to install python-311 requirements [#661](https://github.com/apache/arrow-datafusion-python/pull/661) (Michael-J-Ward) + +**Merged pull requests:** + +- Switch to Ruff for Python linting [#529](https://github.com/apache/arrow-datafusion-python/pull/529) (andygrove) +- Remove sql-on-pandas/polars/cudf examples [#602](https://github.com/apache/arrow-datafusion-python/pull/602) (andygrove) +- build(deps): bump object_store from 0.9.0 to 0.9.1 [#611](https://github.com/apache/arrow-datafusion-python/pull/611) (dependabot[bot]) +- More missing array funcs [#605](https://github.com/apache/arrow-datafusion-python/pull/605) (judahrand) +- feat: add execute_stream and execute_stream_partitioned [#610](https://github.com/apache/arrow-datafusion-python/pull/610) (mesejo) +- build(deps): bump uuid from 1.7.0 to 1.8.0 [#615](https://github.com/apache/arrow-datafusion-python/pull/615) (dependabot[bot]) +- Bind SQLOptions and relative ctx method #567 [#588](https://github.com/apache/arrow-datafusion-python/pull/588) (giacomorebecchi) +- bugfix: no panic on empty table [#613](https://github.com/apache/arrow-datafusion-python/pull/613) (mesejo) +- Expose `register_listing_table` [#618](https://github.com/apache/arrow-datafusion-python/pull/618) (henrifroese) +- Expose unnest feature [#641](https://github.com/apache/arrow-datafusion-python/pull/641) (timsaucer) +- Update domain names and paths in asf yaml [#643](https://github.com/apache/arrow-datafusion-python/pull/643) (andygrove) +- use python 3.11 to publish docs [#645](https://github.com/apache/arrow-datafusion-python/pull/645) (andygrove) +- docs: update docs CI to install python-311 requirements [#661](https://github.com/apache/arrow-datafusion-python/pull/661) (Michael-J-Ward) +- Upgrade Datafusion to v37.1.0 [#669](https://github.com/apache/arrow-datafusion-python/pull/669) (Michael-J-Ward) + ## [36.0.0](https://github.com/apache/arrow-datafusion-python/tree/36.0.0) (2024-03-02) **Implemented enhancements:** diff --git a/Cargo.toml b/Cargo.toml index f04b23d3e..9da36d710 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ version = "37.1.0" homepage = "https://datafusion.apache.org/python" repository = "https://github.com/apache/datafusion-python" authors = ["Apache DataFusion "] -description = "Apache Arrow DataFusion DataFrame and SQL Query Engine" +description = "Apache DataFusion DataFrame and SQL Query Engine" readme = "README.md" license = "Apache-2.0" edition = "2021" From 7fd0c96b6f59c750e7dd59f92beed7d57d371f6a Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Thu, 9 May 2024 12:01:06 -0400 Subject: [PATCH 002/181] Add document about basics of working with expressions (#668) --- .../common-operations/expressions.rst | 94 +++++++++++++++++++ .../user-guide/common-operations/index.rst | 1 + 2 files changed, 95 insertions(+) create mode 100644 docs/source/user-guide/common-operations/expressions.rst diff --git a/docs/source/user-guide/common-operations/expressions.rst b/docs/source/user-guide/common-operations/expressions.rst new file mode 100644 index 000000000..ebb514f14 --- /dev/null +++ b/docs/source/user-guide/common-operations/expressions.rst @@ -0,0 +1,94 @@ +.. 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. + +Expressions +=========== + +In DataFusion an expression is an abstraction that represents a computation. +Expressions are used as the primary inputs and ouputs for most functions within +DataFusion. As such, expressions can be combined to create expression trees, a +concept shared across most compilers and databases. + +Column +------ + +The first expression most new users will interact with is the Column, which is created by calling :func:`col`. +This expression represents a column within a DataFrame. The function :func:`col` takes as in input a string +and returns an expression as it's output. + +Literal +------- + +Literal expressions represent a single value. These are helpful in a wide range of operations where +a specific, known value is of interest. You can create a literal expression using the function :func:`lit`. +The type of the object passed to the :func:`lit` function will be used to convert it to a known data type. + +In the following example we create expressions for the column named `color` and the literal scalar string `red`. +The resultant variable `red_units` is itself also an expression. + +.. ipython:: python + + red_units = col("color") == lit("red") + +Boolean +------- + +When combining expressions that evaluate to a boolean value, you can combine these expressions using boolean operators. +It is important to note that in order to combine these expressions, you *must* use bitwise operators. See the following +examples for the and, or, and not operations. + + +.. ipython:: python + + red_or_green_units = (col("color") == lit("red")) | (col("color") == lit("green")) + heavy_red_units = (col("color") == lit("red")) & (col("weight") > lit(42)) + not_red_units = ~(col("color") == lit("red")) + +Functions +--------- + +As mentioned before, most functions in DataFusion return an expression at their output. This allows us to create +a wide variety of expressions built up from other expressions. For example, :func:`.alias` is a function that takes +as it input a single expression and returns an expression in which the name of the expression has changed. + +The following example shows a series of expressions that are built up from functions operating on expressions. + +.. ipython:: python + + from datafusion import SessionContext + from datafusion import column, lit + from datafusion import functions as f + import random + + ctx = SessionContext() + df = ctx.from_pydict( + { + "name": ["Albert", "Becca", "Carlos", "Dante"], + "age": [42, 67, 27, 71], + "years_in_position": [13, 21, 10, 54], + }, + name="employees" + ) + + age_col = col("age") + renamed_age = age_col.alias("age_in_years") + start_age = age_col - col("years_in_position") + started_young = start_age < lit(18) + can_retire = age_col > lit(65) + long_timer = started_young & can_retire + + df.filter(long_timer).select(col("name"), renamed_age, col("years_in_position")) diff --git a/docs/source/user-guide/common-operations/index.rst b/docs/source/user-guide/common-operations/index.rst index 950afb93e..b15b04c62 100644 --- a/docs/source/user-guide/common-operations/index.rst +++ b/docs/source/user-guide/common-operations/index.rst @@ -23,6 +23,7 @@ Common Operations basic-info select-and-filter + expressions joins functions aggregations From c6bdc9fecc4e8e28b055af28887f8cd7c19378ce Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Thu, 9 May 2024 12:17:39 -0600 Subject: [PATCH 003/181] chore: Update Python release process now that DataFusion is TLP (#674) --- dev/release/README.md | 59 +++---------- dev/release/create-tarball.sh | 34 ++++---- dev/release/generate-changelog.py | 2 +- dev/release/release-tarball.sh | 12 +-- .../update_change_log-datafusion-python.sh | 33 ------- dev/release/update_change_log.sh | 87 ------------------- dev/release/verify-release-candidate.sh | 28 +++--- 7 files changed, 50 insertions(+), 205 deletions(-) delete mode 100755 dev/release/update_change_log-datafusion-python.sh delete mode 100755 dev/release/update_change_log.sh diff --git a/dev/release/README.md b/dev/release/README.md index 6bd2c1eb2..c4372c832 100644 --- a/dev/release/README.md +++ b/dev/release/README.md @@ -63,7 +63,7 @@ We maintain a `CHANGELOG.md` so our users know what has been changed between rel The changelog is generated using a Python script: ```bash -$ GITHUB_TOKEN= ./dev/release/generate-changelog.py apache/arrow-datafusion-python 24.0.0 HEAD > dev/changelog/25.0.0.md +$ GITHUB_TOKEN= ./dev/release/generate-changelog.py apache/datafusion-python 24.0.0 HEAD > dev/changelog/25.0.0.md ``` This script creates a changelog from GitHub PRs based on the labels associated with them as well as looking for @@ -83,9 +83,9 @@ This process is not fully automated, so there are some additional manual steps: - Add the following content (copy from the previous version's changelog and update as appropriate: ``` -## [24.0.0](https://github.com/apache/arrow-datafusion-python/tree/24.0.0) (2023-05-06) +## [24.0.0](https://github.com/apache/datafusion-python/tree/24.0.0) (2023-05-06) -[Full Changelog](https://github.com/apache/arrow-datafusion-python/compare/23.0.0...24.0.0) +[Full Changelog](https://github.com/apache/datafusion-python/compare/23.0.0...24.0.0) ``` ### Preparing a Release Candidate @@ -103,42 +103,7 @@ git push apache 0.8.0-rc1 ./dev/release/create-tarball.sh 0.8.0 1 ``` -This will also create the email template to send to the mailing list. Here is an example: - -``` -To: dev@arrow.apache.org -Subject: [VOTE][RUST][DataFusion] Release DataFusion Python Bindings 0.7.0 RC2 -Hi, - -I would like to propose a release of Apache Arrow DataFusion Python Bindings, -version 0.7.0. - -This release candidate is based on commit: bd1b78b6d444b7ab172c6aec23fa58c842a592d7 [1] -The proposed release tarball and signatures are hosted at [2]. -The changelog is located at [3]. -The Python wheels are located at [4]. - -Please download, verify checksums and signatures, run the unit tests, and vote -on the release. The vote will be open for at least 72 hours. - -Only votes from PMC members are binding, but all members of the community are -encouraged to test the release and vote with "(non-binding)". - -The standard verification procedure is documented at https://github.com/apache/arrow-datafusion-python/blob/main/dev/release/README.md#verifying-release-candidates. - -[ ] +1 Release this as Apache Arrow DataFusion Python 0.7.0 -[ ] +0 -[ ] -1 Do not release this as Apache Arrow DataFusion Python 0.7.0 because... - -Here is my vote: - -+1 - -[1]: https://github.com/apache/arrow-datafusion-python/tree/bd1b78b6d444b7ab172c6aec23fa58c842a592d7 -[2]: https://dist.apache.org/repos/dist/dev/arrow/apache-arrow-datafusion-python-0.7.0-rc2 -[3]: https://github.com/apache/arrow-datafusion-python/blob/bd1b78b6d444b7ab172c6aec23fa58c842a592d7/CHANGELOG.md -[4]: https://test.pypi.org/project/datafusion/0.7.0/ -``` +This will also create the email template to send to the mailing list. Create a draft email using this content, but do not send until after completing the next step. @@ -151,7 +116,7 @@ This section assumes some familiarity with publishing Python packages to PyPi. F Pushing an `rc` tag to the release branch will cause a GitHub Workflow to run that will build the Python wheels. -Go to https://github.com/apache/arrow-datafusion-python/actions and look for an action named "Python Release Build" +Go to https://github.com/apache/datafusion-python/actions and look for an action named "Python Release Build" that has run against the pushed tag. Click on the action and scroll down to the bottom of the page titled "Artifacts". Download `dist.zip`. It should @@ -266,10 +231,10 @@ git push apache 0.8.0 ### Add the release to Apache Reporter -Add the release to https://reporter.apache.org/addrelease.html?arrow with a version name prefixed with `RS-DATAFUSION-PYTHON`, -for example `RS-DATAFUSION-PYTHON-31.0.0`. +Add the release to https://reporter.apache.org/addrelease.html?datafusion with a version name prefixed with `DATAFUSION-PYTHON`, +for example `DATAFUSION-PYTHON-31.0.0`. -The release information is used to generate a template for a board report (see example +The release information is used to generate a template for a board report (see example from Apache Arrow [here](https://github.com/apache/arrow/pull/14357)). ### Delete old RCs and Releases @@ -284,13 +249,13 @@ Release candidates should be deleted once the release is published. Get a list of DataFusion release candidates: ```bash -svn ls https://dist.apache.org/repos/dist/dev/arrow | grep datafusion-python +svn ls https://dist.apache.org/repos/dist/dev/datafusion | grep datafusion-python ``` Delete a release candidate: ```bash -svn delete -m "delete old DataFusion RC" https://dist.apache.org/repos/dist/dev/arrow/apache-arrow-datafusion-python-7.1.0-rc1/ +svn delete -m "delete old DataFusion RC" https://dist.apache.org/repos/dist/dev/datafusion/apache-datafusion-python-7.1.0-rc1/ ``` #### Deleting old releases from `release` svn @@ -300,11 +265,11 @@ Only the latest release should be available. Delete old releases after publishin Get a list of DataFusion releases: ```bash -svn ls https://dist.apache.org/repos/dist/release/arrow | grep datafusion-python +svn ls https://dist.apache.org/repos/dist/release/datafusion | grep datafusion-python ``` Delete a release: ```bash -svn delete -m "delete old DataFusion release" https://dist.apache.org/repos/dist/release/arrow/arrow-datafusion-python-7.0.0 +svn delete -m "delete old DataFusion release" https://dist.apache.org/repos/dist/release/datafusion/datafusion-python-7.0.0 ``` diff --git a/dev/release/create-tarball.sh b/dev/release/create-tarball.sh index c05da5b75..d6ca76561 100755 --- a/dev/release/create-tarball.sh +++ b/dev/release/create-tarball.sh @@ -21,9 +21,9 @@ # Adapted from https://github.com/apache/arrow-rs/tree/master/dev/release/create-tarball.sh # This script creates a signed tarball in -# dev/dist/apache-arrow-datafusion-python--.tar.gz and uploads it to +# dev/dist/apache-datafusion-python--.tar.gz and uploads it to # the "dev" area of the dist.apache.arrow repository and prepares an -# email for sending to the dev@arrow.apache.org list for a formal +# email for sending to the dev@datafusion.apache.org list for a formal # vote. # # See release/README.md for full release instructions @@ -65,25 +65,25 @@ tag="${version}-rc${rc}" echo "Attempting to create ${tarball} from tag ${tag}" release_hash=$(cd "${SOURCE_TOP_DIR}" && git rev-list --max-count=1 ${tag}) -release=apache-arrow-datafusion-python-${version} +release=apache-datafusion-python-${version} distdir=${SOURCE_TOP_DIR}/dev/dist/${release}-rc${rc} tarname=${release}.tar.gz tarball=${distdir}/${tarname} -url="https://dist.apache.org/repos/dist/dev/arrow/${release}-rc${rc}" +url="https://dist.apache.org/repos/dist/dev/datafusion/${release}-rc${rc}" if [ -z "$release_hash" ]; then echo "Cannot continue: unknown git tag: ${tag}" fi -echo "Draft email for dev@arrow.apache.org mailing list" +echo "Draft email for dev@datafusion.apache.org mailing list" echo "" echo "---------------------------------------------------------" cat < ${tarball}.sha256 (cd ${distdir} && shasum -a 512 ${tarname}) > ${tarball}.sha512 -echo "Uploading to apache dist/dev to ${url}" -svn co --depth=empty https://dist.apache.org/repos/dist/dev/arrow ${SOURCE_TOP_DIR}/dev/dist +echo "Uploading to datafusion dist/dev to ${url}" +svn co --depth=empty https://dist.apache.org/repos/dist/dev/datafusion ${SOURCE_TOP_DIR}/dev/dist svn add ${distdir} -svn ci -m "Apache Arrow DataFusion Python ${version} ${rc}" ${distdir} +svn ci -m "Apache DataFusion Python ${version} ${rc}" ${distdir} diff --git a/dev/release/generate-changelog.py b/dev/release/generate-changelog.py index 01d640669..af097ce98 100755 --- a/dev/release/generate-changelog.py +++ b/dev/release/generate-changelog.py @@ -102,7 +102,7 @@ def cli(args=None): parser = argparse.ArgumentParser() parser.add_argument( - "project", help="The project name e.g. apache/arrow-datafusion-python" + "project", help="The project name e.g. apache/datafusion-python" ) parser.add_argument("tag1", help="The previous release tag") parser.add_argument("tag2", help="The current release tag") diff --git a/dev/release/release-tarball.sh b/dev/release/release-tarball.sh index f5e8eb1bf..8c305a676 100755 --- a/dev/release/release-tarball.sh +++ b/dev/release/release-tarball.sh @@ -43,7 +43,7 @@ fi version=$1 rc=$2 -tmp_dir=tmp-apache-arrow-datafusion-python-dist +tmp_dir=tmp-apache-datafusion-python-dist echo "Recreate temporary directory: ${tmp_dir}" rm -rf ${tmp_dir} @@ -52,23 +52,23 @@ mkdir -p ${tmp_dir} echo "Clone dev dist repository" svn \ co \ - https://dist.apache.org/repos/dist/dev/arrow/apache-arrow-datafusion-python-${version}-rc${rc} \ + https://dist.apache.org/repos/dist/dev/datafusion/apache-datafusion-python-${version}-rc${rc} \ ${tmp_dir}/dev echo "Clone release dist repository" -svn co https://dist.apache.org/repos/dist/release/arrow ${tmp_dir}/release +svn co https://dist.apache.org/repos/dist/release/datafusion ${tmp_dir}/release echo "Copy ${version}-rc${rc} to release working copy" -release_version=arrow-datafusion-python-${version} +release_version=datafusion-python-${version} mkdir -p ${tmp_dir}/release/${release_version} cp -r ${tmp_dir}/dev/* ${tmp_dir}/release/${release_version}/ svn add ${tmp_dir}/release/${release_version} echo "Commit release" -svn ci -m "Apache Arrow DataFusion Python ${version}" ${tmp_dir}/release +svn ci -m "Apache DataFusion Python ${version}" ${tmp_dir}/release echo "Clean up" rm -rf ${tmp_dir} echo "Success! The release is available here:" -echo " https://dist.apache.org/repos/dist/release/arrow/${release_version}" +echo " https://dist.apache.org/repos/dist/release/datafusion/${release_version}" diff --git a/dev/release/update_change_log-datafusion-python.sh b/dev/release/update_change_log-datafusion-python.sh deleted file mode 100755 index a11447f0b..000000000 --- a/dev/release/update_change_log-datafusion-python.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# -# 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. -# - -# Usage: -# CHANGELOG_GITHUB_TOKEN= ./update_change_log-datafusion.sh main 8.0.0 7.1.0 -# CHANGELOG_GITHUB_TOKEN= ./update_change_log-datafusion.sh maint-7.x 7.1.0 7.0.0 - -RELEASE_BRANCH=$1 -RELEASE_TAG=$2 -BASE_TAG=$3 - -SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -${SOURCE_DIR}/update_change_log.sh \ - "${BASE_TAG}" \ - --future-release "${RELEASE_TAG}" \ - --release-branch "${RELEASE_BRANCH}" diff --git a/dev/release/update_change_log.sh b/dev/release/update_change_log.sh deleted file mode 100755 index a0b398131..000000000 --- a/dev/release/update_change_log.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -# -# 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. -# - -# Adapted from https://github.com/apache/arrow-rs/tree/master/dev/release/update_change_log.sh - -# invokes the changelog generator from -# https://github.com/github-changelog-generator/github-changelog-generator -# -# With the config located in -# arrow-datafusion/.github_changelog_generator -# -# Usage: -# CHANGELOG_GITHUB_TOKEN= ./update_change_log.sh - -set -e - -SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SOURCE_TOP_DIR="$(cd "${SOURCE_DIR}/../../" && pwd)" - -if [[ "$#" -lt 1 ]]; then - echo "USAGE: $0 SINCE_TAG EXTRA_ARGS..." - exit 1 -fi - -SINCE_TAG=$1 -shift 1 - -OUTPUT_PATH="CHANGELOG.md" - -pushd ${SOURCE_TOP_DIR} - -# reset content in changelog -git checkout "${SINCE_TAG}" "${OUTPUT_PATH}" -# remove license header so github-changelog-generator has a clean base to append -sed -i.bak '1,18d' "${OUTPUT_PATH}" - -docker run -it --rm \ - --cpus "0.1" \ - -e CHANGELOG_GITHUB_TOKEN=$CHANGELOG_GITHUB_TOKEN \ - -v "$(pwd)":/usr/local/src/your-app \ - githubchangeloggenerator/github-changelog-generator \ - --user apache \ - --project arrow-datafusion-python \ - --since-tag "${SINCE_TAG}" \ - --base "${OUTPUT_PATH}" \ - --output "${OUTPUT_PATH}" \ - "$@" - -sed -i.bak "s/\\\n/\n\n/" "${OUTPUT_PATH}" - -echo ' -' | cat - "${OUTPUT_PATH}" > "${OUTPUT_PATH}".tmp -mv "${OUTPUT_PATH}".tmp "${OUTPUT_PATH}" diff --git a/dev/release/verify-release-candidate.sh b/dev/release/verify-release-candidate.sh index be86f69e0..14c0baee8 100755 --- a/dev/release/verify-release-candidate.sh +++ b/dev/release/verify-release-candidate.sh @@ -32,8 +32,8 @@ set -x set -o pipefail SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" -ARROW_DIR="$(dirname $(dirname ${SOURCE_DIR}))" -ARROW_DIST_URL='https://dist.apache.org/repos/dist/dev/arrow' +DATAFUSION_PYTHON_DIR="$(dirname $(dirname ${SOURCE_DIR}))" +DATAFUSION_PYTHON_DIST_URL='https://dist.apache.org/repos/dist/dev/datafusion' download_dist_file() { curl \ @@ -41,11 +41,11 @@ download_dist_file() { --show-error \ --fail \ --location \ - --remote-name $ARROW_DIST_URL/$1 + --remote-name $DATAFUSION_PYTHON_DIST_URL/$1 } download_rc_file() { - download_dist_file apache-arrow-datafusion-python-${VERSION}-rc${RC_NUMBER}/$1 + download_dist_file apache-datafusion-python-${VERSION}-rc${RC_NUMBER}/$1 } import_gpg_keys() { @@ -89,19 +89,19 @@ verify_dir_artifact_signatures() { setup_tempdir() { cleanup() { if [ "${TEST_SUCCESS}" = "yes" ]; then - rm -fr "${ARROW_TMPDIR}" + rm -fr "${DATAFUSION_PYTHON_TMPDIR}" else - echo "Failed to verify release candidate. See ${ARROW_TMPDIR} for details." + echo "Failed to verify release candidate. See ${DATAFUSION_PYTHON_TMPDIR} for details." fi } - if [ -z "${ARROW_TMPDIR}" ]; then - # clean up automatically if ARROW_TMPDIR is not defined - ARROW_TMPDIR=$(mktemp -d -t "$1.XXXXX") + if [ -z "${DATAFUSION_PYTHON_TMPDIR}" ]; then + # clean up automatically if DATAFUSION_PYTHON_TMPDIR is not defined + DATAFUSION_PYTHON_TMPDIR=$(mktemp -d -t "$1.XXXXX") trap cleanup EXIT else # don't clean up automatically - mkdir -p "${ARROW_TMPDIR}" + mkdir -p "${DATAFUSION_PYTHON_TMPDIR}" fi } @@ -142,11 +142,11 @@ test_source_distribution() { TEST_SUCCESS=no -setup_tempdir "arrow-${VERSION}" -echo "Working in sandbox ${ARROW_TMPDIR}" -cd ${ARROW_TMPDIR} +setup_tempdir "datafusion-python-${VERSION}" +echo "Working in sandbox ${DATAFUSION_PYTHON_TMPDIR}" +cd ${DATAFUSION_PYTHON_TMPDIR} -dist_name="apache-arrow-datafusion-python-${VERSION}" +dist_name="apache-datafusion-python-${VERSION}" import_gpg_keys fetch_archive ${dist_name} tar xf ${dist_name}.tar.gz From 7d4a40cc73c37bcf41ccd3c6480627b03c9f93cc Mon Sep 17 00:00:00 2001 From: Michael J Ward Date: Sat, 11 May 2024 13:32:01 -0500 Subject: [PATCH 004/181] Fix Docs (#676) --- .github/workflows/docs.yaml | 6 ++++++ docs/source/api/object_store.rst | 2 +- docs/source/user-guide/common-operations/functions.rst | 3 +-- docs/source/user-guide/configuration.rst | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 1193d7810..855164852 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -4,6 +4,9 @@ on: - main tags-ignore: - "**-rc**" + pull_request: + branches: + - main name: Deploy DataFusion Python site @@ -13,6 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Set target branch + if: github.event_name == 'push' && github.ref == 'refs/heads/main' id: target-branch run: | set -x @@ -27,6 +31,7 @@ jobs: - name: Checkout docs sources uses: actions/checkout@v3 - name: Checkout docs target branch + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/checkout@v3 with: fetch-depth: 0 @@ -64,6 +69,7 @@ jobs: make html - name: Copy & push the generated HTML + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | set -x cd docs-target diff --git a/docs/source/api/object_store.rst b/docs/source/api/object_store.rst index eeb6c4326..8d78f0724 100644 --- a/docs/source/api/object_store.rst +++ b/docs/source/api/object_store.rst @@ -19,7 +19,7 @@ .. currentmodule:: datafusion.object_store ObjectStore -========= +=========== .. autosummary:: :toctree: ../generated/ diff --git a/docs/source/user-guide/common-operations/functions.rst b/docs/source/user-guide/common-operations/functions.rst index 7e5c592d8..50b493098 100644 --- a/docs/source/user-guide/common-operations/functions.rst +++ b/docs/source/user-guide/common-operations/functions.rst @@ -92,13 +92,12 @@ DataFusion offers a range of helpful options. f.left(col('"Name"'), literal(4)).alias("code") ) -This also includes the functions for regular expressions :func:`.regexp_replace` and :func:`.regexp_match` +This also includes the functions for regular expressions like :func:`.regexp_match` .. ipython:: python df.select( f.regexp_match(col('"Name"'), literal("Char")).alias("dragons"), - f.regexp_replace(col('"Name"'), literal("saur"), literal("fleur")).alias("flowers") ) diff --git a/docs/source/user-guide/configuration.rst b/docs/source/user-guide/configuration.rst index 63b0dc3e2..0c1a4818a 100644 --- a/docs/source/user-guide/configuration.rst +++ b/docs/source/user-guide/configuration.rst @@ -16,7 +16,7 @@ .. under the License. Configuration -======== +============= Let's look at how we can configure DataFusion. When creating a :code:`SessionContext`, you can pass in a :code:`SessionConfig` and :code:`RuntimeConfig` object. These two cover a wide range of options. @@ -48,4 +48,4 @@ a :code:`SessionConfig` and :code:`RuntimeConfig` object. These two cover a wide You can read more about available :code:`SessionConfig` options `here `_, -and about :code:`RuntimeConfig` options `here https://docs.rs/datafusion/latest/datafusion/execution/runtime_env/struct.RuntimeConfig.html`_. +and about :code:`RuntimeConfig` options `here `_. From d71c436ae2006843dc720bfdfcb8b3aeb434815e Mon Sep 17 00:00:00 2001 From: Tim Saucer Date: Mon, 13 May 2024 10:00:18 -0400 Subject: [PATCH 005/181] Add examples from TPC-H (#666) * Update location of docker image * Initial commit for queries 1-3 * Commit queries 4-7 of TPC-H in examples * Add required license text * Add additional text around why to use a case statement in the example * add market share example * Add example for product type profit measure * Inital commit returned item report * Linting * Initial commit of q11 example * Initial commit of q12 from tpc-h * Initial commit for customer distribution example * Initial commit of promotion effect example * Initial commit of q15 in tph-c, top supplier * Initial commit of q16 in tph-c, part supplier relationship * Initial commit of q17 in tph-c, small quatity order * Initial commit of q18 in tph-c, large volume customer * Initial commit of q19 in tph-c, discounted revenue * Initial commit of q20 in tph-c, potential part promotion * Initial commit of q21 in tph-c, supplier who kept order waiting * Initial commit of q22 in tph-c, global sales opportunity * Adding readme information and marking text as copyrighted * Minimum part cost must be identified per part not across all parts that match the filters * Change ordering of output rows to match spec * Set parameter to match spec * Set parameter to match spec * setting values to match spec * Linting * Expand on readme to link to examples within tpch folder * Minor typo --- benchmarks/tpch/tpch-gen.sh | 6 +- examples/README.md | 64 +++++++ examples/tpch/.gitignore | 2 + examples/tpch/README.md | 57 ++++++ examples/tpch/convert_data_to_parquet.py | 142 ++++++++++++++ examples/tpch/q01_pricing_summary_report.py | 90 +++++++++ examples/tpch/q02_minimum_cost_supplier.py | 139 ++++++++++++++ examples/tpch/q03_shipping_priority.py | 86 +++++++++ examples/tpch/q04_order_priority_checking.py | 80 ++++++++ examples/tpch/q05_local_supplier_volume.py | 102 ++++++++++ .../tpch/q06_forecasting_revenue_change.py | 87 +++++++++ examples/tpch/q07_volume_shipping.py | 123 ++++++++++++ examples/tpch/q08_market_share.py | 175 ++++++++++++++++++ .../tpch/q09_product_type_profit_measure.py | 93 ++++++++++ examples/tpch/q10_returned_item_reporting.py | 108 +++++++++++ .../q11_important_stock_identification.py | 82 ++++++++ examples/tpch/q12_ship_mode_order_priority.py | 112 +++++++++++ examples/tpch/q13_customer_distribution.py | 64 +++++++ examples/tpch/q14_promotion_effect.py | 81 ++++++++ examples/tpch/q15_top_supplier.py | 87 +++++++++ .../tpch/q16_part_supplier_relationship.py | 85 +++++++++ examples/tpch/q17_small_quantity_order.py | 69 +++++++ examples/tpch/q18_large_volume_customer.py | 65 +++++++ examples/tpch/q19_discounted_revenue.py | 137 ++++++++++++++ examples/tpch/q20_potential_part_promotion.py | 97 ++++++++++ .../tpch/q21_suppliers_kept_orders_waiting.py | 114 ++++++++++++ examples/tpch/q22_global_sales_opportunity.py | 76 ++++++++ 27 files changed, 2420 insertions(+), 3 deletions(-) create mode 100644 examples/tpch/.gitignore create mode 100644 examples/tpch/README.md create mode 100644 examples/tpch/convert_data_to_parquet.py create mode 100644 examples/tpch/q01_pricing_summary_report.py create mode 100644 examples/tpch/q02_minimum_cost_supplier.py create mode 100644 examples/tpch/q03_shipping_priority.py create mode 100644 examples/tpch/q04_order_priority_checking.py create mode 100644 examples/tpch/q05_local_supplier_volume.py create mode 100644 examples/tpch/q06_forecasting_revenue_change.py create mode 100644 examples/tpch/q07_volume_shipping.py create mode 100644 examples/tpch/q08_market_share.py create mode 100644 examples/tpch/q09_product_type_profit_measure.py create mode 100644 examples/tpch/q10_returned_item_reporting.py create mode 100644 examples/tpch/q11_important_stock_identification.py create mode 100644 examples/tpch/q12_ship_mode_order_priority.py create mode 100644 examples/tpch/q13_customer_distribution.py create mode 100644 examples/tpch/q14_promotion_effect.py create mode 100644 examples/tpch/q15_top_supplier.py create mode 100644 examples/tpch/q16_part_supplier_relationship.py create mode 100644 examples/tpch/q17_small_quantity_order.py create mode 100644 examples/tpch/q18_large_volume_customer.py create mode 100644 examples/tpch/q19_discounted_revenue.py create mode 100644 examples/tpch/q20_potential_part_promotion.py create mode 100644 examples/tpch/q21_suppliers_kept_orders_waiting.py create mode 100644 examples/tpch/q22_global_sales_opportunity.py diff --git a/benchmarks/tpch/tpch-gen.sh b/benchmarks/tpch/tpch-gen.sh index e27472a3d..15cab12a5 100755 --- a/benchmarks/tpch/tpch-gen.sh +++ b/benchmarks/tpch/tpch-gen.sh @@ -29,7 +29,7 @@ FILE=./data/supplier.tbl if test -f "$FILE"; then echo "$FILE exists." else - docker run -v `pwd`/data:/data -it --rm ghcr.io/databloom-ai/tpch-docker:main -vf -s $1 + docker run -v `pwd`/data:/data -it --rm ghcr.io/scalytics/tpch-docker:main -vf -s $1 # workaround for https://github.com/apache/arrow-datafusion/issues/6147 mv data/customer.tbl data/customer.csv @@ -49,5 +49,5 @@ FILE=./data/answers/q1.out if test -f "$FILE"; then echo "$FILE exists." else - docker run -v `pwd`/data:/data -it --entrypoint /bin/bash --rm ghcr.io/databloom-ai/tpch-docker:main -c "cp /opt/tpch/2.18.0_rc2/dbgen/answers/* /data/answers/" -fi \ No newline at end of file + docker run -v `pwd`/data:/data -it --entrypoint /bin/bash --rm ghcr.io/scalytics/tpch-docker:main -c "cp /opt/tpch/2.18.0_rc2/dbgen/answers/* /data/answers/" +fi diff --git a/examples/README.md b/examples/README.md index 82405955b..0ef194afe 100644 --- a/examples/README.md +++ b/examples/README.md @@ -52,3 +52,67 @@ Here is a direct link to the file used in the examples: - [Executing SQL on Polars](./sql-on-polars.py) - [Executing SQL on Pandas](./sql-on-pandas.py) - [Executing SQL on cuDF](./sql-on-cudf.py) + +## TPC-H Examples + +Within the subdirectory `tpch` there are 22 examples that reproduce queries in +the TPC-H specification. These include realistic data that can be generated at +arbitrary scale and allow the user to see use cases for a variety of data frame +operations. + +In the list below we describe which new operations can be found in the examples. +The queries are designed to be of increasing complexity, so it is recommended to +review them in order. For brevity, the following list does not include operations +found in previous examples. + +- [Convert CSV to Parquet](./tpch/convert_data_to_parquet.py) + - Read from a CSV files where the delimiter is something other than a comma + - Specify schema during CVS reading + - Write to a parquet file +- [Pricing Summary Report](./tpch/q01_pricing_summary_report.py) + - Aggregation computing the maximum value, average, sum, and number of entries + - Filter data by date and interval + - Sorting +- [Minimum Cost Supplier](./tpch/q02_minimum_cost_supplier.py) + - Window operation to find minimum + - Sorting in descending order +- [Shipping Priority](./tpch/q03_shipping_priority.py) +- [Order Priority Checking](./tpch/q04_order_priority_checking.py) + - Aggregating multiple times in one data frame +- [Local Supplier Volume](./tpch/q05_local_supplier_volume.py) +- [Forecasting Revenue Change](./tpch/q06_forecasting_revenue_change.py) + - Using collect and extracting values as a python object +- [Volume Shipping](./tpch/q07_volume_shipping.py) + - Finding multiple distinct and mutually exclusive values within one dataframe + - Using `case` and `when` statements +- [Market Share](./tpch/q08_market_share.py) + - The operations in this query are similar to those in the prior examples, but + it is a more complex example of using filters, joins, and aggregates + - Using left outer joins +- [Product Type Profit Measure](./tpch/q09_product_type_profit_measure.py) + - Extract year from a date +- [Returned Item Reporting](./tpch/q10_returned_item_reporting.py) +- [Important Stock Identification](./tpch/q11_important_stock_identification.py) +- [Shipping Modes and Order](./tpch/q12_ship_mode_order_priority.py) + - Finding non-null values using a boolean operation in a filter + - Case statement with default value +- [Customer Distribution](./tpch/q13_customer_distribution.py) +- [Promotion Effect](./tpch/q14_promotion_effect.py) +- [Top Supplier](./tpch/q15_top_supplier.py) +- [Parts/Supplier Relationship](./tpch/q16_part_supplier_relationship.py) + - Using anti joins + - Using regular expressions (regex) + - Creating arrays of literal values + - Determine if an element exists within an array +- [Small-Quantity-Order Revenue](./tpch/q17_small_quantity_order.py) +- [Large Volume Customer](./tpch/q18_large_volume_customer.py) +- [Discounted Revenue](./tpch/q19_discounted_revenue.py) + - Creating a user defined function (UDF) + - Convert pyarrow Array to python values + - Filtering based on a UDF +- [Potential Part Promotion](./tpch/q20_potential_part_promotion.py) + - Extracting part of a string using substr +- [Suppliers Who Kept Orders Waiting](./tpch/q21_suppliers_kept_orders_waiting.py) + - Using array aggregation + - Determining the size of array elements +- [Global Sales Opportunity](./tpch/q22_global_sales_opportunity.py) diff --git a/examples/tpch/.gitignore b/examples/tpch/.gitignore new file mode 100644 index 000000000..9e67bd47d --- /dev/null +++ b/examples/tpch/.gitignore @@ -0,0 +1,2 @@ +data + diff --git a/examples/tpch/README.md b/examples/tpch/README.md new file mode 100644 index 000000000..7c52c8230 --- /dev/null +++ b/examples/tpch/README.md @@ -0,0 +1,57 @@ + + +# DataFusion Python Examples for TPC-H + +These examples reproduce the problems listed in the Transaction Process Council +TPC-H benchmark. The purpose of these examples is to demonstrate how to use +different aspects of Data Fusion and not necessarily geared towards creating the +most performant queries possible. Within each example is a description of the +problem. For users who are familiar with SQL style commands, you can compare the +approaches in these examples with those listed in the specification. + +- https://www.tpc.org/tpch/ + +The examples provided are based on version 2.18.0 of the TPC-H specification. + +## Data Setup + +To run these examples, you must first generate a dataset. The `dbgen` tool +provided by TPC can create datasets of arbitrary scale. For testing it is +typically sufficient to create a 1 gigabyte dataset. For convenience, this +repository has a script which uses docker to create this dataset. From the +`benchmarks/tpch` directory execute the following script. + +```bash +./tpch-gen.sh 1 +``` + +The examples provided use parquet files for the tables generated by `dbgen`. +A python script is provided to convert the text files from `dbgen` into parquet +files expected by the examples. From the `examples/tpch` directory you can +execute the following command to create the necessary parquet files. + +```bash +python convert_data_to_parquet.py +``` + +## Description of Examples + +For easier access, a description of the techniques demonstrated in each file +is in the README.md file in the `examples` directory. diff --git a/examples/tpch/convert_data_to_parquet.py b/examples/tpch/convert_data_to_parquet.py new file mode 100644 index 000000000..178b7fb39 --- /dev/null +++ b/examples/tpch/convert_data_to_parquet.py @@ -0,0 +1,142 @@ +# 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. + +""" +This is a utility function that will consumer the data generated by dbgen from TPC-H and convert +it into a parquet file with the column names as expected by the TPC-H specification. It assumes +the data generated resides in a path ../../benchmarks/tpch/data relative to the current file, +as will be generated by the script provided in this repository. +""" + +import os +import pyarrow +import datafusion + +ctx = datafusion.SessionContext() + +all_schemas = {} + +all_schemas["customer"] = [ + ("C_CUSTKEY", pyarrow.int32()), + ("C_NAME", pyarrow.string()), + ("C_ADDRESS", pyarrow.string()), + ("C_NATIONKEY", pyarrow.int32()), + ("C_PHONE", pyarrow.string()), + ("C_ACCTBAL", pyarrow.float32()), + ("C_MKTSEGMENT", pyarrow.string()), + ("C_COMMENT", pyarrow.string()), +] + +all_schemas["lineitem"] = [ + ("L_ORDERKEY", pyarrow.int32()), + ("L_PARTKEY", pyarrow.int32()), + ("L_SUPPKEY", pyarrow.int32()), + ("L_LINENUMBER", pyarrow.int32()), + ("L_QUANTITY", pyarrow.float32()), + ("L_EXTENDEDPRICE", pyarrow.float32()), + ("L_DISCOUNT", pyarrow.float32()), + ("L_TAX", pyarrow.float32()), + ("L_RETURNFLAG", pyarrow.string()), + ("L_LINESTATUS", pyarrow.string()), + ("L_SHIPDATE", pyarrow.date32()), + ("L_COMMITDATE", pyarrow.date32()), + ("L_RECEIPTDATE", pyarrow.date32()), + ("L_SHIPINSTRUCT", pyarrow.string()), + ("L_SHIPMODE", pyarrow.string()), + ("L_COMMENT", pyarrow.string()), +] + +all_schemas["nation"] = [ + ("N_NATIONKEY", pyarrow.int32()), + ("N_NAME", pyarrow.string()), + ("N_REGIONKEY", pyarrow.int32()), + ("N_COMMENT", pyarrow.string()), +] + +all_schemas["orders"] = [ + ("O_ORDERKEY", pyarrow.int32()), + ("O_CUSTKEY", pyarrow.int32()), + ("O_ORDERSTATUS", pyarrow.string()), + ("O_TOTALPRICE", pyarrow.float32()), + ("O_ORDERDATE", pyarrow.date32()), + ("O_ORDERPRIORITY", pyarrow.string()), + ("O_CLERK", pyarrow.string()), + ("O_SHIPPRIORITY", pyarrow.int32()), + ("O_COMMENT", pyarrow.string()), +] + +all_schemas["part"] = [ + ("P_PARTKEY", pyarrow.int32()), + ("P_NAME", pyarrow.string()), + ("P_MFGR", pyarrow.string()), + ("P_BRAND", pyarrow.string()), + ("P_TYPE", pyarrow.string()), + ("P_SIZE", pyarrow.int32()), + ("P_CONTAINER", pyarrow.string()), + ("P_RETAILPRICE", pyarrow.float32()), + ("P_COMMENT", pyarrow.string()), +] + +all_schemas["partsupp"] = [ + ("PS_PARTKEY", pyarrow.int32()), + ("PS_SUPPKEY", pyarrow.int32()), + ("PS_AVAILQTY", pyarrow.int32()), + ("PS_SUPPLYCOST", pyarrow.float32()), + ("PS_COMMENT", pyarrow.string()), +] + +all_schemas["region"] = [ + ("r_REGIONKEY", pyarrow.int32()), + ("r_NAME", pyarrow.string()), + ("r_COMMENT", pyarrow.string()), +] + +all_schemas["supplier"] = [ + ("S_SUPPKEY", pyarrow.int32()), + ("S_NAME", pyarrow.string()), + ("S_ADDRESS", pyarrow.string()), + ("S_NATIONKEY", pyarrow.int32()), + ("S_PHONE", pyarrow.string()), + ("S_ACCTBAL", pyarrow.float32()), + ("S_COMMENT", pyarrow.string()), +] + +curr_dir = os.path.dirname(os.path.abspath(__file__)) +for filename, curr_schema in all_schemas.items(): + + # For convenience, go ahead and convert the schema column names to lowercase + curr_schema = [(s[0].lower(), s[1]) for s in curr_schema] + + # Pre-collect the output columns so we can ignore the null field we add + # in to handle the trailing | in the file + output_cols = [r[0] for r in curr_schema] + + # Trailing | requires extra field for in processing + curr_schema.append(("some_null", pyarrow.null())) + + schema = pyarrow.schema(curr_schema) + + source_file = os.path.abspath( + os.path.join(curr_dir, f"../../benchmarks/tpch/data/{filename}.csv") + ) + dest_file = os.path.abspath(os.path.join(curr_dir, f"./data/{filename}.parquet")) + + df = ctx.read_csv(source_file, schema=schema, has_header=False, delimiter="|") + + df = df.select_columns(*output_cols) + + df.write_parquet(dest_file, compression="snappy") diff --git a/examples/tpch/q01_pricing_summary_report.py b/examples/tpch/q01_pricing_summary_report.py new file mode 100644 index 000000000..1aafccab0 --- /dev/null +++ b/examples/tpch/q01_pricing_summary_report.py @@ -0,0 +1,90 @@ +# 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. + +""" +TPC-H Problem Statement Query 1: + +The Pricing Summary Report Query provides a summary pricing report for all lineitems shipped as of +a given date. The date is within 60 - 120 days of the greatest ship date contained in the database. +The query lists totals for extended price, discounted extended price, discounted extended price +plus tax, average quantity, average extended price, and average discount. These aggregates are +grouped by RETURNFLAG and LINESTATUS, and listed in ascending order of RETURNFLAG and LINESTATUS. +A count of the number of lineitems in each group is included. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +ctx = SessionContext() + +df = ctx.read_parquet("data/lineitem.parquet") + +# It may be that the date can be hard coded, based on examples shown. +# This approach will work with any date range in the provided data set. + +greatest_ship_date = df.aggregate( + [], [F.max(col("l_shipdate")).alias("shipdate")] +).collect()[0]["shipdate"][0] + +# From the given problem, this is how close to the last date in the database we +# want to report results for. It should be between 60-120 days before the end. +DAYS_BEFORE_FINAL = 68 + +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval = pa.scalar((0, 0, DAYS_BEFORE_FINAL), type=pa.month_day_nano_interval()) + +print("Final date in database:", greatest_ship_date) + +# Filter data to the dates of interest +df = df.filter(col("l_shipdate") <= lit(greatest_ship_date) - lit(interval)) + +# Aggregate the results + +df = df.aggregate( + [col("l_returnflag"), col("l_linestatus")], + [ + F.sum(col("l_quantity")).alias("sum_qty"), + F.sum(col("l_extendedprice")).alias("sum_base_price"), + F.sum(col("l_extendedprice") * (lit(1.0) - col("l_discount"))).alias( + "sum_disc_price" + ), + F.sum( + col("l_extendedprice") + * (lit(1.0) - col("l_discount")) + * (lit(1.0) + col("l_tax")) + ).alias("sum_charge"), + F.avg(col("l_quantity")).alias("avg_qty"), + F.avg(col("l_extendedprice")).alias("avg_price"), + F.avg(col("l_discount")).alias("avg_disc"), + F.count(col("l_returnflag")).alias( + "count_order" + ), # Counting any column should return same result + ], +) + +# Sort per the expected result + +df = df.sort(col("l_returnflag").sort(), col("l_linestatus").sort()) + +# Note: There appears to be a discrepancy between what is returned here and what is in the generated +# answers file for the case of return flag N and line status O, but I did not investigate further. + +df.show() diff --git a/examples/tpch/q02_minimum_cost_supplier.py b/examples/tpch/q02_minimum_cost_supplier.py new file mode 100644 index 000000000..262e2cf46 --- /dev/null +++ b/examples/tpch/q02_minimum_cost_supplier.py @@ -0,0 +1,139 @@ +# 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. + +""" +TPC-H Problem Statement Query 2: + +The Minimum Cost Supplier Query finds, in a given region, for each part of a certain type and size, +the supplier who can supply it at minimum cost. If several suppliers in that region offer the +desired part type and size at the same (minimum) cost, the query lists the parts from suppliers with +the 100 highest account balances. For each supplier, the query lists the supplier's account balance, +name and nation; the part's number and manufacturer; the supplier's address, phone number and +comment information. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +import datafusion +from datafusion import SessionContext, col, lit, functions as F + +# This is the part we're looking for +SIZE_OF_INTEREST = 15 +TYPE_OF_INTEREST = "BRASS" +REGION_OF_INTEREST = "EUROPE" + +# Load the dataframes we need + +ctx = SessionContext() + +df_part = ctx.read_parquet("data/part.parquet").select_columns( + "p_partkey", "p_mfgr", "p_type", "p_size" +) +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_acctbal", + "s_name", + "s_address", + "s_phone", + "s_comment", + "s_nationkey", + "s_suppkey", +) +df_partsupp = ctx.read_parquet("data/partsupp.parquet").select_columns( + "ps_partkey", "ps_suppkey", "ps_supplycost" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_regionkey", "n_name" +) +df_region = ctx.read_parquet("data/region.parquet").select_columns( + "r_regionkey", "r_name" +) + +# Filter down parts. Part names contain the type of interest, so we can use strpos to find where +# in the p_type column the word is. `strpos` will return 0 if not found, otherwise the position +# in the string where it is located. + +df_part = df_part.filter( + F.strpos(col("p_type"), lit(TYPE_OF_INTEREST)) > lit(0) +).filter(col("p_size") == lit(SIZE_OF_INTEREST)) + +# Filter regions down to the one of interest + +df_region = df_region.filter(col("r_name") == lit(REGION_OF_INTEREST)) + +# Now that we have the region, find suppliers in that region. Suppliers are tied to their nation +# and nations are tied to the region. + +df_nation = df_nation.join(df_region, (["n_regionkey"], ["r_regionkey"]), how="inner") +df_supplier = df_supplier.join( + df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner" +) + +# Now that we know who the potential suppliers are for the part, we can limit out part +# supplies table down. We can further join down to the specific parts we've identified +# as matching the request + +df = df_partsupp.join(df_supplier, (["ps_suppkey"], ["s_suppkey"]), how="inner") + +# Locate the minimum cost across all suppliers. There are multiple ways you could do this, +# but one way is to create a window function across all suppliers, find the minimum, and +# create a column of that value. We can then filter down any rows for which the cost and +# minimum do not match. + +# The default window frame as of 5/6/2024 is from unbounded preceeding to the current row. +# We want to evaluate the entire data frame, so we specify this. +window_frame = datafusion.WindowFrame("rows", None, None) +df = df.with_column( + "min_cost", + F.window( + "min", + [col("ps_supplycost")], + partition_by=[col("ps_partkey")], + window_frame=window_frame, + ), +) + +df = df.filter(col("min_cost") == col("ps_supplycost")) + +df = df.join(df_part, (["ps_partkey"], ["p_partkey"]), how="inner") + +# From the problem statement, these are the values we wish to output + +df = df.select_columns( + "s_acctbal", + "s_name", + "n_name", + "p_partkey", + "p_mfgr", + "s_address", + "s_phone", + "s_comment", +) + +# Sort and display 100 entries +df = df.sort( + col("s_acctbal").sort(ascending=False), + col("n_name").sort(), + col("s_name").sort(), + col("p_partkey").sort(), +) + +df = df.limit(100) + +# Show results + +df.show() diff --git a/examples/tpch/q03_shipping_priority.py b/examples/tpch/q03_shipping_priority.py new file mode 100644 index 000000000..78993e9e4 --- /dev/null +++ b/examples/tpch/q03_shipping_priority.py @@ -0,0 +1,86 @@ +# 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. + +""" +TPC-H Problem Statement Query 3: + +The Shipping Priority Query retrieves the shipping priority and potential revenue, defined as the +sum of l_extendedprice * (1-l_discount), of the orders having the largest revenue among those that +had not been shipped as of a given date. Orders are listed in decreasing order of revenue. If more +than 10 unshipped orders exist, only the 10 orders with the largest revenue are listed. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datafusion import SessionContext, col, lit, functions as F + +SEGMENT_OF_INTEREST = "BUILDING" +DATE_OF_INTEREST = "1995-03-15" + +# Load the dataframes we need + +ctx = SessionContext() + +df_customer = ctx.read_parquet("data/customer.parquet").select_columns( + "c_mktsegment", "c_custkey" +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderdate", "o_shippriority", "o_custkey", "o_orderkey" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_orderkey", "l_extendedprice", "l_discount", "l_shipdate" +) + +# Limit dataframes to the rows of interest + +df_customer = df_customer.filter(col("c_mktsegment") == lit(SEGMENT_OF_INTEREST)) +df_orders = df_orders.filter(col("o_orderdate") < lit(DATE_OF_INTEREST)) +df_lineitem = df_lineitem.filter(col("l_shipdate") > lit(DATE_OF_INTEREST)) + +# Join all 3 dataframes + +df = df_customer.join(df_orders, (["c_custkey"], ["o_custkey"]), how="inner").join( + df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner" +) + +# Compute the revenue + +df = df.aggregate( + [col("l_orderkey")], + [ + F.first_value(col("o_orderdate")).alias("o_orderdate"), + F.first_value(col("o_shippriority")).alias("o_shippriority"), + F.sum(col("l_extendedprice") * (lit(1.0) - col("l_discount"))).alias("revenue"), + ], +) + +# Sort by priority + +df = df.sort(col("revenue").sort(ascending=False), col("o_orderdate").sort()) + +# Only return 100 results + +df = df.limit(100) + +# Change the order that the columns are reported in just to match the spec + +df = df.select_columns("l_orderkey", "revenue", "o_orderdate", "o_shippriority") + +# Show result + +df.show() diff --git a/examples/tpch/q04_order_priority_checking.py b/examples/tpch/q04_order_priority_checking.py new file mode 100644 index 000000000..b691d5b19 --- /dev/null +++ b/examples/tpch/q04_order_priority_checking.py @@ -0,0 +1,80 @@ +# 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. + +""" +TPC-H Problem Statement Query 4: + +The Order Priority Checking Query counts the number of orders ordered in a given quarter of a given +year in which at least one lineitem was received by the customer later than its committed date. The +query lists the count of such orders for each order priority sorted in ascending priority order. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +# Ideally we could put 3 months into the interval. See note below. +INTERVAL_DAYS = 92 +DATE_OF_INTEREST = "1993-07-01" + +# Load the dataframes we need + +ctx = SessionContext() + +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderdate", "o_orderpriority", "o_orderkey" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_orderkey", "l_commitdate", "l_receiptdate" +) + +# Create a date object from the string +date = datetime.strptime(DATE_OF_INTEREST, "%Y-%m-%d").date() + +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval = pa.scalar((0, 0, INTERVAL_DAYS), type=pa.month_day_nano_interval()) + +# Limit results to cases where commitment date before receipt date +# Aggregate the results so we only get one row to join with the order table. +# Alterately, and likely more idomatic is instead of `.aggregate` you could +# do `.select_columns("l_orderkey").distinct()`. The goal here is to show +# mulitple examples of how to use Data Fusion. +df_lineitem = df_lineitem.filter(col("l_commitdate") < col("l_receiptdate")).aggregate( + [col("l_orderkey")], [] +) + +# Limit orders to date range of interest +df_orders = df_orders.filter(col("o_orderdate") >= lit(date)).filter( + col("o_orderdate") < lit(date) + lit(interval) +) + +# Perform the join to find only orders for which there are lineitems outside of expected range +df = df_orders.join(df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner") + +# Based on priority, find the number of entries +df = df.aggregate( + [col("o_orderpriority")], [F.count(col("o_orderpriority")).alias("order_count")] +) + +# Sort the results +df = df.sort(col("o_orderpriority").sort()) + +df.show() diff --git a/examples/tpch/q05_local_supplier_volume.py b/examples/tpch/q05_local_supplier_volume.py new file mode 100644 index 000000000..7cb6e6324 --- /dev/null +++ b/examples/tpch/q05_local_supplier_volume.py @@ -0,0 +1,102 @@ +# 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. + +""" +TPC-H Problem Statement Query 5: + +The Local Supplier Volume Query lists for each nation in a region the revenue volume that resulted +from lineitem transactions in which the customer ordering parts and the supplier filling them were +both within that nation. The query is run in order to determine whether to institute local +distribution centers in a given region. The query considers only parts ordered in a given year. The +query displays the nations and revenue volume in descending order by revenue. Revenue volume for all +qualifying lineitems in a particular nation is defined as sum(l_extendedprice * (1 - l_discount)). + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + + +DATE_OF_INTEREST = "1994-01-01" +INTERVAL_DAYS = 365 +REGION_OF_INTEREST = "ASIA" + +date = datetime.strptime(DATE_OF_INTEREST, "%Y-%m-%d").date() + +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval = pa.scalar((0, 0, INTERVAL_DAYS), type=pa.month_day_nano_interval()) + +# Load the dataframes we need + +ctx = SessionContext() + +df_customer = ctx.read_parquet("data/customer.parquet").select_columns( + "c_custkey", "c_nationkey" +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_custkey", "o_orderkey", "o_orderdate" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_orderkey", "l_suppkey", "l_extendedprice", "l_discount" +) +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_nationkey" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_regionkey", "n_name" +) +df_region = ctx.read_parquet("data/region.parquet").select_columns( + "r_regionkey", "r_name" +) + +# Restrict dataframes to cases of interest +df_orders = df_orders.filter(col("o_orderdate") >= lit(date)).filter( + col("o_orderdate") < lit(date) + lit(interval) +) + +df_region = df_region.filter(col("r_name") == lit(REGION_OF_INTEREST)) + +# Join all the dataframes + +df = ( + df_customer.join(df_orders, (["c_custkey"], ["o_custkey"]), how="inner") + .join(df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner") + .join( + df_supplier, + (["l_suppkey", "c_nationkey"], ["s_suppkey", "s_nationkey"]), + how="inner", + ) + .join(df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner") + .join(df_region, (["n_regionkey"], ["r_regionkey"]), how="inner") +) + +# Compute the final result + +df = df.aggregate( + [col("n_name")], + [F.sum(col("l_extendedprice") * (lit(1.0) - col("l_discount"))).alias("revenue")], +) + +# Sort in descending order + +df = df.sort(col("revenue").sort(ascending=False)) + +df.show() diff --git a/examples/tpch/q06_forecasting_revenue_change.py b/examples/tpch/q06_forecasting_revenue_change.py new file mode 100644 index 000000000..5fbb91778 --- /dev/null +++ b/examples/tpch/q06_forecasting_revenue_change.py @@ -0,0 +1,87 @@ +# 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. + +""" +TPC-H Problem Statement Query 6: + +The Forecasting Revenue Change Query considers all the lineitems shipped in a given year with +discounts between DISCOUNT-0.01 and DISCOUNT+0.01. The query lists the amount by which the total +revenue would have increased if these discounts had been eliminated for lineitems with l_quantity +less than quantity. Note that the potential revenue increase is equal to the sum of +[l_extendedprice * l_discount] for all lineitems with discounts and quantities in the qualifying +range. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +# Variables from the example query + +DATE_OF_INTEREST = "1994-01-01" +DISCOUT = 0.06 +DELTA = 0.01 +QUANTITY = 24 + +INTERVAL_DAYS = 365 + +date = datetime.strptime(DATE_OF_INTEREST, "%Y-%m-%d").date() + +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval = pa.scalar((0, 0, INTERVAL_DAYS), type=pa.month_day_nano_interval()) + +# Load the dataframes we need + +ctx = SessionContext() + +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_shipdate", "l_quantity", "l_extendedprice", "l_discount" +) + +# Filter down to lineitems of interest + +df = ( + df_lineitem.filter(col("l_shipdate") >= lit(date)) + .filter(col("l_shipdate") < lit(date) + lit(interval)) + .filter(col("l_discount") >= lit(DISCOUT) - lit(DELTA)) + .filter(col("l_discount") <= lit(DISCOUT) + lit(DELTA)) + .filter(col("l_quantity") < lit(QUANTITY)) +) + +# Add up all the "lost" revenue + +df = df.aggregate( + [], [F.sum(col("l_extendedprice") * col("l_discount")).alias("revenue")] +) + +# Show the single result. We could do a `show()` but since we want to demonstrate features of how +# to use Data Fusion, instead collect the result as a python object and print it out. + +# collect() should give a list of record batches. This is a small query, so we should get a +# single batch back, hence the index [0]. Within each record batch we only care about the +# single column result `revenue`. Since we have only one row returned because we aggregated +# over the entire dataframe, we can index it at 0. Then convert the DoubleScalar into a +# simple python object. + +revenue = df.collect()[0]["revenue"][0].as_py() + +# Note: the output value from this query may be dependant on the size of the database generated +print(f"Potential lost revenue: {revenue:.2f}") diff --git a/examples/tpch/q07_volume_shipping.py b/examples/tpch/q07_volume_shipping.py new file mode 100644 index 000000000..3c87f9375 --- /dev/null +++ b/examples/tpch/q07_volume_shipping.py @@ -0,0 +1,123 @@ +# 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. + +""" +TPC-H Problem Statement Query 7: + +The Volume Shipping Query finds, for two given nations, the gross discounted revenues derived from +lineitems in which parts were shipped from a supplier in either nation to a customer in the other +nation during 1995 and 1996. The query lists the supplier nation, the customer nation, the year, +and the revenue from shipments that took place in that year. The query orders the answer by +Supplier nation, Customer nation, and year (all ascending). + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +# Variables of interest to query over + +nation_1 = lit("FRANCE") +nation_2 = lit("GERMANY") + +START_DATE = "1995-01-01" +END_DATE = "1996-12-31" + +start_date = lit(datetime.strptime(START_DATE, "%Y-%m-%d").date()) +end_date = lit(datetime.strptime(END_DATE, "%Y-%m-%d").date()) + + +# Load the dataframes we need + +ctx = SessionContext() + +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_nationkey" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_shipdate", "l_extendedprice", "l_discount", "l_suppkey", "l_orderkey" +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderkey", "o_custkey" +) +df_customer = ctx.read_parquet("data/customer.parquet").select_columns( + "c_custkey", "c_nationkey" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_name" +) + + +# Filter to time of interest +df_lineitem = df_lineitem.filter(col("l_shipdate") >= start_date).filter( + col("l_shipdate") <= end_date +) + + +# A simpler way to do the following operation is to use a filter, but we also want to demonstrate +# how to use case statements. Here we are assigning `n_name` to be itself when it is either of +# the two nations of interest. Since there is no `otherwise()` statement, any values that do +# not match these will result in a null value and then get filtered out. +# +# To do the same using a simle filter would be: +# df_nation = df_nation.filter((F.col("n_name") == nation_1) | (F.col("n_name") == nation_2)) +df_nation = df_nation.with_column( + "n_name", + F.case(col("n_name")) + .when(nation_1, col("n_name")) + .when(nation_2, col("n_name")) + .end(), +).filter(~col("n_name").is_null()) + + +# Limit suppliers to either nation +df_supplier = df_supplier.join( + df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner" +).select(col("s_suppkey"), col("n_name").alias("supp_nation")) + +# Limit customers to either nation +df_customer = df_customer.join( + df_nation, (["c_nationkey"], ["n_nationkey"]), how="inner" +).select(col("c_custkey"), col("n_name").alias("cust_nation")) + +# Join up all the data frames from line items, and make sure the supplier and customer are in +# different nations. +df = ( + df_lineitem.join(df_orders, (["l_orderkey"], ["o_orderkey"]), how="inner") + .join(df_customer, (["o_custkey"], ["c_custkey"]), how="inner") + .join(df_supplier, (["l_suppkey"], ["s_suppkey"]), how="inner") + .filter(col("cust_nation") != col("supp_nation")) +) + +# Extract out two values for every line item +df = df.with_column( + "l_year", F.datepart(lit("year"), col("l_shipdate")).cast(pa.int32()) +).with_column("volume", col("l_extendedprice") * (lit(1.0) - col("l_discount"))) + +# Aggregate the results +df = df.aggregate( + [col("supp_nation"), col("cust_nation"), col("l_year")], + [F.sum(col("volume")).alias("revenue")], +) + +# Sort based on problem statement requirements +df = df.sort(col("supp_nation").sort(), col("cust_nation").sort(), col("l_year").sort()) + +df.show() diff --git a/examples/tpch/q08_market_share.py b/examples/tpch/q08_market_share.py new file mode 100644 index 000000000..a415156ec --- /dev/null +++ b/examples/tpch/q08_market_share.py @@ -0,0 +1,175 @@ +# 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. + +""" +TPC-H Problem Statement Query 8: + +The market share for a given nation within a given region is defined as the fraction of the +revenue, the sum of [l_extendedprice * (1-l_discount)], from the products of a specified type in +that region that was supplied by suppliers from the given nation. The query determines this for the +years 1995 and 1996 presented in this order. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +supplier_nation = lit("BRAZIL") +customer_region = lit("AMERICA") +part_of_interest = lit("ECONOMY ANODIZED STEEL") + +START_DATE = "1995-01-01" +END_DATE = "1996-12-31" + +start_date = lit(datetime.strptime(START_DATE, "%Y-%m-%d").date()) +end_date = lit(datetime.strptime(END_DATE, "%Y-%m-%d").date()) + + +# Load the dataframes we need + +ctx = SessionContext() + +df_part = ctx.read_parquet("data/part.parquet").select_columns("p_partkey", "p_type") +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_nationkey" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_partkey", "l_extendedprice", "l_discount", "l_suppkey", "l_orderkey" +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderkey", "o_custkey", "o_orderdate" +) +df_customer = ctx.read_parquet("data/customer.parquet").select_columns( + "c_custkey", "c_nationkey" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_name", "n_regionkey" +) +df_region = ctx.read_parquet("data/region.parquet").select_columns( + "r_regionkey", "r_name" +) + +# Limit possible parts to the one specified +df_part = df_part.filter(col("p_type") == part_of_interest) + +# Limit orders to those in the specified range + +df_orders = df_orders.filter(col("o_orderdate") >= start_date).filter( + col("o_orderdate") <= end_date +) + +# Part 1: Find customers in the region + +# We want customers in region specified by region_of_interest. This will be used to compute +# the total sales of the part of interest. We want to know of those sales what fraction +# was supplied by the nation of interest. There is no guarantee that the nation of +# interest is within the region of interest. + +# First we find all the sales that make up the basis. + +df_regional_customers = df_region.filter(col("r_name") == customer_region) + +# After this join we have all of the possible sales nations +df_regional_customers = df_regional_customers.join( + df_nation, (["r_regionkey"], ["n_regionkey"]), how="inner" +) + +# Now find the possible customers +df_regional_customers = df_regional_customers.join( + df_customer, (["n_nationkey"], ["c_nationkey"]), how="inner" +) + +# Next find orders for these customers +df_regional_customers = df_regional_customers.join( + df_orders, (["c_custkey"], ["o_custkey"]), how="inner" +) + +# Find all line items from these orders +df_regional_customers = df_regional_customers.join( + df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner" +) + +# Limit to the part of interest +df_regional_customers = df_regional_customers.join( + df_part, (["l_partkey"], ["p_partkey"]), how="inner" +) + +# Compute the volume for each line item +df_regional_customers = df_regional_customers.with_column( + "volume", col("l_extendedprice") * (lit(1.0) - col("l_discount")) +) + +# Part 2: Find suppliers from the nation + +# Now that we have all of the sales of that part in the specified region, we need +# to determine which of those came from suppliers in the nation we are interested in. + +df_national_suppliers = df_nation.filter(col("n_name") == supplier_nation) + +# Determine the suppliers by the limited nation key we have in our single row df above +df_national_suppliers = df_national_suppliers.join( + df_supplier, (["n_nationkey"], ["s_nationkey"]), how="inner" +) + +# When we join to the customer dataframe, we don't want to confuse other columns, so only +# select the supplier key that we need +df_national_suppliers = df_national_suppliers.select_columns("s_suppkey") + + +# Part 3: Combine suppliers and customers and compute the market share + +# Now we can do a left outer join on the suppkey. Those line items from other suppliers +# will get a null value. We can check for the existence of this null to compute a volume +# column only from suppliers in the nation we are evaluating. + +df = df_regional_customers.join( + df_national_suppliers, (["l_suppkey"], ["s_suppkey"]), how="left" +) + +# Use a case statement to compute the volume sold by suppliers in the nation of interest +df = df.with_column( + "national_volume", + F.case(col("s_suppkey").is_null()) + .when(lit(False), col("volume")) + .otherwise(lit(0.0)), +) + +df = df.with_column( + "o_year", F.datepart(lit("year"), col("o_orderdate")).cast(pa.int32()) +) + + +# Lastly, sum up the results + +df = df.aggregate( + [col("o_year")], + [ + F.sum(col("volume")).alias("volume"), + F.sum(col("national_volume")).alias("national_volume"), + ], +) + +df = df.select( + col("o_year"), (F.col("national_volume") / F.col("volume")).alias("mkt_share") +) + +df = df.sort(col("o_year").sort()) + +df.show() diff --git a/examples/tpch/q09_product_type_profit_measure.py b/examples/tpch/q09_product_type_profit_measure.py new file mode 100644 index 000000000..4fdfc1cba --- /dev/null +++ b/examples/tpch/q09_product_type_profit_measure.py @@ -0,0 +1,93 @@ +# 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. + +""" +TPC-H Problem Statement Query 9: + +The Product Type Profit Measure Query finds, for each nation and each year, the profit for all parts +ordered in that year that contain a specified substring in their names and that were filled by a +supplier in that nation. The profit is defined as the sum of +[(l_extendedprice*(1-l_discount)) - (ps_supplycost * l_quantity)] for all lineitems describing +parts in the specified line. The query lists the nations in ascending alphabetical order and, for +each nation, the year and profit in descending order by year (most recent first). + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +part_color = lit("green") + +# Load the dataframes we need + +ctx = SessionContext() + +df_part = ctx.read_parquet("data/part.parquet").select_columns("p_partkey", "p_name") +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_nationkey" +) +df_partsupp = ctx.read_parquet("data/partsupp.parquet").select_columns( + "ps_suppkey", "ps_partkey", "ps_supplycost" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_partkey", + "l_extendedprice", + "l_discount", + "l_suppkey", + "l_orderkey", + "l_quantity", +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderkey", "o_custkey", "o_orderdate" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_name", "n_regionkey" +) + +# Limit possible parts to the color specified +df = df_part.filter(F.strpos(col("p_name"), part_color) > lit(0)) + +# We have a series of joins that get us to limit down to the line items we need +df = df.join(df_lineitem, (["p_partkey"], ["l_partkey"]), how="inner") +df = df.join(df_supplier, (["l_suppkey"], ["s_suppkey"]), how="inner") +df = df.join(df_orders, (["l_orderkey"], ["o_orderkey"]), how="inner") +df = df.join( + df_partsupp, (["l_suppkey", "l_partkey"], ["ps_suppkey", "ps_partkey"]), how="inner" +) +df = df.join(df_nation, (["s_nationkey"], ["n_nationkey"]), how="inner") + +# Compute the intermediate values and limit down to the expressions we need +df = df.select( + col("n_name").alias("nation"), + F.datepart(lit("year"), col("o_orderdate")).cast(pa.int32()).alias("o_year"), + ( + col("l_extendedprice") * (lit(1.0) - col("l_discount")) + - (col("ps_supplycost") * col("l_quantity")) + ).alias("amount"), +) + +# Sum up the values by nation and year +df = df.aggregate( + [col("nation"), col("o_year")], [F.sum(col("amount")).alias("profit")] +) + +# Sort according to the problem specification +df = df.sort(col("nation").sort(), col("o_year").sort(ascending=False)) + +df.show() diff --git a/examples/tpch/q10_returned_item_reporting.py b/examples/tpch/q10_returned_item_reporting.py new file mode 100644 index 000000000..1879027c1 --- /dev/null +++ b/examples/tpch/q10_returned_item_reporting.py @@ -0,0 +1,108 @@ +# 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. + +""" +TPC-H Problem Statement Query 10: + +The Returned Item Reporting Query finds the top 20 customers, in terms of their effect on lost +revenue for a given quarter, who have returned parts. The query considers only parts that were +ordered in the specified quarter. The query lists the customer's name, address, nation, phone +number, account balance, comment information and revenue lost. The customers are listed in +descending order of lost revenue. Revenue lost is defined as +sum(l_extendedprice*(1-l_discount)) for all qualifying lineitems. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +DATE_START_OF_QUARTER = "1993-10-01" + +date_start_of_quarter = lit(datetime.strptime(DATE_START_OF_QUARTER, "%Y-%m-%d").date()) + +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval_one_quarter = lit(pa.scalar((0, 0, 120), type=pa.month_day_nano_interval())) + +# Load the dataframes we need + +ctx = SessionContext() + +df_customer = ctx.read_parquet("data/customer.parquet").select_columns( + "c_custkey", + "c_nationkey", + "c_name", + "c_acctbal", + "c_address", + "c_phone", + "c_comment", +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_extendedprice", "l_discount", "l_orderkey", "l_returnflag" +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderkey", "o_custkey", "o_orderdate" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_name", "n_regionkey" +) + +# limit to returns +df_lineitem = df_lineitem.filter(col("l_returnflag") == lit("R")) + + +# Rather than aggregate by all of the customer fields as you might do looking at the specification, +# we can aggregate by o_custkey and then join in the customer data at the end. + +df = df_orders.filter(col("o_orderdate") >= date_start_of_quarter).filter( + col("o_orderdate") < date_start_of_quarter + interval_one_quarter +) + +df = df.join(df_lineitem, (["o_orderkey"], ["l_orderkey"]), how="inner") + +# Compute the revenue +df = df.aggregate( + [col("o_custkey")], + [F.sum(col("l_extendedprice") * (lit(1.0) - col("l_discount"))).alias("revenue")], +) + +# Now join in the customer data +df = df.join(df_customer, (["o_custkey"], ["c_custkey"]), how="inner") +df = df.join(df_nation, (["c_nationkey"], ["n_nationkey"]), how="inner") + +# These are the columns the problem statement requires +df = df.select_columns( + "c_custkey", + "c_name", + "revenue", + "c_acctbal", + "n_name", + "c_address", + "c_phone", + "c_comment", +) + +# Sort the results in descending order +df = df.sort(col("revenue").sort(ascending=False)) + +# Only return the top 20 results +df = df.limit(20) + +df.show() diff --git a/examples/tpch/q11_important_stock_identification.py b/examples/tpch/q11_important_stock_identification.py new file mode 100644 index 000000000..78fe26dbf --- /dev/null +++ b/examples/tpch/q11_important_stock_identification.py @@ -0,0 +1,82 @@ +# 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. + +""" +TPC-H Problem Statement Query 11: + +The Important Stock Identification Query finds, from scanning the available stock of suppliers +in a given nation, all the parts that represent a significant percentage of the total value of +all available parts. The query displays the part number and the value of those parts in +descending order of value. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datafusion import SessionContext, WindowFrame, col, lit, functions as F + +NATION = "GERMANY" +FRACTION = 0.0001 + +# Load the dataframes we need + +ctx = SessionContext() + +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_nationkey" +) +df_partsupp = ctx.read_parquet("data/partsupp.parquet").select_columns( + "ps_supplycost", "ps_availqty", "ps_suppkey", "ps_partkey" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_name" +) + +# limit to returns +df_nation = df_nation.filter(col("n_name") == lit(NATION)) + +# Find part supplies of within this target nation + +df = df_nation.join(df_supplier, (["n_nationkey"], ["s_nationkey"]), how="inner") + +df = df.join(df_partsupp, (["s_suppkey"], ["ps_suppkey"]), how="inner") + + +# Compute the value of individual parts +df = df.with_column("value", col("ps_supplycost") * col("ps_availqty")) + +# Compute total value of specific parts +df = df.aggregate([col("ps_partkey")], [F.sum(col("value")).alias("value")]) + +# By default window functions go from unbounded preceeding to current row, but we want +# to compute this sum across all rows +window_frame = WindowFrame("rows", None, None) + +df = df.with_column( + "total_value", F.window("sum", [col("value")], window_frame=window_frame) +) + +# Limit to the parts for which there is a significant value based on the fraction of the total +df = df.filter(col("value") / col("total_value") > lit(FRACTION)) + +# We only need to report on these two columns +df = df.select_columns("ps_partkey", "value") + +# Sort in descending order of value +df = df.sort(col("value").sort(ascending=False)) + +df.show() diff --git a/examples/tpch/q12_ship_mode_order_priority.py b/examples/tpch/q12_ship_mode_order_priority.py new file mode 100644 index 000000000..e76efa54e --- /dev/null +++ b/examples/tpch/q12_ship_mode_order_priority.py @@ -0,0 +1,112 @@ +# 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. + +""" +TPC-H Problem Statement Query 12: + +The Shipping Modes and Order Priority Query counts, by ship mode, for lineitems actually received +by customers in a given year, the number of lineitems belonging to orders for which the +l_receiptdate exceeds the l_commitdate for two different specified ship modes. Only lineitems that +were actually shipped before the l_commitdate are considered. The late lineitems are partitioned +into two groups, those with priority URGENT or HIGH, and those with a priority other than URGENT or +HIGH. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +SHIP_MODE_1 = "MAIL" +SHIP_MODE_2 = "SHIP" +DATE_OF_INTEREST = "1994-01-01" + +# Load the dataframes we need + +ctx = SessionContext() + +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderkey", "o_orderpriority" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_orderkey", "l_shipmode", "l_commitdate", "l_shipdate", "l_receiptdate" +) + +date = datetime.strptime(DATE_OF_INTEREST, "%Y-%m-%d").date() + +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval = pa.scalar((0, 0, 365), type=pa.month_day_nano_interval()) + + +df = df_lineitem.filter(col("l_receiptdate") >= lit(date)).filter( + col("l_receiptdate") < lit(date) + lit(interval) +) + +# Note: It is not recommended to use array_has because it treats the second argument as an argument +# so if you pass it col("l_shipmode") it will pass the entire array to process which is very slow. +# Instead check the position of the entry is not null. +df = df.filter( + ~F.array_position( + F.make_array(lit(SHIP_MODE_1), lit(SHIP_MODE_2)), col("l_shipmode") + ).is_null() +) + +# Since we have only two values, it's much easier to do this as a filter where the l_shipmode +# matches either of the two values, but we want to show doing some array operations in this +# example. If you want to see this done with filters, comment out the above line and uncomment +# this one. +# df = df.filter((col("l_shipmode") == lit(SHIP_MODE_1)) | (col("l_shipmode") == lit(SHIP_MODE_2))) + + +# We need order priority, so join order df to line item +df = df.join(df_orders, (["l_orderkey"], ["o_orderkey"]), how="inner") + +# Restrict to line items we care about based on the problem statement. +df = df.filter(col("l_commitdate") < col("l_receiptdate")) + +df = df.filter(col("l_shipdate") < col("l_commitdate")) + +df = df.with_column( + "high_line_value", + F.case(col("o_orderpriority")) + .when(lit("1-URGENT"), lit(1)) + .when(lit("2-HIGH"), lit(1)) + .otherwise(lit(0)), +) + +# Aggregate the results +df = df.aggregate( + [col("l_shipmode")], + [ + F.sum(col("high_line_value")).alias("high_line_count"), + F.count(col("high_line_value")).alias("all_lines_count"), + ], +) + +# Compute the final output +df = df.select( + col("l_shipmode"), + col("high_line_count"), + (col("all_lines_count") - col("high_line_count")).alias("low_line_count"), +) + +df = df.sort(col("l_shipmode").sort()) + +df.show() diff --git a/examples/tpch/q13_customer_distribution.py b/examples/tpch/q13_customer_distribution.py new file mode 100644 index 000000000..1eb9ca303 --- /dev/null +++ b/examples/tpch/q13_customer_distribution.py @@ -0,0 +1,64 @@ +# 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. + +""" +TPC-H Problem Statement Query 13: + +This query determines the distribution of customers by the number of orders they have made, +including customers who have no record of orders, past or present. It counts and reports how many +customers have no orders, how many have 1, 2, 3, etc. A check is made to ensure that the orders +counted do not fall into one of several special categories of orders. Special categories are +identified in the order comment column by looking for a particular pattern. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datafusion import SessionContext, col, lit, functions as F + +WORD_1 = "special" +WORD_2 = "requests" + +# Load the dataframes we need + +ctx = SessionContext() + +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_custkey", "o_comment" +) +df_customer = ctx.read_parquet("data/customer.parquet").select_columns("c_custkey") + +# Use a regex to remove special cases +df_orders = df_orders.filter( + F.regexp_match(col("o_comment"), lit(f"{WORD_1}.?*{WORD_2}")).is_null() +) + +# Since we may have customers with no orders we must do a left join +df = df_customer.join(df_orders, (["c_custkey"], ["o_custkey"]), how="left") + +# Find the number of orders for each customer +df = df.aggregate([col("c_custkey")], [F.count(col("c_custkey")).alias("c_count")]) + +# Ultimately we want to know the number of customers that have that customer count +df = df.aggregate([col("c_count")], [F.count(col("c_count")).alias("custdist")]) + +# We want to order the results by the highest number of customers per count +df = df.sort( + col("custdist").sort(ascending=False), col("c_count").sort(ascending=False) +) + +df.show() diff --git a/examples/tpch/q14_promotion_effect.py b/examples/tpch/q14_promotion_effect.py new file mode 100644 index 000000000..9ec38366b --- /dev/null +++ b/examples/tpch/q14_promotion_effect.py @@ -0,0 +1,81 @@ +# 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. + +""" +TPC-H Problem Statement Query 14: + +The Promotion Effect Query determines what percentage of the revenue in a given year and month was +derived from promotional parts. The query considers only parts actually shipped in that month and +gives the percentage. Revenue is defined as (l_extendedprice * (1-l_discount)). + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +DATE = "1995-09-01" + +date_of_interest = lit(datetime.strptime(DATE, "%Y-%m-%d").date()) +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval_one_month = lit(pa.scalar((0, 0, 30), type=pa.month_day_nano_interval())) + +# Load the dataframes we need + +ctx = SessionContext() + +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_partkey", "l_shipdate", "l_extendedprice", "l_discount" +) +df_part = ctx.read_parquet("data/part.parquet").select_columns("p_partkey", "p_type") + + +# Check part type begins with PROMO +df_part = df_part.filter( + F.substr(col("p_type"), lit(0), lit(6)) == lit("PROMO") +).with_column("promo_factor", lit(1.0)) + +df_lineitem = df_lineitem.filter(col("l_shipdate") >= date_of_interest).filter( + col("l_shipdate") < date_of_interest + interval_one_month +) + +# Left join so we can sum up the promo parts different from other parts +df = df_lineitem.join(df_part, (["l_partkey"], ["p_partkey"]), "left") + +# Make a factor of 1.0 if it is a promotion, 0.0 otherwise +df = df.with_column("promo_factor", F.coalesce(col("promo_factor"), lit(0.0))) +df = df.with_column("revenue", col("l_extendedprice") * (lit(1.0) - col("l_discount"))) + + +# Sum up the promo and total revenue +df = df.aggregate( + [], + [ + F.sum(col("promo_factor") * col("revenue")).alias("promo_revenue"), + F.sum(col("revenue")).alias("total_revenue"), + ], +) + +# Return the percentage of revenue from promotions +df = df.select( + (lit(100.0) * col("promo_revenue") / col("total_revenue")).alias("promo_revenue") +) + +df.show() diff --git a/examples/tpch/q15_top_supplier.py b/examples/tpch/q15_top_supplier.py new file mode 100644 index 000000000..7113e04fe --- /dev/null +++ b/examples/tpch/q15_top_supplier.py @@ -0,0 +1,87 @@ +# 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. + +""" +TPC-H Problem Statement Query 15: + +The Top Supplier Query finds the supplier who contributed the most to the overall revenue for parts +shipped during a given quarter of a given year. In case of a tie, the query lists all suppliers +whose contribution was equal to the maximum, presented in supplier number order. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, WindowFrame, col, lit, functions as F + +DATE = "1996-01-01" + +date_of_interest = lit(datetime.strptime(DATE, "%Y-%m-%d").date()) +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval_3_months = lit(pa.scalar((0, 0, 90), type=pa.month_day_nano_interval())) + +# Load the dataframes we need + +ctx = SessionContext() + +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_suppkey", "l_shipdate", "l_extendedprice", "l_discount" +) +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", + "s_name", + "s_address", + "s_phone", +) + +# Limit line items to the quarter of interest +df_lineitem = df_lineitem.filter(col("l_shipdate") >= date_of_interest).filter( + col("l_shipdate") < date_of_interest + interval_3_months +) + +df = df_lineitem.aggregate( + [col("l_suppkey")], + [ + F.sum(col("l_extendedprice") * (lit(1.0) - col("l_discount"))).alias( + "total_revenue" + ) + ], +) + +# Use a window function to find the maximum revenue across the entire dataframe +window_frame = WindowFrame("rows", None, None) +df = df.with_column( + "max_revenue", F.window("max", [col("total_revenue")], window_frame=window_frame) +) + +# Find all suppliers whose total revenue is the same as the maximum +df = df.filter(col("total_revenue") == col("max_revenue")) + +# Now that we know the supplier(s) with maximum revenue, get the rest of their information +# from the supplier table +df = df.join(df_supplier, (["l_suppkey"], ["s_suppkey"]), "inner") + +# Return only the colums requested +df = df.select_columns("s_suppkey", "s_name", "s_address", "s_phone", "total_revenue") + +# If we have more than one, sort by supplier number (suppkey) +df = df.sort(col("s_suppkey").sort()) + +df.show() diff --git a/examples/tpch/q16_part_supplier_relationship.py b/examples/tpch/q16_part_supplier_relationship.py new file mode 100644 index 000000000..5f941d5ad --- /dev/null +++ b/examples/tpch/q16_part_supplier_relationship.py @@ -0,0 +1,85 @@ +# 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. + +""" +TPC-H Problem Statement Query 16: + +The Parts/Supplier Relationship Query counts the number of suppliers who can supply parts that +satisfy a particular customer's requirements. The customer is interested in parts of eight +different sizes as long as they are not of a given type, not of a given brand, and not from a +supplier who has had complaints registered at the Better Business Bureau. Results must be presented +in descending count and ascending brand, type, and size. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +BRAND = "Brand#45" +TYPE_TO_IGNORE = "MEDIUM POLISHED" +SIZES_OF_INTEREST = [49, 14, 23, 45, 19, 3, 36, 9] + +# Load the dataframes we need + +ctx = SessionContext() + +df_part = ctx.read_parquet("data/part.parquet").select_columns( + "p_partkey", "p_brand", "p_type", "p_size" +) +df_partsupp = ctx.read_parquet("data/partsupp.parquet").select_columns( + "ps_suppkey", "ps_partkey" +) +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_comment" +) + +df_unwanted_suppliers = df_supplier.filter( + ~F.regexp_match(col("s_comment"), lit("Customer.?*Complaints")).is_null() +) + +# Remove unwanted suppliers +df_partsupp = df_partsupp.join( + df_unwanted_suppliers, (["ps_suppkey"], ["s_suppkey"]), "anti" +) + +# Select the parts we are interested in +df_part = df_part.filter(col("p_brand") == lit(BRAND)) +df_part = df_part.filter( + F.substr(col("p_type"), lit(0), lit(len(TYPE_TO_IGNORE) + 1)) != lit(TYPE_TO_IGNORE) +) + +# Python conversion of integer to literal casts it to int64 but the data for +# part size is stored as an int32, so perform a cast. Then check to find if the part +# size is within the array of possible sizes by checking the position of it is not +# null. +p_sizes = F.make_array(*[lit(s).cast(pa.int32()) for s in SIZES_OF_INTEREST]) +df_part = df_part.filter(~F.array_position(p_sizes, col("p_size")).is_null()) + +df = df_part.join(df_partsupp, (["p_partkey"], ["ps_partkey"]), "inner") + +df = df.select_columns("p_brand", "p_type", "p_size", "ps_suppkey").distinct() + +df = df.aggregate( + [col("p_brand"), col("p_type"), col("p_size")], + [F.count(col("ps_suppkey")).alias("supplier_cnt")], +) + +df = df.sort(col("supplier_cnt").sort(ascending=False)) + +df.show() diff --git a/examples/tpch/q17_small_quantity_order.py b/examples/tpch/q17_small_quantity_order.py new file mode 100644 index 000000000..aae238b2f --- /dev/null +++ b/examples/tpch/q17_small_quantity_order.py @@ -0,0 +1,69 @@ +# 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. + +""" +TPC-H Problem Statement Query 17: + +The Small-Quantity-Order Revenue Query considers parts of a given brand and with a given container +type and determines the average lineitem quantity of such parts ordered for all orders (past and +pending) in the 7-year database. What would be the average yearly gross (undiscounted) loss in +revenue if orders for these parts with a quantity of less than 20% of this average were no longer +taken? + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datafusion import SessionContext, WindowFrame, col, lit, functions as F + +BRAND = "Brand#23" +CONTAINER = "MED BOX" + +# Load the dataframes we need + +ctx = SessionContext() + +df_part = ctx.read_parquet("data/part.parquet").select_columns( + "p_partkey", "p_brand", "p_container" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_partkey", "l_quantity", "l_extendedprice" +) + +# Limit to the problem statement's brand and container types +df = df_part.filter(col("p_brand") == lit(BRAND)).filter( + col("p_container") == lit(CONTAINER) +) + +# Combine data +df = df.join(df_lineitem, (["p_partkey"], ["l_partkey"]), "inner") + +# Find the average quantity +window_frame = WindowFrame("rows", None, None) +df = df.with_column( + "avg_quantity", F.window("avg", [col("l_quantity")], window_frame=window_frame) +) + +df = df.filter(col("l_quantity") < lit(0.2) * col("avg_quantity")) + +# Compute the total +df = df.aggregate([], [F.sum(col("l_extendedprice")).alias("total")]) + +# Divide by number of years in the problem statement to get average +df = df.select((col("total") / lit(7.0)).alias("avg_yearly")) + +df.show() diff --git a/examples/tpch/q18_large_volume_customer.py b/examples/tpch/q18_large_volume_customer.py new file mode 100644 index 000000000..96ca08ff7 --- /dev/null +++ b/examples/tpch/q18_large_volume_customer.py @@ -0,0 +1,65 @@ +# 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. + +""" +TPC-H Problem Statement Query 18: + +The Large Volume Customer Query finds a list of the top 100 customers who have ever placed large +quantity orders. The query lists the customer name, customer key, the order key, date and total +price and the quantity for the order. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datafusion import SessionContext, col, lit, functions as F + +QUANTITY = 300 + +# Load the dataframes we need + +ctx = SessionContext() + +df_customer = ctx.read_parquet("data/customer.parquet").select_columns( + "c_custkey", "c_name" +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderkey", "o_custkey", "o_orderdate", "o_totalprice" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_orderkey", "l_quantity", "l_extendedprice" +) + +df = df_lineitem.aggregate( + [col("l_orderkey")], [F.sum(col("l_quantity")).alias("total_quantity")] +) + +# Limit to orders in which the total quantity is above a threshold +df = df.filter(col("total_quantity") > lit(QUANTITY)) + +# We've identified the orders of interest, now join the additional data +# we are required to report on +df = df.join(df_orders, (["l_orderkey"], ["o_orderkey"]), "inner") +df = df.join(df_customer, (["o_custkey"], ["c_custkey"]), "inner") + +df = df.select_columns( + "c_name", "c_custkey", "o_orderkey", "o_orderdate", "o_totalprice", "total_quantity" +) + +df = df.sort(col("o_totalprice").sort(ascending=False), col("o_orderdate").sort()) + +df.show() diff --git a/examples/tpch/q19_discounted_revenue.py b/examples/tpch/q19_discounted_revenue.py new file mode 100644 index 000000000..20ad48a77 --- /dev/null +++ b/examples/tpch/q19_discounted_revenue.py @@ -0,0 +1,137 @@ +# 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. + +""" +TPC-H Problem Statement Query 19: + +The Discounted Revenue query finds the gross discounted revenue for all orders for three different +types of parts that were shipped by air and delivered in person. Parts are selected based on the +combination of specific brands, a list of containers, and a range of sizes. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +import pyarrow as pa +from datafusion import SessionContext, col, lit, udf, functions as F + +items_of_interest = { + "Brand#12": { + "min_quantity": 1, + "containers": ["SM CASE", "SM BOX", "SM PACK", "SM PKG"], + "max_size": 5, + }, + "Brand#23": { + "min_quantity": 10, + "containers": ["MED BAG", "MED BOX", "MED PKG", "MED PACK"], + "max_size": 10, + }, + "Brand#34": { + "min_quantity": 20, + "containers": ["LG CASE", "LG BOX", "LG PACK", "LG PKG"], + "max_size": 15, + }, +} + +# Load the dataframes we need + +ctx = SessionContext() + +df_part = ctx.read_parquet("data/part.parquet").select_columns( + "p_partkey", "p_brand", "p_container", "p_size" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_partkey", + "l_quantity", + "l_shipmode", + "l_shipinstruct", + "l_extendedprice", + "l_discount", +) + +# These limitations apply to all line items, so go ahead and do them first + +df = df_lineitem.filter(col("l_shipinstruct") == lit("DELIVER IN PERSON")) + +# Small note: The data generated uses "REG AIR" but the spec says "AIR REG" +df = df.filter( + (col("l_shipmode") == lit("AIR")) | (col("l_shipmode") == lit("REG AIR")) +) + +df = df.join(df_part, (["l_partkey"], ["p_partkey"]), "inner") + + +# Create the user defined function (UDF) definition that does the work +def is_of_interest( + brand_arr: pa.Array, + container_arr: pa.Array, + quantity_arr: pa.Array, + size_arr: pa.Array, +) -> pa.Array: + """ + The purpose of this function is to demonstrate how a UDF works, taking as input a pyarrow Array + and generating a resultant Array. The length of the inputs should match and there should be the + same number of rows in the output. + """ + result = [] + for idx, brand in enumerate(brand_arr): + brand = brand.as_py() + if brand in items_of_interest: + values_of_interest = items_of_interest[brand] + + container_matches = ( + container_arr[idx].as_py() in values_of_interest["containers"] + ) + + quantity = quantity_arr[idx].as_py() + quantity_matches = ( + values_of_interest["min_quantity"] + <= quantity + <= values_of_interest["min_quantity"] + 10 + ) + + size = size_arr[idx].as_py() + size_matches = 1 <= size <= values_of_interest["max_size"] + + result.append(container_matches and quantity_matches and size_matches) + else: + result.append(False) + + return pa.array(result) + + +# Turn the above function into a UDF that DataFusion can understand +is_of_interest_udf = udf( + is_of_interest, + [pa.utf8(), pa.utf8(), pa.float32(), pa.int32()], + pa.bool_(), + "stable", +) + +# Filter results using the above UDF +df = df.filter( + is_of_interest_udf( + col("p_brand"), col("p_container"), col("l_quantity"), col("p_size") + ) +) + +df = df.aggregate( + [], + [F.sum(col("l_extendedprice") * (lit(1.0) - col("l_discount"))).alias("revenue")], +) + +df.show() diff --git a/examples/tpch/q20_potential_part_promotion.py b/examples/tpch/q20_potential_part_promotion.py new file mode 100644 index 000000000..09686db05 --- /dev/null +++ b/examples/tpch/q20_potential_part_promotion.py @@ -0,0 +1,97 @@ +# 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. + +""" +TPC-H Problem Statement Query 20: + +The Potential Part Promotion query identifies suppliers who have an excess of a given part +available; an excess is defined to be more than 50% of the parts like the given part that the +supplier shipped in a given year for a given nation. Only parts whose names share a certain naming +convention are considered. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datetime import datetime +import pyarrow as pa +from datafusion import SessionContext, col, lit, functions as F + +COLOR_OF_INTEREST = "forest" +DATE_OF_INTEREST = "1994-01-01" +NATION_OF_INTEREST = "CANADA" + +# Load the dataframes we need + +ctx = SessionContext() + +df_part = ctx.read_parquet("data/part.parquet").select_columns("p_partkey", "p_name") +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_shipdate", "l_partkey", "l_suppkey", "l_quantity" +) +df_partsupp = ctx.read_parquet("data/partsupp.parquet").select_columns( + "ps_partkey", "ps_suppkey", "ps_availqty" +) +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_address", "s_name", "s_nationkey" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_name" +) + +date = datetime.strptime(DATE_OF_INTEREST, "%Y-%m-%d").date() + +# Note: this is a hack on setting the values. It should be set differently once +# https://github.com/apache/datafusion-python/issues/665 is resolved. +interval = pa.scalar((0, 0, 365), type=pa.month_day_nano_interval()) + +# Filter down dataframes +df_nation = df_nation.filter(col("n_name") == lit(NATION_OF_INTEREST)) +df_part = df_part.filter( + F.substr(col("p_name"), lit(0), lit(len(COLOR_OF_INTEREST) + 1)) + == lit(COLOR_OF_INTEREST) +) + +df = df_lineitem.filter(col("l_shipdate") >= lit(date)).filter( + col("l_shipdate") < lit(date) + lit(interval) +) + +# This will filter down the line items to the parts of interest +df = df.join(df_part, (["l_partkey"], ["p_partkey"]), "inner") + +# Compute the total sold and limit ourselves to indivdual supplier/part combinations +df = df.aggregate( + [col("l_partkey"), col("l_suppkey")], [F.sum(col("l_quantity")).alias("total_sold")] +) + +df = df.join( + df_partsupp, (["l_partkey", "l_suppkey"], ["ps_partkey", "ps_suppkey"]), "inner" +) + +# Find cases of excess quantity +df.filter(col("ps_availqty") > lit(0.5) * col("total_sold")) + +# We could do these joins earlier, but now limit to the nation of interest suppliers +df = df.join(df_supplier, (["ps_suppkey"], ["s_suppkey"]), "inner") +df = df.join(df_nation, (["s_nationkey"], ["n_nationkey"]), "inner") + +# Restrict to the requested data per the problem statement +df = df.select_columns("s_name", "s_address") + +df = df.sort(col("s_name").sort()) + +df.show() diff --git a/examples/tpch/q21_suppliers_kept_orders_waiting.py b/examples/tpch/q21_suppliers_kept_orders_waiting.py new file mode 100644 index 000000000..2f58d6e79 --- /dev/null +++ b/examples/tpch/q21_suppliers_kept_orders_waiting.py @@ -0,0 +1,114 @@ +# 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. + +""" +TPC-H Problem Statement Query 21: + +The Suppliers Who Kept Orders Waiting query identifies suppliers, for a given nation, whose product +was part of a multi-supplier order (with current status of 'F') where they were the only supplier +who failed to meet the committed delivery date. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datafusion import SessionContext, col, lit, functions as F + +NATION_OF_INTEREST = "SAUDI ARABIA" + +# Load the dataframes we need + +ctx = SessionContext() + +df_orders = ctx.read_parquet("data/orders.parquet").select_columns( + "o_orderkey", "o_orderstatus" +) +df_lineitem = ctx.read_parquet("data/lineitem.parquet").select_columns( + "l_orderkey", "l_receiptdate", "l_commitdate", "l_suppkey" +) +df_supplier = ctx.read_parquet("data/supplier.parquet").select_columns( + "s_suppkey", "s_name", "s_nationkey" +) +df_nation = ctx.read_parquet("data/nation.parquet").select_columns( + "n_nationkey", "n_name" +) + +# Limit to suppliers in the nation of interest +df_suppliers_of_interest = df_nation.filter(col("n_name") == lit(NATION_OF_INTEREST)) + +df_suppliers_of_interest = df_suppliers_of_interest.join( + df_supplier, (["n_nationkey"], ["s_nationkey"]), "inner" +) + +# Find the failed orders and all their line items +df = df_orders.filter(col("o_orderstatus") == lit("F")) + +df = df_lineitem.join(df, (["l_orderkey"], ["o_orderkey"]), "inner") + +# Identify the line items for which the order is failed due to. +df = df.with_column( + "failed_supp", + F.case(col("l_receiptdate") > col("l_commitdate")) + .when(lit(True), col("l_suppkey")) + .end(), +) + +# There are other ways we could do this but the purpose of this example is to work with rows where +# an element is an array of values. In this case, we will create two columns of arrays. One will be +# an array of all of the suppliers who made up this order. That way we can filter the dataframe for +# only orders where this array is larger than one for multiple supplier orders. The second column +# is all of the suppliers who failed to make their commitment. We can filter the second column for +# arrays with size one. That combination will give us orders that had multiple suppliers where only +# one failed. Use distinct=True in the blow aggregation so we don't get multipe line items from the +# same supplier reported in either array. +df = df.aggregate( + [col("o_orderkey")], + [ + F.array_agg(col("l_suppkey"), distinct=True).alias("all_suppliers"), + F.array_agg(col("failed_supp"), distinct=True).alias("failed_suppliers"), + ], +) + +# Remove the null entries that will get returned by array_agg so we can test to see where we only +# have a single failed supplier in a multiple supplier order +df = df.with_column( + "failed_suppliers", F.array_remove(col("failed_suppliers"), lit(None)) +) + +# This is the check described above which will identify single failed supplier in a multiple +# supplier order. +df = df.filter(F.array_length(col("failed_suppliers")) == lit(1)).filter( + F.array_length(col("all_suppliers")) > lit(1) +) + +# Since we have an array we know is exactly one element long, we can extract that single value. +df = df.select( + col("o_orderkey"), F.array_element(col("failed_suppliers"), lit(1)).alias("suppkey") +) + +# Join to the supplier of interest list for the nation of interest +df = df.join(df_suppliers_of_interest, (["suppkey"], ["s_suppkey"]), "inner") + +# Count how many orders that supplier is the only failed supplier for +df = df.aggregate([col("s_name")], [F.count(col("o_orderkey")).alias("numwait")]) + +# Return in descending order +df = df.sort(col("numwait").sort(ascending=False)) + +df = df.limit(100) + +df.show() diff --git a/examples/tpch/q22_global_sales_opportunity.py b/examples/tpch/q22_global_sales_opportunity.py new file mode 100644 index 000000000..d2d0c5a0d --- /dev/null +++ b/examples/tpch/q22_global_sales_opportunity.py @@ -0,0 +1,76 @@ +# 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. + +""" +TPC-H Problem Statement Query 22: + +This query counts how many customers within a specific range of country codes have not placed +orders for 7 years but who have a greater than average “positive” account balance. It also reflects +the magnitude of that balance. Country code is defined as the first two characters of c_phone. + +The above problem statement text is copyrighted by the Transaction Processing Performance Council +as part of their TPC Benchmark H Specification revision 2.18.0. +""" + +from datafusion import SessionContext, WindowFrame, col, lit, functions as F + +NATION_CODE = 13 + +# Load the dataframes we need + +ctx = SessionContext() + +df_customer = ctx.read_parquet("data/customer.parquet").select_columns( + "c_phone", "c_acctbal", "c_custkey" +) +df_orders = ctx.read_parquet("data/orders.parquet").select_columns("o_custkey") + +# The nation code is a two digit number, but we need to convert it to a string literal +nation_code = lit(str(NATION_CODE)) + +# Use the substring operation to extract the first two charaters of the phone number +df = df_customer.with_column("cntrycode", F.substr(col("c_phone"), lit(0), lit(3))) + +# Limit our search to customers with some balance and in the country code above +df = df.filter(col("c_acctbal") > lit(0.0)) +df = df.filter(nation_code == col("cntrycode")) + +# Compute the average balance. By default, the window frame is from unbounded preceeding to the +# current row. We want our frame to cover the entire data frame. +window_frame = WindowFrame("rows", None, None) +df = df.with_column( + "avg_balance", F.window("avg", [col("c_acctbal")], window_frame=window_frame) +) + +# Limit results to customers with above average balance +df = df.filter(col("c_acctbal") > col("avg_balance")) + +# Limit results to customers with no orders +df = df.join(df_orders, (["c_custkey"], ["o_custkey"]), "anti") + +# Count up the customers and the balances +df = df.aggregate( + [col("cntrycode")], + [ + F.count(col("c_custkey")).alias("numcust"), + F.sum(col("c_acctbal")).alias("totacctbal"), + ], +) + +df = df.sort(col("cntrycode").sort()) + +df.show() From d41eba47ab5b88a8e94a83f7e22a863710c9d28b Mon Sep 17 00:00:00 2001 From: Michael J Ward Date: Mon, 13 May 2024 13:04:03 -0500 Subject: [PATCH 006/181] ci: clean conda cache before building the packages (#689) --- .github/workflows/conda.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml index f8474565d..c33ae551f 100644 --- a/.github/workflows/conda.yml +++ b/.github/workflows/conda.yml @@ -81,6 +81,9 @@ jobs: which python pip list mamba list + # Clean the conda cache + - name: Clean Conda Cache + run: conda clean --all --yes - name: Build conda packages run: | # suffix for nightly package versions From 01a370e69931311db6ce08337aaefb309a668c3c Mon Sep 17 00:00:00 2001 From: Michael J Ward Date: Tue, 14 May 2024 08:40:58 -0500 Subject: [PATCH 007/181] Upgrade to datafusion 38 (#691) * chore: upgrade datafusion Deps Ref #690 * update concat and concat_ws to use datafusion_functions Moved in https://github.com/apache/datafusion/pull/10089 * feat: upgrade functions.rs Upstream is continuing it's migration to UDFs. Ref https://github.com/apache/datafusion/pull/10098 Ref https://github.com/apache/datafusion/pull/10372 * fix ScalarUDF import * feat: remove deprecated suppors_filter_pushdown and impl supports_filters_pushdown Deprecated function removed in https://github.com/apache/datafusion/pull/9923 * use `unnest_columns_with_options` instead of deprecated `unnest_column_with_option` * remove ScalarFunction wrappers These relied on upstream BuiltinScalarFunction, which are now removed. Ref https://github.com/apache/datafusion/pull/10098 * update dataframe `test_describe` `null_count` was fixed upstream. Ref https://github.com/apache/datafusion/pull/10260 * remove PyDFField and related methods DFField was removed upstream. Ref: https://github.com/apache/datafusion/pull/9595 * bump `datafusion-python` package version to 38.0.0 * re-implement `PyExpr::column_name` The previous implementation relied on `DFField` which was removed upstream. Ref: https://github.com/apache/datafusion/pull/9595 --- Cargo.lock | 145 +++++++++++++-------------- Cargo.toml | 16 +-- datafusion/__init__.py | 3 - datafusion/tests/test_dataframe.py | 6 +- datafusion/tests/test_imports.py | 7 +- src/common.rs | 2 - src/common/df_field.rs | 111 --------------------- src/dataframe.rs | 2 +- src/dataset.rs | 16 ++- src/expr.rs | 26 +++-- src/expr/scalar_function.rs | 65 ------------ src/functions.rs | 152 +++++++++++++++-------------- src/udf.rs | 2 +- 13 files changed, 181 insertions(+), 372 deletions(-) delete mode 100644 src/common/df_field.rs delete mode 100644 src/expr/scalar_function.rs diff --git a/Cargo.lock b/Cargo.lock index 5eb791b46..6b4568b96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,9 +733,9 @@ dependencies = [ [[package]] name = "datafusion" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85069782056753459dc47e386219aa1fdac5b731f26c28abb8c0ffd4b7c5ab11" +checksum = "05fb4eeeb7109393a0739ac5b8fd892f95ccef691421491c85544f7997366f68" dependencies = [ "ahash", "apache-avro", @@ -754,6 +754,7 @@ dependencies = [ "datafusion-execution", "datafusion-expr", "datafusion-functions", + "datafusion-functions-aggregate", "datafusion-functions-array", "datafusion-optimizer", "datafusion-physical-expr", @@ -786,9 +787,9 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "309d9040751f6dc9e33c85dce6abb55a46ef7ea3644577dd014611c379447ef3" +checksum = "741aeac15c82f239f2fc17deccaab19873abbd62987be20023689b15fa72fa09" dependencies = [ "ahash", "apache-avro", @@ -809,18 +810,18 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e4a44d8ef1b1e85d32234e6012364c411c3787859bb3bba893b0332cb03dfd" +checksum = "6e8ddfb8d8cb51646a30da0122ecfffb81ca16919ae9a3495a9e7468bdcd52b8" dependencies = [ "tokio", ] [[package]] name = "datafusion-execution" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a3a29ae36bcde07d179cc33b45656a8e7e4d023623e320e48dcf1200eeee95" +checksum = "282122f90b20e8f98ebfa101e4bf20e718fd2684cf81bef4e8c6366571c64404" dependencies = [ "arrow", "chrono", @@ -839,9 +840,9 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3542aa322029c2121a671ce08000d4b274171070df13f697b14169ccf4f628" +checksum = "5478588f733df0dfd87a62671c7478f590952c95fa2fa5c137e3ff2929491e22" dependencies = [ "ahash", "arrow", @@ -849,6 +850,7 @@ dependencies = [ "chrono", "datafusion-common", "paste", + "serde_json", "sqlparser", "strum 0.26.1", "strum_macros 0.26.1", @@ -856,9 +858,9 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd221792c666eac174ecc09e606312844772acc12cbec61a420c2fca1ee70959" +checksum = "f4afd261cea6ac9c3ca1192fd5e9f940596d8e9208c5b1333f4961405db53185" dependencies = [ "arrow", "base64 0.22.1", @@ -869,21 +871,39 @@ dependencies = [ "datafusion-execution", "datafusion-expr", "datafusion-physical-expr", + "hashbrown 0.14.3", "hex", "itertools 0.12.0", "log", "md-5", + "rand", "regex", "sha2", "unicode-segmentation", "uuid", ] +[[package]] +name = "datafusion-functions-aggregate" +version = "38.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b36a6c4838ab94b5bf8f7a96ce6ce059d805c5d1dcaa6ace49e034eb65cd999" +dependencies = [ + "arrow", + "datafusion-common", + "datafusion-execution", + "datafusion-expr", + "datafusion-physical-expr-common", + "log", + "paste", + "sqlparser", +] + [[package]] name = "datafusion-functions-array" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e501801e84d9c6ef54caaebcda1b18a6196a24176c12fb70e969bc0572e03c55" +checksum = "d5fdd200a6233f48d3362e7ccb784f926f759100e44ae2137a5e2dcb986a59c4" dependencies = [ "arrow", "arrow-array", @@ -901,9 +921,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bd7f5087817deb961764e8c973d243b54f8572db414a8f0a8f33a48f991e0a" +checksum = "54f2820938810e8a2d71228fd6f59f33396aebc5f5f687fcbf14de5aab6a7e1a" dependencies = [ "arrow", "async-trait", @@ -912,6 +932,7 @@ dependencies = [ "datafusion-expr", "datafusion-physical-expr", "hashbrown 0.14.3", + "indexmap", "itertools 0.12.0", "log", "regex-syntax", @@ -919,9 +940,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cabc0d9aaa0f5eb1b472112f16223c9ffd2fb04e58cbf65c0a331ee6e993f96" +checksum = "9adf8eb12716f52ddf01e09eb6c94d3c9b291e062c05c91b839a448bddba2ff8" dependencies = [ "ahash", "arrow", @@ -931,37 +952,45 @@ dependencies = [ "arrow-schema", "arrow-string", "base64 0.22.1", - "blake2", - "blake3", "chrono", "datafusion-common", "datafusion-execution", "datafusion-expr", + "datafusion-functions-aggregate", + "datafusion-physical-expr-common", "half", "hashbrown 0.14.3", "hex", "indexmap", "itertools 0.12.0", "log", - "md-5", "paste", "petgraph", - "rand", "regex", - "sha2", - "unicode-segmentation", +] + +[[package]] +name = "datafusion-physical-expr-common" +version = "38.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5472c3230584c150197b3f2c23f2392b9dc54dbfb62ad41e7e36447cfce4be" +dependencies = [ + "arrow", + "datafusion-common", + "datafusion-expr", ] [[package]] name = "datafusion-physical-plan" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c0523e9c8880f2492a88bbd857dde02bed1ed23f3e9211a89d3d7ec3b44af9" +checksum = "18ae750c38389685a8b62e5b899bbbec488950755ad6d218f3662d35b800c4fe" dependencies = [ "ahash", "arrow", "arrow-array", "arrow-buffer", + "arrow-ord", "arrow-schema", "async-trait", "chrono", @@ -969,7 +998,9 @@ dependencies = [ "datafusion-common-runtime", "datafusion-execution", "datafusion-expr", + "datafusion-functions-aggregate", "datafusion-physical-expr", + "datafusion-physical-expr-common", "futures", "half", "hashbrown 0.14.3", @@ -985,7 +1016,7 @@ dependencies = [ [[package]] name = "datafusion-python" -version = "37.1.0" +version = "38.0.0" dependencies = [ "async-trait", "datafusion", @@ -1013,9 +1044,9 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb54b42227136f6287573f2434b1de249fe1b8e6cd6cc73a634e4a3ec29356" +checksum = "befc67a3cdfbfa76853f43b10ac27337821bb98e519ab6baf431fcc0bcfcafdb" dependencies = [ "arrow", "arrow-array", @@ -1029,9 +1060,9 @@ dependencies = [ [[package]] name = "datafusion-substrait" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3b496697ac22a857c7d497b9d6b40edec19ed2e3e86e2b77051541fefb4c6d" +checksum = "1f62542caa77df003e23a8bc2f1b8a1ffc682fe447c7fcb4905d109e3d7a5b9d" dependencies = [ "async-recursion", "chrono", @@ -1260,19 +1291,6 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "git2" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" -dependencies = [ - "bitflags 2.4.2", - "libc", - "libgit2-sys", - "log", - "url", -] - [[package]] name = "glob" version = "0.3.1" @@ -1654,18 +1672,6 @@ dependencies = [ "rle-decode-fast", ] -[[package]] -name = "libgit2-sys" -version = "0.16.2+1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - [[package]] name = "libm" version = "0.2.8" @@ -1682,18 +1688,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libz-sys" -version = "1.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295c17e837573c8c821dbaeb3cceb3d745ad082f7572191409e69cbc1b3fd050" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -2762,9 +2756,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "sqlparser" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf9c7ff146298ffda83a200f8d5084f08dcee1edfc135fcc1d646a45d50ffd6" +checksum = "f7bbffee862a796d67959a89859d6b1046bb5016d63e23835ad0da182777bbe0" dependencies = [ "log", "sqlparser_derive", @@ -2830,11 +2824,10 @@ dependencies = [ [[package]] name = "substrait" -version = "0.28.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9531ae6784dee4c018ebdb0226872b63cc28765bfa65c1e53b6c58584232af" +checksum = "f01344023c2614171a9ffd6e387eea14e12f7387c5b6adb33f1563187d65e376" dependencies = [ - "git2", "heck 0.5.0", "prettyplease", "prost", @@ -3221,12 +3214,6 @@ dependencies = [ "serde", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 9da36d710..cde3be222 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ [package] name = "datafusion-python" -version = "37.1.0" +version = "38.0.0" homepage = "https://datafusion.apache.org/python" repository = "https://github.com/apache/datafusion-python" authors = ["Apache DataFusion "] @@ -37,13 +37,13 @@ substrait = ["dep:datafusion-substrait"] tokio = { version = "1.35", features = ["macros", "rt", "rt-multi-thread", "sync"] } rand = "0.8" pyo3 = { version = "0.20", features = ["extension-module", "abi3", "abi3-py38"] } -datafusion = { version = "37.1.0", features = ["pyarrow", "avro", "unicode_expressions"] } -datafusion-common = { version = "37.1.0", features = ["pyarrow"] } -datafusion-expr = "37.1.0" -datafusion-functions-array = "37.1.0" -datafusion-optimizer = "37.1.0" -datafusion-sql = "37.1.0" -datafusion-substrait = { version = "37.1.0", optional = true } +datafusion = { version = "38.0.0", features = ["pyarrow", "avro", "unicode_expressions"] } +datafusion-common = { version = "38.0.0", features = ["pyarrow"] } +datafusion-expr = "38.0.0" +datafusion-functions-array = "38.0.0" +datafusion-optimizer = "38.0.0" +datafusion-sql = "38.0.0" +datafusion-substrait = { version = "38.0.0", optional = true } prost = "0.12" prost-types = "0.12" uuid = { version = "1.8", features = ["v4"] } diff --git a/datafusion/__init__.py b/datafusion/__init__.py index c50bf649d..d0b823bbd 100644 --- a/datafusion/__init__.py +++ b/datafusion/__init__.py @@ -37,7 +37,6 @@ ) from .common import ( - DFField, DFSchema, ) @@ -64,8 +63,6 @@ IsNotFalse, IsNotUnknown, Negative, - ScalarFunction, - BuiltinScalarFunction, InList, Exists, Subquery, diff --git a/datafusion/tests/test_dataframe.py b/datafusion/tests/test_dataframe.py index efb1679b9..2f6a818ea 100644 --- a/datafusion/tests/test_dataframe.py +++ b/datafusion/tests/test_dataframe.py @@ -730,9 +730,9 @@ def test_describe(df): "max", "median", ], - "a": [3.0, 3.0, 2.0, 1.0, 1.0, 3.0, 2.0], - "b": [3.0, 3.0, 5.0, 1.0, 4.0, 6.0, 5.0], - "c": [3.0, 3.0, 7.0, 1.7320508075688772, 5.0, 8.0, 8.0], + "a": [3.0, 0.0, 2.0, 1.0, 1.0, 3.0, 2.0], + "b": [3.0, 0.0, 5.0, 1.0, 4.0, 6.0, 5.0], + "c": [3.0, 0.0, 7.0, 1.7320508075688772, 5.0, 8.0, 8.0], } diff --git a/datafusion/tests/test_imports.py b/datafusion/tests/test_imports.py index 766ddce89..2a8a3de83 100644 --- a/datafusion/tests/test_imports.py +++ b/datafusion/tests/test_imports.py @@ -27,7 +27,6 @@ ) from datafusion.common import ( - DFField, DFSchema, ) @@ -64,8 +63,6 @@ IsNotFalse, IsNotUnknown, Negative, - ScalarFunction, - BuiltinScalarFunction, InList, Exists, Subquery, @@ -139,8 +136,6 @@ def test_class_module_is_datafusion(): IsNotFalse, IsNotUnknown, Negative, - ScalarFunction, - BuiltinScalarFunction, InList, Exists, Subquery, @@ -165,7 +160,7 @@ def test_class_module_is_datafusion(): assert klass.__module__ == "datafusion.expr" # schema - for klass in [DFField, DFSchema]: + for klass in [DFSchema]: assert klass.__module__ == "datafusion.common" diff --git a/src/common.rs b/src/common.rs index 45523173c..682639aca 100644 --- a/src/common.rs +++ b/src/common.rs @@ -18,7 +18,6 @@ use pyo3::prelude::*; pub mod data_type; -pub mod df_field; pub mod df_schema; pub mod function; pub mod schema; @@ -26,7 +25,6 @@ pub mod schema; /// Initializes the `common` module to match the pattern of `datafusion-common` https://docs.rs/datafusion-common/18.0.0/datafusion_common/index.html pub(crate) fn init_module(m: &PyModule) -> PyResult<()> { m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/common/df_field.rs b/src/common/df_field.rs deleted file mode 100644 index 68c05361f..000000000 --- a/src/common/df_field.rs +++ /dev/null @@ -1,111 +0,0 @@ -// 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. - -use datafusion::arrow::datatypes::DataType; -use datafusion_common::{DFField, OwnedTableReference}; -use pyo3::prelude::*; - -use super::data_type::PyDataType; - -/// PyDFField wraps an arrow-datafusion `DFField` struct type -/// and also supplies convenience methods for interacting -/// with the `DFField` instance in the context of Python -#[pyclass(name = "DFField", module = "datafusion.common", subclass)] -#[derive(Debug, Clone)] -pub struct PyDFField { - pub field: DFField, -} - -impl From for DFField { - fn from(py_field: PyDFField) -> DFField { - py_field.field - } -} - -impl From for PyDFField { - fn from(field: DFField) -> PyDFField { - PyDFField { field } - } -} - -#[pymethods] -impl PyDFField { - #[new] - #[pyo3(signature = (qualifier=None, name="", data_type=DataType::Int64.into(), nullable=false))] - fn new(qualifier: Option, name: &str, data_type: PyDataType, nullable: bool) -> Self { - PyDFField { - field: DFField::new( - qualifier.map(OwnedTableReference::from), - name, - data_type.into(), - nullable, - ), - } - } - - // TODO: Need bindings for Array `Field` first - // #[staticmethod] - // #[pyo3(name = "from")] - // fn py_from(field: Field) -> Self {} - - // TODO: Need bindings for Array `Field` first - // #[staticmethod] - // #[pyo3(name = "from_qualified")] - // fn py_from_qualified(field: Field) -> Self {} - - #[pyo3(name = "name")] - fn py_name(&self) -> PyResult { - Ok(self.field.name().clone()) - } - - #[pyo3(name = "data_type")] - fn py_data_type(&self) -> PyResult { - Ok(self.field.data_type().clone().into()) - } - - #[pyo3(name = "is_nullable")] - fn py_is_nullable(&self) -> PyResult { - Ok(self.field.is_nullable()) - } - - #[pyo3(name = "qualified_name")] - fn py_qualified_name(&self) -> PyResult { - Ok(self.field.qualified_name()) - } - - // TODO: Need bindings for `Column` first - // #[pyo3(name = "qualified_column")] - // fn py_qualified_column(&self) -> PyResult {} - - // TODO: Need bindings for `Column` first - // #[pyo3(name = "unqualified_column")] - // fn py_unqualified_column(&self) -> PyResult {} - - #[pyo3(name = "qualifier")] - fn py_qualifier(&self) -> PyResult> { - Ok(self.field.qualifier().map(|q| format!("{}", q))) - } - - // TODO: Need bindings for Arrow `Field` first - // #[pyo3(name = "field")] - // fn py_field(&self) -> PyResult {} - - #[pyo3(name = "strip_qualifier")] - fn py_strip_qualifier(&self) -> PyResult { - Ok(self.field.clone().strip_qualifier().into()) - } -} diff --git a/src/dataframe.rs b/src/dataframe.rs index f1efc0c7a..8f4514398 100644 --- a/src/dataframe.rs +++ b/src/dataframe.rs @@ -301,7 +301,7 @@ impl PyDataFrame { .df .as_ref() .clone() - .unnest_column_with_options(column, unnest_options)?; + .unnest_columns_with_options(&[column], unnest_options)?; Ok(Self::new(df)) } diff --git a/src/dataset.rs b/src/dataset.rs index 713610c51..fcbb503c0 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -117,10 +117,16 @@ impl TableProvider for Dataset { /// Tests whether the table provider can make use of a filter expression /// to optimise data retrieval. - fn supports_filter_pushdown(&self, filter: &Expr) -> DFResult { - match PyArrowFilterExpression::try_from(filter) { - Ok(_) => Ok(TableProviderFilterPushDown::Exact), - _ => Ok(TableProviderFilterPushDown::Unsupported), - } + fn supports_filters_pushdown( + &self, + filter: &[&Expr], + ) -> DFResult> { + filter + .iter() + .map(|&f| match PyArrowFilterExpression::try_from(f) { + Ok(_) => Ok(TableProviderFilterPushDown::Exact), + _ => Ok(TableProviderFilterPushDown::Unsupported), + }) + .collect() } } diff --git a/src/expr.rs b/src/expr.rs index 3be0d025c..2f1477457 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -15,20 +15,20 @@ // specific language governing permissions and limitations // under the License. +use datafusion_expr::utils::exprlist_to_fields; +use datafusion_expr::LogicalPlan; use pyo3::{basic::CompareOp, prelude::*}; use std::convert::{From, Into}; +use std::sync::Arc; -use datafusion::arrow::datatypes::DataType; +use datafusion::arrow::datatypes::{DataType, Field}; use datafusion::arrow::pyarrow::PyArrowType; use datafusion::scalar::ScalarValue; -use datafusion_common::DFField; use datafusion_expr::{ col, expr::{AggregateFunction, InList, InSubquery, ScalarFunction, Sort, WindowFunction}, - lit, - utils::exprlist_to_fields, - Between, BinaryExpr, Case, Cast, Expr, GetFieldAccess, GetIndexedField, Like, LogicalPlan, - Operator, TryCast, + lit, Between, BinaryExpr, Case, Cast, Expr, GetFieldAccess, GetIndexedField, Like, Operator, + TryCast, }; use crate::common::data_type::{DataTypeMap, RexType}; @@ -80,7 +80,6 @@ pub mod logical_node; pub mod placeholder; pub mod projection; pub mod repartition; -pub mod scalar_function; pub mod scalar_subquery; pub mod scalar_variable; pub mod signature; @@ -567,14 +566,14 @@ impl PyExpr { impl PyExpr { pub fn _column_name(&self, plan: &LogicalPlan) -> Result { let field = Self::expr_to_field(&self.expr, plan)?; - Ok(field.qualified_column().flat_name()) + Ok(field.name().to_owned()) } - /// Create a [DFField] representing an [Expr], given an input [LogicalPlan] to resolve against + /// Create a [Field] representing an [Expr], given an input [LogicalPlan] to resolve against pub fn expr_to_field( expr: &Expr, input_plan: &LogicalPlan, - ) -> Result { + ) -> Result, DataFusionError> { match expr { Expr::Sort(Sort { expr, .. }) => { // DataFusion does not support create_name for sort expressions (since they never @@ -583,16 +582,15 @@ impl PyExpr { } Expr::Wildcard { .. } => { // Since * could be any of the valid column names just return the first one - Ok(input_plan.schema().field(0).clone()) + Ok(Arc::new(input_plan.schema().field(0).clone())) } _ => { let fields = exprlist_to_fields(&[expr.clone()], input_plan).map_err(PyErr::from)?; - Ok(fields[0].clone()) + Ok(fields[0].1.clone()) } } } - fn _types(expr: &Expr) -> PyResult { match expr { Expr::BinaryExpr(BinaryExpr { @@ -665,8 +663,6 @@ pub(crate) fn init_module(m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/expr/scalar_function.rs b/src/expr/scalar_function.rs deleted file mode 100644 index 776ca3297..000000000 --- a/src/expr/scalar_function.rs +++ /dev/null @@ -1,65 +0,0 @@ -// 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. - -use crate::expr::PyExpr; -use datafusion_expr::{BuiltinScalarFunction, Expr}; -use pyo3::prelude::*; - -#[pyclass(name = "ScalarFunction", module = "datafusion.expr", subclass)] -#[derive(Clone)] -pub struct PyScalarFunction { - scalar_function: BuiltinScalarFunction, - args: Vec, -} - -impl PyScalarFunction { - pub fn new(scalar_function: BuiltinScalarFunction, args: Vec) -> Self { - Self { - scalar_function, - args, - } - } -} - -#[pyclass(name = "BuiltinScalarFunction", module = "datafusion.expr", subclass)] -#[derive(Clone)] -pub struct PyBuiltinScalarFunction { - scalar_function: BuiltinScalarFunction, -} - -impl From for PyBuiltinScalarFunction { - fn from(scalar_function: BuiltinScalarFunction) -> PyBuiltinScalarFunction { - PyBuiltinScalarFunction { scalar_function } - } -} - -impl From for BuiltinScalarFunction { - fn from(scalar_function: PyBuiltinScalarFunction) -> Self { - scalar_function.scalar_function - } -} - -#[pymethods] -impl PyScalarFunction { - fn fun(&self) -> PyResult { - Ok(self.scalar_function.into()) - } - - fn args(&self) -> PyResult> { - Ok(self.args.iter().map(|e| e.clone().into()).collect()) - } -} diff --git a/src/functions.rs b/src/functions.rs index 7f6b1a877..4b137d90d 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -24,17 +24,46 @@ use crate::expr::window::PyWindowFrame; use crate::expr::PyExpr; use datafusion::execution::FunctionRegistry; use datafusion::functions; +use datafusion::functions_aggregate; use datafusion_common::{Column, ScalarValue, TableReference}; use datafusion_expr::expr::Alias; use datafusion_expr::{ aggregate_function, expr::{ - find_df_window_func, AggregateFunction, AggregateFunctionDefinition, ScalarFunction, Sort, - WindowFunction, + find_df_window_func, AggregateFunction, AggregateFunctionDefinition, Sort, WindowFunction, }, - lit, BuiltinScalarFunction, Expr, WindowFunctionDefinition, + lit, Expr, WindowFunctionDefinition, }; +#[pyfunction] +#[pyo3(signature = (y, x, distinct = false, filter = None, order_by = None))] +pub fn covar_samp( + y: PyExpr, + x: PyExpr, + distinct: bool, + filter: Option, + order_by: Option>, + // null_treatment: Option, +) -> PyExpr { + let filter = filter.map(|x| Box::new(x.expr)); + let order_by = order_by.map(|x| x.into_iter().map(|x| x.expr).collect::>()); + functions_aggregate::expr_fn::covar_samp(y.expr, x.expr, distinct, filter, order_by, None) + .into() +} + +#[pyfunction] +#[pyo3(signature = (y, x, distinct = false, filter = None, order_by = None))] +pub fn covar( + y: PyExpr, + x: PyExpr, + distinct: bool, + filter: Option, + order_by: Option>, +) -> PyExpr { + // alias for covar_samp + covar_samp(y, x, distinct, filter, order_by) +} + #[pyfunction] fn in_list(expr: PyExpr, value: Vec, negated: bool) -> PyExpr { datafusion_expr::in_list( @@ -134,7 +163,7 @@ fn digest(value: PyExpr, method: PyExpr) -> PyExpr { #[pyo3(signature = (*args))] fn concat(args: Vec) -> PyResult { let args = args.into_iter().map(|e| e.expr).collect::>(); - Ok(datafusion_expr::concat(&args).into()) + Ok(functions::string::expr_fn::concat(args).into()) } /// Concatenates all but the first argument, with separators. @@ -144,7 +173,7 @@ fn concat(args: Vec) -> PyResult { #[pyo3(signature = (sep, *args))] fn concat_ws(sep: String, args: Vec) -> PyResult { let args = args.into_iter().map(|e| e.expr).collect::>(); - Ok(datafusion_expr::concat_ws(lit(sep), args).into()) + Ok(functions::string::expr_fn::concat_ws(lit(sep), args).into()) } /// Creates a new Sort Expr @@ -249,27 +278,6 @@ fn window( }) } -macro_rules! scalar_function { - ($NAME: ident, $FUNC: ident) => { - scalar_function!($NAME, $FUNC, stringify!($NAME)); - }; - - ($NAME: ident, $FUNC: ident, $DOC: expr) => { - #[doc = $DOC] - #[pyfunction] - #[pyo3(signature = (*args))] - fn $NAME(args: Vec) -> PyExpr { - let expr = datafusion_expr::Expr::ScalarFunction(ScalarFunction { - func_def: datafusion_expr::ScalarFunctionDefinition::BuiltIn( - BuiltinScalarFunction::$FUNC, - ), - args: args.into_iter().map(|e| e.into()).collect(), - }); - expr.into() - } - }; -} - macro_rules! aggregate_function { ($NAME: ident, $FUNC: ident) => { aggregate_function!($NAME, $FUNC, stringify!($NAME)); @@ -370,21 +378,21 @@ macro_rules! array_fn { expr_fn!(abs, num); expr_fn!(acos, num); -scalar_function!(acosh, Acosh); +expr_fn!(acosh, num); expr_fn!(ascii, arg1, "Returns the numeric code of the first character of the argument. In UTF8 encoding, returns the Unicode code point of the character. In other multibyte encodings, the argument must be an ASCII character."); expr_fn!(asin, num); -scalar_function!(asinh, Asinh); -scalar_function!(atan, Atan); -scalar_function!(atanh, Atanh); -scalar_function!(atan2, Atan2); +expr_fn!(asinh, num); +expr_fn!(atan, num); +expr_fn!(atanh, num); +expr_fn!(atan2, y x); expr_fn!( bit_length, arg, "Returns number of bits in the string (8 times the octet_length)." ); expr_fn_vec!(btrim, "Removes the longest string containing only characters in characters (a space by default) from the start and end of string."); -scalar_function!(cbrt, Cbrt); -scalar_function!(ceil, Ceil); +expr_fn!(cbrt, num); +expr_fn!(ceil, num); expr_fn!( character_length, string, @@ -393,44 +401,44 @@ expr_fn!( expr_fn!(length, string); expr_fn!(char_length, string); expr_fn!(chr, arg, "Returns the character with the given code."); -scalar_function!(coalesce, Coalesce); -scalar_function!(cos, Cos); -scalar_function!(cosh, Cosh); -scalar_function!(degrees, Degrees); +expr_fn_vec!(coalesce); +expr_fn!(cos, num); +expr_fn!(cosh, num); +expr_fn!(degrees, num); expr_fn!(decode, input encoding); expr_fn!(encode, input encoding); -scalar_function!(exp, Exp); -scalar_function!(factorial, Factorial); -scalar_function!(floor, Floor); -scalar_function!(gcd, Gcd); -scalar_function!(initcap, InitCap, "Converts the first letter of each word to upper case and the rest to lower case. Words are sequences of alphanumeric characters separated by non-alphanumeric characters."); +expr_fn!(exp, num); +expr_fn!(factorial, num); +expr_fn!(floor, num); +expr_fn!(gcd, x y); +expr_fn!(initcap, string, "Converts the first letter of each word to upper case and the rest to lower case. Words are sequences of alphanumeric characters separated by non-alphanumeric characters."); expr_fn!(isnan, num); -scalar_function!(iszero, Iszero); -scalar_function!(lcm, Lcm); -scalar_function!(left, Left, "Returns first n characters in the string, or when n is negative, returns all but last |n| characters."); -scalar_function!(ln, Ln); -scalar_function!(log, Log); -scalar_function!(log10, Log10); -scalar_function!(log2, Log2); +expr_fn!(iszero, num); +expr_fn!(lcm, x y); +expr_fn!(left, string n, "Returns first n characters in the string, or when n is negative, returns all but last |n| characters."); +expr_fn!(ln, num); +expr_fn!(log, base num); +expr_fn!(log10, num); +expr_fn!(log2, num); expr_fn!(lower, arg1, "Converts the string to all lower case"); -scalar_function!(lpad, Lpad, "Extends the string to length length by prepending the characters fill (a space by default). If the string is already longer than length then it is truncated (on the right)."); +expr_fn_vec!(lpad, "Extends the string to length length by prepending the characters fill (a space by default). If the string is already longer than length then it is truncated (on the right)."); expr_fn_vec!(ltrim, "Removes the longest string containing only characters in characters (a space by default) from the start of string."); expr_fn!( md5, input_arg, "Computes the MD5 hash of the argument, with the result written in hexadecimal." ); -scalar_function!( +expr_fn!( nanvl, - Nanvl, + x y, "Returns x if x is not NaN otherwise returns y." ); expr_fn!(nullif, arg_1 arg_2); expr_fn_vec!(octet_length, "Returns number of bytes in the string. Since this version of the function accepts type character directly, it will not strip trailing spaces."); -scalar_function!(pi, Pi); -scalar_function!(power, Power); -scalar_function!(pow, Power); -scalar_function!(radians, Radians); +expr_fn!(pi); +expr_fn!(power, base exponent); +expr_fn!(pow, power, base exponent); +expr_fn!(radians, num); expr_fn!(regexp_match, input_arg1 input_arg2); expr_fn!( regexp_replace, @@ -443,31 +451,31 @@ expr_fn!( string from to, "Replaces all occurrences in string of substring from with substring to." ); -scalar_function!( +expr_fn!( reverse, - Reverse, + string, "Reverses the order of the characters in the string." ); -scalar_function!(right, Right, "Returns last n characters in the string, or when n is negative, returns all but first |n| characters."); -scalar_function!(round, Round); -scalar_function!(rpad, Rpad, "Extends the string to length length by appending the characters fill (a space by default). If the string is already longer than length then it is truncated."); +expr_fn!(right, string n, "Returns last n characters in the string, or when n is negative, returns all but first |n| characters."); +expr_fn_vec!(round); +expr_fn_vec!(rpad, "Extends the string to length length by appending the characters fill (a space by default). If the string is already longer than length then it is truncated."); expr_fn_vec!(rtrim, "Removes the longest string containing only characters in characters (a space by default) from the end of string."); expr_fn!(sha224, input_arg1); expr_fn!(sha256, input_arg1); expr_fn!(sha384, input_arg1); expr_fn!(sha512, input_arg1); -scalar_function!(signum, Signum); -scalar_function!(sin, Sin); -scalar_function!(sinh, Sinh); +expr_fn!(signum, num); +expr_fn!(sin, num); +expr_fn!(sinh, num); expr_fn!( split_part, string delimiter index, "Splits string at occurrences of delimiter and returns the n'th field (counting from one)." ); -scalar_function!(sqrt, Sqrt); +expr_fn!(sqrt, num); expr_fn!(starts_with, arg1 arg2, "Returns true if string starts with prefix."); -scalar_function!(strpos, Strpos, "Returns starting index of specified substring within string, or zero if it's not present. (Same as position(substring in string), but note the reversed argument order.)"); -scalar_function!(substr, Substr); +expr_fn!(strpos, string substring, "Returns starting index of specified substring within string, or zero if it's not present. (Same as position(substring in string), but note the reversed argument order.)"); +expr_fn!(substr, string position); expr_fn!(tan, num); expr_fn!(tanh, num); expr_fn!( @@ -488,15 +496,15 @@ expr_fn!(date_trunc, part date); expr_fn!(datetrunc, date_trunc, part date); expr_fn!(date_bin, stride source origin); -scalar_function!(translate, Translate, "Replaces each character in string that matches a character in the from set with the corresponding character in the to set. If from is longer than to, occurrences of the extra characters in from are deleted."); +expr_fn!(translate, string from to, "Replaces each character in string that matches a character in the from set with the corresponding character in the to set. If from is longer than to, occurrences of the extra characters in from are deleted."); expr_fn_vec!(trim, "Removes the longest string containing only characters in characters (a space by default) from the start, end, or both ends (BOTH is the default) of string."); -scalar_function!(trunc, Trunc); +expr_fn_vec!(trunc); expr_fn!(upper, arg1, "Converts the string to all upper case."); expr_fn!(uuid); -expr_fn!(r#struct, args); // Use raw identifier since struct is a keyword +expr_fn_vec!(r#struct); // Use raw identifier since struct is a keyword expr_fn!(from_unixtime, unixtime); expr_fn!(arrow_typeof, arg_1); -scalar_function!(random, Random); +expr_fn!(random); // Array Functions array_fn!(array_append, array element); @@ -565,9 +573,7 @@ aggregate_function!(array_agg, ArrayAgg); aggregate_function!(avg, Avg); aggregate_function!(corr, Correlation); aggregate_function!(count, Count); -aggregate_function!(covar, Covariance); aggregate_function!(covar_pop, CovariancePop); -aggregate_function!(covar_samp, Covariance); aggregate_function!(grouping, Grouping); aggregate_function!(max, Max); aggregate_function!(mean, Avg); diff --git a/src/udf.rs b/src/udf.rs index 69519f499..8f5ca30b1 100644 --- a/src/udf.rs +++ b/src/udf.rs @@ -23,9 +23,9 @@ use datafusion::arrow::array::{make_array, Array, ArrayData, ArrayRef}; use datafusion::arrow::datatypes::DataType; use datafusion::arrow::pyarrow::{FromPyArrow, PyArrowType, ToPyArrow}; use datafusion::error::DataFusionError; -use datafusion::physical_plan::udf::ScalarUDF; use datafusion_expr::create_udf; use datafusion_expr::function::ScalarFunctionImplementation; +use datafusion_expr::ScalarUDF; use crate::expr::PyExpr; use crate::utils::parse_volatility; From 0c82b3fefa844783f5296bbeafb714b187c63ebb Mon Sep 17 00:00:00 2001 From: Michael J Ward Date: Tue, 14 May 2024 13:31:56 -0500 Subject: [PATCH 008/181] =?UTF-8?q?chore:=20update=20to=20maturin's=20reco?= =?UTF-8?q?mmended=20project=20layout=20for=20rust/python=E2=80=A6=20(#695?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update to maturin's recommended project layout for rust/python projects The previous layout leads to an import error when installing with `maturin build` and `pip install .`. This error was common enough that `maturin` changed the recommended project layout to what this commit does. A prior PR attempted to solve this by altering `lib.name` in Cargo.toml, but that did not work for me. - [Prior PR](https://github.com/apache/datafusion-python/pull/694) - [maturin ImportError issue](https://github.com/PyO3/maturin/issues/490) - [maturin changes recommended project structure](https://github.com/PyO3/maturin/pull/855) * ci: update `ruff check` for nested python directory --- .github/workflows/build.yml | 2 +- pyproject.toml | 1 + {datafusion => python/datafusion}/__init__.py | 0 {datafusion => python/datafusion}/common.py | 0 {datafusion => python/datafusion}/expr.py | 0 {datafusion => python/datafusion}/functions.py | 0 {datafusion => python/datafusion}/input/__init__.py | 0 {datafusion => python/datafusion}/input/base.py | 0 {datafusion => python/datafusion}/input/location.py | 0 {datafusion => python/datafusion}/object_store.py | 0 {datafusion => python/datafusion}/substrait.py | 0 {datafusion => python/datafusion}/tests/__init__.py | 0 {datafusion => python/datafusion}/tests/conftest.py | 0 .../datafusion}/tests/data_test_context/data.json | 0 {datafusion => python/datafusion}/tests/generic.py | 0 {datafusion => python/datafusion}/tests/test_aggregation.py | 0 {datafusion => python/datafusion}/tests/test_catalog.py | 0 {datafusion => python/datafusion}/tests/test_config.py | 0 {datafusion => python/datafusion}/tests/test_context.py | 0 {datafusion => python/datafusion}/tests/test_dataframe.py | 0 {datafusion => python/datafusion}/tests/test_expr.py | 0 {datafusion => python/datafusion}/tests/test_functions.py | 0 {datafusion => python/datafusion}/tests/test_imports.py | 0 {datafusion => python/datafusion}/tests/test_indexing.py | 0 {datafusion => python/datafusion}/tests/test_input.py | 0 {datafusion => python/datafusion}/tests/test_sql.py | 0 {datafusion => python/datafusion}/tests/test_store.py | 0 {datafusion => python/datafusion}/tests/test_substrait.py | 0 {datafusion => python/datafusion}/tests/test_udaf.py | 0 29 files changed, 2 insertions(+), 1 deletion(-) rename {datafusion => python/datafusion}/__init__.py (100%) rename {datafusion => python/datafusion}/common.py (100%) rename {datafusion => python/datafusion}/expr.py (100%) rename {datafusion => python/datafusion}/functions.py (100%) rename {datafusion => python/datafusion}/input/__init__.py (100%) rename {datafusion => python/datafusion}/input/base.py (100%) rename {datafusion => python/datafusion}/input/location.py (100%) rename {datafusion => python/datafusion}/object_store.py (100%) rename {datafusion => python/datafusion}/substrait.py (100%) rename {datafusion => python/datafusion}/tests/__init__.py (100%) rename {datafusion => python/datafusion}/tests/conftest.py (100%) rename {datafusion => python/datafusion}/tests/data_test_context/data.json (100%) rename {datafusion => python/datafusion}/tests/generic.py (100%) rename {datafusion => python/datafusion}/tests/test_aggregation.py (100%) rename {datafusion => python/datafusion}/tests/test_catalog.py (100%) rename {datafusion => python/datafusion}/tests/test_config.py (100%) rename {datafusion => python/datafusion}/tests/test_context.py (100%) rename {datafusion => python/datafusion}/tests/test_dataframe.py (100%) rename {datafusion => python/datafusion}/tests/test_expr.py (100%) rename {datafusion => python/datafusion}/tests/test_functions.py (100%) rename {datafusion => python/datafusion}/tests/test_imports.py (100%) rename {datafusion => python/datafusion}/tests/test_indexing.py (100%) rename {datafusion => python/datafusion}/tests/test_input.py (100%) rename {datafusion => python/datafusion}/tests/test_sql.py (100%) rename {datafusion => python/datafusion}/tests/test_store.py (100%) rename {datafusion => python/datafusion}/tests/test_substrait.py (100%) rename {datafusion => python/datafusion}/tests/test_udaf.py (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e03c2cbde..239b1718b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: pip install ruff # Update output format to enable automatic inline annotations. - name: Run Ruff - run: ruff check --output-format=github datafusion + run: ruff check --output-format=github python/ generate-license: runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index d35360519..24bd29617 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ repository = "https://github.com/apache/arrow-datafusion-python" profile = "black" [tool.maturin] +python-source = "python" module-name = "datafusion._internal" include = [ { path = "Cargo.lock", format = "sdist" } diff --git a/datafusion/__init__.py b/python/datafusion/__init__.py similarity index 100% rename from datafusion/__init__.py rename to python/datafusion/__init__.py diff --git a/datafusion/common.py b/python/datafusion/common.py similarity index 100% rename from datafusion/common.py rename to python/datafusion/common.py diff --git a/datafusion/expr.py b/python/datafusion/expr.py similarity index 100% rename from datafusion/expr.py rename to python/datafusion/expr.py diff --git a/datafusion/functions.py b/python/datafusion/functions.py similarity index 100% rename from datafusion/functions.py rename to python/datafusion/functions.py diff --git a/datafusion/input/__init__.py b/python/datafusion/input/__init__.py similarity index 100% rename from datafusion/input/__init__.py rename to python/datafusion/input/__init__.py diff --git a/datafusion/input/base.py b/python/datafusion/input/base.py similarity index 100% rename from datafusion/input/base.py rename to python/datafusion/input/base.py diff --git a/datafusion/input/location.py b/python/datafusion/input/location.py similarity index 100% rename from datafusion/input/location.py rename to python/datafusion/input/location.py diff --git a/datafusion/object_store.py b/python/datafusion/object_store.py similarity index 100% rename from datafusion/object_store.py rename to python/datafusion/object_store.py diff --git a/datafusion/substrait.py b/python/datafusion/substrait.py similarity index 100% rename from datafusion/substrait.py rename to python/datafusion/substrait.py diff --git a/datafusion/tests/__init__.py b/python/datafusion/tests/__init__.py similarity index 100% rename from datafusion/tests/__init__.py rename to python/datafusion/tests/__init__.py diff --git a/datafusion/tests/conftest.py b/python/datafusion/tests/conftest.py similarity index 100% rename from datafusion/tests/conftest.py rename to python/datafusion/tests/conftest.py diff --git a/datafusion/tests/data_test_context/data.json b/python/datafusion/tests/data_test_context/data.json similarity index 100% rename from datafusion/tests/data_test_context/data.json rename to python/datafusion/tests/data_test_context/data.json diff --git a/datafusion/tests/generic.py b/python/datafusion/tests/generic.py similarity index 100% rename from datafusion/tests/generic.py rename to python/datafusion/tests/generic.py diff --git a/datafusion/tests/test_aggregation.py b/python/datafusion/tests/test_aggregation.py similarity index 100% rename from datafusion/tests/test_aggregation.py rename to python/datafusion/tests/test_aggregation.py diff --git a/datafusion/tests/test_catalog.py b/python/datafusion/tests/test_catalog.py similarity index 100% rename from datafusion/tests/test_catalog.py rename to python/datafusion/tests/test_catalog.py diff --git a/datafusion/tests/test_config.py b/python/datafusion/tests/test_config.py similarity index 100% rename from datafusion/tests/test_config.py rename to python/datafusion/tests/test_config.py diff --git a/datafusion/tests/test_context.py b/python/datafusion/tests/test_context.py similarity index 100% rename from datafusion/tests/test_context.py rename to python/datafusion/tests/test_context.py diff --git a/datafusion/tests/test_dataframe.py b/python/datafusion/tests/test_dataframe.py similarity index 100% rename from datafusion/tests/test_dataframe.py rename to python/datafusion/tests/test_dataframe.py diff --git a/datafusion/tests/test_expr.py b/python/datafusion/tests/test_expr.py similarity index 100% rename from datafusion/tests/test_expr.py rename to python/datafusion/tests/test_expr.py diff --git a/datafusion/tests/test_functions.py b/python/datafusion/tests/test_functions.py similarity index 100% rename from datafusion/tests/test_functions.py rename to python/datafusion/tests/test_functions.py diff --git a/datafusion/tests/test_imports.py b/python/datafusion/tests/test_imports.py similarity index 100% rename from datafusion/tests/test_imports.py rename to python/datafusion/tests/test_imports.py diff --git a/datafusion/tests/test_indexing.py b/python/datafusion/tests/test_indexing.py similarity index 100% rename from datafusion/tests/test_indexing.py rename to python/datafusion/tests/test_indexing.py diff --git a/datafusion/tests/test_input.py b/python/datafusion/tests/test_input.py similarity index 100% rename from datafusion/tests/test_input.py rename to python/datafusion/tests/test_input.py diff --git a/datafusion/tests/test_sql.py b/python/datafusion/tests/test_sql.py similarity index 100% rename from datafusion/tests/test_sql.py rename to python/datafusion/tests/test_sql.py diff --git a/datafusion/tests/test_store.py b/python/datafusion/tests/test_store.py similarity index 100% rename from datafusion/tests/test_store.py rename to python/datafusion/tests/test_store.py diff --git a/datafusion/tests/test_substrait.py b/python/datafusion/tests/test_substrait.py similarity index 100% rename from datafusion/tests/test_substrait.py rename to python/datafusion/tests/test_substrait.py diff --git a/datafusion/tests/test_udaf.py b/python/datafusion/tests/test_udaf.py similarity index 100% rename from datafusion/tests/test_udaf.py rename to python/datafusion/tests/test_udaf.py From 1da28b5019ce2e9f2a563d0088c75a41602970b5 Mon Sep 17 00:00:00 2001 From: Michael J Ward Date: Tue, 14 May 2024 17:09:06 -0500 Subject: [PATCH 009/181] chore: update cargo deps (#698) Closes #672 rustls Closes #682 syn Closes #653 parking_lot closes #648 object_store Closes #625 h2 Closes #623 tokio Closes #608 mio Closes #597 pyo3 Closes #642 pyo3-build-config Closes #627 prost Closes #634 prost-types Closes #637 async-trait --- Cargo.lock | 112 ++++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b4568b96..9b7ed560f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,18 +376,18 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -1036,7 +1036,7 @@ dependencies = [ "pyo3-build-config", "rand", "regex-syntax", - "syn 2.0.60", + "syn 2.0.63", "tokio", "url", "uuid", @@ -1231,7 +1231,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -1299,9 +1299,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1781,9 +1781,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -1946,9 +1946,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -2089,6 +2089,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2102,7 +2108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2116,9 +2122,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -2146,15 +2152,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "9554e3ab233f0a932403704f1a1d08c30d5ccd931adfdfa1e8b5a19b52c1d55a" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2177,15 +2183,16 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ "cfg-if", "indoc", "libc", "memoffset", "parking_lot", + "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", @@ -2194,9 +2201,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" dependencies = [ "once_cell", "target-lexicon", @@ -2204,9 +2211,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" dependencies = [ "libc", "pyo3-build-config", @@ -2214,26 +2221,27 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] name = "pyo3-macros-backend" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ "heck 0.4.1", "proc-macro2", + "pyo3-build-config", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2437,9 +2445,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", @@ -2616,7 +2624,7 @@ checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2650,7 +2658,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2772,7 +2780,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2806,7 +2814,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2819,7 +2827,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2839,7 +2847,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.60", + "syn 2.0.63", "typify", "walkdir", ] @@ -2863,9 +2871,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" dependencies = [ "proc-macro2", "quote", @@ -2929,7 +2937,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2969,9 +2977,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -2992,7 +3000,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -3044,7 +3052,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -3089,7 +3097,7 @@ checksum = "f03ca4cb38206e2bef0700092660bb74d696f808514dae47fa1467cbfe26e96e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -3121,7 +3129,7 @@ dependencies = [ "regress", "schemars", "serde_json", - "syn 2.0.60", + "syn 2.0.63", "thiserror", "unicode-ident", ] @@ -3138,7 +3146,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn 2.0.60", + "syn 2.0.63", "typify-impl", ] @@ -3266,7 +3274,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", "wasm-bindgen-shared", ] @@ -3300,7 +3308,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3554,7 +3562,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] From 344ebad1989d6e7c14c63be63dd7132aadbea710 Mon Sep 17 00:00:00 2001 From: Richard Tia Date: Tue, 14 May 2024 22:33:11 -0400 Subject: [PATCH 010/181] feat: add python bindings for ends_with function (#693) * fix: resolve merge conflicts * fix: wrap pyfunction * fix: alphabetical ordering of function * refactor: update arg names * fix: assert correct column number --- python/datafusion/tests/test_functions.py | 2 ++ src/functions.rs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/python/datafusion/tests/test_functions.py b/python/datafusion/tests/test_functions.py index d834f587f..d2dafc199 100644 --- a/python/datafusion/tests/test_functions.py +++ b/python/datafusion/tests/test_functions.py @@ -533,6 +533,7 @@ def test_string_functions(df): f.translate(column("a"), literal("or"), literal("ld")), f.trim(column("c")), f.upper(column("c")), + f.ends_with(column("a"), literal("llo")), ) result = df.collect() assert len(result) == 1 @@ -573,6 +574,7 @@ def test_string_functions(df): assert result.column(25) == pa.array(["Helll", "Wldld", "!"]) assert result.column(26) == pa.array(["hello", "world", "!"]) assert result.column(27) == pa.array(["HELLO ", " WORLD ", " !"]) + assert result.column(28) == pa.array([True, False, False]) def test_hash_functions(df): diff --git a/src/functions.rs b/src/functions.rs index 4b137d90d..8a7d73839 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -407,6 +407,7 @@ expr_fn!(cosh, num); expr_fn!(degrees, num); expr_fn!(decode, input encoding); expr_fn!(encode, input encoding); +expr_fn!(ends_with, string suffix, "Returns true if string ends with suffix."); expr_fn!(exp, num); expr_fn!(factorial, num); expr_fn!(floor, num); @@ -473,7 +474,7 @@ expr_fn!( "Splits string at occurrences of delimiter and returns the n'th field (counting from one)." ); expr_fn!(sqrt, num); -expr_fn!(starts_with, arg1 arg2, "Returns true if string starts with prefix."); +expr_fn!(starts_with, string prefix, "Returns true if string starts with prefix."); expr_fn!(strpos, string substring, "Returns starting index of specified substring within string, or zero if it's not present. (Same as position(substring in string), but note the reversed argument order.)"); expr_fn!(substr, string position); expr_fn!(tan, num); @@ -652,6 +653,7 @@ pub(crate) fn init_module(m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(datetrunc))?; m.add_wrapped(wrap_pyfunction!(date_trunc))?; m.add_wrapped(wrap_pyfunction!(digest))?; + m.add_wrapped(wrap_pyfunction!(ends_with))?; m.add_wrapped(wrap_pyfunction!(exp))?; m.add_wrapped(wrap_pyfunction!(factorial))?; m.add_wrapped(wrap_pyfunction!(floor))?; From d6c42b4dec452f75305f37f937ed1669dbf5809e Mon Sep 17 00:00:00 2001 From: Michael J Ward Date: Wed, 15 May 2024 09:18:47 -0500 Subject: [PATCH 011/181] feat: expose `named_struct` in python (#700) Ref #692 --- python/datafusion/tests/test_functions.py | 26 +++++++++++++++++++++++ src/functions.rs | 2 ++ 2 files changed, 28 insertions(+) diff --git a/python/datafusion/tests/test_functions.py b/python/datafusion/tests/test_functions.py index d2dafc199..d34e46b33 100644 --- a/python/datafusion/tests/test_functions.py +++ b/python/datafusion/tests/test_functions.py @@ -50,6 +50,32 @@ def df(): return ctx.create_dataframe([[batch]]) +def test_named_struct(df): + df = df.with_column( + "d", + f.named_struct( + literal("a"), + column("a"), + literal("b"), + column("b"), + literal("c"), + column("c"), + ), + ) + + expected = """DataFrame() ++-------+---+---------+------------------------------+ +| a | b | c | d | ++-------+---+---------+------------------------------+ +| Hello | 4 | hello | {a: Hello, b: 4, c: hello } | +| World | 5 | world | {a: World, b: 5, c: world } | +| ! | 6 | ! | {a: !, b: 6, c: !} | ++-------+---+---------+------------------------------+ +""".strip() + + assert str(df) == expected + + def test_literal(df): df = df.select( literal(1), diff --git a/src/functions.rs b/src/functions.rs index 8a7d73839..975025b6b 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -503,6 +503,7 @@ expr_fn_vec!(trunc); expr_fn!(upper, arg1, "Converts the string to all upper case."); expr_fn!(uuid); expr_fn_vec!(r#struct); // Use raw identifier since struct is a keyword +expr_fn_vec!(named_struct); expr_fn!(from_unixtime, unixtime); expr_fn!(arrow_typeof, arg_1); expr_fn!(random); @@ -680,6 +681,7 @@ pub(crate) fn init_module(m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(mean))?; m.add_wrapped(wrap_pyfunction!(median))?; m.add_wrapped(wrap_pyfunction!(min))?; + m.add_wrapped(wrap_pyfunction!(named_struct))?; m.add_wrapped(wrap_pyfunction!(nanvl))?; m.add_wrapped(wrap_pyfunction!(now))?; m.add_wrapped(wrap_pyfunction!(nullif))?; From 856b31014299f34f1e2191f61273bb40a791c47d Mon Sep 17 00:00:00 2001 From: Michael J Ward Date: Wed, 15 May 2024 11:36:42 -0500 Subject: [PATCH 012/181] Website fixes (#702) * docs: link to examples using full URL in README PyPI otherwise renders these relative to the `datafusion-python` page, so when users currently get a 404 when they click on one of these links. Fixes #699 * docs: update project.urls in pyproject.toml * docs: update README with apache TLP URLs * docs u pdate docs/README.md with apache TLP URLs * docs: update index.rst with TLP URLs * docs: update to new branded logos --- README.md | 34 +++++++++--------- docs/README.md | 12 +++---- .../_static/images/2x_bgwhite_original.png | Bin 0 -> 60451 bytes .../DataFusion-Logo-Background-White.png | Bin 12401 -> 0 bytes .../DataFusion-Logo-Background-White.svg | 1 - .../_static/images/DataFusion-Logo-Dark.png | Bin 20134 -> 0 bytes .../_static/images/DataFusion-Logo-Dark.svg | 1 - .../_static/images/DataFusion-Logo-Light.png | Bin 19102 -> 0 bytes .../_static/images/DataFusion-Logo-Light.svg | 1 - docs/source/_static/images/original.png | Bin 0 -> 26337 bytes docs/source/_static/images/original.svg | 31 ++++++++++++++++ docs/source/_static/images/original2x.png | Bin 0 -> 66517 bytes docs/source/_templates/docs-sidebar.html | 2 +- docs/source/conf.py | 2 +- docs/source/index.rst | 8 ++--- pyproject.toml | 6 ++-- 16 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 docs/source/_static/images/2x_bgwhite_original.png delete mode 100644 docs/source/_static/images/DataFusion-Logo-Background-White.png delete mode 100644 docs/source/_static/images/DataFusion-Logo-Background-White.svg delete mode 100644 docs/source/_static/images/DataFusion-Logo-Dark.png delete mode 100644 docs/source/_static/images/DataFusion-Logo-Dark.svg delete mode 100644 docs/source/_static/images/DataFusion-Logo-Light.png delete mode 100644 docs/source/_static/images/DataFusion-Logo-Light.svg create mode 100644 docs/source/_static/images/original.png create mode 100644 docs/source/_static/images/original.svg create mode 100644 docs/source/_static/images/original2x.png diff --git a/README.md b/README.md index 4345b52ed..b1d5397ef 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,16 @@ # DataFusion in Python -[![Python test](https://github.com/apache/arrow-datafusion-python/actions/workflows/test.yaml/badge.svg)](https://github.com/apache/arrow-datafusion-python/actions/workflows/test.yaml) -[![Python Release Build](https://github.com/apache/arrow-datafusion-python/actions/workflows/build.yml/badge.svg)](https://github.com/apache/arrow-datafusion-python/actions/workflows/build.yml) +[![Python test](https://github.com/apache/datafusion-python/actions/workflows/test.yaml/badge.svg)](https://github.com/apache/datafusion-python/actions/workflows/test.yaml) +[![Python Release Build](https://github.com/apache/datafusion-python/actions/workflows/build.yml/badge.svg)](https://github.com/apache/datafusion-python/actions/workflows/build.yml) -This is a Python library that binds to [Apache Arrow](https://arrow.apache.org/) in-memory query engine [DataFusion](https://github.com/apache/arrow-datafusion). +This is a Python library that binds to [Apache Arrow](https://arrow.apache.org/) in-memory query engine [DataFusion](https://github.com/apache/datafusion). DataFusion's Python bindings can be used as a foundation for building new data systems in Python. Here are some examples: - [Dask SQL](https://github.com/dask-contrib/dask-sql) uses DataFusion's Python bindings for SQL parsing, query planning, and logical plan optimizations, and then transpiles the logical plan to Dask operations for execution. -- [DataFusion Ballista](https://github.com/apache/arrow-ballista) is a distributed SQL query engine that extends +- [DataFusion Ballista](https://github.com/apache/datafusion-ballista) is a distributed SQL query engine that extends DataFusion's Python bindings for distributed use cases. It is also possible to use these Python bindings directly for DataFrame and SQL operations, but you may find that @@ -120,23 +120,23 @@ See [examples](examples/README.md) for more information. ### Executing Queries with DataFusion -- [Query a Parquet file using SQL](./examples/sql-parquet.py) -- [Query a Parquet file using the DataFrame API](./examples/dataframe-parquet.py) -- [Run a SQL query and store the results in a Pandas DataFrame](./examples/sql-to-pandas.py) -- [Run a SQL query with a Python user-defined function (UDF)](./examples/sql-using-python-udf.py) -- [Run a SQL query with a Python user-defined aggregation function (UDAF)](./examples/sql-using-python-udaf.py) -- [Query PyArrow Data](./examples/query-pyarrow-data.py) -- [Create dataframe](./examples/import.py) -- [Export dataframe](./examples/export.py) +- [Query a Parquet file using SQL](https://github.com/apache/datafusion-python/blob/main/examples/sql-parquet.py) +- [Query a Parquet file using the DataFrame API](https://github.com/apache/datafusion-python/blob/main/examples/dataframe-parquet.py) +- [Run a SQL query and store the results in a Pandas DataFrame](https://github.com/apache/datafusion-python/blob/main/examples/sql-to-pandas.py) +- [Run a SQL query with a Python user-defined function (UDF)](https://github.com/apache/datafusion-python/blob/main/examples/sql-using-python-udf.py) +- [Run a SQL query with a Python user-defined aggregation function (UDAF)](https://github.com/apache/datafusion-python/blob/main/examples/sql-using-python-udaf.py) +- [Query PyArrow Data](https://github.com/apache/datafusion-python/blob/main/examples/query-pyarrow-data.py) +- [Create dataframe](https://github.com/apache/datafusion-python/blob/main/examples/import.py) +- [Export dataframe](https://github.com/apache/datafusion-python/blob/main/examples/export.py) ### Running User-Defined Python Code -- [Register a Python UDF with DataFusion](./examples/python-udf.py) -- [Register a Python UDAF with DataFusion](./examples/python-udaf.py) +- [Register a Python UDF with DataFusion](https://github.com/apache/datafusion-python/blob/main/examples/python-udf.py) +- [Register a Python UDAF with DataFusion](https://github.com/apache/datafusion-python/blob/main/examples/python-udaf.py) ### Substrait Support -- [Serialize query plans using Substrait](./examples/substrait.py) +- [Serialize query plans using Substrait](https://github.com/apache/datafusion-python/blob/main/examples/substrait.py) ## How to install (from pip) @@ -172,7 +172,7 @@ Bootstrap (Conda): ```bash # fetch this repo -git clone git@github.com:apache/arrow-datafusion-python.git +git clone git@github.com:apache/datafusion-python.git # create the conda environment for dev conda env create -f ./conda/environments/datafusion-dev.yaml -n datafusion-dev # activate the conda environment @@ -183,7 +183,7 @@ Bootstrap (Pip): ```bash # fetch this repo -git clone git@github.com:apache/arrow-datafusion-python.git +git clone git@github.com:apache/datafusion-python.git # prepare development environment (used to build wheel / install in development) python3 -m venv venv # activate the venv diff --git a/docs/README.md b/docs/README.md index 8cb101d92..b4b94120e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,7 +20,7 @@ # DataFusion Documentation This folder contains the source content of the [Python API](./source/api). -This is published to https://arrow.apache.org/datafusion-python/ by a GitHub action +This is published to https://datafusion.apache.org/python by a GitHub action when changes are merged to the main branch. ## Dependencies @@ -66,15 +66,15 @@ firefox build/html/index.html ## Release Process -This documentation is hosted at https://arrow.apache.org/datafusion-python/ +This documentation is hosted at https://datafusion.apache.org/python When the PR is merged to the `main` branch of the DataFusion -repository, a [github workflow](https://github.com/apache/arrow-datafusion-python/blob/main/.github/workflows/docs.yaml) which: +repository, a [github workflow](https://github.com/apache/datafusion-python/blob/main/.github/workflows/docs.yaml) which: 1. Builds the html content -2. Pushes the html content to the [`asf-site`](https://github.com/apache/arrow-datafusion-python/tree/asf-site) branch in this repository. +2. Pushes the html content to the [`asf-site`](https://github.com/apache/datafusion-python/tree/asf-site) branch in this repository. The Apache Software Foundation provides https://arrow.apache.org/, which serves content based on the configuration in -[.asf.yaml](https://github.com/apache/arrow-datafusion-python/blob/main/.asf.yaml), -which specifies the target as https://arrow.apache.org/datafusion-python/. \ No newline at end of file +[.asf.yaml](https://github.com/apache/datafusion-python/blob/main/.asf.yaml), +which specifies the target as https://datafusion.apache.org/python. \ No newline at end of file diff --git a/docs/source/_static/images/2x_bgwhite_original.png b/docs/source/_static/images/2x_bgwhite_original.png new file mode 100644 index 0000000000000000000000000000000000000000..abb5fca6e4619327822eb03a71a2b19b4b93e0db GIT binary patch literal 60451 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 Il&nJk2THRYC;$Ke literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/DataFusion-Logo-Background-White.png b/docs/source/_static/images/DataFusion-Logo-Background-White.png deleted file mode 100644 index 023c2373fc4949037ca1e11bfb72e2e469a0bda7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12401 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 diff --git a/docs/source/_static/images/DataFusion-Logo-Background-White.svg b/docs/source/_static/images/DataFusion-Logo-Background-White.svg deleted file mode 100644 index b3bb47c5e..000000000 --- a/docs/source/_static/images/DataFusion-Logo-Background-White.svg +++ /dev/null @@ -1 +0,0 @@ -Codestin Search App \ No newline at end of file diff --git a/docs/source/_static/images/DataFusion-Logo-Dark.png b/docs/source/_static/images/DataFusion-Logo-Dark.png deleted file mode 100644 index cc60f12a0e4f5a3bba66a01f377adda7a03c8113..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20134 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 QKzI&v%+|@K7IP}?e|Xv(bN~PV diff --git a/docs/source/_static/images/DataFusion-Logo-Dark.svg b/docs/source/_static/images/DataFusion-Logo-Dark.svg deleted file mode 100644 index e16f24443..000000000 --- a/docs/source/_static/images/DataFusion-Logo-Dark.svg +++ /dev/null @@ -1 +0,0 @@ -Codestin Search App \ No newline at end of file diff --git a/docs/source/_static/images/DataFusion-Logo-Light.png b/docs/source/_static/images/DataFusion-Logo-Light.png deleted file mode 100644 index 8992213b0e6072414aaddb574b2336cc6beca66b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19102 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≫uSMcw~$CUKE7o& zc60k-R+m_zNHC4sv<>I}ervA2=stG7W!1=b4fBmK?6Yk5 z^4MFI8}m<%WrwZ<=K4nJEiNJE-<%|tqZ^sKPCNu98M`!^eteoEOBBxx%Z5GKciqD6 za)0tQZE-dXpU5Z}xM;&T6@x>~RoE21Tln1`r38(ezl?uiUUmc1oaZ0GR8-X^{)!H@ zJ`1*j%v0Gdo$U*}v0XM*UuxZF?aL-mxRxz8qDCt+F5kLQD-v5?6 zNqZg))cf~@YWCS1t|Y;<$3cz~jVW@R%Uag2g7qYhlnW2i(}XR!emQ*RZ&d14NuK*` zPmXMa^8x5_&Hrrqxlt;y1Ly_M#l%LPC+)KgB;N0Zwbhe(D_hUIC6n}0=hxe-nfd6~ zL}838e_DI|_3-%1*F~W*9EC01l(OO`Wv$8HIsJ`Bo8mRWY#gBGos_jtnT%1nHlDh{ zPihxP?lo`^L?Z;5Px4~By#*}jF!t-MySO~HnZAq}C4tKE*R_J$#XiVm`C%O4A6dq~ z3+$Ju7M?P#BOiNAnJUXt^%)nf8M0RXVL+ySVlWX@F{Dlj2i* z)0>cSfIZ}}S_*GJ0EoEDU1x(J^Pg{CW$?-UkXiihCOc=*eNP>ON01`kapzwESu1Em z8+NtuOAbX!QQ_q`ar)>;PogO2h*4p*$$E&NU*}xq>YH-yt-o>BT=luIMU;waEh91? z68=yHH8b`@?*xhr-3>0jM#-HwZ7CI=F`CK9d5?5|eh!H$j4RJlkC#!IDG7YGczyE< z)DKyjIKtrqzJ8pD_d^{9k34SK#IJwlXePOgMq`~z*94@rL|D1E0=J2ryaVRP&&Gj- zI-N3C>MCtgc(;8+;V+)-dQR=$EWRTMFpAnrt8229%j4oDOMqj+C8cUrFa{k7^7)wx zue9x6a?u`8{V^E5eFZXc^N1^Xr~M$d>$b5~@_^^^P2HW31=1ee5ZUL+?$A1>djR5> zg9n$XX<>l0TYY?lQv6{4cdJKdPCxsPbnOd$?H+Iak0O3#GVA5XH9ez-Qjd0pMOByL zjo!d@6lyWs(O$;iQ7Hm_vc~M;{fhj^LSi-x7hwTD1KcURn-r@ z9~g)A;oeCt1}oO9{rXd<{M=mPx@qre+gHyVRL5hHtSizsl}H6U?q~{T2;OvJEuvoG zgK(MVZTPz`{sIHetFAi;jNQ%GY+8j|9^N1QzkV+k`r;(u{1KfuZxHW__7v@H3qKf< z`Z(N$7<9aHszS>?k9B*~{bdf0o_Xj0h-iOIf81U7yFWn?3x`9&4s!g{Zro`un*KUNS%gicXte8zTyaba=?#x4s%bv$4){`7?T@@BZ_8 zSy_DP^eVq42JNOCyB#^a&%kzUm^rkTQ@GqZ7-wyWkC@6O`>?+>9a)JHv zu6oHSYKcB5GwYp0x1Wo;o)}l;lcRWZt1y3{pgg3$+rBJxWagLo0XEqKz2qX_9ShUO zic@wnzU}%FJ77|^_9d`adefu*aycB`@G;}aqug*9C>c3<(#%F4ud6na$I9P4mLJ)G z%?uDv*G#w2`@94dFX(wYQU$w)7`l=!#MA5$$4%tg>U-lU^D2Jc`$ePu%{*YX|5E}6 zix-@;(^!x)_XB|fXEO21V}RFtp{xfuQDevYF`=>*`gGe+nJa z(fFB3-kTDU+&(uwZuYii%;@LP_#>OU82=*y$BP7#j>@3=yl4A9)pT#~;YA>>q;Fnx8BUXOFKgdPm|FNUk@cI-+LSb)kD|TM~h;4Y=nW-y~h~~;*rNKm%6 z!d31!h;Bbli!xOxyhG+49XX%C-hC{018GUK!r21_9(X>1jsrVVA#$I4+om7u%N-tX zlCHMv{jE0^TE`!eftioN_uaWQhk>ya1WYP8!x;JXruV7t{v({7^c~~0CvIb0wAXfj zjz4Xtl0fDw94^c41S~4|ZqTUj(}0p|TCo!r_lWTKxxBP_+|2x1U_u2H1^%JR50lsR zi8$@y?tZcEf${%*tFYs1?WgI}WwrX(S1A6LlUc;}kwz&MGW0?^?m(dT7qyss47-n> zzqUl}LxCD-i#)?!|AC<8uV`F+QdOA=B(ChS4;P_0$JtRlHzoz$^hejnN`K6K!7 zp(43ccbOyB!hdBPb`P_ex6WFrGZwD$MRuBxe?S|yjs7KMCwH3<;ST_hjAtE4OqF?t z?*A0+DL32=Irfe`T=?}GELI-W#6#!&U>KKuItavb1z5_%w%+63-E*Hzn>;7BWRp*^ zYb(Gwj73nLuK-Bn{i;O)as!6dDQ4vEtUXx@N@d5s5g zh;kVW1q}zl6RoSxVz0ZdZ_@28M>b1NFjrQ&YjJKN;DcvZWB!-F#SyFGSLrl;nV9*Q zBZtE5l}E}wUkgRL%hx{bbD@1&+z#-1vF}`zLUvANKq&AB)5<;j%q~|nhU+FhF=177 z|M@vW=(vRUcp#(3kgYME^=*blNJLlMDruA(y_jcxY2^2(ql|_qfr;;cQJ+Ea`1- zvGT@Jpjb&~l*(441g^*322P@1vEDDk5EZ&n!sgVZTT8kOZZBfT7tt#t_#ozR@;>cBFSvO&kVzxV47fm( zx+CB!FL3eWPm_xG#bFT}3%o94W>@5p6ZUZC@XeV8bE73Za@Ud%9kLFt6Y0HFAv0MLJ*gp56Fvs!|2Lz+#6Ip*$!C(>$4$@l zzM6s8NeRZPD9A;+j@R1pehwO{Jx%_LgGPdVz+?yZ62M&hCAMOO%g9&dJPu)bIozku z7kY5fN4noMv{ESbnEor)GpZ+LJA5^*?v)@2e?h>FaIxo569&4h_~KuZP@TbrxA>zt z*A|vq=-6UW$}ou?FyY%g2B70icY>K*-s<^dIgY&pOEMT+tZj~S)FZ+_oBN>YnHuiX z3iZd;3&Mh(tJx=Y3Oq*WRJA7Qh248flIC^60@w*#@YHJ6010yr;8G!^!LDl)c~^1) z+cM4;H!ob&Vc5sM{+N7PvW4<3N3w&&Mi|EG|CMn5@l5A&9H$*|=QK@rh|c=4{Fuz| zA6e04ex##Q5wTpmEeXx$N5fUuwo(|H&Cg`$I#x#^LynyM*1}GHtjo_e!pO2*?)%;S z{rNoJpU3DdXoIevSO&@ID8yoe5a z9>BAIUjoqXuT7TD%0BM+@NFO!MZoxXmZ{7>%p9sRc&c`P@TgJ((Y5AB54CH>kC>df zvCxZni_#M<(cn$BynV{3a8*jDr0@q5sD9g(r)e;9NDe>n*-o?2yg3RZ=~xqxc!+oG zWZ!W^V59|4P;S8_Qsstcg2x&Qe0;}A)T%CIJ?vfIKxMSDB~yRCmBzgXjUO(!m;`L& zTN+IT%vomf0~1-mb=7bw_)wOG?Q zi%((mLtZkGN)17}*NpUWsr{!t3uSSnfOMax`caLm72L(wp_C8>_tsW0gU7506DL-; z$cGE!{0;WY=TaWn@1TzVvfa-L;1#kdK*K_b=@{OhB8yZu3uV6_5UmqI6TJ)(L zhacYNLh20}TwdvBgVq}0R$;u)oJ&$sNItEm4+zVa^AyQhu|#XhwWr#>xINA?UCAF%E)#-|KNi5}AD7F(4V&$u@OcHnd+FvNfdBO6yX1-d9|zn7!a zvMI*lXGE2P042+Jq$j-xB2nUk-t&DgW+BF7-j^vy>`zp;Rp0nT9!O}Q}8Ywd2_1@9w$au z_vw%GUztUMB@(y0@U{)aPlMm1U;3YF! zG|2AhhADgvvMhCMX(i57pf*|GM%C)vn>+RUpSst=q0+#)B$)8@yvdH3v>0>~wTkb(wmK^20>_El5VToasS& zy5cc5A?g4MhvkK;mLrkxYxSF8fEVdS&mZ}yN10cZ_||#*ihe(6(a2c6^}ik1#le;c zPc8D?j=>|6YmvS;J)FOxZA-`?&|4S^=XkKegcEG(8F)KQA04?hw>gzh5juCd>Y54k z@F`p2opK4ZKIMQbfcYSVMQDQkd?F;nNVwVQkpH4{_FkJk6WzXHEYx(7qXp8P@Ue;E zk(qp4zS86DwE+7y}%rTcvFMm-=PFQO^w~DTqc&6lER$#2m ziMXCv$}aAAki^JO_y)N*Koz_;d#_%lEy{In;}63EVqGR8m!lZeSwIEBR5OB;EX9RZ zkE+HXMd+~aLk|L64?@U)m>o+Uh!H!3CYPN8`|Fxa0qLQd@2BV68Z$I!fGpf&2kLD| z{dNx*BZCWQ+QCW9P_+%~KD4ULsa*6$XGxXt9WAxHgK0p_&3QtOuP7_PVuPzQBqdHi ztvseEW)7~Z)kG~oo@Yq@HHaV89T8#(YjQMU|Asou5veQs#*>pqk8O*}R*JQrL|&7W z4QeBLZ=zv`ChmXvh$(X-J=Q`~9)}Yco*Z#RL*b-J`4<`beOVj`=i&rwXk>dtkCj;8 zz`AM_O-RhEu&wb)vt6`W`6G8_vHz7tYz3I7IPq=t$Aklf{WTUq^Xj3}tS7Wj2YO5?6{?_)F$xtL}R zWY#g6tAWnu38R#6J$QS-M;#aD!O?FGzVF4ugC!|E$n}6Kp?CS=5m6>wP}T(M0P#S67*R= z5BS$wBA!g%!J+;)NxrF>jpW3bB6fi7`Ik@+&W%2Rnes3LpDR|$*IaCodestin Search App \ No newline at end of file diff --git a/docs/source/_static/images/original.png b/docs/source/_static/images/original.png new file mode 100644 index 0000000000000000000000000000000000000000..687f946760b059a463901537403ed81a1adfab69 GIT binary patch literal 26337 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%0DOG?ys|uk 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/_static/images/original2x.png b/docs/source/_static/images/original2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a7402109b211b3d69c240f0c6e29892af8a09378 GIT binary patch literal 66517 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 z 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 bxBdf~+`;xDbWd*ifB#F0ei5kz>-+yd&UsDn literal 0 HcmV?d00001 diff --git a/docs/source/_templates/docs-sidebar.html b/docs/source/_templates/docs-sidebar.html index 6541b7713..44deeed25 100644 --- a/docs/source/_templates/docs-sidebar.html +++ b/docs/source/_templates/docs-sidebar.html @@ -1,6 +1,6 @@ - +