Skip to content

Commit 4eaa336

Browse files
mjmj
authored andcommitted
Accept SQL Server CREATE TABLE trailing commas with shared element parsing
1 parent a206b86 commit 4eaa336

3 files changed

Lines changed: 108 additions & 31 deletions

File tree

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

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13044,15 +13044,12 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1304413044
{
1304513045
CreateTable createTable = new CreateTable();
1304613046
Table table = null;
13047-
List<ColumnDefinition> columnDefinitions = new ArrayList<ColumnDefinition>();
1304813047
List<TableElement> tableElements = new ArrayList<TableElement>();
13048+
TableElement element;
1304913049
List<String> tableOptions = new ArrayList<String>();
1305013050
List<TableOption> typedTableOptions = new ArrayList<TableOption>();
1305113051
List<String> createOptions = new ArrayList<String>();
1305213052
Token tk = null;
13053-
ColumnDefinition coldef = null;
13054-
List<Index> indexes = new ArrayList<Index>();
13055-
Index index = null;
1305613053
List<String> parameter = new ArrayList<String>();
1305713054
TableOption tableOption = null;
1305813055
SpannerInterleaveIn interleaveIn = null;
@@ -13065,7 +13062,6 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1306513062
Table partitionOfTable = null;
1306613063
PartitionBound partitionBound = null;
1306713064
ColDataType ofType = null;
13068-
LikeClause likeClause = null;
1306913065
}
1307013066
{
1307113067
{ createTable.setOrReplace(isUsingOrReplace);}
@@ -13088,33 +13084,12 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1308813084
)
1308913085
|
1309013086
(
13091-
"("
13092-
(
13093-
LOOKAHEAD(<K_LIKE>) likeClause=LikeClause() { tableElements.add(likeClause); }
13094-
|
13095-
LOOKAHEAD(3) index = CreateTableConstraint()
13096-
{ indexes.add(index); tableElements.add(index); }
13097-
|
13098-
coldef = CreateTableColumnDefinition(ofType != null)
13099-
{ columnDefinitions.add(coldef); tableElements.add(coldef); }
13100-
)
13101-
13102-
(
13103-
","
13104-
(
13105-
LOOKAHEAD(<K_LIKE>) likeClause=LikeClause() { tableElements.add(likeClause); }
13106-
|
13107-
LOOKAHEAD(3) (
13108-
index = CreateTableConstraint()
13109-
{ indexes.add(index); tableElements.add(index); }
13110-
)
13111-
|
13112-
(
13113-
coldef = CreateTableColumnDefinition(ofType != null)
13114-
{ columnDefinitions.add(coldef); tableElements.add(coldef); }
13115-
)
13116-
)
13087+
"(" element=CreateTableElement(ofType != null) { tableElements.add(element); }
13088+
( LOOKAHEAD(2) "," element=CreateTableElement(ofType != null)
13089+
{ tableElements.add(element); }
1311713090
)*
13091+
[ LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect))
13092+
&& getToken(1).kind == K_COMMA && getToken(2).kind == CLOSING_BRACKET }) "," ]
1311813093

1311913094
")"
1312013095
)
@@ -13163,6 +13138,17 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1316313138
}
1316413139
}
1316513140

13141+
TableElement CreateTableElement(boolean typed):
13142+
{ TableElement element; }
13143+
{
13144+
(
13145+
LOOKAHEAD(<K_LIKE>) element=LikeClause()
13146+
| LOOKAHEAD(3) element=CreateTableConstraint()
13147+
| element=CreateTableColumnDefinition(typed)
13148+
)
13149+
{ return element; }
13150+
}
13151+
1316613152
ColumnDefinition CreateTableColumnDefinition(boolean typed):
1316713153
{
1316813154
ColumnDefinition column = null;

src/site/sphinx/usage.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,8 @@ With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
770770
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
771771
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
772772
retain their existing interpretation as optional index names.
773+
SQL Server ``CREATE TABLE`` also accepts a trailing comma after the final column
774+
or table constraint. SQL output normalizes the definition by omitting that comma.
773775

774776
``CREATE UNIQUE NONCLUSTERED INDEX ix ON t (id)`` also requires
775777
``Dialect.SQLSERVER``. Uniqueness remains in ``Index.getType()`` and clustering
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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;
11+
12+
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
14+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
15+
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
16+
import net.sf.jsqlparser.statement.create.table.CreateTable;
17+
import net.sf.jsqlparser.statement.create.table.Index;
18+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.ValueSource;
22+
23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
class SqlServerCreateTableSeparatorTest {
26+
private CreateTable parse(String sql) throws JSQLParserException {
27+
return (CreateTable) CCJSqlParserUtil.parse(sql,
28+
p -> p.withDialect(Dialect.SQLSERVER).withUnsupportedStatements(false));
29+
}
30+
31+
@ParameterizedTest
32+
@ValueSource(strings = {"CREATE TABLE t (id int,)",
33+
"CREATE TABLE t (id int, name varchar(20),)",
34+
"CREATE TABLE t (id int, PRIMARY KEY (id),)",
35+
"CREATE TABLE t (id int, CHECK (id > 0),)",
36+
"CREATE TABLE t (id int, /* last element */ )"})
37+
void acceptsOneTrailingSeparatorAndNormalizesBothRenderers(String sql) throws Exception {
38+
CreateTable table = parse(sql);
39+
String normalized = table.toString();
40+
assertFalse(normalized.contains(",)"));
41+
assertEquals(normalized, parse(normalized).toString());
42+
StringBuilder buffer = new StringBuilder();
43+
table.accept(new StatementDeParser(buffer), null);
44+
assertEquals(normalized, buffer.toString());
45+
assertEquals(table.getTableElements().size(),
46+
parse(buffer.toString()).getTableElements().size());
47+
}
48+
49+
@Test
50+
void preservesSakilaColumnsAndNonclusteredPrimaryKey() throws Exception {
51+
CreateTable table = parse("CREATE TABLE film_text (film_id INT NOT NULL, "
52+
+ "title VARCHAR(255) NOT NULL, description TEXT, PRIMARY KEY NONCLUSTERED (film_id),)");
53+
assertEquals(4, table.getTableElements().size());
54+
assertInstanceOf(ColumnDefinition.class, table.getTableElements().get(0));
55+
Index primaryKey = assertInstanceOf(Index.class, table.getTableElements().get(3));
56+
assertEquals("PRIMARY KEY", primaryKey.getType());
57+
assertEquals(Index.Clustering.NONCLUSTERED, primaryKey.getClustering());
58+
assertEquals(3, table.getColumnDefinitions().size());
59+
table.getColumnDefinitions().get(0).setColumnName("renamed_id");
60+
assertTrue(table.toString().contains("renamed_id INT"));
61+
assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1;",
62+
p -> p.withDialect(Dialect.SQLSERVER)).size());
63+
}
64+
65+
@ParameterizedTest
66+
@ValueSource(strings = {"CREATE TABLE t (,)",
67+
"CREATE TABLE t (, id int)", "CREATE TABLE t (id int,,)",
68+
"CREATE TABLE t (id int,, name int)", "CREATE TABLE t (id int, PRIMARY KEY,)",
69+
"CREATE TABLE t (id int, PRIMARY KEY (id,))",
70+
"CREATE FUNCTION f() RETURNS @r TABLE (id int,) AS BEGIN RETURN; END"})
71+
void rejectsMissingElementsAndDoesNotWidenOtherDefinitionLists(String sql) {
72+
assertThrows(JSQLParserException.class, () -> parse(sql));
73+
}
74+
75+
@Test
76+
void retainsOtherDialectsAndCreateTableForms() throws Exception {
77+
String trailing = "CREATE TABLE t (id int,)";
78+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(trailing));
79+
for (Dialect dialect : new Dialect[] {Dialect.POSTGRESQL, Dialect.MYSQL, Dialect.ORACLE}) {
80+
assertThrows(JSQLParserException.class,
81+
() -> CCJSqlParserUtil.parse(trailing, p -> p.withDialect(dialect)));
82+
}
83+
for (String sql : new String[] {"CREATE TABLE t (id int, PRIMARY KEY (id))",
84+
"CREATE TABLE t (id) AS SELECT 1", "CREATE TABLE t AS SELECT 1",
85+
"CREATE TABLE t ()"}) {
86+
assertEquals(CCJSqlParserUtil.parse(sql).toString(), parse(sql).toString());
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)