You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by th...@apache.org on 2021/11/22 18:36:34 UTC

[jackrabbit-oak] branch OAK-9625 created (now ae74561)

This is an automated email from the ASF dual-hosted git repository.

thomasm pushed a change to branch OAK-9625
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git.


      at ae74561  OAK-9625 Support ordered index for first value of a multi-valued property, and path

This branch includes the following new commits:

     new ae74561  OAK-9625 Support ordered index for first value of a multi-valued property, and path

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[jackrabbit-oak] 01/01: OAK-9625 Support ordered index for first value of a multi-valued property, and path

Posted by th...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

thomasm pushed a commit to branch OAK-9625
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit ae7456195ab705de368cf90a25ae8e4c06fc5900
Author: thomasm <th...@apache.org>
AuthorDate: Mon Nov 22 19:36:23 2021 +0100

    OAK-9625 Support ordered index for first value of a multi-valued property, and path
---
 .../org/apache/jackrabbit/oak/query/QueryImpl.java | 119 ++++++------
 .../apache/jackrabbit/oak/query/SQL2Parser.java    |  36 ++--
 .../oak/query/ast/AstElementFactory.java           |  18 +-
 .../jackrabbit/oak/query/ast/AstVisitor.java       |   7 +-
 .../jackrabbit/oak/query/ast/AstVisitorBase.java   |  16 +-
 .../apache/jackrabbit/oak/query/ast/FirstImpl.java | 139 ++++++++++++++
 .../apache/jackrabbit/oak/query/ast/PathImpl.java  | 136 ++++++++++++++
 .../oak/query/xpath/XPathToSQL2Converter.java      |  75 ++++----
 .../jackrabbit/oak/query/SQL2ParserTest.java       |  26 ++-
 .../org/apache/jackrabbit/oak/query/xpath.txt      |  16 ++
 .../apache/jackrabbit/oak/jcr/query/QueryTest.java | 206 ++++++++++++---------
 .../jackrabbit/oak/spi/query/QueryConstants.java   |   6 +
 .../jackrabbit/oak/spi/query/package-info.java     |   2 +-
 .../index/search/util/FunctionIndexProcessor.java  |  29 ++-
 .../search/util/FunctionIndexProcessorTest.java    |  65 +++++++
 15 files changed, 691 insertions(+), 205 deletions(-)

diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java
index 634b8f3..029bc14 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java
@@ -51,6 +51,7 @@ import org.apache.jackrabbit.oak.query.ast.DescendantNodeImpl;
 import org.apache.jackrabbit.oak.query.ast.DescendantNodeJoinConditionImpl;
 import org.apache.jackrabbit.oak.query.ast.DynamicOperandImpl;
 import org.apache.jackrabbit.oak.query.ast.EquiJoinConditionImpl;
+import org.apache.jackrabbit.oak.query.ast.FirstImpl;
 import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl;
 import org.apache.jackrabbit.oak.query.ast.FullTextSearchScoreImpl;
 import org.apache.jackrabbit.oak.query.ast.InImpl;
@@ -66,6 +67,7 @@ import org.apache.jackrabbit.oak.query.ast.NodeNameImpl;
 import org.apache.jackrabbit.oak.query.ast.NotImpl;
 import org.apache.jackrabbit.oak.query.ast.OrImpl;
 import org.apache.jackrabbit.oak.query.ast.OrderingImpl;
+import org.apache.jackrabbit.oak.query.ast.PathImpl;
 import org.apache.jackrabbit.oak.query.ast.PropertyExistenceImpl;
 import org.apache.jackrabbit.oak.query.ast.PropertyInexistenceImpl;
 import org.apache.jackrabbit.oak.query.ast.PropertyValueImpl;
@@ -111,7 +113,7 @@ import com.google.common.collect.Ordering;
  */
 public class QueryImpl implements Query {
 
-    public static final UnsupportedOperationException TOO_MANY_UNION = 
+    public static final UnsupportedOperationException TOO_MANY_UNION =
             new UnsupportedOperationException("Too many union queries");
     public final static int MAX_UNION = Integer.getInteger("oak.sql2MaxUnion", 1000);
 
@@ -133,17 +135,17 @@ public class QueryImpl implements Query {
     SourceImpl source;
     private String statement;
     final HashMap<String, PropertyValue> bindVariableMap = new HashMap<String, PropertyValue>();
-    
+
     /**
      * The map of indexes (each selector uses one index)
      */
     final HashMap<String, Integer> selectorIndexes = new HashMap<String, Integer>();
-    
+
     /**
      * The list of selectors of this query. For a join, there can be multiple selectors.
      */
     final ArrayList<SelectorImpl> selectors = new ArrayList<SelectorImpl>();
-    
+
     ConstraintImpl constraint;
 
     /**
@@ -152,7 +154,7 @@ public class QueryImpl implements Query {
      * purposes.
      */
     private boolean traversalEnabled = true;
-    
+
     /**
      * The query option to be used for this query.
      */
@@ -160,13 +162,13 @@ public class QueryImpl implements Query {
 
     private OrderingImpl[] orderings;
     private ColumnImpl[] columns;
-    
+
     /**
      * The columns that make a row distinct. This is all columns
      * except for "jcr:score".
      */
     private boolean[] distinctColumns;
-    
+
     private boolean explain, measure;
     private boolean distinct;
     private long limit = Long.MAX_VALUE;
@@ -174,7 +176,7 @@ public class QueryImpl implements Query {
     private long size = -1;
     private boolean prepared;
     private ExecutionContext context;
-    
+
     /**
      * whether the object has been initialised or not
      */
@@ -183,7 +185,7 @@ public class QueryImpl implements Query {
     private boolean isSortedByIndex;
 
     private final NamePathMapper namePathMapper;
-    
+
     private double estimatedCost;
 
     private final QueryEngineSettings settings;
@@ -226,7 +228,7 @@ public class QueryImpl implements Query {
                 bindVariableMap.put(node.getBindVariableName(), null);
                 return true;
             }
-            
+
              @Override
             public boolean visit(ChildNodeImpl node) {
                 node.setQuery(query);
@@ -248,6 +250,12 @@ public class QueryImpl implements Query {
             }
 
             @Override
+            public boolean visit(FirstImpl node) {
+                node.setQuery(query);
+                return super.visit(node);
+            }
+
+            @Override
             public boolean visit(ColumnImpl node) {
                 node.setQuery(query);
                 return true;
@@ -287,14 +295,14 @@ public class QueryImpl implements Query {
                 node.bindSelector(source);
                 return super.visit(node);
             }
-            
+
             @Override
             public boolean visit(SimilarImpl node) {
                 node.setQuery(query);
                 node.bindSelector(source);
                 return super.visit(node);
             }
-            
+
             @Override
             public boolean visit(SpellcheckImpl node) {
                 node.setQuery(query);
@@ -337,12 +345,19 @@ public class QueryImpl implements Query {
             }
 
             @Override
+            public boolean visit(PathImpl node) {
+                node.setQuery(query);
+                node.bindSelector(source);
+                return true;
+            }
+
+            @Override
             public boolean visit(PropertyExistenceImpl node) {
                 node.setQuery(query);
                 node.bindSelector(source);
                 return true;
             }
-            
+
             @Override
             public boolean visit(PropertyInexistenceImpl node) {
                 node.setQuery(query);
@@ -411,7 +426,7 @@ public class QueryImpl implements Query {
                 node.setQuery(query);
                 return super.visit(node);
             }
-            
+
             @Override
             public boolean visit(AndImpl node) {
                 node.setQuery(query);
@@ -446,7 +461,7 @@ public class QueryImpl implements Query {
             }
             distinctColumns[i] = distinct;
         }
-        
+
         init = true;
     }
 
@@ -491,7 +506,7 @@ public class QueryImpl implements Query {
     public void setMeasure(boolean measure) {
         this.measure = measure;
     }
-    
+
     public void setDistinct(boolean distinct) {
         this.distinct = distinct;
     }
@@ -504,7 +519,7 @@ public class QueryImpl implements Query {
     /**
      * If one of the indexes wants a warning to be logged due to path mismatch,
      * then get the warning message. Otherwise, return null.
-     * 
+     *
      * @return null (in the normal case) or the list of index plan names (if
      *         some index wants a warning to be logged)
      */
@@ -557,7 +572,7 @@ public class QueryImpl implements Query {
             }
         }
     }
-    
+
     @Override
     public Iterator<ResultRowImpl> getRows() {
         prepare();
@@ -682,7 +697,7 @@ public class QueryImpl implements Query {
     public String getPlan() {
         return source.getPlan(context.getBaseState());
     }
-    
+
     @Override
     public String getIndexCostInfo() {
         return source.getIndexCostInfo(context.getBaseState());
@@ -750,7 +765,7 @@ public class QueryImpl implements Query {
         source = result;
         isSortedByIndex = canSortByIndex();
     }
-    
+
     private static SourceImpl buildJoin(SourceImpl result, SourceImpl last, List<JoinConditionImpl> conditions) {
         if (result == null) {
             return last;
@@ -772,12 +787,12 @@ public class QueryImpl implements Query {
         // no join condition was found
         return null;
     }
- 
+
     /**
      * <b>!Test purpose only! <b>
-     * 
+     *
      * this creates a filter for the given query
-     * 
+     *
      */
     Filter createFilter(boolean preparing) {
         return source.createFilter(preparing);
@@ -995,7 +1010,7 @@ public class QueryImpl implements Query {
     public int getColumnIndex(String columnName) {
         return getColumnIndex(columns, columnName);
     }
-    
+
     static int getColumnIndex(ColumnImpl[] columns, String columnName) {
         for (int i = 0, size = columns.length; i < size; i++) {
             ColumnImpl c = columns[i];
@@ -1021,7 +1036,7 @@ public class QueryImpl implements Query {
         for (int i = 0; i < list.length; i++) {
             list[i] = selectors.get(i).getSelectorName();
         }
-        // reverse names to that for xpath, 
+        // reverse names to that for xpath,
         // the first selector is the same as the node iterator
         Collections.reverse(Arrays.asList(list));
         return list;
@@ -1067,7 +1082,7 @@ public class QueryImpl implements Query {
         // current index is below the minimum cost of the next index.
         List<? extends QueryIndex> queryIndexes = MINIMAL_COST_ORDERING
                 .sortedCopy(indexProvider.getQueryIndexes(rootState));
-        List<OrderEntry> sortOrder = getSortOrder(filter); 
+        List<OrderEntry> sortOrder = getSortOrder(filter);
         for (int i = 0; i < queryIndexes.size(); i++) {
             QueryIndex index = queryIndexes.get(i);
             double minCost = index.getMinimumCost();
@@ -1100,7 +1115,7 @@ public class QueryImpl implements Query {
                         filter, sortOrder, rootState);
                 cost = Double.POSITIVE_INFINITY;
                 for (IndexPlan p : ipList) {
-                    
+
                     long entryCount = p.getEstimatedEntryCount();
                     if (p.getSupportsPathRestriction()) {
                         entryCount = scaleEntryCount(rootState, filter, entryCount);
@@ -1199,7 +1214,7 @@ public class QueryImpl implements Query {
         return new SelectorExecutionPlan(filter.getSelector(), bestIndex,
                 bestPlan, bestCost);
     }
-    
+
     private long scaleEntryCount(NodeState rootState, FilterImpl filter, long count) {
         PathRestriction r = filter.getPathRestriction();
         if (r != PathRestriction.ALL_CHILDREN) {
@@ -1220,14 +1235,14 @@ public class QueryImpl implements Query {
             totalNodesCount = 1;
         }
         // same logic as for the property index (see ContentMirrorStoreStrategy):
-        
+
         // assume nodes in the index are evenly distributed in the repository (old idea)
         long countScaledDown = (long) ((double) count / totalNodesCount * filterPathCount);
         // assume 80% of the indexed nodes are in this subtree
         long mostNodesFromThisSubtree = (long) (filterPathCount * 0.8);
         // count can at most be the assumed subtree size
         count = Math.min(count, mostNodesFromThisSubtree);
-        // this in theory should not have any effect, 
+        // this in theory should not have any effect,
         // except if the above estimates are incorrect,
         // so this is just for safety feature
         count = Math.max(count, countScaledDown);
@@ -1238,7 +1253,7 @@ public class QueryImpl implements Query {
     public boolean isPotentiallySlow() {
         return potentiallySlowTraversalQuery;
     }
-    
+
     @Override
     public void verifyNotPotentiallySlow() {
         if (potentiallySlowTraversalQuery) {
@@ -1269,7 +1284,7 @@ public class QueryImpl implements Query {
             }
         }
     }
-    
+
     private List<OrderEntry> getSortOrder(FilterImpl filter) {
         if (orderings == null) {
             return null;
@@ -1288,7 +1303,7 @@ public class QueryImpl implements Query {
         }
         return sortOrder;
     }
-    
+
     private void logDebug(String msg) {
         if (isInternal) {
             LOG.trace(msg);
@@ -1331,7 +1346,7 @@ public class QueryImpl implements Query {
     /**
      * Validate the path is syntactically correct, and convert it to an Oak
      * internal path (including namespace remapping if needed).
-     * 
+     *
      * @param path the path
      * @return the the converted path
      */
@@ -1381,7 +1396,7 @@ public class QueryImpl implements Query {
     public long getSize() {
         return size;
     }
-    
+
     @Override
     public long getSize(SizePrecision precision, long max) {
         // Note: DISTINCT is ignored
@@ -1409,10 +1424,10 @@ public class QueryImpl implements Query {
     public ExecutionContext getExecutionContext() {
         return context;
     }
-    
+
     /**
      * Add two values, but don't let it overflow or underflow.
-     * 
+     *
      * @param x the first value
      * @param y the second value
      * @return the sum, or Long.MIN_VALUE for underflow, or Long.MAX_VALUE for
@@ -1428,7 +1443,7 @@ public class QueryImpl implements Query {
     @Override
     public Query buildAlternativeQuery() {
         Query result = this;
-        
+
         if (constraint != null) {
             Set<ConstraintImpl> unionList;
             try {
@@ -1461,14 +1476,14 @@ public class QueryImpl implements Query {
                     // re-composing the statement for better debug messages
                     left.statement = recomposeStatement(left);
                 }
-                
+
                 result = newAlternativeUnionQuery(left, right);
             }
         }
-        
+
         return result;
     }
-    
+
     private static String recomposeStatement(@NotNull QueryImpl query) {
         checkNotNull(query);
         String original = query.getStatement();
@@ -1477,7 +1492,7 @@ public class QueryImpl implements Query {
         final String where = " WHERE ";
         final String orderBy = " ORDER BY ";
         int whereOffset = where.length();
-        
+
         if (query.getConstraint() == null) {
             recomputed.append(original);
         } else {
@@ -1489,18 +1504,18 @@ public class QueryImpl implements Query {
         }
         return recomputed.toString();
     }
-    
+
     /**
      * Convenience method for creating a UnionQueryImpl with proper settings.
-     * 
+     *
      * @param left the first subquery
      * @param right the second subquery
      * @return the union query
      */
     private UnionQueryImpl newAlternativeUnionQuery(@NotNull Query left, @NotNull Query right) {
         UnionQueryImpl u = new UnionQueryImpl(
-            false, 
-            checkNotNull(left, "`left` cannot be null"), 
+            false,
+            checkNotNull(left, "`left` cannot be null"),
             checkNotNull(right, "`right` cannot be null"),
             this.settings);
         u.setExplain(explain);
@@ -1510,20 +1525,20 @@ public class QueryImpl implements Query {
         u.setOrderings(orderings);
         return u;
     }
-    
+
     @Override
     public Query copyOf() {
         if (isInit()) {
             throw new IllegalStateException("QueryImpl cannot be cloned once initialised.");
         }
-        
+
         List<ColumnImpl> cols = newArrayList();
         for (ColumnImpl c : columns) {
             cols.add((ColumnImpl) copyElementAndCheckReference(c));
         }
-                
+
         QueryImpl copy = new QueryImpl(
-            this.statement, 
+            this.statement,
             (SourceImpl) copyElementAndCheckReference(this.source),
             this.constraint,
             cols.toArray(new ColumnImpl[0]),
@@ -1536,7 +1551,7 @@ public class QueryImpl implements Query {
         copy.distinct = this.distinct;
         copy.queryOptions = this.queryOptions;
 
-        return copy;        
+        return copy;
     }
 
     @Override
@@ -1561,5 +1576,5 @@ public class QueryImpl implements Query {
     public QueryExecutionStats getQueryExecutionStats() {
         return stats;
     }
-    
+
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java
index ff87f08..0f5df2e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java
@@ -65,7 +65,7 @@ import org.slf4j.LoggerFactory;
  * language (here named SQL-1) is also supported.
  */
 public class SQL2Parser {
-    
+
     private static final Logger LOG = LoggerFactory.getLogger(SQL2Parser.class);
 
     // Character types, used during the tokenizer phase
@@ -109,16 +109,16 @@ public class SQL2Parser {
     private boolean supportSQL1;
 
     private NamePathMapper namePathMapper;
-    
+
     private final QueryEngineSettings settings;
-    
+
     private boolean literalUsageLogged;
 
     private final QueryExecutionStats stats;
 
     /**
      * Create a new parser. A parser can be re-used, but it is not thread safe.
-     * 
+     *
      * @param namePathMapper the name-path mapper to use
      * @param nodeTypes the nodetypes
      * @param settings the query engine settings
@@ -210,10 +210,10 @@ public class SQL2Parser {
 
         return q;
     }
-    
+
     /**
      * as {@link #parse(String, boolean)} by providing {@code true} to the initialisation flag.
-     * 
+     *
      * @param query
      * @return the parsed query
      * @throws ParseException
@@ -221,7 +221,7 @@ public class SQL2Parser {
     public Query parse(final String query) throws ParseException {
         return parse(query, true);
     }
-    
+
     private QueryImpl parseSelect() throws ParseException {
         read("SELECT");
         boolean distinct = readIf("DISTINCT");
@@ -290,7 +290,7 @@ public class SQL2Parser {
 
         return factory.selector(nodeTypeInfo, selectorName);
     }
-    
+
     private String readLabel() throws ParseException {
         String label = readName();
         if (!label.matches("[a-zA-Z0-9_]*") || label.isEmpty() || label.length() > 128) {
@@ -509,7 +509,7 @@ public class SQL2Parser {
     private PropertyExistenceImpl getPropertyExistence(PropertyValueImpl p) throws ParseException {
         return factory.propertyExistence(p.getSelectorName(), p.getPropertyName());
     }
-    
+
     private PropertyInexistenceImpl getPropertyInexistence(PropertyValueImpl p) throws ParseException {
         return factory.propertyInexistence(p.getSelectorName(), p.getPropertyName());
     }
@@ -621,7 +621,7 @@ public class SQL2Parser {
             } else {
                 selectorName = getOnlySelectorName();
             }
-            c = factory.spellcheck(selectorName, parseStaticOperand());            
+            c = factory.spellcheck(selectorName, parseStaticOperand());
         } else if ("SUGGEST".equalsIgnoreCase(functionName)) {
             String selectorName;
             if (currentTokenType == IDENTIFIER) {
@@ -676,6 +676,12 @@ public class SQL2Parser {
             } else {
                 op = factory.nodeLocalName(readName());
             }
+        } else if ("PATH".equalsIgnoreCase(functionName)) {
+            if (isToken(")")) {
+                op = factory.path(getOnlySelectorName());
+            } else {
+                op = factory.path(readName());
+            }
         } else if ("SCORE".equalsIgnoreCase(functionName)) {
             if (isToken(")")) {
                 op = factory.fullTextSearchScore(getOnlySelectorName());
@@ -687,6 +693,8 @@ public class SQL2Parser {
             read(",");
             DynamicOperandImpl op2 = parseDynamicOperand();
             op = factory.coalesce(op1, op2);
+        } else if ("FIRST".equalsIgnoreCase(functionName)) {
+            op = factory.first(parseDynamicOperand());
         } else if ("LOWER".equalsIgnoreCase(functionName)) {
             op = factory.lowerCase(parseDynamicOperand());
         } else if ("UPPER".equalsIgnoreCase(functionName)) {
@@ -938,7 +946,7 @@ public class SQL2Parser {
         }
         return list;
     }
-    
+
     private boolean readOptionalAlias(ColumnOrWildcard column) throws ParseException {
         if (readIf("AS")) {
             column.columnName = readName();
@@ -1097,7 +1105,7 @@ public class SQL2Parser {
                     type = CHAR_SPECIAL_1;
                     break;
                 }
-                types[i] = type = CHAR_IGNORE;                
+                types[i] = type = CHAR_IGNORE;
                 startLoop = i;
                 i += 2;
                 checkRunOver(i, len, startLoop);
@@ -1105,7 +1113,7 @@ public class SQL2Parser {
                     i++;
                     checkRunOver(i, len, startLoop);
                 }
-                i++;          
+                i++;
                 break;
             case '[':
                 types[i] = type = CHAR_BRACKETED;
@@ -1453,7 +1461,7 @@ public class SQL2Parser {
 
     /**
      * Whether the given statement is an internal query.
-     *  
+     *
      * @param statement the statement
      * @return true for an internal query
      */
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
index aeaf4d7..d3a3841 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
@@ -109,6 +109,10 @@ public class AstElementFactory {
         return new LowerCaseImpl(operand);
     }
 
+    public FirstImpl first(DynamicOperandImpl operand) {
+        return new FirstImpl(operand);
+    }
+
     public NodeLocalNameImpl nodeLocalName(String selectorName) {
         return new NodeLocalNameImpl(selectorName);
     }
@@ -117,6 +121,10 @@ public class AstElementFactory {
         return new NodeNameImpl(selectorName);
     }
 
+    public PathImpl path(String selectorName) {
+        return new PathImpl(selectorName);
+    }
+
     public NotImpl not(ConstraintImpl constraint) {
         return new NotImpl(constraint);
     }
@@ -128,7 +136,7 @@ public class AstElementFactory {
     public PropertyExistenceImpl propertyExistence(String selectorName, String propertyName) {
         return new PropertyExistenceImpl(selectorName, propertyName);
     }
-    
+
     public PropertyInexistenceImpl propertyInexistence(String selectorName, String propertyName) {
         return new PropertyInexistenceImpl(selectorName, propertyName);
     }
@@ -178,27 +186,27 @@ public class AstElementFactory {
     public ConstraintImpl suggest(String selectorName, StaticOperandImpl expression) {
         return new SuggestImpl(selectorName, expression);
     }
-    
+
     /**
      * <p>
      * as the {@link AstElement#copyOf()} can return {@code this} is the cloning is not implemented
      * by the subclass, this method add some spice around it by checking for this case and tracking
      * a DEBUG message in the logs.
      * </p>
-     * 
+     *
      * @param e the element to be cloned. Cannot be null.
      * @return same as {@link AstElement#copyOf()}
      */
     @NotNull
     public static AstElement copyElementAndCheckReference(@NotNull final AstElement e) {
         AstElement clone = checkNotNull(e).copyOf();
-        
+
         if (clone == e && LOG.isDebugEnabled()) {
             LOG.debug(
                 "Failed to clone the AstElement. Returning same reference; the client may fail. {} - {}",
                 e.getClass().getName(), e);
         }
-        
+
         return clone;
     }
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java
index 75c78f7..af3044a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java
@@ -33,6 +33,8 @@ public interface AstVisitor {
 
     boolean visit(CoalesceImpl node);
 
+    boolean visit(FirstImpl node);
+
     boolean visit(ColumnImpl node);
 
     boolean visit(ComparisonImpl node);
@@ -61,6 +63,8 @@ public interface AstVisitor {
 
     boolean visit(NodeNameImpl node);
 
+    boolean visit(PathImpl node);
+
     boolean visit(NotImpl node);
 
     boolean visit(OrderingImpl node);
@@ -84,8 +88,9 @@ public interface AstVisitor {
     boolean visit(NativeFunctionImpl node);
 
     boolean visit(SimilarImpl node);
-    
+
     boolean visit(SpellcheckImpl node);
 
     boolean visit(SuggestImpl suggest);
+
 }
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java
index 13df985..6d2a545 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java
@@ -43,7 +43,7 @@ public abstract class AstVisitorBase implements AstVisitor {
         node.getOperand2().accept(this);
         return true;
     }
-    
+
     /**
      * Calls accept on the all operands in the "in" node.
      */
@@ -64,7 +64,7 @@ public abstract class AstVisitorBase implements AstVisitor {
         node.getFullTextSearchExpression().accept(this);
         return true;
     }
-    
+
     /**
      * Calls accept on the static operand in the native search constraint.
      */
@@ -73,7 +73,7 @@ public abstract class AstVisitorBase implements AstVisitor {
         node.getNativeSearchExpression().accept(this);
         return true;
     }
-    
+
     /**
      * Calls accept on the static operand in the similar search constraint.
      */
@@ -82,7 +82,7 @@ public abstract class AstVisitorBase implements AstVisitor {
         node.getPathExpression().accept(this);
         return true;
     }
-    
+
     /**
      * Calls accept on the static operand in the spellcheck search constraint.
      */
@@ -139,6 +139,14 @@ public abstract class AstVisitorBase implements AstVisitor {
     }
 
     /**
+     * Calls accept on the dynamic operand in the first node.
+     */
+    @Override
+    public boolean visit(FirstImpl node) {
+        return node.getOperand().accept(this);
+    }
+
+    /**
      * Calls accept on the constraint in the NOT node.
      */
     @Override
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FirstImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FirstImpl.java
new file mode 100644
index 0000000..27f929f
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FirstImpl.java
@@ -0,0 +1,139 @@
+/*
+ * 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.List;
+import java.util.Set;
+
+import javax.jcr.PropertyType;
+
+import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
+import org.apache.jackrabbit.oak.query.index.FilterImpl;
+import org.apache.jackrabbit.oak.spi.query.QueryConstants;
+import org.apache.jackrabbit.oak.spi.query.QueryIndex.OrderEntry;
+
+/**
+ * The function "first(..)".
+ */
+public class FirstImpl extends DynamicOperandImpl {
+
+    private final DynamicOperandImpl operand;
+
+    public FirstImpl(DynamicOperandImpl operand) {
+        this.operand = operand;
+    }
+
+    public DynamicOperandImpl getOperand() {
+        return operand;
+    }
+
+    @Override
+    boolean accept(AstVisitor v) {
+        return v.visit(this);
+    }
+
+    @Override
+    public String toString() {
+        return "first(" + operand + ')';
+    }
+
+    @Override
+    public PropertyExistenceImpl getPropertyExistence() {
+        return operand.getPropertyExistence();
+    }
+
+    @Override
+    public Set<SelectorImpl> getSelectors() {
+        return operand.getSelectors();
+    }
+
+    @Override
+    public PropertyValue currentProperty() {
+        PropertyValue p = operand.currentProperty();
+        if (p == null) {
+            return null;
+        }
+        if (p.isArray()) {
+            if (p.count() == 0) {
+                return null;
+            }
+            return PropertyValues.newString(p.getValue(Type.STRING, 0));
+        }
+        return p;
+    }
+
+    @Override
+    public void restrict(FilterImpl f, Operator operator, PropertyValue v) {
+        if (operator == Operator.NOT_EQUAL && v != null) {
+            // not supported
+            return;
+        }
+        String fn = getFunction(f.getSelector());
+        if (fn != null) {
+            f.restrictProperty(QueryConstants.FUNCTION_RESTRICTION_PREFIX + fn,
+                    operator, v, PropertyType.STRING);
+        }
+    }
+
+    @Override
+    public void restrictList(FilterImpl f, List<PropertyValue> list) {
+        String fn = getFunction(f.getSelector());
+        f.restrictPropertyAsList(QueryConstants.FUNCTION_RESTRICTION_PREFIX + fn, list);
+    }
+
+    @Override
+    public String getFunction(SelectorImpl s) {
+        String f = operand.getFunction(s);
+        if (f == null) {
+            return null;
+        }
+        return "first*" + f;
+    }
+
+    @Override
+    public boolean canRestrictSelector(SelectorImpl s) {
+        return operand.canRestrictSelector(s);
+    }
+
+    @Override
+    int getPropertyType() {
+        return PropertyType.STRING;
+    }
+
+    @Override
+    public DynamicOperandImpl createCopy() {
+        return new FirstImpl(operand.createCopy());
+    }
+
+    @Override
+    public OrderEntry getOrderEntry(SelectorImpl s, OrderingImpl o) {
+        String fn = getFunction(s);
+        if (fn != null) {
+            return new OrderEntry(
+                QueryConstants.FUNCTION_RESTRICTION_PREFIX + fn,
+                Type.STRING,
+                o.isDescending() ?
+                OrderEntry.Order.DESCENDING : OrderEntry.Order.ASCENDING);
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PathImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PathImpl.java
new file mode 100644
index 0000000..42ba271
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/PathImpl.java
@@ -0,0 +1,136 @@
+/*
+ * 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.List;
+import java.util.Set;
+
+import javax.jcr.PropertyType;
+
+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.plugins.memory.PropertyValues;
+import org.apache.jackrabbit.oak.spi.query.QueryConstants;
+import org.apache.jackrabbit.oak.spi.query.QueryIndex.OrderEntry;
+
+/**
+ * The function "name(..)".
+ */
+public class PathImpl extends DynamicOperandImpl {
+
+    private final String selectorName;
+    private SelectorImpl selector;
+
+    public PathImpl(String selectorName) {
+        this.selectorName = selectorName;
+    }
+
+    @Override
+    boolean accept(AstVisitor v) {
+        return v.visit(this);
+    }
+
+    @Override
+    public String toString() {
+        return "path(" + quote(selectorName) + ')';
+    }
+
+    public void bindSelector(SourceImpl source) {
+        selector = source.getExistingSelector(selectorName);
+    }
+
+    @Override
+    public PropertyExistenceImpl getPropertyExistence() {
+        return null;
+    }
+
+    @Override
+    public Set<SelectorImpl> getSelectors() {
+        return Collections.singleton(selector);
+    }
+
+    @Override
+    public PropertyValue currentProperty() {
+        String path = selector.currentPath();
+        if (path == null) {
+            return null;
+        }
+        return PropertyValues.newString(path);
+    }
+
+    @Override
+    public void restrict(FilterImpl f, Operator operator, PropertyValue v) {
+        if (v == null) {
+            return;
+        }
+        if (operator == Operator.NOT_EQUAL && v != null) {
+            // not supported
+            return;
+        }
+        if (f.getSelector().equals(selector)) {
+            String path = v.getValue(Type.STRING);
+            f.restrictProperty(QueryConstants.RESTRICTION_PATH,
+                    operator, PropertyValues.newString(path));
+        }
+    }
+
+    @Override
+    public void restrictList(FilterImpl f, List<PropertyValue> list) {
+        // optimizations of type "NAME(..) IN(A, B)" are not supported
+    }
+
+    @Override
+    public String getFunction(SelectorImpl s) {
+        if (!s.equals(selector)) {
+            return null;
+        }
+        return "@" + QueryConstants.RESTRICTION_PATH;
+    }
+
+    @Override
+    public boolean canRestrictSelector(SelectorImpl s) {
+        return s.equals(selector);
+    }
+
+    @Override
+    int getPropertyType() {
+        return PropertyType.STRING;
+    }
+
+    @Override
+    public DynamicOperandImpl createCopy() {
+        return new PathImpl(selectorName);
+    }
+
+    @Override
+    public OrderEntry getOrderEntry(SelectorImpl s, OrderingImpl o) {
+        if (!s.equals(selector)) {
+            // ordered by a different selector
+            return null;
+        }
+        return new OrderEntry(
+                QueryConstants.FUNCTION_RESTRICTION_PREFIX + getFunction(s),
+            Type.STRING,
+            o.isDescending() ?
+            OrderEntry.Order.DESCENDING : OrderEntry.Order.ASCENDING);
+    }
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java
index 711c7fd..c2d7b62 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java
@@ -33,18 +33,18 @@ import java.util.Locale;
  * This class can can convert a XPATH query to a SQL2 query.
  */
 public class XPathToSQL2Converter {
-    
+
     /**
-     * Optimize queries of the form "from [nt:base] where [jcr:primaryType] = 'x'" 
+     * Optimize queries of the form "from [nt:base] where [jcr:primaryType] = 'x'"
      * to "from [x] where [jcr:primaryType] = 'x'".
      * Enabled by default.
      */
     public static final boolean NODETYPE_OPTIMIZATION = Boolean.parseBoolean(
             System.getProperty("oak.xpathNodeTypeOptimization", "true"));
-    
+
     /**
      * Convert queries of the form "where [jcr:primaryType] = 'x' or [jcr:primaryType] = 'y'"
-     * to "select ... where [jcr:primaryType] = 'x' union select ... where [jcr:primaryType] = 'y'". 
+     * to "select ... where [jcr:primaryType] = 'x' union select ... where [jcr:primaryType] = 'y'".
      * If disabled, only one query with "where [jcr:primaryType] in ('x', 'y') is used.
      * Enabled by default.
      */
@@ -88,11 +88,11 @@ public class XPathToSQL2Converter {
         statement = statement.optimize();
         return statement.toString();
     }
-    
+
     private Statement convertToStatement(String query) throws ParseException {
-        
+
         query = query.trim();
-        
+
         Statement statement = new Statement();
 
         if (query.startsWith("explain ")) {
@@ -103,19 +103,19 @@ public class XPathToSQL2Converter {
             query = query.substring("measure".length()).trim();
             statement.setMeasure(true);
         }
-        
+
         if (query.isEmpty()) {
             // special case, will always result in an empty result
             query = "//jcr:root";
         }
-        
+
         statement.setOriginalQuery(query);
-        
+
         initialize(query);
-        
+
         expected = new ArrayList<String>();
         read();
-        
+
         if (currentTokenType == END) {
             throw getSyntaxError("the query may not be empty");
         }
@@ -126,11 +126,11 @@ public class XPathToSQL2Converter {
         boolean startOfQuery = true;
 
         while (true) {
-            
+
             // if true, path or nodeType conditions are not allowed
             boolean shortcut = false;
             boolean slash = readIf("/");
-            
+
             if (!slash) {
                 if (startOfQuery) {
                     // the query doesn't start with "/"
@@ -209,7 +209,7 @@ public class XPathToSQL2Converter {
                             currentSelector.nodeName = "jcr:xmltext";
                         } else {
                             currentSelector.path = PathUtils.concat(currentSelector.path, "jcr:xmltext");
-                        }                        
+                        }
                     } else if ("element".equals(identifier)) {
                         // "...element(..."
                         if (readIf(")")) {
@@ -298,7 +298,7 @@ public class XPathToSQL2Converter {
                     } else if (readIf("rep:spellcheck")) {
                         // only rep:spellcheck() is currently supported
                         read("(");
-                        read(")");                        
+                        read(")");
                         Expression.Property p = new Expression.Property(currentSelector, "rep:spellcheck()", false);
                         statement.addSelectColumn(p);
                     } else if (readIf("rep:suggest")) {
@@ -410,7 +410,7 @@ public class XPathToSQL2Converter {
         statement.setColumnSelector(currentSelector);
         statement.setSelectors(selectors);
         statement.setQueryOptions(options);
-        
+
         Expression where = null;
         for (Selector s : selectors) {
             where = Expression.and(where, s.condition);
@@ -418,7 +418,7 @@ public class XPathToSQL2Converter {
         statement.setWhere(where);
         return statement;
     }
-    
+
     private void appendNodeName(String name) {
         if (!currentSelector.isChild) {
             currentSelector.nodeName = name;
@@ -435,7 +435,7 @@ public class XPathToSQL2Converter {
             }
         }
     }
-    
+
     /**
      * Switch back to the old selector when reading a property. This occurs
      * after reading a "/", but then reading a property or a list of properties.
@@ -464,8 +464,8 @@ public class XPathToSQL2Converter {
             String n = currentSelector.nodeName;
             // encode again, because it will be decoded again
             n = ISO9075.encode(n);
-            Expression.Condition c = new Expression.Condition(f, "=", 
-                    Expression.Literal.newString(n), 
+            Expression.Condition c = new Expression.Condition(f, "=",
+                    Expression.Literal.newString(n),
                     Expression.PRECEDENCE_CONDITION);
             condition = Expression.and(condition, c);
         }
@@ -485,7 +485,7 @@ public class XPathToSQL2Converter {
                 c.params.add(new Expression.SelectorExpr(currentSelector));
                 c.params.add(new Expression.SelectorExpr(selectors.get(selectors.size() - 1)));
                 joinCondition = c;
-            } 
+            }
         } else if (currentSelector.isParent) {
             if (isFirstSelector) {
                 throw getSyntaxError();
@@ -602,7 +602,7 @@ public class XPathToSQL2Converter {
             c = new Expression.Condition(left, "<=", parseExpression(), Expression.PRECEDENCE_CONDITION);
         } else if (readIf(">=")) {
             c = new Expression.Condition(left, ">=", parseExpression(), Expression.PRECEDENCE_CONDITION);
-        // TODO support "x eq y"? it seems this only matches for single value properties?  
+        // TODO support "x eq y"? it seems this only matches for single value properties?
         // } else if (readIf("eq")) {
         //    c = new Condition(left, "==", parseExpression(), Expression.PRECEDENCE_CONDITION);
         } else {
@@ -701,7 +701,7 @@ public class XPathToSQL2Converter {
 
     private Expression parseFunction(String functionName) throws ParseException {
         if ("jcr:like".equals(functionName)) {
-            Expression.Condition c = new Expression.Condition(parseExpression(), 
+            Expression.Condition c = new Expression.Condition(parseExpression(),
                     "like", null, Expression.PRECEDENCE_CONDITION);
             read(",");
             c.right = parseExpression();
@@ -724,10 +724,8 @@ public class XPathToSQL2Converter {
             Expression.Cast c = new Expression.Cast(expr, "date");
             read(")");
             return c;
-        } else if ("fn:coalesce".equals(functionName)) {
-            Expression.Function f = new Expression.Function("coalesce");
-            f.params.add(parseExpression());
-            read(",");
+        } else if ("jcr:first".equals(functionName)) {
+            Expression.Function f = new Expression.Function("first");
             f.params.add(parseExpression());
             read(")");
             return f;
@@ -755,6 +753,15 @@ public class XPathToSQL2Converter {
             }
             f.params.add(new Expression.SelectorExpr(currentSelector));
             return f;
+        } else if ("fn:path".equals(functionName)) {
+            Expression.Function f = new Expression.Function("path");
+            if (!readIf(")")) {
+                // only path(.) and path() are currently supported
+                read(".");
+                read(")");
+            }
+            f.params.add(new Expression.SelectorExpr(currentSelector));
+            return f;
         } else if ("fn:local-name".equals(functionName)) {
             Expression.Function f = new Expression.Function("localname");
             if (!readIf(")")) {
@@ -791,7 +798,7 @@ public class XPathToSQL2Converter {
             read(")");
             return new Expression.Suggest(term);
         } else {
-            throw getSyntaxError("jcr:like | jcr:contains | jcr:score | xs:dateTime | " + 
+            throw getSyntaxError("jcr:like | jcr:contains | jcr:score | xs:dateTime | " +
                     "fn:lower-case | fn:upper-case | fn:name | rep:similar | rep:spellcheck | rep:suggest");
         }
     }
@@ -826,10 +833,10 @@ public class XPathToSQL2Converter {
         }
         return new Expression.Property(currentSelector, readPathSegment(), false);
     }
-    
+
     /**
      * Read open bracket (optional), and optional dot, and close bracket.
-     * 
+     *
      * @param readOpenBracket whether to read the open bracket (false if this
      *            was already read)
      * @throws ParseException if close bracket or the dot were not read
@@ -977,7 +984,7 @@ public class XPathToSQL2Converter {
                 // for example in "fn:lower-case"
                 // the '.' can be part of a name,
                 // for example in "@offloading.status"
-                if (type != CHAR_NAME && type != CHAR_VALUE 
+                if (type != CHAR_NAME && type != CHAR_VALUE
                         && chars[i] != '-'
                         && chars[i] != '.') {
                     break;
@@ -1152,7 +1159,7 @@ public class XPathToSQL2Converter {
         }
         return new ParseException("Query:\n" + query, index);
     }
-    
+
     private Statement convertToUnion(String query, Statement statement,
             int startParseIndex) throws ParseException {
         int start = query.indexOf("(", startParseIndex);
@@ -1186,7 +1193,7 @@ public class XPathToSQL2Converter {
             }
         }
         String or = partList.substring(lastOrIndex, parseIndex - 1);
-        parts.add(or);        
+        parts.add(or);
         String end = partList.substring(parseIndex);
         Statement result = null;
         ArrayList<Order> orderList = null;
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java
index 1243ccc..0dfd964 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java
@@ -36,15 +36,15 @@ public class SQL2ParserTest {
     private static final NodeTypeInfoProvider nodeTypes = new NodeStateNodeTypeInfoProvider(INITIAL_CONTENT);
 
     private static final SQL2Parser p = createTestSQL2Parser();
-    
+
     public static SQL2Parser createTestSQL2Parser() {
         return createTestSQL2Parser(NamePathMapper.DEFAULT, nodeTypes, new QueryEngineSettings());
     }
-    
+
     public static SQL2Parser createTestSQL2Parser(NamePathMapper mappings, NodeTypeInfoProvider nodeTypes2,
             QueryEngineSettings qeSettings) {
         QueryStatsData data = new QueryStatsData("", "");
-        return new SQL2Parser(mappings, nodeTypes2, new QueryEngineSettings(), 
+        return new SQL2Parser(mappings, nodeTypes2, new QueryEngineSettings(),
                 data.new QueryExecutionStats());
     }
 
@@ -53,8 +53,8 @@ public class SQL2ParserTest {
     public void testIgnoreSqlComment() throws ParseException {
         p.parse("select * from [nt:unstructured] /* sql comment */");
         p.parse("select [jcr:path], [jcr:score], * from [nt:base] as a /* xpath: //* */");
-        p.parse("/* begin query */ select [jcr:path] /* this is the path */, " + 
-                "[jcr:score] /* the score */, * /* everything*/ " + 
+        p.parse("/* begin query */ select [jcr:path] /* this is the path */, " +
+                "[jcr:score] /* the score */, * /* everything*/ " +
                 "from [nt:base] /* all node types */ as a /* an identifier */");
     }
 
@@ -68,12 +68,12 @@ public class SQL2ParserTest {
         p.parse(new XPathToSQL2Converter()
                 .convert("/jcr:root/test/*/nt:resource[@jcr:encoding]"));
         p.parse(new XPathToSQL2Converter()
-                .convert("/jcr:root/test/*/*/nt:resource[@jcr:encoding]"));        
+                .convert("/jcr:root/test/*/*/nt:resource[@jcr:encoding]"));
         String xpath = "/jcr:root/etc/commerce/products//*[@cq:commerceType = 'product' " +
                 "and ((@size = 'M' or */@size= 'M' or */*/@size = 'M' " +
                 "or */*/*/@size = 'M' or */*/*/*/@size = 'M' or */*/*/*/*/@size = 'M'))]";
         p.parse(new XPathToSQL2Converter()
-                .convert(xpath));        
+                .convert(xpath));
     }
 
     // see OAK-OAK-830: XPathToSQL2Converter fails to wrap or clauses
@@ -105,12 +105,24 @@ public class SQL2ParserTest {
                 .convert("//*[fn:coalesce(fn:coalesce(j:c/@a, b), fn:coalesce(c, c:d)) = 'a']"));
     }
 
+    @Test
+    public void testFirst() throws ParseException {
+        p.parse("SELECT * FROM [nt:base] WHERE FIRST([d:t])='a'");
+
+        p.parse("SELECT * FROM [nt:base] WHERE FIRST([jcr:mixinTypes])='a'");
+    }
+
     @Test(expected = ParseException.class)
     public void coalesceFailsWithNoParam() throws ParseException {
         p.parse("SELECT * FROM [nt:base] WHERE COALESCE()='a'");
     }
 
     @Test(expected = ParseException.class)
+    public void firstFailsWithNoParam() throws ParseException {
+        p.parse("SELECT * FROM [nt:base] WHERE FIRST()='a'");
+    }
+
+    @Test(expected = ParseException.class)
     public void coalesceFailsWithOneParam() throws ParseException {
         p.parse("SELECT * FROM [nt:base] WHERE COALESCE([a])='a'");
     }
diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt
index 0098931..900200e 100644
--- a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt
+++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt
@@ -26,6 +26,22 @@
 
 #
 
+xpath2sql /jcr:root/content//element(*, nt:base)[fn:path() >= $lastValue] order by fn:path()
+select [jcr:path], [jcr:score], *
+  from [nt:base] as a
+  where path() >= @lastValue
+  and isdescendantnode(a, '/content')
+  order by path()
+  /* xpath ... */
+
+xpath2sql /jcr:root/content//element(*, nt:base)[jcr:first(@vanityPath) >= $lastValue] order by jcr:first(@vanityPath), @jcr:path
+select [jcr:path], [jcr:score], *
+  from [nt:base] as a
+  where first([vanityPath]) >= @lastValue
+  and isdescendantnode(a, '/content')
+  order by first([vanityPath]), [jcr:path]
+  /* xpath ... */
+
 xpath2sql /jcr:root/content//element(*, nt:base)[@jcr:lastModified >= $lastModified] order by @jcr:lastModified, @jcr:path
 select [jcr:path], [jcr:score], *
   from [nt:base] as a
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java
index ca4df14..3bc4ab6 100644
--- a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java
@@ -69,12 +69,12 @@ public class QueryTest extends AbstractRepositoryTest {
     public QueryTest(NodeStoreFixture fixture) {
         super(fixture);
     }
-    
+
     @Test
     public void traversalOption() throws Exception {
         Session session = getAdminSession();
         QueryManager qm = session.getWorkspace().getQueryManager();
-        
+
         // for union queries:
         // both subqueries use an index
         assertTrue(isValidQuery(qm, Query.JCR_SQL2,
@@ -88,22 +88,22 @@ public class QueryTest extends AbstractRepositoryTest {
         // first one does, second one does not
         assertFalse(isValidQuery(qm, Query.JCR_SQL2,
                 "select * from [nt:base] where [x] = 2 or [jcr:uuid] = 1 option(traversal fail)"));
-        
+
         // queries that possibly use traversal (depending on the join order)
         assertTrue(isValidQuery(qm, "xpath",
                 "/jcr:root/content//*/jcr:content[@jcr:uuid='1'] option(traversal fail)"));
         assertTrue(isValidQuery(qm, "xpath",
                 "/jcr:root/content/*/jcr:content[@jcr:uuid='1'] option(traversal fail)"));
         assertTrue(isValidQuery(qm, Query.JCR_SQL2,
-                "select * from [nt:base] as [a] inner join [nt:base] as [b] on ischildnode(b, a) " + 
+                "select * from [nt:base] as [a] inner join [nt:base] as [b] on ischildnode(b, a) " +
                 "where [a].[jcr:uuid] = 1 option(traversal fail)"));
         assertTrue(isValidQuery(qm, Query.JCR_SQL2,
-                "select * from [nt:base] as [a] inner join [nt:base] as [b] on ischildnode(a, b) " + 
+                "select * from [nt:base] as [a] inner join [nt:base] as [b] on ischildnode(a, b) " +
                 "where [a].[jcr:uuid] = 1 option(traversal fail)"));
 
         // union with joins
         assertTrue(isValidQuery(qm, Query.JCR_SQL2,
-                "select * from [nt:base] as [a] inner join [nt:base] as [b] on ischildnode(a, b) " + 
+                "select * from [nt:base] as [a] inner join [nt:base] as [b] on ischildnode(a, b) " +
                 "where ischildnode([a], '/') or [a].[jcr:uuid] = 1 option(traversal fail)"));
 
         assertFalse(isValidQuery(qm, "xpath",
@@ -118,7 +118,7 @@ public class QueryTest extends AbstractRepositoryTest {
                 "select * from [nt:base] option(traversal ok)"));
         assertTrue(isValidQuery(qm, Query.JCR_SQL2,
                 "select * from [nt:base] option(traversal warn)"));
-        
+
         // the following is not really traversal, it is just listing child nodes:
         assertTrue(isValidQuery(qm, "xpath",
                 "/jcr:root/*[@test] option(traversal fail)"));
@@ -127,7 +127,7 @@ public class QueryTest extends AbstractRepositoryTest {
                 "/jcr:root/oak:index[@test] option(traversal fail)"));
 
     }
-    
+
     private static boolean isValidQuery(QueryManager qm, String language, String query) throws RepositoryException {
         try {
             qm.createQuery(query, language).execute();
@@ -137,7 +137,7 @@ public class QueryTest extends AbstractRepositoryTest {
             return false;
         }
     }
-    
+
     @Test
     public void firstSelector() throws Exception {
         Session session = getAdminSession();
@@ -167,80 +167,118 @@ public class QueryTest extends AbstractRepositoryTest {
         // b.setProperty("join", a.getProperty("jcr:uuid").getString(), PropertyType.STRING);
         session.save();
         assertEquals("/a",
-                getNodeList(session, 
-                        "select [a].* from [nt:unstructured] as [a] "+ 
-                                "inner join [nt:unstructured] as [b] " + 
+                getNodeList(session,
+                        "select [a].* from [nt:unstructured] as [a] "+
+                                "inner join [nt:unstructured] as [b] " +
                                 "on [a].[jcr:uuid] = [b].[join] where issamenode([a], '/a')",
                         Query.JCR_SQL2));
         assertEquals("/a",
-                getNodeList(session, 
-                        "select [a].* from [nt:unstructured] as [a] "+ 
-                                "inner join [nt:unstructured] as [b] " + 
+                getNodeList(session,
+                        "select [a].* from [nt:unstructured] as [a] "+
+                                "inner join [nt:unstructured] as [b] " +
                                 "on [b].[join] = [a].[jcr:uuid] where issamenode([a], '/a')",
                         Query.JCR_SQL2));
     }
-    
+
     @Test
     public void typeConversion() throws Exception {
         Session session = getAdminSession();
         Node root = session.getRootNode();
-        
+
         Node test = root.addNode("test");
         test.addNode("a", "oak:Unstructured").setProperty("time", "2001-01-01T00:00:00.000Z", PropertyType.DATE);
         test.addNode("b", "oak:Unstructured").setProperty("time", "2010-01-01T00:00:00.000Z", PropertyType.DATE);
         test.addNode("c", "oak:Unstructured").setProperty("time", "2020-01-01T00:00:00.000Z", PropertyType.DATE);
         session.save();
-        
+
         assertEquals("/test/c",
-                getNodeList(session, 
+                getNodeList(session,
                 "select [jcr:path] " +
-                "from [nt:base] " + 
+                "from [nt:base] " +
                 "where [time] > '2011-01-01T00:00:00.000z'", Query.JCR_SQL2));
 
     }
-    
+
+    @Test
+    public void first() throws Exception {
+        Session session = getAdminSession();
+        Node root = session.getRootNode();
+
+        Node test = root.addNode("test");
+        test.addNode("a", "oak:Unstructured").setProperty("test", new String[] {"a", "b"}, PropertyType.STRING);
+        test.addNode("b", "oak:Unstructured").setProperty("test", new String[] {"b", "a"}, PropertyType.STRING);
+        test.addNode("c", "oak:Unstructured").setProperty("test", "a");
+        test.addNode("d", "oak:Unstructured").setProperty("test", "b");
+        session.save();
+
+        assertEquals("/test/a, /test/c",
+                getNodeList(session,
+                "select [jcr:path] " +
+                "from [nt:base] " +
+                "where first([test]) = 'a'", Query.JCR_SQL2));
+    }
+
+    @Test
+    public void path() throws Exception {
+        Session session = getAdminSession();
+        Node root = session.getRootNode();
+
+        Node test = root.addNode("test");
+        test.addNode("a", "oak:Unstructured");
+        test.addNode("b", "oak:Unstructured");
+        test.addNode("c", "oak:Unstructured");
+        session.save();
+
+        assertEquals("/test/c, /test/b",
+                getNodeList(session,
+                "select [jcr:path] " +
+                "from [nt:base] " +
+                "where path() >= '/test/b' " +
+                "order by path() desc", Query.JCR_SQL2));
+    }
+
     @Test
     public void twoSelectors() throws Exception {
         Session session = getAdminSession();
         Node root = session.getRootNode();
-        
+
         Node test = root.addNode("test");
         test.addNode("testNode", "oak:Unstructured");
         session.save();
-        
+
         assertEquals("/test/testNode",
-                getNodeList(session, 
+                getNodeList(session,
                 "select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.* " +
                 "from [nt:base] as a " +
-                "inner join [nt:base] as b " + 
+                "inner join [nt:base] as b " +
                 "on ischildnode(b, a) " +
                 "where issamenode(a, '/test')", Query.JCR_SQL2));
 
         assertEquals("/test/testNode",
-                getNodeList(session, 
+                getNodeList(session,
                 "select b.[jcr:path] as [jcr:path], b.[jcr:score] as [jcr:score], b.* " +
                 "from [nt:base] as b " +
-                "inner join [nt:base] as a " + 
+                "inner join [nt:base] as a " +
                 "on ischildnode(b, a) " +
                 "where issamenode(b, '/test/testNode')", Query.JCR_SQL2));
 
         assertEquals("/test",
-                getNodeList(session, 
+                getNodeList(session,
                 "select a.[jcr:path] as [jcr:path], a.[jcr:score] as [jcr:score], a.* " +
                 "from [nt:base] as a " +
-                "inner join [nt:base] as b " + 
+                "inner join [nt:base] as b " +
                 "on ischildnode(b, a) " +
                 "where issamenode(a, '/test')", Query.JCR_SQL2));
-        
+
         assertEquals("/test",
-                getNodeList(session, 
+                getNodeList(session,
                 "select a.[jcr:path] as [jcr:path], a.[jcr:score] as [jcr:score], a.* " +
                 "from [nt:base] as b " +
-                "inner join [nt:base] as a " + 
+                "inner join [nt:base] as a " +
                 "on ischildnode(b, a) " +
                 "where issamenode(b, '/test/testNode')", Query.JCR_SQL2));
     }
-    
+
     private static String getNodeList(Session session, String query, String language) throws RepositoryException {
         QueryResult r = session.getWorkspace().getQueryManager()
                 .createQuery(query, language).execute();
@@ -250,16 +288,16 @@ public class QueryTest extends AbstractRepositoryTest {
             if (buff.length() > 0) {
                 buff.append(", ");
             }
-            buff.append(it.nextNode().getPath());        
+            buff.append(it.nextNode().getPath());
         }
         return buff.toString();
     }
-    
+
     @Test
     public void noDeclaringNodeTypesIndex() throws Exception {
         Session session = getAdminSession();
         Node root = session.getRootNode();
-        
+
         // set declaringNodeTypes to an empty array
         Node nodeTypeIndex = root.getNode("oak:index").getNode("nodetype");
         nodeTypeIndex.setProperty("declaringNodeTypes", new String[] {
@@ -279,7 +317,7 @@ public class QueryTest extends AbstractRepositoryTest {
         assertTrue(it.hasNext());
         assertEquals("/test/testNode", it.nextNode().getPath());
     }
-    
+
     @Test
     public void propertyIndexWithDeclaringNodeTypeAndRelativQuery() throws RepositoryException {
         Session session = getAdminSession();
@@ -290,26 +328,26 @@ public class QueryTest extends AbstractRepositoryTest {
         r = session.getWorkspace().getQueryManager()
                 .createQuery("explain " + query, "xpath").execute();
         rit = r.getRows();
-        assertEquals("[rep:Authorizable] as [a] /* property principalName = admin " + 
-                "where [a].[rep:principalName] = 'admin' */", 
+        assertEquals("[rep:Authorizable] as [a] /* property principalName = admin " +
+                "where [a].[rep:principalName] = 'admin' */",
                 rit.nextRow().getValue("plan").getString());
-        
+
         query = "//element(*, rep:Authorizable)[admin/@rep:principalName = 'admin']";
         r = session.getWorkspace().getQueryManager()
                 .createQuery("explain " + query, "xpath").execute();
         rit = r.getRows();
-        assertEquals("[rep:Authorizable] as [a] /* nodeType " + 
-                "Filter(query=explain select [jcr:path], [jcr:score], * " + 
-                "from [rep:Authorizable] as a " + 
-                "where [admin/rep:principalName] = 'admin' " + 
-                "/* xpath: //element(*, rep:Authorizable)[" + 
-                "admin/@rep:principalName = 'admin'] */, path=*, " + 
-                "property=[admin/rep:principalName=[admin]]) " + 
-                "where [a].[admin/rep:principalName] = 'admin' */", 
+        assertEquals("[rep:Authorizable] as [a] /* nodeType " +
+                "Filter(query=explain select [jcr:path], [jcr:score], * " +
+                "from [rep:Authorizable] as a " +
+                "where [admin/rep:principalName] = 'admin' " +
+                "/* xpath: //element(*, rep:Authorizable)[" +
+                "admin/@rep:principalName = 'admin'] */, path=*, " +
+                "property=[admin/rep:principalName=[admin]]) " +
+                "where [a].[admin/rep:principalName] = 'admin' */",
                 rit.nextRow().getValue("plan").getString());
-        
+
     }
-    
+
     @Test
     public void date() throws Exception {
         Session session = getAdminSession();
@@ -318,7 +356,7 @@ public class QueryTest extends AbstractRepositoryTest {
         Node t2 = session.getRootNode().addNode("t2");
         t2.setProperty("x", "2007-06-22T01:02:03.000Z", PropertyType.DATE);
         session.save();
-        
+
         String query = "//*[x='a' or x='b']";
         QueryResult r = session.getWorkspace().
                 getQueryManager().createQuery(
@@ -326,18 +364,18 @@ public class QueryTest extends AbstractRepositoryTest {
         NodeIterator it = r.getNodes();
         assertFalse(it.hasNext());
     }
-    
+
     @Test
     public void unicode() throws Exception {
         Session session = getAdminSession();
         Node content = session.getRootNode().addNode("test");
         String[][] list = {
-                {"three", "\u00e4\u00f6\u00fc"}, 
-                {"two", "123456789"}, 
-                {"one", "\u3360\u3361\u3362\u3363\u3364\u3365\u3366\u3367\u3368\u3369"}, 
+                {"three", "\u00e4\u00f6\u00fc"},
+                {"two", "123456789"},
+                {"one", "\u3360\u3361\u3362\u3363\u3364\u3365\u3366\u3367\u3368\u3369"},
         };
         for (String[] pair : list) {
-            content.addNode(pair[0]).setProperty("prop", 
+            content.addNode(pair[0]).setProperty("prop",
                     "propValue testSearch " + pair[1] + " data");
         }
         session.save();
@@ -351,9 +389,9 @@ public class QueryTest extends AbstractRepositoryTest {
             String path = it.nextNode().getPath();
             assertEquals("/test/" + pair[0], path);
             assertFalse(it.hasNext());
-        }        
+        }
     }
-    
+
     @Test
     @Ignore("OAK-1215")
     public void anyChildNodeProperty() throws Exception {
@@ -370,7 +408,7 @@ public class QueryTest extends AbstractRepositoryTest {
         String path = it.nextNode().getPath();
         assertEquals("/test/one", path);
         assertFalse(it.hasNext());
-        
+
         query = "//*[*/*/@prop = 'hello']";
         r = session.getWorkspace().getQueryManager().createQuery(
                 query, "xpath").execute();
@@ -443,16 +481,16 @@ public class QueryTest extends AbstractRepositoryTest {
         QueryManager qm = session.getWorkspace().getQueryManager();
         Query q;
         q = qm.createQuery(
-                "SELECT * FROM [nt:base] AS s WHERE ISDESCENDANTNODE(s,[/hello])", 
+                "SELECT * FROM [nt:base] AS s WHERE ISDESCENDANTNODE(s,[/hello])",
                 Query.JCR_SQL2);
         assertEquals("/hello/world", getPaths(q));
         q = qm.createQuery(
-                "SELECT * FROM [nt:base] AS s WHERE ISDESCENDANTNODE(s,\"/hello\")", 
+                "SELECT * FROM [nt:base] AS s WHERE ISDESCENDANTNODE(s,\"/hello\")",
                 Query.JCR_SQL2);
         assertEquals("/hello/world", getPaths(q));
         try {
             q = qm.createQuery(
-                    "SELECT * FROM [nt:base] AS s WHERE ISDESCENDANTNODE(s,[\"/hello\"])", 
+                    "SELECT * FROM [nt:base] AS s WHERE ISDESCENDANTNODE(s,[\"/hello\"])",
                     Query.JCR_SQL2);
             getPaths(q);
             fail();
@@ -472,10 +510,10 @@ public class QueryTest extends AbstractRepositoryTest {
         session.save();
         QueryManager qm = session.getWorkspace().getQueryManager();
         Query q;
-        
-        q = qm.createQuery("select a.[jcr:path] from [nt:base] as a " + 
+
+        q = qm.createQuery("select a.[jcr:path] from [nt:base] as a " +
                     "inner join [nt:base] as b " +
-                    "on ischildnode(a, b) " + 
+                    "on ischildnode(a, b) " +
                     "where a.x = 1 or a.x = 2 or b.x = 3 or b.x = 4", Query.JCR_SQL2);
         assertEquals("/hello", getPaths(q));
 
@@ -492,18 +530,18 @@ public class QueryTest extends AbstractRepositoryTest {
         session.save();
         QueryManager qm = session.getWorkspace().getQueryManager();
         Query q;
-        
+
         q = qm.createQuery("/jcr:root/hel_x006c_o/*", Query.XPATH);
         assertEquals("/hello/world", getPaths(q));
-        
+
         q = qm.createQuery("//hel_x006c_o", Query.XPATH);
         assertEquals("/hello", getPaths(q));
-        
+
         q = qm.createQuery("//element(hel_x006c_o, nt:base)", Query.XPATH);
         assertEquals("/hello", getPaths(q));
 
     }
-    
+
     private static String getPaths(Query q) throws RepositoryException {
         QueryResult r = q.execute();
         RowIterator it = r.getRows();
@@ -631,7 +669,7 @@ public class QueryTest extends AbstractRepositoryTest {
             assertFalse(it.hasNext());
         }
     }
-    
+
     @Test
     public void limit() throws RepositoryException {
         Session session = getAdminSession();
@@ -661,7 +699,7 @@ public class QueryTest extends AbstractRepositoryTest {
                 assertEquals(l, r.getRows().getSize());
                 assertEquals(l, r.getNodes().getSize());
                 Row row;
-                
+
                 for (int x = offset + 1, i = 0; i < limit && x < 4; i++, x++) {
                     assertTrue(it.hasNext());
                     row = it.nextRow();
@@ -693,17 +731,17 @@ public class QueryTest extends AbstractRepositoryTest {
         assertEquals(new HashSet<String>(Arrays.asList("/folder1", "/folder2", "/folder2/folder3")),
                 paths);
     }
-    
+
     @Test
     public void noLiterals() throws RepositoryException {
         Session session = getAdminSession();
         ValueFactory vf = session.getValueFactory();
         QueryManager qm = session.getWorkspace().getQueryManager();
-        
+
         // insecure
         try {
             Query q = qm.createQuery(
-                    "select text from [nt:base] where password = 'x'", 
+                    "select text from [nt:base] where password = 'x'",
                     Query.JCR_SQL2 + "-noLiterals");
             q.execute();
             fail();
@@ -714,7 +752,7 @@ public class QueryTest extends AbstractRepositoryTest {
 
         // secure
         Query q = qm.createQuery(
-                "select text from [nt:base] where password = $p", 
+                "select text from [nt:base] where password = $p",
                 Query.JCR_SQL2 + "-noLiterals");
         q.bindValue("p", vf.createValue("x"));
         q.execute();
@@ -814,7 +852,7 @@ public class QueryTest extends AbstractRepositoryTest {
             }
         }
     }
-    
+
     @Test
     public void testOak1128() throws RepositoryException {
         Session session = createAdminSession();
@@ -881,7 +919,7 @@ public class QueryTest extends AbstractRepositoryTest {
         assertFalse(ni.hasNext());
         session.logout();
     }
-    
+
     @Test
     public void approxCount() throws Exception {
         Session session = createAdminSession();
@@ -906,7 +944,7 @@ public class QueryTest extends AbstractRepositoryTest {
         }
         double c = getCost(session, "//*[@x=1]");
         assertTrue("cost: " + c, c > 0 && c < 100000);
-        
+
         // *without* the counter index, the estimated cost to traverse is high
         session.getNode("/oak:index/counter").remove();
         session.save();
@@ -921,8 +959,8 @@ public class QueryTest extends AbstractRepositoryTest {
         Session session = createAdminSession();
         String xpath = "/jcr:root//element(*,rep:User)[xyz/@jcr:primaryType]";
         assertPlan(getPlan(session, xpath), "[rep:User] as [a] /* nodeType");
-        
-        session.getNode("/oak:index/nodetype").setProperty("declaringNodeTypes", 
+
+        session.getNode("/oak:index/nodetype").setProperty("declaringNodeTypes",
                 new String[]{"oak:Unstructured"}, PropertyType.NAME);
         session.save();
 
@@ -945,7 +983,7 @@ public class QueryTest extends AbstractRepositoryTest {
         } catch (InvalidQueryException e) {
             // expected
         }
-        
+
         session.logout();
     }
 
@@ -976,7 +1014,7 @@ public class QueryTest extends AbstractRepositoryTest {
     private static void assertPlan(String plan, String planPrefix) {
         assertTrue("Unexpected plan: " + plan, plan.startsWith(planPrefix));
     }
-    
+
     private static String getPlan(Session session, String xpath) throws RepositoryException {
         QueryManager qm = session.getWorkspace().getQueryManager();
         QueryResult qr = qm.createQuery("explain " + xpath, "xpath").execute();
@@ -984,7 +1022,7 @@ public class QueryTest extends AbstractRepositoryTest {
         String plan = r.getValue("plan").getString();
         return plan;
     }
-    
+
     private static double getCost(Session session, String xpath) throws RepositoryException {
         QueryManager qm = session.getWorkspace().getQueryManager();
         QueryResult qr = qm.createQuery("explain measure " + xpath, "xpath").execute();
@@ -995,7 +1033,7 @@ public class QueryTest extends AbstractRepositoryTest {
         double c = Double.parseDouble(json.getProperties().get("a"));
         return c;
     }
-    
+
     private static JsonObject parseJson(String json) {
         JsopTokenizer t = new JsopTokenizer(json);
         t.read('{');
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryConstants.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryConstants.java
index 5e09c31..5385c54 100644
--- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryConstants.java
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryConstants.java
@@ -34,6 +34,12 @@ public abstract class QueryConstants {
     public static final String RESTRICTION_NAME = ":name";
 
     /**
+     * Name of the property restriction used to express query performed
+     * via PATH function
+     */
+    public static final String RESTRICTION_PATH = ":path";
+
+    /**
      * The prefix for restrictions for function-based indexes, for example
      * upper(propertyName). Syntax: "function*expression". In order to support
      * all kinds of expressions in the future (including nested expressions and
diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java
index ef85afb..9edb703 100644
--- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java
+++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java
@@ -18,7 +18,7 @@
 /**
  * This package contains oak query index related classes.
  */
-@Version("1.4.0")
+@Version("1.5.0")
 package org.apache.jackrabbit.oak.spi.query;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java b/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java
index 8e24f4a..793c393 100644
--- a/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java
+++ b/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java
@@ -92,7 +92,7 @@ public class FunctionIndexProcessor {
         }
 
         PropertyState ret = stack.pop();
-        return ret==EMPTY_PROPERTY_STATE ? null : ret;
+        return ret == EMPTY_PROPERTY_STATE ? null : ret;
     }
 
     /**
@@ -141,6 +141,12 @@ public class FunctionIndexProcessor {
             } else if ("length".equals(functionName)) {
                 x = ps.size(i);
                 type = Type.LONG;
+            } else if ("first".equals(functionName)) {
+                if (i > 0) {
+                    break;
+                }
+                x = ps.getValue(Type.STRING, 0);
+                type = Type.STRING;
             } else {
                 LOG.debug("Unknown function {}", functionName);
                 return null;
@@ -175,6 +181,9 @@ public class FunctionIndexProcessor {
         } else if (":name".equals(propertyName)) {
             ps = PropertyStates.createProperty("value",
                     PathUtils.getName(path), Type.STRING);
+        } else if (":path".equals(propertyName)) {
+            ps = PropertyStates.createProperty("value",
+                   path, Type.STRING);
         } else {
             ps = state.getProperty(propertyName);
         }
@@ -211,6 +220,9 @@ public class FunctionIndexProcessor {
         if (match("fn:name()") || match("name()")) {
             return "@:name";
         }
+        if (match("fn:path()") || match("path()")) {
+            return "@:path";
+        }
         if (match("fn:upper-case(") || match("upper(")) {
             return "upper*" + parse() + read(")");
         }
@@ -220,6 +232,9 @@ public class FunctionIndexProcessor {
         if (match("fn:coalesce(") || match("coalesce(")) {
             return "coalesce*" + parse() + readCommaAndWhitespace() + parse() + read(")");
         }
+        if (match("jcr:first(") || match("first(")) {
+            return "first*" + parse() + read(")");
+        }
         if (match("fn:string-length(") || match("length(")) {
             return "length*" + parse() + read(")");
         }
@@ -233,7 +248,11 @@ public class FunctionIndexProcessor {
             }
             prop = prop.substring(0, prop.lastIndexOf(']'));
             remaining = remaining.substring(prop.length() + 1);
-            return property(prop.replaceAll("]]", "]"));
+            String x = prop.replaceAll("]]", "]");
+            if ("jcr:path".equals(x)) {
+                return "@:path";
+            }
+            return property(x);
         } else {
             String prop = remaining;
             int paren = remaining.indexOf(')');
@@ -246,7 +265,11 @@ public class FunctionIndexProcessor {
                 prop = remaining.substring(0, end);
             }
             remaining = remaining.substring(prop.length());
-            return property(prop.replaceAll("@", ""));
+            String x = prop.replaceAll("@", "");
+            if ("jcr:path".equals(x)) {
+                return "@:path";
+            }
+            return property(x);
         }
     }
 
diff --git a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessorTest.java b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessorTest.java
index 0876b5e..e01f89f 100644
--- a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessorTest.java
+++ b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessorTest.java
@@ -61,6 +61,49 @@ public class FunctionIndexProcessorTest {
                 FunctionIndexProcessor.tryCalculateValue("x",
                 EMPTY_NODE.builder().setProperty("data", "Hello World").getNodeState(),
                 new String[]{"function", "lower", "@data"}).toString());
+        // coalesce
+        assertEquals("value = Hello",
+                FunctionIndexProcessor.tryCalculateValue("x",
+                EMPTY_NODE.builder().
+                    setProperty("data1", "Hello").
+                    setProperty("data2", "World").getNodeState(),
+                new String[]{"function", "coalesce", "@data1", "@data2"}).toString());
+        assertEquals("value = World",
+                FunctionIndexProcessor.tryCalculateValue("x",
+                EMPTY_NODE.builder().setProperty("data2", "World").getNodeState(),
+                new String[]{"function", "coalesce", "@data1", "@data2"}).toString());
+        assertEquals("value = Hello",
+                FunctionIndexProcessor.tryCalculateValue("x",
+                EMPTY_NODE.builder().setProperty("data1", "Hello").getNodeState(),
+                new String[]{"function", "coalesce", "@data1", "@data2"}).toString());
+        assertEquals("null",
+                "" + FunctionIndexProcessor.tryCalculateValue("x",
+                EMPTY_NODE.builder().setProperty("data3", "Hello").getNodeState(),
+                new String[]{"function", "coalesce", "@data1", "@data2"}));
+        // first
+        assertEquals("value = Hello",
+                FunctionIndexProcessor.tryCalculateValue("x",
+                EMPTY_NODE.builder().setProperty("array", Arrays.asList("Hello", "World"), Type.STRINGS).getNodeState(),
+                new String[]{"function", "first", "@array"}).toString());
+        assertEquals("value = Hello",
+                FunctionIndexProcessor.tryCalculateValue("x",
+                EMPTY_NODE.builder().setProperty("array", Arrays.asList("Hello"), Type.STRINGS).getNodeState(),
+                new String[]{"function", "first", "@array"}).toString());
+        // name
+        assertEquals("value = abc:content",
+                FunctionIndexProcessor.tryCalculateValue("abc:content",
+                EMPTY_NODE.builder().getNodeState(),
+                new String[]{"function", "@:name"}).toString());
+        // localname
+        assertEquals("value = content",
+                FunctionIndexProcessor.tryCalculateValue("abc:content",
+                EMPTY_NODE.builder().getNodeState(),
+                new String[]{"function", "@:localname"}).toString());
+        // path
+        assertEquals("value = /content",
+                FunctionIndexProcessor.tryCalculateValue("/content",
+                EMPTY_NODE.builder().getNodeState(),
+                new String[]{"function", "@:path"}).toString());
     }
 
     @Test
@@ -84,6 +127,15 @@ public class FunctionIndexProcessorTest {
                 "fn:string-length(fn:name())",
                 "function*length*@:name");
         checkConvert(
+                "fn:path()",
+                "function*@:path");
+        checkConvert(
+                "fn:string-length(fn:path())",
+                "function*length*@:path");
+        checkConvert(
+                "fn:string-length(@jcr:path)",
+                "function*length*@:path");
+        checkConvert(
                 "fn:lower-case(fn:upper-case(test/@data))",
                 "function*lower*upper*@test/data");
         checkConvert("fn:coalesce(jcr:content/@foo2, jcr:content/@foo)",
@@ -94,6 +146,8 @@ public class FunctionIndexProcessorTest {
                 "function*coalesce*@jcr:content/foo2*coalesce*@jcr:content/foo*lower*@:name");
         checkConvert("fn:coalesce(fn:coalesce(jcr:content/@foo2,jcr:content/@foo), fn:coalesce(@a:b, @c:d))",
                 "function*coalesce*coalesce*@jcr:content/foo2*@jcr:content/foo*coalesce*@a:b*@c:d");
+        checkConvert("jcr:first(jcr:content/@foo2)",
+                "function*first*@jcr:content/foo2");
     }
 
     @Test
@@ -117,6 +171,15 @@ public class FunctionIndexProcessorTest {
                 "length(name())",
                 "function*length*@:name");
         checkConvert(
+                "path()",
+                "function*@:path");
+        checkConvert(
+                "length(path())",
+                "function*length*@:path");
+        checkConvert(
+                "length([jcr:path])",
+                "function*length*@:path");
+        checkConvert(
                 "lower(upper([test/data]))",
                 "function*lower*upper*@test/data");
         // the ']' character is escaped as ']]'
@@ -131,6 +194,8 @@ public class FunctionIndexProcessorTest {
                 "function*coalesce*@jcr:content/foo2*coalesce*@jcr:content/foo*lower*@:name");
         checkConvert("coalesce(coalesce([jcr:content/foo2],[jcr:content/foo]), coalesce([a:b], [c:d]))",
                 "function*coalesce*coalesce*@jcr:content/foo2*@jcr:content/foo*coalesce*@a:b*@c:d");
+        checkConvert("first([jcr:content/foo2])",
+                "function*first*@jcr:content/foo2");
     }
 
     private static void checkConvert(String function, String expectedPolishNotation) {