This is an automated email from the ASF dual-hosted git repository.
thelabdude pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 86d95ce SOLR-9853: Ability to project multi-valued fields in SQL
query results (#252)
86d95ce is described below
commit 86d95ce05d2273c6b5692f911fa047dd7ebf5fba
Author: Timothy Potter <[email protected]>
AuthorDate: Wed Aug 11 08:28:18 2021 -0600
SOLR-9853: Ability to project multi-valued fields in SQL query results
(#252)
---
solr/CHANGES.txt | 2 +
.../apache/solr/handler/sql/SolrEnumerator.java | 13 ---
.../org/apache/solr/handler/sql/SolrSchema.java | 112 +++++++++++++--------
.../test-files/solr/configsets/sql/conf/schema.xml | 15 +++
.../org/apache/solr/handler/TestSQLHandler.java | 108 ++++++++++++++++++++
.../solr-ref-guide/src/parallel-sql-interface.adoc | 9 +-
.../solr/client/solrj/io/stream/JDBCStream.java | 18 +++-
.../apache/solr/client/solrj/io/sql/JdbcTest.java | 86 ++++++++++++++--
8 files changed, 296 insertions(+), 67 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 326f91b..db129f4 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -392,6 +392,8 @@ Improvements
* SOLR-15576: Allow filtering on ISO-8601 formatted timestamp literals in SQL
WHERE clause (Timothy Potter)
+* SOLR-9853: Project multi-valued fields in SQL query results (Timothy Potter)
+
Optimizations
---------------------
* SOLR-15433: Replace transient core cache LRU by Caffeine cache. (Bruno
Roustant)
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrEnumerator.java
b/solr/core/src/java/org/apache/solr/handler/sql/SolrEnumerator.java
index 944435c..e450755 100644
--- a/solr/core/src/java/org/apache/solr/handler/sql/SolrEnumerator.java
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrEnumerator.java
@@ -26,7 +26,6 @@ import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Date;
import java.util.List;
-import java.util.ArrayList;
import java.util.Map;
/** Enumerator that reads from a Solr collection. */
@@ -36,7 +35,6 @@ class SolrEnumerator implements Enumerator<Object> {
private final TupleStream tupleStream;
private final List<Map.Entry<String, Class<?>>> fields;
private Tuple current;
- private char sep = 31;
/** Creates a SolrEnumerator.
*
@@ -93,17 +91,6 @@ class SolrEnumerator implements Enumerator<Object> {
val = tuple.getDate(field.getKey());
}
- if(val instanceof ArrayList) {
- ArrayList<?> arrayList = (ArrayList<?>) val;
- StringBuilder buf = new StringBuilder();
-
- for(Object o : arrayList) {
- buf.append(sep);
- buf.append(o.toString());
- }
- val = buf.toString();
- }
-
return val;
}
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrSchema.java
b/solr/core/src/java/org/apache/solr/handler/sql/SolrSchema.java
index b5c99e3..14f3102 100644
--- a/solr/core/src/java/org/apache/solr/handler/sql/SolrSchema.java
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrSchema.java
@@ -24,6 +24,8 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -36,6 +38,7 @@ import org.apache.calcite.rel.type.RelProtoDataType;
import org.apache.calcite.schema.Table;
import org.apache.calcite.schema.impl.AbstractSchema;
import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
+import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.io.SolrClientCache;
@@ -57,6 +60,11 @@ class SolrSchema extends AbstractSchema implements Closeable
{
final SolrClientCache solrClientCache;
private volatile boolean isClosed = false;
+ // collection schema gets requested 2x times during query execution,
+ // so there's some benefit to caching it for the duration of a statement
+ // every statement gets a new SolrSchema instance
+ private Map<String, RelDataType> schemaCache = new ConcurrentHashMap<>();
+
SolrSchema(Properties properties, SolrClientCache solrClientCache) {
super();
this.properties = properties;
@@ -133,8 +141,27 @@ class SolrSchema extends AbstractSchema implements
Closeable {
private boolean isStoredOrDocValues(final EnumSet<FieldFlag> flags) {
return flags != null && (flags.contains(FieldFlag.STORED) ||
flags.contains(FieldFlag.DOC_VALUES));
}
-
+
+ private EnumSet<FieldFlag> getFieldFlags(final LukeResponse.FieldInfo
luceneFieldInfo) {
+ EnumSet<FieldFlag> flags = luceneFieldInfo.getSchemaFlags();
+ if (flags == null) {
+ String fieldSchema = luceneFieldInfo.getSchema();
+ if (fieldSchema != null) {
+ flags = LukeResponse.FieldInfo.parseFlags(fieldSchema);
+ }
+ }
+ return flags;
+ }
+
RelProtoDataType getRelDataType(String collection) {
+ return RelDataTypeImpl.proto(getRowSchema(collection));
+ }
+
+ RelDataType getRowSchema(String collection) {
+ return schemaCache.computeIfAbsent(collection, this::buildRowSchema);
+ }
+
+ RelDataType buildRowSchema(String collection) {
// Temporary type factory, just for the duration of this method. Allowable
// because we're creating a proto-type, not a type; before being used, the
// proto-type will be copied into a real type factory.
@@ -147,12 +174,12 @@ class SolrSchema extends AbstractSchema implements
Closeable {
LukeResponse schema = getSchema(collection);
// Only want fields that are stored or have docValues enabled
Map<String, LukeResponse.FieldInfo> storedFields =
schema.getFieldInfo().entrySet().stream()
- .filter(e -> isStoredOrDocValues(e.getValue().getFlags()))
+ .filter(e -> isStoredOrDocValues(getFieldFlags(e.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// merge the actual fields in use returned by Luke with the declared
fields in the schema that are empty
Map<String, LukeResponse.FieldInfo> combinedFields =
Stream.of(fieldsInUseMap, storedFields)
.flatMap(map -> map.entrySet().stream())
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(v1, v2) -> v1));
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(v1, v2) -> v1, TreeMap::new));
Map<String, Class<?>> javaClassForTypeMap = new HashMap<>(); // local
cache for custom field types we've already resolved
@@ -166,51 +193,54 @@ class SolrSchema extends AbstractSchema implements
Closeable {
}
RelDataType type;
- switch (luceneFieldType) {
- case "string":
- type = typeFactory.createJavaType(String.class);
- break;
- case "tint":
- case "tlong":
- case "int":
- case "long":
- case "pint":
- case "plong":
- type = typeFactory.createJavaType(Long.class);
- break;
- case "tfloat":
- case "tdouble":
- case "float":
- case "double":
- case "pfloat":
- case "pdouble":
- type = typeFactory.createJavaType(Double.class);
- break;
- case "pdate":
- type = typeFactory.createJavaType(Date.class);
- break;
- default:
- Class<?> javaClass = javaClassForTypeMap.get(luceneFieldType);
- if (javaClass == null) {
- javaClass =
guessJavaClassForFieldType(schema.getFieldTypeInfo().get(luceneFieldType));
- javaClassForTypeMap.put(luceneFieldType, javaClass);
- }
- type = typeFactory.createJavaType(javaClass);
- }
- /*
- EnumSet<FieldFlag> flags =
luceneFieldInfo.parseFlags(luceneFieldInfo.getSchema());
- if(flags != null && flags.contains(FieldFlag.MULTI_VALUED)) {
- type = typeFactory.createArrayType(type, -1);
+ // We have to pass multi-valued fields through Calcite as SQL Type ANY
+ // Array doesn't work for aggregations! Calcite doesn't like GROUP BY on
an ARRAY field
+ // but Solr happily computes aggs on a multi-valued field, so we have a
paradigm mis-match and
+ // ANY is the best way to retain use of operators on multi-valued fields
while still being able
+ // to GROUP BY and project the multi-valued fields in results
+ EnumSet<FieldFlag> flags = getFieldFlags(luceneFieldInfo);
+ if (flags != null && flags.contains(FieldFlag.MULTI_VALUED)) {
+ type = typeFactory.createSqlType(SqlTypeName.ANY);
+ } else {
+ switch (luceneFieldType) {
+ case "string":
+ type = typeFactory.createJavaType(String.class);
+ break;
+ case "tint":
+ case "tlong":
+ case "int":
+ case "long":
+ case "pint":
+ case "plong":
+ type = typeFactory.createJavaType(Long.class);
+ break;
+ case "tfloat":
+ case "tdouble":
+ case "float":
+ case "double":
+ case "pfloat":
+ case "pdouble":
+ type = typeFactory.createJavaType(Double.class);
+ break;
+ case "pdate":
+ type = typeFactory.createJavaType(Date.class);
+ break;
+ default:
+ Class<?> javaClass = javaClassForTypeMap.get(luceneFieldType);
+ if (javaClass == null) {
+ javaClass =
guessJavaClassForFieldType(schema.getFieldTypeInfo().get(luceneFieldType));
+ javaClassForTypeMap.put(luceneFieldType, javaClass);
+ }
+ type = typeFactory.createJavaType(javaClass);
+ }
}
- */
-
fieldInfo.add(entry.getKey(), type).nullable(true);
}
fieldInfo.add("_query_", typeFactory.createJavaType(String.class));
fieldInfo.add("score", typeFactory.createJavaType(Double.class));
- return RelDataTypeImpl.proto(fieldInfo.build());
+ return fieldInfo.build();
}
private Class<?> guessJavaClassForFieldType(LukeResponse.FieldTypeInfo
typeInfo) {
diff --git a/solr/core/src/test-files/solr/configsets/sql/conf/schema.xml
b/solr/core/src/test-files/solr/configsets/sql/conf/schema.xml
index 0638a79..a945d2e 100644
--- a/solr/core/src/test-files/solr/configsets/sql/conf/schema.xml
+++ b/solr/core/src/test-files/solr/configsets/sql/conf/schema.xml
@@ -19,6 +19,19 @@
<!-- The Solr schema file, version 1.6 -->
<schema name="sql" version="1.6">
+
+ <field name="pdoublexmv" type="pdouble" multiValued="true" stored="true"
docValues="true"/>s
+ <field name="pdoublesx" type="pdoubles" stored="true"/>
+ <field name="stringsx" type="strings" stored="true" indexed="true"/>
+ <field name="stringxmv" type="string" stored="true" multiValued="true"
docValues="true"/>
+
+ <field name="booleans" type="boolean" stored="true" indexed="true"
docValues="true" multiValued="true"/>
+ <field name="longs" type="plongx" stored="true" indexed="true"
docValues="true" multiValued="true"/>
+
+ <fieldType name="pdouble" class="solr.DoublePointField" docValues="true"/>
+ <fieldType name="pdoubles" class="solr.DoublePointField" docValues="true"
multiValued="true"/>
+
+
<!-- attribute "name" is the name of this schema and is only used for
display purposes.
Applications should change this to reflect the nature of the search
collection.
version="x.y" is Solr's version number for the schema syntax and
semantics. It should
@@ -68,6 +81,7 @@
<field name="notstored" type="string" indexed="true" docValues="false"
stored="false"/>
<field name="dvonly" type="string" docValues="true" stored="false"/>
+ <field name="textmv" type="text" multiValued="true"/>
<!-- Field type demonstrating an Analyzer failure -->
<fieldType name="failtype1" class="solr.TextField">
@@ -119,6 +133,7 @@
<fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>
<fieldType name="string" class="solr.StrField" sortMissingLast="true"/>
+ <fieldType name="strings" class="solr.StrField" sortMissingLast="true"
multiValued="true" docValues="true" indexed="true"/>
<!-- format for date is 1995-12-31T23:59:59.999Z and only the fractional
seconds part (.999) is optional.
diff --git a/solr/core/src/test/org/apache/solr/handler/TestSQLHandler.java
b/solr/core/src/test/org/apache/solr/handler/TestSQLHandler.java
index adb7332..db5f2a2 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestSQLHandler.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestSQLHandler.java
@@ -17,10 +17,13 @@
package org.apache.solr.handler;
import java.io.IOException;
+import java.time.Instant;
import java.util.Arrays;
+import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import java.util.stream.Collectors;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.LuceneTestCase.Slow;
@@ -2290,4 +2293,109 @@ public class TestSQLHandler extends SolrCloudTestCase {
expectThrows(IOException.class, () -> expectResults("SELECT id, stringx,
notafield_i FROM $ALIAS", 5));
expectThrows(IOException.class, () -> expectResults("SELECT id, stringx,
notstored FROM $ALIAS", 5));
}
+
+ @Test
+ public void testMultiValuedFieldHandling() throws Exception {
+ List<String> textmv = Arrays.asList("just some text here", "across
multiple values", "the quick brown fox jumped over the lazy dog");
+ List<String> listOfTimestamps = Arrays.asList("2021-08-06T15:37:52Z",
"2021-08-06T15:37:53Z", "2021-08-06T15:37:54Z");
+ List<Date> dates = listOfTimestamps.stream().map(ts -> new
Date(Instant.parse(ts).toEpochMilli())).collect(Collectors.toList());
+ List<String> stringxmv = Arrays.asList("a", "b", "c");
+ List<String> stringsx = Arrays.asList("d", "e", "f");
+ List<Double> pdoublesx = Arrays.asList(1d, 2d, 3d);
+ List<Double> pdoublexmv = Arrays.asList(4d, 5d, 6d);
+ List<Boolean> booleans = Arrays.asList(false, true);
+ List<Long> evenLongs = Arrays.asList(2L, 4L, 6L);
+ List<Long> oddLongs = Arrays.asList(1L, 3L, 5L);
+
+ UpdateRequest update = new UpdateRequest();
+ final int maxDocs = 10;
+ for (int i = 0; i < maxDocs; i++) {
+ SolrInputDocument doc = new SolrInputDocument("id", String.valueOf(i));
+ if (i % 2 == 0) {
+ doc.setField("stringsx", stringsx);
+ doc.setField("pdoublexmv", pdoublexmv);
+ doc.setField("longs", evenLongs);
+ } else {
+ // stringsx & pdoublexmv null
+ doc.setField("longs", oddLongs);
+ }
+ doc.setField("stringxmv", stringxmv);
+ doc.setField("pdoublesx", pdoublesx);
+ doc.setField("pdatexs", dates);
+ doc.setField("textmv", textmv);
+ doc.setField("booleans", booleans);
+ update.add(doc);
+ }
+ update.add("id", String.valueOf(maxDocs)); // all multi-valued fields are
null
+ update.commit(cluster.getSolrClient(), COLLECTIONORALIAS);
+
+ expectResults("SELECT stringxmv, stringsx, booleans FROM $ALIAS WHERE
stringxmv > 'a'", 10);
+ expectResults("SELECT stringxmv, stringsx, booleans FROM $ALIAS WHERE
stringxmv NOT IN ('a')", 1);
+ expectResults("SELECT stringxmv, stringsx, booleans FROM $ALIAS WHERE
stringxmv > 'a' LIMIT 10", 10);
+ expectResults("SELECT stringxmv, stringsx, booleans FROM $ALIAS WHERE
stringxmv NOT IN ('a') LIMIT 10", 1);
+
+ // can't sort by a mv field
+ expectThrows(IOException.class,
+ () -> expectResults("SELECT stringxmv FROM $ALIAS WHERE stringxmv IS
NOT NULL ORDER BY stringxmv ASC", 0));
+
+ // even id's have these fields, odd's are null ...
+ expectListInResults("0", "stringsx", stringsx, -1, 5);
+ expectListInResults("0", "pdoublexmv", pdoublexmv, -1, 5);
+ expectListInResults("1", "stringsx", null, -1, 0);
+ expectListInResults("1", "pdoublexmv", null, -1, 0);
+ expectListInResults("2", "stringsx", stringsx, 10, 5);
+ expectListInResults("2", "pdoublexmv", pdoublexmv, 10, 5);
+
+ expectListInResults("1", "stringxmv", stringxmv, -1, 10);
+ expectListInResults("1", "pdoublesx", pdoublesx, -1, 10);
+ expectListInResults("1", "pdatexs", listOfTimestamps, -1, 10);
+ expectListInResults("1", "booleans", booleans, -1, 10);
+ expectListInResults("1", "longs", oddLongs, -1, 5);
+
+ expectListInResults("2", "stringxmv", stringxmv, 10, 10);
+ expectListInResults("2", "pdoublesx", pdoublesx, 10, 10);
+ expectListInResults("2", "pdatexs", listOfTimestamps, 10, 10);
+ expectListInResults("2", "textmv", textmv, 10, 10);
+ expectListInResults("2", "booleans", booleans, 10, 10);
+ expectListInResults("2", "longs", evenLongs, 10, 5);
+
+ expectAggCount("stringxmv", 3);
+ expectAggCount("stringsx", 3);
+ expectAggCount("pdoublesx", 3);
+ expectAggCount("pdoublexmv", 3);
+ expectAggCount("pdatexs", 3);
+ expectAggCount("booleans", 2);
+ expectAggCount("longs", 6);
+ }
+
+ private void expectListInResults(String id, String mvField, List<?>
expected, int limit, int expCount) throws Exception {
+ String projection = limit > 0 ? "*" : "id," + mvField;
+ String sql = "SELECT " + projection + " FROM $ALIAS WHERE id='" + id + "'";
+ if (limit > 0) sql += " LIMIT " + limit;
+ List<Tuple> results = expectResults(sql, 1);
+ if (expected != null) {
+ assertEquals(expected, results.get(0).get(mvField));
+ } else {
+ assertNull(results.get(0).get(mvField));
+ }
+
+ if (expected != null) {
+ String crit = "'" + expected.get(0) + "'";
+ sql = "SELECT " + projection + " FROM $ALIAS WHERE " + mvField + "=" +
crit;
+ if (limit > 0) sql += " LIMIT " + limit;
+ expectResults(sql, expCount);
+
+ // test "IN" operator but skip for text analyzed fields
+ if (!"textmv".equals(mvField)) {
+ String inClause = expected.stream().map(o -> "'" + o +
"'").collect(Collectors.joining(","));
+ sql = "SELECT " + projection + " FROM $ALIAS WHERE " + mvField + " IN
(" + inClause + ")";
+ if (limit > 0) sql += " LIMIT " + limit;
+ expectResults(sql, expCount);
+ }
+ }
+ }
+
+ private void expectAggCount(String mvField, int expCount) throws Exception {
+ expectResults("SELECT COUNT(*), " + mvField + " FROM $ALIAS GROUP BY " +
mvField, expCount);
+ }
}
diff --git a/solr/solr-ref-guide/src/parallel-sql-interface.adoc
b/solr/solr-ref-guide/src/parallel-sql-interface.adoc
index 68caf06..c888ca9 100644
--- a/solr/solr-ref-guide/src/parallel-sql-interface.adoc
+++ b/solr/solr-ref-guide/src/parallel-sql-interface.adoc
@@ -124,6 +124,13 @@ In some cases, fields used in SQL queries must be
configured as DocValue fields.
If queries are unlimited, all fields must be DocValue fields.
If queries are limited (with the `limit` clause) then fields do not have to be
have DocValues enabled.
+.Multi-valued Fields
+[IMPORTANT]
+====
+Multi-valued fields in the project list will be returned as a `List` of
values; with JDBC, use `getObject(col)` to retrieve the multi-valued field and
then cast to a `List`.
+In general, you can project, filter, and group by, but you cannot sort by
multi-valued fields.
+====
+
=== Sending Queries
The SQL Interface provides a basic JDBC driver and an HTTP interface to
perform queries.
@@ -299,7 +306,6 @@ The parallel SQL interface supports and pushes down most
common SQL operators, s
|Operator |Description |Example |Solr Query
|= |Equals |`fielda = 10` |`fielda:10`
|<> |Does not equal |`fielda <> 10` |`-fielda:10`
-|!= |Does not equal |`fielda != 10` |`-fielda:10`
|> |Greater than |`fielda > 10` |`fielda:{10 TO *]`
|>= |Greater than or equals |`fielda >= 10` |`fielda:[10 TO *]`
|< |Less than |`fielda < 10` |`fielda:[* TO 10}`
@@ -311,6 +317,7 @@ The parallel SQL interface supports and pushes down most
common SQL operators, s
|IS NOT NULL |Match columns with value |`fielda IS NOT NULL` |`field:*`
|===
+* Use `<>` instead of `!=` for not equals
* IN, LIKE, BETWEEN support the NOT keyword to find rows where the condition
is not true, such as `fielda NOT LIKE 'day%'`
* String literals must be wrapped in single-quotes; double-quotes indicate
database objects and not a string literal.
* A simplistic LIKE can be used with an asterisk wildcard, such as `field =
'sam*'`; this is Solr specific and not part of the SQL standard.
diff --git
a/solr/solrj/src/java/org/apache/solr/client/solrj/io/stream/JDBCStream.java
b/solr/solrj/src/java/org/apache/solr/client/solrj/io/stream/JDBCStream.java
index 8045e34..3044a86 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/io/stream/JDBCStream.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/io/stream/JDBCStream.java
@@ -387,7 +387,7 @@ public class JDBCStream extends TupleStream implements
Expressible {
return columnName;
}
};
- }
+ }
// Here we are switching to check against the SQL type because date/times
are
// notorious for not being consistent. We don't know if the driver is
mapping
// to a java.time.* type or some old-school type.
@@ -427,7 +427,21 @@ public class JDBCStream extends TupleStream implements
Expressible {
return columnName;
}
};
- }
+ } else if (Object.class.getName().equals(className)) {
+ // Calcite SQL type ANY comes across as generic Object (for multi-valued
fields)
+ valueSelector = new ResultSetValueSelector() {
+ @Override
+ public Object selectValue(ResultSet resultSet) throws SQLException {
+ Object obj = resultSet.getObject(columnNumber);
+ return resultSet.wasNull() ? null : obj;
+ }
+
+ @Override
+ public String getColumnName() {
+ return columnName;
+ }
+ };
+ }
// Now we're going to start seeing if things are assignable from the
returned type
// to a more general type - this allows us to cover cases where something
we weren't
// explicitly expecting, but can handle, is being returned.
diff --git
a/solr/solrj/src/test/org/apache/solr/client/solrj/io/sql/JdbcTest.java
b/solr/solrj/src/test/org/apache/solr/client/solrj/io/sql/JdbcTest.java
index 066b47e..4451778 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/io/sql/JdbcTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/io/sql/JdbcTest.java
@@ -40,6 +40,7 @@ import
org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.cloud.AbstractDistribZkTestBase;
import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ZkStateReader;
import org.junit.BeforeClass;
@@ -84,7 +85,7 @@ public class JdbcTest extends SolrCloudTestCase {
CollectionAdminRequest.createAlias(COLLECTIONORALIAS,
collection).process(cluster.getSolrClient());
}
- new UpdateRequest()
+ UpdateRequest update = new UpdateRequest()
.add(id, "0", "a_s", "hello0", "a_i", "0", "a_f", "1", "testnull_i",
null)
.add(id, "2", "a_s", "hello0", "a_i", "2", "a_f", "2", "testnull_i",
"2")
.add(id, "3", "a_s", "hello3", "a_i", "3", "a_f", "3", "testnull_i",
null)
@@ -94,8 +95,21 @@ public class JdbcTest extends SolrCloudTestCase {
.add(id, "6", "a_s", "hello4", "a_i", "11", "a_f", "7", "testnull_i",
null)
.add(id, "7", "a_s", "hello3", "a_i", "12", "a_f", "8", "testnull_i",
"8")
.add(id, "8", "a_s", "hello3", "a_i", "13", "a_f", "9", "testnull_i",
null)
- .add(id, "9", "a_s", "hello0", "a_i", "14", "a_f", "10", "testnull_i",
"10")
- .commit(cluster.getSolrClient(), collection);
+ .add(id, "9", "a_s", "hello0", "a_i", "14", "a_f", "10", "testnull_i",
"10");
+
+ SolrInputDocument withMVs = new SolrInputDocument();
+ withMVs.setField(id, "10");
+ withMVs.setField("s_multi", Arrays.asList("abc", "lmn", "xyz"));
+ withMVs.setField("d_multi", Arrays.asList(1d, 2d, 3d));
+ update.add(withMVs);
+
+ withMVs = new SolrInputDocument();
+ withMVs.setField(id, "11");
+ withMVs.setField("s_multi", Arrays.asList("a", "b", "c"));
+ withMVs.setField("d_multi", Arrays.asList(3d, 4d, 5d));
+ update.add(withMVs);
+
+ update.commit(cluster.getSolrClient(), collection);
zkHost = cluster.getZkServer().getZkAddress();
}
@@ -107,7 +121,7 @@ public class JdbcTest extends SolrCloudTestCase {
try (Connection con = DriverManager.getConnection("jdbc:solr://" + zkHost
+ "?collection=" + COLLECTIONORALIAS, props)) {
try (Statement stmt = con.createStatement()) {
- try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " order by a_i desc limit 2")) {
+ try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " WHERE a_i IS NOT NULL order by a_i desc limit 2")) {
assertTrue(rs.next());
assertEquals(14, rs.getLong("a_i"));
@@ -130,7 +144,7 @@ public class JdbcTest extends SolrCloudTestCase {
}
//Test statement reuse
- try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " order by a_i asc limit 2")) {
+ try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " WHERE a_i IS NOT NULL order by a_i asc limit 2")) {
assertTrue(rs.next());
assertEquals(0, rs.getLong("a_i"));
@@ -155,7 +169,7 @@ public class JdbcTest extends SolrCloudTestCase {
//Test connection reuse
try (Statement stmt = con.createStatement()) {
- try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " order by a_i desc limit 2")) {
+ try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " WHERE a_i IS NOT NULL order by a_i desc limit 2")) {
assertTrue(rs.next());
assertEquals(14, rs.getLong("a_i"));
@@ -171,7 +185,7 @@ public class JdbcTest extends SolrCloudTestCase {
//Test statement reuse
stmt.setMaxRows(2);
- try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " order by a_i asc")) {
+ try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " WHERE a_i IS NOT NULL order by a_i asc")) {
assertTrue(rs.next());
assertEquals(0, rs.getLong("a_i"));
@@ -186,7 +200,7 @@ public class JdbcTest extends SolrCloudTestCase {
}
//Test simple loop. Since limit is set it will override the statement
maxRows.
- try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " order by a_i asc LIMIT 100")) {
+ try (ResultSet rs = stmt.executeQuery("select id, a_i, a_s, a_f from "
+ COLLECTIONORALIAS + " WHERE a_i IS NOT NULL order by a_i asc LIMIT
100")) {
int count = 0;
while (rs.next()) {
++count;
@@ -246,7 +260,7 @@ public class JdbcTest extends SolrCloudTestCase {
props.put("numWorkers", "2");
try (Connection con = DriverManager.getConnection("jdbc:solr://" + zkHost
+ "?collection=" + COLLECTIONORALIAS, props)) {
try (Statement stmt = con.createStatement()) {
- try (ResultSet rs = stmt.executeQuery("select a_s, sum(a_f) from " +
COLLECTIONORALIAS + " group by a_s " +
+ try (ResultSet rs = stmt.executeQuery("select a_s, sum(a_f) from " +
COLLECTIONORALIAS + " WHERE a_s IS NOT NULL group by a_s " +
"order by sum(a_f) desc")) {
assertTrue(rs.next());
@@ -290,7 +304,7 @@ public class JdbcTest extends SolrCloudTestCase {
assert (p.getProperty("numWorkers").equals("2"));
try (Statement stmt = con.createStatement()) {
- try (ResultSet rs = stmt.executeQuery("select a_s, sum(a_f) from " +
COLLECTIONORALIAS + " group by a_s " +
+ try (ResultSet rs = stmt.executeQuery("select a_s, sum(a_f) from " +
COLLECTIONORALIAS + " WHERE a_s IS NOT NULL group by a_s " +
"order by sum(a_f) desc")) {
assertTrue(rs.next());
@@ -909,4 +923,56 @@ public class JdbcTest extends SolrCloudTestCase {
assertFalse(rs.next());
}
+
+ @Test
+ public void doTestMultiValued() throws Exception {
+
+ Properties props = new Properties();
+
+ try (Connection con = DriverManager.getConnection("jdbc:solr://" + zkHost
+ "?collection=" + COLLECTIONORALIAS, props)) {
+ try (Statement stmt = con.createStatement()) {
+ // Multi-valued field projection
+ try (ResultSet rs = stmt.executeQuery("select id, s_multi, d_multi
from " + COLLECTIONORALIAS + " WHERE s_multi IS NOT NULL order by id desc limit
2")) {
+ assertTrue(rs.next());
+
+ assertEquals(Arrays.asList("a", "b", "c"), rs.getObject("s_multi"));
+ assertEquals(Arrays.asList(3d, 4d, 5d), rs.getObject("d_multi"));
+
+ assertTrue(rs.next());
+
+ assertEquals(Arrays.asList("abc", "lmn", "xyz"),
rs.getObject("s_multi"));
+ assertEquals(Arrays.asList(1d, 2d, 3d), rs.getObject("d_multi"));
+
+ assertFalse(rs.next());
+ }
+
+ // Filtering with multi-valued fields
+ try (ResultSet rs = stmt.executeQuery("select id, s_multi, d_multi
from " + COLLECTIONORALIAS + " WHERE s_multi IN ('a', 'abc') AND d_multi >= 1
order by id desc limit 2")) {
+ assertTrue(rs.next());
+
+ assertEquals(Arrays.asList("a", "b", "c"), rs.getObject("s_multi"));
+ assertEquals(Arrays.asList(3d, 4d, 5d), rs.getObject("d_multi"));
+
+ assertTrue(rs.next());
+
+ assertEquals(Arrays.asList("abc", "lmn", "xyz"),
rs.getObject("s_multi"));
+ assertEquals(Arrays.asList(1d, 2d, 3d), rs.getObject("d_multi"));
+
+ assertFalse(rs.next());
+ }
+
+ // group by multi-valued
+ try (ResultSet rs = stmt.executeQuery("select count(*) as the_count,
d_multi from " + COLLECTIONORALIAS + " WHERE d_multi IS NOT NULL GROUP BY
d_multi ORDER BY count(*) DESC")) {
+ assertTrue(rs.next());
+ assertEquals(2, rs.getLong("the_count"));
+ assertTrue(3d == rs.getDouble("d_multi"));
+ assertTrue(rs.next()); // 4 other values, all with count 1
+ assertTrue(rs.next());
+ assertTrue(rs.next());
+ assertTrue(rs.next());
+ assertFalse(rs.next());
+ }
+ }
+ }
+ }
}