Implement schema upgrading
Change-Id: I3a82db2e8a2f446477806e93b236d5bd77957660
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/README_ECLIPSE b/README_ECLIPSE
index 9b7cc01..8e3c06d 100644
--- a/README_ECLIPSE
+++ b/README_ECLIPSE
@@ -17,3 +17,25 @@
- Import... and select GoogleFormat.xml
+
+Unit Tests
+==========
+
+The unit tests assume there is a local PostgreSQL and MySQL server
+listening on their respective default ports, with the following
+database configuration:
+
+PostgreSQL:
+
+ createuser -S -D -R -E -P gwtorm
+ Password: gwtorm
+
+ createdb -E UTF-8 -O gwtorm gwtorm
+
+MySQL:
+
+ create user 'gwtorm'@'localhost' identified by 'gwtorm';
+ create database gwtorm;
+ alter database gwtorm charset=utf8;
+ grant all on gwtorm.* to 'gwtorm'@'localhost';
+ flush privileges;
diff --git a/pom.xml b/pom.xml
index 387b44b..de1e587 100644
--- a/pom.xml
+++ b/pom.xml
@@ -346,7 +346,21 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
- <version>1.1.104</version>
+ <version>1.2.125</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ <version>8.4-701.jdbc4</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <version>5.1.10</version>
<scope>test</scope>
</dependency>
</dependencies>
diff --git a/src/main/java/com/google/gwtorm/client/Schema.java b/src/main/java/com/google/gwtorm/client/Schema.java
index 13def7c..09c4ba9 100644
--- a/src/main/java/com/google/gwtorm/client/Schema.java
+++ b/src/main/java/com/google/gwtorm/client/Schema.java
@@ -56,11 +56,24 @@
*/
public interface Schema {
/**
- * Automatically create the database tables.
+ * Add any missing columns, create any missing tables or sequences.
+ * <p>
+ * This method does not drop any unused columns or tables, leaving them intact
+ * for applications to continue to query after the update. Any unused columns
+ * that are NOT NULL are altered to accept NULL.
*
- * @throws OrmException tables already exist or create permission is denied.
+ * @throws OrmException one or more objects could not be added to the schema.
*/
- void createSchema() throws OrmException;
+ void updateSchema() throws OrmException;
+
+ /**
+ * Drop any unused columns, tables, or sequences.
+ * <p>
+ * This method destroys data, as columns may be removed entirely.
+ *
+ * @throws OrmException one or more drops could not be completed.
+ */
+ void pruneSchema() throws OrmException;
/**
* Begin a new transaction.
diff --git a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
index 9eba791..007d5f4 100644
--- a/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
+++ b/src/main/java/com/google/gwtorm/jdbc/JdbcSchema.java
@@ -19,6 +19,7 @@
import com.google.gwtorm.client.OrmRunnable;
import com.google.gwtorm.client.Schema;
import com.google.gwtorm.client.Transaction;
+import com.google.gwtorm.schema.ColumnModel;
import com.google.gwtorm.schema.RelationModel;
import com.google.gwtorm.schema.SchemaModel;
import com.google.gwtorm.schema.SequenceModel;
@@ -27,6 +28,8 @@
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
/** Internal base class for implementations of {@link Schema}. */
public abstract class JdbcSchema implements Schema {
@@ -68,25 +71,136 @@
}
}
- public void createSchema() throws OrmException {
- final SqlDialect dialect = dbDef.getDialect();
- final SchemaModel model = dbDef.getSchemaModel();
+ public void updateSchema() throws OrmException {
try {
- final Statement stmt;
+ createSequences();
+ createRelations();
- stmt = getConnection().createStatement();
- try {
- for (final SequenceModel s : model.getSequences()) {
- stmt.execute(s.getCreateSequenceSql(dialect));
- }
- for (final RelationModel r : model.getRelations()) {
- stmt.execute(r.getCreateTableSql(dialect));
- }
- } finally {
- stmt.close();
+ for (final RelationModel rel : dbDef.getSchemaModel().getRelations()) {
+ addColumns(rel);
}
} catch (SQLException e) {
- throw new OrmException("Schema creation failure", e);
+ throw new OrmException("Schema update failure", e);
+ }
+ }
+
+ private void createSequences() throws SQLException {
+ final SqlDialect dialect = dbDef.getDialect();
+ final SchemaModel model = dbDef.getSchemaModel();
+ final Statement stmt = getConnection().createStatement();
+ try {
+ Set<String> have = dialect.listSequences(getConnection());
+ for (final SequenceModel s : model.getSequences()) {
+ if (!have.contains(s.getSequenceName().toLowerCase())) {
+ stmt.execute(s.getCreateSequenceSql(dialect));
+ }
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private void createRelations() throws SQLException {
+ final SqlDialect dialect = dbDef.getDialect();
+ final SchemaModel model = dbDef.getSchemaModel();
+ final Statement stmt = getConnection().createStatement();
+ try {
+ Set<String> have = dialect.listTables(getConnection());
+ for (final RelationModel r : model.getRelations()) {
+ if (!have.contains(r.getRelationName().toLowerCase())) {
+ stmt.execute(r.getCreateTableSql(dialect));
+ }
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private void addColumns(final RelationModel rel) throws SQLException {
+ final SqlDialect dialect = dbDef.getDialect();
+ final SchemaModel model = dbDef.getSchemaModel();
+ final Statement stmt = getConnection().createStatement();
+ try {
+ Set<String> have = dialect.listColumns( //
+ getConnection(), rel.getRelationName().toLowerCase());
+ for (final ColumnModel c : rel.getColumns()) {
+ if (!have.contains(c.getColumnName().toLowerCase())) {
+ dialect.addColumn(stmt, rel.getRelationName(), c);
+ }
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ public void pruneSchema() throws OrmException {
+ try {
+ pruneSequences();
+ pruneRelations();
+
+ for (final RelationModel rel : dbDef.getSchemaModel().getRelations()) {
+ pruneColumns(rel);
+ }
+ } catch (SQLException e) {
+ throw new OrmException("Schema prune failure", e);
+ }
+ }
+
+ private void pruneSequences() throws SQLException {
+ final SqlDialect dialect = dbDef.getDialect();
+ final SchemaModel model = dbDef.getSchemaModel();
+ final Statement stmt = getConnection().createStatement();
+ try {
+ HashSet<String> want = new HashSet<String>();
+ for (final SequenceModel s : model.getSequences()) {
+ want.add(s.getSequenceName().toLowerCase());
+ }
+ for (final String sequence : dialect.listSequences(getConnection())) {
+ if (!want.contains(sequence)) {
+ stmt.execute(dialect.getDropSequenceSql(sequence));
+ }
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private void pruneRelations() throws SQLException {
+ final SqlDialect dialect = dbDef.getDialect();
+ final SchemaModel model = dbDef.getSchemaModel();
+ final Statement stmt = getConnection().createStatement();
+ try {
+ HashSet<String> want = new HashSet<String>();
+ for (final RelationModel r : model.getRelations()) {
+ want.add(r.getRelationName().toLowerCase());
+ }
+ for (final String table : dialect.listTables(getConnection())) {
+ if (!want.contains(table)) {
+ stmt.execute("DROP TABLE " + table);
+ }
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private void pruneColumns(final RelationModel rel) throws SQLException {
+ final SqlDialect dialect = dbDef.getDialect();
+ final SchemaModel model = dbDef.getSchemaModel();
+ final Statement stmt = getConnection().createStatement();
+ try {
+ HashSet<String> want = new HashSet<String>();
+ for (final ColumnModel c : rel.getColumns()) {
+ want.add(c.getColumnName().toLowerCase());
+ }
+ for (String column : dialect.listColumns( //
+ getConnection(), rel.getRelationName().toLowerCase())) {
+ if (!want.contains(column)) {
+ dialect.dropColumn(stmt, rel.getRelationName(), column);
+ }
+ }
+ } finally {
+ stmt.close();
}
}
diff --git a/src/main/java/com/google/gwtorm/schema/RelationModel.java b/src/main/java/com/google/gwtorm/schema/RelationModel.java
index 06edd1d..726ebbf 100644
--- a/src/main/java/com/google/gwtorm/schema/RelationModel.java
+++ b/src/main/java/com/google/gwtorm/schema/RelationModel.java
@@ -209,6 +209,13 @@
r.append(col.getColumnName());
r.append(" ");
r.append(dialect.getSqlTypeInfo(col).getSqlType(col, dialect));
+
+ String check =
+ dialect.getSqlTypeInfo(col).getCheckConstraint(col, dialect);
+ if (check != null) {
+ r.append(' ');
+ r.append(check);
+ }
if (i.hasNext()) {
r.append(",");
}
diff --git a/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java b/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java
index 04380a1..8b79dd3 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/DialectH2.java
@@ -16,8 +16,14 @@
import com.google.gwtorm.client.OrmDuplicateKeyException;
import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.schema.ColumnModel;
+import java.sql.Connection;
+import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
/** Dialect for <a href="http://www.h2database.com/">H2</a> */
public class DialectH2 extends SqlDialect {
@@ -38,4 +44,51 @@
public String getNextSequenceValueSql(final String seqname) {
return "SELECT NEXT VALUE FOR " + seqname;
}
+
+ @Override
+ public Set<String> listSequences(Connection db) throws SQLException {
+ Statement s = db.createStatement();
+ try {
+ ResultSet rs =
+ s.executeQuery("SELECT SEQUENCE_NAME"
+ + " FROM INFORMATION_SCHEMA.SEQUENCES"
+ + " WHERE SEQUENCE_SCHEMA = 'PUBLIC'");
+ try {
+ HashSet<String> sequences = new HashSet<String>();
+ while (rs.next()) {
+ sequences.add(rs.getString(1).toLowerCase());
+ }
+ return sequences;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ s.close();
+ }
+ }
+
+ @Override
+ public void addColumn(Statement stmt, String tableName, ColumnModel col)
+ throws SQLException {
+ final StringBuilder r = new StringBuilder();
+ r.append("ALTER TABLE ");
+ r.append(tableName);
+ r.append(" ADD ");
+ r.append(col.getColumnName());
+ r.append(" ");
+ r.append(getSqlTypeInfo(col).getSqlType(col, this));
+ stmt.execute(r.toString());
+
+ String check = getSqlTypeInfo(col).getCheckConstraint(col, this);
+ if (check != null) {
+ r.setLength(0);
+ r.append("ALTER TABLE ");
+ r.append(tableName);
+ r.append(" ADD CONSTRAINT ");
+ r.append(col.getColumnName() + "_check");
+ r.append(' ');
+ r.append(check);
+ stmt.execute(r.toString());
+ }
+ }
}
diff --git a/src/main/java/com/google/gwtorm/schema/sql/DialectMySQL.java b/src/main/java/com/google/gwtorm/schema/sql/DialectMySQL.java
index 0fa42cf..047e2fd 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/DialectMySQL.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/DialectMySQL.java
@@ -16,15 +16,17 @@
import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.Sequence;
import com.google.gwtorm.schema.ColumnModel;
import com.google.gwtorm.schema.SequenceModel;
import java.sql.Connection;
+import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
+import java.util.HashSet;
+import java.util.Set;
/** Dialect for <a href="http://www.mysql.com/">MySQL</a> */
public class DialectMySQL extends SqlDialect {
@@ -61,7 +63,6 @@
@Override
public String getCreateSequenceSql(final SequenceModel seq) {
- final Sequence s = seq.getSequence();
final StringBuilder r = new StringBuilder();
r.append("CREATE TABLE ");
r.append(seq.getSequenceName());
@@ -70,6 +71,11 @@
}
@Override
+ public String getDropSequenceSql(String name) {
+ return "DROP TABLE " + name;
+ }
+
+ @Override
public String getNextSequenceValueSql(final String seqname) {
return seqname;
}
@@ -101,4 +107,66 @@
throw convertError("sequence", seqname, e);
}
}
+
+ @Override
+ public Set<String> listTables(final Connection db) throws SQLException {
+ final String[] types = new String[] {"TABLE"};
+ final ResultSet rs = db.getMetaData().getTables(null, null, null, types);
+ try {
+ HashSet<String> tables = new HashSet<String>();
+ while (rs.next()) {
+ final String name = rs.getString("TABLE_NAME");
+ if (!isSequence(db, name)) {
+ tables.add(name.toLowerCase());
+ }
+ }
+ return tables;
+ } finally {
+ rs.close();
+ }
+ }
+
+ @Override
+ public Set<String> listSequences(final Connection db) throws SQLException {
+ final String[] types = new String[] {"TABLE"};
+ final ResultSet rs = db.getMetaData().getTables(null, null, null, types);
+ try {
+ HashSet<String> sequences = new HashSet<String>();
+ while (rs.next()) {
+ final String name = rs.getString("TABLE_NAME");
+ if (isSequence(db, name)) {
+ sequences.add(name.toLowerCase());
+ }
+ }
+ return sequences;
+ } finally {
+ rs.close();
+ }
+ }
+
+ private boolean isSequence(Connection db, String tableName)
+ throws SQLException {
+ final DatabaseMetaData meta = db.getMetaData();
+ if (meta.storesUpperCaseIdentifiers()) {
+ tableName = tableName.toUpperCase();
+ } else if (meta.storesLowerCaseIdentifiers()) {
+ tableName = tableName.toLowerCase();
+ }
+
+ ResultSet rs = meta.getColumns(null, null, tableName, null);
+ try {
+ int cnt = 0;
+ boolean serial = false;
+ while (rs.next()) {
+ cnt++;
+ if (rs.getInt("DATA_TYPE") == Types.BIGINT
+ && "YES".equalsIgnoreCase(rs.getString("IS_AUTOINCREMENT"))) {
+ serial = true;
+ }
+ }
+ return cnt == 1 && serial;
+ } finally {
+ rs.close();
+ }
+ }
}
diff --git a/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java b/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java
index 560b70a..bffc7ad 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/DialectPostgreSQL.java
@@ -19,8 +19,12 @@
import com.google.gwtorm.schema.RelationModel;
import java.sql.Connection;
+import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
import java.sql.Types;
+import java.util.HashSet;
+import java.util.Set;
/** Dialect for <a href="http://www.postgresql.org/>PostgreSQL</a> */
public class DialectPostgreSQL extends SqlDialect {
@@ -66,6 +70,26 @@
sqlBuffer.append("WITH (OIDS = FALSE)");
}
+ @Override
+ public Set<String> listSequences(Connection db) throws SQLException {
+ Statement s = db.createStatement();
+ try {
+ ResultSet rs =
+ s.executeQuery("SELECT relname FROM pg_class WHERE relkind = 'S'");
+ try {
+ HashSet<String> sequences = new HashSet<String>();
+ while (rs.next()) {
+ sequences.add(rs.getString(1).toLowerCase());
+ }
+ return sequences;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ s.close();
+ }
+ }
+
private static class Pre82 extends DialectPostgreSQL {
@Override
public void appendCreateTableStorage(final StringBuilder sqlBuffer,
diff --git a/src/main/java/com/google/gwtorm/schema/sql/SqlBooleanTypeInfo.java b/src/main/java/com/google/gwtorm/schema/sql/SqlBooleanTypeInfo.java
index 9828f4a..afea6b3 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/SqlBooleanTypeInfo.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/SqlBooleanTypeInfo.java
@@ -35,11 +35,19 @@
r.append(" DEFAULT " + f);
r.append(" NOT NULL");
}
- r.append(" CHECK (" + name + " IN (" + t + "," + f + "))");
return r.toString();
}
@Override
+ public String getCheckConstraint(final ColumnModel column,
+ final SqlDialect dialect) {
+ final String name = column.getColumnName();
+ final String t = getTrueLiteralValue();
+ final String f = getFalseLiteralValue();
+ return " CHECK (" + name + " IN (" + t + "," + f + "))";
+ }
+
+ @Override
protected String getJavaSqlTypeAlias() {
return "String";
}
diff --git a/src/main/java/com/google/gwtorm/schema/sql/SqlByteArrayTypeInfo.java b/src/main/java/com/google/gwtorm/schema/sql/SqlByteArrayTypeInfo.java
index 24b60c1..12ed53a 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/SqlByteArrayTypeInfo.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/SqlByteArrayTypeInfo.java
@@ -103,7 +103,7 @@
if (raw != null) {
ps.setBinaryStream(col, new ByteArrayInputStream(raw), raw.length);
} else {
- ps.setNull(col, Types.LONGVARCHAR);
+ ps.setNull(col, Types.VARBINARY);
}
}
}
diff --git a/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java b/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
index 7a8d243..540b334 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/SqlDialect.java
@@ -21,12 +21,15 @@
import com.google.gwtorm.schema.SequenceModel;
import java.sql.Connection;
+import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
public abstract class SqlDialect {
protected final Map<Class<?>, SqlTypeInfo> types;
@@ -163,6 +166,10 @@
return r.toString();
}
+ public String getDropSequenceSql(final String name) {
+ return "DROP SEQUENCE " + name;
+ }
+
/**
* Append driver specific storage parameters to a CREATE TABLE statement.
*
@@ -174,5 +181,108 @@
final RelationModel relationModel) {
}
+ /**
+ * List all tables in the current database schema.
+ *
+ * @param db connection to the schema.
+ * @return set of declared tables, in lowercase.
+ * @throws SQLException the tables cannot be listed.
+ */
+ public Set<String> listTables(final Connection db) throws SQLException {
+ final String[] types = new String[] {"TABLE"};
+ final ResultSet rs = db.getMetaData().getTables(null, null, null, types);
+ try {
+ HashSet<String> tables = new HashSet<String>();
+ while (rs.next()) {
+ tables.add(rs.getString("TABLE_NAME").toLowerCase());
+ }
+ return tables;
+ } finally {
+ rs.close();
+ }
+ }
+
+ /**
+ * List all sequences in the current database schema.
+ *
+ * @param db connection to the schema.
+ * @return set of declared sequences, in lowercase.
+ * @throws SQLException the sequence objects cannot be listed.
+ */
+ public abstract Set<String> listSequences(final Connection db)
+ throws SQLException;
+
+ /**
+ * List all columns in the given table name.
+ *
+ * @param db connection to the schema.
+ * @param tableName the table to list columns from, in lowercase.
+ * @return set of declared columns, in lowercase.
+ * @throws SQLException the columns cannot be listed from the relation.
+ */
+ public Set<String> listColumns(final Connection db, String tableName)
+ throws SQLException {
+ final DatabaseMetaData meta = db.getMetaData();
+ if (meta.storesUpperCaseIdentifiers()) {
+ tableName = tableName.toUpperCase();
+ } else if (meta.storesLowerCaseIdentifiers()) {
+ tableName = tableName.toLowerCase();
+ }
+
+ ResultSet rs = meta.getColumns(null, null, tableName, null);
+ try {
+ HashSet<String> columns = new HashSet<String>();
+ while (rs.next()) {
+ columns.add(rs.getString("COLUMN_NAME").toLowerCase());
+ }
+ return columns;
+ } finally {
+ rs.close();
+ }
+ }
+
+ /**
+ * Add one column to an existing table.
+ *
+ * @param stmt statement to use to execute the SQL command(s).
+ * @param tableName table to add the column onto.
+ * @param col definition of the column.
+ * @throws SQLException the column could not be added.
+ */
+ public void addColumn(Statement stmt, String tableName, ColumnModel col)
+ throws SQLException {
+ final StringBuilder r = new StringBuilder();
+ r.append("ALTER TABLE ");
+ r.append(tableName);
+ r.append(" ADD ");
+ r.append(col.getColumnName());
+ r.append(" ");
+ r.append(getSqlTypeInfo(col).getSqlType(col, this));
+ String check = getSqlTypeInfo(col).getCheckConstraint(col, this);
+ if (check != null) {
+ r.append(' ');
+ r.append(check);
+ }
+ stmt.execute(r.toString());
+ }
+
+ /**
+ * Drop one column from an existing table.
+ *
+ * @param stmt statement to use to execute the SQL command(s).
+ * @param tableName table to add the column onto.
+ * @param column name of the column to drop.
+ * @throws SQLException the column could not be added.
+ */
+ public void dropColumn(Statement stmt, String tableName, String column)
+ throws SQLException {
+ final StringBuilder r = new StringBuilder();
+ r.append("ALTER TABLE ");
+ r.append(tableName);
+ r.append(" DROP COLUMN ");
+ r.append(column);
+ stmt.execute(r.toString());
+ }
+
public abstract String getNextSequenceValueSql(String seqname);
}
diff --git a/src/main/java/com/google/gwtorm/schema/sql/SqlTypeInfo.java b/src/main/java/com/google/gwtorm/schema/sql/SqlTypeInfo.java
index 35ed96a..ba9064e 100644
--- a/src/main/java/com/google/gwtorm/schema/sql/SqlTypeInfo.java
+++ b/src/main/java/com/google/gwtorm/schema/sql/SqlTypeInfo.java
@@ -29,6 +29,11 @@
public abstract String getSqlType(ColumnModel column, SqlDialect dialect);
+ public String getCheckConstraint(final ColumnModel column,
+ final SqlDialect dialect) {
+ return null;
+ }
+
protected abstract String getJavaSqlTypeAlias();
protected abstract int getSqlTypeConstant();
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java b/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java
new file mode 100644
index 0000000..d79bfcb
--- /dev/null
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectH2Test.java
@@ -0,0 +1,139 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.schema.sql;
+
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.data.PhoneBookDb;
+import com.google.gwtorm.data.TestAddress;
+import com.google.gwtorm.data.TestPerson;
+import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.jdbc.SimpleDataSource;
+
+import junit.framework.TestCase;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.Set;
+
+public class DialectH2Test extends TestCase {
+ private Connection db;
+ private SqlDialect dialect;
+ private Database<PhoneBookDb> phoneBook;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ org.h2.Driver.load();
+ db = DriverManager.getConnection("jdbc:h2:mem:DialectH2Test");
+ dialect = new DialectH2().refine(db);
+
+ final Properties p = new Properties();
+ p.setProperty("driver", org.h2.Driver.class.getName());
+ p.setProperty("url", db.getMetaData().getURL());
+ phoneBook =
+ new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
+ }
+
+ @Override
+ protected void tearDown() {
+ if (db != null) {
+ try {
+ db.close();
+ } catch (SQLException e) {
+ throw new RuntimeException("Cannot close database", e);
+ }
+ }
+ db = null;
+ }
+
+ private void execute(final String sql) throws SQLException {
+ final Statement stmt = db.createStatement();
+ try {
+ stmt.execute(sql);
+ } finally {
+ stmt.close();
+ }
+ }
+
+ public void testListSequences() throws SQLException {
+ assertTrue(dialect.listSequences(db).isEmpty());
+
+ execute("CREATE SEQUENCE cnt");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ Set<String> s = dialect.listSequences(db);
+ assertEquals(1, s.size());
+ assertTrue(s.contains("cnt"));
+ assertFalse(s.contains("foo"));
+ }
+
+ public void testListTables() throws SQLException {
+ assertTrue(dialect.listTables(db).isEmpty());
+
+ execute("CREATE SEQUENCE cnt");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ Set<String> s = dialect.listTables(db);
+ assertEquals(1, s.size());
+ assertFalse(s.contains("cnt"));
+ assertTrue(s.contains("foo"));
+ }
+
+ public void testUpgradeSchema() throws SQLException, OrmException {
+ final PhoneBookDb p = phoneBook.open();
+ try {
+ p.updateSchema();
+
+ execute("CREATE SEQUENCE cnt");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ execute("ALTER TABLE people ADD COLUMN fake_name VARCHAR(20)");
+ execute("ALTER TABLE people DROP COLUMN registered");
+ execute("DROP TABLE addresses");
+ execute("DROP SEQUENCE address_id");
+
+ Set<String> sequences, tables;
+
+ p.updateSchema();
+ sequences = dialect.listSequences(db);
+ tables = dialect.listTables(db);
+ assertTrue(sequences.contains("cnt"));
+ assertTrue(tables.contains("foo"));
+
+ assertTrue(sequences.contains("address_id"));
+ assertTrue(tables.contains("addresses"));
+
+ p.pruneSchema();
+ sequences = dialect.listSequences(db);
+ tables = dialect.listTables(db);
+ assertFalse(sequences.contains("cnt"));
+ assertFalse(tables.contains("foo"));
+
+ final TestPerson.Key pk = new TestPerson.Key("Bob");
+ final TestPerson bob = new TestPerson(pk, p.nextAddressId());
+ p.people().insert(Collections.singleton(bob));
+
+ final TestAddress addr =
+ new TestAddress(new TestAddress.Key(pk, "home"), "some place");
+ p.addresses().insert(Collections.singleton(addr));
+ } finally {
+ p.close();
+ }
+ }
+}
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java b/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java
new file mode 100644
index 0000000..22a8bd3
--- /dev/null
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectMySQLTest.java
@@ -0,0 +1,162 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+package com.google.gwtorm.schema.sql;
+
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.data.PhoneBookDb;
+import com.google.gwtorm.data.TestAddress;
+import com.google.gwtorm.data.TestPerson;
+import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.jdbc.SimpleDataSource;
+
+import junit.framework.TestCase;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.Set;
+
+public class DialectMySQLTest extends TestCase {
+ private Connection db;
+ private SqlDialect dialect;
+ private Database<PhoneBookDb> phoneBook;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ Class.forName(com.mysql.jdbc.Driver.class.getName());
+
+ final String host = "localhost";
+ final String database = "gwtorm";
+ final String user = "gwtorm";
+ final String pass = "gwtorm";
+
+ final String url = "jdbc:mysql://" + host + "/" + database;
+ db = DriverManager.getConnection(url, user, pass);
+ dialect = new DialectMySQL().refine(db);
+
+ final Properties p = new Properties();
+ p.setProperty("driver", com.mysql.jdbc.Driver.class.getName());
+ p.setProperty("url", db.getMetaData().getURL());
+ p.setProperty("user", user);
+ p.setProperty("password", pass);
+ phoneBook =
+ new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
+
+ drop("TABLE address_id");
+ drop("TABLE addresses");
+ drop("TABLE cnt");
+ drop("TABLE foo");
+ drop("TABLE people");
+ }
+
+ private void drop(String drop) {
+ try {
+ execute("DROP " + drop);
+ } catch (SQLException e) {
+ }
+ }
+
+ @Override
+ protected void tearDown() {
+ if (db != null) {
+ try {
+ db.close();
+ } catch (SQLException e) {
+ throw new RuntimeException("Cannot close database", e);
+ }
+ }
+ db = null;
+ }
+
+ private void execute(final String sql) throws SQLException {
+ final Statement stmt = db.createStatement();
+ try {
+ stmt.execute(sql);
+ } finally {
+ stmt.close();
+ }
+ }
+
+ public void testListSequences() throws SQLException {
+ assertTrue(dialect.listSequences(db).isEmpty());
+
+ execute("CREATE TABLE cnt (s SERIAL)");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ Set<String> s = dialect.listSequences(db);
+ assertEquals(1, s.size());
+ assertTrue(s.contains("cnt"));
+ assertFalse(s.contains("foo"));
+ }
+
+ public void testListTables() throws SQLException {
+ assertTrue(dialect.listTables(db).isEmpty());
+
+ execute("CREATE TABLE cnt (s SERIAL)");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ Set<String> s = dialect.listTables(db);
+ assertEquals(1, s.size());
+ assertFalse(s.contains("cnt"));
+ assertTrue(s.contains("foo"));
+ }
+
+ public void testUpgradeSchema() throws SQLException, OrmException {
+ final PhoneBookDb p = phoneBook.open();
+ try {
+ p.updateSchema();
+
+ execute("CREATE TABLE cnt (s SERIAL)");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ execute("ALTER TABLE people ADD COLUMN fake_name VARCHAR(20)");
+ execute("ALTER TABLE people DROP COLUMN registered");
+ execute("DROP TABLE addresses");
+ execute("DROP TABLE address_id");
+
+ Set<String> sequences, tables;
+
+ p.updateSchema();
+ sequences = dialect.listSequences(db);
+ tables = dialect.listTables(db);
+ assertTrue(sequences.contains("cnt"));
+ assertTrue(tables.contains("foo"));
+
+ assertTrue(sequences.contains("address_id"));
+ assertTrue(tables.contains("addresses"));
+
+ p.pruneSchema();
+ sequences = dialect.listSequences(db);
+ tables = dialect.listTables(db);
+ assertFalse(sequences.contains("cnt"));
+ assertFalse(tables.contains("foo"));
+
+ final TestPerson.Key pk = new TestPerson.Key("Bob");
+ final TestPerson bob = new TestPerson(pk, p.nextAddressId());
+ p.people().insert(Collections.singleton(bob));
+
+ final TestAddress addr =
+ new TestAddress(new TestAddress.Key(pk, "home"), "some place");
+ p.addresses().insert(Collections.singleton(addr));
+ } finally {
+ p.close();
+ }
+ }
+}
diff --git a/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java b/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java
new file mode 100644
index 0000000..fe6440a
--- /dev/null
+++ b/src/test/java/com/google/gwtorm/schema/sql/DialectPostgreSQLTest.java
@@ -0,0 +1,161 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+package com.google.gwtorm.schema.sql;
+
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.data.PhoneBookDb;
+import com.google.gwtorm.data.TestAddress;
+import com.google.gwtorm.data.TestPerson;
+import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.jdbc.SimpleDataSource;
+
+import junit.framework.TestCase;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.Set;
+
+public class DialectPostgreSQLTest extends TestCase {
+ private Connection db;
+ private SqlDialect dialect;
+ private Database<PhoneBookDb> phoneBook;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ Class.forName(org.postgresql.Driver.class.getName());
+
+ final String database = "gwtorm";
+ final String user = "gwtorm";
+ final String pass = "gwtorm";
+
+ db = DriverManager.getConnection("jdbc:postgresql:" + database, user, pass);
+ dialect = new DialectPostgreSQL().refine(db);
+
+ final Properties p = new Properties();
+ p.setProperty("driver", org.postgresql.Driver.class.getName());
+ p.setProperty("url", db.getMetaData().getURL());
+ p.setProperty("user", user);
+ p.setProperty("password", pass);
+ phoneBook =
+ new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
+
+ drop("SEQUENCE address_id");
+ drop("SEQUENCE cnt");
+
+ drop("TABLE addresses");
+ drop("TABLE foo");
+ drop("TABLE people");
+ }
+
+ private void drop(String drop) {
+ try {
+ execute("DROP " + drop);
+ } catch (SQLException e) {
+ }
+ }
+
+ @Override
+ protected void tearDown() {
+ if (db != null) {
+ try {
+ db.close();
+ } catch (SQLException e) {
+ throw new RuntimeException("Cannot close database", e);
+ }
+ }
+ db = null;
+ }
+
+ private void execute(final String sql) throws SQLException {
+ final Statement stmt = db.createStatement();
+ try {
+ stmt.execute(sql);
+ } finally {
+ stmt.close();
+ }
+ }
+
+ public void testListSequences() throws SQLException {
+ assertTrue(dialect.listSequences(db).isEmpty());
+
+ execute("CREATE SEQUENCE cnt");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ Set<String> s = dialect.listSequences(db);
+ assertEquals(1, s.size());
+ assertTrue(s.contains("cnt"));
+ assertFalse(s.contains("foo"));
+ }
+
+ public void testListTables() throws SQLException {
+ assertTrue(dialect.listTables(db).isEmpty());
+
+ execute("CREATE SEQUENCE cnt");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ Set<String> s = dialect.listTables(db);
+ assertEquals(1, s.size());
+ assertFalse(s.contains("cnt"));
+ assertTrue(s.contains("foo"));
+ }
+
+ public void testUpgradeSchema() throws SQLException, OrmException {
+ final PhoneBookDb p = phoneBook.open();
+ try {
+ p.updateSchema();
+
+ execute("CREATE SEQUENCE cnt");
+ execute("CREATE TABLE foo (cnt INT)");
+
+ execute("ALTER TABLE people ADD COLUMN fake_name VARCHAR(20)");
+ execute("ALTER TABLE people DROP COLUMN registered");
+ execute("DROP TABLE addresses");
+ execute("DROP SEQUENCE address_id");
+
+ Set<String> sequences, tables;
+
+ p.updateSchema();
+ sequences = dialect.listSequences(db);
+ tables = dialect.listTables(db);
+ assertTrue(sequences.contains("cnt"));
+ assertTrue(tables.contains("foo"));
+
+ assertTrue(sequences.contains("address_id"));
+ assertTrue(tables.contains("addresses"));
+
+ p.pruneSchema();
+ sequences = dialect.listSequences(db);
+ tables = dialect.listTables(db);
+ assertFalse(sequences.contains("cnt"));
+ assertFalse(tables.contains("foo"));
+
+ final TestPerson.Key pk = new TestPerson.Key("Bob");
+ final TestPerson bob = new TestPerson(pk, p.nextAddressId());
+ p.people().insert(Collections.singleton(bob));
+
+ final TestAddress addr =
+ new TestAddress(new TestAddress.Key(pk, "home"), "some place");
+ p.addresses().insert(Collections.singleton(addr));
+ } finally {
+ p.close();
+ }
+ }
+}
diff --git a/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java b/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
index f9afcfe..da242bc 100644
--- a/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
+++ b/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
@@ -71,7 +71,7 @@
protected PhoneBookDb openAndCreate() throws OrmException {
final PhoneBookDb schema = open();
- schema.createSchema();
+ schema.updateSchema();
return schema;
}
@@ -104,7 +104,7 @@
public void testCreateSchema() throws Exception {
final PhoneBookDb schema = open();
- schema.createSchema();
+ schema.updateSchema();
}
public void testNextAddressId() throws Exception {