Skip to content

Commit 6ec6e45

Browse files
committed
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.
1 parent f3330d5 commit 6ec6e45

3 files changed

Lines changed: 121 additions & 14 deletions

File tree

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,22 @@ public StringBuilder appendTo(StringBuilder builder) {
183183
.append(exceptPattern != null ? exceptPattern : exceptColumns);
184184
break;
185185
case EXCLUDE:
186-
builder.append("EXCLUDE ")
187-
.append(parenthesized ? exceptColumns : exceptColumns.get(0));
186+
builder.append("EXCLUDE ").append(parenthesized || exceptColumns.size() != 1
187+
? exceptColumns
188+
: exceptColumns.get(0));
188189
break;
189190
case REPLACE:
191+
boolean replaceParenthesized = parenthesized || replaceItems.size() != 1;
190192
builder.append("REPLACE").append(strict ? " STRICT" : "")
191-
.append(parenthesized ? "(" : " ")
193+
.append(replaceParenthesized ? "(" : " ")
192194
.append(Select.getStringList(replaceItems))
193-
.append(parenthesized ? ")" : "");
195+
.append(replaceParenthesized ? ")" : "");
194196
break;
195197
case RENAME:
196-
builder.append("RENAME ").append(parenthesized ? "(" : "")
198+
boolean renameParenthesized = parenthesized || renameItems.size() != 1;
199+
builder.append("RENAME ").append(renameParenthesized ? "(" : "")
197200
.append(Select.getStringList(renameItems))
198-
.append(parenthesized ? ")" : "");
201+
.append(renameParenthesized ? ")" : "");
199202
break;
200203
default:
201204
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);

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

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,15 +1729,103 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
17291729
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
17301730
return true;
17311731
case K_GROUP: case K_ORDER:
1732-
return following == K_BY;
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);
17331737
case K_LIMIT: case K_OFFSET:
1734-
return following == S_LONG || following == OPENING_BRACKET
1735-
|| "?".equals(getToken(3).image);
1738+
return following == S_LONG || "?".equals(getToken(3).image);
17361739
default:
17371740
return false;
17381741
}
17391742
}
17401743

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+
17411829
/** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */
17421830
private static void requireTransformerAlias(SelectItem<?> item) throws ParseException {
17431831
if (item.getAlias() == null) {
@@ -8799,7 +8887,8 @@ boolean AllColumnsTransformerAhead() #void:
87998887
| <K_EXCLUDE> ParenthesedColumnList() )
88008888
{ return true; }
88018889
// DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)]
8802-
| LOOKAHEAD( <K_EXCLUDE> Column() | <K_REPLACE> | <K_RENAME> ) { return true; }
8890+
| LOOKAHEAD( <K_EXCLUDE> Column() ) { return true; }
8891+
| LOOKAHEAD({ isUnparenthesizedReplaceAhead() || isRenameAhead() }) { return true; }
88038892
| { return false; }
88048893
}
88058894

@@ -8921,7 +9010,8 @@ ColumnsTransformer ColumnsTransformer():
89219010
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
89229011
.setStrict(strict); }
89239012
(
8924-
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
9013+
LOOKAHEAD({ "(".equals(getToken(1).image) && !isUnparenthesizedReplaceAhead(0) })
9014+
"(" replaceItems = SelectItemsList() ")"
89259015
|
89269016
item = SelectItem()
89279017
{
@@ -8966,6 +9056,7 @@ AllColumns AllColumns(boolean allowAdditions):
89669056
// BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY;
89679057
// they may repeat and combine in any order
89689058
(
9059+
// AllColumnsTransformerAhead() tells DuckDB's REPLACE and RENAME from an alias of the star
89699060
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> | <K_RENAME>
89709061
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
89719062
| <K_EXCLUDE> ( ParenthesedColumnList() | Column() ) )

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,18 @@ void testStarRenameAndUnparenthesizedModifiers() throws JSQLParserException {
10421042
net.sf.jsqlparser.expression.ColumnsTransformer.ColumnsTransformerType.RENAME,
10431043
rename.getType());
10441044
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+
((net.sf.jsqlparser.statement.select.AllColumns) select.getSelectItem(0)
1055+
.getExpression()).getTransformers().get(0).getReplaceItems().get(0)
1056+
.toString());
10451057
}
10461058

10471059
@Test
@@ -1054,9 +1066,10 @@ void testTrailingCommaInSelectList() throws JSQLParserException {
10541066
CCJSqlParserUtil.parse("SELECT 1, LIMIT 1").toString());
10551067
Assertions.assertEquals("SELECT (SELECT 1)",
10561068
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);
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);
10601073
}
10611074
}
10621075
}

0 commit comments

Comments
 (0)