Skip to content

Commit fc46acc

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 aa9ecfe commit fc46acc

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
@@ -1689,6 +1689,38 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
16891689
return ":".equals(token.image) || "]".equals(token.image) || ",".equals(token.image);
16901690
}
16911691

1692+
/**
1693+
* Whether the pending "," ends a select item list, as DuckDB allows: it is followed by a
1694+
* closing parenthesis or by a clause that cannot be read as a column such as limit or "group".
1695+
*/
1696+
private boolean isTrailingSelectItemCommaAhead() {
1697+
if (!",".equals(getToken(1).image)) {
1698+
return false;
1699+
}
1700+
Token next = getToken(2);
1701+
int following = getToken(3).kind;
1702+
switch (next.kind) {
1703+
case CLOSING_BRACKET:
1704+
case K_FROM: case K_WHERE: case K_HAVING: case K_WINDOW: case K_INTO:
1705+
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
1706+
return true;
1707+
case K_GROUP: case K_ORDER:
1708+
return following == K_BY;
1709+
case K_LIMIT: case K_OFFSET:
1710+
return following == S_LONG || following == OPENING_BRACKET
1711+
|| "?".equals(getToken(3).image);
1712+
default:
1713+
return false;
1714+
}
1715+
}
1716+
1717+
/** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */
1718+
private static void requireTransformerAlias(SelectItem<?> item) throws ParseException {
1719+
if (item.getAlias() == null) {
1720+
throw new ParseException("Expected AS and a name after " + item);
1721+
}
1722+
}
1723+
16921724
private boolean isAliasAhead() {
16931725
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
16941726
return false;
@@ -8592,11 +8624,13 @@ List<SelectItem<?>> SelectItemsList():
85928624
{
85938625
selectItem=SelectItem() { selectItemsList = new ArrayList<SelectItem<?>>(); selectItemsList.add(selectItem); }
85948626
(
8595-
LOOKAHEAD(2) "," selectItem=SelectItem()
8627+
LOOKAHEAD(2, { !isTrailingSelectItemCommaAhead() }) "," selectItem=SelectItem()
85968628
{
85978629
selectItemsList.add(selectItem);
85988630
}
85998631
)*
8632+
// DuckDB's trailing comma: SELECT a, b, FROM t
8633+
[ LOOKAHEAD({ isTrailingSelectItemCommaAhead() }) "," ]
86008634

86018635
{ return selectItemsList; }
86028636
}
@@ -8662,6 +8696,8 @@ boolean AllColumnsTransformerAhead() #void:
86628696
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
86638697
| <K_EXCLUDE> ParenthesedColumnList() )
86648698
{ return true; }
8699+
// DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)]
8700+
| LOOKAHEAD( <K_EXCLUDE> Column() | <K_REPLACE> | <K_RENAME> ) { return true; }
86658701
| { return false; }
86668702
}
86678703

@@ -8729,6 +8765,8 @@ ColumnsTransformer ColumnsTransformer():
87298765
ParenthesedExpressionList<Column> exceptColumns = null;
87308766
StringValue exceptPattern = null;
87318767
List<SelectItem<?>> replaceItems;
8768+
SelectItem<?> item;
8769+
Column column;
87328770
ColumnsTransformer transformer;
87338771
Token tk;
87348772
boolean strict;
@@ -8761,16 +8799,52 @@ ColumnsTransformer ColumnsTransformer():
87618799
transformer.setExceptColumns(exceptColumns);
87628800
} }
87638801

8764-
| <K_EXCLUDE> exceptColumns = ParenthesedColumnList()
8765-
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE)
8766-
.setExceptColumns(exceptColumns); }
8802+
| <K_EXCLUDE>
8803+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE); }
8804+
(
8805+
exceptColumns = ParenthesedColumnList()
8806+
|
8807+
column = Column()
8808+
{
8809+
exceptColumns = new ParenthesedExpressionList<Column>(column);
8810+
transformer.setParenthesized(false);
8811+
}
8812+
)
8813+
{ transformer.setExceptColumns(exceptColumns); }
87678814

87688815
| <K_REPLACE>
87698816
{ strict = false; }
8770-
[ <K_STRICT> { strict = true; } ]
8771-
"(" replaceItems = SelectItemsList() ")"
8817+
[ LOOKAHEAD({ getToken(1).kind == K_STRICT && "(".equals(getToken(2).image) })
8818+
<K_STRICT> { strict = true; } ]
87728819
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
8773-
.setStrict(strict).setReplaceItems(replaceItems); }
8820+
.setStrict(strict); }
8821+
(
8822+
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
8823+
|
8824+
item = SelectItem()
8825+
{
8826+
requireTransformerAlias(item);
8827+
replaceItems = new ArrayList<SelectItem<?>>();
8828+
replaceItems.add(item);
8829+
transformer.setParenthesized(false);
8830+
}
8831+
)
8832+
{ transformer.setReplaceItems(replaceItems); }
8833+
8834+
| <K_RENAME>
8835+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.RENAME); }
8836+
(
8837+
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
8838+
|
8839+
item = SelectItem()
8840+
{
8841+
requireTransformerAlias(item);
8842+
replaceItems = new ArrayList<SelectItem<?>>();
8843+
replaceItems.add(item);
8844+
transformer.setParenthesized(false);
8845+
}
8846+
)
8847+
{ transformer.setRenameItems(replaceItems); }
87748848
)
87758849
{ return transformer; }
87768850
}
@@ -8787,12 +8861,12 @@ AllColumns AllColumns(boolean allowAdditions):
87878861
}
87888862
{
87898863
"*"
8790-
// BigData allows EXCEPT, DuckDB allows EXCLUDE, ClickHouse allows APPLY;
8864+
// BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY;
87918865
// they may repeat and combine in any order
87928866
(
8793-
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
8867+
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> | <K_RENAME>
87948868
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
8795-
| <K_EXCLUDE> ParenthesedColumnList() )
8869+
| <K_EXCLUDE> ( ParenthesedColumnList() | Column() ) )
87968870
{
87978871
if (!allowAdditions || !AllColumnsTransformerAhead()) {
87988872
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)