Skip to content

Commit 0f685b8

Browse files
hayssamsclaude
andcommitted
Support DuckDB FROM-first queries
DuckDB lets the FROM clause lead, with the SELECT clause optional: FROM tbl SELECT a and FROM tbl WHERE a > 1. PlainSelect records the order in a fromFirst flag, and both the AST's own rendering and SelectDeParser emit the clauses accordingly. Bare FROM queries and the pipe syntax are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent 154bb24 commit 0f685b8

4 files changed

Lines changed: 331 additions & 130 deletions

File tree

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

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class PlainSelect extends Select {
6666
private boolean isUsingFinal = false;
6767
private boolean isUsingOnly = false;
6868
private boolean useWithNoLog = false;
69+
private boolean fromFirst = false;
6970
private Table intoTempTable = null;
7071
private List<UpdateSet> settings = null;
7172

@@ -543,9 +544,83 @@ public void setWindowDefinitions(List<WindowDefinition> windowDefinitions) {
543544

544545
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength",
545546
"PMD.NPathComplexity"})
547+
/**
548+
* DuckDB allows the FROM clause to come first, with the SELECT clause optional:
549+
* {@code FROM tbl SELECT a} and {@code FROM tbl WHERE a > 1}.
550+
*/
551+
public boolean isFromFirst() {
552+
return fromFirst;
553+
}
554+
555+
public void setFromFirst(boolean fromFirst) {
556+
this.fromFirst = fromFirst;
557+
}
558+
559+
public PlainSelect withFromFirst(boolean fromFirst) {
560+
setFromFirst(fromFirst);
561+
return this;
562+
}
563+
546564
public StringBuilder appendSelectBodyTo(StringBuilder builder) {
565+
if (fromFirst) {
566+
// DuckDB allows the FROM clause to lead: FROM tbl [SELECT ...] [WHERE ...]
567+
appendFromClauseTo(builder);
568+
if (selectItems != null && !selectItems.isEmpty()) {
569+
builder.append(" ");
570+
appendSelectClauseTo(builder);
571+
appendIntoClausesTo(builder);
572+
}
573+
} else {
574+
appendSelectClauseTo(builder);
575+
appendIntoClausesTo(builder);
576+
appendFromClauseTo(builder);
577+
}
578+
579+
if (preWhere != null) {
580+
builder.append(" PREWHERE ").append(preWhere);
581+
}
582+
if (where != null) {
583+
builder.append(" WHERE ").append(where);
584+
}
585+
if (oracleHierarchical != null) {
586+
builder.append(oracleHierarchical);
587+
}
588+
if (preferringClause != null) {
589+
builder.append(" ").append(preferringClause);
590+
}
591+
if (groupBy != null) {
592+
builder.append(" ").append(groupBy);
593+
}
594+
if (having != null) {
595+
builder.append(" HAVING ").append(having);
596+
}
597+
if (qualify != null) {
598+
builder.append(" QUALIFY ").append(qualify);
599+
}
600+
if (windowDefinitions != null) {
601+
builder.append(" WINDOW ");
602+
builder.append(windowDefinitions.stream().map(WindowDefinition::toString)
603+
.collect(joining(", ")));
604+
}
605+
appendEmitClauseTo(builder);
606+
if (intoTempTable != null) {
607+
builder.append(" INTO TEMP ").append(intoTempTable);
608+
}
609+
if (useWithNoLog) {
610+
builder.append(" WITH NO LOG");
611+
}
612+
return builder;
613+
}
614+
615+
private void appendSelectClauseTo(StringBuilder builder) {
547616
builder.append("SELECT ");
617+
appendSelectHintsTo(builder);
618+
appendSelectQualifiersTo(builder);
619+
appendMySqlSelectFlagsTo(builder);
620+
builder.append(getStringList(selectItems));
621+
}
548622

623+
private void appendSelectHintsTo(StringBuilder builder) {
549624
if (this.mySqlHintStraightJoin) {
550625
builder.append("STRAIGHT_JOIN ");
551626
}
@@ -561,7 +636,9 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
561636
if (first != null) {
562637
builder.append(first).append(" ");
563638
}
639+
}
564640

641+
private void appendSelectQualifiersTo(StringBuilder builder) {
565642
if (distinct != null) {
566643
builder.append(distinct).append(" ");
567644
}
@@ -580,14 +657,19 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
580657
if (top != null) {
581658
builder.append(top).append(" ");
582659
}
660+
}
661+
662+
private void appendMySqlSelectFlagsTo(StringBuilder builder) {
583663
if (mySqlCacheFlag != null) {
584664
builder.append(mySqlCacheFlag.name()).append(" ");
585665
}
666+
586667
if (mySqlSqlCalcFoundRows) {
587668
builder.append("SQL_CALC_FOUND_ROWS").append(" ");
588669
}
589-
builder.append(getStringList(selectItems));
670+
}
590671

672+
private void appendIntoClausesTo(StringBuilder builder) {
591673
if (intoTables != null) {
592674
builder.append(" INTO ");
593675
for (Iterator<Table> iter = intoTables.iterator(); iter.hasNext();) {
@@ -603,9 +685,11 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
603685
.getPosition() == MySqlSelectIntoClause.Position.BEFORE_FROM) {
604686
builder.append(" ").append(mySqlSelectIntoClause);
605687
}
688+
}
606689

690+
private void appendFromClauseTo(StringBuilder builder) {
607691
if (fromItem != null) {
608-
builder.append(" FROM ");
692+
builder.append(fromFirst ? "FROM " : " FROM ");
609693
if (isUsingOnly) {
610694
builder.append("ONLY ");
611695
}
@@ -633,42 +717,9 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
633717
builder.append(" WINDOW ").append(ksqlWindow);
634718
}
635719
}
636-
if (preWhere != null) {
637-
builder.append(" PREWHERE ").append(preWhere);
638-
}
639-
if (where != null) {
640-
builder.append(" WHERE ").append(where);
641-
}
642-
if (oracleHierarchical != null) {
643-
builder.append(oracleHierarchical);
644-
}
645-
if (preferringClause != null) {
646-
builder.append(" ").append(preferringClause);
647-
}
648-
if (groupBy != null) {
649-
builder.append(" ").append(groupBy);
650-
}
651-
if (having != null) {
652-
builder.append(" HAVING ").append(having);
653-
}
654-
if (qualify != null) {
655-
builder.append(" QUALIFY ").append(qualify);
656-
}
657-
if (windowDefinitions != null) {
658-
builder.append(" WINDOW ");
659-
builder.append(windowDefinitions.stream().map(WindowDefinition::toString)
660-
.collect(joining(", ")));
661-
}
662-
appendEmitClauseTo(builder);
663-
if (intoTempTable != null) {
664-
builder.append(" INTO TEMP ").append(intoTempTable);
665-
}
666-
if (useWithNoLog) {
667-
builder.append(" WITH NO LOG");
668-
}
669-
return builder;
670720
}
671721

722+
672723
@Override
673724
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength",
674725
"PMD.NPathComplexity"})

src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java

Lines changed: 111 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -198,101 +198,16 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
198198
}
199199
}
200200

201-
builder.append("SELECT ");
202-
203-
if (plainSelect.getMySqlHintStraightJoin()) {
204-
builder.append("STRAIGHT_JOIN ");
205-
}
206-
207-
OracleHint hint = plainSelect.getOracleHint();
208-
if (hint != null) {
209-
builder.append(hint).append(" ");
210-
}
211-
212-
Skip skip = plainSelect.getSkip();
213-
if (skip != null) {
214-
builder.append(skip).append(" ");
215-
}
216-
217-
First first = plainSelect.getFirst();
218-
if (first != null) {
219-
builder.append(first).append(" ");
220-
}
221-
222-
deparseDistinctClause(plainSelect.getDistinct());
223-
224-
if (plainSelect.getBigQuerySelectQualifier() != null) {
225-
switch (plainSelect.getBigQuerySelectQualifier()) {
226-
case AS_STRUCT:
227-
builder.append("AS STRUCT ");
228-
break;
229-
case AS_VALUE:
230-
builder.append("AS VALUE ");
231-
break;
232-
}
233-
}
234-
235-
Top top = plainSelect.getTop();
236-
if (top != null) {
237-
visit(top);
238-
}
239-
240-
if (plainSelect.getMySqlSqlCacheFlag() != null) {
241-
builder.append(plainSelect.getMySqlSqlCacheFlag().name()).append(" ");
242-
}
243-
244-
if (plainSelect.getMySqlSqlCalcFoundRows()) {
245-
builder.append("SQL_CALC_FOUND_ROWS").append(" ");
246-
}
247-
248-
deparseSelectItemsClause(plainSelect.getSelectItems());
249-
250-
if (plainSelect.getIntoTables() != null) {
251-
builder.append(" INTO ");
252-
for (Iterator<Table> iter = plainSelect.getIntoTables().iterator(); iter.hasNext();) {
253-
visit(iter.next(), context);
254-
if (iter.hasNext()) {
255-
builder.append(", ");
256-
}
257-
}
258-
}
259-
260-
deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.BEFORE_FROM, context);
261-
262-
if (plainSelect.getFromItem() != null) {
263-
builder.append(" FROM ");
264-
if (plainSelect.isUsingOnly()) {
265-
builder.append("ONLY ");
266-
}
267-
plainSelect.getFromItem().accept(this, context);
268-
269-
if (plainSelect.getFromItem() instanceof Table) {
270-
Table table = (Table) plainSelect.getFromItem();
271-
if (table.getSampleClause() != null) {
272-
table.getSampleClause().appendTo(builder);
273-
}
274-
}
275-
}
276-
277-
if (plainSelect.getLateralViews() != null) {
278-
for (LateralView lateralView : plainSelect.getLateralViews()) {
279-
deparseLateralView(lateralView);
280-
}
281-
}
282-
283-
if (plainSelect.getJoins() != null) {
284-
for (Join join : plainSelect.getJoins()) {
285-
deparseJoin(join);
201+
if (plainSelect.isFromFirst()) {
202+
// DuckDB allows the FROM clause to lead, with an optional SELECT clause behind it
203+
deparseFromClause(plainSelect, context);
204+
if (plainSelect.getSelectItems() != null && !plainSelect.getSelectItems().isEmpty()) {
205+
builder.append(" ");
206+
deparseSelectClause(plainSelect, context);
286207
}
287-
}
288-
289-
if (plainSelect.isUsingFinal()) {
290-
builder.append(" FINAL");
291-
}
292-
293-
if (plainSelect.getKsqlWindow() != null) {
294-
builder.append(" WINDOW ");
295-
builder.append(plainSelect.getKsqlWindow().toString());
208+
} else {
209+
deparseSelectClause(plainSelect, context);
210+
deparseFromClause(plainSelect, context);
296211
}
297212

298213
deparsePreWhereClause(plainSelect);
@@ -419,6 +334,108 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
419334
return builder;
420335
}
421336

337+
private <S> void deparseSelectClause(PlainSelect plainSelect, S context) {
338+
builder.append("SELECT ");
339+
340+
if (plainSelect.getMySqlHintStraightJoin()) {
341+
builder.append("STRAIGHT_JOIN ");
342+
}
343+
344+
OracleHint hint = plainSelect.getOracleHint();
345+
if (hint != null) {
346+
builder.append(hint).append(" ");
347+
}
348+
349+
Skip skip = plainSelect.getSkip();
350+
if (skip != null) {
351+
builder.append(skip).append(" ");
352+
}
353+
354+
First first = plainSelect.getFirst();
355+
if (first != null) {
356+
builder.append(first).append(" ");
357+
}
358+
359+
deparseDistinctClause(plainSelect.getDistinct());
360+
361+
if (plainSelect.getBigQuerySelectQualifier() != null) {
362+
switch (plainSelect.getBigQuerySelectQualifier()) {
363+
case AS_STRUCT:
364+
builder.append("AS STRUCT ");
365+
break;
366+
case AS_VALUE:
367+
builder.append("AS VALUE ");
368+
break;
369+
}
370+
}
371+
372+
Top top = plainSelect.getTop();
373+
if (top != null) {
374+
visit(top);
375+
}
376+
377+
if (plainSelect.getMySqlSqlCacheFlag() != null) {
378+
builder.append(plainSelect.getMySqlSqlCacheFlag().name()).append(" ");
379+
}
380+
381+
if (plainSelect.getMySqlSqlCalcFoundRows()) {
382+
builder.append("SQL_CALC_FOUND_ROWS").append(" ");
383+
}
384+
385+
deparseSelectItemsClause(plainSelect.getSelectItems());
386+
387+
if (plainSelect.getIntoTables() != null) {
388+
builder.append(" INTO ");
389+
for (Iterator<Table> iter = plainSelect.getIntoTables().iterator(); iter.hasNext();) {
390+
visit(iter.next(), context);
391+
if (iter.hasNext()) {
392+
builder.append(", ");
393+
}
394+
}
395+
}
396+
397+
deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.BEFORE_FROM, context);
398+
}
399+
400+
private <S> void deparseFromClause(PlainSelect plainSelect, S context) {
401+
if (plainSelect.getFromItem() != null) {
402+
builder.append(plainSelect.isFromFirst() ? "FROM " : " FROM ");
403+
if (plainSelect.isUsingOnly()) {
404+
builder.append("ONLY ");
405+
}
406+
plainSelect.getFromItem().accept(this, context);
407+
408+
if (plainSelect.getFromItem() instanceof Table) {
409+
Table table = (Table) plainSelect.getFromItem();
410+
if (table.getSampleClause() != null) {
411+
table.getSampleClause().appendTo(builder);
412+
}
413+
}
414+
}
415+
416+
if (plainSelect.getLateralViews() != null) {
417+
for (LateralView lateralView : plainSelect.getLateralViews()) {
418+
deparseLateralView(lateralView);
419+
}
420+
}
421+
422+
if (plainSelect.getJoins() != null) {
423+
for (Join join : plainSelect.getJoins()) {
424+
deparseJoin(join);
425+
}
426+
}
427+
428+
if (plainSelect.isUsingFinal()) {
429+
builder.append(" FINAL");
430+
}
431+
432+
if (plainSelect.getKsqlWindow() != null) {
433+
builder.append(" WINDOW ");
434+
builder.append(plainSelect.getKsqlWindow().toString());
435+
}
436+
}
437+
438+
422439
@Override
423440
public <S> StringBuilder visit(PivotQuery pivotQuery, S context) {
424441
List<WithItem<?>> withItemsList = pivotQuery.getWithItemsList();

0 commit comments

Comments
 (0)