Skip to content

Commit daa8118

Browse files
hayssamsclaude
andcommitted
Support DuckDB 2.0 APPROX NEAREST similarity joins
Adds JOIN t APPROX NEAREST n BY SIMILARITY expression, the vector similarity join of DuckDB 2.0. APPROX no longer binds as a table alias in that position. Also adds the lookahead the UNPIVOT statement's INTO clause needed to keep the grammar free of choice conflicts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqinwHBKuAtPEmtXr3b5P2
1 parent 315fc79 commit daa8118

4 files changed

Lines changed: 88 additions & 1 deletion

File tree

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class Join extends ASTNodeAccessImpl {
3737
private boolean cross = false;
3838
private boolean semi = false;
3939
private boolean anti = false;
40+
private Long approxNearest;
41+
private Expression similarity;
4042
private boolean any = false;
4143
private boolean all = false;
4244
private boolean straight = false;
@@ -195,6 +197,36 @@ public Join withFetch(boolean b) {
195197
*
196198
* @return true if is a "SEMI" join
197199
*/
200+
/**
201+
* DuckDB 2.0's similarity join:
202+
* {@code JOIN products APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(...)}.
203+
*/
204+
public Long getApproxNearest() {
205+
return approxNearest;
206+
}
207+
208+
public void setApproxNearest(Long approxNearest) {
209+
this.approxNearest = approxNearest;
210+
}
211+
212+
public Join withApproxNearest(Long approxNearest) {
213+
setApproxNearest(approxNearest);
214+
return this;
215+
}
216+
217+
public Expression getSimilarity() {
218+
return similarity;
219+
}
220+
221+
public void setSimilarity(Expression similarity) {
222+
this.similarity = similarity;
223+
}
224+
225+
public Join withSimilarity(Expression similarity) {
226+
setSimilarity(similarity);
227+
return this;
228+
}
229+
198230
public boolean isAnti() {
199231
return anti;
200232
}
@@ -577,6 +609,11 @@ public String toString() {
577609
builder.append(fromItem).append((joinWindow != null) ? " WITHIN " + joinWindow : "");
578610
}
579611

612+
if (approxNearest != null) {
613+
builder.append(" APPROX NEAREST ").append(approxNearest)
614+
.append(" BY SIMILARITY ").append(similarity);
615+
}
616+
580617
for (Expression onExpression : onExpressions) {
581618
builder.append(" ON ").append(onExpression);
582619
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,12 @@ public void deparseJoin(Join join) {
773773
builder.append(" WITHIN ");
774774
builder.append(join.getJoinWindow().toString());
775775
}
776+
if (join.getApproxNearest() != null) {
777+
builder.append(" APPROX NEAREST ").append(join.getApproxNearest())
778+
.append(" BY SIMILARITY ");
779+
join.getSimilarity().accept(expressionVisitor, null);
780+
}
781+
776782
for (Expression onExpression : join.getOnExpressions()) {
777783
builder.append(" ON ");
778784
onExpression.accept(expressionVisitor, null);

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
12731273
|| "POSITION".equalsIgnoreCase(getToken(2).image));
12741274
}
12751275

1276+
/** DuckDB 2.0's APPROX NEAREST similarity join, never a table alias. */
1277+
private boolean isApproxNearestAhead() {
1278+
return getToken(1).kind == K_APPROX && getToken(2).kind == K_NEAREST;
1279+
}
1280+
12761281
/** SEMI/ANTI stay contextual identifiers unless they prefix a JOIN. */
12771282
private boolean isSemiAntiJoinAhead() {
12781283
int kind = getToken(1).kind;
@@ -1311,7 +1316,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
13111316
* reserved keywords.
13121317
*/
13131318
private boolean isAliasAhead() {
1314-
if (isAsOfJoinAhead() || isSemiAntiJoinAhead()) {
1319+
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
13151320
return false;
13161321
}
13171322
Token t = getToken(1);
@@ -2008,6 +2013,7 @@ String NonReservedWord() :
20082013
| tk=<K_ANTI:"ANTI">
20092014
| tk=<K_APPEND_ONLY:"APPEND_ONLY">
20102015
| tk=<K_APPLY:"APPLY">
2016+
| tk=<K_APPROX: "APPROX">
20112017
| tk=<K_APPROXIMATE:"APPROXIMATE">
20122018
| tk=<K_ARCHIVE: "ARCHIVE">
20132019
| tk=<K_ARRAY_LITERAL: "ARRAY" >
@@ -2216,6 +2222,7 @@ String NonReservedWord() :
22162222
| tk=<K_NAME:"NAME">
22172223
| tk=<K_NEVER:"NEVER">
22182224
| tk=<K_NEXT:"NEXT">
2225+
| tk=<K_NEAREST: "NEAREST">
22192226
| tk=<K_NEXTVAL: ( (("NEXTVAL")((" ")+("FOR"))?) | ( ("NEXT")(" ")+("VALUE") (" ")+("FOR") ) )>
22202227
| tk=<K_NO:"NO">
22212228
| tk=<K_NOCACHE:"NOCACHE">
@@ -2321,6 +2328,7 @@ String NonReservedWord() :
23212328
| tk=<K_SHARE : "SHARE">
23222329
| tk=<K_SIBLINGS:"SIBLINGS">
23232330
| tk=<K_SIMILAR:"SIMILAR">
2331+
| tk=<K_SIMILARITY: "SIMILARITY">
23242332
| tk=<K_SIZE:"SIZE">
23252333
| tk=<K_SKIP: "SKIP">
23262334
| tk=<K_SLAVE: "SLAVE">
@@ -7745,7 +7753,9 @@ UnPivotQuery UnPivotQuery():
77457753
{
77467754
<K_UNPIVOT> fromItem=FromItem() { unPivotQuery.setFromItem(fromItem); }
77477755
<K_ON> onExpressions=ExpressionList() { unPivotQuery.setOnExpressions(onExpressions); }
7756+
// greedy: INTO after the ON list belongs to this UNPIVOT
77487757
[
7758+
LOOKAHEAD(2)
77497759
<K_INTO> <K_NAME> nameColumn=RelObjectName() { unPivotQuery.setNameColumn(nameColumn); }
77507760
<K_VALUE> valueColumns=ColumnList() { unPivotQuery.setValueColumns(valueColumns); }
77517761
]
@@ -8234,6 +8244,7 @@ Join JoinerExpression() #JoinerExpression:
82348244
List<Column> columns = null;
82358245
KSQLJoinWindow joinWindow = null;
82368246
JoinHint joinHint = null;
8247+
Token tk = null;
82378248
}
82388249
{
82398250
[ LOOKAHEAD({ isAsOfJoinAhead() }) <S_IDENTIFIER> { join.setAsOf(true); } ]
@@ -8289,6 +8300,12 @@ Join JoinerExpression() #JoinerExpression:
82898300

82908301
right=FromItem()
82918302

8303+
// DuckDB 2.0: JOIN products APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(...)
8304+
[
8305+
LOOKAHEAD({ isApproxNearestAhead() })
8306+
<K_APPROX> <K_NEAREST> tk=<S_LONG> { join.setApproxNearest(Long.valueOf(tk.image)); }
8307+
<K_BY> <K_SIMILARITY> onExpression=Expression() { join.setSimilarity(onExpression); }
8308+
]
82928309
[
82938310
LOOKAHEAD(2) (
82948311
[ <K_WITHIN> joinWindow = KSQLJoinWindowClause() {join.setJoinWindow(joinWindow);} ]

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,4 +638,31 @@ void testUnPivotInFromClauseStillParses() throws JSQLParserException {
638638
String sqlStr = "SELECT * FROM monthly_sales UNPIVOT (sales FOR month IN (jan, feb))";
639639
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
640640
}
641+
642+
@Test
643+
void testApproxNearestJoin() throws JSQLParserException {
644+
String sqlStr = "SELECT * FROM queries q INNER JOIN products t "
645+
+ "APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(q.vec, t.vec)";
646+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
647+
Join join = select.getJoins().get(0);
648+
649+
Assertions.assertEquals(2L, join.getApproxNearest());
650+
Assertions.assertEquals("array_cosine_similarity(q.vec, t.vec)",
651+
join.getSimilarity().toString());
652+
}
653+
654+
@Test
655+
void testApproxNearestJoinWithoutAlias() throws JSQLParserException {
656+
String sqlStr = "SELECT * FROM queries JOIN products "
657+
+ "APPROX NEAREST 5 BY SIMILARITY array_distance(queries.vec, products.vec)";
658+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
659+
660+
Assertions.assertNull(((Table) select.getJoins().get(0).getFromItem()).getAlias());
661+
}
662+
663+
@Test
664+
void testApproxRemainsUsableAsIdentifier() throws JSQLParserException {
665+
String sqlStr = "SELECT approx, nearest, similarity FROM t";
666+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
667+
}
641668
}

0 commit comments

Comments
 (0)