Index: src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java (revision 1655562) +++ src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java (working copy) @@ -62,6 +62,7 @@ import org.apache.jackrabbit.oak.query.ast.SelectorImpl; import org.apache.jackrabbit.oak.query.ast.SimilarImpl; import org.apache.jackrabbit.oak.query.ast.SourceImpl; +import org.apache.jackrabbit.oak.query.ast.SpellcheckImpl; import org.apache.jackrabbit.oak.query.ast.UpperCaseImpl; import org.apache.jackrabbit.oak.query.index.FilterImpl; import org.apache.jackrabbit.oak.query.index.TraversingIndex; @@ -236,6 +237,13 @@ node.bindSelector(source); return super.visit(node); } + + @Override + public boolean visit(SpellcheckImpl node) { + node.setQuery(query); + node.bindSelector(source); + return super.visit(node); + } @Override public boolean visit(FullTextSearchScoreImpl node) { Index: src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java (revision 1655562) +++ src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java (working copy) @@ -557,6 +557,15 @@ String language = readString().getValue(Type.STRING); read(","); c = factory.nativeFunction(selectorName, language, parseStaticOperand()); + } else if ("SPELLCHECK".equalsIgnoreCase(functionName)) { + String selectorName; + if (currentTokenType == IDENTIFIER) { + selectorName = readName(); + read(","); + } else { + selectorName = getOnlySelectorName(); + } + c = factory.spellcheck(selectorName, parseStaticOperand()); } else { return null; } @@ -829,26 +838,29 @@ } read(")"); } + readOptionalAlias(column); } else { column.propertyName = readName(); - if (readIf(".")) { + if (column.propertyName.equals("rep:spellcheck")) { + if (readIf("(")) { + read(")"); + column.propertyName = ":spellcheck"; + } + readOptionalAlias(column); + } else if (readIf(".")) { column.selectorName = column.propertyName; if (readIf("*")) { column.propertyName = null; } else { column.propertyName = readName(); - if (readIf("AS")) { - column.columnName = readName(); - } else { + if (!readOptionalAlias(column)) { column.columnName = column.selectorName + "." + column.propertyName; } } } else { - if (readIf("AS")) { - column.columnName = readName(); - } + readOptionalAlias(column); } } list.add(column); @@ -856,6 +868,14 @@ } return list; } + + private boolean readOptionalAlias(ColumnOrWildcard column) throws ParseException { + if (readIf("AS")) { + column.columnName = readName(); + return true; + } + return false; + } private ColumnImpl[] resolveColumns(ArrayList list) throws ParseException { ArrayList columns = new ArrayList(); Index: src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java (revision 1655562) +++ src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java (working copy) @@ -157,4 +157,8 @@ return new SimilarImpl(selectorName, propertyName, path); } + public ConstraintImpl spellcheck(String selectorName, StaticOperandImpl expression) { + return new SpellcheckImpl(selectorName, expression); + } + } Index: src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java (revision 1655562) +++ src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java (working copy) @@ -79,8 +79,10 @@ boolean visit(UpperCaseImpl node); - boolean visit(NativeFunctionImpl nativeFunctionImpl); + boolean visit(NativeFunctionImpl node); - boolean visit(SimilarImpl similarImpl); + boolean visit(SimilarImpl node); + + boolean visit(SpellcheckImpl node); } \ No newline at end of file Index: src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java (revision 1655562) +++ src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java (working copy) @@ -82,6 +82,15 @@ node.getPathExpression().accept(this); return true; } + + /** + * Calls accept on the static operand in the spellcheck search constraint. + */ + @Override + public boolean visit(SpellcheckImpl node) { + node.getExpression().accept(this); + return true; + } /** * Calls accept on the two sources and the join condition in the join node. Index: src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java (revision 0) +++ src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java (working copy) @@ -0,0 +1,112 @@ +/* + * 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.jackrabbit.oak.query.ast; + +import java.util.Collections; +import java.util.Set; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.query.QueryIndex.FulltextQueryIndex; + +/** + * Support for "similar(...) + */ +public class SpellcheckImpl extends ConstraintImpl { + + public static final String NATIVE_LUCENE_LANGUAGE = "lucene"; + + public static final String SPELLCHECK_PREFIX = "spellcheck?term="; + + private final String selectorName; + private final StaticOperandImpl expression; + private SelectorImpl selector; + + SpellcheckImpl(String selectorName, StaticOperandImpl expression) { + this.selectorName = selectorName; + this.expression = expression; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("spellcheck("); + builder.append(quote(selectorName)); + builder.append(", "); + builder.append(getExpression()); + builder.append(')'); + return builder.toString(); + } + + @Override + public boolean evaluate() { + // disable evaluation if a fulltext index is used, + // and because we don't know how to process native + // conditions + if (!(selector.getIndex() instanceof FulltextQueryIndex)) { + throw new IllegalArgumentException("No full-text index was found that can process the condition " + toString()); + } + // we assume the index only returns the requested entries + return true; + } + + @Override + public Set getPropertyExistenceConditions() { + return Collections.emptySet(); + } + + @Override + public void restrict(FilterImpl f) { + if (f.getSelector().equals(selector)) { + PropertyValue p = expression.currentValue(); + String term = p.getValue(Type.STRING); + String query = SPELLCHECK_PREFIX + term; + PropertyValue v = PropertyValues.newString(query); + f.restrictProperty(NativeFunctionImpl.NATIVE_PREFIX + NATIVE_LUCENE_LANGUAGE, Operator.EQUAL, v); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (s.equals(selector)) { + selector.restrictSelector(this); + } + } + + @Override + public Set getSelectors() { + return Collections.emptySet(); + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + public StaticOperandImpl getExpression() { + return expression; + } + +} \ No newline at end of file Index: src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt =================================================================== --- src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt (revision 1655562) +++ src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt (working copy) @@ -26,6 +26,9 @@ # sql-1 query (nt:unstructured needs to be escaped in sql-2) +sql1 SELECT rep:spellcheck() FROM nt:base WHERE jcr:path = '/' AND SPELLCHECK('jackrabit') +java.lang.IllegalArgumentException: No full-text index was found that can process the condition spellcheck([nt:base], 'jackrabit') + sql1 select prop1 from nt:unstructured where prop1 is not null order by prop1 asc sql1 select excerpt(.) from nt:resource where contains(., 'jackrabbit')