Skip to content

Commit f7d84c8

Browse files
authored
DuckDB: support * RENAME, unparenthesized star modifiers and a trailing comma (#2801)
* 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. * Keep stars aliased rename or replace and functions named limit - Unparenthesized REPLACE needs an AS at the top level of its item, and RENAME needs `column AS name` for each item, so `* rename` and `* replace` stay aliases of the star. - `REPLACE (a + 1) AS a` is a single item, as in DuckDB, not a list without an alias. - A comma before `limit(` or `offset(` starts a select item again, and `group by` / `order by` right before FROM or the end are a column and its alias. - A transformer with more than one item or column prints parentheses. * Refer to `AllColumns` by its simple name in `DuckDBTest`
1 parent e7a5a43 commit f7d84c8

3 files changed

Lines changed: 274 additions & 14 deletions

File tree

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

Lines changed: 48 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,22 @@ 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 ").append(parenthesized || exceptColumns.size() != 1
187+
? exceptColumns
188+
: exceptColumns.get(0));
154189
break;
155190
case REPLACE:
156-
builder.append("REPLACE").append(strict ? " STRICT" : "").append("(")
157-
.append(Select.getStringList(replaceItems)).append(")");
191+
boolean replaceParenthesized = parenthesized || replaceItems.size() != 1;
192+
builder.append("REPLACE").append(strict ? " STRICT" : "")
193+
.append(replaceParenthesized ? "(" : " ")
194+
.append(Select.getStringList(replaceItems))
195+
.append(replaceParenthesized ? ")" : "");
196+
break;
197+
case RENAME:
198+
boolean renameParenthesized = parenthesized || renameItems.size() != 1;
199+
builder.append("RENAME ").append(renameParenthesized ? "(" : "")
200+
.append(Select.getStringList(renameItems))
201+
.append(renameParenthesized ? ")" : "");
158202
break;
159203
default:
160204
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);

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

Lines changed: 175 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,6 +1713,126 @@ 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+
// not a column named group aliased by: SELECT a, group by FROM t
1733+
int afterBy = getToken(4).kind;
1734+
return following == K_BY && afterBy != K_FROM && afterBy != EOF
1735+
&& afterBy != CLOSING_BRACKET && afterBy != ST_SEMICOLON
1736+
&& !",".equals(getToken(4).image);
1737+
case K_LIMIT: case K_OFFSET:
1738+
return following == S_LONG || "?".equals(getToken(3).image);
1739+
default:
1740+
return false;
1741+
}
1742+
}
1743+
1744+
/**
1745+
* Whether the REPLACE at {@code offset} is DuckDB's unparenthesized {@code REPLACE expr AS name}:
1746+
* an AS follows at the top level before the end of the select item, so that a star aliased
1747+
* "replace" keeps parsing. With {@code offset} 0, the REPLACE has been consumed.
1748+
*/
1749+
private boolean isUnparenthesizedReplaceAhead(int offset) {
1750+
Token token = getToken(offset);
1751+
if (offset > 0 && token.kind != K_REPLACE) {
1752+
return false;
1753+
}
1754+
int depth = 0;
1755+
while (true) {
1756+
token = nextWithItemToken(token);
1757+
if (token.kind == EOF || ";".equals(token.image)) {
1758+
return false;
1759+
}
1760+
if ("(".equals(token.image) || "[".equals(token.image) || "{".equals(token.image)) {
1761+
depth++;
1762+
} else if (")".equals(token.image) || "]".equals(token.image)
1763+
|| "}".equals(token.image)) {
1764+
if (--depth < 0) {
1765+
return false;
1766+
}
1767+
} else if (depth == 0) {
1768+
if (token.kind == K_AS) {
1769+
return true;
1770+
}
1771+
if (token.kind == K_FROM || ",".equals(token.image)) {
1772+
return false;
1773+
}
1774+
}
1775+
}
1776+
}
1777+
1778+
private boolean isUnparenthesizedReplaceAhead() {
1779+
return isUnparenthesizedReplaceAhead(1);
1780+
}
1781+
1782+
/**
1783+
* Whether DuckDB's {@code RENAME (column AS name, ...)} or {@code RENAME column AS name}
1784+
* follows, every column being renamed with AS, so that a star aliased "rename" keeps parsing.
1785+
*/
1786+
private boolean isRenameAhead() {
1787+
Token token = getToken(1);
1788+
if (token.kind != K_RENAME) {
1789+
return false;
1790+
}
1791+
token = nextWithItemToken(token);
1792+
boolean parenthesized = "(".equals(token.image);
1793+
if (parenthesized) {
1794+
token = nextWithItemToken(token);
1795+
}
1796+
while (true) {
1797+
// column [. column]* AS name
1798+
while (true) {
1799+
if (!isIdentifierishNameToken(token)) {
1800+
return false;
1801+
}
1802+
token = nextWithItemToken(token);
1803+
if (!".".equals(token.image)) {
1804+
break;
1805+
}
1806+
token = nextWithItemToken(token);
1807+
}
1808+
if (token.kind != K_AS) {
1809+
return false;
1810+
}
1811+
token = nextWithItemToken(token);
1812+
if (!isIdentifierishNameToken(token)) {
1813+
return false;
1814+
}
1815+
token = nextWithItemToken(token);
1816+
if (!parenthesized) {
1817+
return true;
1818+
}
1819+
if (")".equals(token.image)) {
1820+
return true;
1821+
}
1822+
if (!",".equals(token.image)) {
1823+
return false;
1824+
}
1825+
token = nextWithItemToken(token);
1826+
}
1827+
}
1828+
1829+
/** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */
1830+
private static void requireTransformerAlias(SelectItem<?> item) throws ParseException {
1831+
if (item.getAlias() == null) {
1832+
throw new ParseException("Expected AS and a name after " + item);
1833+
}
1834+
}
1835+
17161836
private boolean isAliasAhead() {
17171837
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
17181838
return false;
@@ -8694,11 +8814,13 @@ List<SelectItem<?>> SelectItemsList():
86948814
{
86958815
selectItem=SelectItem() { selectItemsList = new ArrayList<SelectItem<?>>(); selectItemsList.add(selectItem); }
86968816
(
8697-
LOOKAHEAD(2) "," selectItem=SelectItem()
8817+
LOOKAHEAD(2, { !isTrailingSelectItemCommaAhead() }) "," selectItem=SelectItem()
86988818
{
86998819
selectItemsList.add(selectItem);
87008820
}
87018821
)*
8822+
// DuckDB's trailing comma: SELECT a, b, FROM t
8823+
[ LOOKAHEAD({ isTrailingSelectItemCommaAhead() }) "," ]
87028824

87038825
{ return selectItemsList; }
87048826
}
@@ -8764,6 +8886,9 @@ boolean AllColumnsTransformerAhead() #void:
87648886
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
87658887
| <K_EXCLUDE> ParenthesedColumnList() )
87668888
{ return true; }
8889+
// DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)]
8890+
| LOOKAHEAD( <K_EXCLUDE> Column() ) { return true; }
8891+
| LOOKAHEAD({ isUnparenthesizedReplaceAhead() || isRenameAhead() }) { return true; }
87678892
| { return false; }
87688893
}
87698894

@@ -8831,6 +8956,8 @@ ColumnsTransformer ColumnsTransformer():
88318956
ParenthesedExpressionList<Column> exceptColumns = null;
88328957
StringValue exceptPattern = null;
88338958
List<SelectItem<?>> replaceItems;
8959+
SelectItem<?> item;
8960+
Column column;
88348961
ColumnsTransformer transformer;
88358962
Token tk;
88368963
boolean strict;
@@ -8863,16 +8990,53 @@ ColumnsTransformer ColumnsTransformer():
88638990
transformer.setExceptColumns(exceptColumns);
88648991
} }
88658992

8866-
| <K_EXCLUDE> exceptColumns = ParenthesedColumnList()
8867-
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE)
8868-
.setExceptColumns(exceptColumns); }
8993+
| <K_EXCLUDE>
8994+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE); }
8995+
(
8996+
exceptColumns = ParenthesedColumnList()
8997+
|
8998+
column = Column()
8999+
{
9000+
exceptColumns = new ParenthesedExpressionList<Column>(column);
9001+
transformer.setParenthesized(false);
9002+
}
9003+
)
9004+
{ transformer.setExceptColumns(exceptColumns); }
88699005

88709006
| <K_REPLACE>
88719007
{ strict = false; }
8872-
[ <K_STRICT> { strict = true; } ]
8873-
"(" replaceItems = SelectItemsList() ")"
9008+
[ LOOKAHEAD({ getToken(1).kind == K_STRICT && "(".equals(getToken(2).image) })
9009+
<K_STRICT> { strict = true; } ]
88749010
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
8875-
.setStrict(strict).setReplaceItems(replaceItems); }
9011+
.setStrict(strict); }
9012+
(
9013+
LOOKAHEAD({ "(".equals(getToken(1).image) && !isUnparenthesizedReplaceAhead(0) })
9014+
"(" replaceItems = SelectItemsList() ")"
9015+
|
9016+
item = SelectItem()
9017+
{
9018+
requireTransformerAlias(item);
9019+
replaceItems = new ArrayList<SelectItem<?>>();
9020+
replaceItems.add(item);
9021+
transformer.setParenthesized(false);
9022+
}
9023+
)
9024+
{ transformer.setReplaceItems(replaceItems); }
9025+
9026+
| <K_RENAME>
9027+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.RENAME); }
9028+
(
9029+
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
9030+
|
9031+
item = SelectItem()
9032+
{
9033+
requireTransformerAlias(item);
9034+
replaceItems = new ArrayList<SelectItem<?>>();
9035+
replaceItems.add(item);
9036+
transformer.setParenthesized(false);
9037+
}
9038+
)
9039+
{ transformer.setRenameItems(replaceItems); }
88769040
)
88779041
{ return transformer; }
88789042
}
@@ -8889,12 +9053,13 @@ AllColumns AllColumns(boolean allowAdditions):
88899053
}
88909054
{
88919055
"*"
8892-
// BigData allows EXCEPT, DuckDB allows EXCLUDE, ClickHouse allows APPLY;
9056+
// BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY;
88939057
// they may repeat and combine in any order
88949058
(
8895-
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
9059+
// AllColumnsTransformerAhead() tells DuckDB's REPLACE and RENAME from an alias of the star
9060+
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> | <K_RENAME>
88969061
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
8897-
| <K_EXCLUDE> ParenthesedColumnList() )
9062+
| <K_EXCLUDE> ( ParenthesedColumnList() | Column() ) )
88989063
{
88999064
if (!allowAdditions || !AllColumnsTransformerAhead()) {
89009065
break;

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,4 +1021,55 @@ 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+
((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+
// a star aliased rename or replace keeps parsing
1047+
for (String sqlStr : new String[] {"SELECT * rename FROM t", "SELECT * replace FROM t",
1048+
"SELECT t.* rename, b FROM t", "SELECT * rename(a, b) FROM t"}) {
1049+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
1050+
}
1051+
// DuckDB reads a parenthesized expression followed by AS as the single item
1052+
select = (PlainSelect) CCJSqlParserUtil.parse("SELECT * REPLACE (a + 1) AS a FROM t");
1053+
Assertions.assertEquals("(a + 1) AS a",
1054+
((AllColumns) select.getSelectItem(0)
1055+
.getExpression()).getTransformers().get(0).getReplaceItems().get(0)
1056+
.toString());
1057+
}
1058+
1059+
@Test
1060+
void testTrailingCommaInSelectList() throws JSQLParserException {
1061+
Assertions.assertEquals("SELECT i, j FROM t WHERE i > 0",
1062+
CCJSqlParserUtil.parse("SELECT i, j, FROM t WHERE i > 0").toString());
1063+
Assertions.assertThrows(JSQLParserException.class,
1064+
() -> CCJSqlParserUtil.parse("SELECT * RENAME i FROM t"));
1065+
Assertions.assertEquals("SELECT 1 LIMIT 1",
1066+
CCJSqlParserUtil.parse("SELECT 1, LIMIT 1").toString());
1067+
Assertions.assertEquals("SELECT (SELECT 1)",
1068+
CCJSqlParserUtil.parse("SELECT (SELECT 1,)").toString());
1069+
// keywords that may name a column or a function still do
1070+
for (String item : new String[] {"limit", "offset", "group", "order", "qualify",
1071+
"limit(1)", "offset(1)", "group by", "order by"}) {
1072+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a, " + item + " FROM t", true);
1073+
}
1074+
}
10241075
}

0 commit comments

Comments
 (0)