diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 92f99568d..3b1cc5a1c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1712,6 +1712,21 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + private boolean isLargeObjectTypeAhead() { + int offset = 1; + if (isKeywordAhead("NATIONAL")) { + offset++; + if (!"CHARACTER".equalsIgnoreCase(getToken(offset).image)) { + return false; + } + } + String name = getToken(offset).image; + return ("CHARACTER".equalsIgnoreCase(name) || "CHAR".equalsIgnoreCase(name) + || "NCHAR".equalsIgnoreCase(name) || "BINARY".equalsIgnoreCase(name)) + && "LARGE".equalsIgnoreCase(getToken(offset + 1).image) + && "OBJECT".equalsIgnoreCase(getToken(offset + 2).image); + } + /** * Extracts the numeric precision embedded in a DT_ZONE token image such as * "TIMESTAMP(3) WITH TIME ZONE", or null when the image carries no parameter. @@ -13586,6 +13601,22 @@ ColDataType DataType(): } } +/** Shared SQL large-object names; length and array suffixes stay in ColDataType. */ +String LargeObjectTypeName(): +{ + Token national = null; + Token type; + Token large; + Token object; +} +{ + [ LOOKAHEAD() national= ] + ( type= | type= | type= | type= ) + large= object= + { return (national == null ? "" : national.image + " ") + + type.image + " " + large.image + " " + object.image; } +} + ColDataType ColDataType(): { ColDataType colDataType = new ColDataType(); @@ -13612,6 +13643,9 @@ ColDataType ColDataType(): } { ( + LOOKAHEAD({ isLargeObjectTypeAhead() }) type=LargeObjectTypeName() + { colDataType.setDataType(type); } + | LOOKAHEAD({ isXmlTypeName(getToken(1).image) && "(".equals(getToken(2).image) }) ( tk= | tk= ) { colDataType.setDataType(tk.image); } diff --git a/src/test/java/net/sf/jsqlparser/parser/LargeObjectTypeTest.java b/src/test/java/net/sf/jsqlparser/parser/LargeObjectTypeTest.java new file mode 100644 index 000000000..1c7a1a0ce --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LargeObjectTypeTest.java @@ -0,0 +1,89 @@ +/*- + * #%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.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.CastExpression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import net.sf.jsqlparser.statement.select.PlainSelect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class LargeObjectTypeTest { + @ParameterizedTest + @ValueSource(strings = {"CHARACTER LARGE OBJECT", "CHAR LARGE OBJECT", "NCHAR LARGE OBJECT", + "NATIONAL CHARACTER LARGE OBJECT", "BINARY LARGE OBJECT", "character large object"}) + void sharesTypeNameAndLengthAcrossTypeFragmentsCastsAndColumns(String name) throws Exception { + ColDataType type = CCJSqlParserUtil.parseColDataType(name + "(3)"); + assertEquals(name, type.getDataType()); + assertEquals(3, type.getPrecision()); + assertEquals(Arrays.asList("3"), type.getArgumentsStringList()); + assertEquals(name, CCJSqlParserUtil.parseColDataType(name).getDataType()); + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT CAST('abc' AS " + name + "(3)) FROM t"); + CastExpression cast = (CastExpression) select.getSelectItem(0).getExpression(); + assertEquals(name, cast.getColDataType().getDataType()); + assertEquals(3, cast.getColDataType().getPrecision()); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (payload " + name + "(3) NOT NULL)"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD payload " + name + "(3)"); + assertEquals(type.toString(), + CCJSqlParserUtil.parseColDataType(type.toString()).toString()); + } + + @Test + void parsesTheDownstreamConditionWithoutRewritingLiteralContents() throws Exception { + String sql = "(status IN (CAST('hi' AS CHARACTER LARGE OBJECT(2)), " + + "CAST('low' AS CHARACTER LARGE OBJECT(3))))"; + var condition = CCJSqlParserUtil.parseCondExpression(sql, false); + List literals = new ArrayList<>(); + condition.accept(new ExpressionVisitorAdapter() { + @Override + public Void visit(StringValue value, S context) { + literals.add(value.getValue()); + return null; + } + }, null); + assertEquals(Arrays.asList("hi", "low"), literals); + assertEquals(condition.toString(), + CCJSqlParserUtil.parseCondExpression(condition.toString(), false).toString()); + assertSqlCanBeParsedAndDeparsed("SELECT CAST('CHARACTER LARGE OBJECT' " + + "AS CHARACTER LARGE OBJECT(22)) FROM t"); + } + + @Test + void acceptsTokenWhitespaceWithoutChangingIdentifiersOrExistingTypes() throws Exception { + assertEquals("CHARACTER LARGE OBJECT", + CCJSqlParserUtil.parseColDataType("CHARACTER /* comment */ LARGE\nOBJECT") + .getDataType()); + for (String type : Arrays.asList("CHARACTER VARYING(3)", "NATIONAL CHARACTER(3)", + "CLOB", "BLOB", "\"CHARACTER LARGE OBJECT\"", "app.large")) { + ColDataType parsed = CCJSqlParserUtil.parseColDataType(type); + assertEquals(parsed.toString(), + CCJSqlParserUtil.parseColDataType(parsed.toString()).toString()); + } + assertSqlCanBeParsedAndDeparsed("SELECT large, object FROM t"); + } + + @ParameterizedTest + @ValueSource(strings = {"CHARACTER LARGE", "CHARACTER LARGE OTHER", "INT LARGE OBJECT"}) + void rejectsIncompleteOrNonCharacterTypeNames(String type) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parseColDataType(type)); + } +}