diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java index 4dc0f69f7..2496f816d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CreateTable.java @@ -38,6 +38,7 @@ public class CreateTable implements Statement { private DuplicateHandling duplicateHandling; private Table likeTable; private Table cloneTable; + private List inherits; private ColDataType ofType; private boolean selectParenthesis; private boolean useAsKeyword = true; @@ -347,6 +348,27 @@ public void setPartitionBound(PartitionBound partitionBound) { this.partitionBound = partitionBound; } + /** PostgreSQL parent tables, in declaration order; null if INHERITS is absent. */ + public List
getInherits() { + return inherits; + } + + public void setInherits(List
inherits) { + this.inherits = inherits; + } + + public CreateTable withInherits(List
inherits) { + setInherits(inherits); + return this; + } + + /** Shared rendering of the structured parent references. */ + public void appendInheritanceTo(StringBuilder builder) { + if (inherits != null) { + builder.append(" INHERITS ").append(PlainSelect.getStringList(inherits, true, true)); + } + } + public boolean isSelectParenthesis() { return selectParenthesis; } @@ -369,6 +391,7 @@ public String toString() { StringBuilder b = new StringBuilder(); appendCreateClause(b); appendColumnDefinitions(b); + appendInheritanceTo(b); if (partitionBound != null) { b.append(" ").append(partitionBound); } diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 21a6b7361..5c6bfe7d1 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -109,6 +109,9 @@ public static void visit(CreateTable table, Consumer expressions, } }); } + if (table.getInherits() != null) { + table.getInherits().forEach(parent -> accept(parent, tables)); + } accept(table.getTrailingLikeTable(), tables); accept(table.getPartitionOf(), tables); visit(table.getPartitioning(), expressions); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java index 2d26b7a10..4ca8c4425 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateTableDeParser.java @@ -106,6 +106,7 @@ public void deParse(CreateTable createTable) { builder.append(")"); } + createTable.appendInheritanceTo(builder); if (createTable.getPartitionBound() != null) { builder.append(' '); createTable.getPartitionBound().appendTo(builder, diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 3b6e2548e..c034a4757 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14984,6 +14984,8 @@ CreateTable CreateTable(boolean isUsingOrReplace): Table partitionOfTable = null; PartitionBound partitionBound = null; ColDataType ofType = null; + Table parent; + List
parents = new ArrayList
(); boolean withData; } { @@ -15008,6 +15010,8 @@ CreateTable CreateTable(boolean isUsingOrReplace): [ LOOKAHEAD(2) partitionOfTable=Table() { createTable.setPartitionOf(partitionOfTable); } ] [ LOOKAHEAD(2) ( + LOOKAHEAD("(" ")") "(" ")" { createTable.setTableElements(tableElements); } + | LOOKAHEAD(3) ( "(" tableColumn=RelObjectName() { columns.add(tableColumn); } ("," tableColumn=RelObjectName() { columns.add(tableColumn); } )* ")" ) @@ -15024,6 +15028,10 @@ CreateTable CreateTable(boolean isUsingOrReplace): ) ) ] + [ LOOKAHEAD({ isKeywordAhead("INHERITS") }) ContextualKeyword("INHERITS") + "(" parent=Table() { parents.add(parent); } + ( "," parent=Table() { parents.add(parent); } )* ")" + { createTable.setInherits(parents); } ] [ LOOKAHEAD({ partitionOfTable != null }) partitionBound=PostgreSqlPartitionBound() { createTable.setPartitionBound(partitionBound); } ] diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableInheritanceTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableInheritanceTest.java new file mode 100644 index 000000000..d6d697b73 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlTableInheritanceTest.java @@ -0,0 +1,78 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +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; + +class PostgreSqlTableInheritanceTest { + @ParameterizedTest + @ValueSource(strings = {"parent", "public.parent", "public.parent, other.parent2", + "\"Odd.Schema\".\"Parent.Name\""}) + void modelsAndVisitsParentTables(String parents) throws JSQLParserException { + for (String definition : new String[] {"()", "(extra INT)", "(LIKE source, extra INT)"}) { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE child " + definition + " INHERITS (" + parents + + ") WITH (fillfactor = 80)"); + List
inherited = table.getInherits(); + assertEquals(parents.split(", ").length, inherited.size()); + assertEquals(parents.split(", ")[0], inherited.get(0).getFullyQualifiedName()); + List
visited = new ArrayList<>(); + TableDefinitionTraversal.visit(table, e -> { + }, visited::add); + for (Table parent : inherited) { + assertTrue(visited.stream().anyMatch(t -> t == parent)); + assertTrue(new TablesNamesFinder().getTables(table) + .contains(parent.getFullyQualifiedName())); + } + roundTrip(table); + inherited.set(0, new Table("replacement")); + assertTrue(table.toString().contains("INHERITS (replacement")); + roundTrip(table); + } + } + + @Test + void preservesEmptyDefinitionsAndCanRemoveInheritance() throws JSQLParserException { + CreateTable table = + (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE child () INHERITS (parent)"); + assertNotNull(table.getTableElements()); + assertTrue(table.getTableElements().isEmpty()); + table.setInherits(null); + assertEquals("CREATE TABLE child ()", table.toString()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1").size()); + } + + @ParameterizedTest + @ValueSource(strings = {"()", "(parent,)", "(parent AS p)", "(parent + 1)"}) + void rejectsMalformedParentLists(String parents) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("CREATE TABLE child () INHERITS " + parents)); + } + + private static void roundTrip(CreateTable table) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + table.accept(new StatementDeParser(out)); + assertEquals(table.toString(), out.toString()); + assertEquals(out.toString(), CCJSqlParserUtil.parse(out.toString()).toString()); + } +}