Skip to content

Commit c63f275

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 f1763dd commit c63f275

3 files changed

Lines changed: 70 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
@@ -1425,23 +1425,29 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
14251425
}
14261426
}
14271427

1428-
/**
1429-
* CREATE [TEMP|TEMPORARY] MACRO, DuckDB's inlined function definition, or its FUNCTION alias:
1430-
* FUNCTION [IF NOT EXISTS] name (parameters) AS body, where AS directly follows the parameters
1431-
* and the body is not a string, unlike other dialects' CREATE FUNCTION (RETURNS ..., or
1432-
* PostgreSQL's AS $$ ... $$ LANGUAGE ...).
1433-
*/
1428+
/** CREATE [TEMP|TEMPORARY] MACRO, DuckDB's inlined function definition. */
14341429
private boolean isCreateMacroAhead() {
14351430
int offset = 1;
14361431
int kind = getToken(offset).kind;
14371432
if (kind == K_TEMP || kind == K_TEMPORARY) {
14381433
offset++;
14391434
}
1440-
kind = getToken(offset).kind;
1441-
if (kind == K_MACRO) {
1442-
return true;
1435+
return getToken(offset).kind == K_MACRO;
1436+
}
1437+
1438+
/**
1439+
* DuckDB's FUNCTION alias of MACRO: FUNCTION [IF NOT EXISTS] name (parameters) AS body, where
1440+
* AS directly follows the parameters, the body is not a string and no LANGUAGE or OPTIONS
1441+
* clause follows it, unlike other dialects' CREATE FUNCTION (RETURNS ..., PostgreSQL's
1442+
* AS $tag$ ... $tag$ LANGUAGE ..., BigQuery's AS (expr) OPTIONS (...)).
1443+
*/
1444+
private boolean isCreateFunctionMacroAhead() {
1445+
int offset = 1;
1446+
int kind = getToken(offset).kind;
1447+
if (kind == K_TEMP || kind == K_TEMPORARY) {
1448+
offset++;
14431449
}
1444-
if (kind != K_FUNCTION) {
1450+
if (getToken(offset).kind != K_FUNCTION) {
14451451
return false;
14461452
}
14471453
// IF NOT EXISTS and a qualified name, at most 8 tokens, up to the parameter list
@@ -1466,9 +1472,30 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
14661472
}
14671473
offset++;
14681474
} while (depth > 0);
1469-
kind = getToken(offset + 1).kind;
1470-
return getToken(offset).kind == K_AS
1471-
&& kind != S_CHAR_LITERAL && kind != S_DOLLAR_QUOTED_STRING;
1475+
if (getToken(offset).kind != K_AS) {
1476+
return false;
1477+
}
1478+
Token body = getToken(offset + 1);
1479+
if (body.kind == S_CHAR_LITERAL || body.kind == S_DOLLAR_QUOTED_STRING
1480+
|| body.image.length() > 1 && body.image.startsWith("$") && body.image.endsWith("$")) {
1481+
return false;
1482+
}
1483+
// no LANGUAGE or OPTIONS clause outside parentheses up to the end of the statement
1484+
depth = 0;
1485+
for (offset++;; offset++) {
1486+
Token token = getToken(offset);
1487+
if (token.kind == EOF || depth == 0 && token.kind == ST_SEMICOLON) {
1488+
return true;
1489+
}
1490+
if (token.kind == OPENING_BRACKET) {
1491+
depth++;
1492+
} else if (token.kind == CLOSING_BRACKET) {
1493+
depth--;
1494+
} else if (depth == 0 && ("LANGUAGE".equalsIgnoreCase(token.image)
1495+
|| "OPTIONS".equalsIgnoreCase(token.image))) {
1496+
return false;
1497+
}
1498+
}
14721499
}
14731500

14741501
/** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */
@@ -20250,6 +20277,8 @@ Statement Create():
2025020277
|
2025120278
LOOKAHEAD({ isCreateMacroAhead() }) statement = CreateMacro(isUsingOrReplace)
2025220279
|
20280+
LOOKAHEAD({ isCreateFunctionMacroAhead() }) statement = CreateMacro(isUsingOrReplace)
20281+
|
2025320282
statement = CreateFunctionStatement(isUsingOrReplace)
2025420283
|
2025520284
statement = CreateSchema()

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
import net.sf.jsqlparser.JSQLParserException;
1313
import net.sf.jsqlparser.expression.Function;
1414
import net.sf.jsqlparser.expression.LambdaExpression;
15-
import java.util.Arrays;
16-
import java.util.Collections;
17-
import net.sf.jsqlparser.util.TablesNamesFinder;
18-
import net.sf.jsqlparser.statement.create.function.CreateFunction;
1915
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
2016
import net.sf.jsqlparser.schema.Table;
2117
import net.sf.jsqlparser.statement.create.table.CreateTable;
@@ -30,14 +26,17 @@
3026
import net.sf.jsqlparser.statement.PrepareStatement;
3127
import net.sf.jsqlparser.statement.DeallocateStatement;
3228
import net.sf.jsqlparser.statement.CopyStatement;
29+
import net.sf.jsqlparser.statement.create.function.CreateFunction;
3330
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
3431
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
3532
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
3633
import net.sf.jsqlparser.test.TestUtils;
34+
import net.sf.jsqlparser.util.TablesNamesFinder;
3735
import org.junit.jupiter.api.Assertions;
3836
import org.junit.jupiter.api.Test;
3937

4038
import java.util.Arrays;
39+
import java.util.HashSet;
4140

4241
public class DuckDBTest {
4342

@@ -537,18 +536,29 @@ void testCreateMacroWithTypedParameters() throws JSQLParserException {
537536
@Test
538537
void testCreateOverloadedMacro() throws JSQLParserException {
539538
String sqlStr = "CREATE OR REPLACE MACRO is_maximal(a TINYINT) AS a = 127, "
540-
+ "(a INT) AS a = 2147483647, (a, b) AS TABLE SELECT * FROM t WHERE x = a";
539+
+ "(a INT) AS a = 2147483647";
541540
CreateMacro createMacro =
542541
(CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
543542

544-
Assertions.assertEquals(3, createMacro.getOverloads().size());
543+
Assertions.assertEquals(2, createMacro.getOverloads().size());
545544
Assertions.assertEquals("TINYINT",
546545
createMacro.getOverloads().get(0).getParameters().get(0).getType().toString());
547546
Assertions.assertEquals("a = 2147483647",
548547
createMacro.getOverloads().get(1).getExpression().toString());
549-
Assertions.assertTrue(createMacro.getOverloads().get(2).isTable());
550-
Assertions.assertEquals(2, createMacro.getOverloads().get(2).getParameters().size());
551-
Assertions.assertEquals(Collections.singleton("t"), TablesNamesFinder.findTables(sqlStr));
548+
}
549+
550+
@Test
551+
void testCreateOverloadedTableMacro() throws JSQLParserException {
552+
String sqlStr = "CREATE MACRO get_t(i) AS TABLE SELECT * FROM t WHERE id = i, "
553+
+ "(i, j) AS TABLE SELECT * FROM u WHERE id = i AND x = j";
554+
CreateMacro createMacro =
555+
(CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
556+
557+
Assertions.assertEquals(2, createMacro.getOverloads().size());
558+
Assertions.assertTrue(createMacro.getOverloads().get(1).isTable());
559+
Assertions.assertEquals(2, createMacro.getOverloads().get(1).getParameters().size());
560+
Assertions.assertEquals(new HashSet<>(Arrays.asList("t", "u")),
561+
TablesNamesFinder.findTables(sqlStr));
552562
}
553563

554564
@Test
@@ -569,7 +579,9 @@ void testCreateFunctionOfOtherDialectsIsNotAMacro() throws JSQLParserException {
569579
for (String sqlStr : Arrays.asList(
570580
"CREATE FUNCTION add(a integer, b integer) RETURNS integer AS 'select $1 + $2' LANGUAGE SQL",
571581
"CREATE FUNCTION add(IN a integer, OUT b integer) AS 'select $1 + 1' LANGUAGE SQL",
572-
"CREATE FUNCTION add(IN a integer, OUT b integer) AS $$ select $1 + 1 $$ LANGUAGE SQL")) {
582+
"CREATE FUNCTION add(IN a integer, OUT b integer) AS $$ select $1 + 1 $$ LANGUAGE SQL",
583+
"CREATE FUNCTION add(IN a integer, OUT b integer) AS $function$ select $1 + 1 $function$ LANGUAGE SQL",
584+
"CREATE OR REPLACE FUNCTION ds.add(x INT64) AS (x + 1) OPTIONS (description = 'd')")) {
573585
Assertions.assertInstanceOf(CreateFunction.class, CCJSqlParserUtil.parse(sqlStr));
574586
}
575587
}

0 commit comments

Comments
 (0)