Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ engines:
exclude_paths:
# These fixtures use PostgreSQL grammar, not Transact-SQL.
- "src/test/resources/postgresql/**"
# MATCH_RECOGNIZE fixtures use Oracle and BigQuery grammar.
- "src/test/resources/net/sf/jsqlparser/statement/select/match-recognize/**"
- "src/test/resources/net/sf/jsqlparser/statement/select/match-recognize-2350.sql"
exclude_paths:
- "site/**"
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
}

default <S> T visitLimit(Limit limit, S context) {
if (limit != null && !limit.isLimitNull() && !limit.isLimitAll()) {

Check warning on line 113 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 113 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 113 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 113 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 113 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 113 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitNull() in Limit has been deprecated
if (limit.getOffset() != null) {
limit.getOffset().accept(this, context);
}
Expand Down Expand Up @@ -510,6 +510,14 @@
this.visit(extractExpression, null);
}

default <S> T visit(RowPatternFunction function, S context) {
return function.getFunction().accept(this, context);
}

default void visit(RowPatternFunction function) {
visit(function, null);
}

<S> T visit(IntervalExpression intervalExpression, S context);

default void visit(IntervalExpression intervalExpression) {
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/RowPatternFunction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.expression;

import java.util.function.Consumer;
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;

/** An explicit RUNNING or FINAL evaluation mode on a row-pattern function. */
public class RowPatternFunction extends ASTNodeAccessImpl implements Expression {
public enum EvaluationMode {
RUNNING, FINAL
}

private EvaluationMode evaluationMode;
private Function function;

public RowPatternFunction(EvaluationMode evaluationMode, Function function) {
this.evaluationMode = evaluationMode;
this.function = function;
}

public EvaluationMode getEvaluationMode() {
return evaluationMode;
}

public RowPatternFunction setEvaluationMode(EvaluationMode mode) {
evaluationMode = mode;
return this;
}

public Function getFunction() {
return function;
}

public RowPatternFunction setFunction(Function function) {
this.function = function;
return this;
}

@Override
public <T, S> T accept(ExpressionVisitor<T> visitor, S context) {
return visitor.visit(this, context);
}

public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressions) {
builder.append(evaluationMode).append(' ');
expressions.accept(function);
return builder;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
return appendTo(builder, expression -> builder.append(expression)).toString();
}
}
106 changes: 48 additions & 58 deletions src/main/java/net/sf/jsqlparser/expression/StructType.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/*
* STRUCT<T>
Expand Down Expand Up @@ -113,73 +114,62 @@ public StructType add(Expression expression, String aliasName) {
}

public StringBuilder appendTo(StringBuilder builder) {
if (dialect != Dialect.DUCKDB && keyword != null) {
builder.append(keyword);
}
return appendTo(builder, expression -> builder.append(expression));
}

if (dialect != Dialect.DUCKDB && parameters != null && !parameters.isEmpty()) {
builder.append("<");
int i = 0;

for (Map.Entry<String, ColDataType> e : parameters) {
if (0 < i++) {
builder.append(",");
}
// optional name
if (e.getKey() != null && !e.getKey().isEmpty()) {
builder.append(e.getKey()).append(" ");
}

// mandatory type
builder.append(e.getValue());
/** Renders current argument expressions through the caller's visitor. */
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressions) {
if (dialect == Dialect.DUCKDB) {
appendArguments(builder, expressions);
appendTypeParameters(builder, true);
} else {
if (keyword != null) {
builder.append(keyword);
}

builder.append(">");
appendTypeParameters(builder, false);
appendArguments(builder, expressions);
}
return builder;
}

if (arguments != null && !arguments.isEmpty()) {

if (dialect == Dialect.DUCKDB) {
builder.append("{ ");
int i = 0;
for (SelectItem<?> e : arguments) {
if (0 < i++) {
builder.append(",");
}
builder.append(e.getAlias().getName());
builder.append(":");
builder.append(e.getExpression());
}
builder.append(" }");
} else {
builder.append("(");
int i = 0;
for (SelectItem<?> e : arguments) {
if (0 < i++) {
builder.append(",");
}
e.appendTo(builder);
}

builder.append(")");
private void appendTypeParameters(StringBuilder builder, boolean cast) {
if (parameters == null || parameters.isEmpty()) {
return;
}
builder.append(cast ? "::STRUCT( " : "<");
int i = 0;
for (Map.Entry<String, ColDataType> parameter : parameters) {
if (i++ > 0) {
builder.append(',');
}
if (cast || parameter.getKey() != null && !parameter.getKey().isEmpty()) {
builder.append(parameter.getKey()).append(' ');
}
builder.append(parameter.getValue());
}
builder.append(cast ? ')' : '>');
}

if (dialect == Dialect.DUCKDB && parameters != null && !parameters.isEmpty()) {
builder.append("::STRUCT( ");
int i = 0;

for (Map.Entry<String, ColDataType> e : parameters) {
if (0 < i++) {
builder.append(",");
}
builder.append(e.getKey()).append(" ");
builder.append(e.getValue());
private void appendArguments(StringBuilder builder, Consumer<Expression> expressions) {
if (arguments == null || arguments.isEmpty()) {
return;
}
boolean structLiteral = dialect == Dialect.DUCKDB;
builder.append(structLiteral ? "{ " : "(");
int i = 0;
for (SelectItem<?> argument : arguments) {
if (i++ > 0) {
builder.append(',');
}
if (structLiteral) {
builder.append(argument.getAlias().getName()).append(':');
}
expressions.accept(argument.getExpression());
if (!structLiteral && argument.getAlias() != null) {
builder.append(argument.getAlias());
}
builder.append(")");
}

return builder;
builder.append(structLiteral ? " }" : ")");
}

@Override
Expand Down
83 changes: 83 additions & 0 deletions src/main/java/net/sf/jsqlparser/parser/RowPatternTokenContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.parser;

import java.util.ArrayDeque;
import java.util.Deque;

/**
* Recognizes the BigQuery PATTERN lexical region even during JavaCC lookahead. The required initial
* ORDER BY (or optional PARTITION BY) distinguishes a clause from an ordinary function named
* MATCH_RECOGNIZE. Quoted tokens never change this state.
*/
final class RowPatternTokenContext implements CCJSqlParserConstants {
private final Deque<Frame> frames = new ArrayDeque<>();
private int depth;
private boolean pendingClause;

private static final class Frame {
private final int depth;
private int phase;
private int patternDepth = -1;
private boolean pendingPattern;
private boolean patternComplete;

private Frame(int depth) {
this.depth = depth;
}
}

boolean isIdle() {
return frames.isEmpty() && !pendingClause;
}

boolean isInPattern() {
return !frames.isEmpty() && frames.peek().patternDepth >= 0;
}

void accept(Token token) {
if (token.kind == OPENING_BRACKET) {
depth++;
if (pendingClause) {
frames.push(new Frame(depth));
pendingClause = false;
return;
}
if (!frames.isEmpty() && frames.peek().pendingPattern) {
frames.peek().patternDepth = depth;
frames.peek().pendingPattern = false;
}
} else if (token.kind == CLOSING_BRACKET) {
if (!frames.isEmpty()) {
Frame frame = frames.peek();
if (frame.patternDepth == depth) {
frame.patternDepth = -1;
frame.patternComplete = true;
}
if (frame.depth == depth) {
frames.pop();
}
}
depth--;
} else if (!frames.isEmpty() && depth == frames.peek().depth) {
Frame frame = frames.peek();
if (frame.phase == 0) {
frame.phase = token.kind == K_ORDER || token.kind == K_PARTITION ? 1 : -1;
} else if (frame.phase == 1) {
frame.phase = token.kind == K_BY ? 2 : -1;
} else if (frame.phase == 2 && !frame.patternComplete) {
frame.pendingPattern = token.kind == S_IDENTIFIER
&& "PATTERN".equalsIgnoreCase(token.image);
}
}
pendingClause = token.kind == S_IDENTIFIER
&& "MATCH_RECOGNIZE".equalsIgnoreCase(token.image);
}
}
4 changes: 4 additions & 0 deletions src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public enum Feature {
* "SELECT"
*/
select,
/** Structured FROM row pattern recognition. */
matchRecognize,
/** BigQuery longest-match option. */
matchRecognizeOptions,
/**
* "GROUP BY"
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ default <S> T visitJoins(Collection<Join> joins, S context) {
return null;
}

/**
* Existing visitors continue through the input relation; adapters also visit clause
* expressions.
*/
default <S> T visit(MatchRecognize matchRecognize, S context) {
return matchRecognize.getInput().accept(this, context);
}

default void visit(MatchRecognize matchRecognize) {
visit(matchRecognize, null);
}

<S> T visit(Table tableName, S context);

default void visit(Table tableName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,34 @@ public <S> T visitJoins(Collection<Join> joins, S context) {
return null;
}

@Override
public <S> T visit(MatchRecognize matchRecognize, S context) {
matchRecognize.getInput().accept(this, context);
matchRecognize
.forEachExpression(expression -> expression.accept(expressionVisitor, context));
PivotVisitorAdapter<T> pivots = new PivotVisitorAdapter<>(expressionVisitor);
if (matchRecognize.getPivot() != null) {
matchRecognize.getPivot().accept(pivots, context);
}
if (matchRecognize.getUnPivot() != null) {
matchRecognize.getUnPivot().accept(pivots, context);
}
return null;
}

@Override
public <S> T visit(Table table, S context) {
return null;
}

@Override
public <S> T visit(ParenthesedSelect select, S context) {
return select.getPlainSelect().accept(selectVisitor, context);
return select.getSelect().accept(selectVisitor, context);
}

@Override
public <S> T visit(LateralSubSelect lateralSubSelect, S context) {
return lateralSubSelect.getPlainSelect().accept(selectVisitor, context);
return lateralSubSelect.getSelect().accept(selectVisitor, context);
}

@Override
Expand All @@ -108,7 +123,9 @@ public <S> T visit(TableFunction tableFunction, S context) {

@Override
public <S> T visit(ParenthesedFromItem fromItem, S context) {
return fromItem.getFromItem().accept(this, context);
T result = fromItem.getFromItem().accept(this, context);
visitJoins(fromItem.getJoins(), context);
return result;
}

@Override
Expand Down
Loading
Loading