Skip to content

Commit d7f5fe2

Browse files
hayssamsclaude
andcommitted
Support DuckDB INSERT ... BY NAME / BY POSITION
DuckDB matches an INSERT's source columns either positionally or by name. BY previously bound as a table alias, swallowing the clause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent a9345f1 commit d7f5fe2

4 files changed

Lines changed: 88 additions & 1 deletion

File tree

‎src/main/java/net/sf/jsqlparser/statement/insert/Insert.java‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public enum OverridingMode {
5959
private InsertConflictAction conflictAction;
6060
private InsertDuplicateAction duplicateAction;
6161
private Alias rowAlias;
62+
private ColumnMatching columnMatching;
6263
private boolean oracleMultiInsert = false;
6364
private boolean oracleMultiInsertFirst = false;
6465
private List<OracleMultiInsertBranch> oracleMultiInsertBranches;
@@ -231,6 +232,38 @@ public void setWithItemsList(List<WithItem<?>> withItemsList) {
231232
this.withItemsList = withItemsList;
232233
}
233234

235+
/**
236+
* DuckDB matches the source columns of an INSERT either positionally or by name:
237+
* {@code INSERT INTO tbl BY NAME SELECT ...}.
238+
*/
239+
public enum ColumnMatching {
240+
BY_NAME("BY NAME"), BY_POSITION("BY POSITION");
241+
242+
private final String keywords;
243+
244+
ColumnMatching(String keywords) {
245+
this.keywords = keywords;
246+
}
247+
248+
@Override
249+
public String toString() {
250+
return keywords;
251+
}
252+
}
253+
254+
public ColumnMatching getColumnMatching() {
255+
return columnMatching;
256+
}
257+
258+
public void setColumnMatching(ColumnMatching columnMatching) {
259+
this.columnMatching = columnMatching;
260+
}
261+
262+
public Insert withColumnMatching(ColumnMatching columnMatching) {
263+
setColumnMatching(columnMatching);
264+
return this;
265+
}
266+
234267
public OverridingMode getOverridingMode() {
235268
return overridingMode;
236269
}
@@ -406,6 +439,10 @@ private void appendInsertTargetAndValues(StringBuilder sql) {
406439
sql.append(") ");
407440
}
408441

442+
if (columnMatching != null) {
443+
sql.append(columnMatching).append(" ");
444+
}
445+
409446
if (isOverriding()) {
410447
sql.append("OVERRIDING ").append(overridingMode).append(" VALUE ");
411448
}

‎src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public void deParse(Insert insert) {
104104
builder.append(")");
105105
}
106106

107+
if (insert.getColumnMatching() != null) {
108+
builder.append(" ").append(insert.getColumnMatching());
109+
}
110+
107111
if (insert.isOverriding()) {
108112
builder.append(" OVERRIDING ").append(insert.getOverridingMode()).append(" VALUE");
109113
}

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,13 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
12111211
return isReservedKeywordSafeByFollower();
12121212
}
12131213

1214+
/** DuckDB's INSERT ... BY NAME|POSITION column matching, never a table alias. */
1215+
private boolean isColumnMatchingAhead() {
1216+
return getToken(1).kind == K_BY
1217+
&& ("NAME".equalsIgnoreCase(getToken(2).image)
1218+
|| "POSITION".equalsIgnoreCase(getToken(2).image));
1219+
}
1220+
12141221
/** SEMI/ANTI stay contextual identifiers unless they prefix a JOIN. */
12151222
private boolean isSemiAntiJoinAhead() {
12161223
int kind = getToken(1).kind;
@@ -5128,12 +5135,26 @@ Insert Insert():
51285135
] table=TableReference()
51295136
[ LOOKAHEAD(2) <K_PARTITION> "(" partitions=Partitions() ")" ]
51305137

5131-
[ LOOKAHEAD({ isAliasAhead() && !(getToken(1).kind == K_OVERRIDING
5138+
[ LOOKAHEAD({ isAliasAhead() && !isColumnMatchingAhead()
5139+
&& !(getToken(1).kind == K_OVERRIDING
51325140
&& (getToken(2).kind == K_SYSTEM || getToken(2).kind == K_USER)) })
51335141
[<K_AS> { useAs = true; } ] name=RelObjectName() { table.setAlias(new Alias(name,useAs)); }]
51345142

51355143
[ LOOKAHEAD(2) "(" columns=ColumnList() ")" ]
51365144

5145+
// DuckDB: INSERT INTO tbl BY NAME|POSITION SELECT ...
5146+
[
5147+
LOOKAHEAD({ isColumnMatchingAhead() })
5148+
<K_BY>
5149+
(
5150+
<K_NAME> { insert.setColumnMatching(Insert.ColumnMatching.BY_NAME); }
5151+
|
5152+
// POSITION is lexed as a string-function name
5153+
<K_STRING_FUNCTION_NAME>
5154+
{ insert.setColumnMatching(Insert.ColumnMatching.BY_POSITION); }
5155+
)
5156+
]
5157+
51375158
[ LOOKAHEAD(2) <K_OVERRIDING>
51385159
( <K_SYSTEM> { insert.setOverridingMode(Insert.OverridingMode.SYSTEM); }
51395160
| <K_USER> { insert.setOverridingMode(Insert.OverridingMode.USER); } ) <K_VALUE> ]

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import net.sf.jsqlparser.JSQLParserException;
1313
import net.sf.jsqlparser.schema.Table;
1414
import net.sf.jsqlparser.statement.create.table.CreateTable;
15+
import net.sf.jsqlparser.statement.insert.Insert;
1516
import net.sf.jsqlparser.test.TestUtils;
1617
import org.junit.jupiter.api.Assertions;
1718
import org.junit.jupiter.api.Test;
@@ -117,4 +118,28 @@ void testDecimalTypeArgumentsStillParse() throws JSQLParserException {
117118
String sqlStr = "CREATE TABLE t (a DECIMAL(10, 2), b VARCHAR(255))";
118119
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
119120
}
121+
122+
@Test
123+
void testInsertByName() throws JSQLParserException {
124+
String sqlStr = "INSERT INTO t BY NAME SELECT 1 AS b, 2 AS a";
125+
Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
126+
127+
Assertions.assertEquals(Insert.ColumnMatching.BY_NAME, insert.getColumnMatching());
128+
}
129+
130+
@Test
131+
void testInsertByPosition() throws JSQLParserException {
132+
String sqlStr = "INSERT INTO t BY POSITION SELECT 1, 2";
133+
Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
134+
135+
Assertions.assertEquals(Insert.ColumnMatching.BY_POSITION, insert.getColumnMatching());
136+
}
137+
138+
@Test
139+
void testInsertWithoutColumnMatching() throws JSQLParserException {
140+
String sqlStr = "INSERT INTO t (a, b) VALUES (1, 2)";
141+
Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
142+
143+
Assertions.assertNull(insert.getColumnMatching());
144+
}
120145
}

0 commit comments

Comments
 (0)