blob: 0892ea9c080c2b7f0537fcb452346ad57359fb1c [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""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