Skip to content

Commit f3330d5

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 e7a5a43 commit f3330d5

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
@@ -1713,6 +1713,38 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
17131713
}
17141714
}
17151715

1716+
/**
1717+
* Whether the pending "," ends a select item list, as DuckDB allows: it is followed by a
1718+
* closing parenthesis or by a clause that cannot be read as a column such as limit or "group".
1719+
*/
1720+
private boolean isTrailingSelectItemCommaAhead() {
1721+
if (!",".equals(getToken(1).image)) {
1722+
return false;
1723+
}
1724+
Token next = getToken(2);
1725+
int following = getToken(3).kind;
1726+
switch (next.kind) {
1727+
case CLOSING_BRACKET:
1728+
case K_FROM: case K_WHERE: case K_HAVING: case K_WINDOW: case K_INTO:
1729+
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
1730+
return true;
1731+
case K_GROUP: case K_ORDER:
1732+
return following == K_BY;
1733+
case K_LIMIT: case K_OFFSET:
1734+
return following == S_LONG || following == OPENING_BRACKET
1735+
|| "?".equals(getToken(3).image);
1736+
default:
1737+
return false;
1738+
}
1739+
}
1740+
1741+
/** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */
1742+
private static void requireTransformerAlias(SelectItem<?> item) throws ParseException {
1743+
if (item.getAlias() == null) {
1744+
throw new ParseException("Expected AS and a name after " + item);
1745+
}
1746+
}
1747+
17161748
private boolean isAliasAhead() {
17171749
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
17181750
return false;
@@ -8694,11 +8726,13 @@ List<SelectItem<?>> SelectItemsList():
86948726
{
86958727
selectItem=SelectItem() { selectItemsList = new ArrayList<SelectItem<?>>(); selectItemsList.add(selectItem); }
86968728
(
8697-
LOOKAHEAD(2) "," selectItem=SelectItem()
8729+
LOOKAHEAD(2, { !isTrailingSelectItemCommaAhead() }) "," selectItem=SelectItem()
86988730
{
86998731
selectItemsList.add(selectItem);
87008732
}
87018733
)*
8734+
// DuckDB's trailing comma: SELECT a, b, FROM t
8735+
[ LOOKAHEAD({ isTrailingSelectItemCommaAhead() }) "," ]
87028736

87038737
{ return selectItemsList; }
87048738
}
@@ -8764,6 +8798,8 @@ boolean AllColumnsTransformerAhead() #void:
87648798
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
87658799
| <K_EXCLUDE> ParenthesedColumnList() )
87668800
{ return true; }
8801+
// DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)]
8802+
| LOOKAHEAD( <K_EXCLUDE> Column() | <K_REPLACE> | <K_RENAME> ) { return true; }
87678803
| { return false; }
87688804
}
87698805

@@ -8831,6 +8867,8 @@ ColumnsTransformer ColumnsTransformer():
88318867
ParenthesedExpressionList<Column> exceptColumns = null;
88328868
StringValue exceptPattern = null;
88338869
List<SelectItem<?>> replaceItems;
8870+
SelectItem<?> item;
8871+
Column column;
88348872
ColumnsTransformer transformer;
88358873
Token tk;
88368874
boolean strict;
@@ -8863,16 +8901,52 @@ ColumnsTransformer ColumnsTransformer():
88638901
transformer.setExceptColumns(exceptColumns);
88648902
} }
88658903

8866-
| <K_EXCLUDE> exceptColumns = ParenthesedColumnList()
8867-
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE)
8868-
.setExceptColumns(exceptColumns); }
8904+
| <K_EXCLUDE>
8905+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE); }
8906+
(
8907+
exceptColumns = ParenthesedColumnList()
8908+
|
8909+
column = Column()
8910+
{
8911+
exceptColumns = new ParenthesedExpressionList<Column>(column);
8912+
transformer.setParenthesized(false);
8913+
}
8914+
)
8915+
{ transformer.setExceptColumns(exceptColumns); }
88698916

88708917
| <K_REPLACE>
88718918
{ strict = false; }
8872-
[ <K_STRICT> { strict = true; } ]
8873-
"(" replaceItems = SelectItemsList() ")"
8919+
[ LOOKAHEAD({ getToken(1).kind == K_STRICT && "(".equals(getToken(2).image) })
8920+
<K_STRICT> { strict = true; } ]
88748921
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
8875-
.setStrict(strict).setReplaceItems(replaceItems); }
8922+
.setStrict(strict); }
8923+
(
8924+
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
8925+
|
8926+
item = SelectItem()
8927+
{
8928+
requireTransformerAlias(item);
8929+
replaceItems = new ArrayList<SelectItem<?>>();
8930+
replaceItems.add(item);
8931+
transformer.setParenthesized(false);
8932+
}
8933+
)
8934+
{ transformer.setReplaceItems(replaceItems); }
8935+
8936+
| <K_RENAME>
8937+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.RENAME); }
8938+
(
8939+
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
8940+
|
8941+
item = SelectItem()
8942+
{
8943+
requireTransformerAlias(item);
8944+
replaceItems = new ArrayList<SelectItem<?>>();
8945+
replaceItems.add(item);
8946+
transformer.setParenthesized(false);
8947+
}
8948+
)
8949+
{ transformer.setRenameItems(replaceItems); }
88768950
)
88778951
{ return transformer; }
88788952
}
@@ -8889,12 +8963,12 @@ AllColumns AllColumns(boolean allowAdditions):
88898963
}
88908964
{
88918965
"*"
8892-
// BigData allows EXCEPT, DuckDB allows EXCLUDE, ClickHouse allows APPLY;
8966+
// BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY;
88938967
// they may repeat and combine in any order
88948968
(
8895-
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
8969+
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> | <K_RENAME>
88968970
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
8897-
| <K_EXCLUDE> ParenthesedColumnList() )
8971+
| <K_EXCLUDE> ( ParenthesedColumnList() | Column() ) )
88988972
{
88998973
if (!allowAdditions || !AllColumnsTransformerAhead()) {
89008974
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
@@ -1021,4 +1021,42 @@ void testLargeArrayIsScannedOnce() throws JSQLParserException {
10211021
}
10221022
CCJSqlParserUtil.parse(sql.append("]").toString());
10231023
}
1024+
1025+
@Test
1026+
void testStarRenameAndUnparenthesizedModifiers() throws JSQLParserException {
1027+
for (String sqlStr : new String[] {"SELECT * RENAME (i AS x, j AS y) FROM t",
1028+
"SELECT * RENAME i AS x FROM t", "SELECT * EXCLUDE i FROM t",
1029+
"SELECT * EXCLUDE t.i FROM t", "SELECT * REPLACE i + 1 AS i FROM t",
1030+
"SELECT * EXCLUDE (k) RENAME (i AS x) FROM t",
1031+
"SELECT t.* RENAME t.i AS x FROM t",
1032+
"SELECT COLUMNS(* REPLACE i + 1 AS i) FROM t"}) {
1033+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
1034+
}
1035+
1036+
PlainSelect select = (PlainSelect) CCJSqlParserUtil
1037+
.parse("SELECT * RENAME (i AS x) FROM t");
1038+
net.sf.jsqlparser.expression.ColumnsTransformer rename =
1039+
((net.sf.jsqlparser.statement.select.AllColumns) select.getSelectItem(0)
1040+
.getExpression()).getTransformers().get(0);
1041+
Assertions.assertEquals(
1042+
net.sf.jsqlparser.expression.ColumnsTransformer.ColumnsTransformerType.RENAME,
1043+
rename.getType());
1044+
Assertions.assertEquals("i AS x", rename.getRenameItems().get(0).toString());
1045+
}
1046+
1047+
@Test
1048+
void testTrailingCommaInSelectList() throws JSQLParserException {
1049+
Assertions.assertEquals("SELECT i, j FROM t WHERE i > 0",
1050+
CCJSqlParserUtil.parse("SELECT i, j, FROM t WHERE i > 0").toString());
1051+
Assertions.assertThrows(JSQLParserException.class,
1052+
() -> CCJSqlParserUtil.parse("SELECT * RENAME i FROM t"));
1053+
Assertions.assertEquals("SELECT 1 LIMIT 1",
1054+
CCJSqlParserUtil.parse("SELECT 1, LIMIT 1").toString());
1055+
Assertions.assertEquals("SELECT (SELECT 1)",
1056+
CCJSqlParserUtil.parse("SELECT (SELECT 1,)").toString());
1057+
// keywords that may name a column still do
1058+
for (String column : new String[] {"limit", "offset", "group", "order", "qualify"}) {
1059+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a, " + column + " FROM t", true);
1060+
}
1061+
}
10241062
}

0 commit comments

Comments
 (0)