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 a3ddcdb9f..3f922a8a6 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 @@ -29,6 +29,8 @@ public class ColDataType implements Serializable { private String characterSet; private IntervalQualifier intervalQualifier; private List arrayData = new ArrayList(); + private Integer precision; + private Integer scale; public ColDataType() { // empty constructor @@ -38,8 +40,10 @@ public ColDataType(String dataType, int precision, int scale) { this.dataType = dataType; if (precision >= 0) { + this.precision = precision; this.dataType += " (" + (precision == Integer.MAX_VALUE ? "MAX" : precision); if (scale >= 0) { + this.scale = scale; this.dataType += ", " + scale; } this.dataType += ")"; @@ -94,6 +98,32 @@ public void setArrayData(List arrayData) { this.arrayData = arrayData; } + /** + * 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')}. + */ + public Integer getPrecision() { + return precision; + } + + public void setPrecision(Integer precision) { + this.precision = precision; + } + + /** + * The second numeric type parameter, e.g. {@code 2} for {@code DECIMAL(10, 2)}. Returns + * {@code null} when absent. + */ + public Integer getScale() { + return scale; + } + + public void setScale(Integer scale) { + this.scale = scale; + } + @Override public String toString() { StringBuilder arraySpec = new StringBuilder(); @@ -138,6 +168,16 @@ public ColDataType withArrayData(List arrayData) { return this; } + public ColDataType withPrecision(Integer precision) { + this.setPrecision(precision); + return this; + } + + public ColDataType withScale(Integer scale) { + this.setScale(scale); + return this; + } + public ColDataType addArgumentsStringList(String... argumentsStringList) { List collection = Optional.ofNullable(getArgumentsStringList()).orElseGet(ArrayList::new); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8ed4a18d5..3ede11980 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1293,6 +1293,17 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * 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. + */ + private static Integer zonedTypePrecision(String image) { + int open = image.indexOf('('); + if (open < 0) { + return null; + } + return Integer.valueOf(image.substring(open + 1, image.indexOf(')', open)).trim()); + } } @@ -11661,6 +11672,7 @@ ColDataType DataType(): List array = new ArrayList(); List name; ColDataType arrayType; + Integer zonePrecision = null; int precision = -1; int scale = -1; @@ -11681,7 +11693,12 @@ ColDataType DataType(): ( ( tk= | tk= | tk = | tk = | tk = | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= ) { type = tk.image; } + | tk= | tk= | tk= ) + { + type = tk.image; + // A DT_ZONE image already contains its parameter, e.g. "TIMESTAMP(3) WITH TIME ZONE". + zonePrecision = tk.kind == DT_ZONE ? zonedTypePrecision(tk.image) : null; + } ( // MySQL seems to allow: INT UNSIGNED. Do not consume CHARACTER when it starts // the trailing CHARACTER SET clause of a character type. @@ -11698,6 +11715,9 @@ ColDataType DataType(): ] { colDataType = new ColDataType(type, precision, scale); + if (zonePrecision != null) { + colDataType.setPrecision(zonePrecision); + } } ) ) @@ -11721,6 +11741,7 @@ ColDataType ColDataType(): ColDataType arrayType; ColDataType nestedType = null; IntervalQualifier intervalQualifier = null; + Integer zonePrecision = null; int precision = -1; int scale = -1; @@ -11760,7 +11781,12 @@ ColDataType ColDataType(): | tk= | tk= | tk= - ) { schema = tk.image; } + ) + { + schema = tk.image; + // A DT_ZONE image already contains its parameter, e.g. "TIMESTAMP(3) WITH TIME ZONE". + zonePrecision = tk.kind == DT_ZONE ? zonedTypePrecision(tk.image) : null; + } // Consume an optional INTERVAL qualifier such as `hour to minute` or // `day(9) to second`. Only applicable when the matched type is an INTERVAL and @@ -11810,8 +11836,20 @@ ColDataType ColDataType(): [ LOOKAHEAD(2) (tk= | tk=) { colDataType.setCharacterSet(tk.image); } ] { - if (argumentsStringList.size() > 0) + if (argumentsStringList.size() > 0) { colDataType.setArgumentsStringList(argumentsStringList); + // Digits-only arguments are the type's numeric parameters, e.g. mediumint(9). + if (argumentsStringList.size() == 1 && argumentsStringList.get(0).matches("\\d+")) { + colDataType.setPrecision(Integer.valueOf(argumentsStringList.get(0))); + } else if (argumentsStringList.size() == 2 && argumentsStringList.get(0).matches("\\d+") + && argumentsStringList.get(1).matches("\\d+")) { + colDataType.setPrecision(Integer.valueOf(argumentsStringList.get(0))); + colDataType.setScale(Integer.valueOf(argumentsStringList.get(1))); + } + } + if (zonePrecision != null) { + colDataType.setPrecision(zonePrecision); + } return colDataType; } } diff --git a/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java index c8b5ca1ac..725a60a29 100644 --- a/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java @@ -37,7 +37,8 @@ public void testDeclareType() throws JSQLParserException { DeclareStatement created = new DeclareStatement() .addTypeDefExprList( new TypeDefExpr(new UserVariable().withName("find"), - new ColDataType().withDataType("nvarchar (30)"), null)) + new ColDataType().withDataType("nvarchar (30)").withPrecision(30), + null)) .withDeclareType(DeclareType.TYPE); assertDeparse(created, statement); assertEqualsObjectTree(parsed, created); @@ -49,7 +50,7 @@ public void testDeclareTypeWithDefault() throws JSQLParserException { Statement parsed = assertSqlCanBeParsedAndDeparsed(statement); DeclareStatement created = new DeclareStatement() .addTypeDefExprList(new TypeDefExpr(new UserVariable().withName("find"), - new ColDataType().withDataType("varchar (30)"), + new ColDataType().withDataType("varchar (30)").withPrecision(30), new StringValue().withValue("Man%"))) .withDeclareType(DeclareType.TYPE); assertDeparse(created, statement); @@ -63,7 +64,7 @@ public void testDeclareTypeList() throws JSQLParserException { DeclareStatement created = new DeclareStatement().addTypeDefExprList(asList( // new TypeDefExpr( new UserVariable().withName("group"), - new ColDataType().withDataType("nvarchar (50)"), + new ColDataType().withDataType("nvarchar (50)").withPrecision(50), null), new TypeDefExpr(new UserVariable().withName("sales"), new ColDataType().withDataType("money"), null))) 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 5d282fb5f..9a5224686 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 @@ -17,6 +17,7 @@ import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; class ColDataTypeTest { @Test @@ -97,4 +98,57 @@ void testCastAsIntervalWithQualifierRoundTrip() throws JSQLParserException { "SELECT CAST(col AS INTERVAL DAY TO SECOND)", true); assertSqlCanBeParsedAndDeparsed("SELECT CAST(col AS INTERVAL HOUR)", true); } + + @Test + void testStructuredPrecisionForKeywordTypes() throws JSQLParserException { + ColDataType varchar = parseColumnType("CREATE TABLE t (a VARCHAR(255))"); + assertEquals(255, varchar.getPrecision()); + assertNull(varchar.getScale()); + // the rendered string keeps its historical shape + assertEquals("VARCHAR (255)", varchar.getDataType()); + + ColDataType decimal = parseColumnType("CREATE TABLE t (a DECIMAL(10, 2))"); + assertEquals(10, decimal.getPrecision()); + assertEquals(2, decimal.getScale()); + assertEquals("DECIMAL (10, 2)", decimal.getDataType()); + + ColDataType max = parseColumnType("CREATE TABLE t (a VARCHAR(MAX))"); + assertEquals(Integer.MAX_VALUE, max.getPrecision()); + + ColDataType plain = parseColumnType("CREATE TABLE t (a INT)"); + assertNull(plain.getPrecision()); + assertNull(plain.getScale()); + } + + @Test + void testStructuredPrecisionForIdentifierTypes() throws JSQLParserException { + ColDataType mediumInt = parseColumnType("CREATE TABLE t (a mediumint(9))"); + assertEquals(9, mediumInt.getPrecision()); + assertNull(mediumInt.getScale()); + // the string arguments stay available as before + assertEquals(java.util.List.of("9"), mediumInt.getArgumentsStringList()); + + // non-numeric arguments are not numeric parameters + ColDataType enumType = parseColumnType("CREATE TABLE t (a ENUM('small', 'medium'))"); + assertNull(enumType.getPrecision()); + assertNull(enumType.getScale()); + } + + @Test + void testStructuredPrecisionForZonedTypes() throws JSQLParserException { + ColDataType zoned = parseColumnType("CREATE TABLE t (a TIMESTAMP(3) WITH TIME ZONE)"); + assertEquals(3, zoned.getPrecision()); + assertNull(zoned.getScale()); + // the token image keeps its historical shape + assertEquals("TIMESTAMP(3) WITH TIME ZONE", zoned.getDataType()); + + ColDataType unparameterized = + parseColumnType("CREATE TABLE t (a TIMESTAMP WITH TIME ZONE)"); + assertNull(unparameterized.getPrecision()); + } + + private ColDataType parseColumnType(String sqlStr) throws JSQLParserException { + CreateTable create = (CreateTable) assertSqlCanBeParsedAndDeparsed(sqlStr, true); + return create.getColumnDefinitions().get(0).getColDataType(); + } }