This is an automated email from the ASF dual-hosted git repository.
raghavyadav01 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 230045f26d4 Rewrite arrayContains on multi-value columns into an
indexable EQUALS filter (#20000)
230045f26d4 is described below
commit 230045f26d431203bbd03813065290d565fc57a4
Author: RAGHVENDRA KUMAR YADAV <[email protected]>
AuthorDate: Fri Oct 23 13:58:44 2026 -0700
Rewrite arrayContains on multi-value columns into an indexable EQUALS
filter (#20000)
* Rewrite arrayContains predicates on MV columns into EQUALS filters
A boolean arrayContainsInt/arrayContainsString(mvCol, literal) predicate
was evaluated as a scalar
expression on every document, while the equivalent mvCol = literal can use
the dictionary, inverted
index and segment pruning. Add ArrayContainsFilterOptimizer to rewrite it
when the first argument is a
multi-value column whose type matches the function and the second is a
literal. Negations are left
untouched since != on an MV column has different semantics.
* Convert arrayContains literal to the function parameter type before
rewriting
The EQUALS rewrite passed the raw literal through, but the scalar function
converts it first
(truncating doubles, casting longs, trimming strings) and
NumericalFilterOptimizer does not convert
literals for multi-value columns, so e.g. arrayContainsInt(mvCol, 5.0)
failed after the rewrite.
Apply the same PinotDataType conversion as the scalar path and skip the
rewrite when it fails. Also
document the expression override interaction and add combined-predicate
tests.
* Drop the redundant BIG_DECIMAL string case and note why conversion
failures fall back
---
.../pinot/core/query/optimizer/QueryOptimizer.java | 10 +-
.../filter/ArrayContainsFilterOptimizer.java | 166 +++++++++++++++++++++
.../core/query/optimizer/QueryOptimizerTest.java | 48 +++++-
.../ArrayContainsMultiValueQueriesTest.java | 115 ++++++++++++++
4 files changed, 335 insertions(+), 4 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
index 635250800e0..39ce32d90a3 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
@@ -22,6 +22,7 @@ import java.util.List;
import javax.annotation.Nullable;
import org.apache.pinot.common.request.Expression;
import org.apache.pinot.common.request.PinotQuery;
+import
org.apache.pinot.core.query.optimizer.filter.ArrayContainsFilterOptimizer;
import org.apache.pinot.core.query.optimizer.filter.FilterOptimizer;
import
org.apache.pinot.core.query.optimizer.filter.FlattenAndOrFilterOptimizer;
import
org.apache.pinot.core.query.optimizer.filter.IdenticalPredicateFilterOptimizer;
@@ -42,10 +43,13 @@ public class QueryOptimizer {
// be merged
// - TimePredicateFilterOptimizer and MergeRangeFilterOptimizer relies on
NumericalFilterOptimizer to convert the
// values to the proper format so that they can be properly parsed
+ // - ArrayContainsFilterOptimizer runs before MergeEqInFilterOptimizer so
that the EQUALS predicates it produces can
+ // be merged. It converts the literal to the column type itself because
NumericalFilterOptimizer only handles
+ // single-value columns
private static final List<FilterOptimizer> FILTER_OPTIMIZERS =
- List.of(new FlattenAndOrFilterOptimizer(), new
IdenticalPredicateFilterOptimizer(),
- new MergeEqInFilterOptimizer(), new NumericalFilterOptimizer(), new
TimePredicateFilterOptimizer(),
- new MergeRangeFilterOptimizer(), new TextMatchFilterOptimizer());
+ List.of(new FlattenAndOrFilterOptimizer(), new
ArrayContainsFilterOptimizer(),
+ new IdenticalPredicateFilterOptimizer(), new
MergeEqInFilterOptimizer(), new NumericalFilterOptimizer(),
+ new TimePredicateFilterOptimizer(), new MergeRangeFilterOptimizer(),
new TextMatchFilterOptimizer());
private static final List<StatementOptimizer> STATEMENT_OPTIMIZERS =
List.of(new AggregateFunctionRewriteOptimizer());
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/filter/ArrayContainsFilterOptimizer.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/filter/ArrayContainsFilterOptimizer.java
new file mode 100644
index 00000000000..96ccb66368d
--- /dev/null
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/filter/ArrayContainsFilterOptimizer.java
@@ -0,0 +1,166 @@
+/**
+ * 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.pinot.core.query.optimizer.filter;
+
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.pinot.common.request.Expression;
+import org.apache.pinot.common.request.Function;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.LiteralContext;
+import org.apache.pinot.common.utils.request.RequestUtils;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.PinotDataType;
+import org.apache.pinot.sql.FilterKind;
+
+
+/// The `ArrayContainsFilterOptimizer` rewrites a boolean `arrayContains*`
predicate on a multi-value column into the
+/// equivalent `EQUALS` predicate, e.g. `arrayContainsString(mvCol, 'foo')`
(parsed as
+/// `arrayContainsString(mvCol, 'foo') = true`) becomes `mvCol = 'foo'`.
+///
+/// On a multi-value column, `EQUALS` matches a document when any of its
values equals the literal, which is exactly
+/// what `arrayContains*` evaluates. Unlike the scalar function (evaluated by
scanning every document), the `EQUALS`
+/// predicate can be served by the dictionary / inverted index, and can be
used by the segment pruners that run after
+/// the optimizer (on the broker for single-stage queries, on the server for
both single-stage and multi-stage leaf
+/// queries).
+///
+/// The rewrite is only applied when:
+/// - The first argument is an identifier of a multi-value column in the
schema whose stored type matches the function
+/// parameter type (e.g. `arrayContainsInt` on an `INT` column)
+/// - The second argument is a non-null literal that can be converted to the
function parameter type. The literal is
+/// converted with the same [PinotDataType] conversion the scalar function
applies to its literal arguments (e.g.
+/// `5.5` is truncated to `5`, `' 5'` is trimmed), and the converted value
is used in the `EQUALS` predicate, so both
+/// forms match the same values. If the conversion fails, the scalar
function is kept.
+/// - The predicate is `= true`. Negations (`NOT`, `= false`, `!= true`) are
left untouched because `!=` on a
+/// multi-value column means "some value does not match", which differs from
"no value matches"
+///
+/// NOTE: The rewrite runs before broker-side expression overrides are
applied, so an expression override keyed on an
+/// `arrayContains*` predicate that gets rewritten no longer matches.
+public class ArrayContainsFilterOptimizer implements FilterOptimizer {
+ private static final Map<String, PinotDataType> FUNCTION_PARAMETER_TYPES =
+ Map.of("arraycontainsint", PinotDataType.INT, "arraycontainsstring",
PinotDataType.STRING);
+
+ @Override
+ public Expression optimize(Expression filterExpression, @Nullable Schema
schema) {
+ return schema != null ? rewrite(filterExpression, schema) :
filterExpression;
+ }
+
+ private static Expression rewrite(Expression filterExpression, Schema
schema) {
+ Function function = filterExpression.getFunctionCall();
+ if (function == null) {
+ return filterExpression;
+ }
+ String operator = function.getOperator();
+ if (operator.equals(FilterKind.AND.name()) ||
operator.equals(FilterKind.OR.name())) {
+ function.getOperands().replaceAll(operand -> rewrite(operand, schema));
+ return filterExpression;
+ }
+ if (operator.equals(FilterKind.EQUALS.name())) {
+ Expression rewritten = rewriteArrayContains(function.getOperands(),
schema);
+ return rewritten != null ? rewritten : filterExpression;
+ }
+ // Do not descend into NOT (see class Javadoc)
+ return filterExpression;
+ }
+
+ /// Returns the rewritten `EQUALS` predicate for `arrayContains*(column,
literal) = true`, or `null` if the operands
+ /// do not match that shape.
+ @Nullable
+ private static Expression rewriteArrayContains(List<Expression>
equalsOperands, Schema schema) {
+ Literal rhs = equalsOperands.get(1).getLiteral();
+ if (rhs == null || !rhs.isSetBoolValue() || !rhs.getBoolValue()) {
+ return null;
+ }
+ Function arrayContains = equalsOperands.get(0).getFunctionCall();
+ if (arrayContains == null) {
+ return null;
+ }
+ PinotDataType parameterType =
+
FUNCTION_PARAMETER_TYPES.get(RequestUtils.canonicalizeFunctionName(arrayContains.getOperator()));
+ if (parameterType == null) {
+ return null;
+ }
+ List<Expression> arguments = arrayContains.getOperands();
+ if (arguments.size() != 2) {
+ return null;
+ }
+ Expression column = arguments.get(0);
+ Literal literal = arguments.get(1).getLiteral();
+ if (column.getIdentifier() == null || literal == null ||
literal.isSetNullValue()) {
+ return null;
+ }
+ FieldSpec fieldSpec =
schema.getFieldSpecFor(column.getIdentifier().getName());
+ if (fieldSpec == null || fieldSpec.isSingleValueField()
+ || fieldSpec.getDataType() != getColumnDataType(parameterType)) {
+ return null;
+ }
+ Expression value = convertLiteral(literal, parameterType);
+ return value != null ?
RequestUtils.getFunctionExpression(FilterKind.EQUALS.name(), column, value) :
null;
+ }
+
+ private static DataType getColumnDataType(PinotDataType parameterType) {
+ return parameterType == PinotDataType.INT ? DataType.INT : DataType.STRING;
+ }
+
+ /// Converts the literal to the function parameter type the same way
`ScalarTransformFunctionWrapper` converts a
+ /// literal argument, or returns `null` if it cannot be converted.
+ @Nullable
+ private static Expression convertLiteral(Literal literal, PinotDataType
parameterType) {
+ Object value;
+ try {
+ LiteralContext literalContext = new LiteralContext(literal);
+ switch (literalContext.getType()) {
+ case BOOLEAN:
+ value = parameterType.convert(literalContext.getBooleanValue(),
PinotDataType.BOOLEAN);
+ break;
+ case INT:
+ value = parameterType.convert(literalContext.getIntValue(),
PinotDataType.INT);
+ break;
+ case LONG:
+ value = parameterType.convert(literalContext.getLongValue(),
PinotDataType.LONG);
+ break;
+ case FLOAT:
+ value = parameterType.convert(literalContext.getFloatValue(),
PinotDataType.FLOAT);
+ break;
+ case DOUBLE:
+ value = parameterType.convert(literalContext.getDoubleValue(),
PinotDataType.DOUBLE);
+ break;
+ case BIG_DECIMAL:
+ value = parameterType.convert(literalContext.getBigDecimalValue(),
PinotDataType.BIG_DECIMAL);
+ break;
+ case STRING:
+ value = parameterType.convert(literalContext.getStringValue(),
PinotDataType.STRING);
+ break;
+ default:
+ return null;
+ }
+ } catch (Exception e) {
+ // Any literal that does not convert keeps the scalar function, which
reports or handles it as before.
+ return null;
+ }
+ if (value == null) {
+ return null;
+ }
+ return parameterType == PinotDataType.INT ?
RequestUtils.getLiteralExpression((int) (Integer) value)
+ : RequestUtils.getLiteralExpression((String) value);
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
index 27148e92dcf..cf2eb9bd664 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
@@ -50,7 +50,9 @@ public class QueryOptimizerTest {
.addSingleValueDimension("double", DataType.DOUBLE)
.addSingleValueDimension("string", DataType.STRING)
.addSingleValueDimension("bytes", DataType.BYTES)
- .addMultiValueDimension("mvInt", DataType.INT).build();
+ .addMultiValueDimension("mvInt", DataType.INT)
+ .addMultiValueDimension("mvLong", DataType.LONG)
+ .addMultiValueDimension("mvString", DataType.STRING).build();
@Test
public void testNoFilter() {
@@ -376,6 +378,50 @@ public class QueryOptimizerTest {
testCannotOptimizeQuery("SELECT * FROM testTable WHERE NOT
TEXT_MATCH(string, 'foo')");
}
+ @Test
+ public void testArrayContainsFilter() {
+ // A boolean arrayContains predicate on an MV column is rewritten into an
EQUALS predicate, which has MV
+ // "any element matches" semantics and can use the inverted index and
segment pruning
+ testQuery("SELECT * FROM testTable WHERE arrayContainsString(mvString,
'foo')",
+ "SELECT * FROM testTable WHERE mvString = 'foo'");
+ testQuery("SELECT * FROM testTable WHERE ARRAY_CONTAINS_STRING(mvString,
'foo')",
+ "SELECT * FROM testTable WHERE mvString = 'foo'");
+ testQuery("SELECT * FROM testTable WHERE arrayContainsString(mvString,
'foo') = true",
+ "SELECT * FROM testTable WHERE mvString = 'foo'");
+ testQuery("SELECT * FROM testTable WHERE arrayContainsInt(mvInt, 5)",
+ "SELECT * FROM testTable WHERE mvInt = 5");
+ testQuery("SELECT * FROM testTable WHERE arrayContainsInt(mvInt, 5) AND
int = 1",
+ "SELECT * FROM testTable WHERE mvInt = 5 AND int = 1");
+ // Combines with the other optimizers (merged into IN)
+ testQuery("SELECT * FROM testTable WHERE arrayContainsString(mvString,
'foo') OR mvString = 'bar'",
+ "SELECT * FROM testTable WHERE mvString IN ('foo', 'bar')");
+
+ // The literal is converted to the function parameter type the same way
the scalar function does it
+ testQuery("SELECT * FROM testTable WHERE arrayContainsInt(mvInt, 5.0)",
"SELECT * FROM testTable WHERE mvInt = 5");
+ testQuery("SELECT * FROM testTable WHERE arrayContainsInt(mvInt, 5.5)",
"SELECT * FROM testTable WHERE mvInt = 5");
+ testQuery("SELECT * FROM testTable WHERE arrayContainsInt(mvInt, ' 5')",
"SELECT * FROM testTable WHERE mvInt = 5");
+ testQuery("SELECT * FROM testTable WHERE arrayContainsInt(mvInt,
4294967301)",
+ "SELECT * FROM testTable WHERE mvInt = 5");
+ testQuery("SELECT * FROM testTable WHERE arrayContainsString(mvString, 5)",
+ "SELECT * FROM testTable WHERE mvString = '5'");
+ // Literal that cannot be converted keeps the scalar function
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsInt(mvInt, 'foo')");
+
+ // NOT arrayContains means "no element matches", while `!=` on an MV
column means "some element does not
+ // match", so negations must not be rewritten
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE NOT
arrayContainsString(mvString, 'foo')");
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsString(mvString, 'foo') = false");
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsString(mvString, 'foo') <> true");
+ // Non-identifier / non-literal arguments
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsString(mvString, string)");
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsInt(arraySliceInt(mvInt, 0, 2), 5)");
+ // Column not in the schema, single-value column, or function type that
does not match the column type
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsString(nonExistingColumn, 'foo')");
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsString(string, 'foo')");
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsString(mvInt, '5')");
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE
arrayContainsInt(mvLong, 5)");
+ }
+
private static void testQuery(String actual, String expected) {
assertNotEquals(actual, expected, "You must provide different queries to
test");
PinotQuery actualPinotQuery = CalciteSqlParser.compileToPinotQuery(actual);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/ArrayContainsMultiValueQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayContainsMultiValueQueriesTest.java
new file mode 100644
index 00000000000..d4292d1bf16
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayContainsMultiValueQueriesTest.java
@@ -0,0 +1,115 @@
+/**
+ * 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.pinot.queries;
+
+import org.apache.pinot.common.response.broker.BrokerResponseNative;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+/// Verifies that a boolean `arrayContains*` predicate on a multi-value column
is executed like the equivalent `=`
+/// predicate (inverted index / dictionary lookup, no scan) and returns the
same result as the scalar function.
+public class ArrayContainsMultiValueQueriesTest extends
BaseMultiValueQueriesTest {
+ // 4 identical segments, 100000 docs each
+ private static final long TOTAL_DOCS = 400000L;
+
+ @Test
+ public void testArrayContainsUsesIndex() {
+ // Unoptimized query evaluates the scalar function on every document,
which gives the expected result
+ String query = "SELECT COUNT(*) FROM testTable WHERE
arrayContainsInt(column7, 225)";
+ BrokerResponseNative brokerResponse = getBrokerResponse(query);
+ long expectedCount = getCount(brokerResponse);
+ assertTrue(expectedCount > 0);
+ assertEquals(brokerResponse.getNumEntriesScannedInFilter(), TOTAL_DOCS);
+ assertEquals(getCount(getBrokerResponseForOptimizedQuery("SELECT COUNT(*)
FROM testTable WHERE column7 = 225",
+ SCHEMA)), expectedCount);
+
+ brokerResponse = getBrokerResponseForOptimizedQuery(query, SCHEMA);
+ assertEquals(getCount(brokerResponse), expectedCount);
+ assertEquals(brokerResponse.getNumEntriesScannedInFilter(), 0L);
+
+ brokerResponse = getBrokerResponseForOptimizedQuery(query + " = true",
SCHEMA);
+ assertEquals(getCount(brokerResponse), expectedCount);
+ assertEquals(brokerResponse.getNumEntriesScannedInFilter(), 0L);
+ }
+
+ @Test
+ public void testArrayContainsMissingValue() {
+ BrokerResponseNative brokerResponse = getBrokerResponseForOptimizedQuery(
+ "SELECT COUNT(*) FROM testTable WHERE arrayContainsInt(column7,
-12345)", SCHEMA);
+ assertEquals(getCount(brokerResponse), 0L);
+ assertEquals(brokerResponse.getNumEntriesScannedInFilter(), 0L);
+ }
+
+ @Test
+ public void testNotArrayContainsKeepsSemantics() {
+ long containsCount = getCount(getBrokerResponseForOptimizedQuery(
+ "SELECT COUNT(*) FROM testTable WHERE arrayContainsInt(column7, 225)",
SCHEMA));
+ // NOT arrayContains means "no value equals 225", which is the complement
of arrayContains
+ long notContainsCount = getCount(getBrokerResponseForOptimizedQuery(
+ "SELECT COUNT(*) FROM testTable WHERE NOT arrayContainsInt(column7,
225)", SCHEMA));
+ assertEquals(notContainsCount, TOTAL_DOCS - containsCount);
+ }
+
+ @Test
+ public void testArrayContainsLiteralConversion() {
+ // Literals are converted to int the same way the scalar function converts
them (truncate / cast / trim)
+ long expectedCount = getCount(getBrokerResponse("SELECT COUNT(*) FROM
testTable WHERE column7 = 225"));
+ assertTrue(expectedCount > 0);
+ for (String literal : new String[]{"225.0", "225.5", "' 225'",
"4294967521"}) {
+ String filter = "arrayContainsInt(column7, " + literal + ")";
+ BrokerResponseNative brokerResponse = assertSameAsScalar(filter);
+ assertEquals(getCount(brokerResponse), expectedCount, filter);
+ assertEquals(brokerResponse.getNumEntriesScannedInFilter(), 0L, filter);
+ }
+ }
+
+ @Test
+ public void testArrayContainsCombinedPredicates() {
+ String[] filters = {
+ "arrayContainsInt(column7, 225) OR arrayContainsInt(column7, 407)",
+ "arrayContainsInt(column7, 225) AND arrayContainsInt(column7, 407)",
+ "(arrayContainsInt(column7, 225) AND column3 <> 'w') OR
(arrayContainsInt(column7, 407) "
+ + "AND NOT arrayContainsInt(column7, 225))",
+ "arrayContainsInt(column7, 225) AND (arrayContainsInt(column7, 407) OR
column8 > 500000)"
+ };
+ for (String filter : filters) {
+ assertSameAsScalar(filter);
+ }
+ // Both predicates on the same MV column must stay independent (any value
matches each), not collapse to empty
+ assertTrue(getCount(assertSameAsScalar("arrayContainsInt(column7, 225) OR
arrayContainsInt(column7, 407)")) > 0);
+ }
+
+ /// Runs the filter both through the optimizer and as the plain scalar
function, asserts the counts match, and
+ /// returns the optimized response.
+ private BrokerResponseNative assertSameAsScalar(String filter) {
+ String query = "SELECT COUNT(*) FROM testTable WHERE " + filter;
+ BrokerResponseNative scalarResponse = getBrokerResponse(query);
+ BrokerResponseNative optimizedResponse =
getBrokerResponseForOptimizedQuery(query, SCHEMA);
+ assertTrue(optimizedResponse.getExceptions().isEmpty(), filter + ": " +
optimizedResponse.getExceptions());
+ assertEquals(getCount(optimizedResponse), getCount(scalarResponse),
filter);
+ return optimizedResponse;
+ }
+
+ private static long getCount(BrokerResponseNative brokerResponse) {
+ return (long) brokerResponse.getResultTable().getRows().get(0)[0];
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]