This is an automated email from the ASF dual-hosted git repository.
kfaraz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 15e50434614 perf: add datasource filter pushdown for sys.segments
table (#19718)
15e50434614 is described below
commit 15e50434614ebdc553cf321cbdaab832078ee1f5
Author: jtuglu1 <[email protected]>
AuthorDate: Wed Jul 22 23:19:25 2026 -0700
perf: add datasource filter pushdown for sys.segments table (#19718)
For large used segment counts (~20M+), the UI and `sys.segments` tables are
extremely slow.
This is the first patch in a series of changes to speed up queries against
the `sys.segments`
(and potentially other sys tables) and, transitively, the Druid console UI.
Changes:
- Push down the datasource filter to `MetadataSegmentView` and
`BrokerSegmentMetadataCache`
- Add `SystemSchemaFilters` utility to extract column values on which a
strict filter may apply
- This will help speed up queries with equality or IN filter on the
datasource column
---
benchmarks/pom.xml | 5 +
.../calcite/schema/SysSegmentsTableBenchmark.java | 286 +++++++++++++++++++++
.../metadata/AbstractSegmentMetadataCache.java | 17 ++
.../sql/calcite/schema/MetadataSegmentView.java | 22 +-
.../druid/sql/calcite/schema/SystemSchema.java | 34 ++-
.../sql/calcite/schema/SystemSchemaFilters.java | 184 +++++++++++++
.../schema/SystemServerPropertiesTable.java | 61 +----
.../calcite/schema/SystemSchemaFiltersTest.java | 274 ++++++++++++++++++++
.../druid/sql/calcite/schema/SystemSchemaTest.java | 127 ++++++++-
9 files changed, 944 insertions(+), 66 deletions(-)
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index ebe7c0d1814..bc921acb159 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -124,6 +124,11 @@
<artifactId>calcite-core</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.calcite</groupId>
+ <artifactId>calcite-linq4j</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>com.github.wnameless</groupId>
<artifactId>json-flattener</artifactId>
diff --git
a/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java
b/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java
new file mode 100644
index 00000000000..9eadf5bffee
--- /dev/null
+++
b/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java
@@ -0,0 +1,286 @@
+/*
+ * 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.druid.sql.calcite.schema;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.calcite.DataContext;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.linq4j.QueryProvider;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.druid.client.BrokerSegmentWatcherConfig;
+import org.apache.druid.client.InternalQueryConfig;
+import org.apache.druid.client.TimelineServerView;
+import org.apache.druid.client.coordinator.NoopCoordinatorClient;
+import org.apache.druid.jackson.DefaultObjectMapper;
+import org.apache.druid.java.util.common.CloseableIterators;
+import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.parsers.CloseableIterator;
+import org.apache.druid.segment.join.JoinableFactory;
+import org.apache.druid.segment.metadata.CentralizedDatasourceSchemaConfig;
+import org.apache.druid.server.QueryLifecycleFactory;
+import org.apache.druid.server.SegmentManager;
+import org.apache.druid.server.metrics.NoopServiceEmitter;
+import org.apache.druid.server.security.AllowAllAuthorizer;
+import org.apache.druid.server.security.AuthenticationResult;
+import org.apache.druid.server.security.Authorizer;
+import org.apache.druid.server.security.AuthorizerMapper;
+import org.apache.druid.server.security.Escalator;
+import org.apache.druid.sql.calcite.planner.CatalogResolver;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.SegmentId;
+import org.apache.druid.timeline.SegmentStatusInCluster;
+import org.apache.druid.timeline.partition.LinearShardSpec;
+import org.easymock.EasyMock;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks representative {@code sys.segments} queries as they run through
+ * {@link SystemSchema.SegmentsTable#scan}, against a synthetic cluster of
{@code numSegments}
+ * segments spread across {@code numDataSources} datasources.
+ *
+ * <p>The benchmark deliberately has NO in-code "pushdown on/off" switch: it
simply issues each query
+ * through the production scan path. To measure the effect of the datasource
filter push-down, run
+ * this benchmark on the patched code and again on the reverted code (e.g.
{@code git stash} the
+ * SystemSchema / MetadataSegmentView / AbstractSegmentMetadataCache changes,
rebuild, rerun) and
+ * compare. Pre-patch, {@link #SINGLE_DATASOURCE_EQUALS}/{@link
#MULTI_DATASOURCE_IN} return every segment because
+ * {@code scan} ignores its filters; post-patch they return only the matching
datasources. That
+ * difference in materialized work per query IS the optimization being
measured.
+ *
+ * <p>Sample queries:
+ * <ul>
+ * <li>{@code BASE_SCAN} - {@code SELECT * FROM sys.segments} (the
unfiltered console/aggregate baseline)</li>
+ * <li>{@code SINGLE_DATASOURCE_EQUALS} - {@code WHERE datasource =
'datasource_0'} (single-datasource drill-down)</li>
+ * <li>{@code MULTI_DATASOURCE_IN} - {@code WHERE datasource IN
('datasource_0','datasource_1','datasource_2')}</li>
+ * </ul>
+ *
+ * <p>The available-segment cache is left empty so the benchmark isolates the
published-segment path,
+ * which dominates cost on large clusters.
+ */
+@State(Scope.Benchmark)
+@Fork(value = 1, jvmArgsAppend = {"-Xmx12g"})
+@Warmup(iterations = 3, time = 3)
+@Measurement(iterations = 5, time = 3)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+public class SysSegmentsTableBenchmark
+{
+ public enum Query
+ {
+ BASE_SCAN,
+ SINGLE_DATASOURCE_EQUALS,
+ MULTI_DATASOURCE_IN
+ }
+
+ @Param({"1000000", "10000000"})
+ private int numSegments;
+
+ @Param({"1000"})
+ private int numDataSources;
+
+ @Param
+ private Query query;
+
+ private SystemSchema.SegmentsTable segmentsTable;
+ private List<SegmentStatusInCluster> publishedSegments;
+ private DataContext dataContext;
+ private Map<Query, List<RexNode>> filtersByQuery;
+
+ /**
+ * An empty {@link BrokerSegmentMetadataCache}: the benchmark never
announces segments to it, so the
+ * available-segment side of the scan contributes nothing and the published
path is measured in
+ * isolation. Mirrors the null-arg construction proven in
DruidSchemaInternRowSignatureBenchmark.
+ */
+ private static class EmptyBrokerSegmentMetadataCache extends
BrokerSegmentMetadataCache
+ {
+ EmptyBrokerSegmentMetadataCache()
+ {
+ super(
+ EasyMock.mock(QueryLifecycleFactory.class),
+ EasyMock.mock(TimelineServerView.class),
+ BrokerSegmentMetadataCacheConfig.create(),
+ EasyMock.mock(Escalator.class),
+ EasyMock.mock(InternalQueryConfig.class),
+ new NoopServiceEmitter(),
+ new PhysicalDatasourceMetadataFactory(
+ EasyMock.mock(JoinableFactory.class),
+ EasyMock.mock(SegmentManager.class)
+ ),
+ new NoopCoordinatorClient(),
+ CentralizedDatasourceSchemaConfig.create()
+ );
+ }
+ }
+
+ @Setup(Level.Trial)
+ public void setup()
+ {
+ final List<SegmentStatusInCluster> segments = new ArrayList<>(numSegments);
+ for (int i = 0; i < numSegments; i++) {
+ final String dataSource = StringUtils.format("datasource_%d", i %
numDataSources);
+ final int dayOffset = i / numDataSources;
+ final SegmentId segmentId = SegmentId.of(
+ dataSource,
+ Intervals.utc(dayOffset * 86_400_000L, (dayOffset + 1) *
86_400_000L),
+ "1",
+ new LinearShardSpec(0)
+ );
+ final DataSegment segment =
DataSegment.builder(segmentId).size(1000L).build();
+ segments.add(new SegmentStatusInCluster(segment, false, 1, 100L, false));
+ }
+ // sys.segments serves published segments sorted by SegmentId
(datasource-prefixed).
+ segments.sort(Comparator.naturalOrder());
+ publishedSegments = ImmutableList.copyOf(segments);
+
+ // Returns a fresh iterator over the synthetic segments on every fetch
(cache disabled below).
+ final NoopCoordinatorClient coordinatorClient = new NoopCoordinatorClient()
+ {
+ @Override
+ public ListenableFuture<CloseableIterator<SegmentStatusInCluster>>
fetchAllUsedSegmentsWithOvershadowedStatus(
+ Set<String> watchedDataSources,
+ boolean includeRealtimeSegments
+ )
+ {
+ return
Futures.immediateFuture(CloseableIterators.withEmptyBaggage(publishedSegments.iterator()));
+ }
+ };
+
+ final BrokerSegmentMetadataCacheConfig config = new
DefaultObjectMapper().convertValue(
+ ImmutableMap.of("metadataSegmentCacheEnable", false),
+ BrokerSegmentMetadataCacheConfig.class
+ );
+
+ final MetadataSegmentView metadataView = new MetadataSegmentView(
+ coordinatorClient,
+ new BrokerSegmentWatcherConfig(),
+ config,
+ new NoopServiceEmitter()
+ );
+
+ final DruidSchema druidSchema =
+ new DruidSchema(new EmptyBrokerSegmentMetadataCache(), null,
CatalogResolver.NULL_RESOLVER);
+
+ final AuthorizerMapper authorizerMapper = new AuthorizerMapper(null)
+ {
+ @Override
+ public Authorizer getAuthorizer(String name)
+ {
+ return new AllowAllAuthorizer(null);
+ }
+ };
+
+ segmentsTable = new SystemSchema.SegmentsTable(druidSchema, metadataView,
new DefaultObjectMapper(), authorizerMapper);
+
+ filtersByQuery = buildFilters();
+
+ final AuthenticationResult authenticationResult = new
AuthenticationResult("benchmark", "benchmark", null, null);
+ dataContext = new DataContext()
+ {
+ @Override
+ public SchemaPlus getRootSchema()
+ {
+ return null;
+ }
+
+ @Override
+ public JavaTypeFactoryImpl getTypeFactory()
+ {
+ return null;
+ }
+
+ @Override
+ public QueryProvider getQueryProvider()
+ {
+ return null;
+ }
+
+ @Override
+ public Object get(String name)
+ {
+ return PlannerContext.DATA_CTX_AUTHENTICATION_RESULT.equals(name) ?
authenticationResult : null;
+ }
+ };
+ }
+
+ /**
+ * Builds the filter list for each sample query. The datasource input-ref is
given the literal's
+ * type so Calcite does not wrap the literal in a CAST (which would defeat
the push-down extractor).
+ * "datasource" is column index 1 in SEGMENTS_SIGNATURE.
+ */
+ private static Map<Query, List<RexNode>> buildFilters()
+ {
+ final RexBuilder rexBuilder = new RexBuilder(new JavaTypeFactoryImpl());
+ final RexLiteral ds0 = (RexLiteral) rexBuilder.makeLiteral("datasource_0");
+ final RexLiteral ds1 = (RexLiteral) rexBuilder.makeLiteral("datasource_1");
+ final RexLiteral ds2 = (RexLiteral) rexBuilder.makeLiteral("datasource_2");
+ final RexNode dsRef = rexBuilder.makeInputRef(ds0.getType(), 1);
+
+ final Map<Query, List<RexNode>> filters = new EnumMap<>(Query.class);
+ filters.put(Query.BASE_SCAN, ImmutableList.of());
+ filters.put(
+ Query.SINGLE_DATASOURCE_EQUALS,
+ ImmutableList.of(rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
dsRef, ds0))
+ );
+ filters.put(
+ Query.MULTI_DATASOURCE_IN,
+ ImmutableList.of(rexBuilder.makeIn(dsRef, ImmutableList.of(ds0, ds1,
ds2)))
+ );
+ return filters;
+ }
+
+ @Benchmark
+ public void scan(Blackhole blackhole)
+ {
+ long rows = 0;
+ for (Object[] row : segmentsTable.scan(dataContext,
filtersByQuery.get(query), null)) {
+ blackhole.consume(row);
+ rows++;
+ }
+ blackhole.consume(rows);
+ }
+}
diff --git
a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
index 03d5a44d6ad..d8677fba952 100644
---
a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
+++
b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java
@@ -445,6 +445,23 @@ public abstract class AbstractSegmentMetadataCache<T
extends DataSourceInformati
.iterator();
}
+ /**
+ * Like {@link #iterateSegmentMetadata()} but restricted to the given
datasources, so a pushed-down
+ * {@code datasource} predicate from sys.segments scans only the matching
datasources' segment maps
+ * rather than the whole cluster. A {@code null} argument iterates all
datasources.
+ */
+ public Iterator<AvailableSegmentMetadata> iterateSegmentMetadata(@Nullable
Set<String> dataSources)
+ {
+ if (dataSources == null) {
+ return iterateSegmentMetadata();
+ }
+ return FluentIterable.from(dataSources)
+ .transform(segmentMetadataInfo::get)
+ .filter(Objects::nonNull)
+ .transformAndConcat(Map::values)
+ .iterator();
+ }
+
/**
* Get metadata for the specified segment, which includes information like
RowSignature, realtime & numRows.
*
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
index 34fd6fba779..b292e5e8e04 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/MetadataSegmentView.java
@@ -23,6 +23,7 @@ import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Iterators;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.inject.Inject;
import org.apache.druid.client.BrokerSegmentWatcherConfig;
@@ -49,7 +50,10 @@ import org.apache.druid.timeline.SegmentStatusInCluster;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.joda.time.Duration;
+import javax.annotation.Nullable;
+
import java.util.Iterator;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -185,12 +189,26 @@ public class MetadataSegmentView
*/
Iterator<SegmentStatusInCluster> getSegments()
{
+ return getSegments(null);
+ }
+
+ /**
+ * Returns published (and, with centralized schema, realtime) segment
metadata, optionally
+ * restricted to {@code dataSources}.
+ */
+ Iterator<SegmentStatusInCluster> getSegments(@Nullable Set<String>
dataSources)
+ {
+ final Iterator<SegmentStatusInCluster> base;
if (isCacheEnabled) {
Uninterruptibles.awaitUninterruptibly(cachePopulated);
- return publishedSegments.iterator();
+ base = publishedSegments.iterator();
} else {
- return fetchSegmentMetadataFromCoordinator();
+ // Cache disabled: the Coordinator returns all used segments; filter
client-side to preserve semantics.
+ base = fetchSegmentMetadataFromCoordinator();
}
+ return dataSources == null
+ ? base
+ : Iterators.filter(base, s ->
dataSources.contains(s.getDataSegment().getDataSource()));
}
// Note that coordinator must be up to get segments
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
index 3d3d2a65791..56d6bb593d3 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
@@ -93,6 +93,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -312,6 +313,8 @@ public class SystemSchema extends AbstractSchema
*/
static class SegmentsTable extends AbstractTable implements
ProjectableFilterableTable
{
+ private static final int DATASOURCE_COLUMN =
SEGMENTS_SIGNATURE.indexOf("datasource");
+
private final DruidSchema druidSchema;
private final ObjectMapper jsonMapper;
private final AuthorizerMapper authorizerMapper;
@@ -352,14 +355,23 @@ public class SystemSchema extends AbstractSchema
// get available segments from druidSchema
final BrokerSegmentMetadataCache availableMetadataCache =
druidSchema.cache();
+ // Best-effort push-down of a `datasource` equality/IN filter so we scan
only the matching
+ // datasources instead of every segment in the cluster. Null => no
usable filter => full scan.
+ // The filters are intentionally left in the list, so Calcite still
applies them and correctness
+ // holds even if this extraction is conservative or over-broad.
+ final Set<String> dataSourceFilter = getDataSourceFilter(filters);
+
// Keep track of which segments we emitted from the publishedSegments
iterator, so we don't emit them again
- // from the availableSegments iterator.
+ // from the availableSegments iterator. When a datasource filter is
pushed down we only emit the matching
+ // datasources' segments, so avoid pre-sizing to the whole-cluster
segment count (a huge, wasted allocation).
final Set<SegmentId> segmentsAlreadySeen =
-
Sets.newHashSetWithExpectedSize(availableMetadataCache.getTotalSegments());
+ dataSourceFilter == null
+ ?
Sets.newHashSetWithExpectedSize(availableMetadataCache.getTotalSegments())
+ : new HashSet<>();
// Get segments from metadata segment cache (if enabled in SQL planner
config), else directly from
// Coordinator. This may include both published and realtime segments.
- final Iterator<SegmentStatusInCluster> metadataStoreSegments =
metadataView.getSegments();
+ final Iterator<SegmentStatusInCluster> metadataStoreSegments =
metadataView.getSegments(dataSourceFilter);
final FluentIterable<Object[]> publishedSegments = FluentIterable
.from(() -> getAuthorizedPublishedSegments(metadataStoreSegments,
root))
.transform(val -> {
@@ -428,7 +440,7 @@ public class SystemSchema extends AbstractSchema
// If druid.centralizedDatasourceSchema.enabled is set on the
Coordinator, all the segments in this loop
// would be covered in the previous iteration since Coordinator would
return realtime segments as well.
final FluentIterable<Object[]> availableSegments = FluentIterable
- .from(() ->
getAuthorizedAvailableSegments(availableMetadataCache.iterateSegmentMetadata(),
root))
+ .from(() ->
getAuthorizedAvailableSegments(availableMetadataCache.iterateSegmentMetadata(dataSourceFilter),
root))
.transform(val -> {
final DataSegment segment = val.getSegment();
if (segmentsAlreadySeen.contains(segment.getId())) {
@@ -517,6 +529,20 @@ public class SystemSchema extends AbstractSchema
return authorizedSegments.iterator();
}
+ /**
+ * Best-effort extraction of an exact-match {@code datasource} constraint
(column
+ * {@link #DATASOURCE_COLUMN}) from the pushed-down filters, so
sys.segments can restrict its scan
+ * to the matching datasources rather than materializing every segment in
the cluster. Delegates to
+ * {@link SystemSchemaFilters}, which handles {@code datasource = 'x'},
{@code datasource IN (...)},
+ * OR-of-equalities, and nested {@code AND}/{@code OR}. Returns {@code
null} when no usable
+ * datasource predicate is present, in which case the previous full-scan
behavior is retained.
+ */
+ @Nullable
+ static Set<String> getDataSourceFilter(List<RexNode> filters)
+ {
+ return SystemSchemaFilters.extractColumnValues(filters,
DATASOURCE_COLUMN);
+ }
+
private static class PartialSegmentData
{
private final long isAvailable;
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchemaFilters.java
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchemaFilters.java
new file mode 100644
index 00000000000..90dd2a7e34c
--- /dev/null
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchemaFilters.java
@@ -0,0 +1,184 @@
+/*
+ * 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.druid.sql.calcite.schema;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Range;
+import com.google.common.collect.Sets;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.util.NlsString;
+import org.apache.calcite.util.Sarg;
+
+import javax.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helpers for pushing a simple string-column predicate down from a {@code
ProjectableFilterableTable}
+ * system-table scan (e.g. {@code datasource} for sys.segments, {@code
server}/{@code service_name} for
+ * sys.server_properties) into that table's data source, so the scan
materializes only the matching
+ * rows instead of the whole cluster.
+ *
+ * <p>All extraction is best-effort. The filters are left in the planner's
filter list, so Calcite
+ * still applies them as a post-filter; correctness therefore holds even if
extraction returns a
+ * conservative (over-broad) set - the caller just does a bit of extra work -
or {@code null} (no
+ * constraint), which retains a full scan. The extractor never returns a set
narrower than the true
+ * constraint on the column, so pushing it down can never drop a matching row.
+ */
+final class SystemSchemaFilters
+{
+ private SystemSchemaFilters()
+ {
+ }
+
+ /**
+ * Extracts the finite set of exact string values that column {@code
columnIndex} is constrained to
+ * by the given filters (a top-level list is implicitly ANDed). Handles
{@code col = 'x'},
+ * {@code col IN (...)} (normalized by Calcite to SEARCH), OR-of-equalities,
and arbitrarily nested
+ * {@code AND}/{@code OR} - including a whole {@code WHERE} passed as a
single {@code AND(...)}
+ * RexCall (as Calcite's filter-scan rule may do), where any conjunct that
does not constrain the
+ * column (e.g. {@code is_active = 1}) is simply ignored.
+ *
+ * @return the bounded value set, or {@code null} when the column is not
bounded to a finite set
+ * (no predicate, or a range/{@code LIKE}/{@code !=} predicate)
+ */
+ @Nullable
+ static Set<String> extractColumnValues(List<RexNode> filters, int
columnIndex)
+ {
+ return intersectConjuncts(filters, columnIndex);
+ }
+
+ /**
+ * Multi-column variant of {@link #extractColumnValues(List, int)}: extracts
a bounded value set for
+ * each of {@code columnIndices} independently. A column with no usable
constraint is absent from the
+ * returned map (rather than mapped to {@code null}).
+ */
+ static Map<Integer, Set<String>> extractColumnValues(List<RexNode> filters,
int... columnIndices)
+ {
+ final Map<Integer, Set<String>> result = new HashMap<>();
+ for (final int columnIndex : columnIndices) {
+ final Set<String> values = extractColumnValues(filters, columnIndex);
+ if (values != null) {
+ result.put(columnIndex, values);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * AND semantics: intersect the value sets from the extractable conjuncts,
ignoring conjuncts that
+ * don't constrain the column. Returns {@code null} if no conjunct yields a
value set.
+ */
+ @Nullable
+ private static Set<String> intersectConjuncts(List<RexNode> conjuncts, int
columnIndex)
+ {
+ Set<String> result = null;
+ for (final RexNode conjunct : conjuncts) {
+ final Set<String> values = extractColumnValues(conjunct, columnIndex);
+ if (values != null) {
+ result = (result == null) ? values : Sets.intersection(result,
values).immutableCopy();
+ }
+ }
+ return result;
+ }
+
+ @Nullable
+ private static Set<String> extractColumnValues(RexNode node, int columnIndex)
+ {
+ if (!(node instanceof RexCall)) {
+ return null;
+ }
+ final RexCall call = (RexCall) node;
+ switch (call.getKind()) {
+ case EQUALS:
+ return equalsColumn(call, columnIndex);
+ case AND:
+ return intersectConjuncts(call.getOperands(), columnIndex);
+ case OR:
+ final Set<String> union = new HashSet<>();
+ for (final RexNode operand : call.getOperands()) {
+ final Set<String> values = extractColumnValues(operand, columnIndex);
+ if (values == null) {
+ // An un-extractable disjunct means we cannot bound the value set
for this OR.
+ return null;
+ }
+ union.addAll(values);
+ }
+ return union;
+ case SEARCH:
+ return searchColumn(call, columnIndex);
+ default:
+ return null;
+ }
+ }
+
+ @Nullable
+ private static Set<String> equalsColumn(RexCall call, int columnIndex)
+ {
+ final List<RexNode> ops = call.getOperands();
+ if (ops.size() != 2) {
+ return null;
+ }
+ final RexNode a = ops.get(0);
+ final RexNode b = ops.get(1);
+ final RexLiteral literal;
+ if (isColumnRef(a, columnIndex) && b instanceof RexLiteral) {
+ literal = (RexLiteral) b;
+ } else if (isColumnRef(b, columnIndex) && a instanceof RexLiteral) {
+ literal = (RexLiteral) a;
+ } else {
+ return null;
+ }
+ final String value = RexLiteral.stringValue(literal);
+ return value == null ? null : ImmutableSet.of(value);
+ }
+
+ @Nullable
+ private static Set<String> searchColumn(RexCall call, int columnIndex)
+ {
+ final List<RexNode> ops = call.getOperands();
+ if (ops.size() != 2 || !isColumnRef(ops.get(0), columnIndex) ||
!(ops.get(1) instanceof RexLiteral)) {
+ return null;
+ }
+ final Sarg<?> sarg = ((RexLiteral) ops.get(1)).getValueAs(Sarg.class);
+ // Only exact-value sets (IN / OR-of-=) can bound the scan; ranges (>, <,
LIKE) cannot.
+ if (sarg == null || !sarg.isPoints()) {
+ return null;
+ }
+ final Set<String> values = new HashSet<>();
+ for (final Range<?> range : sarg.rangeSet.asRanges()) {
+ final Object endpoint = range.lowerEndpoint();
+ values.add(endpoint instanceof NlsString ? ((NlsString)
endpoint).getValue() : String.valueOf(endpoint));
+ }
+ return values;
+ }
+
+ private static boolean isColumnRef(RexNode node, int columnIndex)
+ {
+ return node instanceof RexInputRef && ((RexInputRef) node).getIndex() ==
columnIndex;
+ }
+}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java
index 4b25c3cefa1..cdadd9d17c2 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java
@@ -27,14 +27,10 @@ import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.Linq4j;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexInputRef;
-import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.schema.ProjectableFilterableTable;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.impl.AbstractTable;
-import org.apache.calcite.sql.SqlKind;
import org.apache.druid.discovery.DiscoveryDruidNode;
import org.apache.druid.discovery.DruidNodeDiscoveryProvider;
import org.apache.druid.java.util.common.StringUtils;
@@ -60,7 +56,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -139,8 +134,9 @@ public class SystemServerPropertiesTable extends
AbstractTable implements Projec
);
SystemSchema.checkStateReadAccessForServers(authenticationResult,
authorizerMapper);
- // Extract equality filters to skip fetching properties from non-matching
servers.
- final Map<Integer, Set<String>> columnFilters =
extractColumnEqualityFilters(filters, SERVER_INDEX, SERVICE_NAME_INDEX);
+ // Extract server/service_name constraints to skip fetching properties
from non-matching servers.
+ final Map<Integer, Set<String>> columnFilters =
+ SystemSchemaFilters.extractColumnValues(filters, SERVER_INDEX,
SERVICE_NAME_INDEX);
final Set<String> serverFilter = columnFilters.get(SERVER_INDEX);
final Set<String> serviceNameFilter =
columnFilters.get(SERVICE_NAME_INDEX);
@@ -183,57 +179,6 @@ public class SystemServerPropertiesTable extends
AbstractTable implements Projec
return Linq4j.asEnumerable(rows);
}
- /**
- * Extracts simple equality filters ({@code column = 'literal'}) for the
specified columns.
- * Only handles top-level AND equalities; any other predicate (!=, LIKE, OR,
functions) is
- * ignored and left for Calcite to apply as a post-filter.
- *
- * @return map from column index to the set of literal values; absent key
means no filter for that column
- */
- private static Map<Integer, Set<String>> extractColumnEqualityFilters(final
List<RexNode> filters, final int... columnIndices)
- {
- final Map<Integer, Set<String>> result = new HashMap<>();
- for (final RexNode filter : filters) {
- for (final int columnIndex : columnIndices) {
- final String value = extractEqualityOnColumn(filter, columnIndex);
- if (value != null) {
- result.computeIfAbsent(columnIndex, k -> new HashSet<>()).add(value);
- break;
- }
- }
- }
- return result;
- }
-
- /**
- * Returns the string literal value if the node is a simple {@code column =
'literal'} (or reversed) equality
- * on the given column index. Returns null for anything else — Calcite
handles those as post-filters.
- */
- @Nullable
- private static String extractEqualityOnColumn(final RexNode node, final int
columnIndex)
- {
- if (!(node instanceof RexCall)) {
- return null;
- }
- final RexCall call = (RexCall) node;
- if (call.getKind() != SqlKind.EQUALS) {
- return null;
- }
- final RexNode left = call.getOperands().get(0);
- final RexNode right = call.getOperands().get(1);
-
- if (left instanceof RexInputRef && right instanceof RexLiteral) {
- if (((RexInputRef) left).getIndex() == columnIndex) {
- return RexLiteral.stringValue(right);
- }
- } else if (right instanceof RexInputRef && left instanceof RexLiteral) {
- if (((RexInputRef) right).getIndex() == columnIndex) {
- return RexLiteral.stringValue(left);
- }
- }
- return null;
- }
-
private static Object[] projectRow(final Object[] row, @Nullable final int[]
projects)
{
if (projects == null) {
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaFiltersTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaFiltersTest.java
new file mode 100644
index 00000000000..f8344db8ede
--- /dev/null
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaFiltersTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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.druid.sql.calcite.schema;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Unit tests for the predicate-extraction utilities in {@link
SystemSchemaFilters}. RexNodes are
+ * built by hand (as Calcite's filter-scan rule would hand them to a {@code
ProjectableFilterableTable})
+ * so the extraction logic can be exercised directly without a full planner
run.
+ */
+public class SystemSchemaFiltersTest
+{
+ // Two arbitrary string columns to constrain, plus a numeric-ish column used
for "other column" cases.
+ private static final int COL_A = 1;
+ private static final int COL_B = 2;
+ private static final int COL_OTHER = 4;
+
+ private RexBuilder rexBuilder;
+ private RexLiteral foo;
+ private RexLiteral bar;
+ private RexLiteral baz;
+ private RexNode aRef;
+ private RexNode bRef;
+ private RexNode otherRef;
+
+ @Before
+ public void setUp()
+ {
+ rexBuilder = new RexBuilder(new JavaTypeFactoryImpl());
+ foo = (RexLiteral) rexBuilder.makeLiteral("foo");
+ bar = (RexLiteral) rexBuilder.makeLiteral("bar");
+ baz = (RexLiteral) rexBuilder.makeLiteral("baz");
+ // Match the input-ref type to the literal type so Calcite does not wrap
the literal in a CAST.
+ aRef = rexBuilder.makeInputRef(foo.getType(), COL_A);
+ bRef = rexBuilder.makeInputRef(foo.getType(), COL_B);
+ otherRef = rexBuilder.makeInputRef(foo.getType(), COL_OTHER);
+ }
+
+ @Test
+ public void testEquals()
+ {
+ // col = 'foo'
+ Assert.assertEquals(
+ ImmutableSet.of("foo"),
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
aRef, foo)),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testEqualsReversedOperands()
+ {
+ // 'foo' = col
+ Assert.assertEquals(
+ ImmutableSet.of("foo"),
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
foo, aRef)),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testIn()
+ {
+ // col IN ('foo', 'bar') -- Calcite normalizes IN to a SEARCH over a
points Sarg
+ Assert.assertEquals(
+ ImmutableSet.of("foo", "bar"),
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeIn(aRef, ImmutableList.of(foo,
bar))),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testOrOfEqualities()
+ {
+ // col = 'foo' OR col = 'bar'
+ Assert.assertEquals(
+ ImmutableSet.of("foo", "bar"),
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(
+ SqlStdOperatorTable.OR,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, aRef, foo),
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, aRef, bar)
+ )),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testOrWithUnextractableDisjunctReturnsNull()
+ {
+ // col = 'foo' OR col > 'bar' -- one disjunct cannot bound the set, so the
whole OR is unbounded
+ Assert.assertNull(
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(
+ SqlStdOperatorTable.OR,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, aRef, foo),
+ rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN, aRef,
bar)
+ )),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testTopLevelConjunctsIntersect()
+ {
+ // A top-level filter list is implicitly ANDed: IN ('foo','bar') AND IN
('bar','baz') => {'bar'}
+ Assert.assertEquals(
+ ImmutableSet.of("bar"),
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(
+ rexBuilder.makeIn(aRef, ImmutableList.of(foo, bar)),
+ rexBuilder.makeIn(aRef, ImmutableList.of(bar, baz))
+ ),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testNestedAndConjunctsIntersect()
+ {
+ // A whole WHERE passed as a single AND(...) RexCall: IN ('foo','bar') AND
IN ('bar','baz') => {'bar'}
+ Assert.assertEquals(
+ ImmutableSet.of("bar"),
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeIn(aRef, ImmutableList.of(foo, bar)),
+ rexBuilder.makeIn(aRef, ImmutableList.of(bar, baz))
+ )),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testAndIgnoresNonMatchingConjunct()
+ {
+ // col_a = 'foo' AND col_other = 'foo' => {'foo'} (the non-target conjunct
is ignored)
+ Assert.assertEquals(
+ ImmutableSet.of("foo"),
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, aRef, foo),
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, otherRef, foo)
+ )),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testAndWithNoConstraintOnColumnReturnsNull()
+ {
+ // AND of predicates that never touch the target column => null (full scan
retained)
+ Assert.assertNull(
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, otherRef, foo),
+ rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN,
otherRef, bar)
+ )),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testEmptyFilterListReturnsNull()
+ {
+
Assert.assertNull(SystemSchemaFilters.extractColumnValues(ImmutableList.of(),
COL_A));
+ }
+
+ @Test
+ public void testRangePredicateReturnsNull()
+ {
+ // col > 'foo' -- a range cannot bound the value set
+ Assert.assertNull(
+ SystemSchemaFilters.extractColumnValues(
+
ImmutableList.of(rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN, aRef,
foo)),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testEqualityOnOtherColumnReturnsNull()
+ {
+ Assert.assertNull(
+ SystemSchemaFilters.extractColumnValues(
+ ImmutableList.of(rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
otherRef, foo)),
+ COL_A
+ )
+ );
+ }
+
+ @Test
+ public void testNonRexCallNodeReturnsNull()
+ {
+ // A bare input ref (not a RexCall) constrains nothing.
+
Assert.assertNull(SystemSchemaFilters.extractColumnValues(ImmutableList.of(aRef),
COL_A));
+ }
+
+ @Test
+ public void testMultiColumnExtractsEachIndependently()
+ {
+ // col_a = 'foo' AND col_b IN ('bar', 'baz')
+ final RexNode filter = rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, aRef, foo),
+ rexBuilder.makeIn(bRef, ImmutableList.of(bar, baz))
+ );
+ final Map<Integer, Set<String>> result =
+ SystemSchemaFilters.extractColumnValues(ImmutableList.of(filter),
COL_A, COL_B);
+ Assert.assertEquals(
+ ImmutableMap.of(
+ COL_A, ImmutableSet.of("foo"),
+ COL_B, ImmutableSet.of("bar", "baz")
+ ),
+ result
+ );
+ }
+
+ @Test
+ public void testMultiColumnOmitsUnconstrainedColumn()
+ {
+ // Only col_a is constrained; col_b must be absent from the map (not
mapped to null).
+ final RexNode filter = rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
aRef, foo);
+ final Map<Integer, Set<String>> result =
+ SystemSchemaFilters.extractColumnValues(ImmutableList.of(filter),
COL_A, COL_B);
+ Assert.assertEquals(ImmutableMap.of(COL_A, ImmutableSet.of("foo")),
result);
+ Assert.assertFalse(result.containsKey(COL_B));
+ }
+}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
index ca7410a0180..d956d7bc1c1 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java
@@ -36,6 +36,7 @@ import org.apache.calcite.linq4j.QueryProvider;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.Table;
@@ -607,6 +608,88 @@ public class SystemSchemaTest extends CalciteTestBase
Assert.assertEquals(6, propertiesFields.size());
}
+ @Test
+ public void testSegmentsTableGetDataSourceFilter()
+ {
+ final RexBuilder rexBuilder = new RexBuilder(new JavaTypeFactoryImpl());
+ final RexLiteral foo = (RexLiteral) rexBuilder.makeLiteral("foo");
+ final RexLiteral bar = (RexLiteral) rexBuilder.makeLiteral("bar");
+ final RexLiteral baz = (RexLiteral) rexBuilder.makeLiteral("baz");
+ // Match the input-ref type to the literal type so Calcite does not wrap
the literal in a CAST.
+ // "datasource" is column index 1, "size" is column index 4 in
SEGMENTS_SIGNATURE.
+ final RexNode dsRef = rexBuilder.makeInputRef(foo.getType(), 1);
+ final RexNode sizeRef = rexBuilder.makeInputRef(foo.getType(), 4);
+
+ // datasource = 'foo'
+ Assert.assertEquals(
+ ImmutableSet.of("foo"),
+ SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, dsRef, foo)))
+ );
+ // 'foo' = datasource (reversed operands)
+ Assert.assertEquals(
+ ImmutableSet.of("foo"),
+ SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, foo, dsRef)))
+ );
+ // datasource IN ('foo', 'bar')
+ Assert.assertEquals(
+ ImmutableSet.of("foo", "bar"),
+ SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeIn(dsRef, ImmutableList.of(foo, bar))))
+ );
+ // datasource = 'foo' OR datasource = 'bar'
+ Assert.assertEquals(
+ ImmutableSet.of("foo", "bar"),
+ SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(
+ SqlStdOperatorTable.OR,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, dsRef, foo),
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, dsRef, bar))))
+ );
+ // ANDed conjuncts intersect: IN ('foo','bar') AND IN ('bar','baz') =>
{'bar'}
+ Assert.assertEquals(
+ ImmutableSet.of("bar"),
+ SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeIn(dsRef, ImmutableList.of(foo, bar)),
+ rexBuilder.makeIn(dsRef, ImmutableList.of(bar, baz))))
+ );
+ // No filters => null (full scan retained)
+ Assert.assertNull(SegmentsTable.getDataSourceFilter(ImmutableList.of()));
+ // Range predicate on datasource cannot bound the scan => null
+ Assert.assertNull(SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN, dsRef, foo))));
+ // Equality on a non-datasource column => null
+ Assert.assertNull(SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, sizeRef, foo))));
+
+ // Compound predicate passed as a single AND(...) RexCall (as Calcite's
filter-scan rule may do):
+ // datasource = 'foo' AND <non-datasource predicate> => {foo} (the
non-datasource conjunct is ignored)
+ Assert.assertEquals(
+ ImmutableSet.of("foo"),
+ SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, dsRef, foo),
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, sizeRef,
foo))))
+ );
+ // AND of two datasource constraints intersects: IN ('foo','bar') AND IN
('bar','baz') => {'bar'}
+ Assert.assertEquals(
+ ImmutableSet.of("bar"),
+ SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeIn(dsRef, ImmutableList.of(foo, bar)),
+ rexBuilder.makeIn(dsRef, ImmutableList.of(bar, baz)))))
+ );
+ // AND with no datasource conjunct => null (full scan retained)
+ Assert.assertNull(SegmentsTable.getDataSourceFilter(ImmutableList.of(
+ rexBuilder.makeCall(
+ SqlStdOperatorTable.AND,
+ rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, sizeRef, foo),
+ rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN, sizeRef,
foo)))));
+ }
+
@Test
public void testSegmentsTable() throws Exception
{
@@ -619,7 +702,7 @@ public class SystemSchemaTest extends CalciteTestBase
new SegmentStatusInCluster(segment2, false, 0, null, false)
));
-
EasyMock.expect(metadataView.getSegments()).andReturn(publishedSegments.iterator()).once();
+
EasyMock.expect(metadataView.getSegments(EasyMock.anyObject())).andReturn(publishedSegments.iterator()).once();
EasyMock.replay(request, responseHolder, responseHandler, metadataView);
DataContext dataContext = createDataContext(Users.SUPER);
@@ -739,7 +822,7 @@ public class SystemSchemaTest extends CalciteTestBase
new SegmentStatusInCluster(segment2, false, 0, null, false)
));
-
EasyMock.expect(metadataView.getSegments()).andReturn(publishedSegments.iterator()).once();
+
EasyMock.expect(metadataView.getSegments(EasyMock.anyObject())).andReturn(publishedSegments.iterator()).once();
EasyMock.replay(request, responseHolder, responseHandler, metadataView);
DataContext dataContext = createDataContext(Users.SUPER);
@@ -1802,6 +1885,46 @@ public class SystemSchemaTest extends CalciteTestBase
EasyMock.verify(druidNodeDiscoveryProvider, httpClient);
}
+ @Test
+ public void testPropertiesTable_filterPushdownInFilter()
+ {
+ SystemServerPropertiesTable propertiesTable = new
SystemServerPropertiesTable(
+ druidNodeDiscoveryProvider,
+ authMapper,
+ httpClient,
+ MAPPER
+ );
+
+ mockAllNodeRolesWithCoordinator(coordinator, coordinator2);
+
+ // server IN ('localhost:8081', 'nonexistent:9999') — only coordinator
(8081) matches, so exactly
+ // one node is fetched. A single HTTP call proves the IN predicate is
pushed down (pre-refactor the
+ // SEARCH form was not extracted and both nodes would have been fetched).
+ HttpResponse resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK);
+ StringFullResponseHolder holder = new StringFullResponseHolder(resp,
StandardCharsets.UTF_8);
+ holder.addChunk("{\"druid.key\": \"val\"}");
+ EasyMock.expect(
+ httpClient.go(EasyMock.isA(Request.class),
EasyMock.isA(StringFullResponseHandler.class))
+ ).andReturn(Futures.immediateFuture(holder)).once();
+
+ EasyMock.replay(druidNodeDiscoveryProvider, httpClient);
+
+ final RexBuilder rexBuilder = new RexBuilder(new JavaTypeFactoryImpl());
+ final RelDataType rowType = propertiesTable.getRowType(new
JavaTypeFactoryImpl());
+ final RexNode serverIn = rexBuilder.makeIn(
+
rexBuilder.makeInputRef(rowType.getFieldList().get(SERVER_INDEX).getType(),
SERVER_INDEX),
+ ImmutableList.of(rexBuilder.makeLiteral("localhost:8081"),
rexBuilder.makeLiteral("nonexistent:9999"))
+ );
+
+ final List<Object[]> rows =
+ propertiesTable.scan(createDataContext(Users.SUPER),
ImmutableList.of(serverIn), null).toList();
+
+ Assert.assertEquals(1, rows.size());
+ Assert.assertEquals("localhost:8081", rows.get(0)[0]);
+
+ EasyMock.verify(druidNodeDiscoveryProvider, httpClient);
+ }
+
@Test
public void testPropertiesTable_filterPushdownServiceNameAndNonMatching()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]