001/*-
002 * #%L
003 * HAPI FHIR Server - SQL Migration
004 * %%
005 * Copyright (C) 2014 - 2025 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 BuilderCompleteTask 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                        return new BuilderCompleteTask(task);
344                }
345
346                public BuilderCompleteTask renameTable(String theVersion, String theNewTableName) {
347                        RenameTableTask task = new RenameTableTask(myRelease, theVersion, getTableName(), theNewTableName);
348                        addTask(task);
349                        return new BuilderCompleteTask(task);
350                }
351
352                public BuilderCompleteTask migratePostgresTextClobToBinaryClob(String theVersion, String theColumnName) {
353                        MigratePostgresTextClobToBinaryClobTask task =
354                                        new MigratePostgresTextClobToBinaryClobTask(myRelease, theVersion);
355                        task.setTableName(getTableName());
356                        task.setColumnName(theColumnName);
357                        addTask(task);
358                        return new BuilderCompleteTask(task);
359                }
360
361                public BuilderCompleteTask migrateBlobToBinary(
362                                String theVersion, String theFromColumName, String theToColumName) {
363                        MigrateColumBlobTypeToBinaryTypeTask task = new MigrateColumBlobTypeToBinaryTypeTask(
364                                        myRelease, theVersion, getTableName(), theFromColumName, theToColumName);
365
366                        addTask(task);
367                        return new BuilderCompleteTask(task);
368                }
369
370                public BuilderCompleteTask migrateClobToText(
371                                String theVersion, String theFromColumName, String theToColumName) {
372                        MigrateColumnClobTypeToTextTypeTask task = new MigrateColumnClobTypeToTextTypeTask(
373                                        myRelease, theVersion, getTableName(), theFromColumName, theToColumName);
374
375                        addTask(task);
376                        return new BuilderCompleteTask(task);
377                }
378
379                public void dropPrimaryKey(String theVersion) {
380                        final DropPrimaryKeyTask task = new DropPrimaryKeyTask(myRelease, theVersion, myTableName);
381                        addTask(task);
382                }
383
384                public class BuilderAddIndexWithName {
385                        private final String myVersion;
386                        private final String myIndexName;
387
388                        public BuilderAddIndexWithName(String theVersion, String theIndexName) {
389                                myVersion = theVersion;
390                                myIndexName = theIndexName;
391                        }
392
393                        public BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique unique(boolean theUnique) {
394                                return new BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique(myVersion, theUnique);
395                        }
396
397                        public class BuilderAddIndexUnique {
398                                private final String myVersion;
399                                private final boolean myUnique;
400                                private String[] myIncludeColumns;
401                                private boolean myOnline = true;
402
403                                public BuilderAddIndexUnique(String theVersion, boolean theUnique) {
404                                        myVersion = theVersion;
405                                        myUnique = theUnique;
406                                }
407
408                                public void withColumnsStub(String... theColumnNames) {
409                                        BuilderCompleteTask task = withColumns(theColumnNames);
410                                        task.withFlag(TaskFlagEnum.DO_NOTHING);
411                                }
412
413                                public BuilderCompleteTask withColumns(String... theColumnNames) {
414                                        AddIndexTask task = new AddIndexTask(myRelease, myVersion);
415                                        task.setTableName(myTableName);
416                                        task.setIndexName(myIndexName);
417                                        task.setUnique(myUnique);
418                                        task.setColumns(theColumnNames);
419                                        task.setOnline(myOnline);
420                                        if (myIncludeColumns != null) {
421                                                task.setIncludeColumns(myIncludeColumns);
422                                        }
423                                        addTask(task);
424                                        return new BuilderCompleteTask(task);
425                                }
426
427                                /**
428                                 * 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.
429                                 */
430                                public BuilderCompleteTask withPossibleNullableColumns(ColumnAndNullable... theColumns) {
431                                        String[] columnNames = Arrays.stream(theColumns)
432                                                        .map(ColumnAndNullable::getColumnName)
433                                                        .toArray(String[]::new);
434                                        String[] nullableColumnNames = Arrays.stream(theColumns)
435                                                        .filter(ColumnAndNullable::isNullable)
436                                                        .map(ColumnAndNullable::getColumnName)
437                                                        .toArray(String[]::new);
438                                        AddIndexTask task = new AddIndexTask(myRelease, myVersion);
439                                        task.setTableName(myTableName);
440                                        task.setIndexName(myIndexName);
441                                        task.setUnique(myUnique);
442                                        task.setColumns(columnNames);
443                                        task.setNullableColumns(nullableColumnNames);
444                                        task.setOnline(myOnline);
445                                        if (myIncludeColumns != null) {
446                                                task.setIncludeColumns(myIncludeColumns);
447                                        }
448                                        addTask(task);
449                                        return new BuilderCompleteTask(task);
450                                }
451
452                                public BuilderAddIndexUnique includeColumns(String... theIncludeColumns) {
453                                        myIncludeColumns = theIncludeColumns;
454                                        return this;
455                                }
456
457                                /**
458                                 * Add the index without locking the table.
459                                 */
460                                public BuilderAddIndexUnique online(boolean theOnlineFlag) {
461                                        myOnline = theOnlineFlag;
462                                        return this;
463                                }
464                        }
465                }
466
467                public class BuilderModifyColumnWithName {
468                        private final String myVersion;
469                        private final String myColumnName;
470
471                        public BuilderModifyColumnWithName(String theVersion, String theColumnName) {
472                                myVersion = theVersion;
473                                myColumnName = theColumnName;
474                        }
475
476                        public String getColumnName() {
477                                return myColumnName;
478                        }
479
480                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nullable() {
481                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(
482                                                myVersion, true);
483                        }
484
485                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable
486                                        nonNullable() {
487                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(
488                                                myVersion, false);
489                        }
490
491                        public class BuilderModifyColumnWithNameAndNullable {
492                                private final String myVersion;
493                                private final boolean myNullable;
494
495                                public BuilderModifyColumnWithNameAndNullable(String theVersion, boolean theNullable) {
496                                        myVersion = theVersion;
497                                        myNullable = theNullable;
498                                }
499
500                                public BuilderCompleteTask withType(ColumnTypeEnum theColumnType) {
501                                        return withType(theColumnType, null);
502                                }
503
504                                public BuilderCompleteTask withType(ColumnTypeEnum theColumnType, Integer theLength) {
505                                        if (theColumnType == ColumnTypeEnum.STRING) {
506                                                if (theLength == null || theLength == 0) {
507                                                        throw new IllegalArgumentException(
508                                                                        Msg.code(52) + "Can not specify length 0 for column of type " + theColumnType);
509                                                }
510                                        } else {
511                                                if (theLength != null) {
512                                                        throw new IllegalArgumentException(
513                                                                        Msg.code(53) + "Can not specify length for column of type " + theColumnType);
514                                                }
515                                        }
516
517                                        ModifyColumnTask task = new ModifyColumnTask(myRelease, myVersion);
518
519                                        task.setColumnName(myColumnName);
520                                        task.setTableName(myTableName);
521                                        if (theLength != null) {
522                                                task.setColumnLength(theLength);
523                                        }
524                                        task.setNullable(myNullable);
525                                        task.setColumnType(theColumnType);
526                                        addTask(task);
527                                        return new BuilderCompleteTask(task);
528                                }
529                        }
530                }
531
532                public class BuilderAddForeignKey {
533                        private final String myVersion;
534                        private final String myForeignKeyName;
535
536                        public BuilderAddForeignKey(String theVersion, String theForeignKeyName) {
537                                myVersion = theVersion;
538                                myForeignKeyName = theForeignKeyName;
539                        }
540
541                        public BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn toColumn(
542                                        String theColumnName) {
543                                return new BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn(
544                                                myVersion, theColumnName);
545                        }
546
547                        public class BuilderAddForeignKeyToColumn extends BuilderWithTableName.BuilderModifyColumnWithName {
548                                public BuilderAddForeignKeyToColumn(String theVersion, String theColumnName) {
549                                        super(theVersion, theColumnName);
550                                }
551
552                                public BuilderCompleteTask references(String theForeignTable, String theForeignColumn) {
553                                        AddForeignKeyTask task = new AddForeignKeyTask(myRelease, myVersion);
554                                        task.setTableName(myTableName);
555                                        task.setConstraintName(myForeignKeyName);
556                                        task.setColumnName(getColumnName());
557                                        task.setForeignTableName(theForeignTable);
558                                        task.setForeignColumnName(theForeignColumn);
559                                        addTask(task);
560                                        return new BuilderCompleteTask(task);
561                                }
562                        }
563                }
564
565                public static class BuilderAddColumnWithName {
566                        private final String myRelease;
567                        private final String myVersion;
568                        private final String myColumnName;
569
570                        @Nullable
571                        private final Object myDefaultValue;
572
573                        private final BaseMigrationTasks.IAcceptsTasks myTaskSink;
574
575                        public BuilderAddColumnWithName(
576                                        String theRelease,
577                                        String theVersion,
578                                        String theColumnName,
579                                        @Nullable Object theDefaultValue,
580                                        BaseMigrationTasks.IAcceptsTasks theTaskSink) {
581                                myRelease = theRelease;
582                                myVersion = theVersion;
583                                myColumnName = theColumnName;
584                                myDefaultValue = theDefaultValue;
585                                myTaskSink = theTaskSink;
586                        }
587
588                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nullable() {
589                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(
590                                                myRelease, myVersion, true);
591                        }
592
593                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nonNullable() {
594                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(
595                                                myRelease, myVersion, false);
596                        }
597
598                        public class BuilderAddColumnWithNameNullable {
599                                private final boolean myNullable;
600                                private final String myRelease;
601                                private final String myVersion;
602
603                                public BuilderAddColumnWithNameNullable(String theRelease, String theVersion, boolean theNullable) {
604                                        myRelease = theRelease;
605                                        myVersion = theVersion;
606                                        myNullable = theNullable;
607                                }
608
609                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType) {
610                                        return type(theColumnType, null);
611                                }
612
613                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType, Integer theLength) {
614                                        AddColumnTask task = new AddColumnTask(myRelease, myVersion);
615                                        task.setColumnName(myColumnName);
616                                        task.setNullable(myNullable);
617                                        task.setColumnType(theColumnType);
618                                        if (theLength != null) {
619                                                task.setColumnLength(theLength);
620                                        }
621                                        task.setDefaultValue(myDefaultValue);
622                                        myTaskSink.addTask(task);
623
624                                        return new BuilderCompleteTask(task);
625                                }
626                        }
627                }
628        }
629
630        public static class BuilderCompleteTask {
631
632                private final BaseTask myTask;
633
634                public BuilderCompleteTask(BaseTask theTask) {
635                        myTask = theTask;
636                }
637
638                public BuilderCompleteTask failureAllowed() {
639                        myTask.addFlag(TaskFlagEnum.FAILURE_ALLOWED);
640                        return this;
641                }
642
643                public BuilderCompleteTask doNothing() {
644                        myTask.addFlag(TaskFlagEnum.DO_NOTHING);
645                        return this;
646                }
647
648                public BuilderCompleteTask onlyAppliesToPlatforms(DriverTypeEnum... theTypes) {
649                        Set<DriverTypeEnum> typesSet = Arrays.stream(theTypes).collect(Collectors.toSet());
650                        myTask.setOnlyAppliesToPlatforms(typesSet);
651                        return this;
652                }
653
654                /**
655                 * Introduce precondition checking logic into the execution of the enclosed task.  This conditional logic will
656                 * be implemented by running an SQL SELECT (including CTEs) to obtain a boolean indicating whether a certain
657                 * condition has been met.
658                 * One example is to check for a specific collation on a column to decide whether to create a new index.
659                 * <p/>
660                 * This method may be called multiple times to add multiple preconditions.  The precondition that evaluates to
661                 * false will stop execution of the task irrespective of any or all other tasks evaluating to true.
662                 *
663                 * @param theSql The SELECT or CTE used to determine if the precondition is valid.
664                 * @param reason A String to indicate the text that is logged if the precondition is not met.
665                 * @return The BuilderCompleteTask in order to chain further method calls on this builder.
666                 */
667                public BuilderCompleteTask onlyIf(@Language("SQL") String theSql, String reason) {
668                        if (!theSql.toUpperCase().startsWith("WITH")
669                                        && !theSql.toUpperCase().startsWith("SELECT")) {
670                                throw new IllegalArgumentException(Msg.code(2455)
671                                                + String.format(
672                                                                "Only SELECT statements (including CTEs) are allowed here.  Please check your SQL: [%s]",
673                                                                theSql));
674                        }
675                        ourLog.debug("SQL to evaluate: {}", theSql);
676
677                        myTask.addPrecondition(new ExecuteTaskPrecondition(
678                                        () -> {
679                                                ourLog.debug("Checking precondition for SQL: {}", theSql);
680                                                return MigrationJdbcUtils.queryForSingleBooleanResultMultipleThrowsException(
681                                                                theSql, myTask.newJdbcTemplate());
682                                        },
683                                        reason));
684
685                        return this;
686                }
687
688                public BuilderCompleteTask runEvenDuringSchemaInitialization() {
689                        myTask.addFlag(TaskFlagEnum.RUN_DURING_SCHEMA_INITIALIZATION);
690                        return this;
691                }
692
693                public BuilderCompleteTask setTransactional(boolean theFlag) {
694                        myTask.setTransactional(theFlag);
695                        return this;
696                }
697
698                public BuilderCompleteTask heavyweightSkipByDefault() {
699                        myTask.addFlag(TaskFlagEnum.HEAVYWEIGHT_SKIP_BY_DEFAULT);
700                        return this;
701                }
702
703                public BuilderCompleteTask withFlag(TaskFlagEnum theFlag) {
704                        myTask.addFlag(theFlag);
705                        return this;
706                }
707        }
708
709        public class BuilderAddTableRawSql {
710
711                private final AddTableRawSqlTask myTask;
712
713                protected BuilderAddTableRawSql(String theVersion, String theTableName) {
714                        myTask = new AddTableRawSqlTask(myRelease, theVersion);
715                        myTask.setTableName(theTableName);
716                        addTask(myTask);
717                }
718
719                public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) {
720                        myTask.addSql(theDriverTypeEnum, theSql);
721                        return this;
722                }
723
724                public void addSql(@Language("SQL") String theSql) {
725                        myTask.addSql(theSql);
726                }
727        }
728
729        public class BuilderAddTableByColumns extends BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks {
730                private final String myVersion;
731                private final AddTableByColumnTask myTask;
732
733                public BuilderAddTableByColumns(
734                                String theRelease,
735                                String theVersion,
736                                BaseMigrationTasks.IAcceptsTasks theSink,
737                                String theTableName,
738                                List<String> thePkColumnNames) {
739                        super(theRelease, theSink, theTableName);
740                        myVersion = theVersion;
741                        myTask = new AddTableByColumnTask(myRelease, theVersion);
742                        myTask.setTableName(theTableName);
743                        myTask.setPkColumns(thePkColumnNames);
744                        theSink.addTask(myTask);
745                }
746
747                public BuilderAddColumnWithName addColumn(String theColumnName) {
748                        return new BuilderAddColumnWithName(myRelease, myVersion, theColumnName, null, this);
749                }
750
751                @Override
752                public void addTask(BaseTask theTask) {
753                        if (theTask instanceof AddColumnTask) {
754                                myTask.addAddColumnTask((AddColumnTask) theTask);
755                        } else {
756                                super.addTask(theTask);
757                        }
758                }
759
760                public BuilderCompleteTask withFlags() {
761                        return new BuilderCompleteTask(myTask);
762                }
763        }
764
765        public String getRelease() {
766                return myRelease;
767        }
768}