001/*-
002 * #%L
003 * HAPI FHIR Server - SQL Migration
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.migrate.tasks.api;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
024import ca.uhn.fhir.jpa.migrate.MigrationJdbcUtils;
025import ca.uhn.fhir.jpa.migrate.taskdef.AddColumnTask;
026import ca.uhn.fhir.jpa.migrate.taskdef.AddForeignKeyTask;
027import ca.uhn.fhir.jpa.migrate.taskdef.AddIdGeneratorTask;
028import ca.uhn.fhir.jpa.migrate.taskdef.AddIndexTask;
029import ca.uhn.fhir.jpa.migrate.taskdef.AddPrimaryKeyTask;
030import ca.uhn.fhir.jpa.migrate.taskdef.AddTableByColumnTask;
031import ca.uhn.fhir.jpa.migrate.taskdef.AddTableRawSqlTask;
032import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableTask;
033import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask;
034import ca.uhn.fhir.jpa.migrate.taskdef.ColumnTypeEnum;
035import ca.uhn.fhir.jpa.migrate.taskdef.DropColumnTask;
036import ca.uhn.fhir.jpa.migrate.taskdef.DropForeignKeyTask;
037import ca.uhn.fhir.jpa.migrate.taskdef.DropIdGeneratorTask;
038import ca.uhn.fhir.jpa.migrate.taskdef.DropIndexTask;
039import ca.uhn.fhir.jpa.migrate.taskdef.DropPrimaryKeyTask;
040import ca.uhn.fhir.jpa.migrate.taskdef.DropTableTask;
041import ca.uhn.fhir.jpa.migrate.taskdef.ExecuteRawSqlTask;
042import ca.uhn.fhir.jpa.migrate.taskdef.ExecuteTaskPrecondition;
043import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask;
044import ca.uhn.fhir.jpa.migrate.taskdef.MigrateColumBlobTypeToBinaryTypeTask;
045import ca.uhn.fhir.jpa.migrate.taskdef.MigrateColumnClobTypeToTextTypeTask;
046import ca.uhn.fhir.jpa.migrate.taskdef.MigratePostgresTextClobToBinaryClobTask;
047import ca.uhn.fhir.jpa.migrate.taskdef.ModifyColumnTask;
048import ca.uhn.fhir.jpa.migrate.taskdef.NopTask;
049import ca.uhn.fhir.jpa.migrate.taskdef.RenameColumnTask;
050import ca.uhn.fhir.jpa.migrate.taskdef.RenameIndexTask;
051import ca.uhn.fhir.jpa.migrate.taskdef.RenameTableTask;
052import jakarta.annotation.Nullable;
053import org.apache.commons.lang3.Validate;
054import org.intellij.lang.annotations.Language;
055import org.slf4j.Logger;
056import org.slf4j.LoggerFactory;
057
058import java.util.Arrays;
059import java.util.Collections;
060import java.util.HashMap;
061import java.util.List;
062import java.util.Map;
063import java.util.Optional;
064import java.util.Set;
065import java.util.stream.Collectors;
066
067public class Builder {
068        private static final Logger ourLog = LoggerFactory.getLogger(Builder.class);
069
070        private final String myRelease;
071        private final BaseMigrationTasks.IAcceptsTasks mySink;
072
073        public Builder(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink) {
074                myRelease = theRelease;
075                mySink = theSink;
076        }
077
078        public BuilderWithTableName onTable(String theTableName) {
079                return new BuilderWithTableName(myRelease, mySink, theTableName);
080        }
081
082        public void addTask(BaseTask theTask) {
083                mySink.addTask(theTask);
084        }
085
086        public BuilderAddTableRawSql addTableRawSql(String theVersion, String theTableName) {
087                return new BuilderAddTableRawSql(theVersion, theTableName);
088        }
089
090        public BuilderCompleteTask executeRawSql(String theVersion, @Language("SQL") String theSql) {
091                ExecuteRawSqlTask task = executeRawSqlOptional(theVersion, theSql);
092                return new BuilderCompleteTask(task);
093        }
094
095        public void executeRawSqlStub(String theVersion, @Language("SQL") String theSql) {
096                BuilderCompleteTask task = executeRawSql(theVersion, theSql);
097                task.withFlag(TaskFlagEnum.DO_NOTHING);
098        }
099
100        private ExecuteRawSqlTask executeRawSqlOptional(String theVersion, @Language("SQL") String theSql) {
101                ExecuteRawSqlTask task = new ExecuteRawSqlTask(myRelease, theVersion).addSql(theSql);
102                mySink.addTask(task);
103                return task;
104        }
105
106        public InitializeSchemaTask initializeSchema(
107                        String theVersion, ISchemaInitializationProvider theSchemaInitializationProvider) {
108                InitializeSchemaTask task = new InitializeSchemaTask(myRelease, theVersion, theSchemaInitializationProvider);
109                mySink.addTask(task);
110                return task;
111        }
112
113        @SuppressWarnings("unused")
114        public InitializeSchemaTask initializeSchema(
115                        String theVersion, String theSchemaName, ISchemaInitializationProvider theSchemaInitializationProvider) {
116                InitializeSchemaTask task = new InitializeSchemaTask(myRelease, theVersion, theSchemaInitializationProvider);
117                task.setDescription("Initialize " + theSchemaName + " schema");
118                mySink.addTask(task);
119                return task;
120        }
121
122        public Builder executeRawSql(String theVersion, DriverTypeEnum theDriver, @Language("SQL") String theSql) {
123                mySink.addTask(new ExecuteRawSqlTask(myRelease, theVersion).addSql(theDriver, theSql));
124                return this;
125        }
126
127        /**
128         * Builder method to define a raw SQL execution migration that needs to take place against multiple database types,
129         * and the SQL they need to use is not equal. Provide a map of driver types to SQL statements.
130         *
131         * @param theVersion The version of the migration.
132         * @param theDriverToSql Map of driver types to SQL statements.
133         */
134        public Builder executeRawSql(String theVersion, Map<DriverTypeEnum, String> theDriverToSql) {
135                Map<DriverTypeEnum, List<String>> singleSqlStatementMap = new HashMap<>();
136                theDriverToSql.entrySet().stream().forEach(entry -> {
137                        singleSqlStatementMap.put(entry.getKey(), Collections.singletonList(entry.getValue()));
138                });
139                return executeRawSqls(theVersion, singleSqlStatementMap);
140        }
141
142        /**
143         * Builder method to define a raw SQL execution migration that needs to take place against multiple database types,
144         * and the SQL they need to use is not equal, and there are multiple sql commands for a given database.
145         * Provide a map of driver types to list of SQL statements.
146         *
147         * @param theVersion The version of the migration.
148         * @param theDriverToSqls Map of driver types to list of SQL statements.
149         */
150        public Builder executeRawSqls(String theVersion, Map<DriverTypeEnum, List<String>> theDriverToSqls) {
151                ExecuteRawSqlTask executeRawSqlTask = new ExecuteRawSqlTask(myRelease, theVersion);
152                theDriverToSqls.entrySet().stream().forEach(entry -> {
153                        entry.getValue().forEach(sql -> executeRawSqlTask.addSql(entry.getKey(), sql));
154                });
155                mySink.addTask(executeRawSqlTask);
156                return this;
157        }
158
159        // Flyway doesn't support these kinds of migrations
160        @Deprecated
161        public Builder startSectionWithMessage(String theMessage) {
162                // Do nothing
163                return this;
164        }
165
166        public BuilderAddTableByColumns addTableByColumns(
167                        String theVersion, String theTableName, String... thePkColumnNames) {
168                return new BuilderAddTableByColumns(
169                                myRelease, theVersion, mySink, theTableName, Arrays.asList(thePkColumnNames));
170        }
171
172        public void addIdGenerator(String theVersion, String theGeneratorName) {
173                AddIdGeneratorTask task = new AddIdGeneratorTask(myRelease, theVersion, theGeneratorName);
174                addTask(task);
175        }
176
177        public BuilderCompleteTask dropIdGenerator(String theVersion, String theIdGeneratorName) {
178                DropIdGeneratorTask task = new DropIdGeneratorTask(myRelease, theVersion, theIdGeneratorName);
179                addTask(task);
180                return new BuilderCompleteTask(task);
181        }
182
183        public void addNop(String theVersion) {
184                addTask(new NopTask(myRelease, theVersion));
185        }
186
187        public static class BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks {
188                private final String myRelease;
189                private final BaseMigrationTasks.IAcceptsTasks mySink;
190                private final String myTableName;
191                private BaseTask myLastAddedTask;
192
193                public BuilderWithTableName(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink, String theTableName) {
194                        myRelease = theRelease;
195                        mySink = theSink;
196                        myTableName = theTableName;
197                }
198
199                public String getTableName() {
200                        return myTableName;
201                }
202
203                public BuilderCompleteTask dropIndex(String theVersion, String theIndexName) {
204                        BaseTask task = dropIndexOptional(theVersion, theIndexName);
205                        return new BuilderCompleteTask(task);
206                }
207
208                /**
209                 * Drop index without taking write lock on PG, Oracle, MSSQL.
210                 */
211                public BuilderCompleteTask dropIndexOnline(String theVersion, String theIndexName) {
212                        DropIndexTask task = dropIndexOptional(theVersion, theIndexName);
213                        task.setOnline(true);
214                        return new BuilderCompleteTask(task);
215                }
216
217                public void dropIndexStub(String theVersion, String theIndexName) {
218                        DropIndexTask task = dropIndexOptional(theVersion, theIndexName);
219                        task.addFlag(TaskFlagEnum.DO_NOTHING);
220                }
221
222                private DropIndexTask dropIndexOptional(String theVersion, String theIndexName) {
223                        DropIndexTask task = new DropIndexTask(myRelease, theVersion);
224                        task.setIndexName(theIndexName);
225                        task.setTableName(myTableName);
226                        addTask(task);
227                        return task;
228                }
229
230                /**
231                 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong
232                 */
233                @Deprecated
234                public void renameIndex(String theVersion, String theOldIndexName, String theNewIndexName) {
235                        renameIndexOptional(theVersion, theOldIndexName, theNewIndexName);
236                }
237
238                /**
239                 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong
240                 */
241                public void renameIndexStub(String theVersion, String theOldIndexName, String theNewIndexName) {
242                        RenameIndexTask task = renameIndexOptional(theVersion, theOldIndexName, theNewIndexName);
243                        task.addFlag(TaskFlagEnum.DO_NOTHING);
244                }
245
246                private RenameIndexTask renameIndexOptional(String theVersion, String theOldIndexName, String theNewIndexName) {
247                        RenameIndexTask task = new RenameIndexTask(myRelease, theVersion);
248                        task.setOldIndexName(theOldIndexName);
249                        task.setNewIndexName(theNewIndexName);
250                        task.setTableName(myTableName);
251                        addTask(task);
252                        return task;
253                }
254
255                public void dropThisTable(String theVersion) {
256                        DropTableTask task = new DropTableTask(myRelease, theVersion);
257                        task.setTableName(myTableName);
258                        addTask(task);
259                }
260
261                public BuilderWithTableName.BuilderAddIndexWithName addIndex(String theVersion, String theIndexName) {
262                        return new BuilderWithTableName.BuilderAddIndexWithName(theVersion, theIndexName);
263                }
264
265                public BuilderWithTableName.BuilderAddColumnWithName addColumn(String theVersion, String theColumnName) {
266                        return new BuilderWithTableName.BuilderAddColumnWithName(myRelease, theVersion, theColumnName, null, this);
267                }
268
269                public BuilderWithTableName.BuilderAddColumnWithName addColumn(
270                                String theVersion, String theColumnName, Object theDefaultValue) {
271                        return new BuilderWithTableName.BuilderAddColumnWithName(
272                                        myRelease, theVersion, theColumnName, theDefaultValue, this);
273                }
274
275                public BuilderCompleteTask dropColumn(String theVersion, String theColumnName) {
276                        Validate.notBlank(theColumnName);
277                        DropColumnTask task = new DropColumnTask(myRelease, theVersion);
278                        task.setTableName(myTableName);
279                        task.setColumnName(theColumnName);
280                        addTask(task);
281                        return new BuilderCompleteTask(task);
282                }
283
284                @Override
285                public void addTask(BaseTask theTask) {
286                        ((BaseTableTask) theTask).setTableName(myTableName);
287                        myLastAddedTask = theTask;
288                        mySink.addTask(theTask);
289                }
290
291                public BuilderWithTableName.BuilderModifyColumnWithName modifyColumn(String theVersion, String theColumnName) {
292                        return new BuilderWithTableName.BuilderModifyColumnWithName(theVersion, theColumnName);
293                }
294
295                public BuilderWithTableName.BuilderAddForeignKey addForeignKey(String theVersion, String theForeignKeyName) {
296                        return new BuilderWithTableName.BuilderAddForeignKey(theVersion, theForeignKeyName);
297                }
298
299                public BuilderWithTableName renameColumn(String theVersion, String theOldName, String theNewName) {
300                        return renameColumn(theVersion, theOldName, theNewName, false, false);
301                }
302
303                /**
304                 * @param theOldName                            The old column name
305                 * @param theNewName                            The new column name
306                 * @param isOkayIfNeitherColumnExists           Setting this to true means that it's not an error if neither column exists
307                 * @param theDeleteTargetColumnFirstIfBothExist Setting this to true causes the migrator to be ok with the target column existing. It will make sure that there is no data in the column with the new name, then delete it if so in order to make room for the renamed column. If there is data it will still bomb out.
308                 */
309                public BuilderWithTableName renameColumn(
310                                String theVersion,
311                                String theOldName,
312                                String theNewName,
313                                boolean isOkayIfNeitherColumnExists,
314                                boolean theDeleteTargetColumnFirstIfBothExist) {
315                        RenameColumnTask task = new RenameColumnTask(myRelease, theVersion);
316                        task.setTableName(myTableName);
317                        task.setOldName(theOldName);
318                        task.setNewName(theNewName);
319                        task.setOkayIfNeitherColumnExists(isOkayIfNeitherColumnExists);
320                        task.setDeleteTargetColumnFirstIfBothExist(theDeleteTargetColumnFirstIfBothExist);
321                        addTask(task);
322                        return this;
323                }
324
325                public Optional<BaseTask> getLastAddedTask() {
326                        return Optional.ofNullable(myLastAddedTask);
327                }
328
329                public void addPrimaryKey(String theVersion, String... theColumnsInOrder) {
330                        addTask(new AddPrimaryKeyTask(myRelease, theVersion, myTableName, theColumnsInOrder));
331                }
332
333                /**
334                 * @param theFkName          the name of the foreign key
335                 * @param theParentTableName the name of the table that exports the foreign key
336                 */
337                public void dropForeignKey(String theVersion, String theFkName, String theParentTableName) {
338                        DropForeignKeyTask task = new DropForeignKeyTask(myRelease, theVersion);
339                        task.setConstraintName(theFkName);
340                        task.setTableName(getTableName());
341                        task.setParentTableName(theParentTableName);
342                        addTask(task);
343                }
344
345                public BuilderCompleteTask renameTable(String theVersion, String theNewTableName) {
346                        RenameTableTask task = new RenameTableTask(myRelease, theVersion, getTableName(), theNewTableName);
347                        addTask(task);
348                        return new BuilderCompleteTask(task);
349                }
350
351                public BuilderCompleteTask migratePostgresTextClobToBinaryClob(String theVersion, String theColumnName) {
352                        MigratePostgresTextClobToBinaryClobTask task =
353                                        new MigratePostgresTextClobToBinaryClobTask(myRelease, theVersion);
354                        task.setTableName(getTableName());
355                        task.setColumnName(theColumnName);
356                        addTask(task);
357                        return new BuilderCompleteTask(task);
358                }
359
360                public BuilderCompleteTask migrateBlobToBinary(
361                                String theVersion, String theFromColumName, String theToColumName) {
362                        MigrateColumBlobTypeToBinaryTypeTask task = new MigrateColumBlobTypeToBinaryTypeTask(
363                                        myRelease, theVersion, getTableName(), theFromColumName, theToColumName);
364
365                        addTask(task);
366                        return new BuilderCompleteTask(task);
367                }
368
369                public BuilderCompleteTask migrateClobToText(
370                                String theVersion, String theFromColumName, String theToColumName) {
371                        MigrateColumnClobTypeToTextTypeTask task = new MigrateColumnClobTypeToTextTypeTask(
372                                        myRelease, theVersion, getTableName(), theFromColumName, theToColumName);
373
374                        addTask(task);
375                        return new BuilderCompleteTask(task);
376                }
377
378                public void dropPrimaryKey(String theVersion) {
379                        final DropPrimaryKeyTask task = new DropPrimaryKeyTask(myRelease, theVersion, myTableName);
380                        addTask(task);
381                }
382
383                public class BuilderAddIndexWithName {
384                        private final String myVersion;
385                        private final String myIndexName;
386
387                        public BuilderAddIndexWithName(String theVersion, String theIndexName) {
388                                myVersion = theVersion;
389                                myIndexName = theIndexName;
390                        }
391
392                        public BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique unique(boolean theUnique) {
393                                return new BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique(myVersion, theUnique);
394                        }
395
396                        public class BuilderAddIndexUnique {
397                                private final String myVersion;
398                                private final boolean myUnique;
399                                private String[] myIncludeColumns;
400                                private boolean myOnline;
401
402                                public BuilderAddIndexUnique(String theVersion, boolean theUnique) {
403                                        myVersion = theVersion;
404                                        myUnique = theUnique;
405                                }
406
407                                public void withColumnsStub(String... theColumnNames) {
408                                        BuilderCompleteTask task = withColumns(theColumnNames);
409                                        task.withFlag(TaskFlagEnum.DO_NOTHING);
410                                }
411
412                                public BuilderCompleteTask withColumns(String... theColumnNames) {
413                                        AddIndexTask task = new AddIndexTask(myRelease, myVersion);
414                                        task.setTableName(myTableName);
415                                        task.setIndexName(myIndexName);
416                                        task.setUnique(myUnique);
417                                        task.setColumns(theColumnNames);
418                                        task.setOnline(myOnline);
419                                        if (myIncludeColumns != null) {
420                                                task.setIncludeColumns(myIncludeColumns);
421                                        }
422                                        addTask(task);
423                                        return new BuilderCompleteTask(task);
424                                }
425
426                                /**
427                                 * THis is strictly needed for SQL Server, as it will create filtered indexes on nullable columns, and we have to build a tail clause which matches what the SQL Server Hibernate dialect does.
428                                 */
429                                public BuilderCompleteTask withPossibleNullableColumns(ColumnAndNullable... theColumns) {
430                                        String[] columnNames = Arrays.stream(theColumns)
431                                                        .map(ColumnAndNullable::getColumnName)
432                                                        .toArray(String[]::new);
433                                        String[] nullableColumnNames = Arrays.stream(theColumns)
434                                                        .filter(ColumnAndNullable::isNullable)
435                                                        .map(ColumnAndNullable::getColumnName)
436                                                        .toArray(String[]::new);
437                                        AddIndexTask task = new AddIndexTask(myRelease, myVersion);
438                                        task.setTableName(myTableName);
439                                        task.setIndexName(myIndexName);
440                                        task.setUnique(myUnique);
441                                        task.setColumns(columnNames);
442                                        task.setNullableColumns(nullableColumnNames);
443                                        task.setOnline(myOnline);
444                                        if (myIncludeColumns != null) {
445                                                task.setIncludeColumns(myIncludeColumns);
446                                        }
447                                        addTask(task);
448                                        return new BuilderCompleteTask(task);
449                                }
450
451                                public BuilderAddIndexUnique includeColumns(String... theIncludeColumns) {
452                                        myIncludeColumns = theIncludeColumns;
453                                        return this;
454                                }
455
456                                /**
457                                 * Add the index without locking the table.
458                                 */
459                                public BuilderAddIndexUnique online(boolean theOnlineFlag) {
460                                        myOnline = theOnlineFlag;
461                                        return this;
462                                }
463                        }
464                }
465
466                public class BuilderModifyColumnWithName {
467                        private final String myVersion;
468                        private final String myColumnName;
469
470                        public BuilderModifyColumnWithName(String theVersion, String theColumnName) {
471                                myVersion = theVersion;
472                                myColumnName = theColumnName;
473                        }
474
475                        public String getColumnName() {
476                                return myColumnName;
477                        }
478
479                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nullable() {
480                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(
481                                                myVersion, true);
482                        }
483
484                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable
485                                        nonNullable() {
486                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(
487                                                myVersion, false);
488                        }
489
490                        public class BuilderModifyColumnWithNameAndNullable {
491                                private final String myVersion;
492                                private final boolean myNullable;
493
494                                public BuilderModifyColumnWithNameAndNullable(String theVersion, boolean theNullable) {
495                                        myVersion = theVersion;
496                                        myNullable = theNullable;
497                                }
498
499                                public BuilderCompleteTask withType(ColumnTypeEnum theColumnType) {
500                                        return withType(theColumnType, null);
501                                }
502
503                                public BuilderCompleteTask withType(ColumnTypeEnum theColumnType, Integer theLength) {
504                                        if (theColumnType == ColumnTypeEnum.STRING) {
505                                                if (theLength == null || theLength == 0) {
506                                                        throw new IllegalArgumentException(
507                                                                        Msg.code(52) + "Can not specify length 0 for column of type " + theColumnType);
508                                                }
509                                        } else {
510                                                if (theLength != null) {
511                                                        throw new IllegalArgumentException(
512                                                                        Msg.code(53) + "Can not specify length for column of type " + theColumnType);
513                                                }
514                                        }
515
516                                        ModifyColumnTask task = new ModifyColumnTask(myRelease, myVersion);
517
518                                        task.setColumnName(myColumnName);
519                                        task.setTableName(myTableName);
520                                        if (theLength != null) {
521                                                task.setColumnLength(theLength);
522                                        }
523                                        task.setNullable(myNullable);
524                                        task.setColumnType(theColumnType);
525                                        addTask(task);
526                                        return new BuilderCompleteTask(task);
527                                }
528                        }
529                }
530
531                public class BuilderAddForeignKey {
532                        private final String myVersion;
533                        private final String myForeignKeyName;
534
535                        public BuilderAddForeignKey(String theVersion, String theForeignKeyName) {
536                                myVersion = theVersion;
537                                myForeignKeyName = theForeignKeyName;
538                        }
539
540                        public BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn toColumn(
541                                        String theColumnName) {
542                                return new BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn(
543                                                myVersion, theColumnName);
544                        }
545
546                        public class BuilderAddForeignKeyToColumn extends BuilderWithTableName.BuilderModifyColumnWithName {
547                                public BuilderAddForeignKeyToColumn(String theVersion, String theColumnName) {
548                                        super(theVersion, theColumnName);
549                                }
550
551                                public BuilderCompleteTask references(String theForeignTable, String theForeignColumn) {
552                                        AddForeignKeyTask task = new AddForeignKeyTask(myRelease, myVersion);
553                                        task.setTableName(myTableName);
554                                        task.setConstraintName(myForeignKeyName);
555                                        task.setColumnName(getColumnName());
556                                        task.setForeignTableName(theForeignTable);
557                                        task.setForeignColumnName(theForeignColumn);
558                                        addTask(task);
559                                        return new BuilderCompleteTask(task);
560                                }
561                        }
562                }
563
564                public static class BuilderAddColumnWithName {
565                        private final String myRelease;
566                        private final String myVersion;
567                        private final String myColumnName;
568
569                        @Nullable
570                        private final Object myDefaultValue;
571
572                        private final BaseMigrationTasks.IAcceptsTasks myTaskSink;
573
574                        public BuilderAddColumnWithName(
575                                        String theRelease,
576                                        String theVersion,
577                                        String theColumnName,
578                                        @Nullable Object theDefaultValue,
579                                        BaseMigrationTasks.IAcceptsTasks theTaskSink) {
580                                myRelease = theRelease;
581                                myVersion = theVersion;
582                                myColumnName = theColumnName;
583                                myDefaultValue = theDefaultValue;
584                                myTaskSink = theTaskSink;
585                        }
586
587                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nullable() {
588                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(
589                                                myRelease, myVersion, true);
590                        }
591
592                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nonNullable() {
593                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(
594                                                myRelease, myVersion, false);
595                        }
596
597                        public class BuilderAddColumnWithNameNullable {
598                                private final boolean myNullable;
599                                private final String myRelease;
600                                private final String myVersion;
601
602                                public BuilderAddColumnWithNameNullable(String theRelease, String theVersion, boolean theNullable) {
603                                        myRelease = theRelease;
604                                        myVersion = theVersion;
605                                        myNullable = theNullable;
606                                }
607
608                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType) {
609                                        return type(theColumnType, null);
610                                }
611
612                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType, Integer theLength) {
613                                        AddColumnTask task = new AddColumnTask(myRelease, myVersion);
614                                        task.setColumnName(myColumnName);
615                                        task.setNullable(myNullable);
616                                        task.setColumnType(theColumnType);
617                                        if (theLength != null) {
618                                                task.setColumnLength(theLength);
619                                        }
620                                        task.setDefaultValue(myDefaultValue);
621                                        myTaskSink.addTask(task);
622
623                                        return new BuilderCompleteTask(task);
624                                }
625                        }
626                }
627        }
628
629        public static class BuilderCompleteTask {
630
631                private final BaseTask myTask;
632
633                public BuilderCompleteTask(BaseTask theTask) {
634                        myTask = theTask;
635                }
636
637                public BuilderCompleteTask failureAllowed() {
638                        myTask.addFlag(TaskFlagEnum.FAILURE_ALLOWED);
639                        return this;
640                }
641
642                public BuilderCompleteTask doNothing() {
643                        myTask.addFlag(TaskFlagEnum.DO_NOTHING);
644                        return this;
645                }
646
647                public BuilderCompleteTask onlyAppliesToPlatforms(DriverTypeEnum... theTypes) {
648                        Set<DriverTypeEnum> typesSet = Arrays.stream(theTypes).collect(Collectors.toSet());
649                        myTask.setOnlyAppliesToPlatforms(typesSet);
650                        return this;
651                }
652
653                /**
654                 * Introduce precondition checking logic into the execution of the enclosed task.  This conditional logic will
655                 * be implemented by running an SQL SELECT (including CTEs) to obtain a boolean indicating whether a certain
656                 * condition has been met.
657                 * One example is to check for a specific collation on a column to decide whether to create a new index.
658                 * <p/>
659                 * This method may be called multiple times to add multiple preconditions.  The precondition that evaluates to
660                 * false will stop execution of the task irrespective of any or all other tasks evaluating to true.
661                 *
662                 * @param theSql The SELECT or CTE used to determine if the precondition is valid.
663                 * @param reason A String to indicate the text that is logged if the precondition is not met.
664                 * @return The BuilderCompleteTask in order to chain further method calls on this builder.
665                 */
666                public BuilderCompleteTask onlyIf(@Language("SQL") String theSql, String reason) {
667                        if (!theSql.toUpperCase().startsWith("WITH")
668                                        && !theSql.toUpperCase().startsWith("SELECT")) {
669                                throw new IllegalArgumentException(Msg.code(2455)
670                                                + String.format(
671                                                                "Only SELECT statements (including CTEs) are allowed here.  Please check your SQL: [%s]",
672                                                                theSql));
673                        }
674                        ourLog.debug("SQL to evaluate: {}", theSql);
675
676                        myTask.addPrecondition(new ExecuteTaskPrecondition(
677                                        () -> {
678                                                ourLog.debug("Checking precondition for SQL: {}", theSql);
679                                                return MigrationJdbcUtils.queryForSingleBooleanResultMultipleThrowsException(
680                                                                theSql, myTask.newJdbcTemplate());
681                                        },
682                                        reason));
683
684                        return this;
685                }
686
687                public BuilderCompleteTask runEvenDuringSchemaInitialization() {
688                        myTask.addFlag(TaskFlagEnum.RUN_DURING_SCHEMA_INITIALIZATION);
689                        return this;
690                }
691
692                public BuilderCompleteTask setTransactional(boolean theFlag) {
693                        myTask.setTransactional(theFlag);
694                        return this;
695                }
696
697                public BuilderCompleteTask heavyweightSkipByDefault() {
698                        myTask.addFlag(TaskFlagEnum.HEAVYWEIGHT_SKIP_BY_DEFAULT);
699                        return this;
700                }
701
702                public BuilderCompleteTask withFlag(TaskFlagEnum theFlag) {
703                        myTask.addFlag(theFlag);
704                        return this;
705                }
706        }
707
708        public class BuilderAddTableRawSql {
709
710                private final AddTableRawSqlTask myTask;
711
712                protected BuilderAddTableRawSql(String theVersion, String theTableName) {
713                        myTask = new AddTableRawSqlTask(myRelease, theVersion);
714                        myTask.setTableName(theTableName);
715                        addTask(myTask);
716                }
717
718                public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) {
719                        myTask.addSql(theDriverTypeEnum, theSql);
720                        return this;
721                }
722
723                public void addSql(@Language("SQL") String theSql) {
724                        myTask.addSql(theSql);
725                }
726        }
727
728        public class BuilderAddTableByColumns extends BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks {
729                private final String myVersion;
730                private final AddTableByColumnTask myTask;
731
732                public BuilderAddTableByColumns(
733                                String theRelease,
734                                String theVersion,
735                                BaseMigrationTasks.IAcceptsTasks theSink,
736                                String theTableName,
737                                List<String> thePkColumnNames) {
738                        super(theRelease, theSink, theTableName);
739                        myVersion = theVersion;
740                        myTask = new AddTableByColumnTask(myRelease, theVersion);
741                        myTask.setTableName(theTableName);
742                        myTask.setPkColumns(thePkColumnNames);
743                        theSink.addTask(myTask);
744                }
745
746                public BuilderAddColumnWithName addColumn(String theColumnName) {
747                        return new BuilderAddColumnWithName(myRelease, myVersion, theColumnName, null, this);
748                }
749
750                @Override
751                public void addTask(BaseTask theTask) {
752                        if (theTask instanceof AddColumnTask) {
753                                myTask.addAddColumnTask((AddColumnTask) theTask);
754                        } else {
755                                super.addTask(theTask);
756                        }
757                }
758
759                public BuilderCompleteTask withFlags() {
760                        return new BuilderCompleteTask(myTask);
761                }
762        }
763
764        public String getRelease() {
765                return myRelease;
766        }
767}