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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
|---|---|
| **Queries** | `SELECT` · `WITH …` · Piped SQL |
| **ksqlDB windows** | JOIN `WITHIN`, window `GRACE PERIOD`, and `EMIT CHANGES`/`FINAL` |
| **ClickHouse column selection** | `COLUMNS('regexp')` select items with chained `APPLY`, `EXCEPT`, and `REPLACE` transformers |
| **DML** | `INSERT` · `UPDATE` · `UPSERT` · `MERGE` · `DELETE` · `TRUNCATE TABLE` |
| **DDL** | `CREATE …` · `ALTER …` · `DROP …` |
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/ColumnsExpression.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*-
* #%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.ArrayList;
import java.util.List;
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;

/**
* A ClickHouse {@code COLUMNS('regexp')} matcher with one or more transformers, for example
* {@code COLUMNS('^metric_') APPLY(x -> round(x, 2))}.
*
* Without a transformer, {@code COLUMNS('regexp')} keeps parsing as a regular
* {@link net.sf.jsqlparser.expression.Function}.
*/
public class ColumnsExpression extends ASTNodeAccessImpl implements Expression {

private Expression columns;
private List<ColumnsTransformer> transformers;

public ColumnsExpression(Expression columns) {
this.columns = columns;
this.transformers = new ArrayList<>();
}

public ColumnsExpression(Expression columns, List<ColumnsTransformer> transformers) {
this.columns = columns;
this.transformers = transformers;
}

public Expression getColumns() {
return columns;
}

public ColumnsExpression setColumns(Expression columns) {
this.columns = columns;
return this;
}

public List<ColumnsTransformer> getTransformers() {
return transformers;
}

public ColumnsExpression setTransformers(List<ColumnsTransformer> transformers) {
this.transformers = transformers;
return this;
}

public List<Expression> getAllExpressions() {
List<Expression> expressions = new ArrayList<>();
if (columns != null) {
expressions.add(columns);
}
for (ColumnsTransformer transformer : transformers) {
transformer.collectExpressions(expressions);
}
return expressions;
}

public StringBuilder appendTo(StringBuilder builder) {
builder.append(columns);
for (ColumnsTransformer transformer : transformers) {
builder.append(" ").append(transformer);
}
return builder;
}

@Override
public String toString() {
return appendTo(new StringBuilder()).toString();
}

@Override
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
return expressionVisitor.visit(this, context);
}
}
121 changes: 121 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/ColumnsTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*-
* #%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.List;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectItem;

/**
* A ClickHouse transformer following a {@code COLUMNS(...)} matcher, for example the
* {@code APPLY(x -> round(x, 2))} in {@code SELECT COLUMNS('^m') APPLY(x -> round(x, 2))}.
*
* ClickHouse parses its transformers in a loop, so they may repeat and combine in any order.
*/
public class ColumnsTransformer extends ASTNodeAccessImpl {

public enum ColumnsTransformerType {
APPLY, EXCEPT, REPLACE
}

private ColumnsTransformerType type;
private Expression applyExpression;
private ParenthesedExpressionList<Column> exceptColumns;
private List<SelectItem<?>> replaceItems;

public ColumnsTransformer(ColumnsTransformerType type) {
this.type = type;
}

public ColumnsTransformerType getType() {
return type;
}

public ColumnsTransformer setType(ColumnsTransformerType type) {
this.type = type;
return this;
}

public Expression getApplyExpression() {
return applyExpression;
}

public ColumnsTransformer setApplyExpression(Expression applyExpression) {
this.applyExpression = applyExpression;
return this;
}

public ParenthesedExpressionList<Column> getExceptColumns() {
return exceptColumns;
}

public ColumnsTransformer setExceptColumns(ParenthesedExpressionList<Column> exceptColumns) {
this.exceptColumns = exceptColumns;
return this;
}

public List<SelectItem<?>> getReplaceItems() {
return replaceItems;
}

public ColumnsTransformer setReplaceItems(List<SelectItem<?>> replaceItems) {
this.replaceItems = replaceItems;
return this;
}

void collectExpressions(List<Expression> expressions) {
switch (type) {
case APPLY:
if (applyExpression != null) {
expressions.add(applyExpression);
}
break;
case EXCEPT:
if (exceptColumns != null) {
expressions.addAll(exceptColumns);
}
break;
case REPLACE:
if (replaceItems != null) {
for (SelectItem<?> item : replaceItems) {
expressions.add(item.getExpression());
}
}
break;
default:
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);
}
}

public StringBuilder appendTo(StringBuilder builder) {
switch (type) {
case APPLY:
builder.append("APPLY(").append(applyExpression).append(")");
break;
case EXCEPT:
builder.append("EXCEPT ").append(exceptColumns);
break;
case REPLACE:
builder.append("REPLACE(").append(Select.getStringList(replaceItems)).append(")");
break;
default:
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);
}
return builder;
}

@Override
public String toString() {
return appendTo(new StringBuilder()).toString();
}
}
13 changes: 13 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
}
if (limit.getByExpressions() != null) {
limit.getByExpressions().accept(this, context);
}

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

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
}
return null;
}
Expand Down Expand Up @@ -796,6 +796,19 @@
this.visit(lambdaExpression, null);
}

default <S> T visit(ColumnsExpression columnsExpression, S context) {
for (Expression expression : columnsExpression.getAllExpressions()) {
if (expression != null) {
expression.accept(this, context);
}
}
return null;
}

default void visit(ColumnsExpression columnsExpression) {
this.visit(columnsExpression, null);
}

<S> T visit(HighExpression highExpression, S context);

default void visit(HighExpression highExpression) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -781,12 +781,12 @@

@Override
public <S> T visit(ConnectByRootOperator connectByRootOperator, S context) {
return connectByRootOperator.getColumn().accept(this, context);

Check warning on line 784 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] getColumn() in ConnectByRootOperator has been deprecated

Check warning on line 784 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] getColumn() in ConnectByRootOperator has been deprecated

Check warning on line 784 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] getColumn() in ConnectByRootOperator has been deprecated
}

@Override
public <S> T visit(ConnectByPriorOperator connectByPriorOperator, S context) {
return connectByPriorOperator.getColumn().accept(this, context);

Check warning on line 789 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] getColumn() in ConnectByPriorOperator has been deprecated

Check warning on line 789 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] getColumn() in ConnectByPriorOperator has been deprecated

Check warning on line 789 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] getColumn() in ConnectByPriorOperator has been deprecated
}

@Override
Expand Down Expand Up @@ -875,6 +875,11 @@
return lambdaExpression.getExpression().accept(this, context);
}

@Override
public <S> T visit(ColumnsExpression columnsExpression, S context) {
return visitExpressions(columnsExpression, context, columnsExpression.getAllExpressions());
}

@Override
public <S> T visit(HighExpression highExpression, S context) {
return highExpression.getExpression().accept(this, context);
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2227,6 +2227,16 @@ public <S> Void visit(LambdaExpression lambdaExpression, S context) {
return null;
}

@Override
public <S> Void visit(ColumnsExpression columnsExpression, S context) {
for (Expression expression : columnsExpression.getAllExpressions()) {
if (expression != null) {
expression.accept(this, context);
}
}
return null;
}

@Override
public <S> Void visit(HighExpression highExpression, S context) {
highExpression.getExpression().accept(this, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import net.sf.jsqlparser.expression.CaseExpression;
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.CollateExpression;
import net.sf.jsqlparser.expression.ColumnsExpression;
import net.sf.jsqlparser.expression.ColumnsTransformer;
import net.sf.jsqlparser.expression.ConnectByPriorOperator;
import net.sf.jsqlparser.expression.ConnectByRootOperator;
import net.sf.jsqlparser.expression.DateTimeLiteralExpression;
Expand Down Expand Up @@ -1926,6 +1928,43 @@ public <S> StringBuilder visit(LambdaExpression lambdaExpression, S context) {
return builder;
}

@Override
public <S> StringBuilder visit(ColumnsExpression columnsExpression, S context) {
columnsExpression.getColumns().accept(this, context);
for (ColumnsTransformer transformer : columnsExpression.getTransformers()) {
switch (transformer.getType()) {
case APPLY:
builder.append(" APPLY(");
transformer.getApplyExpression().accept(this, context);
builder.append(")");
break;
case EXCEPT:
builder.append(" EXCEPT ");
transformer.getExceptColumns().accept(this, context);
break;
case REPLACE:
builder.append(" REPLACE(");
boolean first = true;
for (SelectItem<?> item : transformer.getReplaceItems()) {
if (!first) {
builder.append(", ");
}
first = false;
item.getExpression().accept(this, context);
if (item.getAlias() != null) {
builder.append(item.getAlias());
}
}
builder.append(")");
break;
default:
throw new IllegalStateException(
"Unhandled ColumnsTransformerType: " + transformer.getType());
}
}
return builder;
}

@Override
public <S> StringBuilder visit(HighExpression highExpression, S context) {
return builder.append(highExpression.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import net.sf.jsqlparser.expression.CaseExpression;
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.CollateExpression;
import net.sf.jsqlparser.expression.ColumnsExpression;
import net.sf.jsqlparser.expression.ConnectByPriorOperator;
import net.sf.jsqlparser.expression.ConnectByRootOperator;
import net.sf.jsqlparser.expression.DateTimeLiteralExpression;
Expand Down Expand Up @@ -1218,6 +1219,16 @@ public <S> Void visit(LambdaExpression lambdaExpression, S context) {
return null;
}

@Override
public <S> Void visit(ColumnsExpression columnsExpression, S context) {
for (Expression expression : columnsExpression.getAllExpressions()) {
if (expression != null) {
expression.accept(this, context);
}
}
return null;
}

@Override
public <S> Void visit(HighExpression highExpression, S context) {
highExpression.getExpression().accept(this, context);
Expand Down
Loading
Loading