Skip to content
Merged
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
52 changes: 48 additions & 4 deletions src/main/java/net/sf/jsqlparser/expression/ColumnsTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@
public class ColumnsTransformer extends ASTNodeAccessImpl {

public enum ColumnsTransformerType {
APPLY, EXCEPT, EXCLUDE, REPLACE
APPLY, EXCEPT, EXCLUDE, REPLACE, RENAME
}

private ColumnsTransformerType type;
private Expression applyExpression;
private ParenthesedExpressionList<Column> exceptColumns;
private StringValue exceptPattern;
private List<SelectItem<?>> replaceItems;
private List<SelectItem<?>> renameItems;
private boolean parenthesized = true;
private boolean strict;

public ColumnsTransformer(ColumnsTransformerType type) {
Expand Down Expand Up @@ -108,6 +110,30 @@ public ColumnsTransformer setReplaceItems(List<SelectItem<?>> replaceItems) {
return this;
}

/** DuckDB's {@code RENAME (column AS name, ...)}: each item is a column with its new name. */
public List<SelectItem<?>> getRenameItems() {
return renameItems;
}

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

/**
* Whether the columns or items of an {@code EXCLUDE}, {@code REPLACE} or {@code RENAME} are
* parenthesized; DuckDB also accepts a single one without parentheses, e.g.
* {@code * EXCLUDE i}.
*/
public boolean isParenthesized() {
return parenthesized;
}

public ColumnsTransformer setParenthesized(boolean parenthesized) {
this.parenthesized = parenthesized;
return this;
}

void collectExpressions(List<Expression> expressions) {
switch (type) {
case APPLY:
Expand Down Expand Up @@ -135,6 +161,13 @@ void collectExpressions(List<Expression> expressions) {
}
}
break;
case RENAME:
if (renameItems != null) {
for (SelectItem<?> item : renameItems) {
expressions.add(item.getExpression());
}
}
break;
default:
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);
}
Expand All @@ -150,11 +183,22 @@ public StringBuilder appendTo(StringBuilder builder) {
.append(exceptPattern != null ? exceptPattern : exceptColumns);
break;
case EXCLUDE:
builder.append("EXCLUDE ").append(exceptColumns);
builder.append("EXCLUDE ").append(parenthesized || exceptColumns.size() != 1
? exceptColumns
: exceptColumns.get(0));
break;
case REPLACE:
builder.append("REPLACE").append(strict ? " STRICT" : "").append("(")
.append(Select.getStringList(replaceItems)).append(")");
boolean replaceParenthesized = parenthesized || replaceItems.size() != 1;
builder.append("REPLACE").append(strict ? " STRICT" : "")
.append(replaceParenthesized ? "(" : " ")
.append(Select.getStringList(replaceItems))
.append(replaceParenthesized ? ")" : "");
break;
case RENAME:
boolean renameParenthesized = parenthesized || renameItems.size() != 1;
builder.append("RENAME ").append(renameParenthesized ? "(" : "")
.append(Select.getStringList(renameItems))
.append(renameParenthesized ? ")" : "");
break;
default:
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);
Expand Down
185 changes: 175 additions & 10 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,126 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
}
}

/**
* Whether the pending "," ends a select item list, as DuckDB allows: it is followed by a
* closing parenthesis or by a clause that cannot be read as a column such as limit or "group".
*/
private boolean isTrailingSelectItemCommaAhead() {
if (!",".equals(getToken(1).image)) {
return false;
}
Token next = getToken(2);
int following = getToken(3).kind;
switch (next.kind) {
case CLOSING_BRACKET:
case K_FROM: case K_WHERE: case K_HAVING: case K_WINDOW: case K_INTO:
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
return true;
case K_GROUP: case K_ORDER:
// not a column named group aliased by: SELECT a, group by FROM t
int afterBy = getToken(4).kind;
return following == K_BY && afterBy != K_FROM && afterBy != EOF
&& afterBy != CLOSING_BRACKET && afterBy != ST_SEMICOLON
&& !",".equals(getToken(4).image);
case K_LIMIT: case K_OFFSET:
return following == S_LONG || "?".equals(getToken(3).image);
default:
return false;
}
}

/**
* Whether the REPLACE at {@code offset} is DuckDB's unparenthesized {@code REPLACE expr AS name}:
* an AS follows at the top level before the end of the select item, so that a star aliased
* "replace" keeps parsing. With {@code offset} 0, the REPLACE has been consumed.
*/
private boolean isUnparenthesizedReplaceAhead(int offset) {
Token token = getToken(offset);
if (offset > 0 && token.kind != K_REPLACE) {
return false;
}
int depth = 0;
while (true) {
token = nextWithItemToken(token);
if (token.kind == EOF || ";".equals(token.image)) {
return false;
}
if ("(".equals(token.image) || "[".equals(token.image) || "{".equals(token.image)) {
depth++;
} else if (")".equals(token.image) || "]".equals(token.image)
|| "}".equals(token.image)) {
if (--depth < 0) {
return false;
}
} else if (depth == 0) {
if (token.kind == K_AS) {
return true;
}
if (token.kind == K_FROM || ",".equals(token.image)) {
return false;
}
}
}
}

private boolean isUnparenthesizedReplaceAhead() {
return isUnparenthesizedReplaceAhead(1);
}

/**
* Whether DuckDB's {@code RENAME (column AS name, ...)} or {@code RENAME column AS name}
* follows, every column being renamed with AS, so that a star aliased "rename" keeps parsing.
*/
private boolean isRenameAhead() {
Token token = getToken(1);
if (token.kind != K_RENAME) {
return false;
}
token = nextWithItemToken(token);
boolean parenthesized = "(".equals(token.image);
if (parenthesized) {
token = nextWithItemToken(token);
}
while (true) {
// column [. column]* AS name
while (true) {
if (!isIdentifierishNameToken(token)) {
return false;
}
token = nextWithItemToken(token);
if (!".".equals(token.image)) {
break;
}
token = nextWithItemToken(token);
}
if (token.kind != K_AS) {
return false;
}
token = nextWithItemToken(token);
if (!isIdentifierishNameToken(token)) {
return false;
}
token = nextWithItemToken(token);
if (!parenthesized) {
return true;
}
if (")".equals(token.image)) {
return true;
}
if (!",".equals(token.image)) {
return false;
}
token = nextWithItemToken(token);
}
}

/** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */
private static void requireTransformerAlias(SelectItem<?> item) throws ParseException {
if (item.getAlias() == null) {
throw new ParseException("Expected AS and a name after " + item);
}
}

private boolean isAliasAhead() {
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
return false;
Expand Down Expand Up @@ -8694,11 +8814,13 @@ List<SelectItem<?>> SelectItemsList():
{
selectItem=SelectItem() { selectItemsList = new ArrayList<SelectItem<?>>(); selectItemsList.add(selectItem); }
(
LOOKAHEAD(2) "," selectItem=SelectItem()
LOOKAHEAD(2, { !isTrailingSelectItemCommaAhead() }) "," selectItem=SelectItem()
{
selectItemsList.add(selectItem);
}
)*
// DuckDB's trailing comma: SELECT a, b, FROM t
[ LOOKAHEAD({ isTrailingSelectItemCommaAhead() }) "," ]

{ return selectItemsList; }
}
Expand Down Expand Up @@ -8764,6 +8886,9 @@ boolean AllColumnsTransformerAhead() #void:
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
| <K_EXCLUDE> ParenthesedColumnList() )
{ return true; }
// DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)]
| LOOKAHEAD( <K_EXCLUDE> Column() ) { return true; }
| LOOKAHEAD({ isUnparenthesizedReplaceAhead() || isRenameAhead() }) { return true; }
| { return false; }
}

Expand Down Expand Up @@ -8831,6 +8956,8 @@ ColumnsTransformer ColumnsTransformer():
ParenthesedExpressionList<Column> exceptColumns = null;
StringValue exceptPattern = null;
List<SelectItem<?>> replaceItems;
SelectItem<?> item;
Column column;
ColumnsTransformer transformer;
Token tk;
boolean strict;
Expand Down Expand Up @@ -8863,16 +8990,53 @@ ColumnsTransformer ColumnsTransformer():
transformer.setExceptColumns(exceptColumns);
} }

| <K_EXCLUDE> exceptColumns = ParenthesedColumnList()
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE)
.setExceptColumns(exceptColumns); }
| <K_EXCLUDE>
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE); }
(
exceptColumns = ParenthesedColumnList()
|
column = Column()
{
exceptColumns = new ParenthesedExpressionList<Column>(column);
transformer.setParenthesized(false);
}
)
{ transformer.setExceptColumns(exceptColumns); }

| <K_REPLACE>
{ strict = false; }
[ <K_STRICT> { strict = true; } ]
"(" replaceItems = SelectItemsList() ")"
[ LOOKAHEAD({ getToken(1).kind == K_STRICT && "(".equals(getToken(2).image) })
<K_STRICT> { strict = true; } ]
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
.setStrict(strict).setReplaceItems(replaceItems); }
.setStrict(strict); }
(
LOOKAHEAD({ "(".equals(getToken(1).image) && !isUnparenthesizedReplaceAhead(0) })
"(" replaceItems = SelectItemsList() ")"
|
item = SelectItem()
{
requireTransformerAlias(item);
replaceItems = new ArrayList<SelectItem<?>>();
replaceItems.add(item);
transformer.setParenthesized(false);
}
)
{ transformer.setReplaceItems(replaceItems); }

| <K_RENAME>
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.RENAME); }
(
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
|
item = SelectItem()
{
requireTransformerAlias(item);
replaceItems = new ArrayList<SelectItem<?>>();
replaceItems.add(item);
transformer.setParenthesized(false);
}
)
{ transformer.setRenameItems(replaceItems); }
)
{ return transformer; }
}
Expand All @@ -8889,12 +9053,13 @@ AllColumns AllColumns(boolean allowAdditions):
}
{
"*"
// BigData allows EXCEPT, DuckDB allows EXCLUDE, ClickHouse allows APPLY;
// BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY;
// they may repeat and combine in any order
(
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
// AllColumnsTransformerAhead() tells DuckDB's REPLACE and RENAME from an alias of the star
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> | <K_RENAME>
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
| <K_EXCLUDE> ParenthesedColumnList() )
| <K_EXCLUDE> ( ParenthesedColumnList() | Column() ) )
{
if (!allowAdditions || !AllColumnsTransformerAhead()) {
break;
Expand Down
51 changes: 51 additions & 0 deletions src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1021,4 +1021,55 @@ void testLargeArrayIsScannedOnce() throws JSQLParserException {
}
CCJSqlParserUtil.parse(sql.append("]").toString());
}

@Test
void testStarRenameAndUnparenthesizedModifiers() throws JSQLParserException {
for (String sqlStr : new String[] {"SELECT * RENAME (i AS x, j AS y) FROM t",
"SELECT * RENAME i AS x FROM t", "SELECT * EXCLUDE i FROM t",
"SELECT * EXCLUDE t.i FROM t", "SELECT * REPLACE i + 1 AS i FROM t",
"SELECT * EXCLUDE (k) RENAME (i AS x) FROM t",
"SELECT t.* RENAME t.i AS x FROM t",
"SELECT COLUMNS(* REPLACE i + 1 AS i) FROM t"}) {
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
}

PlainSelect select = (PlainSelect) CCJSqlParserUtil
.parse("SELECT * RENAME (i AS x) FROM t");
net.sf.jsqlparser.expression.ColumnsTransformer rename =
((AllColumns) select.getSelectItem(0)
.getExpression()).getTransformers().get(0);
Assertions.assertEquals(
net.sf.jsqlparser.expression.ColumnsTransformer.ColumnsTransformerType.RENAME,
rename.getType());
Assertions.assertEquals("i AS x", rename.getRenameItems().get(0).toString());

// a star aliased rename or replace keeps parsing
for (String sqlStr : new String[] {"SELECT * rename FROM t", "SELECT * replace FROM t",
"SELECT t.* rename, b FROM t", "SELECT * rename(a, b) FROM t"}) {
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
}
// DuckDB reads a parenthesized expression followed by AS as the single item
select = (PlainSelect) CCJSqlParserUtil.parse("SELECT * REPLACE (a + 1) AS a FROM t");
Assertions.assertEquals("(a + 1) AS a",
((AllColumns) select.getSelectItem(0)
.getExpression()).getTransformers().get(0).getReplaceItems().get(0)
.toString());
}

@Test
void testTrailingCommaInSelectList() throws JSQLParserException {
Assertions.assertEquals("SELECT i, j FROM t WHERE i > 0",
CCJSqlParserUtil.parse("SELECT i, j, FROM t WHERE i > 0").toString());
Assertions.assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse("SELECT * RENAME i FROM t"));
Assertions.assertEquals("SELECT 1 LIMIT 1",
CCJSqlParserUtil.parse("SELECT 1, LIMIT 1").toString());
Assertions.assertEquals("SELECT (SELECT 1)",
CCJSqlParserUtil.parse("SELECT (SELECT 1,)").toString());
// keywords that may name a column or a function still do
for (String item : new String[] {"limit", "offset", "group", "order", "qualify",
"limit(1)", "offset(1)", "group by", "order by"}) {
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a, " + item + " FROM t", true);
}
}
}
Loading