diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e8a6d6d19..dc8d14836 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -13044,15 +13044,12 @@ CreateTable CreateTable(boolean isUsingOrReplace): { CreateTable createTable = new CreateTable(); Table table = null; - List columnDefinitions = new ArrayList(); List tableElements = new ArrayList(); + TableElement element; List tableOptions = new ArrayList(); List typedTableOptions = new ArrayList(); List createOptions = new ArrayList(); Token tk = null; - ColumnDefinition coldef = null; - List indexes = new ArrayList(); - Index index = null; List parameter = new ArrayList(); TableOption tableOption = null; SpannerInterleaveIn interleaveIn = null; @@ -13065,7 +13062,6 @@ CreateTable CreateTable(boolean isUsingOrReplace): Table partitionOfTable = null; PartitionBound partitionBound = null; ColDataType ofType = null; - LikeClause likeClause = null; } { { createTable.setOrReplace(isUsingOrReplace);} @@ -13088,33 +13084,12 @@ CreateTable CreateTable(boolean isUsingOrReplace): ) | ( - "(" - ( - LOOKAHEAD() likeClause=LikeClause() { tableElements.add(likeClause); } - | - LOOKAHEAD(3) index = CreateTableConstraint() - { indexes.add(index); tableElements.add(index); } - | - coldef = CreateTableColumnDefinition(ofType != null) - { columnDefinitions.add(coldef); tableElements.add(coldef); } - ) - - ( - "," - ( - LOOKAHEAD() likeClause=LikeClause() { tableElements.add(likeClause); } - | - LOOKAHEAD(3) ( - index = CreateTableConstraint() - { indexes.add(index); tableElements.add(index); } - ) - | - ( - coldef = CreateTableColumnDefinition(ofType != null) - { columnDefinitions.add(coldef); tableElements.add(coldef); } - ) - ) + "(" element=CreateTableElement(ofType != null) { tableElements.add(element); } + ( LOOKAHEAD(2) "," element=CreateTableElement(ofType != null) + { tableElements.add(element); } )* + [ LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_COMMA && getToken(2).kind == CLOSING_BRACKET }) "," ] ")" ) @@ -13163,6 +13138,17 @@ CreateTable CreateTable(boolean isUsingOrReplace): } } +TableElement CreateTableElement(boolean typed): +{ TableElement element; } +{ + ( + LOOKAHEAD() element=LikeClause() + | LOOKAHEAD(3) element=CreateTableConstraint() + | element=CreateTableColumnDefinition(typed) + ) + { return element; } +} + ColumnDefinition CreateTableColumnDefinition(boolean typed): { ColumnDefinition column = null; diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 35bdc082a..124b6108a 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -770,6 +770,8 @@ With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and ``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()`` for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words retain their existing interpretation as optional index names. +SQL Server ``CREATE TABLE`` also accepts a trailing comma after the final column +or table constraint. SQL output normalizes the definition by omitting that comma. ``CREATE UNIQUE NONCLUSTERED INDEX ix ON t (id)`` also requires ``Dialect.SQLSERVER``. Uniqueness remains in ``Index.getType()`` and clustering diff --git a/src/test/java/net/sf/jsqlparser/statement/create/SqlServerCreateTableSeparatorTest.java b/src/test/java/net/sf/jsqlparser/statement/create/SqlServerCreateTableSeparatorTest.java new file mode 100644 index 000000000..bf850f020 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/SqlServerCreateTableSeparatorTest.java @@ -0,0 +1,89 @@ +/*- + * #%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; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class SqlServerCreateTableSeparatorTest { + private CreateTable parse(String sql) throws JSQLParserException { + return (CreateTable) CCJSqlParserUtil.parse(sql, + p -> p.withDialect(Dialect.SQLSERVER).withUnsupportedStatements(false)); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (id int,)", + "CREATE TABLE t (id int, name varchar(20),)", + "CREATE TABLE t (id int, PRIMARY KEY (id),)", + "CREATE TABLE t (id int, CHECK (id > 0),)", + "CREATE TABLE t (id int, /* last element */ )"}) + void acceptsOneTrailingSeparatorAndNormalizesBothRenderers(String sql) throws Exception { + CreateTable table = parse(sql); + String normalized = table.toString(); + assertFalse(normalized.contains(",)")); + assertEquals(normalized, parse(normalized).toString()); + StringBuilder buffer = new StringBuilder(); + table.accept(new StatementDeParser(buffer), null); + assertEquals(normalized, buffer.toString()); + assertEquals(table.getTableElements().size(), + parse(buffer.toString()).getTableElements().size()); + } + + @Test + void preservesSakilaColumnsAndNonclusteredPrimaryKey() throws Exception { + CreateTable table = parse("CREATE TABLE film_text (film_id INT NOT NULL, " + + "title VARCHAR(255) NOT NULL, description TEXT, PRIMARY KEY NONCLUSTERED (film_id),)"); + assertEquals(4, table.getTableElements().size()); + assertInstanceOf(ColumnDefinition.class, table.getTableElements().get(0)); + Index primaryKey = assertInstanceOf(Index.class, table.getTableElements().get(3)); + assertEquals("PRIMARY KEY", primaryKey.getType()); + assertEquals(Index.Clustering.NONCLUSTERED, primaryKey.getClustering()); + assertEquals(3, table.getColumnDefinitions().size()); + table.getColumnDefinitions().get(0).setColumnName("renamed_id"); + assertTrue(table.toString().contains("renamed_id INT")); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1;", + p -> p.withDialect(Dialect.SQLSERVER)).size()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE TABLE t (,)", + "CREATE TABLE t (, id int)", "CREATE TABLE t (id int,,)", + "CREATE TABLE t (id int,, name int)", "CREATE TABLE t (id int, PRIMARY KEY,)", + "CREATE TABLE t (id int, PRIMARY KEY (id,))", + "CREATE FUNCTION f() RETURNS @r TABLE (id int,) AS BEGIN RETURN; END"}) + void rejectsMissingElementsAndDoesNotWidenOtherDefinitionLists(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void retainsOtherDialectsAndCreateTableForms() throws Exception { + String trailing = "CREATE TABLE t (id int,)"; + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(trailing)); + for (Dialect dialect : new Dialect[] {Dialect.POSTGRESQL, Dialect.MYSQL, Dialect.ORACLE}) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse(trailing, p -> p.withDialect(dialect))); + } + for (String sql : new String[] {"CREATE TABLE t (id int, PRIMARY KEY (id))", + "CREATE TABLE t (id) AS SELECT 1", "CREATE TABLE t AS SELECT 1", + "CREATE TABLE t ()"}) { + assertEquals(CCJSqlParserUtil.parse(sql).toString(), parse(sql).toString()); + } + } +}