Skip to content

Commit a45ab0f

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 576e098 commit a45ab0f

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
@@ -1675,15 +1675,103 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
16751675
case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS:
16761676
return true;
16771677
case K_GROUP: case K_ORDER:
1678-
return following == K_BY;
1678+
// not a column named group aliased by: SELECT a, group by FROM t
1679+
int afterBy = getToken(4).kind;
1680+
return following == K_BY && afterBy != K_FROM && afterBy != EOF
1681+
&& afterBy != CLOSING_BRACKET && afterBy != ST_SEMICOLON
1682+
&& !",".equals(getToken(4).image);
16791683
case K_LIMIT: case K_OFFSET:
1680-
return following == S_LONG || following == OPENING_BRACKET
1681-
|| "?".equals(getToken(3).image);
1684+
return following == S_LONG || "?".equals(getToken(3).image);
16821685
default:
16831686
return false;
16841687
}
16851688
}
16861689

1690+
/**
1691+
* Whether the REPLACE at {@code offset} is DuckDB's unparenthesized {@code REPLACE expr AS name}:
1692+
* an AS follows at the top level before the end of the select item, so that a star aliased
1693+
* "replace" keeps parsing. With {@code offset} 0, the REPLACE has been consumed.
1694+
*/
1695+
private boolean isUnparenthesizedReplaceAhead(int offset) {
1696+
Token token = getToken(offset);
1697+
if (offset > 0 && token.kind != K_REPLACE) {
1698+
return false;
1699+
}
1700+
int depth = 0;
1701+
while (true) {
1702+
token = nextWithItemToken(token);
1703+
if (token.kind == EOF || ";".equals(token.image)) {
1704+
return false;
1705+
}
1706+
if ("(".equals(token.image) || "[".equals(token.image) || "{".equals(token.image)) {
1707+
depth++;
1708+
} else if (")".equals(token.image) || "]".equals(token.image)
1709+
|| "}".equals(token.image)) {
1710+
if (--depth < 0) {
1711+
return false;
1712+
}
1713+
} else if (depth == 0) {
1714+
if (token.kind == K_AS) {
1715+
return true;
1716+
}
1717+
if (token.kind == K_FROM || ",".equals(token.image)) {
1718+
return false;
1719+
}
1720+
}
1721+
}
1722+
}
1723+
1724+
private boolean isUnparenthesizedReplaceAhead() {
1725+
return isUnparenthesizedReplaceAhead(1);
1726+
}
1727+
1728+
/**
1729+
* Whether DuckDB's {@code RENAME (column AS name, ...)} or {@code RENAME column AS name}
1730+
* follows, every column being renamed with AS, so that a star aliased "rename" keeps parsing.
1731+
*/
1732+
private boolean isRenameAhead() {
1733+
Token token = getToken(1);
1734+
if (token.kind != K_RENAME) {
1735+
return false;
1736+
}
1737+
token = nextWithItemToken(token);
1738+
boolean parenthesized = "(".equals(token.image);
1739+
if (parenthesized) {
1740+
token = nextWithItemToken(token);
1741+
}
1742+
while (true) {
1743+
// column [. column]* AS name
1744+
while (true) {
1745+
if (!isIdentifierishNameToken(token)) {
1746+
return false;
1747+
}
1748+
token = nextWithItemToken(token);
1749+
if (!".".equals(token.image)) {
1750+
break;
1751+
}
1752+
token = nextWithItemToken(token);
1753+
}
1754+
if (token.kind != K_AS) {
1755+
return false;
1756+
}
1757+
token = nextWithItemToken(token);
1758+
if (!isIdentifierishNameToken(token)) {
1759+
return false;
1760+
}
1761+
token = nextWithItemToken(token);
1762+
if (!parenthesized) {
1763+
return true;
1764+
}
1765+
if (")".equals(token.image)) {
1766+
return true;
1767+
}
1768+
if (!",".equals(token.image)) {
1769+
return false;
1770+
}
1771+
token = nextWithItemToken(token);
1772+
}
1773+
}
1774+
16871775
/** DuckDB's unparenthesized REPLACE and RENAME name their single item: * RENAME i AS x. */
16881776
private static void requireTransformerAlias(SelectItem<?> item) throws ParseException {
16891777
if (item.getAlias() == null) {
@@ -8667,7 +8755,8 @@ boolean AllColumnsTransformerAhead() #void:
86678755
| <K_EXCLUDE> ParenthesedColumnList() )
86688756
{ return true; }
86698757
// DuckDB: * EXCLUDE i, * REPLACE expr AS i, * RENAME [(]i AS x[)]
8670-
| LOOKAHEAD( <K_EXCLUDE> Column() | <K_REPLACE> | <K_RENAME> ) { return true; }
8758+
| LOOKAHEAD( <K_EXCLUDE> Column() ) { return true; }
8759+
| LOOKAHEAD({ isUnparenthesizedReplaceAhead() || isRenameAhead() }) { return true; }
86718760
| { return false; }
86728761
}
86738762

@@ -8789,7 +8878,8 @@ ColumnsTransformer ColumnsTransformer():
87898878
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
87908879
.setStrict(strict); }
87918880
(
8792-
LOOKAHEAD("(") "(" replaceItems = SelectItemsList() ")"
8881+
LOOKAHEAD({ "(".equals(getToken(1).image) && !isUnparenthesizedReplaceAhead(0) })
8882+
"(" replaceItems = SelectItemsList() ")"
87938883
|
87948884
item = SelectItem()
87958885
{
@@ -8834,6 +8924,7 @@ AllColumns AllColumns(boolean allowAdditions):
88348924
// BigData allows EXCEPT, DuckDB allows EXCLUDE and RENAME, ClickHouse allows APPLY;
88358925
// they may repeat and combine in any order
88368926
(
8927+
// AllColumnsTransformerAhead() tells DuckDB's REPLACE and RENAME from an alias of the star
88378928
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> | <K_RENAME>
88388929
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
88398930
| <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
@@ -1007,6 +1007,18 @@ void testStarRenameAndUnparenthesizedModifiers() throws JSQLParserException {
10071007
net.sf.jsqlparser.expression.ColumnsTransformer.ColumnsTransformerType.RENAME,
10081008
rename.getType());
10091009
Assertions.assertEquals("i AS x", rename.getRenameItems().get(0).toString());
1010+
1011+
// a star aliased rename or replace keeps parsing
1012+
for (String sqlStr : new String[] {"SELECT * rename FROM t", "SELECT * replace FROM t",
1013+
"SELECT t.* rename, b FROM t", "SELECT * rename(a, b) FROM t"}) {
1014+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
1015+
}
1016+
// DuckDB reads a parenthesized expression followed by AS as the single item
1017+
select = (PlainSelect) CCJSqlParserUtil.parse("SELECT * REPLACE (a + 1) AS a FROM t");
1018+
Assertions.assertEquals("(a + 1) AS a",
1019+
((net.sf.jsqlparser.statement.select.AllColumns) select.getSelectItem(0)
1020+
.getExpression()).getTransformers().get(0).getReplaceItems().get(0)
1021+
.toString());
10101022
}
10111023

10121024
@Test
@@ -1019,9 +1031,10 @@ void testTrailingCommaInSelectList() throws JSQLParserException {
10191031
CCJSqlParserUtil.parse("SELECT 1, LIMIT 1").toString());
10201032
Assertions.assertEquals("SELECT (SELECT 1)",
10211033
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);
1034+
// keywords that may name a column or a function still do
1035+
for (String item : new String[] {"limit", "offset", "group", "order", "qualify",
1036+
"limit(1)", "offset(1)", "group by", "order by"}) {
1037+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a, " + item + " FROM t", true);
10251038
}
10261039
}
10271040
}

0 commit comments

Comments
 (0)