diff --git a/src/main/java/com/google/cloud/solutions/spannerddl/diff/DatabaseDefinition.java b/src/main/java/com/google/cloud/solutions/spannerddl/diff/DatabaseDefinition.java index c80fdcd..7652e30 100644 --- a/src/main/java/com/google/cloud/solutions/spannerddl/diff/DatabaseDefinition.java +++ b/src/main/java/com/google/cloud/solutions/spannerddl/diff/DatabaseDefinition.java @@ -27,6 +27,7 @@ import com.google.cloud.solutions.spannerddl.parser.ASTcreate_schema_statement; import com.google.cloud.solutions.spannerddl.parser.ASTcreate_search_index_statement; import com.google.cloud.solutions.spannerddl.parser.ASTcreate_table_statement; +import com.google.cloud.solutions.spannerddl.parser.ASTcreate_view_statement; import com.google.cloud.solutions.spannerddl.parser.ASTddl_statement; import com.google.cloud.solutions.spannerddl.parser.ASTforeign_key; import com.google.cloud.solutions.spannerddl.parser.ASTrow_deletion_policy_clause; @@ -64,6 +65,7 @@ public static DatabaseDefinition create(List statements) { LinkedHashMap changeStreams = new LinkedHashMap<>(); LinkedHashMap alterDatabaseOptions = new LinkedHashMap<>(); LinkedHashMap schemas = new LinkedHashMap<>(); + LinkedHashMap views = new LinkedHashMap<>(); for (ASTddl_statement ddlStatement : statements) { final SimpleNode statement = (SimpleNode) ddlStatement.jjtGetChild(0); @@ -139,6 +141,14 @@ public static DatabaseDefinition create(List statements) { (ASTcreate_schema_statement) ((ASTcreate_or_replace_statement) statement).getSchemaObject()); break; + case DdlParserTreeConstants.JJTCREATE_VIEW_STATEMENT: + views.put( + ((ASTcreate_view_statement) + ((ASTcreate_or_replace_statement) statement).getSchemaObject()) + .getName(), + (ASTcreate_view_statement) + ((ASTcreate_or_replace_statement) statement).getSchemaObject()); + break; default: throw new IllegalArgumentException( "Unsupported statement: " + AstTreeUtils.tokensToString(ddlStatement)); @@ -157,7 +167,8 @@ public static DatabaseDefinition create(List statements) { ImmutableMap.copyOf(ttls), ImmutableMap.copyOf(changeStreams), ImmutableMap.copyOf(alterDatabaseOptions), - ImmutableMap.copyOf(schemas)); + ImmutableMap.copyOf(schemas), + ImmutableMap.copyOf(views)); } public abstract ImmutableMap tablesInCreationOrder(); @@ -175,4 +186,6 @@ public static DatabaseDefinition create(List statements) { abstract ImmutableMap alterDatabaseOptions(); abstract ImmutableMap schemas(); + + abstract ImmutableMap views(); } diff --git a/src/main/java/com/google/cloud/solutions/spannerddl/diff/DdlDiff.java b/src/main/java/com/google/cloud/solutions/spannerddl/diff/DdlDiff.java index 978a276..327e0be 100644 --- a/src/main/java/com/google/cloud/solutions/spannerddl/diff/DdlDiff.java +++ b/src/main/java/com/google/cloud/solutions/spannerddl/diff/DdlDiff.java @@ -31,6 +31,7 @@ import com.google.cloud.solutions.spannerddl.parser.ASTcreate_schema_statement; import com.google.cloud.solutions.spannerddl.parser.ASTcreate_search_index_statement; import com.google.cloud.solutions.spannerddl.parser.ASTcreate_table_statement; +import com.google.cloud.solutions.spannerddl.parser.ASTcreate_view_statement; import com.google.cloud.solutions.spannerddl.parser.ASTddl_statement; import com.google.cloud.solutions.spannerddl.parser.ASTforeign_key; import com.google.cloud.solutions.spannerddl.parser.ASToptions_clause; @@ -103,6 +104,7 @@ public class DdlDiff { private final MapDifference searchIndexDifferences; private final String databaseName; // for alter Database private final MapDifference schemaDifferences; + private final MapDifference viewDifferences; private DdlDiff(DatabaseDefinition originalDb, DatabaseDefinition newDb, String databaseName) throws DdlDiffException { @@ -122,6 +124,7 @@ private DdlDiff(DatabaseDefinition originalDb, DatabaseDefinition newDb, String this.searchIndexDifferences = Maps.difference(originalDb.searchIndexes(), newDb.searchIndexes()); this.schemaDifferences = Maps.difference(originalDb.schemas(), newDb.schemas()); + this.viewDifferences = Maps.difference(originalDb.views(), newDb.views()); if (!alterDatabaseOptionsDifferences.areEqual() && Strings.isNullOrEmpty(databaseName)) { // should never happen, but... @@ -197,6 +200,17 @@ public List generateDifferenceStatements(Map options) } } + // Drop views in original order + for (String viewName : originalDb.views().keySet().asList().reverse()) { + if (viewDifferences.entriesDiffering().containsKey(viewName)) { + LOG.info( "Dropping changed view for re-creation: {}", viewName); + output.add("DROP VIEW " + viewName); + } else if (options.get(ALLOW_DROP_STATEMENTS_OPT) && viewDifferences.entriesOnlyOnLeft().containsKey(viewName)){ + LOG.info( "Dropping deleted view: {}", viewName); + output.add("DROP VIEW " + viewName); + } + } + // drop deleted search indexes. if (options.get(ALLOW_DROP_STATEMENTS_OPT)) { for (String searchIndexName : searchIndexDifferences.entriesOnlyOnLeft().keySet()) { @@ -424,7 +438,16 @@ public List generateDifferenceStatements(Map options) // For each changed search index, apply the add column statements output.addAll(searchIndexUpdateStatements.createStatements()); - // Add all new search indexes + // Create or alter views in new order. + for (ASTcreate_view_statement view : newDb.views().values()) { + if (viewDifferences.entriesOnlyOnRight().containsKey(view.getName())) { + LOG.info("Creating new view: {}", view.getName()); + output.add("CREATE OR REPLACE " + view.toStringBase()); + } else if (viewDifferences.entriesDiffering().containsKey(view.getName())) { + LOG.info("Re-creating new view: {}", view.getName()); + output.add("CREATE OR REPLACE " + view.toStringBase()); + } + } return output.build(); } @@ -813,6 +836,7 @@ public static List parseDdl(String original, boolean parseAnno break; case DdlParserTreeConstants.JJTCREATE_INDEX_STATEMENT: case DdlParserTreeConstants.JJTALTER_DATABASE_STATEMENT: + case DdlParserTreeConstants.JJTCREATE_VIEW_STATEMENT: case DdlParserTreeConstants.JJTCREATE_CHANGE_STREAM_STATEMENT: case DdlParserTreeConstants.JJTCREATE_SEARCH_INDEX_STATEMENT: // no-op - allowed @@ -823,6 +847,7 @@ public static List parseDdl(String original, boolean parseAnno .getSchemaObject() .getId()) { case DdlParserTreeConstants.JJTCREATE_SCHEMA_STATEMENT: + case DdlParserTreeConstants.JJTCREATE_VIEW_STATEMENT: // no-op - allowed break; default: diff --git a/src/main/java/com/google/cloud/solutions/spannerddl/parser/ASTcreate_view_statement.java b/src/main/java/com/google/cloud/solutions/spannerddl/parser/ASTcreate_view_statement.java index 7f12bc6..ed0c945 100644 --- a/src/main/java/com/google/cloud/solutions/spannerddl/parser/ASTcreate_view_statement.java +++ b/src/main/java/com/google/cloud/solutions/spannerddl/parser/ASTcreate_view_statement.java @@ -15,15 +15,60 @@ */ package com.google.cloud.solutions.spannerddl.parser; -// TODO +import com.google.cloud.solutions.spannerddl.diff.AstTreeUtils; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; + public class ASTcreate_view_statement extends SimpleNode { public ASTcreate_view_statement(int id) { super(id); - throw new UnsupportedOperationException("Not Implemented"); } public ASTcreate_view_statement(DdlParser p, int id) { super(p, id); - throw new UnsupportedOperationException("Not Implemented"); + } + + public String toStringBase() { + validateChildren(); + return Joiner.on(" ") + .skipNulls() + .join( + "VIEW", + getName(), + "SQL SECURITY", + AstTreeUtils.tokensToString(AstTreeUtils.getOptionalChildByType(children, ASTsql_security.class)), + "AS", + AstTreeUtils.tokensToString(AstTreeUtils.getOptionalChildByType(children, ASTview_definition.class)) + ); + } + + private void validateChildren() { + AstTreeUtils.validateChildrenClasses( + children, ImmutableSet.of(ASTname.class, ASTsql_security.class, ASTview_definition.class)); + } + + public String getName() { + return AstTreeUtils.tokensToString(AstTreeUtils.getChildByType(children, ASTname.class), false); + } + + @Override + public String toString() { + return Joiner.on(" ") + .skipNulls() + .join( + "CREATE", + toStringBase() + ); + } + + + @Override + public boolean equals(Object obj) { + return (obj instanceof ASTcreate_view_statement) && toString().equals(obj.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); } } diff --git a/src/test/java/com/google/cloud/solutions/spannerddl/diff/DdlDiffFromFilesTest.java b/src/test/java/com/google/cloud/solutions/spannerddl/diff/DdlDiffFromFilesTest.java index b172c90..06936e9 100644 --- a/src/test/java/com/google/cloud/solutions/spannerddl/diff/DdlDiffFromFilesTest.java +++ b/src/test/java/com/google/cloud/solutions/spannerddl/diff/DdlDiffFromFilesTest.java @@ -87,6 +87,20 @@ public void compareDddTextFiles() throws IOException { ".*DROP (SCHEMA|TABLE|COLUMN|CHANGE STREAM|SEARCH INDEX).*")) .collect(Collectors.toList()); + // remove any drop views from the expectedResults if they do not have an equivalent + // CREATE statement. This is because we are allowing recreation of views, but not allowing + // dropping of removed views. + for (String statement : expectedDiff) { + if (statement.startsWith("DROP VIEW ")) { + String viewName = Iterables.get(Splitter.on(' ').split(statement), 2); + // see if there is a matching create statement + Pattern p = Pattern.compile("CREATE .*VIEW " + viewName + " "); + if (expectedDiffNoDrops.stream().noneMatch(s -> p.matcher(s).find())) { + expectedDiffNoDrops.remove(statement); + } + } + } + // remove any drop indexes from the expectedResults if they do not have an equivalent // CREATE statement. This is because we are allowing recreation of indexes, but not allowing // dropping of removed indexes. diff --git a/src/test/resources/ddlParserUnsupported.txt b/src/test/resources/ddlParserUnsupported.txt index d1bf7af..8987156 100644 --- a/src/test/resources/ddlParserUnsupported.txt +++ b/src/test/resources/ddlParserUnsupported.txt @@ -24,14 +24,6 @@ ALTER TABLE Albums DROP ROW DELETION POLICY ALTER TABLE Albums REPLACE ROW DELETION POLICY (OLDER_THAN(timestamp_column, INTERVAL 1 DAY)) -== Test 3 - -CREATE OR REPLACE VIEW test1 SQL SECURITY INVOKER AS SELECT * from test2 - -== Test 4 - -CREATE VIEW test1 SQL SECURITY INVOKER AS SELECT * from test2 - == Test 6 drop change stream test1 @@ -68,18 +60,6 @@ GRANT SELECT ON TABLE table_list TO ROLE role_list REVOKE SELECT ON TABLE table_list TO ROLE role_list -== Test 12a // TODO views - -CREATE VIEW view_name AS query - -== Test 12b drop view not supported - -DROP VIEW view_name - -== Test 13 // TODO views - -CREATE OR REPLACE VIEW view_name AS query - == Test 14a // TODO sequences CREATE SEQUENCE IF NOT EXISTS sequence_name OPTIONS ( sequence_kind='bit_reversed_positive' ) @@ -163,4 +143,4 @@ ALTER SEARCH INDEX AlbumsIndex ADD STORED COLUMN test ALTER SEARCH INDEX AlbumsIndex ADD COLUMN add_token_column -== +== \ No newline at end of file diff --git a/src/test/resources/expectedDdlDiff.txt b/src/test/resources/expectedDdlDiff.txt index f41ac9d..5a73ba4 100644 --- a/src/test/resources/expectedDdlDiff.txt +++ b/src/test/resources/expectedDdlDiff.txt @@ -350,4 +350,30 @@ DROP SCHEMA schema2 CREATE SCHEMA schema3 CREATE TABLE schema3.table1 ( col1 INT64 ) PRIMARY KEY (col1 ASC) -== +== TEST 68 Create view with/without named schama + +CREATE SCHEMA schema1 +CREATE TABLE input ( PK INT64 ) PRIMARY KEY (PK ASC) +CREATE TABLE schema1.input ( PK INT64 ) PRIMARY KEY (PK ASC) +CREATE OR REPLACE VIEW preprocess SQL SECURITY INVOKER AS SELECT t.PK, 1 AS n FROM INPUT AS t +CREATE OR REPLACE VIEW final SQL SECURITY INVOKER AS SELECT v.PK, v.n, "foo" AS s FROM preprocess AS v +CREATE OR REPLACE VIEW schema1.preprocess SQL SECURITY INVOKER AS SELECT t.PK, 1 AS n FROM schema1.INPUT AS t +CREATE OR REPLACE VIEW schema1.final SQL SECURITY INVOKER AS SELECT v.PK, v.n, "foo" AS s FROM schema1.preprocess AS v + + +== TEST 69 Replace view with dependencies + +DROP VIEW final +DROP VIEW preprocess +DROP INDEX inputByCol +ALTER TABLE input DROP COLUMN col1 +ALTER TABLE input ADD COLUMN col2 INT64 +CREATE INDEX inputByCol ON INPUT ( col2 ASC ) +CREATE OR REPLACE VIEW preprocess SQL SECURITY INVOKER AS SELECT t.pk, t.col2, 1 AS n FROM INPUT AS t +CREATE OR REPLACE VIEW final SQL SECURITY INVOKER AS SELECT v.PK, v.col2, v.n, "foo" AS s FROM preprocess AS v + +== TEST 70 Dropping view + +DROP VIEW view2 + +== \ No newline at end of file diff --git a/src/test/resources/newDdl.txt b/src/test/resources/newDdl.txt index 5a0588f..112fb3b 100644 --- a/src/test/resources/newDdl.txt +++ b/src/test/resources/newDdl.txt @@ -563,4 +563,51 @@ CREATE TABLE schema3.table1 ( col1 INT64, )PRIMARY KEY (col1); -== +== TEST 68 Create view with/without named schama + +CREATE TABLE input ( + PK INT64 +) PRIMARY KEY (PK); +CREATE OR REPLACE VIEW preprocess +SQL SECURITY INVOKER +AS SELECT t.PK, 1 AS n + FROM input AS t; +CREATE OR REPLACE VIEW final +SQL SECURITY INVOKER +AS SELECT v.PK, v.n, "foo" AS s + FROM preprocess AS v; +CREATE SCHEMA schema1; +CREATE TABLE schema1.input (PK INT64) PRIMARY KEY (PK); +CREATE OR REPLACE VIEW schema1.preprocess +SQL SECURITY INVOKER +AS SELECT t.PK, 1 AS n + FROM schema1.input AS t; +CREATE OR REPLACE VIEW schema1.final +SQL SECURITY INVOKER +AS SELECT v.PK, v.n, "foo" AS s + FROM schema1.preprocess AS v; + +== TEST 69 Replace view with dependencies + +CREATE TABLE input ( + pk INT64, + col2 INT64 +) PRIMARY KEY (pk); + +CREATE INDEX inputByCol ON input (col2); + +CREATE OR REPLACE VIEW preprocess +SQL SECURITY INVOKER +AS SELECT t.pk, t.col2, 1 AS n + FROM input AS t; + +CREATE OR REPLACE VIEW final +SQL SECURITY INVOKER +AS SELECT v.PK, v.col2, v.n, "foo" AS s + FROM preprocess AS v; + +== TEST 70 Dropping view + +CREATE VIEW view1 SQL SECURITY INVOKER AS SELECT * from test1; + +== \ No newline at end of file diff --git a/src/test/resources/originalDdl.txt b/src/test/resources/originalDdl.txt index afbc85a..0d91958 100644 --- a/src/test/resources/originalDdl.txt +++ b/src/test/resources/originalDdl.txt @@ -561,4 +561,32 @@ CREATE TABLE schema2.table1 ( )PRIMARY KEY (col1); -== +== TEST 68 Create view with/without named schama + +-- nothing + +== TEST 69 Replace view with dependencies + +CREATE TABLE input ( + pk INT64, + col1 INT64 +) PRIMARY KEY (pk); + +CREATE INDEX inputByCol ON input (col1); + +CREATE OR REPLACE VIEW preprocess +SQL SECURITY INVOKER +AS SELECT t.pk, t.col1, 1 AS n + FROM input AS t; + +CREATE OR REPLACE VIEW final +SQL SECURITY INVOKER +AS SELECT v.PK, v.col1, v.n, "foo" AS s + FROM preprocess AS v; + +== TEST 70 Dropping view + +CREATE VIEW view1 SQL SECURITY INVOKER AS SELECT * FROM test1; +CREATE OR REPLACE VIEW view2 SQL SECURITY DEFINER AS SELECT * FROM test2; + +== \ No newline at end of file