| # 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. | |
| """Shared driver/worker helpers for the co-located Ray joins (bucket_join, range_join).""" | |
| import threading | |
| from typing import Dict, List, Optional, Sequence, Union | |
| OnSpec = Union[str, Sequence[str]] | |
| def norm_on(on: OnSpec) -> List[str]: | |
| return [on] if isinstance(on, str) else list(on) | |
| def key_type(table, col): | |
| # Logical type without nullability -- a present key compares the same either way. | |
| return str(table.field_dict[col].type).replace(" NOT NULL", "") | |
| # Per-worker table cache, keyed by schema id (so a schema change invalidates it) and | |
| # lock-guarded against concurrent tasks. Planning always loads a fresh table. | |
| _TABLE_CACHE: Dict = {} | |
| _TABLE_CACHE_LOCK = threading.Lock() | |
| def get_table(table_id, catalog_options, schema_id=None, join_name="join"): | |
| from pypaimon.catalog.catalog_factory import CatalogFactory | |
| if schema_id is None: # planning: always load the latest schema | |
| return CatalogFactory.create(catalog_options).get_table(table_id) | |
| key = (table_id, tuple(sorted(catalog_options.items())), schema_id) | |
| with _TABLE_CACHE_LOCK: | |
| table = _TABLE_CACHE.get(key) | |
| if table is None: | |
| table = CatalogFactory.create(catalog_options).get_table(table_id) | |
| if table.table_schema.id != schema_id: | |
| # get_table loads the latest schema; a mismatch means the schema moved | |
| # after the driver planned, so the split plan is stale -- fail fast. | |
| raise ValueError( | |
| f"{table_id} schema changed during {join_name} (planned {schema_id}, " | |
| f"now {table.table_schema.id}); retry.") | |
| _TABLE_CACHE[key] = table | |
| return table | |
| def read_builder(table_id, catalog_options, projection, schema_id=None, join_name="join"): | |
| rb = get_table(table_id, catalog_options, schema_id, join_name).new_read_builder() | |
| return rb.with_projection(projection) if projection is not None else rb | |
| def read_splits(table_id, catalog_options, projection, splits, schema_id, | |
| join_name="join", predicate=None): | |
| # Snapshot-independent but schema-dependent -> cache by schema id (in get_table). | |
| rb = read_builder(table_id, catalog_options, projection, schema_id, join_name) | |
| if predicate is not None: | |
| rb = rb.with_filter(predicate) | |
| return rb.new_read().to_arrow(splits) | |
| def pin_latest_snapshot(table) -> Optional[int]: | |
| """Pin the table instance to its latest snapshot; returns the snapshot id or None | |
| when the table is empty. Pinning keeps every manifest read of a plan consistent.""" | |
| from pypaimon.common.options.core_options import CoreOptions | |
| snapshot = table.snapshot_manager().get_latest_snapshot() | |
| if snapshot is None: | |
| return None | |
| # Drop any existing scan.mode / point-in-time options first so snapshot-id | |
| # doesn't clash with them. | |
| opts = table.options.options | |
| for key in (CoreOptions.SCAN_MODE, CoreOptions.SCAN_SNAPSHOT_ID, | |
| CoreOptions.SCAN_TAG_NAME, CoreOptions.SCAN_WATERMARK, | |
| CoreOptions.SCAN_TIMESTAMP, CoreOptions.SCAN_TIMESTAMP_MILLIS, | |
| CoreOptions.INCREMENTAL_BETWEEN_TIMESTAMP, | |
| CoreOptions.SCAN_FILE_CREATION_TIME_MILLIS, | |
| CoreOptions.SCAN_CREATION_TIME_MILLIS): | |
| opts.data.pop(key.key(), None) | |
| opts.set(CoreOptions.SCAN_SNAPSHOT_ID, snapshot.id) | |
| return snapshot.id |