[CALCITE-1986] Add RelBuilder.match and methods for building patterns (Dian Fu)
Add methods for building patterns, and documentation. (Julian Hyde) Close apache/calcite#538 Project: http://git-wip-us.apache.org/repos/asf/calcite/repo Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/3e97cff7 Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/3e97cff7 Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/3e97cff7 Branch: refs/heads/master Commit: 3e97cff7253691bbd7df690721981de4c2d9f88b Parents: 2773c48 Author: Dian Fu <[email protected]> Authored: Thu Sep 14 12:47:41 2017 +0800 Committer: Julian Hyde <[email protected]> Committed: Mon Oct 2 11:13:42 2017 -0700 ---------------------------------------------------------------------- .../apache/calcite/rel/core/RelFactories.java | 16 +- .../calcite/rel/logical/LogicalMatch.java | 3 +- .../java/org/apache/calcite/rex/RexBuilder.java | 13 ++ .../calcite/sql2rel/SqlToRelConverter.java | 2 +- .../org/apache/calcite/tools/RelBuilder.java | 154 +++++++++++++++++++ .../org/apache/calcite/test/RelBuilderTest.java | 90 +++++++++++ site/_docs/algebra.md | 24 ++- 7 files changed, 288 insertions(+), 14 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite/blob/3e97cff7/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java index 477bbd4..b4ebbca 100644 --- a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java +++ b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java @@ -49,7 +49,7 @@ import com.google.common.collect.ImmutableList; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TreeSet; +import java.util.SortedSet; /** * Contains factory interface and default implementation for creating various @@ -396,11 +396,12 @@ public class RelFactories { */ public interface MatchFactory { /** Creates a {@link Match}. */ - RelNode createMatchRecognize(RelNode input, RexNode pattern, + RelNode createMatch(RelNode input, RexNode pattern, RelDataType rowType, boolean strictStart, boolean strictEnd, Map<String, RexNode> patternDefinitions, Map<String, RexNode> measures, - RexNode after, Map<String, TreeSet<String>> subsets, boolean allRows, - List<RexNode> partitionKeys, RelCollation orderKeys, RexNode interval); + RexNode after, Map<String, ? extends SortedSet<String>> subsets, + boolean allRows, List<RexNode> partitionKeys, RelCollation orderKeys, + RexNode interval); } /** @@ -408,11 +409,12 @@ public class RelFactories { * that returns a {@link LogicalMatch}. */ private static class MatchFactoryImpl implements MatchFactory { - public RelNode createMatchRecognize(RelNode input, RexNode pattern, + public RelNode createMatch(RelNode input, RexNode pattern, RelDataType rowType, boolean strictStart, boolean strictEnd, Map<String, RexNode> patternDefinitions, Map<String, RexNode> measures, - RexNode after, Map<String, TreeSet<String>> subsets, boolean allRows, - List<RexNode> partitionKeys, RelCollation orderKeys, RexNode interval) { + RexNode after, Map<String, ? extends SortedSet<String>> subsets, + boolean allRows, List<RexNode> partitionKeys, RelCollation orderKeys, + RexNode interval) { return LogicalMatch.create(input, rowType, pattern, strictStart, strictEnd, patternDefinitions, measures, after, subsets, allRows, partitionKeys, orderKeys, interval); http://git-wip-us.apache.org/repos/asf/calcite/blob/3e97cff7/core/src/main/java/org/apache/calcite/rel/logical/LogicalMatch.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalMatch.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalMatch.java index f0e3729..1a840f5 100644 --- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalMatch.java +++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalMatch.java @@ -29,7 +29,6 @@ import org.apache.calcite.rex.RexNode; import java.util.List; import java.util.Map; import java.util.SortedSet; -import java.util.TreeSet; /** * Sub-class of {@link Match} @@ -74,7 +73,7 @@ public class LogicalMatch extends Match { public static LogicalMatch create(RelNode input, RelDataType rowType, RexNode pattern, boolean strictStart, boolean strictEnd, Map<String, RexNode> patternDefinitions, Map<String, RexNode> measures, - RexNode after, Map<String, TreeSet<String>> subsets, boolean allRows, + RexNode after, Map<String, ? extends SortedSet<String>> subsets, boolean allRows, List<RexNode> partitionKeys, RelCollation orderKeys, RexNode interval) { final RelOptCluster cluster = input.getCluster(); final RelTraitSet traitSet = cluster.traitSetOf(Convention.NONE); http://git-wip-us.apache.org/repos/asf/calcite/blob/3e97cff7/core/src/main/java/org/apache/calcite/rex/RexBuilder.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java index 2144ab8..bd6579d 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java +++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java @@ -809,6 +809,19 @@ public class RexBuilder { } /** + * Creates a reference to a given field of the pattern. + * + * @param alpha the pattern name + * @param type Type of field + * @param i Ordinal of field + * @return Reference to field of pattern + */ + public RexPatternFieldRef makePatternFieldRef(String alpha, RelDataType type, int i) { + type = SqlTypeUtil.addCharsetAndCollation(type, typeFactory); + return new RexPatternFieldRef(alpha, i, type); + } + + /** * Creates a literal representing a flag. * * @param flag Flag value http://git-wip-us.apache.org/repos/asf/calcite/blob/3e97cff7/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java index 235dfb4..9967bd5 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java @@ -2239,7 +2239,7 @@ public class SqlToRelConverter { final RelFactories.MatchFactory factory = RelFactories.DEFAULT_MATCH_FACTORY; final RelNode rel = - factory.createMatchRecognize(input, patternNode, + factory.createMatch(input, patternNode, rowType, matchRecognize.getStrictStart().booleanValue(), matchRecognize.getStrictEnd().booleanValue(), definitionNodes.build(), measureNodes.build(), after, http://git-wip-us.apache.org/repos/asf/calcite/blob/3e97cff7/core/src/main/java/org/apache/calcite/tools/RelBuilder.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java index 0a726c7..e55015a 100644 --- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java +++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java @@ -75,6 +75,7 @@ import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -89,6 +90,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.SortedSet; @@ -132,6 +134,7 @@ public class RelBuilder { private final RelFactories.CorrelateFactory correlateFactory; private final RelFactories.ValuesFactory valuesFactory; private final RelFactories.TableScanFactory scanFactory; + private final RelFactories.MatchFactory matchFactory; private final Deque<Frame> stack = new ArrayDeque<>(); private final boolean simplify; private final RexSimplify simplifier; @@ -175,6 +178,9 @@ public class RelBuilder { this.scanFactory = Util.first(context.unwrap(RelFactories.TableScanFactory.class), RelFactories.DEFAULT_TABLE_SCAN_FACTORY); + this.matchFactory = + Util.first(context.unwrap(RelFactories.MatchFactory.class), + RelFactories.DEFAULT_MATCH_FACTORY); final RexExecutor executor = Util.first(context.unwrap(RexExecutor.class), Util.first(cluster.getPlanner().getExecutor(), RexUtil.EXECUTOR)); @@ -773,6 +779,89 @@ public class RelBuilder { return aggregateCall(SqlStdOperatorTable.MAX, false, null, alias, operand); } + // Methods for patterns + + /** + * Creates a reference to a given field of the pattern. + * + * @param alpha the pattern name + * @param type Type of field + * @param i Ordinal of field + * @return Reference to field of pattern + */ + public RexNode patternField(String alpha, RelDataType type, int i) { + return getRexBuilder().makePatternFieldRef(alpha, type, i); + } + + /** Creates a call that concatenates patterns; + * for use in {@link #match}. */ + public RexNode patternConcat(Iterable<? extends RexNode> nodes) { + final ImmutableList<RexNode> list = ImmutableList.copyOf(nodes); + if (list.size() > 2) { + // Convert into binary calls + return patternConcat(patternConcat(Util.skipLast(list)), Util.last(list)); + } + final RelDataType t = getTypeFactory().createSqlType(SqlTypeName.NULL); + return getRexBuilder().makeCall(t, SqlStdOperatorTable.PATTERN_CONCAT, + list); + } + + /** Creates a call that concatenates patterns; + * for use in {@link #match}. */ + public RexNode patternConcat(RexNode... nodes) { + return patternConcat(ImmutableList.copyOf(nodes)); + } + + /** Creates a call that creates alternate patterns; + * for use in {@link #match}. */ + public RexNode patternAlter(Iterable<? extends RexNode> nodes) { + final RelDataType t = getTypeFactory().createSqlType(SqlTypeName.NULL); + return getRexBuilder().makeCall(t, SqlStdOperatorTable.PATTERN_ALTER, + ImmutableList.copyOf(nodes)); + } + + /** Creates a call that creates alternate patterns; + * for use in {@link #match}. */ + public RexNode patternAlter(RexNode... nodes) { + return patternAlter(ImmutableList.copyOf(nodes)); + } + + /** Creates a call that creates quantify patterns; + * for use in {@link #match}. */ + public RexNode patternQuantify(Iterable<? extends RexNode> nodes) { + final RelDataType t = getTypeFactory().createSqlType(SqlTypeName.NULL); + return getRexBuilder().makeCall(t, SqlStdOperatorTable.PATTERN_QUANTIFIER, + ImmutableList.copyOf(nodes)); + } + + /** Creates a call that creates quantify patterns; + * for use in {@link #match}. */ + public RexNode patternQuantify(RexNode... nodes) { + return patternQuantify(ImmutableList.copyOf(nodes)); + } + + /** Creates a call that creates permute patterns; + * for use in {@link #match}. */ + public RexNode patternPermute(Iterable<? extends RexNode> nodes) { + final RelDataType t = getTypeFactory().createSqlType(SqlTypeName.NULL); + return getRexBuilder().makeCall(t, SqlStdOperatorTable.PATTERN_PERMUTE, + ImmutableList.copyOf(nodes)); + } + + /** Creates a call that creates permute patterns; + * for use in {@link #match}. */ + public RexNode patternPermute(RexNode... nodes) { + return patternPermute(ImmutableList.copyOf(nodes)); + } + + /** Creates a call that creates an exclude pattern; + * for use in {@link #match}. */ + public RexNode patternExclude(RexNode node) { + final RelDataType t = getTypeFactory().createSqlType(SqlTypeName.NULL); + return getRexBuilder().makeCall(t, SqlStdOperatorTable.PATTERN_EXCLUDE, + ImmutableList.of(node)); + } + // Methods that create relational expressions /** Creates a {@link org.apache.calcite.rel.core.TableScan} of the table @@ -1711,6 +1800,71 @@ public class RelBuilder { })); } + /** Creates a {@link org.apache.calcite.rel.core.Match}. */ + public RelBuilder match(RexNode pattern, boolean strictStart, + boolean strictEnd, Map<String, RexNode> patternDefinitions, + Iterable<? extends RexNode> measureList, RexNode after, + Map<String, ? extends SortedSet<String>> subsets, boolean allRows, + Iterable<? extends RexNode> partitionKeys, + Iterable<? extends RexNode> orderKeys, RexNode interval) { + final List<RelFieldCollation> fieldCollations = new ArrayList<>(); + for (RexNode orderKey : orderKeys) { + final RelFieldCollation.Direction direction; + switch (orderKey.getKind()) { + case DESCENDING: + direction = RelFieldCollation.Direction.DESCENDING; + orderKey = ((RexCall) orderKey).getOperands().get(0); + break; + case NULLS_FIRST: + case NULLS_LAST: + throw new AssertionError(); + default: + direction = RelFieldCollation.Direction.ASCENDING; + break; + } + final RelFieldCollation.NullDirection nullDirection = + direction.defaultNullDirection(); + final RexInputRef ref = (RexInputRef) orderKey; + fieldCollations.add( + new RelFieldCollation(ref.getIndex(), direction, nullDirection)); + } + + final RelDataTypeFactory.Builder typeBuilder = cluster.getTypeFactory().builder(); + for (RexNode partitionKey : partitionKeys) { + typeBuilder.add(partitionKey.toString(), partitionKey.getType()); + } + if (allRows) { + for (RexNode orderKey : orderKeys) { + if (!typeBuilder.nameExists(orderKey.toString())) { + typeBuilder.add(orderKey.toString(), orderKey.getType()); + } + } + + final RelDataType inputRowType = peek().getRowType(); + for (RelDataTypeField fs : inputRowType.getFieldList()) { + if (!typeBuilder.nameExists(fs.getName())) { + typeBuilder.add(fs); + } + } + } + + final ImmutableMap.Builder<String, RexNode> measures = ImmutableMap.builder(); + for (RexNode measure : measureList) { + List<RexNode> operands = ((RexCall) measure).getOperands(); + String alias = operands.get(1).toString(); + typeBuilder.add(alias, operands.get(0).getType()); + measures.put(alias, operands.get(0)); + } + + final RelNode match = matchFactory.createMatch(peek(), pattern, + typeBuilder.build(), strictStart, strictEnd, patternDefinitions, + measures.build(), after, subsets, allRows, + ImmutableList.copyOf(partitionKeys), RelCollations.of(fieldCollations), + interval); + stack.push(new Frame(match)); + return this; + } + /** Clears the stack. * * <p>The builder's state is now the same as when it was created. */ http://git-wip-us.apache.org/repos/asf/calcite/blob/3e97cff7/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java index 0f66db3..93e3b30 100644 --- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java @@ -27,12 +27,14 @@ import org.apache.calcite.rel.core.TableFunctionScan; import org.apache.calcite.rel.core.TableModify; import org.apache.calcite.rel.core.Window; import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rel.type.RelDataTypeField; import org.apache.calcite.rex.RexCorrelVariable; import org.apache.calcite.rex.RexInputRef; import org.apache.calcite.rex.RexNode; import org.apache.calcite.runtime.CalciteException; import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.sql.SqlMatchRecognize; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.parser.SqlParser; import org.apache.calcite.sql.type.SqlTypeName; @@ -46,6 +48,7 @@ import org.apache.calcite.util.Util; import org.apache.calcite.util.mapping.Mappings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -54,6 +57,7 @@ import org.junit.Test; import java.sql.PreparedStatement; import java.util.Arrays; import java.util.List; +import java.util.TreeSet; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; @@ -1861,6 +1865,92 @@ public class RelBuilderTest { assertThat(e.getMessage(), containsString("cannot derive type")); } } + + @Test public void testMatchRecognize() { + // Equivalent SQL: + // SELECT * + // FROM emp + // MATCH_RECOGNIZE ( + // PARTITION BY deptno + // ORDER BY empno asc + // MEASURES + // STRT.mgr as start_nw, + // LAST(DOWN.mgr) as bottom_nw, + // PATTERN (STRT DOWN+ UP+) WITHIN INTERVAL '5' SECOND + // DEFINE + // DOWN as DOWN.mgr < PREV(DOWN.mgr), + // UP as UP.mgr > PREV(UP.mgr) + // ) + final RelBuilder builder = RelBuilder.create(config().build()).scan("EMP"); + final RelDataTypeFactory typeFactory = builder.getTypeFactory(); + final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER); + + RexNode pattern = builder.patternConcat( + builder.literal("STRT"), + builder.patternQuantify(builder.literal("DOWN"), builder.literal(1), + builder.literal(-1), builder.literal(false)), + builder.patternQuantify(builder.literal("UP"), builder.literal(1), + builder.literal(-1), builder.literal(false))); + + ImmutableMap.Builder<String, RexNode> pdBuilder = new ImmutableMap.Builder<>(); + RexNode downDefinition = builder.call(SqlStdOperatorTable.LESS_THAN, + builder.call(SqlStdOperatorTable.PREV, + builder.patternField("DOWN", intType, 3), + builder.literal(0)), + builder.call(SqlStdOperatorTable.PREV, + builder.patternField("DOWN", intType, 3), + builder.literal(1))); + pdBuilder.put("DOWN", downDefinition); + RexNode upDefinition = builder.call(SqlStdOperatorTable.GREATER_THAN, + builder.call(SqlStdOperatorTable.PREV, + builder.patternField("UP", intType, 3), + builder.literal(0)), + builder.call(SqlStdOperatorTable.PREV, + builder.patternField("UP", intType, 3), + builder.literal(1))); + pdBuilder.put("UP", upDefinition); + + ImmutableList.Builder<RexNode> measuresBuilder = new ImmutableList.Builder<>(); + measuresBuilder.add( + builder.alias(builder.patternField("STRT", intType, 3), + "start_nw")); + measuresBuilder.add( + builder.alias( + builder.call(SqlStdOperatorTable.LAST, + builder.patternField("DOWN", intType, 3), + builder.literal(0)), + "bottom_nw")); + + RexNode after = builder.getRexBuilder().makeFlag( + SqlMatchRecognize.AfterOption.SKIP_TO_NEXT_ROW); + + ImmutableList.Builder<RexNode> partitionKeysBuilder = new ImmutableList.Builder<>(); + partitionKeysBuilder.add(builder.field("DEPTNO")); + + ImmutableList.Builder<RexNode> orderKeysBuilder = new ImmutableList.Builder<>(); + orderKeysBuilder.add(builder.field("EMPNO")); + + RexNode interval = builder.literal("INTERVAL '5' SECOND"); + + final ImmutableMap<String, TreeSet<String>> subsets = ImmutableMap.of(); + final RelNode root = builder + .match(pattern, false, false, pdBuilder.build(), + measuresBuilder.build(), after, subsets, false, + partitionKeysBuilder.build(), orderKeysBuilder.build(), interval) + .build(); + final String expected = "LogicalMatch(partition=[[$7]], order=[[0]], " + + "outputFields=[[$7, 'start_nw', 'bottom_nw']], allRows=[false], " + + "after=[FLAG(SKIP TO NEXT ROW)], pattern=[(('STRT', " + + "PATTERN_QUANTIFIER('DOWN', 1, -1, false)), " + + "PATTERN_QUANTIFIER('UP', 1, -1, false))], " + + "isStrictStarts=[false], isStrictEnds=[false], " + + "interval=['INTERVAL ''5'' SECOND'], subsets=[[]], " + + "patternDefinitions=[[<(PREV(DOWN.$3, 0), PREV(DOWN.$3, 1)), " + + ">(PREV(UP.$3, 0), PREV(UP.$3, 1))]], " + + "inputFields=[[EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO]])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(str(root), is(expected)); + } } // End RelBuilderTest.java http://git-wip-us.apache.org/repos/asf/calcite/blob/3e97cff7/site/_docs/algebra.md ---------------------------------------------------------------------- diff --git a/site/_docs/algebra.md b/site/_docs/algebra.md index 490ca4f..527b2e1 100644 --- a/site/_docs/algebra.md +++ b/site/_docs/algebra.md @@ -274,12 +274,14 @@ return the `RelBuilder`. | `union(all [, n])` | Creates a [Union]({{ site.apiRoot }}/org/apache/calcite/rel/core/Union.html) of the `n` (default two) most recent relational expressions. | `intersect(all [, n])` | Creates an [Intersect]({{ site.apiRoot }}/org/apache/calcite/rel/core/Intersect.html) of the `n` (default two) most recent relational expressions. | `minus(all)` | Creates a [Minus]({{ site.apiRoot }}/org/apache/calcite/rel/core/Minus.html) of the two most recent relational expressions. +| `match(pattern, strictStart,` `strictEnd, patterns, measures,` `after, subsets, allRows,` `partitionKeys, orderKeys,` `interval)` | Creates a [Match]({{ site.apiRoot }}/org/apache/calcite/rel/core/Match.html). Argument types: -* `expr` [RexNode]({{ site.apiRoot }}/org/apache/calcite/rex/RexNode.html) +* `expr`, `interval` [RexNode]({{ site.apiRoot }}/org/apache/calcite/rex/RexNode.html) * `expr...` Array of [RexNode]({{ site.apiRoot }}/org/apache/calcite/rex/RexNode.html) -* `exprList` Iterable of [RexNode]({{ site.apiRoot }}/org/apache/calcite/rex/RexNode.html) +* `exprList`, `measureList`, `partitionKeys`, `orderKeys` Iterable of + [RexNode]({{ site.apiRoot }}/org/apache/calcite/rex/RexNode.html) * `fieldOrdinal` Ordinal of a field within its row (starting from 0) * `fieldName` Name of a field, unique within its row * `fieldName...` Array of String @@ -291,12 +293,14 @@ Argument types: * `value...` Array of Object * `value` Object * `tupleList` Iterable of List of [RexLiteral]({{ site.apiRoot }}/org/apache/calcite/rex/RexLiteral.html) -* `all` boolean -* `distinct` boolean +* `all`, `distinct`, `strictStart`, `strictEnd`, `allRows` boolean * `alias` String * `varHolder` [Holder]({{ site.apiRoot }}/org/apache/calcite/util/Holder.html) of [RexCorrelVariable]({{ site.apiRoot }}/org/apache/calcite/rex/RexCorrelVariable.html) +* `patterns` Map whose key is String, value is [RexNode]({{ site.apiRoot }}/org/apache/calcite/rex/RexNode.html) +* `subsets` Map whose key is String, value is a sorted set of String The builder methods perform various optimizations, including: + * `project` returns its input if asked to project all columns in order * `filter` flattens the condition (so an `AND` and `OR` may have more than 2 children), simplifies (converting say `x = 1 AND TRUE` to `x = 1`) @@ -355,6 +359,18 @@ added to the stack. | `nullsFirst(expr)` | Changes sort order to nulls first (only valid as an argument to `sort` or `sortLimit`) | `nullsLast(expr)` | Changes sort order to nulls last (only valid as an argument to `sort` or `sortLimit`) +#### Pattern methods + +The following methods return patterns for use in `match`. + +| Method | Description +|:------------------- |:----------- +| `patternConcat(pattern...)` | Concatenates patterns +| `patternAlter(pattern...)` | Alternates patterns +| `patternQuantify(pattern, min, max)` | Quantifies a pattern +| `patternPermute(pattern...)` | Permutes a pattern +| `patternExclude(pattern)` | Excludes a pattern + ### Group key methods The following methods return a
