diff --git a/build.gradle b/build.gradle index 6e5cbf4a..8400917d 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ repositories { allprojects { group = 'com.adaptivescale' - version = '1.9.0' + version = '1.9.1' sourceCompatibility = 11 targetCompatibility = 11 } diff --git a/cli/src/main/java/com/adaptivescale/rosetta/cli/Cli.java b/cli/src/main/java/com/adaptivescale/rosetta/cli/Cli.java index e40a6f52..e0f041a9 100644 --- a/cli/src/main/java/com/adaptivescale/rosetta/cli/Cli.java +++ b/cli/src/main/java/com/adaptivescale/rosetta/cli/Cli.java @@ -56,7 +56,7 @@ @Slf4j @CommandLine.Command(name = "cli", mixinStandardHelpOptions = true, - version = "1.9.0", + version = "1.9.1", description = "Declarative Database Management - DDL Transpiler" ) class Cli implements Callable { diff --git a/common/src/main/java/com/adaptivescale/rosetta/common/models/Interleave.java b/common/src/main/java/com/adaptivescale/rosetta/common/models/Interleave.java new file mode 100644 index 00000000..cf353b6f --- /dev/null +++ b/common/src/main/java/com/adaptivescale/rosetta/common/models/Interleave.java @@ -0,0 +1,50 @@ +package com.adaptivescale.rosetta.common.models; + +import java.util.Objects; + +public class Interleave { + + private String tableName; + + private String parentName; + + private String onDeleteAction; + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getParentName() { + return parentName; + } + + public void setParentName(String parentName) { + this.parentName = parentName; + } + + public String getOnDeleteAction() { + return onDeleteAction; + } + + public void setOnDeleteAction(String onDeleteAction) { + this.onDeleteAction = onDeleteAction; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Interleave that = (Interleave) o; + return Objects.equals(tableName, that.tableName) && Objects.equals(parentName, that.parentName) && Objects.equals(onDeleteAction, that.onDeleteAction); + } + + @Override + public int hashCode() { + return Objects.hash(tableName, parentName, onDeleteAction); + } + +} diff --git a/common/src/main/java/com/adaptivescale/rosetta/common/models/Table.java b/common/src/main/java/com/adaptivescale/rosetta/common/models/Table.java index 9428ca21..2969088f 100644 --- a/common/src/main/java/com/adaptivescale/rosetta/common/models/Table.java +++ b/common/src/main/java/com/adaptivescale/rosetta/common/models/Table.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.List; +import java.util.Objects; public class Table { @@ -9,6 +10,9 @@ public class Table { private String description; private String type; private String schema; + + private Interleave interleave; + private List indices; private Collection columns; @@ -54,6 +58,14 @@ public void setDescription(String description) { this.description = description; } + public Interleave getInterleave() { + return interleave; + } + + public void setInterleave(Interleave interleave) { + this.interleave = interleave; + } + public List getIndices() { return indices; } @@ -61,4 +73,17 @@ public List getIndices() { public void setIndices(List indices) { this.indices = indices; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Table table = (Table) o; + return Objects.equals(name, table.name) && Objects.equals(description, table.description) && Objects.equals(type, table.type) && Objects.equals(schema, table.schema) && Objects.equals(interleave, table.interleave) && Objects.equals(indices, table.indices) && Objects.equals(columns, table.columns); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, type, schema, interleave, indices, columns); + } } diff --git a/ddl/src/main/java/com/adaptivescale/rosetta/ddl/change/SpannerChangeFinder.java b/ddl/src/main/java/com/adaptivescale/rosetta/ddl/change/SpannerChangeFinder.java index 8ff5a0a2..0da6379b 100644 --- a/ddl/src/main/java/com/adaptivescale/rosetta/ddl/change/SpannerChangeFinder.java +++ b/ddl/src/main/java/com/adaptivescale/rosetta/ddl/change/SpannerChangeFinder.java @@ -52,6 +52,13 @@ public List> findChanges(Database expected, Database actual) { List> changesFromIndices = findChangesInIndicesForTable(expectedTable, table); changes.addAll(changesFromTables); changes.addAll(changesFromIndices); + + if (checkInterleaveChanges(table, expectedTable)) { + Change tableChangeDrop = ChangeFactory.tableChange(null, table, Change.Status.DROP); + Change
tableChangeAdd = ChangeFactory.tableChange(expectedTable, null, Change.Status.ADD); + changes.add(tableChangeDrop); + changes.add(tableChangeAdd); + } } else { throw new RuntimeException(String.format("Found %d table with name '%s' and schema '%s'", foundedTables.size(), expectedTable.getName(), expectedTable.getSchema())); @@ -77,6 +84,15 @@ public List> findChanges(Database expected, Database actual) { return result; } + private boolean checkInterleaveChanges(Table table, Table expectedTable) { + if ((table.getInterleave() == null && expectedTable.getInterleave() != null) || + (table.getInterleave() != null && expectedTable.getInterleave() == null) || + (table.getInterleave() != null && expectedTable.getInterleave() != null && !table.getInterleave().equals(expectedTable.getInterleave()))) { + return true; + } + return false; + } + private void viewChanges(Database expected, Database actual, List> changes) { // Backwards compatibility if (actual.getViews() == null) { diff --git a/ddl/src/main/java/com/adaptivescale/rosetta/ddl/targets/spanner/SpannerDDLGenerator.java b/ddl/src/main/java/com/adaptivescale/rosetta/ddl/targets/spanner/SpannerDDLGenerator.java index 14b18494..7ff8e392 100644 --- a/ddl/src/main/java/com/adaptivescale/rosetta/ddl/targets/spanner/SpannerDDLGenerator.java +++ b/ddl/src/main/java/com/adaptivescale/rosetta/ddl/targets/spanner/SpannerDDLGenerator.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import static com.adaptivescale.rosetta.ddl.targets.spanner.Constants.DEFAULT_WRAPPER; +import static java.util.Comparator.*; @Slf4j @RosettaModule( @@ -55,7 +56,7 @@ public String createTable(Table table, boolean dropTableIfExists) { if (table.getSchema() != null && !table.getSchema().isBlank()) { stringBuilder.append(DEFAULT_WRAPPER) - .append(table.getSchema()).append(DEFAULT_WRAPPER).append("."); + .append(table.getSchema()).append(DEFAULT_WRAPPER).append("."); } stringBuilder.append(DEFAULT_WRAPPER).append(table.getName()).append(DEFAULT_WRAPPER) @@ -65,6 +66,16 @@ public String createTable(Table table, boolean dropTableIfExists) { stringBuilder.append(primaryKeysForTable.get()); } + if (table.getInterleave() != null) { + stringBuilder.append(",\r"); + stringBuilder.append("INTERLEAVE IN PARENT ") + .append(table.getInterleave().getParentName()); + + final String onDeleteAction = table.getInterleave().getOnDeleteAction(); + if (onDeleteAction != null && !onDeleteAction.isEmpty()) { + stringBuilder.append(" ON DELETE ").append(table.getInterleave().getOnDeleteAction()); + } + } stringBuilder.append(";"); return stringBuilder.toString(); } @@ -85,21 +96,29 @@ public String createDatabase(Database database, boolean dropTableIfExists) { }); if(!missingPrimaryKeys.isEmpty()){ throw new RuntimeException( - String.format("Tables %s are missing primary key. Spanner does not allow table without primary key.", - missingPrimaryKeys.stream().collect(Collectors.joining(",")))); + String.format("Tables %s are missing primary key. Spanner does not allow table without primary key.", + missingPrimaryKeys.stream().collect(Collectors.joining(",")))); } - stringBuilder.append(database.getTables() - .stream() - .map(table -> createTable(table, dropTableIfExists)) - .collect(Collectors.joining("\r\r"))); + List
tablesToCreate = database.getTables().stream().collect(Collectors.toList()); + + //Make sure you create interleaved tables at the end + tablesToCreate.sort(nullsFirst( + comparing(it -> Optional.ofNullable(it.getInterleave()) + .map(Interleave::getTableName) + .orElse(null), nullsFirst(naturalOrder())))); + + stringBuilder.append(tablesToCreate + .stream() + .map(table -> createTable(table, dropTableIfExists)) + .collect(Collectors.joining("\r\r"))); String foreignKeys = database - .getTables() - .stream() - .map(this::foreignKeys) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.joining()); + .getTables() + .stream() + .map(this::foreignKeys) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.joining()); if (!foreignKeys.isEmpty()) { stringBuilder.append("\r").append(foreignKeys).append("\r"); @@ -129,6 +148,11 @@ public String createDatabase(Database database, boolean dropTableIfExists) { @Override public String createForeignKey(ForeignKey foreignKey) { + //For foreign keys returned as result of interleave table, JDBC returns no name. + // Those are created automatically by JDBC once we specify that the table is interleaved. + if (foreignKey.getName() == null) { + return ""; + } return "ALTER TABLE" + handleNullSchema(foreignKey.getSchema(), foreignKey.getTableName()) + " ADD CONSTRAINT " + foreignKey.getName() + " FOREIGN KEY ("+ DEFAULT_WRAPPER + foreignKey.getColumnName() + DEFAULT_WRAPPER +") REFERENCES " + foreignKey.getPrimaryTableName() @@ -246,12 +270,23 @@ private Optional createPrimaryKeysForTable(Table table) { private Optional foreignKeys(Table table) { String result = table.getColumns().stream() - .filter(column -> column.getForeignKeys() != null && !column.getForeignKeys().isEmpty()) - .map(this::createForeignKeys).collect(Collectors.joining()); + .filter(column -> column.getForeignKeys() != null && !column.getForeignKeys().isEmpty()) + .filter(column -> !checkColumnIsInterleaved(table, column)) + .map(this::createForeignKeys).collect(Collectors.joining()); return result.isEmpty() ? Optional.empty() : Optional.of(result); } + private boolean checkColumnIsInterleaved(Table table, Column column) { + if (table.getInterleave() != null && + column.isPrimaryKey() && + table.getInterleave().getParentName().equals(column.getForeignKeys().get(0).getPrimaryTableName())) { + return true; + } + + return false; + } + //ALTER TABLE rosetta.contacts ADD CONSTRAINT contacts_fk FOREIGN KEY (contact_id) REFERENCES rosetta."user"(user_id); private String createForeignKeys(Column column) { return column.getForeignKeys().stream().map(this::createForeignKey).collect(Collectors.joining()); diff --git a/ddl/src/test/java/com/adaptivescale/rosetta/ddl/test/SpannerDDLTest.java b/ddl/src/test/java/com/adaptivescale/rosetta/ddl/test/SpannerDDLTest.java index 13f78c96..1c53cc46 100644 --- a/ddl/src/test/java/com/adaptivescale/rosetta/ddl/test/SpannerDDLTest.java +++ b/ddl/src/test/java/com/adaptivescale/rosetta/ddl/test/SpannerDDLTest.java @@ -16,55 +16,62 @@ public class SpannerDDLTest { - private static final Path resourceDirectory = Paths.get("src", "test", "resources", "ddl", "spanner_ddl"); + private static final Path resourceDirectory = Paths.get("src", "test", "resources", "ddl", "spanner_ddl"); - @Test - public void createDB() throws IOException { + @Test + public void addInterleaveTable() throws IOException { + String ddl = generateDDL("add_interleave_table"); + Assertions.assertEquals("CREATE TABLE AlbumsNew(SingerId INT64 NOT NULL , AlbumId INT64 NOT NULL , AlbumTitle STRING(MAX)) PRIMARY KEY (SingerId, AlbumId),\r" + + "INTERLEAVE IN PARENT Singers ON DELETE CASCADE;", ddl); + } + + @Test + public void createDB() throws IOException { String ddl = generateDDL("clean_database"); Assertions.assertEquals("CREATE TABLE Singers(SingerId STRING(1024) NOT NULL , FirstName STRING(1024), LastName STRING(1024)) PRIMARY KEY (SingerId);CREATE VIEW `SingerNames` SQL SECURITY INVOKER AS SELECT Singers.SingerId AS SingerId, Singers.FirstName || ' ' || Singers.LastName AS Name FROM Singers ;CREATE VIEW `NamesSinger` SQL SECURITY INVOKER AS SELECT Singers.SingerId AS SingerId, Singers.FirstName, Singers.LastName FROM Singers ;", ddl.replaceAll("(\\n)", " ").replaceAll("(\\r)", "")); - } + } - @Test - public void addTable() throws IOException { + @Test + public void addTable() throws IOException { String ddl = generateDDL("add_table"); Assertions.assertEquals("CREATE TABLE Logs(LogId INT64 NOT NULL , Description STRING(1024)) PRIMARY KEY (LogId);", ddl); - } + } - @Test - public void dropTable() throws IOException { + @Test + public void dropTable() throws IOException { String ddl = generateDDL("drop_table"); Assertions.assertEquals("DROP TABLE Logs;", ddl); - } + } - @Test - public void addColumn() throws IOException { + @Test + public void addColumn() throws IOException { String ddl = generateDDL("add_column"); Assertions.assertEquals("ALTER TABLE Logs ADD COLUMN Status STRING(1024);", ddl); - } - - @Test - public void dropColumn() throws IOException { - String ddl = generateDDL("drop_column"); - Assertions.assertEquals("ALTER TABLE Logs DROP COLUMN Status;", ddl); - } - - @Test - public void alterColumnDataType() throws IOException { - String ddl = generateDDL("alter_column_data_type"); - Assertions.assertEquals("ALTER TABLE Logs ALTER COLUMN Status STRING(1024);", ddl); - } - - @Test - public void alterColumnToNullable() throws IOException { - String ddl = generateDDL("alter_column_to_nullable"); - Assertions.assertEquals("ALTER TABLE Logs ALTER COLUMN Status STRING(1024);", ddl); - } - - @Test - public void addView() throws IOException { - String ddl = generateDDL("add_view"); - Assertions.assertEquals("CREATE VIEW `NamesSinger`\n" + + } + + @Test + public void dropColumn() throws IOException { + String ddl = generateDDL("drop_column"); + Assertions.assertEquals("ALTER TABLE Logs DROP COLUMN Status;", ddl); + } + + @Test + public void alterColumnDataType() throws IOException { + String ddl = generateDDL("alter_column_data_type"); + Assertions.assertEquals("ALTER TABLE Logs ALTER COLUMN Status STRING(1024);", ddl); + } + + @Test + public void alterColumnToNullable() throws IOException { + String ddl = generateDDL("alter_column_to_nullable"); + Assertions.assertEquals("ALTER TABLE Logs ALTER COLUMN Status STRING(1024);", ddl); + } + + @Test + public void addView() throws IOException { + String ddl = generateDDL("add_view"); + Assertions.assertEquals("CREATE VIEW `NamesSinger`\n" + "SQL SECURITY INVOKER\n" + "AS\n" + "SELECT\n" + @@ -74,32 +81,32 @@ public void addView() throws IOException { "FROM Singers\n" + ";", ddl); - } - - @Test - public void dropView() throws IOException { - String ddl = generateDDL("drop_view"); - Assertions.assertEquals("DROP VIEW `NamesSinger`;", ddl); - } - - @Test - public void alterView() throws IOException { - String ddl = generateDDL("alter_view"); - Assertions.assertEquals("CREATE OR REPLACE VIEW `NamesSinger`\n" + - "SQL SECURITY INVOKER\n" + + } + + @Test + public void dropView() throws IOException { + String ddl = generateDDL("drop_view"); + Assertions.assertEquals("DROP VIEW `NamesSinger`;", ddl); + } + + @Test + public void alterView() throws IOException { + String ddl = generateDDL("alter_view"); + Assertions.assertEquals("CREATE OR REPLACE VIEW `NamesSinger`\n" + + "SQL SECURITY INVOKER\n" + "AS\n" + "SELECT\n" + " Singers.SingerId AS SingerId,\n" + " Singers.FirstName\n" + "FROM Singers\n" + ";", ddl); - } - - private String generateDDL(String testType) throws IOException { - Database actual = Utils.getDatabase(resourceDirectory.resolve(testType), "actual_model.yaml"); - Database expected = Utils.getDatabase(resourceDirectory.resolve(testType), "expected_model.yaml"); - List> changes = new DefaultChangeFinder().findChanges(expected, actual); - ChangeHandler handler = new ChangeHandlerImplementation(new SpannerDDLGenerator(), null); - return handler.createDDLForChanges(changes); - } + } + + private String generateDDL(String testType) throws IOException { + Database actual = Utils.getDatabase(resourceDirectory.resolve(testType), "actual_model.yaml"); + Database expected = Utils.getDatabase(resourceDirectory.resolve(testType), "expected_model.yaml"); + List> changes = new DefaultChangeFinder().findChanges(expected, actual); + ChangeHandler handler = new ChangeHandlerImplementation(new SpannerDDLGenerator(), null); + return handler.createDDLForChanges(changes); + } } diff --git a/ddl/src/test/resources/ddl/spanner_ddl/add_interleave_table/actual_model.yaml b/ddl/src/test/resources/ddl/spanner_ddl/add_interleave_table/actual_model.yaml new file mode 100644 index 00000000..e0ac711e --- /dev/null +++ b/ddl/src/test/resources/ddl/spanner_ddl/add_interleave_table/actual_model.yaml @@ -0,0 +1,120 @@ +--- +safeMode: false +tables: + - name: "Albums" + type: "TABLE" + schema: "" + interleave: + tableName: "Albums" + parentName: "Singers" + onDeleteAction: "CASCADE" + indices: + - name: "PRIMARY_KEY" + schema: "" + tableName: "Albums" + columnNames: + - "SingerId" + - "AlbumId" + nonUnique: false + indexQualifier: "" + type: 1 + ascOrDesc: "A" + cardinality: -1 + columns: + - name: "SingerId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 1 + columnDisplaySize: 19 + scale: 0 + precision: 19 + foreignKeys: + - schema: "" + tableName: "Albums" + columnName: "SingerId" + deleteRule: "0" + primaryTableSchema: "" + primaryTableName: "Singers" + primaryColumnName: "SingerId" + autoincrement: false + primaryKey: true + nullable: false + - name: "AlbumId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 2 + columnDisplaySize: 19 + scale: 0 + precision: 19 + autoincrement: false + primaryKey: true + nullable: false + - name: "AlbumTitle" + typeName: "STRING(MAX)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 2621440 + scale: 0 + precision: 2621440 + autoincrement: false + primaryKey: false + nullable: true + - name: "Singers" + type: "TABLE" + schema: "" + indices: + - name: "PRIMARY_KEY" + schema: "" + tableName: "Singers" + columnNames: + - "SingerId" + nonUnique: false + indexQualifier: "" + type: 1 + ascOrDesc: "A" + cardinality: -1 + columns: + - name: "SingerId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 1 + columnDisplaySize: 19 + scale: 0 + precision: 19 + autoincrement: false + primaryKey: true + nullable: false + - name: "FirstName" + typeName: "STRING(1024)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 1024 + scale: 0 + precision: 1024 + autoincrement: false + primaryKey: false + nullable: true + - name: "LastName" + typeName: "STRING(1024)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 1024 + scale: 0 + precision: 1024 + autoincrement: false + primaryKey: false + nullable: true + - name: "SingerInfo" + typeName: "BYTES(MAX)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 10485760 + scale: 0 + precision: 10485760 + autoincrement: false + primaryKey: false + nullable: true +views: [] +databaseProductName: "Google Cloud Spanner" +databaseType: "spanner" +operationLevel: "database" diff --git a/ddl/src/test/resources/ddl/spanner_ddl/add_interleave_table/expected_model.yaml b/ddl/src/test/resources/ddl/spanner_ddl/add_interleave_table/expected_model.yaml new file mode 100644 index 00000000..16b403fd --- /dev/null +++ b/ddl/src/test/resources/ddl/spanner_ddl/add_interleave_table/expected_model.yaml @@ -0,0 +1,178 @@ +--- +safeMode: false +tables: + - name: "Albums" + type: "TABLE" + schema: "" + interleave: + tableName: "Albums" + parentName: "Singers" + onDeleteAction: "CASCADE" + indices: + - name: "PRIMARY_KEY" + schema: "" + tableName: "Albums" + columnNames: + - "SingerId" + - "AlbumId" + nonUnique: false + indexQualifier: "" + type: 1 + ascOrDesc: "A" + cardinality: -1 + columns: + - name: "SingerId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 1 + columnDisplaySize: 19 + scale: 0 + precision: 19 + foreignKeys: + - schema: "" + tableName: "Albums" + columnName: "SingerId" + deleteRule: "0" + primaryTableSchema: "" + primaryTableName: "Singers" + primaryColumnName: "SingerId" + autoincrement: false + primaryKey: true + nullable: false + - name: "AlbumId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 2 + columnDisplaySize: 19 + scale: 0 + precision: 19 + autoincrement: false + primaryKey: true + nullable: false + - name: "AlbumTitle" + typeName: "STRING(MAX)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 2621440 + scale: 0 + precision: 2621440 + autoincrement: false + primaryKey: false + nullable: true + - name: "AlbumsNew" + type: "TABLE" + schema: "" + interleave: + tableName: "Albums" + parentName: "Singers" + onDeleteAction: "CASCADE" + indices: + - name: "PRIMARY_KEY" + schema: "" + tableName: "Albums" + columnNames: + - "SingerId" + - "AlbumId" + nonUnique: false + indexQualifier: "" + type: 1 + ascOrDesc: "A" + cardinality: -1 + columns: + - name: "SingerId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 1 + columnDisplaySize: 19 + scale: 0 + precision: 19 + foreignKeys: + - schema: "" + tableName: "Albums" + columnName: "SingerId" + deleteRule: "0" + primaryTableSchema: "" + primaryTableName: "Singers" + primaryColumnName: "SingerId" + autoincrement: false + primaryKey: true + nullable: false + - name: "AlbumId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 2 + columnDisplaySize: 19 + scale: 0 + precision: 19 + autoincrement: false + primaryKey: true + nullable: false + - name: "AlbumTitle" + typeName: "STRING(MAX)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 2621440 + scale: 0 + precision: 2621440 + autoincrement: false + primaryKey: false + nullable: true + - name: "Singers" + type: "TABLE" + schema: "" + indices: + - name: "PRIMARY_KEY" + schema: "" + tableName: "Singers" + columnNames: + - "SingerId" + nonUnique: false + indexQualifier: "" + type: 1 + ascOrDesc: "A" + cardinality: -1 + columns: + - name: "SingerId" + typeName: "INT64" + ordinalPosition: 0 + primaryKeySequenceId: 1 + columnDisplaySize: 19 + scale: 0 + precision: 19 + autoincrement: false + primaryKey: true + nullable: false + - name: "FirstName" + typeName: "STRING(1024)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 1024 + scale: 0 + precision: 1024 + autoincrement: false + primaryKey: false + nullable: true + - name: "LastName" + typeName: "STRING(1024)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 1024 + scale: 0 + precision: 1024 + autoincrement: false + primaryKey: false + nullable: true + - name: "SingerInfo" + typeName: "BYTES(MAX)" + ordinalPosition: 0 + primaryKeySequenceId: 0 + columnDisplaySize: 10485760 + scale: 0 + precision: 10485760 + autoincrement: false + primaryKey: false + nullable: true +views: [] +databaseProductName: "Google Cloud Spanner" +databaseType: "spanner" +operationLevel: "database" diff --git a/diff/src/main/java/com/adaptivescale/rosetta/diff/DefaultTester.java b/diff/src/main/java/com/adaptivescale/rosetta/diff/DefaultTester.java index d3293872..8079fe1f 100644 --- a/diff/src/main/java/com/adaptivescale/rosetta/diff/DefaultTester.java +++ b/diff/src/main/java/com/adaptivescale/rosetta/diff/DefaultTester.java @@ -27,6 +27,11 @@ public class DefaultTester implements Diff, Database, Database> { private static final String VIEW_REMOVED_FORMAT = "View '%s' exists in the model, but it does not exist in the target database."; private static final String VIEW_ADDED_FORMAT = "View '%s' does not exist in the model, but it exists in the target database."; + private static final String INTERLEAVED_CHANGED_FORMAT = "Interleaved Changed: Table '%s'"; + private static final String INTERLEAVED_REMOVED_FORMAT = "Interleaved '%s' table exists in the model, but it does not exist in the target database."; + private static final String INTERLEAVED_ADDED_FORMAT = "Interleaved '%s' table does not exist in the model, but it exists in the target database."; + + @Override public List find(Database localValue, Database targetValue) { @@ -42,6 +47,9 @@ public List find(Database localValue, Database targetValue) { continue; } + List tableInterleaveChanges = checkForInterleaveChanges(table, targetTable.get()); + changes.addAll(tableInterleaveChanges); + Collection columns = table.getColumns(); for (Column localColumn : columns) { Optional targetColumn = getColumn(localColumn.getName(), targetTable.get()); @@ -148,6 +156,24 @@ public List find(Database localValue, Database targetValue) { return changes; } + private List checkForInterleaveChanges(Table localTable, Table targetTable) { + List changes = new ArrayList<>(); + if (localTable.getInterleave() != null && targetTable.getInterleave() == null) { + changes.add(String.format(INTERLEAVED_REMOVED_FORMAT, localTable.getName())); + } + if (localTable.getInterleave() == null && targetTable.getInterleave() != null) { + changes.add(String.format(INTERLEAVED_ADDED_FORMAT, localTable.getName())); + } + + if (localTable.getInterleave() != null && + targetTable.getInterleave() != null && + !localTable.getInterleave().equals(targetTable.getInterleave())) { + changes.add(String.format(INTERLEAVED_CHANGED_FORMAT, localTable.getName())); + } + + return changes; + } + private void testViews(Database localValue, Database targetValue, List changes) { Collection localViews = Optional.ofNullable(localValue.getViews()) .orElse(Collections.emptyList()); @@ -264,7 +290,6 @@ private void testViews(Database localValue, Database targetValue, List c } - private List sameIndices(List localIndices, List targetIndices) { List changeLogs = new ArrayList<>(); diff --git a/source/src/main/java/com/adataptivescale/rosetta/source/core/extractors/table/DefaultTablesExtractor.java b/source/src/main/java/com/adataptivescale/rosetta/source/core/extractors/table/DefaultTablesExtractor.java index 98743bbc..f789040b 100644 --- a/source/src/main/java/com/adataptivescale/rosetta/source/core/extractors/table/DefaultTablesExtractor.java +++ b/source/src/main/java/com/adataptivescale/rosetta/source/core/extractors/table/DefaultTablesExtractor.java @@ -28,7 +28,6 @@ import java.util.Collection; public class DefaultTablesExtractor implements TableExtractor, Connection, java.sql.Connection> { - @Override public Collection
extract(Connection target, java.sql.Connection connection) throws SQLException { DatabaseMetaData metaData = connection.getMetaData(); @@ -47,10 +46,11 @@ public Collection
extract(Connection target, java.sql.Connection connecti if (!resultSet.isClosed()) { resultSet.close(); } + return tables; } - private Table map(ResultSet resultSet) throws SQLException { + protected Table map(ResultSet resultSet) throws SQLException { Table table = new Table(); table.setName(resultSet.getString("TABLE_NAME")); table.setType(resultSet.getString("TABLE_TYPE")); diff --git a/source/src/main/java/com/adataptivescale/rosetta/source/core/extractors/table/SpannerTablesExtractor.java b/source/src/main/java/com/adataptivescale/rosetta/source/core/extractors/table/SpannerTablesExtractor.java new file mode 100644 index 00000000..a6946914 --- /dev/null +++ b/source/src/main/java/com/adataptivescale/rosetta/source/core/extractors/table/SpannerTablesExtractor.java @@ -0,0 +1,73 @@ +package com.adataptivescale.rosetta.source.core.extractors.table; + +import com.adaptivescale.rosetta.common.annotations.RosettaModule; +import com.adaptivescale.rosetta.common.models.Interleave; +import com.adaptivescale.rosetta.common.models.Table; +import com.adaptivescale.rosetta.common.models.input.Connection; +import com.adaptivescale.rosetta.common.types.RosettaModuleTypes; +import org.apache.commons.lang3.ArrayUtils; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@RosettaModule( + name = "spanner", + type = RosettaModuleTypes.TABLE_EXTRACTOR +) +public class SpannerTablesExtractor extends DefaultTablesExtractor { + @Override + public Collection
extract(Connection target, java.sql.Connection connection) throws SQLException { + DatabaseMetaData metaData = connection.getMetaData(); + + ResultSet resultSet = metaData.getTables(target.getDatabaseName(), target.getSchemaName(), null, ArrayUtils.toArray("TABLE")); + + Collection
tables = new ArrayList<>(); + + while (resultSet.next()) { + if (!target.getTables().isEmpty() && + !target.getTables().contains(resultSet.getString("TABLE_NAME"))) continue; + Table table = map(resultSet); + tables.add(table); + } + + if (!resultSet.isClosed()) { + resultSet.close(); + } + List interlevedTables = getInterleavedTables(connection); + for (Interleave interleave: interlevedTables) { + Table table = tables.stream().filter(it -> it.getName().equals(interleave.getTableName())).findFirst().orElse(null); + if (table != null) { + table.setInterleave(interleave); + } + } + return tables; + } + + private List getInterleavedTables(java.sql.Connection connection) throws SQLException { + ResultSet resultSet = connection.createStatement().executeQuery("SELECT\n" + + " TABLE_NAME, PARENT_TABLE_NAME, ON_DELETE_ACTION, table_type, SPANNER_STATE, INTERLEAVE_TYPE\n" + + " FROM\n" + + " information_schema.tables\n" + + " WHERE\n" + + " table_schema = '' AND PARENT_TABLE_NAME IS NOT NULL"); + + List interleavedTables = new ArrayList<>(); + + while (resultSet.next()) { + Interleave interleave = new Interleave(); + interleave.setTableName(resultSet.getString("TABLE_NAME")); + interleave.setParentName(resultSet.getString("PARENT_TABLE_NAME")); + interleave.setOnDeleteAction(resultSet.getString("ON_DELETE_ACTION")); + interleavedTables.add(interleave); + } + + if (!resultSet.isClosed()) { + resultSet.close(); + } + return interleavedTables; + } +}