Skip to content

Commit c919a0c

Browse files
authored
feat(parser): model ColDataType precision and scale as structured properties (#2539)
ColDataType exposed type parameters in three different shapes depending on the grammar path: keyword types fold them into the dataType string ("VARCHAR (255)"), identifier types keep them in argumentsStringList (mediumint(9) -> ["9"]), and zoned types embed them in the lexer token image ("TIMESTAMP(3) WITH TIME ZONE"). A consumer that needs the precision has to re-parse the string, with a different pattern per shape. Populate structured precision and scale on all three paths. The rendered dataType string, toString(), equals() and hashCode() are unchanged; the hand-built object trees in DeclareStatementTest gain the new state.
1 parent f24f9e1 commit c919a0c

4 files changed

Lines changed: 139 additions & 6 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public class ColDataType implements Serializable {
2929
private String characterSet;
3030
private IntervalQualifier intervalQualifier;
3131
private List<Integer> arrayData = new ArrayList<Integer>();
32+
private Integer precision;
33+
private Integer scale;
3234

3335
public ColDataType() {
3436
// empty constructor
@@ -38,8 +40,10 @@ public ColDataType(String dataType, int precision, int scale) {
3840
this.dataType = dataType;
3941

4042
if (precision >= 0) {
43+
this.precision = precision;
4144
this.dataType += " (" + (precision == Integer.MAX_VALUE ? "MAX" : precision);
4245
if (scale >= 0) {
46+
this.scale = scale;
4347
this.dataType += ", " + scale;
4448
}
4549
this.dataType += ")";
@@ -94,6 +98,32 @@ public void setArrayData(List<Integer> arrayData) {
9498
this.arrayData = arrayData;
9599
}
96100

101+
/**
102+
* The first numeric type parameter, e.g. {@code 255} for {@code VARCHAR(255)} or {@code 10} for
103+
* {@code DECIMAL(10, 2)}. {@code MAX} is reported as {@link Integer#MAX_VALUE}. Returns
104+
* {@code null} when the type carries no numeric parameters, e.g. {@code INT} or
105+
* {@code ENUM('a', 'b')}.
106+
*/
107+
public Integer getPrecision() {
108+
return precision;
109+
}
110+
111+
public void setPrecision(Integer precision) {
112+
this.precision = precision;
113+
}
114+
115+
/**
116+
* The second numeric type parameter, e.g. {@code 2} for {@code DECIMAL(10, 2)}. Returns
117+
* {@code null} when absent.
118+
*/
119+
public Integer getScale() {
120+
return scale;
121+
}
122+
123+
public void setScale(Integer scale) {
124+
this.scale = scale;
125+
}
126+
97127
@Override
98128
public String toString() {
99129
StringBuilder arraySpec = new StringBuilder();
@@ -138,6 +168,16 @@ public ColDataType withArrayData(List<Integer> arrayData) {
138168
return this;
139169
}
140170

171+
public ColDataType withPrecision(Integer precision) {
172+
this.setPrecision(precision);
173+
return this;
174+
}
175+
176+
public ColDataType withScale(Integer scale) {
177+
this.setScale(scale);
178+
return this;
179+
}
180+
141181
public ColDataType addArgumentsStringList(String... argumentsStringList) {
142182
List<String> collection =
143183
Optional.ofNullable(getArgumentsStringList()).orElseGet(ArrayList::new);

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
12931293
}
12941294
}
12951295

1296+
/**
1297+
* Extracts the numeric precision embedded in a DT_ZONE token image such as
1298+
* "TIMESTAMP(3) WITH TIME ZONE", or null when the image carries no parameter.
1299+
*/
1300+
private static Integer zonedTypePrecision(String image) {
1301+
int open = image.indexOf('(');
1302+
if (open < 0) {
1303+
return null;
1304+
}
1305+
return Integer.valueOf(image.substring(open + 1, image.indexOf(')', open)).trim());
1306+
}
12961307

12971308
}
12981309

@@ -11739,6 +11750,7 @@ ColDataType DataType():
1173911750
List<Integer> array = new ArrayList<Integer>();
1174011751
List<String> name;
1174111752
ColDataType arrayType;
11753+
Integer zonePrecision = null;
1174211754

1174311755
int precision = -1;
1174411756
int scale = -1;
@@ -11759,7 +11771,12 @@ ColDataType DataType():
1175911771
(
1176011772
( tk=<K_DATETIMELITERAL> | tk=<DT_ZONE> | tk = <DATA_TYPE> | tk = <K_SIGNED> | tk = <K_UNSIGNED>
1176111773
| tk=<K_CHARACTER> | tk=<K_BIT> | tk=<K_BYTES> | tk=<K_BINARY> | tk=<K_BOOLEAN>
11762-
| tk=<K_CHAR> | tk=<K_JSON> | tk=<K_STRING> ) { type = tk.image; }
11774+
| tk=<K_CHAR> | tk=<K_JSON> | tk=<K_STRING> )
11775+
{
11776+
type = tk.image;
11777+
// A DT_ZONE image already contains its parameter, e.g. "TIMESTAMP(3) WITH TIME ZONE".
11778+
zonePrecision = tk.kind == DT_ZONE ? zonedTypePrecision(tk.image) : null;
11779+
}
1176311780
(
1176411781
// MySQL seems to allow: INT UNSIGNED. Do not consume CHARACTER when it starts
1176511782
// the trailing CHARACTER SET clause of a character type.
@@ -11776,6 +11793,9 @@ ColDataType DataType():
1177611793
]
1177711794
{
1177811795
colDataType = new ColDataType(type, precision, scale);
11796+
if (zonePrecision != null) {
11797+
colDataType.setPrecision(zonePrecision);
11798+
}
1177911799
}
1178011800
)
1178111801
)
@@ -11799,6 +11819,7 @@ ColDataType ColDataType():
1179911819
ColDataType arrayType;
1180011820
ColDataType nestedType = null;
1180111821
IntervalQualifier intervalQualifier = null;
11822+
Integer zonePrecision = null;
1180211823

1180311824
int precision = -1;
1180411825
int scale = -1;
@@ -11838,7 +11859,12 @@ ColDataType ColDataType():
1183811859
| tk=<K_PUBLIC>
1183911860
| tk=<K_DATA>
1184011861
| tk=<K_NAME>
11841-
) { schema = tk.image; }
11862+
)
11863+
{
11864+
schema = tk.image;
11865+
// A DT_ZONE image already contains its parameter, e.g. "TIMESTAMP(3) WITH TIME ZONE".
11866+
zonePrecision = tk.kind == DT_ZONE ? zonedTypePrecision(tk.image) : null;
11867+
}
1184211868

1184311869
// Consume an optional INTERVAL qualifier such as `hour to minute` or
1184411870
// `day(9) to second`. Only applicable when the matched type is an INTERVAL and
@@ -11888,8 +11914,20 @@ ColDataType ColDataType():
1188811914
[ LOOKAHEAD(2) <K_CHARACTER> <K_SET> (tk=<S_IDENTIFIER> | tk=<K_BINARY>) { colDataType.setCharacterSet(tk.image); } ]
1188911915

1189011916
{
11891-
if (argumentsStringList.size() > 0)
11917+
if (argumentsStringList.size() > 0) {
1189211918
colDataType.setArgumentsStringList(argumentsStringList);
11919+
// Digits-only arguments are the type's numeric parameters, e.g. mediumint(9).
11920+
if (argumentsStringList.size() == 1 && argumentsStringList.get(0).matches("\\d+")) {
11921+
colDataType.setPrecision(Integer.valueOf(argumentsStringList.get(0)));
11922+
} else if (argumentsStringList.size() == 2 && argumentsStringList.get(0).matches("\\d+")
11923+
&& argumentsStringList.get(1).matches("\\d+")) {
11924+
colDataType.setPrecision(Integer.valueOf(argumentsStringList.get(0)));
11925+
colDataType.setScale(Integer.valueOf(argumentsStringList.get(1)));
11926+
}
11927+
}
11928+
if (zonePrecision != null) {
11929+
colDataType.setPrecision(zonePrecision);
11930+
}
1189311931
return colDataType;
1189411932
}
1189511933
}

src/test/java/net/sf/jsqlparser/statement/DeclareStatementTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public void testDeclareType() throws JSQLParserException {
3737
DeclareStatement created = new DeclareStatement()
3838
.addTypeDefExprList(
3939
new TypeDefExpr(new UserVariable().withName("find"),
40-
new ColDataType().withDataType("nvarchar (30)"), null))
40+
new ColDataType().withDataType("nvarchar (30)").withPrecision(30),
41+
null))
4142
.withDeclareType(DeclareType.TYPE);
4243
assertDeparse(created, statement);
4344
assertEqualsObjectTree(parsed, created);
@@ -49,7 +50,7 @@ public void testDeclareTypeWithDefault() throws JSQLParserException {
4950
Statement parsed = assertSqlCanBeParsedAndDeparsed(statement);
5051
DeclareStatement created = new DeclareStatement()
5152
.addTypeDefExprList(new TypeDefExpr(new UserVariable().withName("find"),
52-
new ColDataType().withDataType("varchar (30)"),
53+
new ColDataType().withDataType("varchar (30)").withPrecision(30),
5354
new StringValue().withValue("Man%")))
5455
.withDeclareType(DeclareType.TYPE);
5556
assertDeparse(created, statement);
@@ -63,7 +64,7 @@ public void testDeclareTypeList() throws JSQLParserException {
6364
DeclareStatement created = new DeclareStatement().addTypeDefExprList(asList( //
6465
new TypeDefExpr(
6566
new UserVariable().withName("group"),
66-
new ColDataType().withDataType("nvarchar (50)"),
67+
new ColDataType().withDataType("nvarchar (50)").withPrecision(50),
6768
null),
6869
new TypeDefExpr(new UserVariable().withName("sales"),
6970
new ColDataType().withDataType("money"), null)))

src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
1818
import static org.junit.jupiter.api.Assertions.assertEquals;
1919
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
2021

2122
class ColDataTypeTest {
2223
@Test
@@ -97,4 +98,57 @@ void testCastAsIntervalWithQualifierRoundTrip() throws JSQLParserException {
9798
"SELECT CAST(col AS INTERVAL DAY TO SECOND)", true);
9899
assertSqlCanBeParsedAndDeparsed("SELECT CAST(col AS INTERVAL HOUR)", true);
99100
}
101+
102+
@Test
103+
void testStructuredPrecisionForKeywordTypes() throws JSQLParserException {
104+
ColDataType varchar = parseColumnType("CREATE TABLE t (a VARCHAR(255))");
105+
assertEquals(255, varchar.getPrecision());
106+
assertNull(varchar.getScale());
107+
// the rendered string keeps its historical shape
108+
assertEquals("VARCHAR (255)", varchar.getDataType());
109+
110+
ColDataType decimal = parseColumnType("CREATE TABLE t (a DECIMAL(10, 2))");
111+
assertEquals(10, decimal.getPrecision());
112+
assertEquals(2, decimal.getScale());
113+
assertEquals("DECIMAL (10, 2)", decimal.getDataType());
114+
115+
ColDataType max = parseColumnType("CREATE TABLE t (a VARCHAR(MAX))");
116+
assertEquals(Integer.MAX_VALUE, max.getPrecision());
117+
118+
ColDataType plain = parseColumnType("CREATE TABLE t (a INT)");
119+
assertNull(plain.getPrecision());
120+
assertNull(plain.getScale());
121+
}
122+
123+
@Test
124+
void testStructuredPrecisionForIdentifierTypes() throws JSQLParserException {
125+
ColDataType mediumInt = parseColumnType("CREATE TABLE t (a mediumint(9))");
126+
assertEquals(9, mediumInt.getPrecision());
127+
assertNull(mediumInt.getScale());
128+
// the string arguments stay available as before
129+
assertEquals(java.util.List.of("9"), mediumInt.getArgumentsStringList());
130+
131+
// non-numeric arguments are not numeric parameters
132+
ColDataType enumType = parseColumnType("CREATE TABLE t (a ENUM('small', 'medium'))");
133+
assertNull(enumType.getPrecision());
134+
assertNull(enumType.getScale());
135+
}
136+
137+
@Test
138+
void testStructuredPrecisionForZonedTypes() throws JSQLParserException {
139+
ColDataType zoned = parseColumnType("CREATE TABLE t (a TIMESTAMP(3) WITH TIME ZONE)");
140+
assertEquals(3, zoned.getPrecision());
141+
assertNull(zoned.getScale());
142+
// the token image keeps its historical shape
143+
assertEquals("TIMESTAMP(3) WITH TIME ZONE", zoned.getDataType());
144+
145+
ColDataType unparameterized =
146+
parseColumnType("CREATE TABLE t (a TIMESTAMP WITH TIME ZONE)");
147+
assertNull(unparameterized.getPrecision());
148+
}
149+
150+
private ColDataType parseColumnType(String sqlStr) throws JSQLParserException {
151+
CreateTable create = (CreateTable) assertSqlCanBeParsedAndDeparsed(sqlStr, true);
152+
return create.getColumnDefinitions().get(0).getColDataType();
153+
}
100154
}

0 commit comments

Comments
 (0)