From 68997e630a6bc733553d44430f187882863d4469 Mon Sep 17 00:00:00 2001 From: Matt Ward Date: Tue, 1 Nov 2011 19:19:45 +0000 Subject: [PATCH] ALF-11030: Create object graph from real database schema git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@31625 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../schemacomp/DbObjectXMLTransformer.java | 18 + .../DbObjectXMLTransformerTest.java | 2 +- .../alfresco/util/schemacomp/ExportDb.java | 383 ++++++++++++++++++ .../util/schemacomp/ExportDbTest.java | 251 ++++++++++++ .../util/schemacomp/SchemaCompTestSuite.java | 1 + .../schemacomp/SchemaCompTestingUtils.java | 14 +- .../util/schemacomp/SchemaComparatorTest.java | 23 +- .../org/alfresco/util/schemacomp/XML.java | 1 + .../alfresco/util/schemacomp/XMLToSchema.java | 5 +- .../util/schemacomp/XMLToSchemaTest.java | 4 +- .../alfresco/util/schemacomp/model/Index.java | 28 +- .../util/schemacomp/model/IndexTest.java | 4 + .../util/schemacomp/xml_to_schema_test.xml | 2 +- 13 files changed, 727 insertions(+), 9 deletions(-) create mode 100644 source/java/org/alfresco/util/schemacomp/ExportDb.java create mode 100644 source/java/org/alfresco/util/schemacomp/ExportDbTest.java diff --git a/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformer.java b/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformer.java index f2e431de83..adc33d4eaa 100644 --- a/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformer.java +++ b/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformer.java @@ -68,8 +68,11 @@ public class DbObjectXMLTransformer String tagName = dbObject.getClass().getSimpleName().toLowerCase(); final AttributesImpl attribs = new AttributesImpl(); attribs.addAttribute("", "", "name", "CDATA", dbObject.getName()); + // Add class-specific attributes. + addAttributes(dbObject, attribs); xmlOut.startElement("", "", tagName, attribs); + // The element's contents can optionally be populated with class-specific content. transformDbObject(dbObject); @@ -77,6 +80,21 @@ public class DbObjectXMLTransformer xmlOut.endElement("", "", tagName); } + /** + * Add class-specific attributes. + * + * @param dbObject + * @param attribs + */ + private void addAttributes(DbObject dbObject, AttributesImpl attribs) + { + if (dbObject instanceof Index) + { + Index index = (Index) dbObject; + attribs.addAttribute("", "", "unique", "CDATA", Boolean.toString(index.isUnique())); + } + } + private void transformDbObject(DbObject dbObject) throws SAXException { if (dbObject instanceof Schema) diff --git a/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformerTest.java b/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformerTest.java index 6c11f5c7ad..f0c1cc447a 100644 --- a/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformerTest.java +++ b/source/java/org/alfresco/util/schemacomp/DbObjectXMLTransformerTest.java @@ -142,7 +142,7 @@ public class DbObjectXMLTransformerTest BufferedReader reader = new BufferedReader(new StringReader(writer.toString())); dumpOutput(); assertHasPreamble(reader); - assertEquals("", reader.readLine()); + assertEquals("", reader.readLine()); assertEquals(" ", reader.readLine()); assertEquals(" first", reader.readLine()); assertEquals(" second", reader.readLine()); diff --git a/source/java/org/alfresco/util/schemacomp/ExportDb.java b/source/java/org/alfresco/util/schemacomp/ExportDb.java new file mode 100644 index 0000000000..477707d865 --- /dev/null +++ b/source/java/org/alfresco/util/schemacomp/ExportDb.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.schemacomp; + +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.util.Map; +import java.util.TreeMap; + +import javax.sql.DataSource; + +import org.alfresco.util.PropertyCheck; +import org.alfresco.util.schemacomp.model.Column; +import org.alfresco.util.schemacomp.model.ForeignKey; +import org.alfresco.util.schemacomp.model.Index; +import org.alfresco.util.schemacomp.model.PrimaryKey; +import org.alfresco.util.schemacomp.model.Schema; +import org.alfresco.util.schemacomp.model.Table; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.TypeNames; +import org.springframework.context.ApplicationContext; + + +/** + * Exports a database schema to an in-memory {@link Schema} object. + * + * @author Matt Ward + */ +public class ExportDb +{ + /** Reverse map from database types to JDBC types (loaded from a Hibernate dialect). */ + private final Map reverseTypeMap = new TreeMap(); + + /** The JDBC DataSource. */ + private DataSource dataSource; + + /** The object graph we're building */ + private Schema schema; + + private Dialect dialect; + + + + public ExportDb(ApplicationContext context) throws Exception + { + this((DataSource) context.getBean("dataSource"), + (Dialect) context.getBean("dialect")); + } + + + /** + * Create a new instance of the tool within the context of an existing database connection + * + * @param connection the database connection to use for metadata queries + * @param dialect the Hibernate dialect + */ + public ExportDb(final DataSource dataSource, final Dialect dialect) throws Exception + { + this.dataSource = dataSource; + this.dialect = dialect; + init(); + } + + + /** + * Initializes the fields ready to perform the database metadata reading + * @param dialect the Hibernate dialect + */ + @SuppressWarnings("unchecked") + private void init() throws Exception + { + final Field typeNamesField = Dialect.class.getDeclaredField("typeNames"); + typeNamesField.setAccessible(true); + final TypeNames typeNames = (TypeNames) typeNamesField.get(dialect); + final Field defaultsField = TypeNames.class.getDeclaredField("defaults"); + defaultsField.setAccessible(true); + final Map forwardMap2 = (Map) defaultsField.get(typeNames); + for (final Map.Entry e : forwardMap2.entrySet()) + { + this.reverseTypeMap.put(e.getValue(), e.getKey()); + } + + final Field weightedField = TypeNames.class.getDeclaredField("weighted"); + weightedField.setAccessible(true); + final Map> forwardMap1 = (Map>) weightedField + .get(typeNames); + for (final Map.Entry> e : forwardMap1.entrySet()) + { + for (final String type : e.getValue().values()) + { + this.reverseTypeMap.put(type, e.getKey()); + } + } + } + + + + public void execute() throws Exception + { + PropertyCheck.mandatory(this, "dataSource", dataSource); + // Get a Connection + Connection connection = dataSource.getConnection(); + + try + { + connection.setAutoCommit(false); + execute(connection); + } + finally + { + try { connection.close(); } catch (Throwable e) {} + } + } + + + + private void execute(Connection con) throws Exception + { + final DatabaseMetaData dbmd = con.getMetaData(); + + // Assume that if there are schemas, we want the one named after the connection user or the one called "dbo" (MS + // SQL hack) + String schemaName = null; + final ResultSet schemas = dbmd.getSchemas(); + while (schemas.next()) + { + final String thisSchema = schemas.getString("TABLE_SCHEM"); + if (thisSchema.equals(dbmd.getUserName()) || thisSchema.equalsIgnoreCase("dbo")) + { + schemaName = thisSchema; + break; + } + } + schemas.close(); + + schema = new Schema(schemaName); + + final ResultSet tables = dbmd.getTables(null, schemaName, "%", new String[] + { + "TABLE", "VIEW" + }); + + + while (tables.next()) + { + final String tableName = tables.getString("TABLE_NAME"); + + // Oracle hack: ignore tables in the recycle bin + if (tableName.startsWith("BIN$")) + { + continue; + } + + Table table = new Table(tableName); + schema.add(table); + + // Table columns + final ResultSet columns = dbmd.getColumns(null, tables.getString("TABLE_SCHEM"), tableName, "%"); + while (columns.next()) + { + String columnName = columns.getString("COLUMN_NAME"); + Column column = new Column(columnName); + + String dbType = columns.getString("TYPE_NAME"); + int colSize = columns.getInt("COLUMN_SIZE"); + int scale = columns.getInt("DECIMAL_DIGITS"); + int jdbcType = columns.getInt("DATA_TYPE"); + String type = generateType(dbType, colSize, scale, jdbcType); + column.setType(type); + + String nullableString = columns.getString("IS_NULLABLE"); + column.setNullable(parseBoolean(nullableString)); + + table.getColumns().add(column); + } + columns.close(); + + + // Primary key + final ResultSet primarykeycols = dbmd.getPrimaryKeys(null, tables.getString("TABLE_SCHEM"), tableName); + + PrimaryKey pk = null; + + while (primarykeycols.next()) + { + if (pk == null) + { + String pkName = primarykeycols.getString("PK_NAME"); + pk = new PrimaryKey(pkName); + } + String columnName = primarykeycols.getString("COLUMN_NAME"); + pk.getColumnNames().add(columnName); + } + primarykeycols.close(); + + // If this table has a primary key, add it. + if (pk != null) + { + table.setPrimaryKey(pk); + } + + + // Indexes + final ResultSet indexes = dbmd.getIndexInfo(null, tables.getString("TABLE_SCHEM"), tableName, false, true); + String lastIndexName = ""; + + Index index = null; + + while (indexes.next()) + { + final String indexName = indexes.getString("INDEX_NAME"); + if (indexName == null) + { + // Oracle seems to have some dummy index entries + continue; + } + // Skip the index corresponding to the PK if it is mentioned + else if (indexName.equals(table.getPrimaryKey().getName())) + { + continue; + } + + if (!indexName.equals(lastIndexName)) + { + index = new Index(indexName); + index.setUnique(!indexes.getBoolean("NON_UNIQUE")); + table.getIndexes().add(index); + lastIndexName = indexName; + } + if (index != null) + { + String columnName = indexes.getString("COLUMN_NAME"); + index.getColumnNames().add(columnName); + } + } + indexes.close(); + + + + final ResultSet foreignkeys = dbmd.getImportedKeys(null, tables.getString("TABLE_SCHEM"), tableName); + String lastKeyName = ""; + + ForeignKey fk = null; + + while (foreignkeys.next()) + { + final String keyName = foreignkeys.getString("FK_NAME"); + if (!keyName.equals(lastKeyName)) + { + fk = new ForeignKey(keyName); + table.getForeignKeys().add(fk); + lastKeyName = keyName; + } + if (fk != null) + { + fk.setLocalColumn(foreignkeys.getString("FKCOLUMN_NAME")); + fk.setTargetTable(foreignkeys.getString("PKTABLE_NAME")); + fk.setTargetColumn(foreignkeys.getString("PKCOLUMN_NAME")); + } + } + foreignkeys.close(); + } + tables.close(); + } + + /** + * Convert a boolean string as used in the database, to a boolean value. + * + * @param nullableString + * @return true if "YES", false if "NO" + */ + private boolean parseBoolean(String nullableString) + { + // TODO: what about (from the javadoc): + // empty string --- if the nullability for the parameter is unknown + if (nullableString.equals("NO")) + { + return false; + } + if (nullableString.equals("YES")) + { + return true; + } + + throw new IllegalArgumentException("Unsupported term \"" + nullableString + + "\", perhaps this database doesn't use YES/NO for booleans?"); + + } + + protected String generateType(final String dbType, int size, final int digits, int sqlType) + throws IllegalArgumentException, IllegalAccessException + { + String dbName = dbType.toLowerCase() + "(" + size + ")"; + if (this.reverseTypeMap.containsKey(dbName)) + { + // the map may contain an exact match, e.g. "char(1)" + return dbName; + } + + dbName = dbType.toLowerCase() + "(" + size + ", " + digits + ")"; + if (this.reverseTypeMap.containsKey(dbName)) + { + // the map may contain an exact match, e.g. "numeric(3, 1)" + return dbName; + } + else + { + String precisionScaleType = dbType + "(" + size + ", " + digits + ")"; + String sizeType = dbType + "(" + size + ")"; + + for (String key : reverseTypeMap.keySet()) + { + // Populate the placeholders, examples: + // varchar($l) => varchar(20) + // numeric($p, $s) => numeric(5, 2) + String popKey = key.replaceAll("\\$p", String.valueOf(size)); + popKey = popKey.replaceAll("\\$s", String.valueOf(digits)); + popKey = popKey.replaceAll("\\$l", String.valueOf(size)); + + // If the populated key matches a precision/scale type or a size type + // then the populated key gives us the string we're after. + if (popKey.equals(precisionScaleType) || popKey.equals(sizeType)) + { + return popKey; + } + } + } + + return dbType; + } + + + /** + * @return the schema + */ + public Schema getSchema() + { + return this.schema; + } + + + + public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException + { + ExportDb exportDb = null; + try + { + exportDb = new ExportDb(null, new PostgreSQLDialect()); + } + catch (Exception error) + { + throw new RuntimeException(error); + } + + if (exportDb != null) + { + String varchar = exportDb.generateType("varchar", 20, 0, 12); + System.out.println("varchar: " + varchar); + + String int4 = exportDb.generateType("int4", 20, 0, 4); + System.out.println("int4: " + int4); + } + } +} diff --git a/source/java/org/alfresco/util/schemacomp/ExportDbTest.java b/source/java/org/alfresco/util/schemacomp/ExportDbTest.java new file mode 100644 index 0000000000..d163415e1b --- /dev/null +++ b/source/java/org/alfresco/util/schemacomp/ExportDbTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.schemacomp; + + +import java.util.Iterator; + +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.schemacomp.model.Column; +import org.alfresco.util.schemacomp.model.DbObject; +import org.alfresco.util.schemacomp.model.ForeignKey; +import org.alfresco.util.schemacomp.model.Index; +import org.alfresco.util.schemacomp.model.Schema; +import org.alfresco.util.schemacomp.model.Sequence; +import org.alfresco.util.schemacomp.model.Table; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.ApplicationContext; + +/** + * Tests for the ExportDb class. + * + * @author Matt Ward + */ +public class ExportDbTest +{ + private ApplicationContext ctx; + private ExportDb exporter; + + @Before + public void setUp() throws Exception + { + ctx = ApplicationContextHelper.getApplicationContext(); + exporter = new ExportDb(ctx); + } + + + @Test + public void exportDb() throws Exception + { + exporter.execute(); + + Schema schema = exporter.getSchema(); + System.out.println(schema); + + Table appliedPatchTable = null; + Table qNameTable = null; + Sequence authoritySeq = null; + + for (DbObject dbo : schema) + { + if (dbo.getName().equals("alf_applied_patch")) + { + appliedPatchTable = (Table) dbo; + } + if (dbo.getName().equals("alf_qname")) + { + qNameTable = (Table) dbo; + } + if (dbo.getName().equals("alf_authority_seq")) + { + authoritySeq = (Sequence) dbo; + } + } + + checkAppliedPatchTable(appliedPatchTable); + checkQNameTable(qNameTable); + // TODO: what to do about sequences? They can't easily be retrieved with JDBC's DatabaseMetaData + //checkAuthoritySequence(authoritySeq); + } + + + /** + * @param qNameTable + */ + private void checkQNameTable(Table qNameTable) + { + /* + CREATE TABLE alf_qname + ( + id INT8 NOT NULL, + version INT8 NOT NULL, + ns_id INT8 NOT NULL, + local_name VARCHAR(200) NOT NULL, + CONSTRAINT fk_alf_qname_ns FOREIGN KEY (ns_id) REFERENCES alf_namespace (id), + PRIMARY KEY (id) + ); + CREATE UNIQUE INDEX ns_id ON alf_qname (ns_id, local_name); + CREATE INDEX fk_alf_qname_ns ON alf_qname (ns_id); + */ + + assertNotNull("Couldn't find table alf_qname", qNameTable); + + Iterator colIt = qNameTable.getColumns().iterator(); + Column col = colIt.next(); + assertEquals("id", col.getName()); + assertEquals("int8", col.getType()); + assertEquals(false, col.isNullable()); + + col = colIt.next(); + assertEquals("version", col.getName()); + assertEquals("int8", col.getType()); + assertEquals(false, col.isNullable()); + + col = colIt.next(); + assertEquals("ns_id", col.getName()); + assertEquals("int8", col.getType()); + assertEquals(false, col.isNullable()); + + col = colIt.next(); + assertEquals("local_name", col.getName()); + assertEquals("varchar(200)", col.getType()); + assertEquals(false, col.isNullable()); + + assertEquals(2, qNameTable.getIndexes().size()); + Iterator indexIt = qNameTable.getIndexes().iterator(); + + Index index = indexIt.next(); + assertEquals("ns_id", index.getName()); + assertEquals(true, index.isUnique()); + assertEquals(2, index.getColumnNames().size()); + assertEquals("ns_id", index.getColumnNames().get(0)); + assertEquals("local_name", index.getColumnNames().get(1)); + + index = indexIt.next(); + assertEquals("fk_alf_qname_ns", index.getName()); + assertEquals(1, index.getColumnNames().size()); + assertEquals("ns_id", index.getColumnNames().get(0)); + + assertEquals("id", qNameTable.getPrimaryKey().getColumnNames().get(0)); + + assertEquals(1, qNameTable.getForeignKeys().size()); + ForeignKey fk = qNameTable.getForeignKeys().get(0); + assertEquals("fk_alf_qname_ns", fk.getName()); + assertEquals("ns_id", fk.getLocalColumn()); + assertEquals("alf_namespace", fk.getTargetTable()); + assertEquals("id", fk.getTargetColumn()); + } + + + /** + * @param appliedPatch + */ + private void checkAppliedPatchTable(Table appliedPatch) + { + /* + CREATE TABLE alf_applied_patch + ( + id VARCHAR(64) NOT NULL, + description VARCHAR(1024), + fixes_from_schema INT4, + fixes_to_schema INT4, + applied_to_schema INT4, + target_schema INT4, + applied_on_date TIMESTAMP, + applied_to_server VARCHAR(64), + was_executed BOOL, + succeeded BOOL, + report VARCHAR(1024), + PRIMARY KEY (id) + ); + */ + assertNotNull("Couldn't find alf_applied_patch", appliedPatch); + + assertEquals("alf_applied_patch", appliedPatch.getName()); + Iterator colIt = appliedPatch.getColumns().iterator(); + Column col = colIt.next(); + assertEquals("id", col.getName()); + assertEquals("varchar(64)", col.getType()); + assertEquals(false, col.isNullable()); + + col = colIt.next(); + assertEquals("description", col.getName()); + assertEquals("varchar(1024)", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("fixes_from_schema", col.getName()); + assertEquals("int4", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("fixes_to_schema", col.getName()); + assertEquals("int4", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("applied_to_schema", col.getName()); + assertEquals("int4", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("target_schema", col.getName()); + assertEquals("int4", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("applied_on_date", col.getName()); + assertEquals("timestamp", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("applied_to_server", col.getName()); + assertEquals("varchar(64)", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("was_executed", col.getName()); + assertEquals("bool", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("succeeded", col.getName()); + assertEquals("bool", col.getType()); + assertEquals(true, col.isNullable()); + + col = colIt.next(); + assertEquals("report", col.getName()); + assertEquals("varchar(1024)", col.getType()); + assertEquals(true, col.isNullable()); + + assertEquals("id", appliedPatch.getPrimaryKey().getColumnNames().get(0)); + } + + + public void checkAuthoritySequence(Sequence seq) + { + /* + CREATE SEQUENCE alf_authority_seq START WITH 1 INCREMENT BY 1; + */ + assertNotNull("Couldn't find sequence alf_authority_seq", seq); + assertEquals("alf_authority_seq", seq.getName()); + } +} diff --git a/source/java/org/alfresco/util/schemacomp/SchemaCompTestSuite.java b/source/java/org/alfresco/util/schemacomp/SchemaCompTestSuite.java index 33d505d5d2..ea09a49a2e 100644 --- a/source/java/org/alfresco/util/schemacomp/SchemaCompTestSuite.java +++ b/source/java/org/alfresco/util/schemacomp/SchemaCompTestSuite.java @@ -32,6 +32,7 @@ import org.junit.runners.Suite; DbObjectXMLTransformerTest.class, DbPropertyTest.class, DefaultComparisonUtilsTest.class, + ExportDbTest.class, SchemaComparatorTest.class, ValidatingVisitorTest.class, SchemaToXMLTest.class, diff --git a/source/java/org/alfresco/util/schemacomp/SchemaCompTestingUtils.java b/source/java/org/alfresco/util/schemacomp/SchemaCompTestingUtils.java index 9cf7c50935..847575bf27 100644 --- a/source/java/org/alfresco/util/schemacomp/SchemaCompTestingUtils.java +++ b/source/java/org/alfresco/util/schemacomp/SchemaCompTestingUtils.java @@ -108,12 +108,24 @@ public class SchemaCompTestingUtils { String[] parts = indexDefs[i].split(" "); String name = parts[0]; - String[] columns = (String[]) ArrayUtils.subarray(parts, 1, parts.length); + + boolean unique = false; + int columnsStart = 1; + + if (parts[1].equals("[unique]")) + { + unique = true; + columnsStart++; + } + + String[] columns = (String[]) ArrayUtils.subarray(parts, columnsStart, parts.length); indexes[i] = new Index(null, name, Arrays.asList(columns)); + indexes[i].setUnique(unique); } return Arrays.asList(indexes); } + public static Sequence sequence(String name) { return new Sequence(name); diff --git a/source/java/org/alfresco/util/schemacomp/SchemaComparatorTest.java b/source/java/org/alfresco/util/schemacomp/SchemaComparatorTest.java index a0d9986cd7..d556887efe 100644 --- a/source/java/org/alfresco/util/schemacomp/SchemaComparatorTest.java +++ b/source/java/org/alfresco/util/schemacomp/SchemaComparatorTest.java @@ -28,6 +28,7 @@ import static org.alfresco.util.schemacomp.SchemaCompTestingUtils.indexes; import static org.alfresco.util.schemacomp.SchemaCompTestingUtils.pk; import static org.alfresco.util.schemacomp.SchemaCompTestingUtils.table; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import java.util.Arrays; @@ -71,14 +72,14 @@ public class SchemaComparatorTest indexes("idx_node id nodeRef"))); left.add(table("table_in_left")); left.add(new Table(left, "tbl_has_diff_pk", columns("id NUMBER(10)", "nodeRef VARCHAR2(200)"), - pk("pk_is_diff", "id"), fkeys(), indexes())); + pk("pk_is_diff", "id"), fkeys(), indexes("idx_one id nodeRef", "idx_two id"))); // Right hand side's database objects. right.add(new Table(right, "tbl_no_diff", columns("id NUMBER(10)", "nodeRef VARCHAR2(200)", "name VARCHAR2(150)"), pk("pk_tbl_no_diff", "id"), fkeys(fk("fk_tbl_no_diff", "nodeRef", "node", "nodeRef")), indexes("idx_node id nodeRef"))); right.add(new Table(right, "tbl_has_diff_pk", columns("id NUMBER(10)", "nodeRef VARCHAR2(200)"), - pk("pk_is_diff", "nodeRef"), fkeys(), indexes())); + pk("pk_is_diff", "nodeRef"), fkeys(), indexes("idx_one id nodeRef", "idx_two [unique] id"))); right.add(table("table_in_right")); @@ -91,7 +92,6 @@ public class SchemaComparatorTest Results differences = comparator.getDifferences(); - assertEquals(5, differences.size()); Iterator it = differences.iterator(); @@ -135,6 +135,23 @@ public class SchemaComparatorTest assertEquals("columnNames[0]", diff.getRight().getPropertyName()); assertEquals("nodeRef", diff.getRight().getPropertyValue()); + // idx_two is unique in the righ_schema but not in the left + diff = it.next(); + assertEquals("left_schema.tbl_has_diff_pk.idx_two.unique", diff.getLeft().getPath()); + assertEquals("right_schema.tbl_has_diff_pk.idx_two.unique", diff.getRight().getPath()); + assertEquals("unique", diff.getLeft().getPropertyName()); + assertEquals(false, diff.getLeft().getPropertyValue()); + assertEquals("unique", diff.getRight().getPropertyName()); + assertEquals(true, diff.getRight().getPropertyValue()); + // Table table_in_right does not exist in the left schema + diff = it.next(); + assertEquals(Where.ONLY_IN_RIGHT, diff.getWhere()); + assertEquals("right_schema.table_in_right", diff.getRight().getPath()); + assertEquals(null, diff.getLeft()); + assertEquals(null, diff.getRight().getPropertyName()); + assertEquals(null, diff.getRight().getPropertyValue()); + + assertFalse("There should be no more differences", it.hasNext()); } } diff --git a/source/java/org/alfresco/util/schemacomp/XML.java b/source/java/org/alfresco/util/schemacomp/XML.java index 7b7cd569bc..c324cc6b49 100644 --- a/source/java/org/alfresco/util/schemacomp/XML.java +++ b/source/java/org/alfresco/util/schemacomp/XML.java @@ -43,4 +43,5 @@ public abstract class XML public static final String EL_TARGET_TABLE = "targettable"; public static final String ATTR_NAME = "name"; + public static final String ATTR_UNIQUE = "unique"; } diff --git a/source/java/org/alfresco/util/schemacomp/XMLToSchema.java b/source/java/org/alfresco/util/schemacomp/XMLToSchema.java index 75459e888b..faaceb80cf 100644 --- a/source/java/org/alfresco/util/schemacomp/XMLToSchema.java +++ b/source/java/org/alfresco/util/schemacomp/XMLToSchema.java @@ -162,7 +162,10 @@ public class XMLToSchema extends DefaultHandler } else if (qName.equals(XML.EL_INDEX)) { - dboStack.push(new Index(atts.getValue(XML.ATTR_NAME))); + Index index = new Index(atts.getValue(XML.ATTR_NAME)); + boolean unique = Boolean.parseBoolean(atts.getValue(XML.ATTR_UNIQUE)); + index.setUnique(unique); + dboStack.push(index); } else if (qName.equals(XML.EL_SEQUENCE)) { diff --git a/source/java/org/alfresco/util/schemacomp/XMLToSchemaTest.java b/source/java/org/alfresco/util/schemacomp/XMLToSchemaTest.java index 512f460499..4dc5729dd6 100644 --- a/source/java/org/alfresco/util/schemacomp/XMLToSchemaTest.java +++ b/source/java/org/alfresco/util/schemacomp/XMLToSchemaTest.java @@ -19,6 +19,8 @@ package org.alfresco.util.schemacomp; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import java.io.BufferedInputStream; @@ -31,7 +33,6 @@ import org.alfresco.util.schemacomp.model.Sequence; import org.alfresco.util.schemacomp.model.Table; import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; /** * Tests for the XMLToSchema class. @@ -89,6 +90,7 @@ public class XMLToSchemaTest assertEquals(1, table.getIndexes().size()); assertEquals("idx_node_by_id", table.getIndexes().get(0).getName()); + assertEquals(true, table.getIndexes().get(0).isUnique()); assertEquals(2, table.getIndexes().get(0).getColumnNames().size()); assertEquals("id", table.getIndexes().get(0).getColumnNames().get(0)); assertEquals("nodeRef", table.getIndexes().get(0).getColumnNames().get(1)); diff --git a/source/java/org/alfresco/util/schemacomp/model/Index.java b/source/java/org/alfresco/util/schemacomp/model/Index.java index 56bf52390b..c3c949eb19 100644 --- a/source/java/org/alfresco/util/schemacomp/model/Index.java +++ b/source/java/org/alfresco/util/schemacomp/model/Index.java @@ -34,7 +34,7 @@ import org.alfresco.util.schemacomp.Result.Strength; public class Index extends AbstractDbObject { private final List columnNames = new ArrayList(); - + private boolean unique; public Index(String name) { @@ -69,12 +69,33 @@ public class Index extends AbstractDbObject this.columnNames.addAll(columnNames); } + /** + * Does this index have the unique attribute? + * + * @return unique + */ + public boolean isUnique() + { + return this.unique; + } + + /** + * @see #isUnique() + * @param unique + */ + public void setUnique(boolean unique) + { + this.unique = unique; + } + + @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((this.columnNames == null) ? 0 : this.columnNames.hashCode()); + result = prime * result + (this.unique ? 1231 : 1237); return result; } @@ -90,6 +111,7 @@ public class Index extends AbstractDbObject if (other.columnNames != null) return false; } else if (!this.columnNames.equals(other.columnNames)) return false; + if (this.unique != other.unique) return false; return true; } @@ -126,6 +148,10 @@ public class Index extends AbstractDbObject new DbProperty(rightIndex, "columnNames"), ctx, strength); + comparisonUtils.compareSimple( + new DbProperty(this, "unique"), + new DbProperty(rightIndex, "unique"), + ctx); } diff --git a/source/java/org/alfresco/util/schemacomp/model/IndexTest.java b/source/java/org/alfresco/util/schemacomp/model/IndexTest.java index 80085671e6..b21d4de1f6 100644 --- a/source/java/org/alfresco/util/schemacomp/model/IndexTest.java +++ b/source/java/org/alfresco/util/schemacomp/model/IndexTest.java @@ -64,6 +64,10 @@ public class IndexTest extends DbObjectTestBase new DbProperty(thatIndex, "columnNames"), ctx, Strength.ERROR); + inOrder.verify(comparisonUtils).compareSimple( + new DbProperty(thisIndex, "unique"), + new DbProperty(thatIndex, "unique"), + ctx); } diff --git a/source/java/org/alfresco/util/schemacomp/xml_to_schema_test.xml b/source/java/org/alfresco/util/schemacomp/xml_to_schema_test.xml index 9788c96a47..1cea988710 100644 --- a/source/java/org/alfresco/util/schemacomp/xml_to_schema_test.xml +++ b/source/java/org/alfresco/util/schemacomp/xml_to_schema_test.xml @@ -32,7 +32,7 @@ - + id nodeRef