This is an automated email from the ASF dual-hosted git repository. yiguolei pushed a commit to branch branch-4.2 in repository https://gitbox.apache.org/repos/asf/doris.git
commit 238cf2c2538fd27d53c65705718a46e72700df39 Author: Gabriel <[email protected]> AuthorDate: Thu Oct 8 09:57:40 2026 +0800 [improvement](lance) Push down array membership and boolean scalar index predicates (#68687) ### What problem does this PR solve? Ordinary Lance scans currently retain array label membership predicates in Doris, and scalar index planning misses usable boolean drivers. This adds Substrait pushdown for built-in `array_contains` on a direct `List<Utf8>` column with a non-NULL constant string, and for nonempty constant-string `arrays_overlap` (including the normal Nereids rewrite of three or more membership disjuncts). Positive indexable boolean conditions can select a LabelList/BTree/Bitmap segment. OR requires usable drivers on both branches of the same selected index. Literal starts_with prefixes and LIKE filters with a usable leading prefix remain eligible for BTree planning. OR branches requiring a refine filter retain fragment scans. Missing FE field/index metadata and competing logical indexes on one column preserve native automatic index selection and fragment ownership. For example, two label conditions joined by AND now reach Lance together and can reduce the index candidate set before row materialization. NULL needles, other array types, dynamic operands, UDFs, and `array_contains_all` remain residual. Doris `array_contains_all` tests a contiguous ordered subsequence, so translating it to Lance set containment would change results. Each task still selects one logical index; this change does not implement multi-column index intersection. Fragment coverage, residual evaluation, and limit handling are preserved. Complement-only predicates and expressions exceeding the pinned native node/depth budget use parallel fragment scans with scalar-index use disabled. An independent positive condition retains one search per index segment even when another conjunct is a complement. This avoids repeated segment-wide searches and prevents large overlap or unindexable OR filters from serializing fallback scans. Dependency: [lance-format/lance-c#93](https://github.com/lance-format/lance-c/pull/93), merged in upstream main at `cd63420bfbe27f6f0a1edcc873b9191af7d52852`, which is now the pinned dependency. The existing Foyer payload is reapplied without implementation changes because [lance-c#73](https://github.com/lance-format/lance-c/pull/73) is still open and supplies cache APIs used by Doris. Remove that remaining patch after #73 merges. The build now records and validates the installed Lance source/archive/patch fingerprint, detects incomplete publication, and refuses stale external build output. Before removing an existing installation, it verifies required rebuild inputs and rejects external definitions that differ from the checkout. The third-party CI script job runs the installation harness under the existing workflow path filters. These third-party/build changes are synchronized to #68689 for master. This PR targets `branch-4.1`. Documentation: https://github.com/apache/doris-website/pull/4184 (English and Chinese; draft pending implementation). ### Validation - 70 focused FE JUnit tests passed after compiling the changed converter/planner/scan sources and tests against an existing FE dependency build. Coverage includes metadata fallbacks, competing same-column indexes in both metadata orders, balanced wide conjunctions, short IN expansion, literal/refined prefixes, nested OR guards, and existing complement/overlap limits. The new regression assertions failed before the fixes. - FE Checkstyle passed with zero violations; the updated Groovy regression suite passed syntax validation. - Executed 12 plans produced by the actual FE converter and scan planner through 30 pinned lance-c scans. Verified result rows and index counters for unavailable field IDs, omitted index metadata, unequal same-column index coverage, a 34-predicate conjunction, 62/63-label overlap combined with two-value IN, literal prefixes, refined LIKE, and direct/nested OR fallback. No segment fallback occurred; metadata/ambiguity cases retained native index loads. Previous integration also covered fully indexed, partially indexed, and unindexed datasets. - The installation harness passed on master and branch-4.1 using independent external source definitions: legacy/matching installs, missing helper/vars/patch/downloader/builder, mismatched and synchronized pin/patch updates, incomplete artifacts, stale builder output, interrupted publication, and retry. Rejected preflight cases verify that the entire installation and builder invocation count remain unchanged. The missing-helper regression failed before the fix. Shell syntax, workflow YAML, and CI path-filter checks passed. - The unchanged pinned upstream main plus Foyer dependency previously passed 75 Rust unit tests, 376 C API tests, and all 3 native C/C++/static OSS consumer tests. Third-party extraction/cache-refresh/patch-failure checks also passed on both dependency definitions. - SQL regressions assert pushed/residual EXPLAIN output, fragment versus segment grouping, actual index searches/candidates/fallbacks, partial coverage, 64/65-label boundaries, mixed complements, and selective LIMIT. Full Doris compilation and distributed SQL execution on this revision are pending CI. ### Release note Support ordinary Lance array-label membership pushdown and safe boolean scalar index planning. ### Check List (For Author) - Test - [x] Regression test added (SQL execution pending CI) - [x] Unit Test - [x] Manual integration test (FE-produced Substrait through lance-c, described above) - Behavior changed: - [x] Yes. Compatible label predicates are evaluated in Lance; eligible positive boolean drivers use scalar segments, while broad complements and unsupported or oversized expressions retain fragment parallelism. - Does this need documentation? - [x] Yes: https://github.com/apache/doris-website/pull/4184 ### Check List (For Reviewer who merge this PR) - [ ] Confirm the release note - [ ] Confirm test cases - [ ] Confirm document - [ ] Confirm the upstream dependency is ready --- .github/workflows/build-thirdparty.yml | 3 + build.sh | 25 +- .../scripts/lance_build_array_predicates.py | 106 ++++++++ .../scripts/lance_build_preinstalled_catalog.py | 4 + .../bitmap_page_lookup.lance | Bin 0 -> 993 bytes .../page_data.lance | Bin 0 -> 769 bytes .../page_lookup.lance | Bin 0 -> 1259 bytes .../0-a4640b8d-55a7-47f8-9392-5ec3d0ed19c9.txn | Bin 0 -> 325 bytes .../1-d21db0fc-7e46-4699-9948-ab6f8d66098c.txn | Bin 0 -> 184 bytes .../2-8498550d-87e2-42c8-931c-1ed0d8bc7371.txn | Bin 0 -> 197 bytes .../_versions/18446744073709551612.manifest | Bin 0 -> 901 bytes .../_versions/18446744073709551613.manifest | Bin 0 -> 735 bytes .../_versions/18446744073709551614.manifest | Bin 0 -> 729 bytes .../_versions/latest_version_hint.json | 1 + ...0010111110100106a0464445da0b5a4b7c3465eb5.lance | Bin 0 -> 1223 bytes ...101111100111010027ce24037932857b1f2b85e24.lance | Bin 0 -> 1223 bytes .../bitmap_page_lookup.lance | Bin 0 -> 865 bytes .../page_data.lance | Bin 0 -> 704 bytes .../page_lookup.lance | Bin 0 -> 1259 bytes .../0-58691d36-bff2-4a01-ad25-ae3642019067.txn | Bin 0 -> 244 bytes .../1-c343d785-33b7-4448-b0da-160c970f78a1.txn | Bin 0 -> 182 bytes .../2-bcdae8c1-73a4-4b25-ad8b-887bf393af0f.txn | Bin 0 -> 195 bytes .../3-8a4b0277-3a05-4eca-8787-2f5ad24763fb.txn | Bin 0 -> 124 bytes .../_versions/18446744073709551611.manifest | Bin 0 -> 824 bytes .../_versions/18446744073709551612.manifest | Bin 0 -> 812 bytes .../_versions/18446744073709551613.manifest | Bin 0 -> 648 bytes .../_versions/18446744073709551614.manifest | Bin 0 -> 565 bytes .../_versions/latest_version_hint.json | 1 + ...011101101000101d214d7401ba969a49532b0ae8b.lance | Bin 0 -> 1223 bytes ...00100001001011139a9b14af698a84d7cb598d6a7.lance | Bin 0 -> 1223 bytes .../0-7c18f76d-3524-4079-835f-7404de102fa9.txn | Bin 0 -> 325 bytes .../_versions/18446744073709551614.manifest | Bin 0 -> 729 bytes .../_versions/latest_version_hint.json | 1 + ...0010010101001015f6f6345d896c3324132a6d562.lance | Bin 0 -> 1223 bytes ...1100100010111008b02e34ab199273cee3ff3688e.lance | Bin 0 -> 1223 bytes .../lance/source/LancePredicateConverter.java | 75 +++++- .../lance/source/LanceScalarIndexPlanner.java | 233 ++++++++++++++-- .../datasource/lance/source/LanceScanPlanner.java | 6 +- .../lance/source/LanceSubstraitSerializer.java | 18 +- .../main/resources/substrait/lance_functions.yaml | 27 ++ .../lance/LancePredicateConverterTest.java | 115 ++++++++ .../lance/source/LanceScalarIndexPlannerTest.java | 292 +++++++++++++++++++++ .../datasource/lance/source/LanceScanNodeTest.java | 69 ++++- .../test_lance_array_predicate_pushdown.groovy | 154 +++++++++++ thirdparty/build-thirdparty.sh | 7 + thirdparty/lance-install.sh | 38 +++ thirdparty/patches/lance-c-foyer.patch | 31 ++- thirdparty/test/lance-install-test.sh | 176 +++++++++++++ thirdparty/vars.sh | 10 +- 49 files changed, 1343 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build-thirdparty.yml b/.github/workflows/build-thirdparty.yml index 3d3d11ff27d..df6339c8ab8 100644 --- a/.github/workflows/build-thirdparty.yml +++ b/.github/workflows/build-thirdparty.yml @@ -80,6 +80,9 @@ jobs: run: | thirdparty/test/adbc-jni-config-test.sh + - name: Test Lance installation + run: bash thirdparty/test/lance-install-test.sh + build_linux: name: Build Third Party Libraries (Linux) needs: changes diff --git a/build.sh b/build.sh index 8449eab271a..5eac08e4078 100755 --- a/build.sh +++ b/build.sh @@ -441,6 +441,7 @@ if [[ "${CLEAN}" -eq 1 && "${BUILD_BE}" -eq 0 && "${BUILD_FE}" -eq 0 && ${BUILD_ fi # build thirdparty libraries if necessary. check last thirdparty lib installation +source "${DORIS_HOME}/thirdparty/lance-install.sh" if [[ "${TARGET_SYSTEM}" == 'Darwin' ]]; then LAST_THIRDPARTY_LIB='libbrotlienc.a' else @@ -456,10 +457,21 @@ if [[ ! -f "${DORIS_THIRDPARTY}/installed/lib/${LAST_THIRDPARTY_LIB}" || ! -f "${DORIS_THIRDPARTY}/installed/include/paimon_rust/paimon.h" || ! -s "${DORIS_THIRDPARTY}/installed/lib64/libpaimon_c.a" || ! -s "${DORIS_THIRDPARTY}/installed/include/paimon_rust/paimon.h" || - -e "${DORIS_THIRDPARTY}/installed/lib64/.paimon-installing" ]]; then - # Compilation images may contain only installed artifacts; never erase them without a rebuild source. - if [[ ! -f "${DORIS_THIRDPARTY}/build-thirdparty.sh" ]]; then - echo "Third-party dependencies require a rebuild, but build-thirdparty.sh is missing." >&2 + -e "${DORIS_THIRDPARTY}/installed/lib64/.paimon-installing" ]] || + ! lance_c_install_is_current "${DORIS_HOME}/thirdparty" "${DORIS_THIRDPARTY}/installed"; then + # External trees can be partially updated or pinned to another revision. Preserve + # the existing prefix unless their build inputs can produce the requested Lance version. + for input in build-thirdparty.sh download-thirdparty.sh vars.sh lance-install.sh patches/lance-c-foyer.patch; do + if [[ ! -f "${DORIS_THIRDPARTY}/${input}" || ! -r "${DORIS_THIRDPARTY}/${input}" ]]; then + echo "Third-party dependencies require a rebuild, but ${input} is missing or unreadable." >&2 + echo "Refresh the compilation image or set DORIS_THIRDPARTY to a complete third-party source tree." >&2 + exit 1 + fi + done + if ! expected_lance_fingerprint="$(lance_c_install_fingerprint "${DORIS_HOME}/thirdparty")" || + ! rebuild_lance_fingerprint="$(lance_c_install_fingerprint "${DORIS_THIRDPARTY}")" || + [[ "${rebuild_lance_fingerprint}" != "${expected_lance_fingerprint}" ]]; then + echo "Lance rebuild sources do not match this checkout; installed dependencies have been preserved." >&2 echo "Refresh the compilation image or set DORIS_THIRDPARTY to a complete third-party source tree." >&2 exit 1 fi @@ -472,6 +484,11 @@ if [[ ! -f "${DORIS_THIRDPARTY}/installed/lib/${LAST_THIRDPARTY_LIB}" || else bash "${DORIS_THIRDPARTY}/build-thirdparty.sh" -j "${PARALLEL}" --clean fi + # An external build script can itself be stale. Never link its old output silently. + if ! lance_c_install_is_current "${DORIS_HOME}/thirdparty" "${DORIS_THIRDPARTY}/installed"; then + echo "Lance dependency revision does not match this checkout. Refresh the third-party build tree." >&2 + exit 1 + fi fi update_submodule() { diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/lance_build_array_predicates.py b/docker/thirdparties/docker-compose/iceberg/scripts/lance_build_array_predicates.py new file mode 100644 index 00000000000..114e5a8a128 --- /dev/null +++ b/docker/thirdparties/docker-compose/iceberg/scripts/lance_build_array_predicates.py @@ -0,0 +1,106 @@ +# 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. + +"""Build isolated array pushdown fixtures with lance_fixture_requirements.txt. + +The standard MinIO mirror publishes these below warehouse/lance/predicate_arrays. +Each dataset uses relative data/index paths and two eight-row fragments. The +partial dataset appends one unindexed fragment after creating its LabelList index. +""" + +import argparse +from pathlib import Path + +import lance +import pyarrow as pa + + +LABELS = [["red"], ["blue"], ["red", "blue"], [], None, + ["red", "red"], [None, "blue"], ["blue", "red"]] * 2 + + +def build(output): + if not __debug__: + raise RuntimeError("Fixture verification requires assertions; do not use python -O") + if lance.__version__ != "7.0.0": + raise RuntimeError("Use the pinned pylance 7.0.0 fixture writer") + output.mkdir(parents=True, exist_ok=True) + table = pa.table({"id": pa.array(range(16), type=pa.int64()), + "labels": pa.array(LABELS, type=pa.list_(pa.string())), + "category": pa.array([i % 3 for i in range(16)], type=pa.int32())}) + for name in ["indexed", "partial", "unindexed"]: + path = output / (name + ".lance") + if path.exists(): + raise FileExistsError(path) + first = table.slice(0, 8) if name == "partial" else table + dataset = lance.write_dataset(first, str(path), max_rows_per_file=8, max_rows_per_group=8) + if name != "unindexed": + dataset.create_scalar_index("labels", "LABEL_LIST", name="labels_idx") + dataset.create_scalar_index("category", "BTREE", name="category_idx") + if name == "partial": + lance.write_dataset(table.slice(8), str(path), mode="append", max_rows_per_file=8) + check(output) + + +def check(output): + if not __debug__: + raise RuntimeError("Fixture verification requires assertions; do not use python -O") + table = pa.table({"id": pa.array(range(16), type=pa.int64()), + "labels": pa.array(LABELS, type=pa.list_(pa.string())), + "category": pa.array([i % 3 for i in range(16)], type=pa.int32())}) + for name in ["indexed", "partial", "unindexed"]: + path = output / (name + ".lance") + dataset = lance.dataset(str(path)) + assert dataset.to_table().to_pydict() == table.to_pydict() + fragments = dataset.get_fragments() + assert len(fragments) == 2 + assert [f.to_table(columns=["id"])["id"].to_pylist() for f in fragments] == [ + list(range(8)), list(range(8, 16))] + indexes = {index["name"]: index for index in dataset.list_indices()} + assert set(indexes) == (set() if name == "unindexed" else {"labels_idx", "category_idx"}) + expected_coverage = {f.fragment_id for f in (fragments[:1] if name == "partial" else fragments)} + # Names alone do not prove the mixed indexed/unindexed fixture: verify each + # physical domain so rebuilding both fragments into an index cannot pass silently. + for index_name, field, index_type in [("labels_idx", "labels", "LabelList"), + ("category_idx", "category", "BTree")]: + if name == "unindexed": + continue + index = indexes[index_name] + assert index["fields"] == [field] and index["type"] == index_type + assert index["fragment_ids"] == expected_coverage, (name, index_name, index) + if name == "partial": + assert len({f.fragment_id for f in fragments} - expected_coverage) == 1 + for predicate, expected in [ + ("array_contains(labels, 'red')", [0, 2, 5, 7, 8, 10, 13, 15]), + ("array_contains(labels, 'red') AND array_contains(labels, 'blue')", [2, 7, 10, 15]), + ("array_contains(labels, 'red') OR array_contains(labels, 'blue')", + [0, 1, 2, 5, 6, 7, 8, 9, 10, 13, 14, 15])]: + for use_index in [False, True]: + ids = dataset.to_table(columns=["id"], filter=predicate, + use_scalar_index=use_index)["id"].to_pylist() + assert sorted(ids) == expected, (name, predicate, ids) + assert len(dataset.list_indices()) == (0 if name == "unindexed" else 2) + print("Verified array predicate fixtures (indexed, partial, unindexed)") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=Path(__file__).parent / + "preinstalled_data/lance/predicate_arrays") + parser.add_argument("--check", action="store_true", help="Verify committed fixtures without rebuilding") + args = parser.parse_args() + (check if args.check else build)(args.output) diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/lance_build_preinstalled_catalog.py b/docker/thirdparties/docker-compose/iceberg/scripts/lance_build_preinstalled_catalog.py index 54560857370..2645e8748d8 100644 --- a/docker/thirdparties/docker-compose/iceberg/scripts/lance_build_preinstalled_catalog.py +++ b/docker/thirdparties/docker-compose/iceberg/scripts/lance_build_preinstalled_catalog.py @@ -89,6 +89,7 @@ import lance import lance_namespace import pyarrow as pa import pyarrow.ipc as ipc +from lance_build_array_predicates import build as build_array_predicates, check as check_array_predicates from lance_build_multivector import build as build_multivector, check as check_multivector from lance_build_nested_null import build as build_nested_null, check as check_nested_null from lance_build_time_travel import check as check_time_travel @@ -1671,6 +1672,8 @@ def check_fts_dataset(location: str, *, table_name: str, index_name: str, def check_catalog(root: Path) -> None: + # The SQL pushdown suite also relies on these independently published datasets. + check_array_predicates(root / "predicate_arrays") check_data_shapes() namespace = lance_namespace.connect("dir", {"root": str(root)}) tables = namespace.list_tables(ListTablesRequest(id=[NAMESPACE])) @@ -1810,6 +1813,7 @@ def main() -> int: staging = Path(staging_name) / "lance" staging.mkdir() build(staging, all_types_source, time_travel_source) + build_array_predicates(staging / "predicate_arrays") check_catalog(staging) backup = output.with_name(output.name + ".old") if backup.exists(): diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/026150a9-7d9e-4859-a412-fa0582e40071/bitmap_page_lookup.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/026150a9-7d9e-4859-a412-fa0582e40071/bitmap_page_lookup.lance new file mode 100644 index 00000000000..8b817459d44 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/026150a9-7d9e-4859-a412-fa0582e40071/bitmap_page_lookup.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/79b28d78-5490-40ba-a969-399822c12126/page_data.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/79b28d78-5490-40ba-a969-399822c12126/page_data.lance new file mode 100644 index 00000000000..af115f4fb6e Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/79b28d78-5490-40ba-a969-399822c12126/page_data.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/79b28d78-5490-40ba-a969-399822c12126/page_lookup.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/79b28d78-5490-40ba-a969-399822c12126/page_lookup.lance new file mode 100644 index 00000000000..bb72eed316c Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_indices/79b28d78-5490-40ba-a969-399822c12126/page_lookup.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/0-a4640b8d-55a7-47f8-9392-5ec3d0ed19c9.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/0-a4640b8d-55a7-47f8-9392-5ec3d0ed19c9.txn new file mode 100644 index 00000000000..2db5ba7fb9b Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/0-a4640b8d-55a7-47f8-9392-5ec3d0ed19c9.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/1-d21db0fc-7e46-4699-9948-ab6f8d66098c.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/1-d21db0fc-7e46-4699-9948-ab6f8d66098c.txn new file mode 100644 index 00000000000..38fdb4d0534 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/1-d21db0fc-7e46-4699-9948-ab6f8d66098c.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/2-8498550d-87e2-42c8-931c-1ed0d8bc7371.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/2-8498550d-87e2-42c8-931c-1ed0d8bc7371.txn new file mode 100644 index 00000000000..75a68117ca4 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_transactions/2-8498550d-87e2-42c8-931c-1ed0d8bc7371.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551612.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551612.manifest new file mode 100644 index 00000000000..b96a888dd78 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551612.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551613.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551613.manifest new file mode 100644 index 00000000000..4e0e7ac5ed3 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551613.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551614.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551614.manifest new file mode 100644 index 00000000000..74c137306c7 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/18446744073709551614.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/latest_version_hint.json b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/latest_version_hint.json new file mode 100644 index 00000000000..f93d3984472 --- /dev/null +++ b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/_versions/latest_version_hint.json @@ -0,0 +1 @@ +{"version":3} \ No newline at end of file diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/data/1001010000010111110100106a0464445da0b5a4b7c3465eb5.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/data/1001010000010111110100106a0464445da0b5a4b7c3465eb5.lance new file mode 100644 index 00000000000..82104af9df6 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/data/1001010000010111110100106a0464445da0b5a4b7c3465eb5.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/data/111000011101111100111010027ce24037932857b1f2b85e24.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/data/111000011101111100111010027ce24037932857b1f2b85e24.lance new file mode 100644 index 00000000000..dfe133e40e7 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/indexed.lance/data/111000011101111100111010027ce24037932857b1f2b85e24.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/9b6b2f0a-bd44-450f-9a3e-ab3f4ace7990/bitmap_page_lookup.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/9b6b2f0a-bd44-450f-9a3e-ab3f4ace7990/bitmap_page_lookup.lance new file mode 100644 index 00000000000..dfd2305285a Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/9b6b2f0a-bd44-450f-9a3e-ab3f4ace7990/bitmap_page_lookup.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/db755621-13aa-4ca9-a495-c0745e34f7c4/page_data.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/db755621-13aa-4ca9-a495-c0745e34f7c4/page_data.lance new file mode 100644 index 00000000000..7e9051906cd Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/db755621-13aa-4ca9-a495-c0745e34f7c4/page_data.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/db755621-13aa-4ca9-a495-c0745e34f7c4/page_lookup.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/db755621-13aa-4ca9-a495-c0745e34f7c4/page_lookup.lance new file mode 100644 index 00000000000..bb72eed316c Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_indices/db755621-13aa-4ca9-a495-c0745e34f7c4/page_lookup.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/0-58691d36-bff2-4a01-ad25-ae3642019067.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/0-58691d36-bff2-4a01-ad25-ae3642019067.txn new file mode 100644 index 00000000000..b8e46a2b19d Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/0-58691d36-bff2-4a01-ad25-ae3642019067.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/1-c343d785-33b7-4448-b0da-160c970f78a1.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/1-c343d785-33b7-4448-b0da-160c970f78a1.txn new file mode 100644 index 00000000000..3eb9059d63e Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/1-c343d785-33b7-4448-b0da-160c970f78a1.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/2-bcdae8c1-73a4-4b25-ad8b-887bf393af0f.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/2-bcdae8c1-73a4-4b25-ad8b-887bf393af0f.txn new file mode 100644 index 00000000000..cbf508f445e Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/2-bcdae8c1-73a4-4b25-ad8b-887bf393af0f.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/3-8a4b0277-3a05-4eca-8787-2f5ad24763fb.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/3-8a4b0277-3a05-4eca-8787-2f5ad24763fb.txn new file mode 100644 index 00000000000..ace146f66c0 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_transactions/3-8a4b0277-3a05-4eca-8787-2f5ad24763fb.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551611.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551611.manifest new file mode 100644 index 00000000000..9e186aac8a1 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551611.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551612.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551612.manifest new file mode 100644 index 00000000000..ca0a927adc0 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551612.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551613.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551613.manifest new file mode 100644 index 00000000000..b5cd6bb01d7 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551613.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551614.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551614.manifest new file mode 100644 index 00000000000..971179fb5c1 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/18446744073709551614.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/latest_version_hint.json b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/latest_version_hint.json new file mode 100644 index 00000000000..205c7a40a84 --- /dev/null +++ b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/_versions/latest_version_hint.json @@ -0,0 +1 @@ +{"version":4} \ No newline at end of file diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/data/011010010011101101000101d214d7401ba969a49532b0ae8b.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/data/011010010011101101000101d214d7401ba969a49532b0ae8b.lance new file mode 100644 index 00000000000..82104af9df6 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/data/011010010011101101000101d214d7401ba969a49532b0ae8b.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/data/11001100100100001001011139a9b14af698a84d7cb598d6a7.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/data/11001100100100001001011139a9b14af698a84d7cb598d6a7.lance new file mode 100644 index 00000000000..dfe133e40e7 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/partial.lance/data/11001100100100001001011139a9b14af698a84d7cb598d6a7.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_transactions/0-7c18f76d-3524-4079-835f-7404de102fa9.txn b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_transactions/0-7c18f76d-3524-4079-835f-7404de102fa9.txn new file mode 100644 index 00000000000..7bb4aa01bae Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_transactions/0-7c18f76d-3524-4079-835f-7404de102fa9.txn differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_versions/18446744073709551614.manifest b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_versions/18446744073709551614.manifest new file mode 100644 index 00000000000..b0e2e53d706 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_versions/18446744073709551614.manifest differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_versions/latest_version_hint.json b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_versions/latest_version_hint.json new file mode 100644 index 00000000000..491d734467a --- /dev/null +++ b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/_versions/latest_version_hint.json @@ -0,0 +1 @@ +{"version":1} \ No newline at end of file diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/data/0011111110010010101001015f6f6345d896c3324132a6d562.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/data/0011111110010010101001015f6f6345d896c3324132a6d562.lance new file mode 100644 index 00000000000..82104af9df6 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/data/0011111110010010101001015f6f6345d896c3324132a6d562.lance differ diff --git a/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/data/1001011111100100010111008b02e34ab199273cee3ff3688e.lance b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/data/1001011111100100010111008b02e34ab199273cee3ff3688e.lance new file mode 100644 index 00000000000..dfe133e40e7 Binary files /dev/null and b/docker/thirdparties/docker-compose/iceberg/scripts/preinstalled_data/lance/predicate_arrays/unindexed.lance/data/1001011111100100010111008b02e34ab199273cee3ff3688e.lance differ diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java index 2e1cd8b890c..2b23992c9dd 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LancePredicateConverter.java @@ -17,6 +17,7 @@ package org.apache.doris.datasource.lance.source; +import org.apache.doris.analysis.ArrayLiteral; import org.apache.doris.analysis.BinaryPredicate; import org.apache.doris.analysis.BoolLiteral; import org.apache.doris.analysis.CompoundPredicate; @@ -48,6 +49,7 @@ import org.apache.arrow.vector.types.pojo.ArrowType; import org.apache.arrow.vector.types.pojo.Field; import org.apache.arrow.vector.types.pojo.Schema; +import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.time.DateTimeException; @@ -67,6 +69,8 @@ import java.util.stream.Collectors; /** Converts predicates with identical Doris and Lance semantics to a Substrait ExtendedExpression. */ public class LancePredicateConverter { private static final TypeCreator REQUIRED = TypeCreator.of(false); + private static final String LANCE_FUNCTIONS = "https://github.com/apache/doris/blob/master/" + + "fe/fe-core/src/main/resources/substrait/lance_functions.yaml"; private static final SimpleExtension.ExtensionCollection EXTENSIONS = loadExtensions(); private final Schema schema; @@ -125,7 +129,7 @@ public class LancePredicateConverter { return convertLike((LikePredicate) expr); } if (expr instanceof FunctionCallExpr) { - return convertStringFunction((FunctionCallExpr) expr); + return convertFunction((FunctionCallExpr) expr); } if (expr instanceof SlotRef) { return convertBooleanSlot((SlotRef) expr); @@ -264,7 +268,7 @@ public class LancePredicateConverter { return convertStringPredicate("like:str_str", predicate.getChild(0), predicate.getChild(1), true); } - private Optional<Expression> convertStringFunction(FunctionCallExpr function) { + private Optional<Expression> convertFunction(FunctionCallExpr function) { if (function.getFnName() == null || function.getFn() == null || function.getFn().getBinaryType() != TFunctionBinaryType.BUILTIN || function.getChildren().size() != 2) { @@ -272,6 +276,10 @@ public class LancePredicateConverter { } String functionName = function.getFnName().getFunction().toLowerCase(Locale.ROOT); switch (functionName) { + case "arrays_overlap": + return convertArraysOverlap(function); + case "array_contains": + return convertArrayContains(function); case "like": return convertStringPredicate( "like:str_str", function.getChild(0), function.getChild(1), true); @@ -286,6 +294,64 @@ public class LancePredicateConverter { } } + private Optional<Expression> convertArraysOverlap(FunctionCallExpr function) { + Expr input = function.getChild(0); + Expr values = function.getChild(1); + if (input instanceof ArrayLiteral) { + input = function.getChild(1); + values = function.getChild(0); + } + SlotRef slot = directSlot(input); + ResolvedField field = slot == null ? null : findField(slot); + if (field == null || !LanceSubstraitSerializer.supportsStringList(field.field) + || !(values instanceof ArrayLiteral) || values.getChildren().isEmpty()) { + return Optional.empty(); + } + // Nereids folds three or more membership disjuncts into arrays_overlap. Expand + // only non-NULL string needles: Doris also matches NULL elements to NULL needles. + List<Expression> level = new ArrayList<>(); + for (Expr needle : values.getChildren()) { + if (!(needle instanceof StringLiteral)) { + return Optional.empty(); + } + level.add(arrayMembership(field, needle.getStringValue())); + } + // Keep expression depth logarithmic for large constant label lists. + while (level.size() > 1) { + List<Expression> next = new ArrayList<>(); + for (int i = 0; i < level.size(); i += 2) { + next.add(i + 1 == level.size() ? level.get(i) + : booleanFunction("or:bool", Arrays.asList(level.get(i), level.get(i + 1)))); + } + level = next; + } + return Optional.of(level.get(0)); + } + + private Optional<Expression> convertArrayContains(FunctionCallExpr function) { + SlotRef slot = directSlot(function.getChild(0)); + ResolvedField field = slot == null ? null : findField(slot); + Expr needle = function.getChild(1); + if (field == null || !LanceSubstraitSerializer.supportsStringList(field.field) + || !(needle instanceof StringLiteral)) { + return Optional.empty(); + } + // Doris matches a NULL needle against NULL elements; DataFusion does not. Also, + // array_contains_all is a contiguous subsequence test, not DataFusion's set containment. + // Only non-NULL scalar membership is interchangeable across the two engines. + return Optional.of(arrayMembership(field, needle.getStringValue())); + } + + private Expression arrayMembership(ResolvedField field, String needle) { + return Expression.ScalarFunctionInvocation.builder() + .declaration(EXTENSIONS.getScalarFunction( + SimpleExtension.FunctionAnchor.of(LANCE_FUNCTIONS, "array_has:list_str"))) + .outputType(TypeCreator.of(field.field.isNullable()).BOOLEAN) + .arguments(Arrays.asList(fieldReference(field), + ExpressionCreator.string(false, needle))) + .build(); + } + private Optional<Expression> convertStringPredicate( String function, Expr input, Expr pattern, boolean rejectEscapedPattern) { SlotRef slot = directSlot(input); @@ -497,8 +563,9 @@ public class LancePredicateConverter { } private static SimpleExtension.ExtensionCollection loadExtensions() { - try { - return SimpleExtension.loadDefaults(); + try (InputStream definitions = + LancePredicateConverter.class.getResourceAsStream("/substrait/lance_functions.yaml")) { + return SimpleExtension.loadDefaults().merge(SimpleExtension.load(LANCE_FUNCTIONS, definitions)); } catch (Exception e) { throw new IllegalStateException("Failed to load Substrait extension definitions", e); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScalarIndexPlanner.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScalarIndexPlanner.java index ab259a05fe2..4d6319242f6 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScalarIndexPlanner.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScalarIndexPlanner.java @@ -17,9 +17,17 @@ package org.apache.doris.datasource.lance.source; +import org.apache.doris.analysis.ArrayLiteral; +import org.apache.doris.analysis.BinaryPredicate; import org.apache.doris.analysis.CompoundPredicate; import org.apache.doris.analysis.Expr; +import org.apache.doris.analysis.FunctionCallExpr; +import org.apache.doris.analysis.InPredicate; +import org.apache.doris.analysis.IsNullPredicate; +import org.apache.doris.analysis.LikePredicate; +import org.apache.doris.analysis.LiteralExpr; import org.apache.doris.analysis.SlotRef; +import org.apache.doris.analysis.StringLiteral; import org.apache.doris.datasource.lance.index.LanceIndexSegmentGroup; import org.apache.doris.datasource.lance.index.LanceIndexSegmentInfo; import org.apache.doris.datasource.lance.metadata.LanceFragmentInfo; @@ -28,6 +36,7 @@ import org.apache.doris.datasource.lance.metadata.LanceTableMetadata; import org.lance.index.IndexType; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -35,6 +44,11 @@ import java.util.Set; /** Assigns one BTree/Bitmap/LabelList segment and a disjoint fragment domain to each ordinary scan task. */ final class LanceScalarIndexPlanner { + // Match the pinned lance-c scoped-expression bounds. Exceeding either bound makes + // native planning scan the whole domain, so do not coalesce fragments in that case. + private static final int MAX_EXPRESSION_NODES = 128; + private static final int MAX_EXPRESSION_DEPTH = 32; + static final class Plan { final String indexName; final LanceSplitBuilder splits; @@ -53,23 +67,39 @@ final class LanceScalarIndexPlanner { || !metadata.getIndexMetadataState().canPlanIndexSegments()) { return null; } - Set<Integer> filterFields = collectFilterFields(metadata, pushedConjuncts); - if (filterFields.isEmpty()) { + if (exceedsExpressionBudget(pushedConjuncts)) { return null; } // Metadata already groups physical segments by logical index. Name order // provides a stable winner when multiple indices cover the same number of rows. List<LanceIndexSegmentGroup> indices = new ArrayList<>(metadata.getIndexes()); indices.sort(java.util.Comparator.comparing(LanceIndexSegmentGroup::getName)); + // Native chooses the first matching parser, not the index with the widest + // coverage. Without its dispatch order, leave competing logical indexes to native. + Set<Integer> indexedFields = new HashSet<>(); + Set<Integer> ambiguousFields = new HashSet<>(); + for (LanceIndexSegmentGroup index : indices) { + for (Integer field : index.getSegments().get(0).getFieldIds()) { + if (!indexedFields.add(field)) { + ambiguousFields.add(field); + } + } + } Plan selected = null; + Map<IndexType, Set<Integer>> fieldsByIndexType = new HashMap<>(); for (LanceIndexSegmentGroup logicalIndex : indices) { List<LanceIndexSegmentInfo> segments = logicalIndex.getSegments(); - // PR #79 supports one top-level key in BTree/Bitmap/LabelList indices. Lance - // performs the final typed driver selection and falls back within the same domain. + // Only coalesce a segment when a positive, indexable necessary condition + // exists. Broad complements keep fragment parallelism without repeating a + // segment-wide index search once per fragment. LanceIndexSegmentInfo index = segments.get(0); if ((index.getIndexType() != IndexType.BTREE && index.getIndexType() != IndexType.BITMAP && index.getIndexType() != IndexType.LABEL_LIST) - || index.getFieldIds().size() != 1 || !filterFields.contains(index.getFieldIds().get(0))) { + || index.getFieldIds().size() != 1 + || ambiguousFields.contains(index.getFieldIds().get(0)) + || !fieldsByIndexType.computeIfAbsent(index.getIndexType(), + type -> collectFilterFields(metadata, pushedConjuncts, type)) + .contains(index.getFieldIds().get(0))) { continue; } Plan candidate = groupFragments(metadata, segments, visibleFragments); @@ -80,26 +110,199 @@ final class LanceScalarIndexPlanner { return selected; } - private static Set<Integer> collectFilterFields(LanceTableMetadata metadata, List<Expr> pushedConjuncts) { - Set<SlotRef> slots = new HashSet<>(); - pushedConjuncts.forEach(expr -> collectDriverSlots(expr, slots)); + private static Set<Integer> collectFilterFields(LanceTableMetadata metadata, + List<Expr> pushedConjuncts, IndexType indexType) { Set<Integer> fields = new HashSet<>(); + for (Expr expr : pushedConjuncts) { + fields.addAll(collectDriverFields(metadata, expr, indexType, false)); + } + return fields; + } + + private static Set<Integer> collectDriverFields(LanceTableMetadata metadata, Expr expr, + IndexType indexType, boolean requireExact) { + Set<Integer> fields = new HashSet<>(); + if (expr instanceof CompoundPredicate) { + CompoundPredicate.Operator op = ((CompoundPredicate) expr).getOp(); + if (op == CompoundPredicate.Operator.NOT) { + return fields; + } + boolean exactBranches = requireExact || op == CompoundPredicate.Operator.OR; + Set<Integer> left = collectDriverFields(metadata, expr.getChild(0), indexType, exactBranches); + Set<Integer> right = collectDriverFields(metadata, expr.getChild(1), indexType, exactBranches); + if (op == CompoundPredicate.Operator.AND) { + // A refine-only conjunct anywhere below OR makes native reject the + // union, even if its sibling could independently drive this index. + if (requireExact && (left.isEmpty() || right.isEmpty())) { + return fields; + } + left.addAll(right); + } else { + // Sharing a slot is insufficient: both OR branches must actually be + // indexable. A suffix LIKE, for example, cannot supply BTree candidates. + left.retainAll(right); + } + return left; + } + if (!isPositiveIndexLeaf(expr, indexType, requireExact)) { + return fields; + } + Set<SlotRef> slots = new HashSet<>(); + expr.collect(SlotRef.class, slots); for (SlotRef slot : slots) { metadata.getLanceFieldId(slot.getColumnName()).ifPresent(fields::add); } return fields; } - private static void collectDriverSlots(Expr expr, Set<SlotRef> slots) { - // A predicate below OR or NOT is not a necessary condition of the whole filter. - // Do not select its index and then force every task into a non-indexed fallback. + private static boolean isPositiveIndexLeaf(Expr expr, IndexType indexType, boolean requireExact) { + if (indexType == IndexType.LABEL_LIST) { + if (!(expr instanceof FunctionCallExpr) || expr.getChildren().size() != 2) { + return false; + } + String name = ((FunctionCallExpr) expr).getFnName().getFunction(); + if ("array_contains".equalsIgnoreCase(name)) { + return expr.getChild(0) instanceof SlotRef && expr.getChild(1) instanceof LiteralExpr; + } + return "arrays_overlap".equalsIgnoreCase(name) && overlapSize(expr) > 0; + } + if (expr instanceof BinaryPredicate) { + BinaryPredicate.Operator op = ((BinaryPredicate) expr).getOp(); + return (op == BinaryPredicate.Operator.EQ || op == BinaryPredicate.Operator.GT + || op == BinaryPredicate.Operator.GE || op == BinaryPredicate.Operator.LT + || op == BinaryPredicate.Operator.LE) + && ((expr.getChild(0) instanceof SlotRef && expr.getChild(1) instanceof LiteralExpr) + || (expr.getChild(1) instanceof SlotRef && expr.getChild(0) instanceof LiteralExpr)); + } + if (expr instanceof InPredicate) { + return !((InPredicate) expr).isNotIn() && expr.getChild(0) instanceof SlotRef; + } + if (expr instanceof IsNullPredicate) { + return !((IsNullPredicate) expr).isNotNull() && expr.getChild(0) instanceof SlotRef; + } + // Bitmap has no prefix-query support. A refined LIKE can drive an AND, + // but native's OR planner rejects branches that need a residual recheck. + if (indexType != IndexType.BTREE || expr.getChildren().size() != 2 + || !(expr.getChild(0) instanceof SlotRef) || !(expr.getChild(1) instanceof StringLiteral)) { + return false; + } + String name = expr instanceof FunctionCallExpr + ? ((FunctionCallExpr) expr).getFnName().getFunction() : ""; + String prefix = ((StringLiteral) expr.getChild(1)).getStringValue(); + if ("starts_with".equalsIgnoreCase(name)) { + // Unlike LIKE, every character in starts_with is literal, including % and _. + return !prefix.isEmpty(); + } + boolean like = (expr instanceof LikePredicate && ((LikePredicate) expr).getOp() == LikePredicate.Operator.LIKE) + || "like".equalsIgnoreCase(name); + if (!like || prefix.isEmpty() || prefix.indexOf('\\') >= 0) { + return false; + } + for (int i = 0; i < prefix.length(); i++) { + char character = prefix.charAt(i); + if (character == '%' || character == '_') { + return i > 0 && (!requireExact || (character == '%' && i == prefix.length() - 1)); + } + } + return true; + } + + private static int overlapSize(Expr expr) { + if (!(expr instanceof FunctionCallExpr) || expr.getChildren().size() != 2 + || !"arrays_overlap".equalsIgnoreCase(((FunctionCallExpr) expr).getFnName().getFunction())) { + return 0; + } + for (int i = 0; i < 2; i++) { + if (expr.getChild(i) instanceof ArrayLiteral && expr.getChild(1 - i) instanceof SlotRef) { + return expr.getChild(i).getChildren().size(); + } + } + return 0; + } + + static boolean shouldDisableFragmentIndex(List<Expr> pushedConjuncts) { + if (pushedConjuncts.isEmpty()) { + return false; + } + // Missing metadata or an ambiguous index name is not evidence against native + // index use. Disable only known expensive shapes, independent of FE discovery. + return exceedsExpressionBudget(pushedConjuncts) + || pushedConjuncts.stream().noneMatch(LanceScalarIndexPlanner::hasPositivePredicate); + } + + private static boolean hasPositivePredicate(Expr expr) { if (expr instanceof CompoundPredicate) { - if (((CompoundPredicate) expr).getOp() == CompoundPredicate.Operator.AND) { - expr.getChildren().forEach(child -> collectDriverSlots(child, slots)); + return ((CompoundPredicate) expr).getOp() != CompoundPredicate.Operator.NOT + && expr.getChildren().stream().anyMatch(LanceScalarIndexPlanner::hasPositivePredicate); + } + if (expr instanceof BinaryPredicate) { + return ((BinaryPredicate) expr).getOp() != BinaryPredicate.Operator.NE; + } + if (expr instanceof InPredicate) { + return !((InPredicate) expr).isNotIn(); + } + if (expr instanceof IsNullPredicate) { + return !((IsNullPredicate) expr).isNotNull(); + } + return true; + } + + private static boolean exceedsExpressionBudget(List<Expr> conjuncts) { + return conjunctionNodes(conjuncts, 0, conjuncts.size(), 0) > MAX_EXPRESSION_NODES; + } + + private static int conjunctionNodes(List<Expr> conjuncts, int begin, int end, int depth) { + if (begin == end) { + return 0; + } + if (end - begin == 1) { + return expressionNodes(conjuncts.get(begin), depth); + } + // The converter emits n-ary and:bool; DataFusion splits its arguments in + // halves. Preserve each leaf's actual depth instead of assuming a left-deep AND. + int middle = begin + (end - begin) / 2; + return 1 + conjunctionNodes(conjuncts, begin, middle, depth + 1) + + conjunctionNodes(conjuncts, middle, end, depth + 1); + } + + private static int expressionNodes(Expr expr, int depth) { + if (depth > MAX_EXPRESSION_DEPTH) { + return MAX_EXPRESSION_NODES + 1; + } + if (expr instanceof CompoundPredicate) { + int nodes = 1; + for (Expr child : expr.getChildren()) { + nodes += expressionNodes(child, depth + 1); + if (nodes > MAX_EXPRESSION_NODES) { + break; + } + } + return nodes; + } + int labels = overlapSize(expr); + // The converter emits a balanced OR tree: N memberships require 2*N-1 nodes. + if (labels > 0) { + int levels = 32 - Integer.numberOfLeadingZeros(labels - 1); + if (labels > MAX_EXPRESSION_NODES / 2 || depth + levels > MAX_EXPRESSION_DEPTH) { + return MAX_EXPRESSION_NODES + 1; + } + return 2 * labels - 1; + } + if (expr instanceof InPredicate) { + InPredicate in = (InPredicate) expr; + int values = in.getInElementNum(); + // DataFusion 54 expands up to three values into a left-deep OR (or + // AND of negated equalities for NOT IN) before the native budget check. + if (values > 0 && values <= 3) { + int negation = in.isNotIn() ? 1 : 0; + if (depth + values - 1 + negation > MAX_EXPRESSION_DEPTH) { + return MAX_EXPRESSION_NODES + 1; + } + return (2 + negation) * values - 1; } - } else { - expr.collect(SlotRef.class, slots); + return in.isNotIn() ? 2 : 1; } + return 1; } private static Plan groupFragments(LanceTableMetadata metadata, List<LanceIndexSegmentInfo> segments, diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScanPlanner.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScanPlanner.java index e95420582b6..23fc3eeb5dd 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScanPlanner.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScanPlanner.java @@ -195,7 +195,11 @@ final class LanceScanPlanner { } else { plan = new LanceSplitBuilder(metadata.getDatasetUri(), metadata.getVersion(), 0); } - plan.addUncoveredFragments(visibleFragments.values(), 1, scalarIndexPlan != null); + // Uncovered fragments must not repeat the selected segment's search. When FE + // could not select a segment, retain native selection unless the filter itself + // is known to cause expensive repeated searches (a complement or oversized tree). + plan.addUncoveredFragments(visibleFragments.values(), 1, scalarIndexPlan != null + || LanceScalarIndexPlanner.shouldDisableFragmentIndex(lancePushedConjuncts)); return plan.buildSplits(); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceSubstraitSerializer.java b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceSubstraitSerializer.java index e5323bde815..79d3f479261 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceSubstraitSerializer.java +++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceSubstraitSerializer.java @@ -108,10 +108,19 @@ final class LanceSubstraitSerializer { || type instanceof ArrowType.LargeUtf8; } + static boolean supportsStringList(Field field) { + // Keep this separate from scalar comparability: list serialization must not enable + // array comparisons, IN, or null checks without their own semantic coverage. + return field.getType() instanceof ArrowType.List && field.getChildren().size() == 1 + && field.getChildren().get(0).getType() instanceof ArrowType.Utf8; + } + static Type fieldType(Field field) { TypeCreator creator = TypeCreator.of(field.isNullable()); ArrowType type = field.getType(); - if (type instanceof ArrowType.Bool) { + if (supportsStringList(field)) { + return creator.list(fieldType(field.getChildren().get(0))); + } else if (type instanceof ArrowType.Bool) { return creator.BOOLEAN; } else if (type instanceof ArrowType.Int) { switch (((ArrowType.Int) type).getBitWidth()) { @@ -198,6 +207,13 @@ final class LanceSubstraitSerializer { } private static Optional<io.substrait.proto.Type> toSubstraitProtoType(Field field) { + if (supportsStringList(field)) { + return Optional.of(io.substrait.proto.Type.newBuilder() + .setList(io.substrait.proto.Type.List.newBuilder() + .setNullability(nullability(field)) + .setType(toSubstraitProtoType(field.getChildren().get(0)).get())) + .build()); + } if (!supportsType(field.getType())) { return Optional.empty(); } diff --git a/fe/fe-core/src/main/resources/substrait/lance_functions.yaml b/fe/fe-core/src/main/resources/substrait/lance_functions.yaml new file mode 100644 index 00000000000..3e6f64ecbb9 --- /dev/null +++ b/fe/fe-core/src/main/resources/substrait/lance_functions.yaml @@ -0,0 +1,27 @@ +# 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. + +%YAML 1.2 +--- +scalar_functions: + - name: array_has + description: Non-null UTF-8 label membership in a List<Utf8> column. + impls: + - args: + - value: list<string> + - value: string + return: boolean diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java index 64e6fd70724..148fbf1e53d 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/LancePredicateConverterTest.java @@ -17,6 +17,7 @@ package org.apache.doris.datasource.lance; +import org.apache.doris.analysis.ArrayLiteral; import org.apache.doris.analysis.BinaryPredicate; import org.apache.doris.analysis.CompoundPredicate; import org.apache.doris.analysis.DateLiteral; @@ -29,8 +30,10 @@ import org.apache.doris.analysis.IntLiteral; import org.apache.doris.analysis.IsNullPredicate; import org.apache.doris.analysis.LargeIntLiteral; import org.apache.doris.analysis.LikePredicate; +import org.apache.doris.analysis.NullLiteral; import org.apache.doris.analysis.SlotRef; import org.apache.doris.analysis.StringLiteral; +import org.apache.doris.catalog.ArrayType; import org.apache.doris.catalog.ScalarFunction; import org.apache.doris.catalog.ScalarType; import org.apache.doris.catalog.Type; @@ -73,6 +76,118 @@ public class LancePredicateConverterTest { Field.nullable("timestamp_ns", new ArrowType.Timestamp(TimeUnit.NANOSECOND, null)), Field.nullable("timestamp_us_utc", new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC"))))); + @Test + public void testStringArrayMembershipSchemaAndFunction() throws Exception { + // An unsupported preceding field must not shift the array's field reference. + Schema schema = new Schema(Arrays.asList( + Field.nullable("nested", ArrowType.Struct.INSTANCE), + new Field("labels", FieldType.nullable(ArrowType.List.INSTANCE), + Collections.singletonList(Field.nullable("item", ArrowType.Utf8.INSTANCE))))); + Expr predicate = arrayFunction("array_contains", new SlotRef(null, "labels"), new StringLiteral("red")); + LancePredicateConverter.ConversionResult result = + new LancePredicateConverter(schema).convert(Collections.singletonList(predicate)); + Assertions.assertEquals(1, result.getPushedConjuncts().size()); + Assertions.assertTrue(result.getResidualConjuncts().isEmpty()); + ExtendedExpression envelope = ExtendedExpression.parseFrom(result.getSubstraitFilter()); + Assertions.assertEquals(Arrays.asList("__unlikely_name_placeholder_doris_0", "labels"), + envelope.getBaseSchema().getNamesList()); + io.substrait.proto.Type.List list = envelope.getBaseSchema().getStruct().getTypes(1).getList(); + Assertions.assertEquals(io.substrait.proto.Type.Nullability.NULLABILITY_NULLABLE, list.getNullability()); + Assertions.assertTrue(list.getType().hasString()); + Assertions.assertEquals(io.substrait.proto.Type.Nullability.NULLABILITY_NULLABLE, + list.getType().getString().getNullability()); + io.substrait.proto.Expression.ScalarFunction function = + envelope.getReferredExpr(0).getExpression().getScalarFunction(); + Assertions.assertEquals(1, function.getArguments(0).getValue().getSelection() + .getDirectReference().getStructField().getField()); + Assertions.assertEquals("red", function.getArguments(1).getValue().getLiteral().getString()); + Assertions.assertEquals(io.substrait.proto.Type.Nullability.NULLABILITY_NULLABLE, + function.getOutputType().getBool().getNullability()); + Assertions.assertTrue(envelope.getExtensionsList().stream() + .anyMatch(extension -> extension.hasExtensionFunction() + && extension.getExtensionFunction().getName().startsWith("array_has:"))); + } + + @Test + public void testArrayMembershipBooleanCombinations() { + LancePredicateConverter arrays = arrayConverter(ArrowType.List.INSTANCE, ArrowType.Utf8.INSTANCE); + Expr red = arrayFunction("array_contains", new SlotRef(null, "labels"), new StringLiteral("red")); + Expr blue = arrayFunction("array_contains", new SlotRef(null, "labels"), new StringLiteral("blue")); + for (Expr predicate : Arrays.asList(red, + new CompoundPredicate(CompoundPredicate.Operator.AND, red, blue), + new CompoundPredicate(CompoundPredicate.Operator.OR, red, blue), + new CompoundPredicate(CompoundPredicate.Operator.NOT, red, null))) { + Assertions.assertEquals(1, arrays.convert(Collections.singletonList(predicate)) + .getPushedConjuncts().size()); + } + } + + @Test + public void testRewrittenArrayOverlapPushdown() throws Exception { + LancePredicateConverter arrays = arrayConverter(ArrowType.List.INSTANCE, ArrowType.Utf8.INSTANCE); + Expr labels = new SlotRef(null, "labels"); + Expr values = new ArrayLiteral(ArrayType.create(Type.STRING, true), + new StringLiteral("red"), new StringLiteral("blue"), new StringLiteral("green")); + for (Expr predicate : Arrays.asList(arrayFunction("arrays_overlap", labels, values), + arrayFunction("arrays_overlap", values, labels), + new CompoundPredicate(CompoundPredicate.Operator.NOT, + arrayFunction("arrays_overlap", labels, values), null))) { + LancePredicateConverter.ConversionResult result = arrays.convert(Collections.singletonList(predicate)); + Assertions.assertTrue(result.getResidualConjuncts().isEmpty()); + String encoded = ExtendedExpression.parseFrom(result.getSubstraitFilter()).toString(); + Assertions.assertTrue(encoded.contains("array_has:list_str")); + Assertions.assertTrue(encoded.contains("or:bool")); + Assertions.assertTrue(encoded.contains("green")); + } + // A NULL needle can match a NULL element in Doris and must not become array_has. + for (Expr valuesWithDifferentSemantics : Arrays.asList( + new ArrayLiteral(ArrayType.create(Type.STRING, true), new StringLiteral("red"), new NullLiteral()), + new ArrayLiteral(), new SlotRef(null, "labels"))) { + Expr predicate = arrayFunction("arrays_overlap", labels, valuesWithDifferentSemantics); + Assertions.assertEquals(Collections.singletonList(predicate), + arrays.convert(Collections.singletonList(predicate)).getResidualConjuncts()); + } + } + + @Test + public void testUnsafeArrayMembershipRemainsResidual() { + LancePredicateConverter arrays = arrayConverter(ArrowType.List.INSTANCE, ArrowType.Utf8.INSTANCE); + Expr red = arrayFunction("array_contains", new SlotRef(null, "labels"), new StringLiteral("red")); + Expr nullNeedle = arrayFunction("array_contains", new SlotRef(null, "labels"), new NullLiteral()); + FunctionCallExpr udf = arrayFunction("array_contains", new SlotRef(null, "labels"), new StringLiteral("red")); + udf.getFn().setBinaryType(TFunctionBinaryType.JAVA_UDF); + for (Expr predicate : Arrays.asList(nullNeedle, udf, + arrayFunction("array_contains_all", new SlotRef(null, "labels"), new StringLiteral("red")), + arrayFunction("array_contains", new SlotRef(null, "labels"), new SlotRef(null, "needle")), + new CompoundPredicate(CompoundPredicate.Operator.OR, red, nullNeedle), + new CompoundPredicate(CompoundPredicate.Operator.NOT, nullNeedle, null), + new IsNullPredicate(new SlotRef(null, "labels"), false))) { + Assertions.assertEquals(Collections.singletonList(predicate), + arrays.convert(Collections.singletonList(predicate)).getResidualConjuncts()); + } + Assertions.assertEquals(1, arrays.convert(Arrays.asList(red, nullNeedle)).getPushedConjuncts().size()); + for (LancePredicateConverter unsupported : Arrays.asList( + arrayConverter(ArrowType.List.INSTANCE, new ArrowType.Int(64, true)), + arrayConverter(ArrowType.List.INSTANCE, ArrowType.LargeUtf8.INSTANCE), + arrayConverter(ArrowType.LargeList.INSTANCE, ArrowType.Utf8.INSTANCE), + arrayConverter(new ArrowType.FixedSizeList(2), ArrowType.Utf8.INSTANCE))) { + Assertions.assertEquals(1, unsupported.convert(Collections.singletonList(red)).getResidualConjuncts().size()); + } + } + + private static LancePredicateConverter arrayConverter(ArrowType container, ArrowType element) { + return new LancePredicateConverter(new Schema(Collections.singletonList( + new Field("labels", FieldType.nullable(container), + Collections.singletonList(Field.nullable("item", element)))))); + } + + private static FunctionCallExpr arrayFunction(String name, Expr... arguments) { + FunctionCallExpr function = new FunctionCallExpr(name, Arrays.asList(arguments)); + function.setFn(new ScalarFunction(new FunctionName(name), + Arrays.asList(ArrayType.create(Type.STRING, true), Type.STRING), Type.BOOLEAN, false, true)); + return function; + } + @Test public void testComparisonAndStringLiteralEncoding() throws Exception { Expr rowId = new SlotRef(null, "row_id"); diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/source/LanceScalarIndexPlannerTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/source/LanceScalarIndexPlannerTest.java new file mode 100644 index 00000000000..d94e2f55474 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/source/LanceScalarIndexPlannerTest.java @@ -0,0 +1,292 @@ +// 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. + +package org.apache.doris.datasource.lance.source; + +import org.apache.doris.analysis.ArrayLiteral; +import org.apache.doris.analysis.BinaryPredicate; +import org.apache.doris.analysis.CompoundPredicate; +import org.apache.doris.analysis.Expr; +import org.apache.doris.analysis.FunctionCallExpr; +import org.apache.doris.analysis.FunctionName; +import org.apache.doris.analysis.InPredicate; +import org.apache.doris.analysis.IntLiteral; +import org.apache.doris.analysis.LikePredicate; +import org.apache.doris.analysis.SlotRef; +import org.apache.doris.analysis.StringLiteral; +import org.apache.doris.catalog.ArrayType; +import org.apache.doris.catalog.ScalarFunction; +import org.apache.doris.catalog.Type; +import org.apache.doris.datasource.lance.index.LanceIndexSegmentInfo; +import org.apache.doris.datasource.lance.metadata.LanceFragmentInfo; +import org.apache.doris.datasource.lance.metadata.LanceTableAccess; +import org.apache.doris.datasource.lance.metadata.LanceTableMetadata; + +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.lance.index.IndexType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class LanceScalarIndexPlannerTest { + @Test + public void testBooleanDriverSelectionPreservesAllBranches() { + Expr key = equal("key", 1); + Expr otherKey = equal("key", 2); + Expr residual = equal("other", 3); + for (Expr filter : Arrays.asList(or(key, otherKey), + or(and(key, residual), and(otherKey, residual)), and(not(and(key, residual)), key))) { + LanceScalarIndexPlanner.Plan plan = plan(filter); + Assertions.assertNotNull(plan, filter.toSql()); + Assertions.assertEquals("key_idx", plan.indexName); + Assertions.assertEquals(1, plan.splits.splitCount()); + Assertions.assertTrue(plan.splits.isCoveredByIndexSegment(1)); + Assertions.assertTrue(plan.splits.isCoveredByIndexSegment(2)); + } + // Pruning either an OR branch or an AND below NOT could drop matching rows. + for (Expr filter : Arrays.asList(not(key), not(or(key, otherKey)), or(key, residual), not(and(key, residual)), + not(or(key, residual)), or(and(key, residual), residual))) { + Assertions.assertNull(plan(filter), filter.toSql()); + } + } + + @Test + public void testConvertedArrayPredicatesSelectLabelList() { + Schema schema = new Schema(Collections.singletonList(new Field("labels", + FieldType.nullable(ArrowType.List.INSTANCE), + Collections.singletonList(Field.nullable("item", ArrowType.Utf8.INSTANCE))))); + LanceFragmentInfo fragment = new LanceFragmentInfo(1, 10, 10); + LanceTableMetadata metadata = LanceTableMetadata.createSnapshotWithIndexes( + new LanceTableAccess("s3://bucket/labels.lance", Collections.emptyMap()), 42, schema, + Collections.singletonList(fragment), Collections.singletonMap("labels", 9), + Collections.singletonList(new LanceIndexSegmentInfo(UUID.randomUUID(), "labels_idx", + Collections.singletonList(9), Collections.singletonList(1L), IndexType.LABEL_LIST, null))); + FunctionCallExpr red = contains("red"); + FunctionCallExpr blue = contains("blue"); + for (Expr filter : Arrays.asList(red, and(red, blue), or(red, blue))) { + LancePredicateConverter.ConversionResult converted = + new LancePredicateConverter(schema).convert(Collections.singletonList(filter)); + Assertions.assertTrue(converted.getResidualConjuncts().isEmpty()); + LanceScalarIndexPlanner.Plan plan = LanceScalarIndexPlanner.plan(metadata, + converted.getPushedConjuncts(), Collections.singletonMap(1L, fragment)); + Assertions.assertNotNull(plan); + Assertions.assertEquals("labels_idx", plan.indexName); + Assertions.assertEquals(1, plan.splits.splitCount()); + } + } + + @Test + public void testPositiveDriverWithComplementSearchesSegmentOnce() { + Expr key = equal("key", 1); + Expr otherNotEqual = new BinaryPredicate(BinaryPredicate.Operator.NE, + new SlotRef(null, "other"), new IntLiteral(0)); + Assertions.assertEquals(1, plan(Arrays.asList(key, otherNotEqual), false).splits.splitCount()); + Assertions.assertEquals(1, plan(and(key, otherNotEqual)).splits.splitCount()); + Assertions.assertEquals(1, plan(and(key, not(equal("key", 2)))).splits.splitCount()); + } + + @Test + public void testUnindexableOrBranchDoesNotGroupFragments() { + Expr suffix = new LikePredicate(LikePredicate.Operator.LIKE, + new SlotRef(null, "key"), new StringLiteral("%y%")); + Expr key = new BinaryPredicate(BinaryPredicate.Operator.EQ, + new SlotRef(null, "key"), new StringLiteral("x")); + Assertions.assertNull(plan(Collections.singletonList(or(key, suffix)), true)); + Assertions.assertNotNull(plan(Collections.singletonList(and(key, suffix)), true)); + Expr prefix = new LikePredicate(LikePredicate.Operator.LIKE, + new SlotRef(null, "key"), new StringLiteral("y%")); + Assertions.assertEquals(1, plan(Collections.singletonList(or(key, prefix)), true).splits.splitCount()); + } + + @Test + public void testExpressionDepthBudget() { + Expr filter = equal("key", 1); + for (int depth = 1; depth <= 32; depth++) { + filter = and(filter, equal("key", depth)); + } + Assertions.assertNotNull(plan(filter)); + Assertions.assertNull(plan(and(filter, equal("key", 33)))); + } + + @Test + public void testOverlapExpressionBudget() throws Exception { + Schema schema = new Schema(Collections.singletonList(new Field("labels", + FieldType.nullable(ArrowType.List.INSTANCE), + Collections.singletonList(Field.nullable("item", ArrowType.Utf8.INSTANCE))))); + LanceFragmentInfo first = new LanceFragmentInfo(1, 10, 10); + LanceFragmentInfo second = new LanceFragmentInfo(2, 10, 10); + Map<Long, LanceFragmentInfo> fragments = new HashMap<>(); + fragments.put(1L, first); + fragments.put(2L, second); + LanceTableMetadata metadata = LanceTableMetadata.createSnapshotWithIndexes( + new LanceTableAccess("s3://bucket/labels.lance", Collections.emptyMap()), 42, schema, + Arrays.asList(first, second), Collections.singletonMap("labels", 9), + Collections.singletonList(new LanceIndexSegmentInfo(UUID.randomUUID(), "labels_idx", + Collections.singletonList(9), Arrays.asList(1L, 2L), IndexType.LABEL_LIST, null))); + for (int count : Arrays.asList(64, 65)) { + StringLiteral[] labels = new StringLiteral[count]; + for (int i = 0; i < count; i++) { + labels[i] = new StringLiteral("label_" + i); + } + FunctionCallExpr overlap = new FunctionCallExpr("arrays_overlap", Arrays.asList( + new SlotRef(null, "labels"), new ArrayLiteral(ArrayType.create(Type.STRING, true), labels))); + overlap.setFn(new ScalarFunction(new FunctionName("arrays_overlap"), + Arrays.asList(ArrayType.create(Type.STRING, true), ArrayType.create(Type.STRING, true)), + Type.BOOLEAN, false, true)); + LancePredicateConverter.ConversionResult converted = new LancePredicateConverter(schema) + .convert(Collections.singletonList(overlap)); + Assertions.assertTrue(converted.getResidualConjuncts().isEmpty()); + LanceScalarIndexPlanner.Plan selected = LanceScalarIndexPlanner.plan(metadata, + converted.getPushedConjuncts(), fragments); + if (count == 64) { + Assertions.assertNotNull(selected); + Assertions.assertEquals(1, selected.splits.splitCount()); + // Count the enclosing AND and its other leaf, not just overlap's 127 nodes. + Assertions.assertNull(LanceScalarIndexPlanner.plan(metadata, + Arrays.asList(overlap, contains("extra")), fragments)); + } else { + Assertions.assertNull(selected); + } + } + } + + @Test + public void testWideConjunctionUsesBalancedDepth() { + List<Expr> filters = new ArrayList<>(); + filters.add(equal("key", 1)); + for (int i = 0; i < 33; i++) { + filters.add(equal("filter_" + i, i)); + } + Assertions.assertNotNull(plan(filters, false)); + } + + @Test + public void testShortInExpansionAtOverlapBudgetBoundary() throws Exception { + Schema schema = new Schema(Arrays.asList(new Field("labels", + FieldType.nullable(ArrowType.List.INSTANCE), + Collections.singletonList(Field.nullable("item", ArrowType.Utf8.INSTANCE))), + Field.nullable("category", new ArrowType.Int(64, true)))); + List<LanceFragmentInfo> fragments = Arrays.asList( + new LanceFragmentInfo(1, 10, 10), new LanceFragmentInfo(2, 10, 10)); + Map<String, Integer> fields = new HashMap<>(); + fields.put("labels", 9); + fields.put("category", 10); + LanceTableMetadata metadata = LanceTableMetadata.createSnapshotWithIndexes( + new LanceTableAccess("s3://bucket/labels.lance", Collections.emptyMap()), 42, schema, + fragments, fields, Arrays.asList( + new LanceIndexSegmentInfo(UUID.randomUUID(), "labels_idx", Collections.singletonList(9), + Arrays.asList(1L, 2L), IndexType.LABEL_LIST, null), + new LanceIndexSegmentInfo(UUID.randomUUID(), "category_idx", Collections.singletonList(10), + Arrays.asList(1L, 2L), IndexType.BTREE, null))); + Map<Long, LanceFragmentInfo> visible = new HashMap<>(); + fragments.forEach(fragment -> visible.put(fragment.getId(), fragment)); + for (int count : Arrays.asList(62, 63)) { + StringLiteral[] labels = new StringLiteral[count]; + for (int i = 0; i < count; i++) { + labels[i] = new StringLiteral("label_" + i); + } + Expr overlap = new FunctionCallExpr("arrays_overlap", Arrays.asList( + new SlotRef(null, "labels"), new ArrayLiteral(ArrayType.create(Type.STRING, true), labels))); + Expr in = new InPredicate(new SlotRef(null, "category"), + Arrays.asList(new IntLiteral(0), new IntLiteral(1)), false); + LanceScalarIndexPlanner.Plan selected = LanceScalarIndexPlanner.plan(metadata, + Arrays.asList(overlap, in), visible); + if (count == 62) { + Assertions.assertNotNull(selected); + } else { + Assertions.assertNull(selected); + } + } + } + + @Test + public void testLiteralAndRefinedPrefixes() { + Expr key = new BinaryPredicate(BinaryPredicate.Operator.EQ, + new SlotRef(null, "key"), new StringLiteral("x")); + for (String prefix : Arrays.asList("test_ns$", "literal%value", "literal\\value")) { + Expr startsWith = new FunctionCallExpr("starts_with", + Arrays.asList(new SlotRef(null, "key"), new StringLiteral(prefix))); + Assertions.assertNotNull(plan(Collections.singletonList(startsWith), true)); + Assertions.assertNotNull(plan(Collections.singletonList(or(key, startsWith)), true)); + } + for (String pattern : Arrays.asList("foo%bar%", "foo_bar%")) { + Expr like = new LikePredicate(LikePredicate.Operator.LIKE, + new SlotRef(null, "key"), new StringLiteral(pattern)); + Assertions.assertNotNull(plan(Collections.singletonList(like), true)); + // Native cannot union an index branch that still carries a refine predicate. + Assertions.assertNull(plan(Collections.singletonList(or(key, like)), true)); + Assertions.assertNull(plan(Collections.singletonList(or(and(key, like), key)), true)); + } + } + + private static FunctionCallExpr contains(String label) { + FunctionCallExpr function = new FunctionCallExpr("array_contains", + Arrays.asList(new SlotRef(null, "labels"), new StringLiteral(label))); + function.setFn(new ScalarFunction(new FunctionName("array_contains"), + Arrays.asList(ArrayType.create(Type.STRING, true), Type.STRING), Type.BOOLEAN, false, true)); + return function; + } + + private static LanceScalarIndexPlanner.Plan plan(Expr filter) { + return plan(Collections.singletonList(filter), false); + } + + private static LanceScalarIndexPlanner.Plan plan(List<Expr> filters, boolean stringKey) { + LanceFragmentInfo first = new LanceFragmentInfo(1, 10, 10); + LanceFragmentInfo second = new LanceFragmentInfo(2, 10, 10); + Map<Long, LanceFragmentInfo> fragments = new HashMap<>(); + fragments.put(1L, first); + fragments.put(2L, second); + Map<String, Integer> fields = new HashMap<>(); + fields.put("key", 9); + fields.put("other", 10); + LanceTableMetadata metadata = LanceTableMetadata.createSnapshotWithIndexes( + new LanceTableAccess("s3://bucket/labels.lance", Collections.emptyMap()), 42, + new Schema(Arrays.asList(Field.nullable("key", stringKey ? ArrowType.Utf8.INSTANCE : new ArrowType.Int(64, true)), + Field.nullable("other", new ArrowType.Int(64, true)))), + Arrays.asList(first, second), fields, Collections.singletonList(new LanceIndexSegmentInfo( + UUID.randomUUID(), "key_idx", Collections.singletonList(9), + Arrays.asList(1L, 2L), IndexType.BTREE, null))); + return LanceScalarIndexPlanner.plan(metadata, filters, fragments); + } + + private static Expr equal(String column, int value) { + return new BinaryPredicate(BinaryPredicate.Operator.EQ, new SlotRef(null, column), new IntLiteral(value)); + } + + private static Expr and(Expr left, Expr right) { + return new CompoundPredicate(CompoundPredicate.Operator.AND, left, right); + } + + private static Expr or(Expr left, Expr right) { + return new CompoundPredicate(CompoundPredicate.Operator.OR, left, right); + } + + private static Expr not(Expr child) { + return new CompoundPredicate(CompoundPredicate.Operator.NOT, child, null); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/source/LanceScanNodeTest.java b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/source/LanceScanNodeTest.java index b68ca1f437c..ac1f0f7218f 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/source/LanceScanNodeTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/lance/source/LanceScanNodeTest.java @@ -234,7 +234,7 @@ public class LanceScanNodeTest { } @Test - public void testScalarSegmentSelectionDoesNotDescendThroughOrOrNot() throws Exception { + public void testScalarSegmentSelectionSupportsSameColumnOrAndNot() throws Exception { Expr predicate = scalarPredicate(); for (Expr filter : Arrays.asList( new CompoundPredicate(CompoundPredicate.Operator.OR, predicate, predicate), @@ -243,11 +243,29 @@ public class LanceScanNodeTest { setMetadata(node, scalarMetadata(Collections.singletonList( scalarSegment(UUID.randomUUID(), IndexType.BTREE, Arrays.asList(1L, 2L, 3L, 4L))))); setPushedConjuncts(node, filter); - Assert.assertEquals(4, node.getSplits(20).size()); + int expectedSplits = ((CompoundPredicate) filter).getOp() == CompoundPredicate.Operator.NOT ? 4 : 1; + List<Split> initialSplits = node.getSplits(20); + Assert.assertEquals(expectedSplits, initialSplits.size()); + if (expectedSplits == 4) { + for (Split split : initialSplits) { + TFileRangeDesc range = new TFileRangeDesc(); + node.setScanParams(range, split); + TLanceFileDesc params = range.getTableFormatParams().getLanceParams(); + Assert.assertFalse(params.isSetIndexSegmentUuids()); + Assert.assertTrue(params.isSetUseScalarIndex()); + Assert.assertFalse(params.isUseScalarIndex()); + } + } setPushedConjuncts(node, new CompoundPredicate(CompoundPredicate.Operator.AND, filter, predicate)); List<Split> splits = node.getSplits(20); Assert.assertEquals(1, splits.size()); - Assert.assertTrue(((LanceSplit) splits.get(0)).getIndexSegmentUuid().isPresent()); + List<Long> assignedFragments = new ArrayList<>(); + for (Split split : splits) { + assignedFragments.addAll(((LanceSplit) split).getFragmentIds()); + Assert.assertTrue(((LanceSplit) split).getIndexSegmentUuid().isPresent()); + } + Collections.sort(assignedFragments); + Assert.assertEquals(Arrays.asList(1L, 2L, 3L, 4L), assignedFragments); } } @@ -286,6 +304,51 @@ public class LanceScanNodeTest { Assert.assertFalse(range.getTableFormatParams().getLanceParams().isSetLimit()); } + @Test + public void testMissingSegmentMetadataPreservesNativeIndexSelection() throws Exception { + List<LanceIndexSegmentInfo> segments = Collections.singletonList( + scalarSegment(UUID.randomUUID(), IndexType.BTREE, Arrays.asList(1L, 2L, 3L, 4L))); + LanceTableMetadata complete = scalarMetadata(segments); + LanceTableAccess access = new LanceTableAccess("s3://bucket/scalar.lance", Collections.emptyMap()); + for (LanceTableMetadata metadata : Arrays.asList( + LanceTableMetadata.createSnapshotWithUnavailableFieldIds(access, 42, + complete.getSchema(), complete.getFragments(), segments), + LanceTableMetadata.createSnapshotWithIndexes(access, 42, + complete.getSchema(), complete.getFragments(), Collections.singletonMap("key", 9), + Collections.emptyList()))) { + assertNativeFragmentSelection(metadata); + } + } + + @Test + public void testCompetingLogicalIndexesPreserveNativeSelection() throws Exception { + List<LanceIndexSegmentInfo> segments = Arrays.asList( + new LanceIndexSegmentInfo(UUID.randomUUID(), "a_key_idx", Collections.singletonList(9), + Collections.singletonList(1L), IndexType.BTREE, null), + new LanceIndexSegmentInfo(UUID.randomUUID(), "z_key_idx", Collections.singletonList(9), + Arrays.asList(1L, 2L, 3L, 4L), IndexType.BTREE, null)); + assertNativeFragmentSelection(scalarMetadata(segments)); + Collections.reverse(segments); + assertNativeFragmentSelection(scalarMetadata(segments)); + } + + private static void assertNativeFragmentSelection(LanceTableMetadata metadata) throws Exception { + LanceScanNode node = newNode(); + setMetadata(node, metadata); + setPushedConjuncts(node, new CompoundPredicate(CompoundPredicate.Operator.OR, + new BinaryPredicate(BinaryPredicate.Operator.EQ, new SlotRef(null, "key"), new IntLiteral(1)), + new BinaryPredicate(BinaryPredicate.Operator.EQ, new SlotRef(null, "key"), new IntLiteral(2)))); + List<Split> splits = node.getSplits(20); + Assert.assertEquals(4, splits.size()); + for (Split split : splits) { + TFileRangeDesc range = new TFileRangeDesc(); + node.setScanParams(range, split); + TLanceFileDesc params = range.getTableFormatParams().getLanceParams(); + Assert.assertFalse(params.isSetIndexSegmentUuids()); + Assert.assertFalse(params.isSetUseScalarIndex()); + } + } + private static Expr scalarPredicate() { return new BinaryPredicate(BinaryPredicate.Operator.GE, new SlotRef(null, "key"), new IntLiteral(2)); } diff --git a/regression-test/suites/external_table_p0/lance/test_lance_array_predicate_pushdown.groovy b/regression-test/suites/external_table_p0/lance/test_lance_array_predicate_pushdown.groovy new file mode 100644 index 00000000000..7c2d21a8e02 --- /dev/null +++ b/regression-test/suites/external_table_p0/lance/test_lance_array_predicate_pushdown.groovy @@ -0,0 +1,154 @@ +// 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. + +import org.apache.doris.regression.action.ProfileAction + +suite("test_lance_array_predicate_pushdown", "p0,external") { + String enabled = context.config.otherConfigs.get("enableIcebergTest") + if (enabled == null || !enabled.equalsIgnoreCase("true")) { + logger.info("Lance array pushdown requires the Iceberg MinIO environment") + return + } + String catalog = "test_lance_array_predicate_pushdown" + String endpoint = "http://${context.config.otherConfigs.get('externalEnvIp')}:" + + context.config.otherConfigs.get('iceberg_minio_port') + sql "DROP CATALOG IF EXISTS ${catalog}" + try { + sql """CREATE CATALOG ${catalog} PROPERTIES ( + "type" = "lance", "lance.catalog.type" = "filesystem", + "warehouse" = "s3://warehouse/lance/predicate_arrays", + "s3.endpoint" = "${endpoint}", "s3.access_key" = "admin", + "s3.secret_key" = "password", "s3.region" = "us-east-1", + "use_path_style" = "true")""" + sql "SET enable_profile = true" + sql "SET profile_level = 2" + def profiles = new ProfileAction(context) + String red = "array_contains(labels, 'red')" + String blue = "array_contains(labels, 'blue')" + String green = "array_contains(labels, 'green')" + def cases = [ + [red, [0, 2, 5, 7, 8, 10, 13, 15], 8, 4], + ["${red} AND ${blue}", [2, 7, 10, 15], 4, 2], + ["${red} OR ${blue}", [0, 1, 2, 5, 6, 7, 8, 9, 10, 13, 14, 15], 12, 6], + // Nereids rewrites this shape to arrays_overlap before Lance conversion. + ["${red} OR ${blue} OR ${green}", [0, 1, 2, 5, 6, 7, 8, 9, 10, 13, 14, 15], 12, 6], + ["arrays_overlap(labels, ['red', 'blue', 'green'])", + [0, 1, 2, 5, 6, 7, 8, 9, 10, 13, 14, 15], 12, 6], + ["NOT arrays_overlap(labels, ['red', 'blue', 'green'])", [3, 11], -1, -1], + ["NOT (${red})", [1, 3, 6, 9, 11, 14], -1, -1], + ["NOT (${green})", [0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15], -1, -1], + ["category IN (0, 1)", [0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15], 11, 6], + ["category = 0 OR category = 1", [0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15], 11, 6], + ["${red} AND category = 1", [7, 10, 13], null], + ["${red} AND category <> 1", [0, 2, 5, 8, 15], null] + ] + for (int size : [64, 65]) { + String needles = (["'red'"] + (1..<size).collect { "'absent_${it}'" }).join(", ") + cases.add(["arrays_overlap(labels, [${needles}])", [0, 2, 5, 7, 8, 10, 13, 15], + size == 64 ? 8 : -1, size == 64 ? 4 : -1]) + } + // Native expands a two-value IN into three index-expression nodes. With + // 63 memberships the combined tree exceeds 128 nodes and must stay parallel. + for (int size : [62, 63]) { + String needles = (["'red'"] + (1..<size).collect { "'absent_${it}'" }).join(", ") + cases.add(["arrays_overlap(labels, [${needles}]) AND category IN (0, 1)", + [0, 7, 10, 13, 15], size == 62 ? null : -1, -1]) + } + for (String table : ["indexed", "partial", "unindexed"]) { + String relation = "${catalog}.`default`.`${table}`" + cases.eachWithIndex { c, caseId -> + String token = "lance_array_${table}_${caseId}_" + UUID.randomUUID().toString() + String query = "SELECT /* ${token} */ id FROM ${relation} WHERE ${c[0]} ORDER BY id" + explain { + sql(query) + contains "lancePushdownPredicate=" + notContains "predicates:" + if (table != "unindexed" && c[2] != -1) { + contains "lanceScalarIndexScan=SEGMENT" + } else { + notContains "lanceScalarIndexScan=SEGMENT" + contains "lanceFragmentGrouping=FRAGMENT" + contains "lanceFragments=2" + } + } + assertEquals(c[1], sql(query).collect { (it[0] as Number).intValue() }) + // A pushed predicate is not necessarily indexed: also verify runtime searches + // and candidate counts, so a non-indexed fallback cannot satisfy this test. + if (table != "unindexed") { + String profile = profiles.getProfileBySql(token, + ["LanceScalarIndexSegmentsSearched", "LanceScalarIndexCandidateRows", + "LanceScalarIndexSegmentFallbacks"]) + def counter = { String name -> + def matches = profile =~ /${name}: (?:sum )?(\d+)\b/ + assertTrue(matches.find(), "Missing counter ${name}: ${profile}") + return matches.group(1).toLong() + } + // Broad complements stay as parallel scans without repeated segment searches. + long expectedSearches = c[2] == -1 ? 0L : 1L + assertEquals(expectedSearches, counter("LanceScalarIndexSegmentsSearched")) + long expectedCandidates = c[2] == null ? -1L : + (c[table == "partial" ? 3 : 2] as Number).longValue() + if (expectedCandidates >= 0) { + assertEquals(expectedCandidates, counter("LanceScalarIndexCandidateRows")) + } + assertEquals(0L, counter("LanceScalarIndexSegmentFallbacks")) + } + } + // NULL needle matching and ordered subsequence matching have Doris-specific semantics. + def residualCases = [ + ["array_contains(labels, NULL)", [6, 14]], + ["arrays_overlap(labels, ['green', NULL])", [6, 14]], + ["arrays_overlap(labels, [])", []], + ["array_contains_all(labels, ['red', 'blue'])", [2, 10]], + ["array_contains_all(labels, ['red', 'red'])", [5, 13]], + ["${red} OR array_contains(labels, NULL)", [0, 2, 5, 6, 7, 8, 10, 13, 14, 15]] + ] + for (def c : residualCases) { + String query = "SELECT id FROM ${relation} WHERE ${c[0]} ORDER BY id" + explain { + sql(query) + contains "predicates:" + notContains "lancePushdownPredicate=" + } + assertEquals(c[1], sql(query).collect { (it[0] as Number).intValue() }) + } + String mixed = "SELECT id FROM ${relation} WHERE ${blue} AND array_contains(labels, NULL) ORDER BY id" + explain { + sql(mixed) + contains "lancePushdownPredicate=" + contains "predicates:" + } + assertEquals([6, 14], sql(mixed).collect { (it[0] as Number).intValue() }) + // An unordered LIMIT reaches the scanner; TopN's bound does not. The two + // selective predicates force results from the indexed and uncovered domains. + for (int expectedId : [5, 13]) { + String limited = "SELECT id FROM ${relation} WHERE ${red} AND id = ${expectedId} LIMIT 1" + explain { + sql(limited) + contains "lanceLimit=1" + contains "lancePushdownPredicate=" + notContains "predicates:" + } + assertEquals([[(long) expectedId]], sql(limited)) + } + // The appended fragment must still contribute matching rows, including with LIMIT. + assertEquals([[15L]], sql("SELECT id FROM ${relation} WHERE ${red} ORDER BY id DESC LIMIT 1")) + } + } finally { + sql "DROP CATALOG IF EXISTS ${catalog}" + } +} diff --git a/thirdparty/build-thirdparty.sh b/thirdparty/build-thirdparty.sh index a5e209a0aed..89c69090e04 100755 --- a/thirdparty/build-thirdparty.sh +++ b/thirdparty/build-thirdparty.sh @@ -156,6 +156,8 @@ if [[ ! -f "${TP_DIR}/vars.sh" ]]; then fi . "${TP_DIR}/vars.sh" +. "${TP_DIR}/lance-install.sh" +LANCE_C_INSTALL_FINGERPRINT="$(lance_c_install_fingerprint "${TP_DIR}")" cd "${TP_DIR}" @@ -2228,9 +2230,14 @@ build_lance_c() { env "${cargo_env[@]}" "${cargo_bin}" "${cargo_args[@]}" mkdir -p "${TP_INSTALL_DIR}/include" "${TP_INSTALL_DIR}/lib64" + # Invalidate before publishing either file so interrupted installs cannot reuse + # a matching marker with a partial header/archive pair. + rm -f "${TP_INSTALL_DIR}/lib64/.lance-c-fingerprint" rm -rf "${TP_INSTALL_DIR}/include/lance" cp -av include/lance "${TP_INSTALL_DIR}/include/" install_rust_archive "${BUILD_DIR}/release/liblance_c.a" + printf '%s\n' "${LANCE_C_INSTALL_FINGERPRINT}" > "${TP_INSTALL_DIR}/lib64/.lance-c-fingerprint.tmp" + mv "${TP_INSTALL_DIR}/lib64/.lance-c-fingerprint.tmp" "${TP_INSTALL_DIR}/lib64/.lance-c-fingerprint" } # paimon-rust diff --git a/thirdparty/lance-install.sh b/thirdparty/lance-install.sh new file mode 100644 index 00000000000..28594d61c79 --- /dev/null +++ b/thirdparty/lance-install.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env 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. + +# Keep the check independent of the installed prefix: an external compilation image +# can carry old vars.sh alongside an ABI-compatible but behaviorally stale archive. +lance_c_install_fingerprint() ( + local definitions="$1" + local TP_DIR="${definitions}" + source "${definitions}/vars.sh" || return 1 + local patch_checksum + patch_checksum="$(cksum < "${definitions}/patches/lance-c-foyer.patch")" || return 1 + printf '%s\n' "${LANCE_C_SOURCE}" "${LANCE_C_MD5SUM}" "${patch_checksum}" +) + +lance_c_install_is_current() { + local definitions="$1" installed="$2" expected + [[ -s "${installed}/lib64/liblance_c.a" && + -s "${installed}/include/lance/lance.h" && + -s "${installed}/include/lance/lance.hpp" && + -s "${installed}/lib64/.lance-c-fingerprint" ]] || return 1 + expected="$(lance_c_install_fingerprint "${definitions}")" || return 1 + [[ "$(cat "${installed}/lib64/.lance-c-fingerprint")" == "${expected}" ]] +} diff --git a/thirdparty/patches/lance-c-foyer.patch b/thirdparty/patches/lance-c-foyer.patch index 73f244c49fd..bb2104ab32d 100644 --- a/thirdparty/patches/lance-c-foyer.patch +++ b/thirdparty/patches/lance-c-foyer.patch @@ -1,8 +1,7 @@ # Foyer data-cache integration for lance-format/lance-c#73. -# Base: 9bd730add2ac70316c1d642b8459011e2dd92022 +# Base: cd63420bfbe27f6f0a1edcc873b9191af7d52852 (lance-format/lance-c main, including #93) # Source: https://github.com/Gabriel39/lance-c/commit/24c7ca4bcb9422c113b0d3e07e4efe1173b0bc9f -# Regenerate the payload in lance-c; do not maintain separate downstream edits: -# git diff --full-index --binary 9bd730add2ac70316c1d642b8459011e2dd92022 24c7ca4bcb9422c113b0d3e07e4efe1173b0bc9f -- . | sed 's/^ $//' +# The unchanged Foyer payload is reapplied to the newer upstream base with git apply --3way. diff --git a/Cargo.lock b/Cargo.lock index 78812f1330e146db295a14276f90f9e28654f399..daf9f85daf3e0ea59bb906e8e3c32470519b084b 100644 --- a/Cargo.lock @@ -199,7 +198,7 @@ index 18654b862440a90aa6bf0dd846e8f1ad6036fd7c..fd134a7151e1df61da201d867a642e9b prost = "0.14" snafu = "0.9" diff --git a/README.md b/README.md -index 9ceccdc4f3f6b5d13dc271be3d2659c7be400306..94de4d1c26d8ab869d1726e2b416d21f21661d6d 100644 +index 9f0871b17b13ebfd96916947b4bee0dd06f6a468..441719c9f3904058127e55d1cd29ea1afc878935 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Based on the [liblance RFC](https://github.com/lance-format/lance/discussions/60 @@ -208,9 +207,9 @@ index 9ceccdc4f3f6b5d13dc271be3d2659c7be400306..94de4d1c26d8ab869d1726e2b416d21f | [x] | Filter pushdown | `lance_scanner_set_substrait_filter()` accepts a serialized Substrait `ExtendedExpression`; `lance_scanner_additional_sql_filter()` adds SQL predicates with AND before scanning starts | +| [x] | Data-file cache | Optional Foyer memory/disk cache for immutable `data/*.lance` reads | - ## Multi-vector search + ## Segment-scoped array label filters -@@ -231,6 +232,35 @@ auto ds = lance::Dataset::open_with_session(session, "data.lance"); +@@ -362,6 +363,35 @@ auto ds = lance::Dataset::open_with_session(session, "data.lance"); auto stats = session.cache_stats(); ``` @@ -247,7 +246,7 @@ index 9ceccdc4f3f6b5d13dc271be3d2659c7be400306..94de4d1c26d8ab869d1726e2b416d21f `lance_dataset_open` takes a `version` argument — `0` means the latest, any diff --git a/include/lance/lance.h b/include/lance/lance.h -index 7630913fd7849d0bff2cb0f31e1ab1199cb9f0fb..1c1d0752efdda5023b29eafe5aa9a87e03dc5303 100644 +index 94772e2a14bf9a8a950dfb2d39112136e27197c0..fe48eb0d9afd1c7157a3bd2230cce7e769120bac 100644 --- a/include/lance/lance.h +++ b/include/lance/lance.h @@ -214,6 +214,36 @@ typedef struct LanceSessionCacheStats { @@ -335,7 +334,7 @@ index 7630913fd7849d0bff2cb0f31e1ab1199cb9f0fb..1c1d0752efdda5023b29eafe5aa9a87e void lance_dataset_close(LanceDataset* dataset); diff --git a/include/lance/lance.hpp b/include/lance/lance.hpp -index 286724e96736e354785e046f2fcfe5ae7c65147b..5070d423453a5d60d31d03ce758ad7dbe83a6ea2 100644 +index edcd39fc3caba387ab490fa84139f24e9376130a..17b71957d0f5c419fe0b3392ce11faa757ddbfe1 100644 --- a/include/lance/lance.hpp +++ b/include/lance/lance.hpp @@ -176,6 +176,13 @@ struct SqlColumn { @@ -2244,7 +2243,7 @@ index 1971510de4a13ee6f04fc39c159b088e78e21d55..ba51c87a8cebb6897c48fb55509f71da // SAFETY: `out_dataset` is non-NULL (checked above) and the caller // guarantees it points to caller-owned, writable storage of size diff --git a/tests/c_api_test.rs b/tests/c_api_test.rs -index 1a25e34a8ca50623812ffedaa7bb5a8c54f7850e..c341859e9434d4578b2833eca6ec313a28f31f32 100644 +index d3bea44529a842fe3ae28ee3f90cd8d45fe0b394..98e6901a2cfeb2cc98df6606e26b04788d4218b2 100644 --- a/tests/c_api_test.rs +++ b/tests/c_api_test.rs @@ -100,10 +100,83 @@ fn create_large_dataset(num_rows: i32) -> (tempfile::TempDir, String) { @@ -2503,7 +2502,7 @@ index 1a25e34a8ca50623812ffedaa7bb5a8c54f7850e..c341859e9434d4578b2833eca6ec313a fn test_dataset_restore_to_current_latest_writes_new_manifest() { // Restoring to the current latest still writes a new manifest. The diff --git a/tests/cpp/test_c_api.c b/tests/cpp/test_c_api.c -index 5df9cde4d8fa971823b9e6c974a8a0db6c9ec9ca..1444e55161631ac8ad976f427d867ad91d358195 100644 +index efad5ed55987976a3f1820220e42fc58bef42638..050543aebc02454cab09f6e0bd38ec10b1d21720 100644 --- a/tests/cpp/test_c_api.c +++ b/tests/cpp/test_c_api.c @@ -171,6 +171,37 @@ static void test_shared_session(const char *uri) { @@ -2544,16 +2543,16 @@ index 5df9cde4d8fa971823b9e6c974a8a0db6c9ec9ca..1444e55161631ac8ad976f427d867ad9 static void test_scan(const char *uri) { printf(" test_scan... "); -@@ -1323,6 +1354,7 @@ int main(int argc, char **argv) { - +@@ -1471,6 +1502,7 @@ int main(int argc, char **argv) { + test_batch_nearest(uri); test_open_and_metadata(uri); test_shared_session(uri); + test_data_cache_session(uri, write_uri); test_scan(uri); + test_distance_range(uri); test_scan_with_limit(uri); - test_scanner_blob_handling(blob_uri); diff --git a/tests/cpp/test_cpp_api.cpp b/tests/cpp/test_cpp_api.cpp -index 0332d1a2687354984d0551e10716df08344a4013..ef64ab0d02bed2f1750c3d388792e4aa44265e24 100644 +index e2d541e4430889aba89ab85c01cc7e3eacf97baa..ac1c10bccfcf399bbbfcfe908d3188484aff0add 100644 --- a/tests/cpp/test_cpp_api.cpp +++ b/tests/cpp/test_cpp_api.cpp @@ -131,6 +131,28 @@ static void test_shared_session(const std::string& uri) { @@ -2585,11 +2584,11 @@ index 0332d1a2687354984d0551e10716df08344a4013..ef64ab0d02bed2f1750c3d388792e4aa static void test_dataset_schema(const std::string& uri) { TEST(test_dataset_schema); -@@ -1211,6 +1233,7 @@ int main(int argc, char** argv) { +@@ -1353,6 +1375,7 @@ int main(int argc, char** argv) { test_dataset_open(uri); test_shared_session(uri); + test_data_cache_session(uri, write_uri); test_dataset_schema(uri); test_scanner_fluent(uri); - test_scanner_async_stream_ownership(uri); + test_distance_range(uri); diff --git a/thirdparty/test/lance-install-test.sh b/thirdparty/test/lance-install-test.sh new file mode 100644 index 00000000000..db93c355c0f --- /dev/null +++ b/thirdparty/test/lance-install-test.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env 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. + +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +work="$(mktemp -d)" +trap 'rm -rf "${work}"' EXIT +mkdir -p "${work}/repo/thirdparty/patches" "${work}/external" +cp "${ROOT}/thirdparty/vars.sh" "${work}/repo/thirdparty/" +cp "${ROOT}/thirdparty/patches/lance-c-foyer.patch" "${work}/repo/thirdparty/patches/" +# Extract the real build gate; all destructive operations stay inside this temporary install. +echo 'set -eo pipefail' > "${work}/gate.sh" +sed -n '/^# build thirdparty libraries if necessary/,/^update_submodule()/p' "${ROOT}/build.sh" \ + | sed '$d' >> "${work}/gate.sh" +export DORIS_HOME="${work}/repo" DORIS_THIRDPARTY="${work}/external" +export TARGET_SYSTEM=Linux CLEAN=0 PARALLEL=1 +export TEST_HELPER="${ROOT}/thirdparty/lance-install.sh" +cp "${TEST_HELPER}" "${work}/repo/thirdparty/" +cp -r "${DORIS_HOME}/thirdparty/." "${DORIS_THIRDPARTY}/" +printf '#!/usr/bin/env bash\nexit 0\n' > "${DORIS_THIRDPARTY}/download-thirdparty.sh" +cat > "${DORIS_THIRDPARTY}/build-thirdparty.sh" <<'BUILDER' +set -euo pipefail +TP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${TP_DIR}/lance-install.sh" +fingerprint="$(lance_c_install_fingerprint "${TP_DIR}")" +echo rebuilt >> "${DORIS_THIRDPARTY}/builds" +installed="${DORIS_THIRDPARTY}/installed" +mkdir -p "${installed}/lib/hadoop_hdfs/native" "${installed}/lib/hadoop_hdfs_3_4/native" "${installed}/lib64" \ + "${installed}/include/lance" "${installed}/include/paimon_rust" +for file in lib/hadoop_hdfs/native/libhdfs.a lib/hadoop_hdfs_3_4/native/libhdfs.a \ + lib64/liblance_c.a lib64/libpaimon_c.a \ + include/lance/lance.h include/lance/lance.hpp include/paimon_rust/paimon.h; do + echo artifact > "${installed}/${file}" +done +printf '%s\n' "${fingerprint}" > "${installed}/lib64/.lance-c-fingerprint" +BUILDER +# A complete legacy image has all old sentinels but no Lance revision marker. +installed="${DORIS_THIRDPARTY}/installed" +mkdir -p "${installed}/lib/hadoop_hdfs/native" "${installed}/lib/hadoop_hdfs_3_4/native" "${installed}/lib64" \ + "${installed}/include/lance" "${installed}/include/paimon_rust" +for file in lib/hadoop_hdfs/native/libhdfs.a lib/hadoop_hdfs_3_4/native/libhdfs.a \ + lib64/liblance_c.a lib64/libpaimon_c.a \ + include/lance/lance.h include/lance/lance.hpp include/paimon_rust/paimon.h; do + echo legacy > "${installed}/${file}" +done +bash "${work}/gate.sh" +[[ -f "${DORIS_THIRDPARTY}/builds" ]] || { echo 'FAIL: reused unversioned Lance archive'; exit 1; } +source "${TEST_HELPER}" +lance_c_install_is_current "${DORIS_HOME}/thirdparty" "${installed}" +check_builds() { + [[ "$(wc -l < "${DORIS_THIRDPARTY}/builds")" -eq "$1" ]] +} +check_builds 1 +bash "${work}/gate.sh" +check_builds 1 +echo 'PASS: legacy image rebuilt; matching install reused' +# Failed preflight must preserve every dependency, not just the Lance archive. +expect_preserved_install() { + cp -a "${installed}" "${work}/saved-install" + cp "${DORIS_THIRDPARTY}/builds" "${work}/saved-builds" + if bash "${work}/gate.sh" > "${work}/preflight.log" 2>&1; then + echo 'FAIL: accepted incomplete or mismatched rebuild sources'; exit 1 + fi + if ! diff -r "${work}/saved-install" "${installed}"; then + echo 'FAIL: rejected rebuild sources after changing installed dependencies'; exit 1 + fi + cmp "${work}/saved-builds" "${DORIS_THIRDPARTY}/builds" + rm -rf "${work}/saved-install" +} +echo stale > "${installed}/lib64/.lance-c-fingerprint" +for file in lance-install.sh vars.sh patches/lance-c-foyer.patch download-thirdparty.sh build-thirdparty.sh; do + mv "${DORIS_THIRDPARTY}/${file}" "${work}/missing-input" + expect_preserved_install + mv "${work}/missing-input" "${DORIS_THIRDPARTY}/${file}" +done +echo 'PASS: incomplete external sources rejected before changing installed dependencies' +echo stale > "${installed}/lib64/.lance-c-fingerprint" +bash "${work}/gate.sh" +check_builds 2 +# Pin and patch updates both invalidate installed artifacts, even with identical ABI. +sed 's/^LANCE_C_SOURCE=.*/LANCE_C_SOURCE="lance-c-test-revision"/' "${DORIS_HOME}/thirdparty/vars.sh" \ + > "${work}/new-vars.sh" +mv "${work}/new-vars.sh" "${DORIS_HOME}/thirdparty/vars.sh" +expect_preserved_install +cp "${DORIS_HOME}/thirdparty/vars.sh" "${DORIS_THIRDPARTY}/vars.sh" +bash "${work}/gate.sh" +check_builds 3 +echo '# test patch update' >> "${DORIS_HOME}/thirdparty/patches/lance-c-foyer.patch" +expect_preserved_install +cp "${DORIS_HOME}/thirdparty/patches/lance-c-foyer.patch" "${DORIS_THIRDPARTY}/patches/" +bash "${work}/gate.sh" +check_builds 4 +echo 'PASS: mismatched sources rejected; synchronized pin and patch updates rebuild' +rm "${installed}/include/lance/lance.h" +bash "${work}/gate.sh" +check_builds 5 +: > "${installed}/lib64/liblance_c.a" +bash "${work}/gate.sh" +check_builds 6 +echo 'PASS: incomplete header/archive install rebuilt' +# A legacy external builder may exit successfully without installing the new revision. +cp "${DORIS_THIRDPARTY}/build-thirdparty.sh" "${work}/good-builder.sh" +printf '#!/usr/bin/env bash\nexit 0\n' > "${DORIS_THIRDPARTY}/build-thirdparty.sh" +rm "${installed}/lib64/.lance-c-fingerprint" +if bash "${work}/gate.sh" > "${work}/old-builder.log" 2>&1; then + echo 'FAIL: accepted output from a stale external builder'; exit 1 +fi +grep -q 'Lance dependency revision does not match' "${work}/old-builder.log" +cp "${work}/good-builder.sh" "${DORIS_THIRDPARTY}/build-thirdparty.sh" +bash "${DORIS_THIRDPARTY}/build-thirdparty.sh" +echo 'PASS: stale external builder cannot silently satisfy the revision gate' +# Images without rebuild sources must fail before deleting installed dependencies. +rm "${DORIS_THIRDPARTY}/build-thirdparty.sh" "${installed}/lib64/.lance-c-fingerprint" +if bash "${work}/gate.sh" > "${work}/missing-source.log" 2>&1; then + echo 'FAIL: accepted stale compilation image without build sources'; exit 1 +fi +[[ -s "${installed}/lib64/liblance_c.a" ]] +echo 'PASS: missing rebuild source fails without deleting installed artifacts' +# Execute the actual publication function with a tiny Cargo stand-in. A failed +# archive copy must invalidate the old marker, and only a complete retry may stamp it. +sed -n '/^build_lance_c()/,/^}/p' "${ROOT}/thirdparty/build-thirdparty.sh" > "${work}/publish-function.sh" +export TP_DIR="${DORIS_HOME}/thirdparty" +source "${TP_DIR}/vars.sh" +export LANCE_C_SOURCE TP_SOURCE_DIR TP_INSTALL_DIR +export LANCE_C_INSTALL_FINGERPRINT="$(lance_c_install_fingerprint "${TP_DIR}")" +mkdir -p "${TP_SOURCE_DIR}/${LANCE_C_SOURCE}/include/lance" "${TP_INSTALL_DIR}/bin" +echo header > "${TP_SOURCE_DIR}/${LANCE_C_SOURCE}/include/lance/lance.h" +echo header > "${TP_SOURCE_DIR}/${LANCE_C_SOURCE}/include/lance/lance.hpp" +printf '#!/bin/sh\nexit 0\n' > "${TP_INSTALL_DIR}/bin/protoc" +chmod +x "${TP_INSTALL_DIR}/bin/protoc" +cat > "${work}/cargo" <<'CARGO' +#!/usr/bin/env bash +set -eu +if [[ "$1" == --version ]]; then + echo 'cargo 1.94.0'; exit 0 +fi +mkdir -p "${CARGO_TARGET_DIR}/release" +echo archive > "${CARGO_TARGET_DIR}/release/liblance_c.a" +CARGO +chmod +x "${work}/cargo" +export LANCE_C_CARGO="${work}/cargo" RUSTUP_TOOLCHAIN=1.94.0 +export BUILD_DIR=build KERNEL=Linux LANCE_C_CARGO_OFFLINE=OFF +cat > "${work}/publish.sh" <<'PUBLISH' +set -eo pipefail +check_if_source_exist() { :; } +install_rust_archive() { + if [[ "${FAIL_INSTALL:-0}" == 1 ]]; then return 1; fi + cp "$1" "${TP_INSTALL_DIR}/lib64/liblance_c.a" +} +source "$1" +build_lance_c +PUBLISH +bash "${work}/publish.sh" "${work}/publish-function.sh" > "${work}/publish.log" 2>&1 +lance_c_install_is_current "${TP_DIR}" "${TP_INSTALL_DIR}" +if FAIL_INSTALL=1 bash "${work}/publish.sh" "${work}/publish-function.sh" >> "${work}/publish.log" 2>&1; then + echo 'FAIL: expected archive publication failure'; exit 1 +fi +[[ ! -e "${TP_INSTALL_DIR}/lib64/.lance-c-fingerprint" ]] +bash "${work}/publish.sh" "${work}/publish-function.sh" >> "${work}/publish.log" 2>&1 +lance_c_install_is_current "${TP_DIR}" "${TP_INSTALL_DIR}" +echo 'PASS: successful install stamped; failed publication invalidated; retry repaired' diff --git a/thirdparty/vars.sh b/thirdparty/vars.sh index fc5e9818c32..147e4e4b2c4 100644 --- a/thirdparty/vars.sh +++ b/thirdparty/vars.sh @@ -580,11 +580,11 @@ PUGIXML_SOURCE=pugixml-1.15 PUGIXML_MD5SUM="3b894c29455eb33a40b165c6e2de5895" # lance-c -# Complete-segment prefilter fixes are supplied by upstream lance-c, not local patches. -LANCE_C_DOWNLOAD="https://codeload.github.com/lance-format/lance-c/tar.gz/9bd730add2ac70316c1d642b8459011e2dd92022" -LANCE_C_NAME="lance-c-9bd730add2ac70316c1d642b8459011e2dd92022.tar.gz" -LANCE_C_SOURCE="lance-c-9bd730add2ac70316c1d642b8459011e2dd92022" -LANCE_C_MD5SUM="63851b09bf1689032579f1a094ff2f37" +# Includes lance-c #93: scoped boolean scalar-index expressions and Substrait label filters. +LANCE_C_DOWNLOAD="https://codeload.github.com/lance-format/lance-c/tar.gz/cd63420bfbe27f6f0a1edcc873b9191af7d52852" +LANCE_C_NAME="lance-c-cd63420bfbe27f6f0a1edcc873b9191af7d52852.tar.gz" +LANCE_C_SOURCE="lance-c-cd63420bfbe27f6f0a1edcc873b9191af7d52852" +LANCE_C_MD5SUM="37d82907559c4fdb8ed6e5b767b0d309" # paimon-rust PAIMON_RUST_DOWNLOAD="https://github.com/apache/paimon-rust/archive/refs/tags/v0.4.0-rc1.tar.gz" --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
