|
| 1 | +/*********************************************************************** |
| 2 | + * Copyright (c) 2013-2025 General Atomics Integrated Intelligence, Inc. |
| 3 | + * All rights reserved. This program and the accompanying materials |
| 4 | + * are made available under the terms of the Apache License, Version 2.0 |
| 5 | + * which accompanies this distribution and is available at |
| 6 | + * https://www.apache.org/licenses/LICENSE-2.0 |
| 7 | + ***********************************************************************/ |
| 8 | + |
| 9 | +package org.locationtech.geomesa.trino.spatial.iceberg.connector; |
| 10 | + |
| 11 | +import io.trino.spi.Page; |
| 12 | +import io.trino.spi.block.Block; |
| 13 | +import io.trino.spi.connector.ConnectorPageSource; |
| 14 | +import io.trino.spi.connector.SourcePage; |
| 15 | +import io.trino.spi.metrics.Metrics; |
| 16 | +import io.trino.spi.type.RealType; |
| 17 | + |
| 18 | +import java.io.IOException; |
| 19 | +import java.util.List; |
| 20 | +import java.util.OptionalLong; |
| 21 | +import java.util.concurrent.CompletableFuture; |
| 22 | +import java.util.function.ObjLongConsumer; |
| 23 | + |
| 24 | +/** |
| 25 | + * Wraps the Iceberg page source with a sound, pre-decode <em>cheap-reject</em>: for a spatial |
| 26 | + * query the connector has already injected the query envelope as REAL domains on a geometry's |
| 27 | + * {@code __X_bbox__} sub-fields, and those domains ride to the worker in the table handle's |
| 28 | + * unenforced predicate. This page source reads only those cheap bbox columns and drops any row |
| 29 | + * whose bbox cannot overlap the envelope — a necessary condition for any intersection-implying |
| 30 | + * predicate — so the engine's exact {@code ST_Intersects}/{@code ST_Within} residual never |
| 31 | + * decodes those rows' geometry WKB. |
| 32 | + * |
| 33 | + * <p><strong>Correctness.</strong> Only rows that <em>fail</em> a necessary condition are |
| 34 | + * dropped, so no matching row is ever removed; the exact predicate still runs on every survivor |
| 35 | + * (the engine's residual is untouched — this is a pre-filter, not a replacement). Null bbox |
| 36 | + * values are kept (cannot prove non-overlap). Bounds come from the domains' |
| 37 | + * {@link io.trino.spi.predicate.SortedRangeSet#getSpan span}, so a disjunction (OR) of envelopes |
| 38 | + * degrades to their bounding box — still a superset, still sound. |
| 39 | + * |
| 40 | + * <p><strong>Laziness.</strong> Only the bbox columns are materialized here; the drop is applied |
| 41 | + * via {@link SourcePage#selectPositions} <em>before</em> the geometry column is read, so the |
| 42 | + * engine decodes WKB only for surviving rows. The bbox columns this page source added to the |
| 43 | + * physical read (beyond what the query projected) are stripped from the returned page. |
| 44 | + */ |
| 45 | +final class BboxRejectingPageSource implements ConnectorPageSource { |
| 46 | + |
| 47 | + /** One injected bbox-envelope bound: the row's value in {@code channel} must lie within |
| 48 | + * {@code [low, high]} (either side may be infinite) or the row cannot overlap. */ |
| 49 | + record BboxBound(int channel, float low, float high) {} |
| 50 | + |
| 51 | + private final ConnectorPageSource delegate; |
| 52 | + private final List<BboxBound> bounds; |
| 53 | + /** Number of channels the query actually requested; any beyond this were added to read the |
| 54 | + * bbox sub-fields and must be hidden from the engine. */ |
| 55 | + private final int outputChannelCount; |
| 56 | + private final boolean stripAddedChannels; |
| 57 | + |
| 58 | + BboxRejectingPageSource(ConnectorPageSource delegate, int outputChannelCount, |
| 59 | + boolean stripAddedChannels, List<BboxBound> bounds) { |
| 60 | + this.delegate = delegate; |
| 61 | + this.outputChannelCount = outputChannelCount; |
| 62 | + this.stripAddedChannels = stripAddedChannels; |
| 63 | + this.bounds = bounds; |
| 64 | + } |
| 65 | + |
| 66 | + @Override |
| 67 | + public SourcePage getNextSourcePage() { |
| 68 | + SourcePage page = delegate.getNextSourcePage(); |
| 69 | + if (page == null) { |
| 70 | + return null; |
| 71 | + } |
| 72 | + int positions = page.getPositionCount(); |
| 73 | + |
| 74 | + // Materialize ONLY the cheap bbox columns (geometry stays lazy). |
| 75 | + Block[] boundBlocks = new Block[bounds.size()]; |
| 76 | + for (int i = 0; i < bounds.size(); i++) { |
| 77 | + boundBlocks[i] = page.getBlock(bounds.get(i).channel()); |
| 78 | + } |
| 79 | + |
| 80 | + int[] retained = new int[positions]; |
| 81 | + int kept = 0; |
| 82 | + for (int p = 0; p < positions; p++) { |
| 83 | + if (overlaps(p, boundBlocks)) { |
| 84 | + retained[kept++] = p; |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + // Apply the drop BEFORE the geometry column is read, so survivors alone decode WKB. |
| 89 | + if (kept < positions) { |
| 90 | + page.selectPositions(retained, 0, kept); |
| 91 | + } |
| 92 | + return stripAddedChannels ? new ChannelPrefixSourcePage(page, outputChannelCount) : page; |
| 93 | + } |
| 94 | + |
| 95 | + /** True iff the row's bbox could overlap the query envelope (every bound satisfied); a null |
| 96 | + * bbox value cannot prove non-overlap, so it does not reject. */ |
| 97 | + private boolean overlaps(int position, Block[] boundBlocks) { |
| 98 | + for (int i = 0; i < bounds.size(); i++) { |
| 99 | + Block b = boundBlocks[i]; |
| 100 | + if (b.isNull(position)) { |
| 101 | + continue; |
| 102 | + } |
| 103 | + float v = RealType.REAL.getFloat(b, position); |
| 104 | + BboxBound bound = bounds.get(i); |
| 105 | + if (v < bound.low() || v > bound.high()) { |
| 106 | + return false; |
| 107 | + } |
| 108 | + } |
| 109 | + return true; |
| 110 | + } |
| 111 | + |
| 112 | + /** |
| 113 | + * Pure delegation |
| 114 | + */ |
| 115 | + @Override public long getCompletedBytes() { return delegate.getCompletedBytes(); } |
| 116 | + @Override public OptionalLong getCompletedPositions() { return delegate.getCompletedPositions(); } |
| 117 | + @Override public long getReadTimeNanos() { return delegate.getReadTimeNanos(); } |
| 118 | + @Override public boolean isFinished() { return delegate.isFinished(); } |
| 119 | + @Override public long getMemoryUsage() { return delegate.getMemoryUsage(); } |
| 120 | + @Override public void close() throws IOException { delegate.close(); } |
| 121 | + @Override public CompletableFuture<?> isBlocked() { return delegate.isBlocked(); } |
| 122 | + @Override public Metrics getMetrics() { return delegate.getMetrics(); } |
| 123 | + |
| 124 | + /** A {@link SourcePage} view exposing only the first {@code channelCount} channels of a |
| 125 | + * delegate, hiding the bbox sub-field columns this page source added to the physical read. */ |
| 126 | + private static final class ChannelPrefixSourcePage implements SourcePage { |
| 127 | + private final SourcePage delegate; |
| 128 | + private final int channelCount; |
| 129 | + |
| 130 | + ChannelPrefixSourcePage(SourcePage delegate, int channelCount) { |
| 131 | + this.delegate = delegate; |
| 132 | + this.channelCount = channelCount; |
| 133 | + } |
| 134 | + |
| 135 | + @Override public int getPositionCount() { return delegate.getPositionCount(); } |
| 136 | + @Override public long getSizeInBytes() { return delegate.getSizeInBytes(); } |
| 137 | + @Override public long getRetainedSizeInBytes() { return delegate.getRetainedSizeInBytes(); } |
| 138 | + @Override public void retainedBytesForEachPart(ObjLongConsumer<Object> consumer) { |
| 139 | + delegate.retainedBytesForEachPart(consumer); |
| 140 | + } |
| 141 | + @Override public int getChannelCount() { return channelCount; } |
| 142 | + @Override public Block getBlock(int channel) { |
| 143 | + if (channel >= channelCount) { |
| 144 | + throw new IndexOutOfBoundsException("channel " + channel + " >= " + channelCount); |
| 145 | + } |
| 146 | + return delegate.getBlock(channel); |
| 147 | + } |
| 148 | + @Override public Page getPage() { |
| 149 | + int[] channels = new int[channelCount]; |
| 150 | + for (int i = 0; i < channelCount; i++) { |
| 151 | + channels[i] = i; |
| 152 | + } |
| 153 | + return delegate.getColumns(channels); |
| 154 | + } |
| 155 | + @Override public Page getColumns(int[] channels) { return delegate.getColumns(channels); } |
| 156 | + @Override public void selectPositions(int[] positions, int offset, int size) { |
| 157 | + delegate.selectPositions(positions, offset, size); |
| 158 | + } |
| 159 | + } |
| 160 | +} |
0 commit comments