View Javadoc

1   // Copyright 2011 Leo Przybylski. All rights reserved.
2   //
3   // Redistribution and use in source and binary forms, with or without modification, are
4   // permitted provided that the following conditions are met:
5   //
6   //    1. Redistributions of source code must retain the above copyright notice, this list of
7   //       conditions and the following disclaimer.
8   //
9   //    2. Redistributions in binary form must reproduce the above copyright notice, this list
10  //       of conditions and the following disclaimer in the documentation and/or other materials
11  //       provided with the distribution.
12  //
13  // THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR IMPLIED
14  // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
15  // FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
16  // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17  // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19  // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
20  // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
21  // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22  //
23  // The views and conclusions contained in the software and documentation are those of the
24  // authors and should not be interpreted as representing official policies, either expressed
25  // or implied, of Leo Przybylski.
26  package liquibase.sqlgenerator.ext;
27  
28  import liquibase.database.Database;
29  import liquibase.database.core.*;
30  import liquibase.exception.ValidationErrors;
31  import liquibase.logging.LogFactory;
32  import liquibase.sql.Sql;
33  import liquibase.sql.UnparsedSql;
34  import liquibase.sqlgenerator.SqlGeneratorChain;
35  import liquibase.statement.AutoIncrementConstraint;
36  import liquibase.statement.ForeignKeyConstraint;
37  import liquibase.statement.UniqueConstraint;
38  import liquibase.statement.core.CreateTableStatement;
39  import liquibase.util.StringUtils;
40  import java.util.Iterator;
41  import java.math.BigDecimal;
42  
43  import static liquibase.ext.Constants.EXTENSION_PRIORITY;
44  
45  /**
46   *
47   * @author Leo Przybylski (leo [at] rsmart.com)
48   */
49  public class CreateTableGenerator extends liquibase.sqlgenerator.core.CreateTableGenerator {
50  
51      @Override
52      public int getPriority() {
53          return EXTENSION_PRIORITY;
54      }
55      
56      @Override
57      public ValidationErrors validate(CreateTableStatement createTableStatement, Database database, SqlGeneratorChain sqlGeneratorChain) {
58          ValidationErrors validationErrors = new ValidationErrors();
59          validationErrors.checkRequiredField("tableName", createTableStatement.getTableName());
60          validationErrors.checkRequiredField("columns", createTableStatement.getColumns());
61          return validationErrors;
62      }
63  
64      @Override
65      public Sql[] generateSql(CreateTableStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain) {
66              StringBuffer buffer = new StringBuffer();
67          buffer.append("CREATE TABLE ").append(database.escapeTableName(null, statement.getTableName())).append(" ");
68          buffer.append("(");
69          
70          boolean isSinglePrimaryKeyColumn = statement.getPrimaryKeyConstraint() != null
71              && statement.getPrimaryKeyConstraint().getColumns().size() == 1;
72          
73          boolean isPrimaryKeyAutoIncrement = false;
74          
75          Iterator<String> columnIterator = statement.getColumns().iterator();
76          while (columnIterator.hasNext()) {
77              String column = columnIterator.next();
78              
79              buffer.append(database.escapeColumnName(null, statement.getTableName(), column));
80              buffer.append(" ").append(statement.getColumnTypes().get(column));
81              
82              AutoIncrementConstraint autoIncrementConstraint = null;
83              
84              for (AutoIncrementConstraint currentAutoIncrementConstraint : statement.getAutoIncrementConstraints()) {
85              	if (column.equals(currentAutoIncrementConstraint.getColumnName())) {
86              		autoIncrementConstraint = currentAutoIncrementConstraint;
87              		break;
88              	}
89              }
90  
91              boolean isAutoIncrementColumn = autoIncrementConstraint != null;            
92              boolean isPrimaryKeyColumn = statement.getPrimaryKeyConstraint() != null
93              		&& statement.getPrimaryKeyConstraint().getColumns().contains(column);
94              isPrimaryKeyAutoIncrement = isPrimaryKeyAutoIncrement
95              		|| isPrimaryKeyColumn && isAutoIncrementColumn;
96              
97              if ((database instanceof SQLiteDatabase) &&
98  					isSinglePrimaryKeyColumn &&
99  					isPrimaryKeyColumn &&
100 					isAutoIncrementColumn) {
101             	String pkName = StringUtils.trimToNull(statement.getPrimaryKeyConstraint().getConstraintName());
102 	            if (pkName == null) {
103 	                pkName = database.generatePrimaryKeyName(statement.getTableName());
104 	            }
105                 if (pkName != null) {
106                     buffer.append(" CONSTRAINT ");
107                     buffer.append(database.escapeConstraintName(pkName));
108                 }
109                 buffer.append(" PRIMARY KEY AUTOINCREMENT");
110 			}
111 
112             // No default for mysql date
113             if ((database instanceof MySQLDatabase) 
114                 && statement.getColumnTypes().get(column).toString().startsWith("DATE")) {
115                 statement.getDefaultValues().put(column, null);
116             }
117 
118             if (statement.getDefaultValue(column) != null) {
119                 Object defaultValue = statement.getDefaultValue(column);
120                 
121                 if ((statement.getColumnTypes().get(column).toString().startsWith("DECIMAL")
122                      || statement.getColumnTypes().get(column).toString().startsWith("NUMERIC"))
123                     && !"null".equalsIgnoreCase(defaultValue.toString())) {
124                     int[] bounds = parseBounds(statement.getColumnTypes().get(column).toString());
125                     BigDecimal parsedValue = new BigDecimal(defaultValue.toString());
126                     
127                     StringBuilder max = new StringBuilder();
128                     for (int i = 0; i < bounds[0] - bounds[1]; i++) max.append("9");
129                     
130                     if (bounds[1] > 0) {
131                         max.append(".");
132                     }
133                     for (int i = 0; i < bounds[1]; i++) max.append("9");
134                     
135                     if (parsedValue.compareTo(new BigDecimal(max.toString())) > 0) {
136                         defaultValue = max;
137                     }
138                 }
139                 if (database instanceof MSSQLDatabase) {
140                     buffer.append(" CONSTRAINT ").append(((MSSQLDatabase) database).generateDefaultConstraintName(statement.getTableName(), column));
141                 }
142                 buffer.append(" DEFAULT ");
143                 buffer.append(statement.getColumnTypes().get(column).convertObjectToString(defaultValue, database));
144             }
145 
146             if (isAutoIncrementColumn) {
147             	// TODO: check if database supports auto increment on non primary key column
148                 if (database.supportsAutoIncrement()) {
149                 	String autoIncrementClause = database.getAutoIncrementClause(autoIncrementConstraint.getStartWith(), autoIncrementConstraint.getIncrementBy());
150                 
151                 	if (!"".equals(autoIncrementClause)) {
152                 		buffer.append(" ").append(autoIncrementClause);
153                 	}
154                 } else {
155                     LogFactory.getLogger().warning(database.getTypeName()+" does not support autoincrement columns as request for "+(database.escapeTableName(null, statement.getTableName())));
156                 }
157             }
158 
159             if (statement.getNotNullColumns().contains(column)) {
160                 buffer.append(" NOT NULL");
161             } else {
162                 if (database instanceof SybaseDatabase || database instanceof SybaseASADatabase) {
163                     buffer.append(" NULL");
164                 }
165             }
166 
167             if (database instanceof InformixDatabase && isSinglePrimaryKeyColumn) {
168             	buffer.append(" PRIMARY KEY");
169             }
170 
171             if (columnIterator.hasNext()) {
172                 buffer.append(", ");
173             }
174         }
175 
176         buffer.append(",");
177 
178         // TODO informixdb
179         if (!( (database instanceof SQLiteDatabase) &&
180 				isSinglePrimaryKeyColumn &&
181 				isPrimaryKeyAutoIncrement) &&
182 
183 				!((database instanceof InformixDatabase) &&
184 				isSinglePrimaryKeyColumn
185 				)) {
186         	// ...skip this code block for sqlite if a single column primary key
187         	// with an autoincrement constraint exists.
188         	// This constraint is added after the column type.
189 
190 	        if (statement.getPrimaryKeyConstraint() != null && statement.getPrimaryKeyConstraint().getColumns().size() > 0) {
191 	        	if (!(database instanceof InformixDatabase)) {
192 		            String pkName = StringUtils.trimToNull(statement.getPrimaryKeyConstraint().getConstraintName());
193 		            if (pkName == null) {
194 		                // TODO ORA-00972: identifier is too long
195 			            // If tableName lenght is more then 28 symbols
196 			            // then generated pkName will be incorrect
197 			            pkName = database.generatePrimaryKeyName(statement.getTableName());
198 		            }
199                     if (pkName != null) {
200                         buffer.append(" CONSTRAINT ");
201                         buffer.append(database.escapeConstraintName(pkName));
202                     }
203                 }
204 	            buffer.append(" PRIMARY KEY (");
205 	            buffer.append(database.escapeColumnNameList(StringUtils.join(statement.getPrimaryKeyConstraint().getColumns(), ", ")));
206 	            buffer.append(")");
207 		        // Setting up table space for PK's index if it exist
208 		        if (database instanceof OracleDatabase &&
209 		            statement.getPrimaryKeyConstraint().getTablespace() != null) {
210 			        buffer.append(" USING INDEX TABLESPACE ");
211 			        buffer.append(statement.getPrimaryKeyConstraint().getTablespace());
212 		        }
213 	            buffer.append(",");
214 	        }
215         }
216 
217         for (ForeignKeyConstraint fkConstraint : statement.getForeignKeyConstraints()) {
218         	if (!(database instanceof InformixDatabase)) {
219         		buffer.append(" CONSTRAINT ");
220                 buffer.append(database.escapeConstraintName(fkConstraint.getForeignKeyName()));
221         	}
222             String referencesString = fkConstraint.getReferences();
223             buffer.append(" FOREIGN KEY (")
224                     .append(database.escapeColumnName(null, statement.getTableName(), fkConstraint.getColumn()))
225                     .append(") REFERENCES ")
226                     .append(referencesString);
227 
228             if (fkConstraint.isDeleteCascade()) {
229                 buffer.append(" ON DELETE CASCADE");
230             }
231 
232             if ((database instanceof InformixDatabase)) {
233             	buffer.append(" CONSTRAINT ");
234             	buffer.append(database.escapeConstraintName(fkConstraint.getForeignKeyName()));
235             }
236 
237             if (fkConstraint.isInitiallyDeferred()) {
238                 buffer.append(" INITIALLY DEFERRED");
239             }
240             if (fkConstraint.isDeferrable()) {
241                 buffer.append(" DEFERRABLE");
242             }
243             buffer.append(",");
244         }
245 
246         for (UniqueConstraint uniqueConstraint : statement.getUniqueConstraints()) {
247             if (uniqueConstraint.getConstraintName() != null && !constraintNameAfterUnique(database)) {
248                 buffer.append(" CONSTRAINT ");
249                 buffer.append(database.escapeConstraintName(uniqueConstraint.getConstraintName()));
250             }
251             buffer.append(" UNIQUE (");
252             buffer.append(database.escapeColumnNameList(StringUtils.join(uniqueConstraint.getColumns(), ", ")));
253             buffer.append(")");
254             if (uniqueConstraint.getConstraintName() != null && constraintNameAfterUnique(database)) {
255                 buffer.append(" CONSTRAINT ");
256                 buffer.append(database.escapeConstraintName(uniqueConstraint.getConstraintName()));
257             }
258             buffer.append(",");
259         }
260 
261 //        if (constraints != null && constraints.getCheck() != null) {
262 //            buffer.append(constraints.getCheck()).append(" ");
263 //        }
264 //    }
265 
266         String sql = buffer.toString().replaceFirst(",\\s*$", "") + ")";
267 
268 //        if (StringUtils.trimToNull(tablespace) != null && database.supportsTablespaces()) {
269 //            if (database instanceof MSSQLDatabase) {
270 //                buffer.append(" ON ").append(tablespace);
271 //            } else if (database instanceof DB2Database) {
272 //                buffer.append(" IN ").append(tablespace);
273 //            } else {
274 //                buffer.append(" TABLESPACE ").append(tablespace);
275 //            }
276 //        }
277 
278         if (statement.getTablespace() != null && database.supportsTablespaces()) {
279             if (database instanceof MSSQLDatabase || database instanceof SybaseASADatabase) {
280                 sql += " ON " + statement.getTablespace();
281             } else if (database instanceof DB2Database || database instanceof InformixDatabase) {
282                 sql += " IN " + statement.getTablespace();
283             } else {
284                 sql += " TABLESPACE " + statement.getTablespace();
285             }
286         }
287 
288         if (database instanceof MySQLDatabase) {
289             sql += " ENGINE = InnoDB ";
290         }
291 
292         return new Sql[] {
293                 new UnparsedSql(sql)
294         };
295     }
296 
297     private boolean constraintNameAfterUnique(Database database) {
298 		return database instanceof InformixDatabase;
299 	}
300 
301     
302     protected int[] parseBounds(final String decimal) {
303         final int[] retval = new int[2];
304         int comma = decimal.indexOf(",");
305 
306         try {
307             if (comma < 0) {
308                 retval[0] = Integer.parseInt(decimal.substring(decimal.indexOf("(") + 1, decimal.lastIndexOf(")")));
309                 retval[1] = 0;
310             }
311             else {
312                 retval[0] = Integer.parseInt(decimal.substring(decimal.indexOf("(") + 1, comma));
313                 retval[1] = Integer.parseInt(decimal.substring(comma + 1, decimal.lastIndexOf(")")));
314             }
315         }
316         catch (StringIndexOutOfBoundsException e) {
317             System.out.println("parsebounds " + decimal);
318             throw e;
319         }
320         return retval;
321     }
322 }