Skip to content

Commit 576e098

Browse files
committed
DuckDB: support * RENAME, unparenthesized star modifiers and a trailing comma
- `* RENAME (i AS x, j AS y)` and `* RENAME i AS x` are the new RENAME `ColumnsTransformer`. - After `*`, `EXCLUDE i`, `REPLACE i + 1 AS i` and `RENAME i AS x` may name a single column or item without parentheses, and print back that way. Unparenthesized REPLACE and RENAME require the `AS name`. - A select list may end with a comma before a clause such as FROM, WHERE or `ORDER BY`, or before `)`. Keywords that may name a column, as in `SELECT a, limit FROM t`, still do. The comma is not printed.
1 parent fde4f03 commit 576e098

3 files changed

Lines changed: 167 additions & 14 deletions

File tree

‎src/main/java/net/sf/jsqlparser/expression/ColumnsTransformer.java‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@
2525
public class ColumnsTransformer extends ASTNodeAccessImpl {
2626

2727
public enum ColumnsTransformerType {
28-
APPLY, EXCEPT, EXCLUDE, REPLACE
28+
APPLY, EXCEPT, EXCLUDE, REPLACE, RENAME
2929
}
3030

3131
private ColumnsTransformerType type;
3232
private Expression applyExpression;
3333
private ParenthesedExpressionList<Column> exceptColumns;
3434
private StringValue exceptPattern;
3535
private List<SelectItem<?>> replaceItems;
36+
private List<SelectItem<?>> renameItems;
37+
private boolean parenthesized = true;
3638
private boolean strict;
3739

3840
public ColumnsTransformer(ColumnsTransformerType type) {
@@ -108,6 +110,30 @@ public ColumnsTransformer setReplaceItems(List<SelectItem<?>> replaceItems) {
108110
return this;
109111
}
110112

113+
/** DuckDB's {@code RENAME (column AS name, ...)}: each item is a column with its new name. */
114+
public List<SelectItem<?>> getRenameItems() {
115+
return renameItems;
116+
}
117+
118+
public ColumnsTransformer setRenameItems(List<SelectItem<?>> renameItems) {
119+
this.renameItems = renameItems;
120+
return this;
121+
}
122+
123+
/**
124+
* Whether the columns or items of an {@code EXCLUDE}, {@code REPLACE} or {@code RENAME} are
125+
* parenthesized; DuckDB also accepts a single one without parentheses, e.g.
126+
* {@code * EXCLUDE i}.
127+
*/
128+
public boolean isParenthesized() {
129+
return parenthesized;
130+
}
131+
132+
public ColumnsTransformer setParenthesized(boolean parenthesized) {
133+
this.parenthesized = parenthesized;
134+
return this;
135+
}
136+
111137
void collectExpressions(List<Expression> expressions) {
112138
switch (type) {
113139
case APPLY:
@@ -135,6 +161,13 @@ void collectExpressions(List<Expression> expressions) {
135161
}
136162
}
137163
break;
164+
case RENAME:
165+
if (renameItems != null) {
166+
for (SelectItem<?> item : renameItems) {
167+
expressions.add(item.getExpression());
168+
}
169+
}
170+
break;
138171
default:
139172
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);
140173
}
@@ -150,11 +183,19 @@ public StringBuilder appendTo(StringBuilder builder) {
150183
.append(exceptPattern != null ? exceptPattern : exceptColumns);
151184
break;
152185
case EXCLUDE:
153-
builder.append("EXCLUDE ").append(exceptColumns);
186+
builder.append("EXCLUDE ")
187+
.append(parenthesized ? exceptColumns : exceptColumns.get(0));
154188
break;
155189
case REPLACE:
156-
builder.append("REPLACE").append(strict ? " STRICT" : "").append("(")
157-
.append(Select.getStringList(replaceItems)).append(")");
190+
builder.append("REPLACE").append(strict ? " STRICT" : "")
191+
.append(parenthesized ? "(" : " ")
192+
.append(Select.getStringList(replaceItems))
193+
.append(parenthesized ? ")" : "");
194+
break;
195+
case RENAME:
196+
builder.append("RENAME ").append(parenthesized ? "(" : "")
197+
.append(Select.getStringList(renameItems))
198+
.append(parenthesized ? ")" : "");
158199
break;
159200
default:
160201
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,38 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
16591659
|| next.kind >= MIN_NON_RESERVED_WORD && next.kind <= MAX_NON_RESERVED_WORD;
16601660
}
16611661

1662+
/**
1663+
* Whether the pending "," ends a select item list, as DuckDB allows: it is followed by a
1664+
* closing parenthesis or by a clause that cannot be read as a column such as limit or "group".
1665+
*/
1666+
private boolean isTrailingSelectItemCommaAhead() {
1667+
if (!",".equals(getToken(1).image)) {
1668+
return false;
1669+
}
1670+
Token next = getToken(2);
1671+
int following = getToken(3).kind;
1672+
switch (next.kind) {
1673+
case CLOSING_BRACKET:
1674+
case K_FROM: case K_WHERE: case K_HAVING: case K_WINDOW: case K_INTO:
1675+
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
1676+
return true;
1677+
case K_GROUP: case K_ORDER:
1678+
return following == K_BY;
1679+
case K_LIMIT: case K_OFFSET:
1680+
return following == S_LONG || following == OPENING_BRACKET
1681+
|| "?".equals(getToken(3).image);
1682+
default:
1683+
return false;
1684+
}
1685+
}
1686+
1687+
/** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */
1688+
private static void requireTransformerAlias(SelectItem<?> item) throws ParseException {
1689+
if (item.getAlias() == null) {
1690+
throw new ParseException("Expected AS and a name after " + item);
1691+
}
1692+
}
1693+
16621694
private boolean isAliasAhead() {
16631695
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
16641696
return false;
@@ -8562,11 +8594,13 @@ List<SelectItem<?>> SelectItemsList():
85628594
{
85638595
selectItem=SelectItem() { selectItemsList = new ArrayList<SelectItem<?>>(); selectItemsList.add(selectItem); }
85648596
(
8565-
LOOKAHEAD(2) "," selectItem=SelectItem()
8597+
LOOKAHEAD(2, { !isTrailingSelectItemCommaAhead() }) "," selectItem=SelectItem()
85668598
{
85678599
selectItemsList.add(selectItem);
85688600
}
85698601
)*
8602+
// DuckDB's trailing comma: SELECT a, b, FROM t
8603+
[ LOOKAHEAD({ isTrailingSelectItemCommaAhead() }) "," ]
85708604

85718605
{ return selectItemsList; }
85728606
}
@@ -8632,6 +8666,8 @@ boolean AllColumnsTransformerAhead() #void:
86328666
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
86338667
| <K_EXCLUDE> ParenthesedColumnList() )
86348668
{ return true; }
8669+
// DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)]
8670+
| LOOKAHEAD( <K_EXCLUDE> Column() | <K_REPLACE> | <K_RENAME> ) { return true; }
86358671
| { return false; }
86368672
}
86378673

@@ -8699,6 +8735,8 @@ ColumnsTransformer ColumnsTransformer():
86998735
ParenthesedExpressionList<Column> exceptColumns = null;
87008736
StringValue exceptPattern = null;
87018737
List<SelectItem<?>> replaceItems;
8738+
SelectItem<?> item;
8739+
Column column;
87028740
ColumnsTransformer transformer;
87038741
Token tk;
87048742
boolean strict;
@@ -8731,16 +8769,52 @@ ColumnsTransformer ColumnsTransformer():
87318769
transformer.setExceptColumns(exceptColumns);
87328770
} }
87338771

8734-
| <K_EXCLUDE> exceptColumns = ParenthesedColumnList()
8735-
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE)
8736-
.setExceptColumns(exceptColumns); }
8772+
| <K_EXCLUDE>
8773+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE); }
8774+
(
8775+
exceptColumns = ParenthesedColumnList()
8776+
|
8777+
column = Column()
8778+
{
8779+
exceptColumns = new ParenthesedExpressionList<Column>(column);
8780+
transformer.setParenthesized(false);
8781+
}
8782+
)
8783+
{ transformer.setExceptColumns(exceptColumns); }
87378784

87388785
| <K_REPLACE>
87398786
{ strict = false; }
8740-
[ <K_STRICT> { strict = true; } ]
8741-
"(" replaceItems = SelectItemsList() ")"
8787+
[ LOOKAHEAD({ getToken(1).kind == K_STRICT && "(".equals(getToken(2).image) })
8788+
<K_STRICT> { strict = true; } ]
87428789
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
8743-
.setStrict(strict).setReplaceItems(replaceItems); }
8790+
.setStrict(strict); }
8791+
(
8792+
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
8793+
|
8794+
item = SelectItem()
8795+
{
8796+
requireTransformerAlias(item);
8797+
replaceItems = new ArrayList<SelectItem<?>>();
8798+
replaceItems.add(item);
8799+
transformer.setParenthesized(false);
8800+
}
8801+
)
8802+
{ transformer.setReplaceItems(replaceItems); }
8803+
8804+
| <K_RENAME>
8805+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.RENAME); }
8806+
(
8807+
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
8808+
|
8809+
item = SelectItem()
8810+
{
8811+
requireTransformerAlias(item);
8812+
replaceItems = new ArrayList<SelectItem<?>>();
8813+
replaceItems.add(item);
8814+
transformer.setParenthesized(false);
8815+
}
8816+
)
8817+
{ transformer.setRenameItems(replaceItems); }
87448818
)
87458819
{ return transformer; }
87468820
}
@@ -8757,12 +8831,12 @@ AllColumns AllColumns(boolean allowAdditions):
87578831
}
87588832
{
87598833
"*"
8760-
// BigData allows EXCEPT, DuckDB allows EXCLUDE, ClickHouse allows APPLY;
8834+
// BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY;
87618835
// they may repeat and combine in any order
87628836
(
8763-
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
8837+
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> | <K_RENAME>
87648838
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
8765-
| <K_EXCLUDE> ParenthesedColumnList() )
8839+
| <K_EXCLUDE> ( ParenthesedColumnList() | Column() ) )
87668840
{
87678841
if (!allowAdditions || !AllColumnsTransformerAhead()) {
87688842
break;

‎src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,4 +986,42 @@ private static String tree(net.sf.jsqlparser.expression.Expression expression) {
986986
}
987987
return expression.toString();
988988
}
989+
990+
@Test
991+
void testStarRenameAndUnparenthesizedModifiers() throws JSQLParserException {
992+
for (String sqlStr : new String[] {"SELECT * RENAME (i AS x, j AS y) FROM t",
993+
"SELECT * RENAME i AS x FROM t", "SELECT * EXCLUDE i FROM t",
994+
"SELECT * EXCLUDE t.i FROM t", "SELECT * REPLACE i + 1 AS i FROM t",
995+
"SELECT * EXCLUDE (k) RENAME (i AS x) FROM t",
996+
"SELECT t.* RENAME t.i AS x FROM t",
997+
"SELECT COLUMNS(* REPLACE i + 1 AS i) FROM t"}) {
998+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
999+
}
1000+
1001+
PlainSelect select = (PlainSelect) CCJSqlParserUtil
1002+
.parse("SELECT * RENAME (i AS x) FROM t");
1003+
net.sf.jsqlparser.expression.ColumnsTransformer rename =
1004+
((net.sf.jsqlparser.statement.select.AllColumns) select.getSelectItem(0)
1005+
.getExpression()).getTransformers().get(0);
1006+
Assertions.assertEquals(
1007+
net.sf.jsqlparser.expression.ColumnsTransformer.ColumnsTransformerType.RENAME,
1008+
rename.getType());
1009+
Assertions.assertEquals("i AS x", rename.getRenameItems().get(0).toString());
1010+
}
1011+
1012+
@Test
1013+
void testTrailingCommaInSelectList() throws JSQLParserException {
1014+
Assertions.assertEquals("SELECT i, j FROM t WHERE i > 0",
1015+
CCJSqlParserUtil.parse("SELECT i, j, FROM t WHERE i > 0").toString());
1016+
Assertions.assertThrows(JSQLParserException.class,
1017+
() -> CCJSqlParserUtil.parse("SELECT * RENAME i FROM t"));
1018+
Assertions.assertEquals("SELECT 1 LIMIT 1",
1019+
CCJSqlParserUtil.parse("SELECT 1, LIMIT 1").toString());
1020+
Assertions.assertEquals("SELECT (SELECT 1)",
1021+
CCJSqlParserUtil.parse("SELECT (SELECT 1,)").toString());
1022+
// keywords that may name a column still do
1023+
for (String column : new String[] {"limit", "offset", "group", "order", "qualify"}) {
1024+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a, " + column + " FROM t", true);
1025+
}
1026+
}
9891027
}

0 commit comments

Comments
 (0)