From 09aaa4853c5a294b1dbace43863e0260d8bfe9e0 Mon Sep 17 00:00:00 2001 From: dengliming Date: Sun, 23 Aug 2026 12:47:08 +0800 Subject: [PATCH] fix(parser): support leading CONTENT/DOCUMENT keyword in XMLPARSE --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 36 +++++++++- .../statement/select/SelectXMLParseTest.java | 65 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 381f3afc1..0bdead858 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -657,6 +657,38 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * XML constructor functions take a bare leading keyword before their first argument, + * e.g. XMLPARSE(CONTENT expr), XMLPARSE(DOCUMENT expr), XMLELEMENT(NAME ident, ...). + * Guarded by the function name so that CONTENT/DOCUMENT/NAME stay plain identifiers + * everywhere else, e.g. f(content). + */ + private void consumeXmlLeadingKeyword(Function function) { + String name = function.getName(); + if (name == null) { + return; + } + name = name.toUpperCase(java.util.Locale.ROOT); + if (!"XMLPARSE".equals(name) && !"XMLELEMENT".equals(name) && !"XMLFOREST".equals(name)) { + return; + } + Token token = getToken(1); + if (token.kind != S_IDENTIFIER && token.kind != K_NAME) { + return; + } + String keyword = token.image.toUpperCase(java.util.Locale.ROOT); + if (!"CONTENT".equals(keyword) && !"DOCUMENT".equals(keyword) && !"NAME".equals(keyword)) { + return; + } + // only a bare leading keyword: an expression must follow + Token next = getToken(2); + if (next.kind == EOF || ")".equals(next.image) || ",".equals(next.image) + || ".".equals(next.image) || "(".equals(next.image)) { + return; + } + function.setExtraKeyword(getNextToken().image); + } + private boolean isKeywordArgumentAhead() { Token t = getToken(1); if (t.kind == EOF || t.image.equals(")")) return false; @@ -10154,9 +10186,9 @@ Function InternalFunction(boolean escaped): } { [ LOOKAHEAD(2) prefixToken = ] - funcName = RelObjectNames() { if (prefixToken!=null) funcName.getNames().add(0, prefixToken.image ); } + funcName = RelObjectNames() { if (prefixToken!=null) funcName.getNames().add(0, prefixToken.image ); retval.setName(funcName.getNames()); } - "(" + "(" { consumeXmlLeadingKeyword(retval); } [ LOOKAHEAD(2) [ LOOKAHEAD(2) ( diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java new file mode 100644 index 000000000..9bb535255 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectXMLParseTest.java @@ -0,0 +1,65 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import net.sf.jsqlparser.JSQLParserException; +import org.junit.jupiter.api.Test; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +/** + * XML constructor functions taking a bare leading keyword before their first argument, e.g. + * {@code XMLPARSE(CONTENT expr)}. + */ +public class SelectXMLParseTest { + + @Test + public void testXmlParseContent() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(content a) FROM mytable"); + } + + @Test + public void testXmlParseDocument() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(document a) FROM mytable"); + } + + @Test + public void testXmlParseUpperCaseKeyword() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(CONTENT a) FROM mytable"); + } + + @Test + public void testXmlAggWithXmlParse() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "SELECT xmlagg(xmlparse(content sn.CODE || ',') ORDER BY sn.CODE).getclobval() AS SN FROM GV_SYS_CODEINFO sn"); + } + + @Test + public void testXmlElementName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlelement(name a, b) FROM mytable"); + } + + @Test + public void testXmlForestName() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT xmlforest(name a) FROM mytable"); + } + + /** + * CONTENT, DOCUMENT and NAME must stay plain identifiers everywhere else. + */ + @Test + public void testKeywordsRemainIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT content, document, name FROM mytable"); + assertSqlCanBeParsedAndDeparsed("SELECT content FROM content c WHERE c.document = 1"); + assertSqlCanBeParsedAndDeparsed("SELECT t.name AS document FROM mytable t"); + assertSqlCanBeParsedAndDeparsed("SELECT f(content) FROM mytable"); + assertSqlCanBeParsedAndDeparsed("SELECT xmlparse(content) FROM mytable"); + } +}