From 2d6b1934943acb3a57104637c7e2477e80a82ae1 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Thu, 29 Oct 2026 09:06:15 +0100 Subject: [PATCH] feat: parse Oracle RAW(n) and NUMBER(*, s) data types DDL exported from Oracle with DBMS_METADATA.GET_DDL prints an INTEGER column as NUMBER(*,0) and binary columns as RAW(16). CREATE TABLE and ALTER TABLE with such columns failed to parse, and so did CAST(x AS NUMBER(*,0)) and CAST(x AS RAW(16)). RAW is a keyword token, so ColDataType did not accept it as a type name. NUMBER(*, s) now takes the same path as NUMBER(p, s): DataType() accepts * in its precision slot for NUMBER only, and ColDataType folds it into the type name as "NUMBER (*, 0)" with a null precision and the scale set. NUMBER(*, *) and VARCHAR(*) stay rejected. --- .../statement/create/table/ColDataType.java | 12 +++++---- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 7 ++++- .../create/table/ColDataTypeTest.java | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java index 89f3d60b76..372abc4b7e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java @@ -103,9 +103,10 @@ public static ColDataType fromTypeParameters(String dataType, String precision, private void setNumericTypeParameters(String parameter, Integer scale) { if (parameter != null) { maxPrecision = "MAX".equalsIgnoreCase(parameter); - precision = maxPrecision ? null : new BigInteger(parameter); + // Oracle's NUMBER(*, s) has no precision of its own. + precision = maxPrecision || "*".equals(parameter) ? null : new BigInteger(parameter); this.scale = scale; - dataType += " (" + (maxPrecision ? "MAX" : precision) + dataType += " (" + (maxPrecision ? "MAX" : precision != null ? precision : parameter) + (scale != null ? ", " + scale : "") + ")"; } } @@ -330,9 +331,10 @@ public ColDataType withXmlTypeModifier(XmlTypeModifier xmlTypeModifier) { * The first numeric type parameter, e.g. {@code 255} for {@code VARCHAR(255)} or {@code 10} for * {@code DECIMAL(10, 2)}. {@code MAX} is reported as {@link Integer#MAX_VALUE}. Returns * {@code null} when the type carries no numeric parameters, e.g. {@code INT} or - * {@code ENUM('a', 'b')}, or when a numeric length exceeds the integer range. Use - * {@link #getNumericPrecision()} for the full range and {@link #isMaxPrecision()} to - * distinguish MAX from the numeric value 2147483647. + * {@code ENUM('a', 'b')}, when the precision is Oracle's {@code *} as in {@code NUMBER(*, 0)}, + * or when a numeric length exceeds the integer range. Use {@link #getNumericPrecision()} for + * the full range and {@link #isMaxPrecision()} to distinguish MAX from the numeric value + * 2147483647. */ public Integer getPrecision() { return maxPrecision ? Integer.valueOf(Integer.MAX_VALUE) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 7339e68ba3..33fc4bb62d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -18902,7 +18902,10 @@ ColDataType DataType(): | tk= | tk= | tk= ) { type += " " + tk.image; } )* [ - LOOKAHEAD(2) "(" ( tk= { precision = tk.image; } | tk= { precision = "MAX"; } ) + LOOKAHEAD(2) "(" ( tk= { precision = tk.image; } | tk= { precision = "MAX"; } + // Oracle's NUMBER(*, s) and NUMBER(*) + | "*" { precision = "*"; requireDdlSyntax("NUMBER".equalsIgnoreCase(type), + "Wildcard precision is only supported for NUMBER"); } ) [ | ] [ "," scaleText=SignedIntegerTypeArgument() { scale = Integer.valueOf(scaleText); } ] ")" @@ -19045,6 +19048,8 @@ ColDataType ColDataType(): | tk= // PostgreSQL's geometric type | tk= + // Oracle's RAW(n) + | tk= ) { schema = tk.image; diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java index 9a52246864..111b8b5715 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java @@ -11,6 +11,7 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.IntervalQualifier; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -18,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; class ColDataTypeTest { @Test @@ -147,6 +149,31 @@ void testStructuredPrecisionForZonedTypes() throws JSQLParserException { assertNull(unparameterized.getPrecision()); } + @Test + void testOracleNumberWithWildcardPrecision() throws JSQLParserException { + ColDataType number = + parseColumnType("CREATE TABLE t (a NUMBER(*, 0), b NUMBER(*), c NUMBER(*, 2))"); + assertEquals("NUMBER (*, 0)", number.getDataType()); + assertEquals("NUMBER", number.getBaseTypeName()); + assertNull(number.getPrecision()); + assertEquals(0, number.getScale()); + + assertSqlCanBeParsedAndDeparsed("SELECT CAST(1.5 AS NUMBER(*, 0)) FROM dual", true); + + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parseColDataType("NUMBER(*, *)")); + } + + @Test + void testOracleRawType() throws JSQLParserException { + ColDataType raw = parseColumnType("CREATE TABLE t (id RAW(16), payload raw(2000))"); + assertEquals("RAW", raw.getDataType()); + assertEquals(16, raw.getPrecision()); + + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t ADD payload RAW(2000)", true); + assertSqlCanBeParsedAndDeparsed("SELECT CAST(HEXTORAW('AB') AS RAW(16)) FROM dual", true); + } + private ColDataType parseColumnType(String sqlStr) throws JSQLParserException { CreateTable create = (CreateTable) assertSqlCanBeParsedAndDeparsed(sqlStr, true); return create.getColumnDefinitions().get(0).getColDataType();