You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by jb...@apache.org on 2017/03/03 16:34:54 UTC
[4/7] lucene-solr:branch_6x: Calcite changes
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrFilter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrFilter.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrFilter.java
new file mode 100644
index 0000000..ce12aec
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrFilter.java
@@ -0,0 +1,382 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.plan.*;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+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.sql.SqlKind;
+import org.apache.calcite.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of a {@link org.apache.calcite.rel.core.Filter} relational expression in Solr.
+ */
+class SolrFilter extends Filter implements SolrRel {
+ SolrFilter(
+ RelOptCluster cluster,
+ RelTraitSet traitSet,
+ RelNode child,
+ RexNode condition) {
+ super(cluster, traitSet, child, condition);
+ assert getConvention() == SolrRel.CONVENTION;
+ assert getConvention() == child.getConvention();
+ }
+
+ @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+ return super.computeSelfCost(planner, mq).multiplyBy(0.1);
+ }
+
+ public SolrFilter copy(RelTraitSet traitSet, RelNode input, RexNode condition) {
+ return new SolrFilter(getCluster(), traitSet, input, condition);
+ }
+
+ public void implement(Implementor implementor) {
+ implementor.visitChild(0, getInput());
+ if(getInput() instanceof SolrAggregate) {
+ HavingTranslator translator = new HavingTranslator(SolrRules.solrFieldNames(getRowType()), implementor.reverseAggMappings);
+ String havingPredicate = translator.translateMatch(condition);
+ implementor.setHavingPredicate(havingPredicate);
+ } else {
+ Translator translator = new Translator(SolrRules.solrFieldNames(getRowType()));
+ String query = translator.translateMatch(condition);
+ implementor.addQuery(query);
+ implementor.setNegativeQuery(translator.negativeQuery);
+ }
+ }
+
+ private static class Translator {
+
+ private final List<String> fieldNames;
+ public boolean negativeQuery = true;
+
+ Translator(List<String> fieldNames) {
+ this.fieldNames = fieldNames;
+ }
+
+ private String translateMatch(RexNode condition) {
+ if (condition.getKind().belongsTo(SqlKind.COMPARISON)) {
+ return translateComparison(condition);
+ } else if (condition.isA(SqlKind.AND)) {
+ return "(" + translateAnd(condition) + ")";
+ } else if (condition.isA(SqlKind.OR)) {
+ return "(" + translateOr(condition) + ")";
+ } else {
+ return null;
+ }
+ }
+
+ private String translateOr(RexNode condition) {
+ List<String> ors = new ArrayList<>();
+ for (RexNode node : RelOptUtil.disjunctions(condition)) {
+ ors.add(translateMatch(node));
+ }
+ return String.join(" OR ", ors);
+ }
+
+ private String translateAnd(RexNode node0) {
+ List<String> andStrings = new ArrayList();
+ List<String> notStrings = new ArrayList();
+
+ List<RexNode> ands = new ArrayList();
+ List<RexNode> nots = new ArrayList();
+ RelOptUtil.decomposeConjunction(node0, ands, nots);
+
+
+ for (RexNode node : ands) {
+ andStrings.add(translateMatch(node));
+ }
+
+ String andString = String.join(" AND ", andStrings);
+
+ if (nots.size() > 0) {
+ for (RexNode node : nots) {
+ notStrings.add(translateMatch(node));
+ }
+ String notString = String.join(" NOT ", notStrings);
+ return "(" + andString + ") NOT (" + notString + ")";
+ } else {
+ return andString;
+ }
+ }
+
+ private String translateComparison(RexNode node) {
+ Pair<String, RexLiteral> binaryTranslated = null;
+ if (((RexCall) node).getOperands().size() == 2) {
+ binaryTranslated = translateBinary((RexCall) node);
+ }
+
+ switch (node.getKind()) {
+ case NOT:
+ return "-" + translateComparison(((RexCall) node).getOperands().get(0));
+ case EQUALS:
+ String terms = binaryTranslated.getValue().toString().trim();
+ terms = terms.replace("'","");
+ if (!terms.startsWith("(") && !terms.startsWith("[") && !terms.startsWith("{")) {
+ terms = "\"" + terms + "\"";
+ }
+
+ String clause = binaryTranslated.getKey() + ":" + terms;
+ this.negativeQuery = false;
+ return clause;
+ case NOT_EQUALS:
+ return "-(" + binaryTranslated.getKey() + ":" + binaryTranslated.getValue() + ")";
+ case LESS_THAN:
+ this.negativeQuery = false;
+ return "(" + binaryTranslated.getKey() + ": [ * TO " + binaryTranslated.getValue() + " })";
+ case LESS_THAN_OR_EQUAL:
+ this.negativeQuery = false;
+ return "(" + binaryTranslated.getKey() + ": [ * TO " + binaryTranslated.getValue() + " ])";
+ case GREATER_THAN:
+ this.negativeQuery = false;
+ return "(" + binaryTranslated.getKey() + ": { " + binaryTranslated.getValue() + " TO * ])";
+ case GREATER_THAN_OR_EQUAL:
+ this.negativeQuery = false;
+ return "(" + binaryTranslated.getKey() + ": [ " + binaryTranslated.getValue() + " TO * ])";
+ default:
+ throw new AssertionError("cannot translate " + node);
+ }
+ }
+
+ /**
+ * Translates a call to a binary operator, reversing arguments if necessary.
+ */
+ private Pair<String, RexLiteral> translateBinary(RexCall call) {
+ List<RexNode> operands = call.getOperands();
+ if (operands.size() != 2) {
+ throw new AssertionError("Invalid number of arguments - " + operands.size());
+ }
+ final RexNode left = operands.get(0);
+ final RexNode right = operands.get(1);
+ final Pair<String, RexLiteral> a = translateBinary2(left, right);
+ if (a != null) {
+ return a;
+ }
+ final Pair<String, RexLiteral> b = translateBinary2(right, left);
+ if (b != null) {
+ return b;
+ }
+ throw new AssertionError("cannot translate call " + call);
+ }
+
+ /**
+ * Translates a call to a binary operator. Returns whether successful.
+ */
+ private Pair<String, RexLiteral> translateBinary2(RexNode left, RexNode right) {
+ switch (right.getKind()) {
+ case LITERAL:
+ break;
+ default:
+ return null;
+ }
+ final RexLiteral rightLiteral = (RexLiteral) right;
+ switch (left.getKind()) {
+ case INPUT_REF:
+ final RexInputRef left1 = (RexInputRef) left;
+ String name = fieldNames.get(left1.getIndex());
+ return new Pair<>(name, rightLiteral);
+ case CAST:
+ return translateBinary2(((RexCall) left).operands.get(0), right);
+// case OTHER_FUNCTION:
+// String itemName = SolrRules.isItem((RexCall) left);
+// if (itemName != null) {
+// return translateOp2(op, itemName, rightLiteral);
+// }
+ default:
+ return null;
+ }
+ }
+ }
+
+ private static class HavingTranslator {
+
+ private final List<String> fieldNames;
+ private Map<String,String> reverseAggMappings;
+
+ HavingTranslator(List<String> fieldNames, Map<String, String> reverseAggMappings) {
+ this.fieldNames = fieldNames;
+ this.reverseAggMappings = reverseAggMappings;
+ }
+
+ private String translateMatch(RexNode condition) {
+ if (condition.getKind().belongsTo(SqlKind.COMPARISON)) {
+ return translateComparison(condition);
+ } else if (condition.isA(SqlKind.AND)) {
+ return translateAnd(condition);
+ } else if (condition.isA(SqlKind.OR)) {
+ return translateOr(condition);
+ } else {
+ return null;
+ }
+ }
+
+ private String translateOr(RexNode condition) {
+ List<String> ors = new ArrayList<>();
+ for (RexNode node : RelOptUtil.disjunctions(condition)) {
+ ors.add(translateMatch(node));
+ }
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("or(");
+ int i = 0;
+ for (i = 0; i < ors.size(); i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+
+ builder.append(ors.get(i));
+ }
+ builder.append(")");
+ return builder.toString();
+ }
+
+ private String translateAnd(RexNode node0) {
+ List<String> andStrings = new ArrayList();
+ List<String> notStrings = new ArrayList();
+
+ List<RexNode> ands = new ArrayList();
+ List<RexNode> nots = new ArrayList();
+
+ RelOptUtil.decomposeConjunction(node0, ands, nots);
+
+ for (RexNode node : ands) {
+ andStrings.add(translateMatch(node));
+ }
+
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("and(");
+ for (int i = 0; i < andStrings.size(); i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+
+ builder.append(andStrings.get(i));
+ }
+ builder.append(")");
+
+
+ if (nots.size() > 0) {
+ for (RexNode node : nots) {
+ notStrings.add(translateMatch(node));
+ }
+
+ StringBuilder notBuilder = new StringBuilder();
+ for(int i=0; i< notStrings.size(); i++) {
+ if(i > 0) {
+ notBuilder.append(",");
+ }
+ notBuilder.append("not(");
+ notBuilder.append(notStrings.get(i));
+ notBuilder.append(")");
+ }
+
+ return "and(" + builder.toString() + ","+ notBuilder.toString()+")";
+ } else {
+ return builder.toString();
+ }
+ }
+
+ private String translateComparison(RexNode node) {
+ Pair<String, RexLiteral> binaryTranslated = null;
+ if (((RexCall) node).getOperands().size() == 2) {
+ binaryTranslated = translateBinary((RexCall) node);
+ }
+
+ switch (node.getKind()) {
+ case EQUALS:
+ String terms = binaryTranslated.getValue().toString().trim();
+ String clause = "eq(" + binaryTranslated.getKey() + "," + terms + ")";
+ return clause;
+ case NOT_EQUALS:
+ return "not(eq(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + "))";
+ case LESS_THAN:
+ return "lt(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
+ case LESS_THAN_OR_EQUAL:
+ return "lteq(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
+ case GREATER_THAN:
+ return "gt(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
+ case GREATER_THAN_OR_EQUAL:
+ return "gteq(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
+ default:
+ throw new AssertionError("cannot translate " + node);
+ }
+ }
+
+ /**
+ * Translates a call to a binary operator, reversing arguments if necessary.
+ */
+ private Pair<String, RexLiteral> translateBinary(RexCall call) {
+ List<RexNode> operands = call.getOperands();
+ if (operands.size() != 2) {
+ throw new AssertionError("Invalid number of arguments - " + operands.size());
+ }
+ final RexNode left = operands.get(0);
+ final RexNode right = operands.get(1);
+ final Pair<String, RexLiteral> a = translateBinary2(left, right);
+
+ if (a != null) {
+ if(reverseAggMappings.containsKey(a.getKey())) {
+ return new Pair<String, RexLiteral>(reverseAggMappings.get(a.getKey()),a.getValue());
+ }
+ return a;
+ }
+ final Pair<String, RexLiteral> b = translateBinary2(right, left);
+ if (b != null) {
+ return b;
+ }
+ throw new AssertionError("cannot translate call " + call);
+ }
+
+ /**
+ * Translates a call to a binary operator. Returns whether successful.
+ */
+ private Pair<String, RexLiteral> translateBinary2(RexNode left, RexNode right) {
+ switch (right.getKind()) {
+ case LITERAL:
+ break;
+ default:
+ return null;
+ }
+
+ final RexLiteral rightLiteral = (RexLiteral) right;
+ switch (left.getKind()) {
+ case INPUT_REF:
+ final RexInputRef left1 = (RexInputRef) left;
+ String name = fieldNames.get(left1.getIndex());
+ return new Pair<>(name, rightLiteral);
+ case CAST:
+ return translateBinary2(((RexCall) left).operands.get(0), right);
+// case OTHER_FUNCTION:
+// String itemName = SolrRules.isItem((RexCall) left);
+// if (itemName != null) {
+// return translateOp2(op, itemName, rightLiteral);
+// }
+ default:
+ return null;
+ }
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrMethod.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrMethod.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrMethod.java
new file mode 100644
index 0000000..b0bf801
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrMethod.java
@@ -0,0 +1,44 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.linq4j.tree.Types;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Builtin methods in the Solr adapter.
+ */
+enum SolrMethod {
+ SOLR_QUERYABLE_QUERY(SolrTable.SolrQueryable.class,
+ "query",
+ List.class,
+ String.class,
+ List.class,
+ List.class,
+ List.class,
+ String.class,
+ String.class,
+ String.class);
+
+ public final Method method;
+
+ SolrMethod(Class clazz, String methodName, Class... argumentTypes) {
+ this.method = Types.lookupMethod(clazz, methodName, argumentTypes);
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrProject.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrProject.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrProject.java
new file mode 100644
index 0000000..c4217f2
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrProject.java
@@ -0,0 +1,64 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.util.Pair;
+
+import java.util.List;
+
+/**
+ * Implementation of {@link org.apache.calcite.rel.core.Project} relational expression in Solr.
+ */
+class SolrProject extends Project implements SolrRel {
+ SolrProject(RelOptCluster cluster, RelTraitSet traitSet,
+ RelNode input, List<? extends RexNode> projects, RelDataType rowType) {
+ super(cluster, traitSet, input, projects, rowType);
+ assert getConvention() == SolrRel.CONVENTION;
+ assert getConvention() == input.getConvention();
+ }
+
+ @Override
+ public Project copy(RelTraitSet traitSet, RelNode input, List<RexNode> projects, RelDataType rowType) {
+ return new SolrProject(getCluster(), traitSet, input, projects, rowType);
+ }
+
+ @Override
+ public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+ return super.computeSelfCost(planner, mq).multiplyBy(0.1);
+ }
+
+ public void implement(Implementor implementor) {
+ implementor.visitChild(0, getInput());
+ final SolrRules.RexToSolrTranslator translator = new SolrRules.RexToSolrTranslator(
+ (JavaTypeFactory) getCluster().getTypeFactory(), SolrRules.solrFieldNames(getInput().getRowType()));
+ for (Pair<RexNode, String> pair : getNamedProjects()) {
+ final String name = pair.right;
+ final String expr = pair.left.accept(translator);
+ implementor.addFieldMapping(name, expr);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrRel.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrRel.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrRel.java
new file mode 100644
index 0000000..557cfe0
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrRel.java
@@ -0,0 +1,105 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.util.Pair;
+import org.apache.solr.client.solrj.io.ops.BooleanOperation;
+
+import java.util.*;
+
+/**
+ * Relational expression that uses Solr calling convention.
+ */
+interface SolrRel extends RelNode {
+ void implement(Implementor implementor);
+
+ /** Calling convention for relational operations that occur in Solr. */
+ Convention CONVENTION = new Convention.Impl("Solr", SolrRel.class);
+
+ /** Callback for the implementation process that converts a tree of {@link SolrRel} nodes into a Solr query. */
+ class Implementor {
+ final Map<String, String> fieldMappings = new HashMap<>();
+ final Map<String, String> reverseAggMappings = new HashMap<>();
+ String query = null;
+ String havingPredicate;
+ boolean negativeQuery;
+ String limitValue = null;
+ final List<Pair<String, String>> orders = new ArrayList<>();
+ final List<String> buckets = new ArrayList<>();
+ final List<Pair<String, String>> metricPairs = new ArrayList<>();
+
+ RelOptTable table;
+ SolrTable solrTable;
+
+ void addFieldMapping(String key, String val) {
+ if(key != null && !fieldMappings.containsKey(key)) {
+ this.fieldMappings.put(key, val);
+ }
+ }
+
+ void addReverseAggMapping(String key, String val) {
+ if(key != null && !reverseAggMappings.containsKey(key)) {
+ this.reverseAggMappings.put(key, val);
+ }
+ }
+
+ void addQuery(String query) {
+ this.query = query;
+ }
+
+ void setNegativeQuery(boolean negativeQuery) {
+ this.negativeQuery = negativeQuery;
+ }
+
+ void addOrder(String column, String direction) {
+ column = this.fieldMappings.getOrDefault(column, column);
+ this.orders.add(new Pair<>(column, direction));
+ }
+
+ void addBucket(String bucket) {
+ bucket = this.fieldMappings.getOrDefault(bucket, bucket);
+ this.buckets.add(bucket);
+ }
+
+ void addMetricPair(String outName, String metric, String column) {
+ column = this.fieldMappings.getOrDefault(column, column);
+ this.metricPairs.add(new Pair<>(metric, column));
+
+ String metricIdentifier = metric.toLowerCase(Locale.ROOT) + "(" + column + ")";
+ if(outName != null) {
+ this.addFieldMapping(outName, metricIdentifier);
+ }
+ }
+
+ void setHavingPredicate(String havingPredicate) {
+ this.havingPredicate = havingPredicate;
+ }
+
+
+ void setLimit(String limit) {
+ limitValue = limit;
+ }
+
+ void visitChild(int ordinal, RelNode input) {
+ assert ordinal == 0;
+ ((SolrRel) input).implement(this);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrRules.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrRules.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrRules.java
new file mode 100644
index 0000000..4cbadda
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrRules.java
@@ -0,0 +1,234 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.plan.*;
+import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.logical.LogicalSort;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexVisitorImpl;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.validate.SqlValidatorUtil;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Rules and relational operators for
+ * {@link SolrRel#CONVENTION}
+ * calling convention.
+ */
+class SolrRules {
+ static final RelOptRule[] RULES = {
+ SolrSortRule.SORT_RULE,
+ SolrFilterRule.FILTER_RULE,
+ SolrProjectRule.PROJECT_RULE,
+ SolrAggregateRule.AGGREGATE_RULE,
+ };
+
+ static List<String> solrFieldNames(final RelDataType rowType) {
+ return SqlValidatorUtil.uniquify(
+ new AbstractList<String>() {
+ @Override
+ public String get(int index) {
+ return rowType.getFieldList().get(index).getName();
+ }
+
+ @Override
+ public int size() {
+ return rowType.getFieldCount();
+ }
+ }, true);
+ }
+
+ /** Translator from {@link RexNode} to strings in Solr's expression language. */
+ static class RexToSolrTranslator extends RexVisitorImpl<String> {
+ private final JavaTypeFactory typeFactory;
+ private final List<String> inFields;
+
+ RexToSolrTranslator(JavaTypeFactory typeFactory, List<String> inFields) {
+ super(true);
+ this.typeFactory = typeFactory;
+ this.inFields = inFields;
+ }
+
+ @Override
+ public String visitInputRef(RexInputRef inputRef) {
+ return inFields.get(inputRef.getIndex());
+ }
+
+ @Override
+ public String visitCall(RexCall call) {
+ final List<String> strings = visitList(call.operands);
+ if (call.getKind() == SqlKind.CAST) {
+ return strings.get(0);
+ }
+
+ return super.visitCall(call);
+ }
+
+ private List<String> visitList(List<RexNode> list) {
+ final List<String> strings = new ArrayList<>();
+ for (RexNode node : list) {
+ strings.add(node.accept(this));
+ }
+ return strings;
+ }
+ }
+
+ /** Base class for planner rules that convert a relational expression to Solr calling convention. */
+ abstract static class SolrConverterRule extends ConverterRule {
+ final Convention out = SolrRel.CONVENTION;
+
+ SolrConverterRule(Class<? extends RelNode> clazz, String description) {
+ this(clazz, relNode -> true, description);
+ }
+
+ <R extends RelNode> SolrConverterRule(Class<R> clazz, Predicate<RelNode> predicate, String description) {
+ super(clazz, Convention.NONE, SolrRel.CONVENTION, description);
+ }
+ }
+
+ /**
+ * Rule to convert a {@link LogicalFilter} to a {@link SolrFilter}.
+ */
+ private static class SolrFilterRule extends SolrConverterRule {
+ private static boolean isNotFilterByExpr(List<RexNode> rexNodes, List<String> fieldNames) {
+
+ // We dont have a way to filter by result of aggregator now
+ boolean result = true;
+
+ for (RexNode rexNode : rexNodes) {
+ if (rexNode instanceof RexCall) {
+ result = result && isNotFilterByExpr(((RexCall) rexNode).getOperands(), fieldNames);
+ } else if (rexNode instanceof RexInputRef) {
+ result = result && !fieldNames.get(((RexInputRef) rexNode).getIndex()).startsWith("EXPR$");
+ }
+ }
+ return result;
+ }
+
+ private static final Predicate<RelNode> FILTER_PREDICATE = relNode -> {
+ List<RexNode> filterOperands = ((RexCall) ((LogicalFilter) relNode).getCondition()).getOperands();
+ return isNotFilterByExpr(filterOperands, SolrRules.solrFieldNames(relNode.getRowType()));
+ };
+
+ private static final SolrFilterRule FILTER_RULE = new SolrFilterRule();
+
+ private SolrFilterRule() {
+ super(LogicalFilter.class, FILTER_PREDICATE, "SolrFilterRule");
+ }
+
+ public RelNode convert(RelNode rel) {
+ final LogicalFilter filter = (LogicalFilter) rel;
+ final RelTraitSet traitSet = filter.getTraitSet().replace(out);
+ return new SolrFilter(
+ rel.getCluster(),
+ traitSet,
+ convert(filter.getInput(), out),
+ filter.getCondition());
+ }
+ }
+
+ /**
+ * Rule to convert a {@link LogicalProject} to a {@link SolrProject}.
+ */
+ private static class SolrProjectRule extends SolrConverterRule {
+ private static final SolrProjectRule PROJECT_RULE = new SolrProjectRule();
+
+ private SolrProjectRule() {
+ super(LogicalProject.class, "SolrProjectRule");
+ }
+
+ public RelNode convert(RelNode rel) {
+ final LogicalProject project = (LogicalProject) rel;
+ final RelNode converted = convert(project.getInput(), out);
+ final RelTraitSet traitSet = project.getTraitSet().replace(out);
+ return new SolrProject(
+ rel.getCluster(),
+ traitSet,
+ converted,
+ project.getProjects(),
+ project.getRowType());
+ }
+ }
+
+ /**
+ * Rule to convert a {@link LogicalSort} to a {@link SolrSort}.
+ */
+ private static class SolrSortRule extends SolrConverterRule {
+ static final SolrSortRule SORT_RULE = new SolrSortRule(LogicalSort.class, "SolrSortRule");
+
+ SolrSortRule(Class<? extends RelNode> clazz, String description) {
+ super(clazz, description);
+ }
+
+ @Override
+ public RelNode convert(RelNode rel) {
+ final Sort sort = (Sort) rel;
+ final RelTraitSet traitSet = sort.getTraitSet().replace(out).replace(sort.getCollation());
+ return new SolrSort(
+ rel.getCluster(),
+ traitSet,
+ convert(sort.getInput(), traitSet.replace(RelCollations.EMPTY)),
+ sort.getCollation(),
+ sort.offset,
+ sort.fetch);
+ }
+ }
+
+ /**
+ * Rule to convert an {@link LogicalAggregate} to an {@link SolrAggregate}.
+ */
+ private static class SolrAggregateRule extends SolrConverterRule {
+// private static final Predicate<RelNode> AGGREGATE_PREDICTE = relNode ->
+// Aggregate.IS_SIMPLE.apply(((LogicalAggregate)relNode));// &&
+// !((LogicalAggregate)relNode).containsDistinctCall();
+
+ private static final RelOptRule AGGREGATE_RULE = new SolrAggregateRule();
+
+ private SolrAggregateRule() {
+ super(LogicalAggregate.class, "SolrAggregateRule");
+ }
+
+ @Override
+ public RelNode convert(RelNode rel) {
+ final LogicalAggregate agg = (LogicalAggregate) rel;
+ final RelTraitSet traitSet = agg.getTraitSet().replace(out);
+ return new SolrAggregate(
+ rel.getCluster(),
+ traitSet,
+ convert(agg.getInput(), traitSet.simplify()),
+ agg.indicator,
+ agg.getGroupSet(),
+ agg.getGroupSets(),
+ agg.getAggCallList());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrSchema.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..83fa537
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrSchema.java
@@ -0,0 +1,128 @@
+/*
+ * 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.solr.handler.sql;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.calcite.rel.type.*;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.impl.AbstractSchema;
+import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.request.LukeRequest;
+import org.apache.solr.client.solrj.response.LukeResponse;
+import org.apache.solr.common.cloud.Aliases;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.luke.FieldFlag;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Properties;
+
+class SolrSchema extends AbstractSchema {
+ final Properties properties;
+
+ SolrSchema(Properties properties) {
+ super();
+ this.properties = properties;
+ }
+
+ @Override
+ protected Map<String, Table> getTableMap() {
+ String zk = this.properties.getProperty("zk");
+ try(CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder().withZkHost(zk).build()) {
+ cloudSolrClient.connect();
+ ZkStateReader zkStateReader = cloudSolrClient.getZkStateReader();
+ ClusterState clusterState = zkStateReader.getClusterState();
+
+ final ImmutableMap.Builder<String, Table> builder = ImmutableMap.builder();
+
+ for (String collection : clusterState.getCollectionsMap().keySet()) {
+ builder.put(collection, new SolrTable(this, collection));
+ }
+
+ Aliases aliases = zkStateReader.getAliases();
+ if(aliases.collectionAliasSize() > 0) {
+ for (Map.Entry<String, String> alias : aliases.getCollectionAliasMap().entrySet()) {
+ builder.put(alias.getKey(), new SolrTable(this, alias.getValue()));
+ }
+ }
+
+ return builder.build();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Map<String, LukeResponse.FieldInfo> getFieldInfo(String collection) {
+ String zk = this.properties.getProperty("zk");
+ try(CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder().withZkHost(zk).build()) {
+ cloudSolrClient.connect();
+ LukeRequest lukeRequest = new LukeRequest();
+ lukeRequest.setNumTerms(0);
+ LukeResponse lukeResponse = lukeRequest.process(cloudSolrClient, collection);
+ return lukeResponse.getFieldInfo();
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ RelProtoDataType getRelDataType(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.
+ final RelDataTypeFactory typeFactory = new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
+ final RelDataTypeFactory.FieldInfoBuilder fieldInfo = typeFactory.builder();
+ Map<String, LukeResponse.FieldInfo> luceneFieldInfoMap = getFieldInfo(collection);
+
+ for(Map.Entry<String, LukeResponse.FieldInfo> entry : luceneFieldInfoMap.entrySet()) {
+ LukeResponse.FieldInfo luceneFieldInfo = entry.getValue();
+
+ RelDataType type;
+ switch (luceneFieldInfo.getType()) {
+ case "string":
+ type = typeFactory.createJavaType(String.class);
+ break;
+ case "int":
+ case "long":
+ type = typeFactory.createJavaType(Long.class);
+ break;
+ case "float":
+ case "double":
+ type = typeFactory.createJavaType(Double.class);
+ break;
+ default:
+ type = typeFactory.createJavaType(String.class);
+ }
+
+ EnumSet<FieldFlag> flags = luceneFieldInfo.parseFlags(luceneFieldInfo.getSchema());
+ /*
+ if(flags != null && flags.contains(FieldFlag.MULTI_VALUED)) {
+ type = typeFactory.createArrayType(type, -1);
+ }
+ */
+
+ 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());
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrSort.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrSort.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrSort.java
new file mode 100644
index 0000000..1c5274a
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrSort.java
@@ -0,0 +1,79 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+
+import java.util.List;
+
+/**
+ * Implementation of {@link org.apache.calcite.rel.core.Sort} relational expression in Solr.
+ */
+class SolrSort extends Sort implements SolrRel {
+
+ SolrSort(RelOptCluster cluster, RelTraitSet traitSet, RelNode child, RelCollation collation, RexNode offset,
+ RexNode fetch) {
+ super(cluster, traitSet, child, collation, offset, fetch);
+
+ assert getConvention() == SolrRel.CONVENTION;
+ assert getConvention() == child.getConvention();
+ }
+
+ @Override
+ public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+ return planner.getCostFactory().makeZeroCost();
+ }
+
+ @Override
+ public Sort copy(RelTraitSet traitSet, RelNode input, RelCollation newCollation, RexNode offset, RexNode fetch) {
+ return new SolrSort(getCluster(), traitSet, input, collation, offset, fetch);
+ }
+
+ public void implement(Implementor implementor) {
+ implementor.visitChild(0, getInput());
+
+ List<RelFieldCollation> sortCollations = collation.getFieldCollations();
+ if (!sortCollations.isEmpty()) {
+ // Construct a series of order clauses from the desired collation
+ final List<RelDataTypeField> fields = getRowType().getFieldList();
+ for (RelFieldCollation fieldCollation : sortCollations) {
+ final String name = fields.get(fieldCollation.getFieldIndex()).getName();
+ String direction = "asc";
+ if (fieldCollation.getDirection().equals(RelFieldCollation.Direction.DESCENDING)) {
+ direction = "desc";
+ }
+ implementor.addOrder(name, direction);
+ }
+ }
+
+
+ if(fetch != null) {
+ implementor.setLimit(((RexLiteral) fetch).getValue().toString());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrTable.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrTable.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrTable.java
new file mode 100644
index 0000000..6784323
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrTable.java
@@ -0,0 +1,842 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.adapter.java.AbstractQueryableTable;
+import org.apache.calcite.linq4j.*;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelProtoDataType;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.TranslatableTable;
+import org.apache.calcite.schema.impl.AbstractTableQueryable;
+import org.apache.calcite.util.Pair;
+import org.apache.solr.client.solrj.io.comp.ComparatorOrder;
+import org.apache.solr.client.solrj.io.comp.FieldComparator;
+import org.apache.solr.client.solrj.io.comp.MultipleFieldComparator;
+import org.apache.solr.client.solrj.io.comp.StreamComparator;
+import org.apache.solr.client.solrj.io.eq.FieldEqualitor;
+import org.apache.solr.client.solrj.io.eq.MultipleFieldEqualitor;
+import org.apache.solr.client.solrj.io.eq.StreamEqualitor;
+import org.apache.solr.client.solrj.io.ops.AndOperation;
+import org.apache.solr.client.solrj.io.ops.BooleanOperation;
+import org.apache.solr.client.solrj.io.ops.EqualsOperation;
+import org.apache.solr.client.solrj.io.ops.GreaterThanEqualToOperation;
+import org.apache.solr.client.solrj.io.ops.GreaterThanOperation;
+import org.apache.solr.client.solrj.io.ops.LessThanEqualToOperation;
+import org.apache.solr.client.solrj.io.ops.LessThanOperation;
+import org.apache.solr.client.solrj.io.ops.NotOperation;
+import org.apache.solr.client.solrj.io.ops.OrOperation;
+import org.apache.solr.client.solrj.io.stream.*;
+import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParser;
+import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
+import org.apache.solr.client.solrj.io.stream.metrics.*;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Table based on a Solr collection
+ */
+class SolrTable extends AbstractQueryableTable implements TranslatableTable {
+ private static final String DEFAULT_QUERY = "*:*";
+ private static final String DEFAULT_VERSION_FIELD = "_version_";
+
+ private final String collection;
+ private final SolrSchema schema;
+ private RelProtoDataType protoRowType;
+
+ SolrTable(SolrSchema schema, String collection) {
+ super(Object[].class);
+ this.schema = schema;
+ this.collection = collection;
+ }
+
+ public String toString() {
+ return "SolrTable {" + collection + "}";
+ }
+
+ public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+ if (protoRowType == null) {
+ protoRowType = schema.getRelDataType(collection);
+ }
+ return protoRowType.apply(typeFactory);
+ }
+
+ private Enumerable<Object> query(final Properties properties) {
+ return query(properties, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(),
+ Collections.emptyList(), null, null, null);
+ }
+
+ /** Executes a Solr query on the underlying table.
+ *
+ * @param properties Connections properties
+ * @param fields List of fields to project
+ * @param query A string for the query
+ * @return Enumerator of results
+ */
+ private Enumerable<Object> query(final Properties properties,
+ final List<Map.Entry<String, Class>> fields,
+ final String query,
+ final List<Pair<String, String>> orders,
+ final List<String> buckets,
+ final List<Pair<String, String>> metricPairs,
+ final String limit,
+ final String negativeQuery,
+ final String havingPredicate) {
+ // SolrParams should be a ModifiableParams instead of a map
+ boolean mapReduce = "map_reduce".equals(properties.getProperty("aggregationMode"));
+ boolean negative = Boolean.parseBoolean(negativeQuery);
+
+ String q = null;
+
+ if (query == null) {
+ q = DEFAULT_QUERY;
+ } else {
+ if(negative) {
+ q = DEFAULT_QUERY + " AND " + query;
+ } else {
+ q = query;
+ }
+ }
+
+ TupleStream tupleStream;
+ String zk = properties.getProperty("zk");
+ try {
+ if (metricPairs.isEmpty() && buckets.isEmpty()) {
+ tupleStream = handleSelect(zk, collection, q, fields, orders, limit);
+ } else {
+ if(buckets.isEmpty()) {
+ tupleStream = handleStats(zk, collection, q, metricPairs);
+ } else {
+ if(mapReduce) {
+ tupleStream = handleGroupByMapReduce(zk,
+ collection,
+ properties,
+ fields,
+ q,
+ orders,
+ buckets,
+ metricPairs,
+ limit,
+ havingPredicate);
+ } else {
+ tupleStream = handleGroupByFacet(zk,
+ collection,
+ fields,
+ q,
+ orders,
+ buckets,
+ metricPairs,
+ limit,
+ havingPredicate);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ final TupleStream finalStream = tupleStream;
+
+ return new AbstractEnumerable<Object>() {
+ // Use original fields list to make sure only the fields specified are enumerated
+ public Enumerator<Object> enumerator() {
+ return new SolrEnumerator(finalStream, fields);
+ }
+ };
+ }
+
+ private static StreamComparator bucketSortComp(List<Bucket> buckets, Map<String,String> dirs) {
+ FieldComparator[] comps = new FieldComparator[buckets.size()];
+ for(int i=0; i<buckets.size(); i++) {
+ ComparatorOrder comparatorOrder = ComparatorOrder.fromString(dirs.get(buckets.get(i).toString()));
+ String sortKey = buckets.get(i).toString();
+ comps[i] = new FieldComparator(sortKey, comparatorOrder);
+ }
+
+ if(comps.length == 1) {
+ return comps[0];
+ } else {
+ return new MultipleFieldComparator(comps);
+ }
+ }
+
+ private static StreamComparator bucketSortComp(Bucket[] buckets, String dir) {
+ FieldComparator[] comps = new FieldComparator[buckets.length];
+ for(int i=0; i<buckets.length; i++) {
+ ComparatorOrder comparatorOrder = ascDescComp(dir);
+ String sortKey = buckets[i].toString();
+ comps[i] = new FieldComparator(sortKey, comparatorOrder);
+ }
+
+ if(comps.length == 1) {
+ return comps[0];
+ } else {
+ return new MultipleFieldComparator(comps);
+ }
+ }
+
+ private String getSortDirection(Map.Entry<String, String> order) {
+ String direction = order.getValue();
+ return direction == null ? "asc" : direction;
+ }
+
+ private StreamComparator getComp(List<? extends Map.Entry<String, String>> orders) {
+ FieldComparator[] comps = new FieldComparator[orders.size()];
+ for(int i = 0; i < orders.size(); i++) {
+ Map.Entry<String, String> order = orders.get(i);
+ String direction = getSortDirection(order);
+ ComparatorOrder comparatorOrder = ComparatorOrder.fromString(direction);
+ String sortKey = order.getKey();
+ comps[i] = new FieldComparator(sortKey, comparatorOrder);
+ }
+
+ if(comps.length == 1) {
+ return comps[0];
+ } else {
+ return new MultipleFieldComparator(comps);
+ }
+ }
+
+ private List<Metric> buildMetrics(List<Pair<String, String>> metricPairs, boolean ifEmptyCount) {
+ List<Metric> metrics = new ArrayList<>(metricPairs.size());
+ metrics.addAll(metricPairs.stream().map(this::getMetric).collect(Collectors.toList()));
+ if(metrics.size() == 0 && ifEmptyCount) {
+ metrics.add(new CountMetric());
+ }
+ return metrics;
+ }
+
+ private Metric getMetric(Pair<String, String> metricPair) {
+ switch (metricPair.getKey()) {
+ case "COUNT":
+ return new CountMetric(metricPair.getValue());
+ case "SUM":
+ case "$SUM0":
+ return new SumMetric(metricPair.getValue());
+ case "MIN":
+ return new MinMetric(metricPair.getValue());
+ case "MAX":
+ return new MaxMetric(metricPair.getValue());
+ case "AVG":
+ return new MeanMetric(metricPair.getValue());
+ default:
+ throw new IllegalArgumentException(metricPair.getKey());
+ }
+ }
+
+ private TupleStream handleSelect(String zk,
+ String collection,
+ String query,
+ List<Map.Entry<String, Class>> fields,
+ List<Pair<String, String>> orders,
+ String limit) throws IOException {
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.add(CommonParams.Q, query);
+
+ //Validate the fields
+ for(Map.Entry<String, Class> entry : fields) {
+ String fname = entry.getKey();
+ if(limit == null && "score".equals(fname)) {
+ throw new IOException("score is not a valid field for unlimited queries.");
+ }
+
+ if(fname.contains("*")) {
+ throw new IOException("* is not supported for column selection.");
+ }
+ }
+
+ String fl = getFields(fields);
+
+ if(orders.size() > 0) {
+ params.add(CommonParams.SORT, getSort(orders));
+ } else {
+ if(limit == null) {
+ params.add(CommonParams.SORT, "_version_ desc");
+ fl = fl+",_version_";
+ } else {
+ params.add(CommonParams.SORT, "score desc");
+ if(fl.indexOf("score") == -1) {
+ fl = fl + ",score";
+ }
+ }
+ }
+
+ params.add(CommonParams.FL, fl);
+
+ if (limit != null) {
+ params.add(CommonParams.ROWS, limit);
+ return new LimitStream(new CloudSolrStream(zk, collection, params), Integer.parseInt(limit));
+ } else {
+ params.add(CommonParams.QT, "/export");
+ return new CloudSolrStream(zk, collection, params);
+ }
+ }
+
+ private String getSort(List<Pair<String, String>> orders) {
+ StringBuilder buf = new StringBuilder();
+ for(Pair<String, String> pair : orders) {
+ if(buf.length() > 0) {
+ buf.append(",");
+ }
+ buf.append(pair.getKey()).append(" ").append(pair.getValue());
+ }
+
+ return buf.toString();
+ }
+
+ private String getSingleSort(Pair<String, String> order) {
+ StringBuilder buf = new StringBuilder();
+ buf.append(order.getKey()).append(" ").append(order.getValue());
+ return buf.toString();
+ }
+
+ private String getFields(List<Map.Entry<String, Class>> fields) {
+ StringBuilder buf = new StringBuilder();
+ for(Map.Entry<String, Class> field : fields) {
+
+ if(buf.length() > 0) {
+ buf.append(",");
+ }
+
+ buf.append(field.getKey());
+ }
+
+ return buf.toString();
+ }
+
+ private String getFields(Set<String> fieldSet) {
+ StringBuilder buf = new StringBuilder();
+ boolean appendVersion = true;
+ for(String field : fieldSet) {
+
+ if(buf.length() > 0) {
+ buf.append(",");
+ }
+
+ if(field.equals("_version_")) {
+ appendVersion = false;
+ }
+
+ buf.append(field);
+ }
+
+ if(appendVersion){
+ buf.append(",_version_");
+ }
+
+ return buf.toString();
+ }
+
+
+ private Set<String> getFieldSet(Metric[] metrics, List<Map.Entry<String, Class>> fields) {
+ HashSet set = new HashSet();
+ for(Metric metric : metrics) {
+ for(String column : metric.getColumns()) {
+ set.add(column);
+ }
+ }
+
+ for(Map.Entry<String, Class> field : fields) {
+ if(field.getKey().indexOf('(') == -1) {
+ set.add(field.getKey());
+ }
+ }
+
+ return set;
+ }
+
+ private static String getSortDirection(List<Pair<String, String>> orders) {
+ if(orders != null && orders.size() > 0) {
+ for(Pair<String,String> item : orders) {
+ return item.getValue();
+ }
+ }
+
+ return "asc";
+ }
+
+ private static String bucketSort(Bucket[] buckets, String dir) {
+ StringBuilder buf = new StringBuilder();
+ boolean comma = false;
+ for(Bucket bucket : buckets) {
+ if(comma) {
+ buf.append(",");
+ }
+ buf.append(bucket.toString()).append(" ").append(dir);
+ comma = true;
+ }
+
+ return buf.toString();
+ }
+
+ private static String getPartitionKeys(Bucket[] buckets) {
+ StringBuilder buf = new StringBuilder();
+ boolean comma = false;
+ for(Bucket bucket : buckets) {
+ if(comma) {
+ buf.append(",");
+ }
+ buf.append(bucket.toString());
+ comma = true;
+ }
+ return buf.toString();
+ }
+
+ private static boolean sortsEqual(Bucket[] buckets, String direction, List<Pair<String, String>> orders) {
+
+ if(buckets.length != orders.size()) {
+ return false;
+ }
+
+ for(int i=0; i< buckets.length; i++) {
+ Bucket bucket = buckets[i];
+ Pair<String, String> order = orders.get(i);
+ if(!bucket.toString().equals(order.getKey())) {
+ return false;
+ }
+
+ if(!order.getValue().toLowerCase(Locale.ROOT).contains(direction.toLowerCase(Locale.ROOT))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private TupleStream handleGroupByMapReduce(String zk,
+ String collection,
+ Properties properties,
+ final List<Map.Entry<String, Class>> fields,
+ final String query,
+ final List<Pair<String, String>> orders,
+ final List<String> _buckets,
+ final List<Pair<String, String>> metricPairs,
+ final String limit,
+ final String havingPredicate) throws IOException {
+
+ int numWorkers = Integer.parseInt(properties.getProperty("numWorkers", "1"));
+
+ Bucket[] buckets = buildBuckets(_buckets, fields);
+ Metric[] metrics = buildMetrics(metricPairs, false).toArray(new Metric[0]);
+
+ if(metrics.length == 0) {
+ return handleSelectDistinctMapReduce(zk, collection, properties, fields, query, orders, buckets, limit);
+ }
+
+ Set<String> fieldSet = getFieldSet(metrics, fields);
+
+ if(metrics.length == 0) {
+ throw new IOException("Group by queries must include atleast one aggregate function.");
+ }
+
+ String fl = getFields(fieldSet);
+ String sortDirection = getSortDirection(orders);
+ String sort = bucketSort(buckets, sortDirection);
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+
+ params.set(CommonParams.FL, fl);
+ params.set(CommonParams.Q, query);
+ //Always use the /export handler for Group By Queries because it requires exporting full result sets.
+ params.set(CommonParams.QT, "/export");
+
+ if(numWorkers > 1) {
+ params.set("partitionKeys", getPartitionKeys(buckets));
+ }
+
+ params.set("sort", sort);
+
+ TupleStream tupleStream = null;
+
+ CloudSolrStream cstream = new CloudSolrStream(zk, collection, params);
+ tupleStream = new RollupStream(cstream, buckets, metrics);
+
+ StreamFactory factory = new StreamFactory()
+ .withFunctionName("search", CloudSolrStream.class)
+ .withFunctionName("parallel", ParallelStream.class)
+ .withFunctionName("rollup", RollupStream.class)
+ .withFunctionName("sum", SumMetric.class)
+ .withFunctionName("min", MinMetric.class)
+ .withFunctionName("max", MaxMetric.class)
+ .withFunctionName("avg", MeanMetric.class)
+ .withFunctionName("count", CountMetric.class)
+ .withFunctionName("and", AndOperation.class)
+ .withFunctionName("or", OrOperation.class)
+ .withFunctionName("not", NotOperation.class)
+ .withFunctionName("eq", EqualsOperation.class)
+ .withFunctionName("gt", GreaterThanOperation.class)
+ .withFunctionName("lt", LessThanOperation.class)
+ .withFunctionName("lteq", LessThanEqualToOperation.class)
+ .withFunctionName("having", HavingStream.class)
+ .withFunctionName("gteq", GreaterThanEqualToOperation.class);
+
+ if(havingPredicate != null) {
+ BooleanOperation booleanOperation = (BooleanOperation)factory.constructOperation(StreamExpressionParser.parse(havingPredicate));
+ tupleStream = new HavingStream(tupleStream, booleanOperation);
+ }
+
+ if(numWorkers > 1) {
+ // Do the rollups in parallel
+ // Maintain the sort of the Tuples coming from the workers.
+ StreamComparator comp = bucketSortComp(buckets, sortDirection);
+ ParallelStream parallelStream = new ParallelStream(zk, collection, tupleStream, numWorkers, comp);
+
+
+ parallelStream.setStreamFactory(factory);
+ tupleStream = parallelStream;
+ }
+
+ //TODO: Currently we are not pushing down the having clause.
+ // We need to push down the having clause to ensure that LIMIT does not cut off records prior to the having filter.
+
+ if(orders != null && orders.size() > 0) {
+ if(!sortsEqual(buckets, sortDirection, orders)) {
+ int lim = (limit == null) ? 100 : Integer.parseInt(limit);
+ StreamComparator comp = getComp(orders);
+ //Rank the Tuples
+ //If parallel stream is used ALL the Rolled up tuples from the workers will be ranked
+ //Providing a true Top or Bottom.
+ tupleStream = new RankStream(tupleStream, lim, comp);
+ } else {
+ // Sort is the same as the same as the underlying stream
+ // Only need to limit the result, not Rank the result
+ if(limit != null) {
+ tupleStream = new LimitStream(tupleStream, Integer.parseInt(limit));
+ }
+ }
+ } else {
+ //No order by, check for limit
+ if(limit != null) {
+ tupleStream = new LimitStream(tupleStream, Integer.parseInt(limit));
+ }
+ }
+
+ return tupleStream;
+ }
+
+ private Bucket[] buildBuckets(List<String> buckets, List<Map.Entry<String, Class>> fields) {
+ Bucket[] bucketsArray = new Bucket[buckets.size()];
+
+ int i=0;
+ for(Map.Entry<String,Class> field : fields) {
+ String fieldName = field.getKey();
+ if(buckets.contains(fieldName)) {
+ bucketsArray[i++] = new Bucket(fieldName);
+ }
+ }
+
+ return bucketsArray;
+ }
+
+ private TupleStream handleGroupByFacet(String zkHost,
+ String collection,
+ final List<Map.Entry<String, Class>> fields,
+ final String query,
+ final List<Pair<String, String>> orders,
+ final List<String> bucketFields,
+ final List<Pair<String, String>> metricPairs,
+ final String lim,
+ final String havingPredicate) throws IOException {
+
+ ModifiableSolrParams solrParams = new ModifiableSolrParams();
+ solrParams.add(CommonParams.Q, query);
+
+ Bucket[] buckets = buildBuckets(bucketFields, fields);
+ Metric[] metrics = buildMetrics(metricPairs, true).toArray(new Metric[0]);
+ if(metrics.length == 0) {
+ metrics = new Metric[1];
+ metrics[0] = new CountMetric();
+ }
+
+ int limit = lim != null ? Integer.parseInt(lim) : 1000;
+
+ FieldComparator[] sorts = null;
+
+ if(orders == null || orders.size() == 0) {
+ sorts = new FieldComparator[buckets.length];
+ for(int i=0; i<sorts.length; i++) {
+ sorts[i] = new FieldComparator("index", ComparatorOrder.ASCENDING);
+ }
+ } else {
+ sorts = getComps(orders);
+ }
+
+ int overfetch = (int)(limit * 1.25);
+
+ TupleStream tupleStream = new FacetStream(zkHost,
+ collection,
+ solrParams,
+ buckets,
+ metrics,
+ sorts,
+ overfetch);
+
+
+
+ StreamFactory factory = new StreamFactory()
+ .withFunctionName("search", CloudSolrStream.class)
+ .withFunctionName("parallel", ParallelStream.class)
+ .withFunctionName("rollup", RollupStream.class)
+ .withFunctionName("sum", SumMetric.class)
+ .withFunctionName("min", MinMetric.class)
+ .withFunctionName("max", MaxMetric.class)
+ .withFunctionName("avg", MeanMetric.class)
+ .withFunctionName("count", CountMetric.class)
+ .withFunctionName("and", AndOperation.class)
+ .withFunctionName("or", OrOperation.class)
+ .withFunctionName("not", NotOperation.class)
+ .withFunctionName("eq", EqualsOperation.class)
+ .withFunctionName("gt", GreaterThanOperation.class)
+ .withFunctionName("lt", LessThanOperation.class)
+ .withFunctionName("lteq", LessThanEqualToOperation.class)
+ .withFunctionName("gteq", GreaterThanEqualToOperation.class);
+
+ if(havingPredicate != null) {
+ BooleanOperation booleanOperation = (BooleanOperation)factory.constructOperation(StreamExpressionParser.parse(havingPredicate));
+ tupleStream = new HavingStream(tupleStream, booleanOperation);
+ }
+
+ if(lim != null)
+ {
+ tupleStream = new LimitStream(tupleStream, limit);
+ }
+
+ return tupleStream;
+ }
+
+ private TupleStream handleSelectDistinctMapReduce(final String zkHost,
+ final String collection,
+ final Properties properties,
+ final List<Map.Entry<String, Class>> fields,
+ final String query,
+ final List<Pair<String, String>> orders,
+ final Bucket[] buckets,
+ final String limit) throws IOException{
+
+ int numWorkers = Integer.parseInt(properties.getProperty("numWorkers", "1"));
+
+ String fl = getFields(fields);
+
+ String sort = null;
+ StreamEqualitor ecomp = null;
+ StreamComparator comp = null;
+
+ if(orders != null && orders.size() > 0) {
+ StreamComparator[] adjustedSorts = adjustSorts(orders, buckets);
+ // Because of the way adjustSorts works we know that each FieldComparator has a single
+ // field name. For this reason we can just look at the leftFieldName
+ FieldEqualitor[] fieldEqualitors = new FieldEqualitor[adjustedSorts.length];
+ StringBuilder buf = new StringBuilder();
+ for(int i=0; i<adjustedSorts.length; i++) {
+ FieldComparator fieldComparator = (FieldComparator)adjustedSorts[i];
+ fieldEqualitors[i] = new FieldEqualitor(fieldComparator.getLeftFieldName());
+ if(i>0) {
+ buf.append(",");
+ }
+ buf.append(fieldComparator.getLeftFieldName()).append(" ").append(fieldComparator.getOrder().toString());
+ }
+
+ sort = buf.toString();
+
+ if(adjustedSorts.length == 1) {
+ ecomp = fieldEqualitors[0];
+ comp = adjustedSorts[0];
+ } else {
+ ecomp = new MultipleFieldEqualitor(fieldEqualitors);
+ comp = new MultipleFieldComparator(adjustedSorts);
+ }
+ } else {
+ StringBuilder sortBuf = new StringBuilder();
+ FieldEqualitor[] equalitors = new FieldEqualitor[buckets.length];
+ StreamComparator[] streamComparators = new StreamComparator[buckets.length];
+ for(int i=0; i<buckets.length; i++) {
+ equalitors[i] = new FieldEqualitor(buckets[i].toString());
+ streamComparators[i] = new FieldComparator(buckets[i].toString(), ComparatorOrder.ASCENDING);
+ if(i>0) {
+ sortBuf.append(',');
+ }
+ sortBuf.append(buckets[i].toString()).append(" asc");
+ }
+
+ sort = sortBuf.toString();
+
+ if(equalitors.length == 1) {
+ ecomp = equalitors[0];
+ comp = streamComparators[0];
+ } else {
+ ecomp = new MultipleFieldEqualitor(equalitors);
+ comp = new MultipleFieldComparator(streamComparators);
+ }
+ }
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+
+ params.set(CommonParams.FL, fl);
+ params.set(CommonParams.Q, query);
+ //Always use the /export handler for Distinct Queries because it requires exporting full result sets.
+ params.set(CommonParams.QT, "/export");
+
+ if(numWorkers > 1) {
+ params.set("partitionKeys", getPartitionKeys(buckets));
+ }
+
+ params.set("sort", sort);
+
+ TupleStream tupleStream = null;
+
+ CloudSolrStream cstream = new CloudSolrStream(zkHost, collection, params);
+ tupleStream = new UniqueStream(cstream, ecomp);
+
+ if(numWorkers > 1) {
+ // Do the unique in parallel
+ // Maintain the sort of the Tuples coming from the workers.
+ ParallelStream parallelStream = new ParallelStream(zkHost, collection, tupleStream, numWorkers, comp);
+
+ StreamFactory factory = new StreamFactory()
+ .withFunctionName("search", CloudSolrStream.class)
+ .withFunctionName("parallel", ParallelStream.class)
+ .withFunctionName("unique", UniqueStream.class);
+
+ parallelStream.setStreamFactory(factory);
+ tupleStream = parallelStream;
+ }
+
+ if(limit != null) {
+ tupleStream = new LimitStream(tupleStream, Integer.parseInt(limit));
+ }
+
+ return tupleStream;
+ }
+
+
+ private StreamComparator[] adjustSorts(List<Pair<String, String>> orders, Bucket[] buckets) throws IOException {
+ List<FieldComparator> adjustedSorts = new ArrayList();
+ Set<String> bucketFields = new HashSet();
+ Set<String> sortFields = new HashSet();
+
+ ComparatorOrder comparatorOrder = ComparatorOrder.ASCENDING;
+ for(Pair<String, String> order : orders) {
+ sortFields.add(order.getKey());
+ adjustedSorts.add(new FieldComparator(order.getKey(), ascDescComp(order.getValue())));
+ comparatorOrder = ascDescComp(order.getValue());
+ }
+
+ for(Bucket bucket : buckets) {
+ bucketFields.add(bucket.toString());
+ }
+
+ for(String sf : sortFields) {
+ if(!bucketFields.contains(sf)) {
+ throw new IOException("All sort fields must be in the field list.");
+ }
+ }
+
+ //Add sort fields if needed
+ if(sortFields.size() < buckets.length) {
+ for(Bucket bucket : buckets) {
+ String b = bucket.toString();
+ if(!sortFields.contains(b)) {
+ adjustedSorts.add(new FieldComparator(bucket.toString(), comparatorOrder));
+ }
+ }
+ }
+
+ return adjustedSorts.toArray(new FieldComparator[adjustedSorts.size()]);
+ }
+
+ private TupleStream handleStats(String zk,
+ String collection,
+ String query,
+ List<Pair<String, String>> metricPairs) {
+
+
+ ModifiableSolrParams solrParams = new ModifiableSolrParams();
+ solrParams.add(CommonParams.Q, query);
+ Metric[] metrics = buildMetrics(metricPairs, false).toArray(new Metric[0]);
+ return new StatsStream(zk, collection, solrParams, metrics);
+ }
+
+ public <T> Queryable<T> asQueryable(QueryProvider queryProvider, SchemaPlus schema, String tableName) {
+ return new SolrQueryable<>(queryProvider, schema, this, tableName);
+ }
+
+ public RelNode toRel(RelOptTable.ToRelContext context, RelOptTable relOptTable) {
+ final RelOptCluster cluster = context.getCluster();
+ return new SolrTableScan(cluster, cluster.traitSetOf(SolrRel.CONVENTION), relOptTable, this, null);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ public static class SolrQueryable<T> extends AbstractTableQueryable<T> {
+ SolrQueryable(QueryProvider queryProvider, SchemaPlus schema, SolrTable table, String tableName) {
+ super(queryProvider, schema, table, tableName);
+ }
+
+ public Enumerator<T> enumerator() {
+ @SuppressWarnings("unchecked")
+ final Enumerable<T> enumerable = (Enumerable<T>) getTable().query(getProperties());
+ return enumerable.enumerator();
+ }
+
+ private SolrTable getTable() {
+ return (SolrTable) table;
+ }
+
+ private Properties getProperties() {
+ return schema.unwrap(SolrSchema.class).properties;
+ }
+
+ /** Called via code-generation.
+ *
+ * @see SolrMethod#SOLR_QUERYABLE_QUERY
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public Enumerable<Object> query(List<Map.Entry<String, Class>> fields, String query, List<Pair<String, String>> order,
+ List<String> buckets, List<Pair<String, String>> metricPairs, String limit, String negativeQuery, String havingPredicate) {
+ return getTable().query(getProperties(), fields, query, order, buckets, metricPairs, limit, negativeQuery, havingPredicate);
+ }
+ }
+
+ private static FieldComparator[] getComps(List<Pair<String, String>> orders) {
+ FieldComparator[] comps = new FieldComparator[orders.size()];
+ for(int i=0; i<orders.size(); i++) {
+ Pair<String,String> sortItem = orders.get(i);
+ String ordering = sortItem.getValue();
+ ComparatorOrder comparatorOrder = ascDescComp(ordering);
+ String sortKey = sortItem.getKey();
+ comps[i] = new FieldComparator(sortKey, comparatorOrder);
+ }
+
+ return comps;
+ }
+
+ private static ComparatorOrder ascDescComp(String s) {
+ if(s.toLowerCase(Locale.ROOT).contains("desc")) {
+ return ComparatorOrder.DESCENDING;
+ } else {
+ return ComparatorOrder.ASCENDING;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrTableScan.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrTableScan.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrTableScan.java
new file mode 100644
index 0000000..88c53ac
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrTableScan.java
@@ -0,0 +1,81 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.plan.*;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+
+import java.util.List;
+
+/**
+ * Relational expression representing a scan of a Solr collection.
+ */
+class SolrTableScan extends TableScan implements SolrRel {
+ private final SolrTable solrTable;
+ private final RelDataType projectRowType;
+
+ /**
+ * Creates a SolrTableScan.
+ *
+ * @param cluster Cluster
+ * @param traitSet Traits
+ * @param table Table
+ * @param solrTable Solr table
+ * @param projectRowType Fields and types to project; null to project raw row
+ */
+ SolrTableScan(RelOptCluster cluster, RelTraitSet traitSet, RelOptTable table, SolrTable solrTable,
+ RelDataType projectRowType) {
+ super(cluster, traitSet, table);
+ this.solrTable = solrTable;
+ this.projectRowType = projectRowType;
+
+ assert solrTable != null;
+ assert getConvention() == SolrRel.CONVENTION;
+ }
+
+ @Override public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+ final float f = projectRowType == null ? 1f : (float) projectRowType.getFieldCount() / 100f;
+ return super.computeSelfCost(planner, mq).multiplyBy(.1 * f);
+ }
+
+ @Override
+ public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+ assert inputs.isEmpty();
+ return this;
+ }
+
+ @Override
+ public RelDataType deriveRowType() {
+ return projectRowType != null ? projectRowType : super.deriveRowType();
+ }
+
+ @Override
+ public void register(RelOptPlanner planner) {
+ planner.addRule(SolrToEnumerableConverterRule.INSTANCE);
+ for (RelOptRule rule : SolrRules.RULES) {
+ planner.addRule(rule);
+ }
+ }
+
+ public void implement(Implementor implementor) {
+ implementor.solrTable = solrTable;
+ implementor.table = table;
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverter.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverter.java
new file mode 100644
index 0000000..10d4d4c
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverter.java
@@ -0,0 +1,135 @@
+/*
+ * 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.solr.handler.sql;
+
+import com.google.common.collect.Lists;
+import org.apache.calcite.adapter.enumerable.*;
+import org.apache.calcite.linq4j.tree.BlockBuilder;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.MethodCallExpression;
+import org.apache.calcite.plan.*;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterImpl;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.runtime.Hook;
+import org.apache.calcite.util.BuiltInMethod;
+import org.apache.calcite.util.Pair;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Relational expression representing a scan of a table in Solr
+ */
+class SolrToEnumerableConverter extends ConverterImpl implements EnumerableRel {
+ SolrToEnumerableConverter(RelOptCluster cluster, RelTraitSet traits, RelNode input) {
+ super(cluster, ConventionTraitDef.INSTANCE, traits, input);
+ }
+
+ @Override
+ public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
+ return new SolrToEnumerableConverter(getCluster(), traitSet, sole(inputs));
+ }
+
+ @Override
+ public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
+ return super.computeSelfCost(planner, mq).multiplyBy(.1);
+ }
+
+ public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
+ // Generates a call to "query" with the appropriate fields
+ final BlockBuilder list = new BlockBuilder();
+ final SolrRel.Implementor solrImplementor = new SolrRel.Implementor();
+ solrImplementor.visitChild(0, getInput());
+ final RelDataType rowType = getRowType();
+ final PhysType physType = PhysTypeImpl.of(implementor.getTypeFactory(), rowType, pref.prefer(JavaRowFormat.ARRAY));
+ final Expression table = list.append("table", solrImplementor.table.getExpression(SolrTable.SolrQueryable.class));
+ final Expression fields =
+ list.append("fields",
+ constantArrayList(
+ Pair.zip(generateFields(SolrRules.solrFieldNames(rowType), solrImplementor.fieldMappings),
+ new AbstractList<Class>() {
+ @Override
+ public Class get(int index) {
+ return physType.fieldClass(index);
+ }
+
+ @Override
+ public int size() {
+ return rowType.getFieldCount();
+ }
+ }),
+ Pair.class));
+ final Expression query = list.append("query", Expressions.constant(solrImplementor.query, String.class));
+ final Expression orders = list.append("orders", constantArrayList(solrImplementor.orders, Pair.class));
+ final Expression buckets = list.append("buckets", constantArrayList(solrImplementor.buckets, String.class));
+ final Expression metricPairs = list.append("metricPairs", constantArrayList(solrImplementor.metricPairs, Pair.class));
+ final Expression limit = list.append("limit", Expressions.constant(solrImplementor.limitValue));
+ final Expression negativeQuery = list.append("negativeQuery", Expressions.constant(Boolean.toString(solrImplementor.negativeQuery), String.class));
+ final Expression havingPredicate = list.append("havingTest", Expressions.constant(solrImplementor.havingPredicate, String.class));
+ Expression enumerable = list.append("enumerable", Expressions.call(table, SolrMethod.SOLR_QUERYABLE_QUERY.method,
+ fields, query, orders, buckets, metricPairs, limit, negativeQuery, havingPredicate));
+ Hook.QUERY_PLAN.run(query);
+ list.add(Expressions.return_(null, enumerable));
+ return implementor.result(physType, list.toBlock());
+ }
+
+ private List<String> generateFields(List<String> queryFields, Map<String, String> fieldMappings) {
+ if(fieldMappings.isEmpty()) {
+ return queryFields;
+ } else {
+ List<String> fields = new ArrayList<>();
+ for(String field : queryFields) {
+ fields.add(getField(fieldMappings, field));
+ }
+ return fields;
+ }
+ }
+
+ private String getField(Map<String, String> fieldMappings, String field) {
+ String retField = field;
+ while(fieldMappings.containsKey(field)) {
+ field = fieldMappings.getOrDefault(field, retField);
+ if(retField.equals(field)) {
+ break;
+ } else {
+ retField = field;
+ }
+ }
+ return retField;
+ }
+
+ /**
+ * E.g. {@code constantArrayList("x", "y")} returns
+ * "Arrays.asList('x', 'y')".
+ */
+ private static <T> MethodCallExpression constantArrayList(List<T> values, Class clazz) {
+ return Expressions.call(BuiltInMethod.ARRAYS_AS_LIST.method,
+ Expressions.newArrayInit(clazz, constantList(values)));
+ }
+
+ /**
+ * E.g. {@code constantList("x", "y")} returns "{ConstantExpression("x"), ConstantExpression("y")}".
+ */
+ private static <T> List<Expression> constantList(List<T> values) {
+ return Lists.transform(values, Expressions::constant);
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverterRule.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverterRule.java b/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverterRule.java
new file mode 100644
index 0000000..80365ca
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/SolrToEnumerableConverterRule.java
@@ -0,0 +1,39 @@
+/*
+ * 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.solr.handler.sql;
+
+import org.apache.calcite.adapter.enumerable.EnumerableConvention;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+
+/**
+ * Rule to convert a relational expression from {@link SolrRel#CONVENTION} to {@link EnumerableConvention}.
+ */
+class SolrToEnumerableConverterRule extends ConverterRule {
+ static final ConverterRule INSTANCE = new SolrToEnumerableConverterRule();
+
+ private SolrToEnumerableConverterRule() {
+ super(RelNode.class, SolrRel.CONVENTION, EnumerableConvention.INSTANCE, "SolrToEnumerableConverterRule");
+ }
+
+ @Override
+ public RelNode convert(RelNode rel) {
+ RelTraitSet newTraitSet = rel.getTraitSet().replace(getOutConvention());
+ return new SolrToEnumerableConverter(rel.getCluster(), newTraitSet, rel);
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/3370dbed/solr/core/src/java/org/apache/solr/handler/sql/package-info.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/sql/package-info.java b/solr/core/src/java/org/apache/solr/handler/sql/package-info.java
new file mode 100644
index 0000000..5aef90d
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/sql/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes related to Apache Calcite implementation in {@link org.apache.solr.handler.SQLHandler}
+ */
+package org.apache.solr.handler.sql;
\ No newline at end of file