Skip to content

Commit 08ab771

Browse files
author
Jules Ivanic
committed
Keep other dialects' CREATE FUNCTION off the macro route
`AS` directly after the parameters with a non-string body was not enough to tell a DuckDB macro apart: PostgreSQL's `AS $function$ ... $function$ LANGUAGE ...` (a tagged dollar quote lexes as an identifier), `AS U&'...' LANGUAGE ...` and BigQuery's `AS (expr) OPTIONS (...)` became macros and failed. `isCreateFunctionMacroAhead()` also rejects a `$tag$` body and a LANGUAGE or OPTIONS clause outside parentheses before the end of the statement. - `setOverloads()` copies its argument, so a null or immutable list no longer breaks the first-overload setters. - The Javadoc notes that, as in DuckDB, a `,` after a TABLE body ending with a list continues that list. - `testCreateOverloadedMacro` no longer mixes scalar and table overloads, which DuckDB rejects; `testCreateOverloadedTableMacro` covers table overloads.
1 parent 3e23858 commit 08ab771

3 files changed

Lines changed: 72 additions & 24 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
* {@link #getParameters()}, {@link #getExpression()} and {@link #getSelect()} and their setters
3131
* address the first overload.
3232
*
33+
* <p>
34+
* As in DuckDB, a {@code ,} after a {@code TABLE} body that ends with a list (FROM, GROUP BY, ORDER
35+
* BY ...) continues that list rather than starting a new overload.
36+
*
3337
* @see <a href="https://duckdb.org/docs/stable/sql/statements/create_macro">CREATE MACRO</a>
3438
*/
3539
public class CreateMacro implements Statement {
@@ -110,8 +114,9 @@ public List<Overload> getOverloads() {
110114
return overloads;
111115
}
112116

117+
/** Copies {@code overloads}, so that the first-overload setters can add to an empty list. */
113118
public void setOverloads(List<Overload> overloads) {
114-
this.overloads = overloads;
119+
this.overloads = overloads == null ? new ArrayList<>() : new ArrayList<>(overloads);
115120
}
116121

117122
public CreateMacro withOverloads(List<Overload> overloads) {

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

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,23 +1440,29 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
14401440
}
14411441
}
14421442

1443-
/**
1444-
* CREATE [TEMP|TEMPORARY] MACRO, DuckDB's inlined function definition, or its FUNCTION alias:
1445-
* FUNCTION [IF NOT EXISTS] name (parameters) AS body, where AS directly follows the parameters
1446-
* and the body is not a string, unlike other dialects' CREATE FUNCTION (RETURNS ..., or
1447-
* PostgreSQL's AS $$ ... $$ LANGUAGE ...).
1448-
*/
1443+
/** CREATE [TEMP|TEMPORARY] MACRO, DuckDB's inlined function definition. */
14491444
private boolean isCreateMacroAhead() {
14501445
int offset = 1;
14511446
int kind = getToken(offset).kind;
14521447
if (kind == K_TEMP || kind == K_TEMPORARY) {
14531448
offset++;
14541449
}
1455-
kind = getToken(offset).kind;
1456-
if (kind == K_MACRO) {
1457-
return true;
1450+
return getToken(offset).kind == K_MACRO;
1451+
}
1452+
1453+
/**
1454+
* DuckDB's FUNCTION alias of MACRO: FUNCTION [IF NOT EXISTS] name (parameters) AS body, where
1455+
* AS directly follows the parameters, the body is not a string and no LANGUAGE or OPTIONS
1456+
* clause follows it, unlike other dialects' CREATE FUNCTION (RETURNS ..., PostgreSQL's
1457+
* AS $tag$ ... $tag$ LANGUAGE ..., BigQuery's AS (expr) OPTIONS (...)).
1458+
*/
1459+
private boolean isCreateFunctionMacroAhead() {
1460+
int offset = 1;
1461+
int kind = getToken(offset).kind;
1462+
if (kind == K_TEMP || kind == K_TEMPORARY) {
1463+
offset++;
14581464
}
1459-
if (kind != K_FUNCTION) {
1465+
if (getToken(offset).kind != K_FUNCTION) {
14601466
return false;
14611467
}
14621468
// IF NOT EXISTS and a qualified name, at most 8 tokens, up to the parameter list
@@ -1481,9 +1487,30 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
14811487
}
14821488
offset++;
14831489
} while (depth > 0);
1484-
kind = getToken(offset + 1).kind;
1485-
return getToken(offset).kind == K_AS
1486-
&& kind != S_CHAR_LITERAL && kind != S_DOLLAR_QUOTED_STRING;
1490+
if (getToken(offset).kind != K_AS) {
1491+
return false;
1492+
}
1493+
Token body = getToken(offset + 1);
1494+
if (body.kind == S_CHAR_LITERAL || body.kind == S_DOLLAR_QUOTED_STRING
1495+
|| body.image.length() > 1 && body.image.startsWith("$") && body.image.endsWith("$")) {
1496+
return false;
1497+
}
1498+
// no LANGUAGE or OPTIONS clause outside parentheses up to the end of the statement
1499+
depth = 0;
1500+
for (offset++;; offset++) {
1501+
Token token = getToken(offset);
1502+
if (token.kind == EOF || depth == 0 && token.kind == ST_SEMICOLON) {
1503+
return true;
1504+
}
1505+
if (token.kind == OPENING_BRACKET) {
1506+
depth++;
1507+
} else if (token.kind == CLOSING_BRACKET) {
1508+
depth--;
1509+
} else if (depth == 0 && ("LANGUAGE".equalsIgnoreCase(token.image)
1510+
|| "OPTIONS".equalsIgnoreCase(token.image))) {
1511+
return false;
1512+
}
1513+
}
14871514
}
14881515

14891516
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
@@ -20216,6 +20243,8 @@ Statement Create():
2021620243
|
2021720244
LOOKAHEAD({ isCreateMacroAhead() }) statement = CreateMacro(isUsingOrReplace)
2021820245
|
20246+
LOOKAHEAD({ isCreateFunctionMacroAhead() }) statement = CreateMacro(isUsingOrReplace)
20247+
|
2021920248
statement = CreateFunctionStatement(isUsingOrReplace)
2022020249
|
2022120250
statement = CreateSchema()

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

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@
1010
package net.sf.jsqlparser.statement.select;
1111

1212
import net.sf.jsqlparser.JSQLParserException;
13-
import java.util.Arrays;
14-
import java.util.Collections;
15-
import net.sf.jsqlparser.util.TablesNamesFinder;
16-
import net.sf.jsqlparser.statement.create.function.CreateFunction;
1713
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1814
import net.sf.jsqlparser.schema.Table;
1915
import net.sf.jsqlparser.statement.create.table.CreateTable;
@@ -28,13 +24,18 @@
2824
import net.sf.jsqlparser.statement.PrepareStatement;
2925
import net.sf.jsqlparser.statement.DeallocateStatement;
3026
import net.sf.jsqlparser.statement.CopyStatement;
27+
import net.sf.jsqlparser.statement.create.function.CreateFunction;
3128
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
3229
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
3330
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
3431
import net.sf.jsqlparser.test.TestUtils;
32+
import net.sf.jsqlparser.util.TablesNamesFinder;
3533
import org.junit.jupiter.api.Assertions;
3634
import org.junit.jupiter.api.Test;
3735

36+
import java.util.Arrays;
37+
import java.util.HashSet;
38+
3839
public class DuckDBTest {
3940

4041
@Test
@@ -533,18 +534,29 @@ void testCreateMacroWithTypedParameters() throws JSQLParserException {
533534
@Test
534535
void testCreateOverloadedMacro() throws JSQLParserException {
535536
String sqlStr = "CREATE OR REPLACE MACRO is_maximal(a TINYINT) AS a = 127, "
536-
+ "(a INT) AS a = 2147483647, (a, b) AS TABLE SELECT * FROM t WHERE x = a";
537+
+ "(a INT) AS a = 2147483647";
537538
CreateMacro createMacro =
538539
(CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
539540

540-
Assertions.assertEquals(3, createMacro.getOverloads().size());
541+
Assertions.assertEquals(2, createMacro.getOverloads().size());
541542
Assertions.assertEquals("TINYINT",
542543
createMacro.getOverloads().get(0).getParameters().get(0).getType().toString());
543544
Assertions.assertEquals("a = 2147483647",
544545
createMacro.getOverloads().get(1).getExpression().toString());
545-
Assertions.assertTrue(createMacro.getOverloads().get(2).isTable());
546-
Assertions.assertEquals(2, createMacro.getOverloads().get(2).getParameters().size());
547-
Assertions.assertEquals(Collections.singleton("t"), TablesNamesFinder.findTables(sqlStr));
546+
}
547+
548+
@Test
549+
void testCreateOverloadedTableMacro() throws JSQLParserException {
550+
String sqlStr = "CREATE MACRO get_t(i) AS TABLE SELECT * FROM t WHERE id = i, "
551+
+ "(i, j) AS TABLE SELECT * FROM u WHERE id = i AND x = j";
552+
CreateMacro createMacro =
553+
(CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
554+
555+
Assertions.assertEquals(2, createMacro.getOverloads().size());
556+
Assertions.assertTrue(createMacro.getOverloads().get(1).isTable());
557+
Assertions.assertEquals(2, createMacro.getOverloads().get(1).getParameters().size());
558+
Assertions.assertEquals(new HashSet<>(Arrays.asList("t", "u")),
559+
TablesNamesFinder.findTables(sqlStr));
548560
}
549561

550562
@Test
@@ -565,7 +577,9 @@ void testCreateFunctionOfOtherDialectsIsNotAMacro() throws JSQLParserException {
565577
for (String sqlStr : Arrays.asList(
566578
"CREATE FUNCTION add(a integer, b integer) RETURNS integer AS 'select $1 + $2' LANGUAGE SQL",
567579
"CREATE FUNCTION add(IN a integer, OUT b integer) AS 'select $1 + 1' LANGUAGE SQL",
568-
"CREATE FUNCTION add(IN a integer, OUT b integer) AS $$ select $1 + 1 $$ LANGUAGE SQL")) {
580+
"CREATE FUNCTION add(IN a integer, OUT b integer) AS $$ select $1 + 1 $$ LANGUAGE SQL",
581+
"CREATE FUNCTION add(IN a integer, OUT b integer) AS $function$ select $1 + 1 $function$ LANGUAGE SQL",
582+
"CREATE OR REPLACE FUNCTION ds.add(x INT64) AS (x + 1) OPTIONS (description = 'd')")) {
569583
Assertions.assertInstanceOf(CreateFunction.class, CCJSqlParserUtil.parse(sqlStr));
570584
}
571585
}

0 commit comments

Comments
 (0)