Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public enum NationalCharacterType {
private Integer scale;
private List<TypeModifier> typeModifiers;
private NationalCharacterType nationalCharacterType;
private XmlTypeModifier xmlTypeModifier;

public ColDataType() {
// empty constructor
Expand Down Expand Up @@ -182,6 +183,19 @@ public boolean isNational() {
return nationalCharacterType != null;
}

public XmlTypeModifier getXmlTypeModifier() {
return xmlTypeModifier;
}

public void setXmlTypeModifier(XmlTypeModifier xmlTypeModifier) {
this.xmlTypeModifier = xmlTypeModifier;
}

public ColDataType withXmlTypeModifier(XmlTypeModifier xmlTypeModifier) {
setXmlTypeModifier(xmlTypeModifier);
return this;
}

/**
* 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
Expand Down Expand Up @@ -220,6 +234,7 @@ public String toString() {
}
return dataType
+ (intervalQualifier != null ? " " + intervalQualifier.toString() : "")
+ (xmlTypeModifier != null ? " " + xmlTypeModifier : "")
+ (argumentsStringList != null
? " " + PlainSelect.getStringList(argumentsStringList, true, true)
: "")
Expand Down Expand Up @@ -330,12 +345,16 @@ public final boolean equals(Object o) {
&& signedness == that.signedness
&& zerofill == that.zerofill
&& Objects.equals(typeModifiers, that.typeModifiers)
&& Objects.equals(xmlTypeModifier, that.xmlTypeModifier)
&& nationalCharacterType == that.nationalCharacterType;
}

@Override
public int hashCode() {
int result = dataType.hashCode();
// Use the same per-code-point case folding as String.equalsIgnoreCase, including Unicode.
int result = dataType.codePoints()
.map(c -> Character.toLowerCase(Character.toUpperCase(c)))
.reduce(0, (hash, c) -> 31 * hash + c);
result = 31 * result + Objects.hashCode(argumentsStringList);
result = 31 * result + Objects.hashCode(characterSet);
result = 31 * result + Objects.hashCode(intervalQualifier);
Expand All @@ -344,6 +363,7 @@ public int hashCode() {
result = 31 * result + Boolean.hashCode(zerofill);
result = 31 * result + Objects.hashCode(typeModifiers);
result = 31 * result + Objects.hashCode(nationalCharacterType);
result = 31 * result + Objects.hashCode(xmlTypeModifier);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*-
* #%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.statement.create.table;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/** A SQL Server XML schema collection reference, with an optional CONTENT/DOCUMENT facet. */
public final class XmlTypeModifier implements Serializable {
public enum Kind {
CONTENT, DOCUMENT
}

private final Kind kind;
private final List<String> schemaCollection;

public XmlTypeModifier(Kind kind, List<String> schemaCollection) {
if (schemaCollection == null || schemaCollection.isEmpty() || schemaCollection.size() > 2
|| schemaCollection.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException(
"An XML schema collection needs a name and optional schema");
}
this.kind = kind;
this.schemaCollection = Collections.unmodifiableList(new ArrayList<>(schemaCollection));
}

/** Null preserves an omitted facet, which SQL Server interprets as CONTENT. */
public Kind getKind() {
return kind;
}

public Kind getEffectiveKind() {
return kind == null ? Kind.CONTENT : kind;
}

/** Name parts retain their original identifier quoting. */
public List<String> getSchemaCollection() {
return schemaCollection;
}

@Override
public String toString() {
return "(" + (kind == null ? "" : kind + " ") + String.join(".", schemaCollection) + ")";
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof XmlTypeModifier)) {
return false;
}
XmlTypeModifier that = (XmlTypeModifier) other;
return kind == that.kind && schemaCollection.equals(that.schemaCollection);
}

@Override
public int hashCode() {
return Objects.hash(kind, schemaCollection);
}
}
32 changes: 32 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
return token.image != null && keyword.equalsIgnoreCase(token.image);
}

private boolean isXmlTypeName(String name) {
return "XML".equalsIgnoreCase(name) || "[XML]".equalsIgnoreCase(name)
|| "\"XML\"".equalsIgnoreCase(name);
}

private boolean isMySqlTableOptionAhead() {
int kind = getToken(1).kind;
if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT
Expand Down Expand Up @@ -12668,9 +12673,15 @@ ColDataType ColDataType():
Token nationalType = null;
Token varying = null;
ColDataType.TypeModifier typeModifier = null;
XmlTypeModifier xmlTypeModifier = null;
}
{
(
LOOKAHEAD({ isXmlTypeName(getToken(1).image) && "(".equals(getToken(2).image) })
( tk=<K_XML> | tk=<S_QUOTED_IDENTIFIER> )
{ colDataType.setDataType(tk.image); }
xmlTypeModifier = XmlTypeModifier() { colDataType.setXmlTypeModifier(xmlTypeModifier); }
|
(
<K_STRUCT>
"("
Expand Down Expand Up @@ -12788,6 +12799,8 @@ ColDataType ColDataType():
[ LOOKAHEAD(2) <K_CHARACTER> <K_SET> (tk=<S_IDENTIFIER> | tk=<K_BINARY>) { colDataType.setCharacterSet(tk.image); } ]

{
requireDdlSyntax(colDataType.getXmlTypeModifier() == null || argumentsStringList.isEmpty(),
"Typed XML cannot have additional type arguments");
if (argumentsStringList.size() > 0) {
colDataType.setArgumentsStringList(argumentsStringList);
// Digits-only arguments are the type's numeric parameters, e.g. mediumint(9).
Expand All @@ -12806,6 +12819,25 @@ ColDataType ColDataType():
}
}

XmlTypeModifier XmlTypeModifier():
{
XmlTypeModifier.Kind kind = null;
List<String> names = new ArrayList<String>();
String name;
Token token;
}
{
"("
[ LOOKAHEAD({ (isKeywordAhead("CONTENT") || isKeywordAhead("DOCUMENT"))
&& !".".equals(getToken(2).image) && !")".equals(getToken(2).image) })
token=<S_IDENTIFIER> { kind = XmlTypeModifier.Kind.valueOf(token.image.toUpperCase(Locale.ROOT)); }
]
name=RelObjectName() { names.add(name); }
[ "." name=RelObjectNameExt() { names.add(name); } ]
")"
{ return new XmlTypeModifier(kind, names); }
}

ColDataType.TypeModifier MySqlTypeModifier():
{
ColDataType.TypeModifier modifier;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*-
* #%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.statement.create.table;

import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.test.TestUtils;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class SqlServerXmlTypeTest {
@ParameterizedTest
@ValueSource(strings = {
"XML(CONTENT dbo.OrderSchema)",
"XML(DOCUMENT dbo.OrderSchema)",
"XML(dbo.OrderSchema)",
"XML(OrderSchema)",
"[xml](CONTENT [Person].[AdditionalContactInfoSchemaCollection])",
"\"xml\"(DOCUMENT \"Person\".\"Order Schema\")",
"XML(CONTENT)",
"XML(DOCUMENT.Collection)",
"XML([schema.with.dot].[collection.with.dot])"
})
void roundTripTypedXml(String typeSql) throws JSQLParserException {
CreateTable table = (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(
"CREATE TABLE dbo.t (payload " + typeSql + " NULL)", true,
parser -> parser.withDialect(Dialect.SQLSERVER));
ColDataType type = table.getColumnDefinitions().get(0).getColDataType();
assertNotNull(type.getXmlTypeModifier());
assertNull(type.getArgumentsStringList());
assertNull(type.getPrecision());
assertNull(type.getScale());
assertEquals(List.of("dbo.t"), new TablesNamesFinder().getTableList(table));
}

@ParameterizedTest
@ValueSource(strings = {
"ALTER TABLE t ADD payload XML(CONTENT dbo.OrderSchema)",
"ALTER TABLE t ALTER COLUMN payload XML(DOCUMENT dbo.OrderSchema)",
"DECLARE @payload XML(CONTENT dbo.OrderSchema)",
"CREATE TABLE t (untyped XML, n DECIMAL(10, 2), s VARCHAR(20))"
})
void reuseTypeParsingAcrossStatements(String sql) throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true,
parser -> parser.withDialect(Dialect.SQLSERVER));
}

@Test
void reproduceIssue1567() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed("CREATE TABLE [Person].[Person] ("
+ "[BusinessEntityID] [int] NOT NULL, [PersonType] [nchar](2) NOT NULL, "
+ "[NameStyle] [dbo].[NameStyle] NOT NULL, [Title] [nvarchar](8) NULL, "
+ "[FirstName] [dbo].[Name] NOT NULL, [MiddleName] [dbo].[Name] NULL, "
+ "[LastName] [dbo].[Name] NOT NULL, [Suffix] [nvarchar](10) NULL, "
+ "[EmailPromotion] [int] NOT NULL, "
+ "[AdditionalContactInfo] [xml](CONTENT [Person].[AdditionalContactInfoSchemaCollection]) NULL, "
+ "[Demographics] [xml](CONTENT [Person].[IndividualSurveySchemaCollection]) NULL, "
+ "[rowguid] [uniqueidentifier] ROWGUIDCOL NOT NULL, [ModifiedDate] [datetime] NOT NULL)",
true, parser -> parser.withDialect(Dialect.SQLSERVER));
}

@ParameterizedTest
@ValueSource(strings = {"XML()", "XML(10)", "XML(OTHER dbo.c)",
"XML(CONTENT dbo.c, dbo.d)", "XML(CONTENT db.dbo.c)", "XML(CONTENT dbo.c"})
void rejectInvalidModifier(String typeSql) {
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse("CREATE TABLE t (x " + typeSql + ")",
parser -> parser.withDialect(Dialect.SQLSERVER)
.withUnsupportedStatements(false)));
}

@Test
void structuredValuesAndEquality() {
XmlTypeModifier omitted = new XmlTypeModifier(null, List.of("dbo", "c"));
assertNull(omitted.getKind());
assertEquals(XmlTypeModifier.Kind.CONTENT, omitted.getEffectiveKind());
assertThrows(UnsupportedOperationException.class,
() -> omitted.getSchemaCollection().clear());
ColDataType lower = new ColDataType("xml").withXmlTypeModifier(omitted);
ColDataType upper = new ColDataType("XML").withXmlTypeModifier(
new XmlTypeModifier(null, List.of("dbo", "c")));
assertEquals(lower, upper);
assertEquals(lower.hashCode(), upper.hashCode());
assertNotEquals(lower, new ColDataType("xml"));
assertNotEquals(omitted,
new XmlTypeModifier(XmlTypeModifier.Kind.CONTENT, List.of("dbo", "c")));
assertNotEquals(omitted, new XmlTypeModifier(null, List.of("dbo", "other")));
lower.setXmlTypeModifier(null);
assertEquals("xml", lower.toString());
}

@Test
void caseInsensitiveTypeHashAlsoHandlesUnicode() {
ColDataType dotted = new ColDataType("\u0130");
ColDataType plain = new ColDataType("i");
assertEquals(dotted, plain);
assertEquals(dotted.hashCode(), plain.hashCode());
}
}
Loading