Skip to content

Commit 9dd197a

Browse files
hayssamsclaude
andcommitted
Support BigQuery UNNEST ... WITH OFFSET after the alias
BigQuery writes UNNEST(array) [AS alias] WITH OFFSET [AS offsetAlias], where WITH OFFSET follows the table alias and carries an alias of its own. The pre-existing pre-alias form (WITH ORDINALITY|OFFSET AS t(a, b)) is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent 1ec22b0 commit 9dd197a

3 files changed

Lines changed: 74 additions & 0 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/select/TableFunction.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class TableFunction extends Function implements FromItem {
2424
private Function function;
2525
private ParenthesedExpressionList<Function> rowsFromFunctions;
2626
private String withClause = null;
27+
private boolean withOffset = false;
28+
private Alias offsetAlias = null;
2729

2830
public TableFunction(Function function) {
2931
this.function = function;
@@ -121,6 +123,37 @@ public TableFunction setPrefix(String prefix) {
121123
return this;
122124
}
123125

126+
/**
127+
* BigQuery's {@code UNNEST(array) [AS alias] WITH OFFSET [AS offsetAlias]}, where the
128+
* {@code WITH OFFSET} clause follows the table alias and carries an alias of its own.
129+
*/
130+
public boolean isWithOffset() {
131+
return withOffset;
132+
}
133+
134+
public void setWithOffset(boolean withOffset) {
135+
this.withOffset = withOffset;
136+
}
137+
138+
public TableFunction withWithOffset(boolean withOffset) {
139+
setWithOffset(withOffset);
140+
return this;
141+
}
142+
143+
public Alias getOffsetAlias() {
144+
return offsetAlias;
145+
}
146+
147+
public void setOffsetAlias(Alias offsetAlias) {
148+
this.offsetAlias = offsetAlias;
149+
this.withOffset = true;
150+
}
151+
152+
public TableFunction withOffsetAlias(Alias offsetAlias) {
153+
setOffsetAlias(offsetAlias);
154+
return this;
155+
}
156+
124157
public String getWithClause() {
125158
return withClause;
126159
}
@@ -211,6 +244,13 @@ public StringBuilder appendTo(StringBuilder builder) {
211244
if (alias != null) {
212245
builder.append(alias);
213246
}
247+
248+
if (withOffset) {
249+
builder.append(" WITH OFFSET");
250+
if (offsetAlias != null) {
251+
builder.append(offsetAlias);
252+
}
253+
}
214254
return builder;
215255
}
216256

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7642,6 +7642,13 @@ FromItem FromItem() #FromItem:
76427642
)
76437643

76447644
[ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { fromItem.setAlias(alias); } ]
7645+
[
7646+
// BigQuery: UNNEST(array) AS alias WITH OFFSET AS offsetAlias
7647+
LOOKAHEAD(2, { fromItem instanceof TableFunction })
7648+
<K_WITH> <K_OFFSET> { ((TableFunction) fromItem).setWithOffset(true); }
7649+
[ LOOKAHEAD({ isAliasAhead() }) alias=Alias()
7650+
{ ((TableFunction) fromItem).setOffsetAlias(alias); } ]
7651+
]
76457652
[
76467653
LOOKAHEAD(2, {fromItem instanceof Table }) timeTravelStr = TimeTravelAfterAlias()
76477654
{ ((Table) fromItem).setTimeTravelStrAfterAlias(timeTravelStr); }

src/test/java/net/sf/jsqlparser/statement/select/BigQueryTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,31 @@ void testOffsetClauseStillParses() throws JSQLParserException {
133133
String sqlStr = "SELECT a FROM t LIMIT 10 OFFSET 5";
134134
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
135135
}
136+
137+
@Test
138+
void testUnnestWithOffsetAfterAlias() throws JSQLParserException {
139+
String sqlStr = "SELECT x, pos FROM UNNEST([1, 2, 3]) AS x WITH OFFSET AS pos";
140+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
141+
TableFunction unnest = (TableFunction) select.getFromItem();
142+
143+
Assertions.assertTrue(unnest.isWithOffset());
144+
Assertions.assertEquals("x", unnest.getAlias().getName());
145+
Assertions.assertEquals("pos", unnest.getOffsetAlias().getName());
146+
}
147+
148+
@Test
149+
void testUnnestWithOffsetWithoutOffsetAlias() throws JSQLParserException {
150+
String sqlStr = "SELECT * FROM t, UNNEST(t.arr) AS c WITH OFFSET";
151+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
152+
}
153+
154+
@Test
155+
void testUnnestWithOrdinalityAliasStillBindsToTableFunction() throws JSQLParserException {
156+
String sqlStr = "SELECT * FROM UNNEST(ARRAY[1, 2, 3]) WITH ORDINALITY AS t (a, b)";
157+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
158+
TableFunction unnest = (TableFunction) select.getFromItem();
159+
160+
Assertions.assertFalse(unnest.isWithOffset());
161+
Assertions.assertEquals("t", unnest.getAlias().getName());
162+
}
136163
}

0 commit comments

Comments
 (0)