Skip to content

Commit c28aa52

Browse files
committed
feat: expose base type names and retain large numeric lengths
1 parent 0036c75 commit c28aa52

4 files changed

Lines changed: 276 additions & 23 deletions

File tree

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

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import net.sf.jsqlparser.statement.select.PlainSelect;
1414

1515
import java.io.Serializable;
16+
import java.math.BigInteger;
1617
import java.util.ArrayList;
1718
import java.util.Collection;
1819
import java.util.Collections;
@@ -43,7 +44,8 @@ public enum NationalCharacterType {
4344
private List<Integer> arrayData = new ArrayList<Integer>();
4445
private Signedness signedness;
4546
private boolean zerofill;
46-
private Integer precision;
47+
private BigInteger precision;
48+
private boolean maxPrecision;
4749
private Integer scale;
4850
private List<TypeModifier> typeModifiers;
4951
private NationalCharacterType nationalCharacterType;
@@ -54,16 +56,29 @@ public ColDataType() {
5456
}
5557

5658
public ColDataType(String dataType, int precision, int scale) {
57-
this.dataType = dataType;
59+
this(dataType);
60+
setNumericTypeParameters(precision < 0 ? null
61+
: precision == Integer.MAX_VALUE ? "MAX" : Integer.toString(precision),
62+
scale < 0 ? null : Integer.valueOf(scale));
63+
}
5864

59-
if (precision >= 0) {
60-
this.precision = precision;
61-
this.dataType += " (" + (precision == Integer.MAX_VALUE ? "MAX" : precision);
62-
if (scale >= 0) {
63-
this.scale = scale;
64-
this.dataType += ", " + scale;
65-
}
66-
this.dataType += ")";
65+
/**
66+
* Creates a type from its numeric parameter spelling. Unlike the legacy primitive constructor,
67+
* a numeric 2147483647 is distinct from MAX and larger lengths are retained without narrowing.
68+
*/
69+
public static ColDataType fromTypeParameters(String dataType, String precision, Integer scale) {
70+
ColDataType type = new ColDataType(dataType);
71+
type.setNumericTypeParameters(precision, scale);
72+
return type;
73+
}
74+
75+
private void setNumericTypeParameters(String parameter, Integer scale) {
76+
if (parameter != null) {
77+
maxPrecision = "MAX".equalsIgnoreCase(parameter);
78+
precision = maxPrecision ? null : new BigInteger(parameter);
79+
this.scale = scale;
80+
dataType += " (" + (maxPrecision ? "MAX" : precision)
81+
+ (scale != null ? ", " + scale : "") + ")";
6782
}
6883
}
6984

@@ -83,6 +98,54 @@ public String getDataType() {
8398
return dataType;
8499
}
85100

101+
/**
102+
* Returns the type name without parenthesized parameters, retaining qualification, quoting and
103+
* multiword names such as TIMESTAMP WITH TIME ZONE. The legacy {@link #getDataType()} spelling
104+
* is unchanged. Quoted parentheses are part of an identifier and are retained.
105+
*/
106+
public String getBaseTypeName() {
107+
if (dataType == null) {
108+
return null;
109+
}
110+
StringBuilder name = new StringBuilder();
111+
int depth = 0;
112+
char quote = 0;
113+
for (int i = 0; i < dataType.length(); i++) {
114+
char c = dataType.charAt(i);
115+
if (quote != 0) {
116+
if (depth == 0) {
117+
name.append(c);
118+
}
119+
if (c == quote) {
120+
if (i + 1 < dataType.length() && dataType.charAt(i + 1) == quote) {
121+
if (depth == 0) {
122+
name.append(quote);
123+
}
124+
i++;
125+
} else {
126+
quote = 0;
127+
}
128+
}
129+
} else if (c == '\'' || c == '"' || c == '`' || c == '[') {
130+
quote = c == '[' ? ']' : c;
131+
if (depth == 0) {
132+
name.append(c);
133+
}
134+
} else if (c == '(') {
135+
depth++;
136+
while (name.length() > 0
137+
&& Character.isWhitespace(name.charAt(name.length() - 1))) {
138+
name.setLength(name.length() - 1);
139+
}
140+
} else if (c == ')' && depth > 0) {
141+
depth--;
142+
} else if (depth == 0) {
143+
name.append(c);
144+
}
145+
}
146+
return name.toString().trim();
147+
}
148+
86149
public void setDataType(String string) {
87150
dataType = string;
88151
}
@@ -200,14 +263,35 @@ public ColDataType withXmlTypeModifier(XmlTypeModifier xmlTypeModifier) {
200263
* The first numeric type parameter, e.g. {@code 255} for {@code VARCHAR(255)} or {@code 10} for
201264
* {@code DECIMAL(10, 2)}. {@code MAX} is reported as {@link Integer#MAX_VALUE}. Returns
202265
* {@code null} when the type carries no numeric parameters, e.g. {@code INT} or
203-
* {@code ENUM('a', 'b')}.
266+
* {@code ENUM('a', 'b')}, or when a numeric length exceeds the integer range. Use
267+
* {@link #getNumericPrecision()} for the full range and {@link #isMaxPrecision()} to
268+
* distinguish MAX from the numeric value 2147483647.
204269
*/
205270
public Integer getPrecision() {
206-
return precision;
271+
return maxPrecision ? Integer.valueOf(Integer.MAX_VALUE)
272+
: precision != null && precision.bitLength() < Integer.SIZE
273+
? Integer.valueOf(precision.intValue())
274+
: null;
207275
}
208276

209277
public void setPrecision(Integer precision) {
278+
setNumericPrecision(precision == null ? null : BigInteger.valueOf(precision));
279+
}
280+
281+
/** Returns the exact numeric parameter, or null for an omitted parameter or MAX. */
282+
public BigInteger getNumericPrecision() {
283+
return precision;
284+
}
285+
286+
/** Updates numeric metadata without changing the legacy rendered type spelling. */
287+
public void setNumericPrecision(BigInteger precision) {
210288
this.precision = precision;
289+
maxPrecision = false;
290+
}
291+
292+
/** Distinguishes the MAX keyword from an equal numeric value in the legacy accessor. */
293+
public boolean isMaxPrecision() {
294+
return maxPrecision;
211295
}
212296

213297
/**

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14929,14 +14929,14 @@ ColDataType DataType():
1492914929
ColDataType arrayType;
1493014930
Integer zonePrecision = null;
1493114931

14932-
int precision = -1;
14932+
String precision = null;
1493314933
int scale = -1;
1493414934
}
1493514935
{
1493614936
(
1493714937
LOOKAHEAD(2) tk=<K_TEXT_LITERAL> {
1493814938
type = tk.image;
14939-
return new ColDataType(type, precision, scale);
14939+
return ColDataType.fromTypeParameters(type, precision, scale < 0 ? null : Integer.valueOf(scale));
1494014940
}
1494114941
|
1494214942
LOOKAHEAD(2) tk=<K_ARRAY_LITERAL> (
@@ -14963,13 +14963,13 @@ ColDataType DataType():
1496314963
| tk=<K_CHAR> | tk=<K_JSON> | tk=<K_STRING> ) { type += " " + tk.image; }
1496414964
)*
1496514965
[
14966-
LOOKAHEAD(2) "(" ( tk=<S_LONG> { precision = Integer.valueOf(tk.image); } | tk=<K_MAX> { precision = Integer.MAX_VALUE; } )
14966+
LOOKAHEAD(2) "(" ( tk=<S_LONG> { precision = tk.image; } | tk=<K_MAX> { precision = "MAX"; } )
1496714967
[ <K_BYTE> | <K_CHAR> ]
1496814968
[ "," tk = <S_LONG> { scale = Integer.valueOf(tk.image); } ]
1496914969
")"
1497014970
]
1497114971
{
14972-
colDataType = new ColDataType(type, precision, scale);
14972+
colDataType = ColDataType.fromTypeParameters(type, precision, scale < 0 ? null : Integer.valueOf(scale));
1497314973
if (zonePrecision != null) {
1497414974
colDataType.setPrecision(zonePrecision);
1497514975
}
@@ -15158,10 +15158,10 @@ ColDataType ColDataType():
1515815158
colDataType.setArgumentsStringList(argumentsStringList);
1515915159
// Digits-only arguments are the type's numeric parameters, e.g. mediumint(9).
1516015160
if (argumentsStringList.size() == 1 && argumentsStringList.get(0).matches("\\d+")) {
15161-
colDataType.setPrecision(Integer.valueOf(argumentsStringList.get(0)));
15161+
colDataType.setNumericPrecision(new java.math.BigInteger(argumentsStringList.get(0)));
1516215162
} else if (argumentsStringList.size() == 2 && argumentsStringList.get(0).matches("\\d+")
1516315163
&& argumentsStringList.get(1).matches("\\d+")) {
15164-
colDataType.setPrecision(Integer.valueOf(argumentsStringList.get(0)));
15164+
colDataType.setNumericPrecision(new java.math.BigInteger(argumentsStringList.get(0)));
1516515165
colDataType.setScale(Integer.valueOf(argumentsStringList.get(1)));
1516615166
}
1516715167
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.table;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertFalse;
14+
import static org.junit.jupiter.api.Assertions.assertNull;
15+
import static org.junit.jupiter.api.Assertions.assertThrows;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
18+
import java.math.BigInteger;
19+
import java.util.List;
20+
import net.sf.jsqlparser.JSQLParserException;
21+
import net.sf.jsqlparser.expression.CastExpression;
22+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
23+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
24+
import net.sf.jsqlparser.statement.select.PlainSelect;
25+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.CsvSource;
29+
import org.junit.jupiter.params.provider.ValueSource;
30+
31+
class TypeMetadataTest {
32+
33+
@ParameterizedTest
34+
@CsvSource(delimiter = '|', value = {
35+
"MYSQL | DECIMAL(65, 30) | DECIMAL | 65 | 30",
36+
"MYSQL | NUMERIC(1, 0) | NUMERIC | 1 | 0",
37+
"MYSQL | VARCHAR(255) | VARCHAR | 255 |",
38+
"MYSQL | NATIONAL VARCHAR(20) | NATIONAL VARCHAR | 20 |",
39+
"MYSQL | mediumint(9) UNSIGNED | mediumint | 9 |",
40+
"MYSQL | ENUM('a(b)', 'c') | ENUM | |",
41+
"POSTGRESQL | NUMERIC(1000, 1000) | NUMERIC | 1000 | 1000",
42+
"POSTGRESQL | NUMERIC(3, 5) | NUMERIC | 3 | 5",
43+
"POSTGRESQL | DECIMAL(10) | DECIMAL | 10 |",
44+
"POSTGRESQL | CHARACTER VARYING(255) | CHARACTER VARYING | 255 |",
45+
"POSTGRESQL | TIMESTAMP(6) WITH TIME ZONE | TIMESTAMP WITH TIME ZONE | 6 |",
46+
"POSTGRESQL | TIME(0) WITHOUT TIME ZONE | TIME WITHOUT TIME ZONE | 0 |",
47+
"POSTGRESQL | NUMERIC(10, 2)[] | NUMERIC | 10 | 2",
48+
"POSTGRESQL | INTERVAL(6) | INTERVAL | 6 |"})
49+
void exposesMetadataAcrossFragmentsDdlAndPostgreSqlCasts(Dialect dialect, String typeName,
50+
String base, Integer precision, Integer scale) throws JSQLParserException {
51+
ColDataType fragment = parseType(typeName, dialect);
52+
assertMetadata(fragment, base, precision, scale);
53+
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
54+
"CREATE TABLE type_case (v " + typeName + ")", p -> p.withDialect(dialect));
55+
assertMetadata(table.getColumnDefinitions().get(0).getColDataType(), base, precision,
56+
scale);
57+
assertMetadata(parseType(fragment.toString(), dialect), base, precision, scale);
58+
if (dialect == Dialect.POSTGRESQL) {
59+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
60+
"SELECT CAST(NULL AS " + typeName + ")", p -> p.withDialect(dialect));
61+
CastExpression cast = select.getSelectItem(0).getExpression(CastExpression.class);
62+
assertMetadata(cast.getColDataType(), base, precision, scale);
63+
}
64+
}
65+
66+
// MySQL BLOB/TEXT family boundaries and the signed-int boundary, verified on MySQL 8.4.
67+
@ParameterizedTest
68+
@ValueSource(strings = {"0", "1", "255", "256", "65535", "65536", "16777215",
69+
"16777216", "2147483647", "2147483648", "4294967295"})
70+
void retainsLargeMySqlLengthsThroughBothRenderers(String length) throws JSQLParserException {
71+
BigInteger expected = new BigInteger(length);
72+
for (String name : List.of("BLOB", "TEXT")) {
73+
ColDataType fragment = parseType(name + "(" + length + ")", Dialect.MYSQL);
74+
assertEquals(name, fragment.getBaseTypeName());
75+
assertEquals(expected, fragment.getNumericPrecision());
76+
assertFalse(fragment.isMaxPrecision());
77+
assertFalse(fragment.toString().contains("MAX"));
78+
assertEquals(expected.bitLength() < Integer.SIZE ? Integer.valueOf(length) : null,
79+
fragment.getPrecision());
80+
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
81+
"CREATE TABLE type_case (v " + name + "(" + length + "))",
82+
p -> p.withDialect(Dialect.MYSQL));
83+
StringBuilder visitor = new StringBuilder();
84+
table.accept(new StatementDeParser(visitor));
85+
for (String rendered : List.of(table.toString(), visitor.toString())) {
86+
CreateTable reparsed = (CreateTable) CCJSqlParserUtil.parse(rendered,
87+
p -> p.withDialect(Dialect.MYSQL));
88+
ColDataType type = reparsed.getColumnDefinitions().get(0).getColDataType();
89+
assertEquals(expected, type.getNumericPrecision());
90+
assertFalse(type.isMaxPrecision());
91+
}
92+
}
93+
}
94+
95+
@Test
96+
void distinguishesMaxKeywordFromItsLegacySentinelValue() throws JSQLParserException {
97+
ColDataType max = parseType("VARCHAR(MAX)", Dialect.SQLSERVER);
98+
assertTrue(max.isMaxPrecision());
99+
assertNull(max.getNumericPrecision());
100+
assertEquals(Integer.MAX_VALUE, max.getPrecision());
101+
assertEquals("VARCHAR (MAX)", max.getDataType());
102+
ColDataType literal = parseType("BLOB(2147483647)", Dialect.MYSQL);
103+
assertFalse(literal.isMaxPrecision());
104+
assertEquals(BigInteger.valueOf(Integer.MAX_VALUE), literal.getNumericPrecision());
105+
assertEquals("BLOB (2147483647)", literal.getDataType());
106+
assertEquals("VARCHAR (MAX)", new ColDataType("VARCHAR", Integer.MAX_VALUE, -1).toString());
107+
assertNull(new ColDataType("VARCHAR", -1, -1).getNumericPrecision());
108+
}
109+
110+
@Test
111+
void metadataSettersDoNotLeaveStaleLargeOrMaxValues() throws JSQLParserException {
112+
ColDataType type = parseType("BLOB(4294967295)", Dialect.MYSQL);
113+
type.setPrecision(5);
114+
assertEquals(BigInteger.valueOf(5), type.getNumericPrecision());
115+
type.setPrecision(null);
116+
assertNull(type.getNumericPrecision());
117+
type = parseType("VARCHAR(MAX)", Dialect.SQLSERVER);
118+
type.setNumericPrecision(BigInteger.valueOf(2147483648L));
119+
assertFalse(type.isMaxPrecision());
120+
assertNull(type.getPrecision());
121+
assertEquals(BigInteger.valueOf(2147483648L), type.getNumericPrecision());
122+
}
123+
124+
@ParameterizedTest
125+
@ValueSource(strings = {"\"odd(type)\"", "public.\"odd(type)\"", "\"a\"\"(b)\""})
126+
void retainsQuotedParenthesesAndQualification(String name) throws JSQLParserException {
127+
assertEquals(name, parseType(name, Dialect.POSTGRESQL).getBaseTypeName());
128+
ColDataType manual = new ColDataType(name, 10, 2);
129+
assertEquals(name, manual.getBaseTypeName());
130+
assertEquals(name + " (10, 2)", manual.getDataType());
131+
}
132+
133+
@Test
134+
void baseNameFollowsLegacyTypeEditsWithoutStaleCache() {
135+
ColDataType type = new ColDataType("DECIMAL", 10, 2);
136+
type.setDataType("VARCHAR (20)");
137+
assertEquals("VARCHAR", type.getBaseTypeName());
138+
assertEquals("VARCHAR (20)", type.getDataType());
139+
type.setDataType(List.of("app", "\"kind(1)\""));
140+
assertEquals("app.\"kind(1)\"", type.getBaseTypeName());
141+
type.setDataType((String) null);
142+
assertNull(type.getBaseTypeName());
143+
}
144+
145+
@ParameterizedTest
146+
@ValueSource(strings = {"BLOB(-1)", "BLOB(1.5)", "BLOB(4294967295", "DECIMAL(10,)"})
147+
void rejectsMalformedNumericParameters(String type) {
148+
assertThrows(JSQLParserException.class, () -> parseType(type, Dialect.MYSQL));
149+
}
150+
151+
@Test
152+
void leavesDatabaseSpecificRangeValidationToTheDatabase() throws JSQLParserException {
153+
// Syntactically numeric, but MySQL rejects this BLOB length as out of range.
154+
String length = "18446744073709551616";
155+
assertEquals(new BigInteger(length),
156+
parseType("BLOB(" + length + ")", Dialect.MYSQL).getNumericPrecision());
157+
}
158+
159+
private static ColDataType parseType(String sql, Dialect dialect) throws JSQLParserException {
160+
return CCJSqlParserUtil.parseColDataType(sql, p -> p.withDialect(dialect));
161+
}
162+
163+
private static void assertMetadata(ColDataType type, String base, Integer precision,
164+
Integer scale) {
165+
assertEquals(base, type.getBaseTypeName());
166+
assertEquals(precision, type.getPrecision());
167+
assertEquals(precision == null ? null : BigInteger.valueOf(precision),
168+
type.getNumericPrecision());
169+
assertEquals(scale, type.getScale());
170+
}
171+
}

‎src/test/java/net/sf/jsqlparser/test/TestUtils.java‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,9 @@ protected void appendDetail(final StringBuffer buffer, final String fieldName,
287287

288288
@Override
289289
protected boolean accept(Class<?> clazz) {
290-
if (includingASTNode) {
291-
return super.accept(clazz);
292-
} else {
293-
return isNotANode(clazz) && super.accept(clazz);
294-
}
290+
// Numeric values have value semantics; do not reflect into encapsulated JDK internals.
291+
return !Number.class.isAssignableFrom(clazz)
292+
&& (includingASTNode || isNotANode(clazz)) && super.accept(clazz);
295293
}
296294

297295
public boolean isNotANode(Class<?> clazz) {

0 commit comments

Comments
 (0)