001/*-
002 * #%L
003 * HAPI FHIR Server - SQL Migration
004 * %%
005 * Copyright (C) 2014 - 2026 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 void addIdGenerator(String theVersion, String theGeneratorName, Integer theIncrement) {
178                AddIdGeneratorTask task = new AddIdGeneratorTask(myRelease, theVersion, theGeneratorName, theIncrement);
179                addTask(task);
180        }
181
182        public BuilderCompleteTask dropIdGenerator(String theVersion, String theIdGeneratorName) {
183                DropIdGeneratorTask task = new DropIdGeneratorTask(myRelease, theVersion, theIdGeneratorName);
184                addTask(task);
185                return new BuilderCompleteTask(task);
186        }
187
188        public void addNop(String theVersion) {
189                addTask(new NopTask(myRelease, theVersion));
190        }
191
192        public static class BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks {
193                private final String myRelease;
194                private final BaseMigrationTasks.IAcceptsTasks mySink;
195                private final String myTableName;
196                private BaseTask myLastAddedTask;
197
198                public BuilderWithTableName(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink, String theTableName) {
199                        myRelease = theRelease;
200                        mySink = theSink;
201                        myTableName = theTableName;
202                }
203
204                public String getTableName() {
205                        return myTableName;
206                }
207
208                public BuilderCompleteTask dropIndex(String theVersion, String theIndexName) {
209                        BaseTask task = dropIndexOptional(theVersion, theIndexName);
210                        return new BuilderCompleteTask(task);
211                }
212
213                /**
214                 * Drop index without taking write lock on PG, Oracle, MSSQL.
215                 */
216                public BuilderCompleteTask dropIndexOnline(String theVersion, String theIndexName) {
217                        DropIndexTask task = dropIndexOptional(theVersion, theIndexName);
218                        task.setOnline(true);
219                        return new BuilderCompleteTask(task);
220                }
221
222                public void dropIndexStub(String theVersion, String theIndexName) {
223                        DropIndexTask task = dropIndexOptional(theVersion, theIndexName);
224                        task.addFlag(TaskFlagEnum.DO_NOTHING);
225                }
226
227                private DropIndexTask dropIndexOptional(String theVersion, String theIndexName) {
228                        DropIndexTask task = new DropIndexTask(myRelease, theVersion);
229                        task.setIndexName(theIndexName);
230                        task.setTableName(myTableName);
231                        addTask(task);
232                        return task;
233                }
234
235                /**
236                 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong
237                 */
238                @Deprecated
239                public void renameIndex(String theVersion, String theOldIndexName, String theNewIndexName) {
240                        renameIndexOptional(theVersion, theOldIndexName, theNewIndexName);
241                }
242
243                /**
244                 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong
245                 */
246                public void renameIndexStub(String theVersion, String theOldIndexName, String theNewIndexName) {
247                        RenameIndexTask task = renameIndexOptional(theVersion, theOldIndexName, theNewIndexName);
248                        task.addFlag(TaskFlagEnum.DO_NOTHING);
249                }
250
251                private RenameIndexTask renameIndexOptional(String theVersion, String theOldIndexName, String theNewIndexName) {
252                        RenameIndexTask task = new RenameIndexTask(myRelease, theVersion);
253                        task.setOldIndexName(theOldIndexName);
254                        task.setNewIndexName(theNewIndexName);
255                        task.setTableName(myTableName);
256                        addTask(task);
257                        return task;
258                }
259
260                public void dropThisTable(String theVersion) {
261                        DropTableTask task = new DropTableTask(myRelease, theVersion);
262                        task.setTableName(myTableName);
263                        addTask(task);
264                }
265
266                public BuilderWithTableName.BuilderAddIndexWithName addIndex(String theVersion, String theIndexName) {
267                        return new BuilderWithTableName.BuilderAddIndexWithName(theVersion, theIndexName);
268                }
269
270                public BuilderWithTableName.BuilderAddColumnWithName addColumn(String theVersion, String theColumnName) {
271                        return new BuilderWithTableName.BuilderAddColumnWithName(myRelease, theVersion, theColumnName, null, this);
272                }
273
274                public BuilderWithTableName.BuilderAddColumnWithName addColumn(
275                                String theVersion, String theColumnName, Object theDefaultValue) {
276                        return new BuilderWithTableName.BuilderAddColumnWithName(
277                                        myRelease, theVersion, theColumnName, theDefaultValue, this);
278                }
279
280                public BuilderCompleteTask dropColumn(String theVersion, String theColumnName) {
281                        Validate.notBlank(theColumnName);
282                        DropColumnTask task = new DropColumnTask(myRelease, theVersion);
283                        task.setTableName(myTableName);
284                        task.setColumnName(theColumnName);
285                        addTask(task);
286                        return new BuilderCompleteTask(task);
287                }
288
289                @Override
290                public void addTask(BaseTask theTask) {
291                        ((BaseTableTask) theTask).setTableName(myTableName);
292                        myLastAddedTask = theTask;
293                        mySink.addTask(theTask);
294                }
295
296                public BuilderWithTableName.BuilderModifyColumnWithName modifyColumn(String theVersion, String theColumnName) {
297                        return new BuilderWithTableName.BuilderModifyColumnWithName(theVersion, theColumnName);
298                }
299
300                public BuilderWithTableName.BuilderAddForeignKey addForeignKey(String theVersion, String theForeignKeyName) {
301                        return new BuilderWithTableName.BuilderAddForeignKey(theVersion, theForeignKeyName);
302                }
303
304                public BuilderWithTableName renameColumn(String theVersion, String theOldName, String theNewName) {
305                        return renameColumn(theVersion, theOldName, theNewName, false, false);
306                }
307
308                /**
309                 * @param theOldName                            The old column name
310                 * @param theNewName                            The new column name
311                 * @param isOkayIfNeitherColumnExists           Setting this to true means that it's not an error if neither column exists
312                 * @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.
313                 */
314                public BuilderWithTableName renameColumn(
315                                String theVersion,
316                                String theOldName,
317                                String theNewName,
318                                boolean isOkayIfNeitherColumnExists,
319                                boolean theDeleteTargetColumnFirstIfBothExist) {
320                        RenameColumnTask task = new RenameColumnTask(myRelease, theVersion);
321                        task.setTableName(myTableName);
322                        task.setOldName(theOldName);
323                        task.setNewName(theNewName);
324                        task.setOkayIfNeitherColumnExists(isOkayIfNeitherColumnExists);
325                        task.setDeleteTargetColumnFirstIfBothExist(theDeleteTargetColumnFirstIfBothExist);
326                        addTask(task);
327                        return this;
328                }
329
330                public Optional<BaseTask> getLastAddedTask() {
331                        return Optional.ofNullable(myLastAddedTask);
332                }
333
334                public void addPrimaryKey(String theVersion, String... theColumnsInOrder) {
335                        addTask(new AddPrimaryKeyTask(myRelease, theVersion, myTableName, theColumnsInOrder));
336                }
337
338                /**
339                 * @param theFkName          the name of the foreign key
340                 * @param theParentTableName the name of the table that exports the foreign key
341                 */
342                public BuilderCompleteTask dropForeignKey(String theVersion, String theFkName, String theParentTableName) {
343                        DropForeignKeyTask task = new DropForeignKeyTask(myRelease, theVersion);
344                        task.setConstraintName(theFkName);
345                        task.setTableName(getTableName());
346                        task.setParentTableName(theParentTableName);
347                        addTask(task);
348                        return new BuilderCompleteTask(task);
349                }
350
351                public BuilderCompleteTask renameTable(String theVersion, String theNewTableName) {
352                        RenameTableTask task = new RenameTableTask(myRelease, theVersion, getTableName(), theNewTableName);
353                        addTask(task);
354                        return new BuilderCompleteTask(task);
355                }
356
357                public BuilderCompleteTask migratePostgresTextClobToBinaryClob(String theVersion, String theColumnName) {
358                        MigratePostgresTextClobToBinaryClobTask task =
359                                        new MigratePostgresTextClobToBinaryClobTask(myRelease, theVersion);
360                        task.setTableName(getTableName());
361                        task.setColumnName(theColumnName);
362                        addTask(task);
363                        return new BuilderCompleteTask(task);
364                }
365
366                public BuilderCompleteTask migrateBlobToBinary(
367                                String theVersion, String theFromColumName, String theToColumName) {
368                        MigrateColumBlobTypeToBinaryTypeTask task = new MigrateColumBlobTypeToBinaryTypeTask(
369                                        myRelease, theVersion, getTableName(), theFromColumName, theToColumName);
370
371                        addTask(task);
372                        return new BuilderCompleteTask(task);
373                }
374
375                public BuilderCompleteTask migrateClobToText(
376                                String theVersion, String theFromColumName, String theToColumName) {
377                        MigrateColumnClobTypeToTextTypeTask task = new MigrateColumnClobTypeToTextTypeTask(
378                                        myRelease, theVersion, getTableName(), theFromColumName, theToColumName);
379
380                        addTask(task);
381                        return new BuilderCompleteTask(task);
382                }
383
384                public void dropPrimaryKey(String theVersion) {
385                        final DropPrimaryKeyTask task = new DropPrimaryKeyTask(myRelease, theVersion, myTableName);
386                        addTask(task);
387                }
388
389                public class BuilderAddIndexWithName {
390                        private final String myVersion;
391                        private final String myIndexName;
392
393                        public BuilderAddIndexWithName(String theVersion, String theIndexName) {
394                                myVersion = theVersion;
395                                myIndexName = theIndexName;
396                        }
397
398                        public BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique unique(boolean theUnique) {
399                                return new BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique(myVersion, theUnique);
400                        }
401
402                        public class BuilderAddIndexUnique {
403                                private final String myVersion;
404                                private final boolean myUnique;
405                                private String[] myIncludeColumns;
406                                private boolean myOnline = true;
407
408                                public BuilderAddIndexUnique(String theVersion, boolean theUnique) {
409                                        myVersion = theVersion;
410                                        myUnique = theUnique;
411                                }
412
413                                public void withColumnsStub(String... theColumnNames) {
414                                        BuilderCompleteTask task = withColumns(theColumnNames);
415                                        task.withFlag(TaskFlagEnum.DO_NOTHING);
416                                }
417
418                                public BuilderCompleteTask withColumns(String... theColumnNames) {
419                                        AddIndexTask task = new AddIndexTask(myRelease, myVersion);
420                                        task.setTableName(myTableName);
421                                        task.setIndexName(myIndexName);
422                                        task.setUnique(myUnique);
423                                        task.setColumns(theColumnNames);
424                                        task.setOnline(myOnline);
425                                        if (myIncludeColumns != null) {
426                                                task.setIncludeColumns(myIncludeColumns);
427                                        }
428                                        addTask(task);
429                                        return new BuilderCompleteTask(task);
430                                }
431
432                                /**
433                                 * 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.
434                                 */
435                                public BuilderCompleteTask withPossibleNullableColumns(ColumnAndNullable... theColumns) {
436                                        String[] columnNames = Arrays.stream(theColumns)
437                                                        .map(ColumnAndNullable::getColumnName)
438                                                        .toArray(String[]::new);
439                                        String[] nullableColumnNames = Arrays.stream(theColumns)
440                                                        .filter(ColumnAndNullable::isNullable)
441                                                        .map(ColumnAndNullable::getColumnName)
442                                                        .toArray(String[]::new);
443                                        AddIndexTask task = new AddIndexTask(myRelease, myVersion);
444                                        task.setTableName(myTableName);
445                                        task.setIndexName(myIndexName);
446                                        task.setUnique(myUnique);
447                                        task.setColumns(columnNames);
448                                        task.setNullableColumns(nullableColumnNames);
449                                        task.setOnline(myOnline);
450                                        if (myIncludeColumns != null) {
451                                                task.setIncludeColumns(myIncludeColumns);
452                                        }
453                                        addTask(task);
454                                        return new BuilderCompleteTask(task);
455                                }
456
457                                public BuilderAddIndexUnique includeColumns(String... theIncludeColumns) {
458                                        myIncludeColumns = theIncludeColumns;
459                                        return this;
460                                }
461
462                                /**
463                                 * Add the index without locking the table.
464                                 */
465                                public BuilderAddIndexUnique online(boolean theOnlineFlag) {
466                                        myOnline = theOnlineFlag;
467                                        return this;
468                                }
469                        }
470                }
471
472                public class BuilderModifyColumnWithName {
473                        private final String myVersion;
474                        private final String myColumnName;
475
476                        public BuilderModifyColumnWithName(String theVersion, String theColumnName) {
477                                myVersion = theVersion;
478                                myColumnName = theColumnName;
479                        }
480
481                        public String getColumnName() {
482                                return myColumnName;
483                        }
484
485                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nullable() {
486                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(
487                                                myVersion, true);
488                        }
489
490                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable
491                                        nonNullable() {
492                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(
493                                                myVersion, false);
494                        }
495
496                        public class BuilderModifyColumnWithNameAndNullable {
497                                private final String myVersion;
498                                private final boolean myNullable;
499
500                                public BuilderModifyColumnWithNameAndNullable(String theVersion, boolean theNullable) {
501                                        myVersion = theVersion;
502                                        myNullable = theNullable;
503                                }
504
505                                public BuilderCompleteTask withType(ColumnTypeEnum theColumnType) {
506                                        return withType(theColumnType, null);
507                                }
508
509                                public BuilderCompleteTask withType(ColumnTypeEnum theColumnType, Integer theLength) {
510                                        if (theColumnType == ColumnTypeEnum.STRING) {
511                                                if (theLength == null || theLength == 0) {
512                                                        throw new IllegalArgumentException(
513                                                                        Msg.code(52) + "Can not specify length 0 for column of type " + theColumnType);
514                                                }
515                                        } else {
516                                                if (theLength != null) {
517                                                        throw new IllegalArgumentException(
518                                                                        Msg.code(53) + "Can not specify length for column of type " + theColumnType);
519                                                }
520                                        }
521
522                                        ModifyColumnTask task = new ModifyColumnTask(myRelease, myVersion);
523
524                                        task.setColumnName(myColumnName);
525                                        task.setTableName(myTableName);
526                                        if (theLength != null) {
527                                                task.setColumnLength(theLength);
528                                        }
529                                        task.setNullable(myNullable);
530                                        task.setColumnType(theColumnType);
531                                        addTask(task);
532                                        return new BuilderCompleteTask(task);
533                                }
534                        }
535                }
536
537                public class BuilderAddForeignKey {
538                        private final String myVersion;
539                        private final String myForeignKeyName;
540
541                        public BuilderAddForeignKey(String theVersion, String theForeignKeyName) {
542                                myVersion = theVersion;
543                                myForeignKeyName = theForeignKeyName;
544                        }
545
546                        public BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn toColumn(
547                                        String theColumnName) {
548                                return new BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn(
549                                                myVersion, theColumnName);
550                        }
551
552                        public class BuilderAddForeignKeyToColumn extends BuilderWithTableName.BuilderModifyColumnWithName {
553                                public BuilderAddForeignKeyToColumn(String theVersion, String theColumnName) {
554                                        super(theVersion, theColumnName);
555                                }
556
557                                public BuilderCompleteTask references(String theForeignTable, String theForeignColumn) {
558                                        AddForeignKeyTask task = new AddForeignKeyTask(myRelease, myVersion);
559                                        task.setTableName(myTableName);
560                                        task.setConstraintName(myForeignKeyName);
561                                        task.setColumnName(getColumnName());
562                                        task.setForeignTableName(theForeignTable);
563                                        task.setForeignColumnName(theForeignColumn);
564                                        addTask(task);
565                                        return new BuilderCompleteTask(task);
566                                }
567                        }
568                }
569
570                public static class BuilderAddColumnWithName {
571                        private final String myRelease;
572                        private final String myVersion;
573                        private final String myColumnName;
574
575                        @Nullable
576                        private final Object myDefaultValue;
577
578                        private final BaseMigrationTasks.IAcceptsTasks myTaskSink;
579
580                        public BuilderAddColumnWithName(
581                                        String theRelease,
582                                        String theVersion,
583                                        String theColumnName,
584                                        @Nullable Object theDefaultValue,
585                                        BaseMigrationTasks.IAcceptsTasks theTaskSink) {
586                                myRelease = theRelease;
587                                myVersion = theVersion;
588                                myColumnName = theColumnName;
589                                myDefaultValue = theDefaultValue;
590                                myTaskSink = theTaskSink;
591                        }
592
593                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nullable() {
594                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(
595                                                myRelease, myVersion, true);
596                        }
597
598                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nonNullable() {
599                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(
600                                                myRelease, myVersion, false);
601                        }
602
603                        public class BuilderAddColumnWithNameNullable {
604                                private final boolean myNullable;
605                                private final String myRelease;
606                                private final String myVersion;
607
608                                public BuilderAddColumnWithNameNullable(String theRelease, String theVersion, boolean theNullable) {
609                                        myRelease = theRelease;
610                                        myVersion = theVersion;
611                                        myNullable = theNullable;
612                                }
613
614                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType) {
615                                        return type(theColumnType, null);
616                                }
617
618                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType, Integer theLength) {
619                                        AddColumnTask task = new AddColumnTask(myRelease, myVersion);
620                                        task.setColumnName(myColumnName);
621                                        task.setNullable(myNullable);
622                                        task.setColumnType(theColumnType);
623                                        if (theLength != null) {
624                                                task.setColumnLength(theLength);
625                                        }
626                                        task.setDefaultValue(myDefaultValue);
627                                        myTaskSink.addTask(task);
628
629                                        return new BuilderCompleteTask(task);
630                                }
631                        }
632                }
633        }
634
635        public static class BuilderCompleteTask {
636
637                private final BaseTask myTask;
638
639                public BuilderCompleteTask(BaseTask theTask) {
640                        myTask = theTask;
641                }
642
643                public BuilderCompleteTask failureAllowed() {
644                        myTask.addFlag(TaskFlagEnum.FAILURE_ALLOWED);
645                        return this;
646                }
647
648                public BuilderCompleteTask doNothing() {
649                        myTask.addFlag(TaskFlagEnum.DO_NOTHING);
650                        return this;
651                }
652
653                public BuilderCompleteTask onlyAppliesToPlatforms(DriverTypeEnum... theTypes) {
654                        Set<DriverTypeEnum> typesSet = Arrays.stream(theTypes).collect(Collectors.toSet());
655                        myTask.setOnlyAppliesToPlatforms(typesSet);
656                        return this;
657                }
658
659                /**
660                 * Introduce precondition checking logic into the execution of the enclosed task.  This conditional logic will
661                 * be implemented by running an SQL SELECT (including CTEs) to obtain a boolean indicating whether a certain
662                 * condition has been met.
663                 * One example is to check for a specific collation on a column to decide whether to create a new index.
664                 * <p/>
665                 * This method may be called multiple times to add multiple preconditions.  The precondition that evaluates to
666                 * false will stop execution of the task irrespective of any or all other tasks evaluating to true.
667                 *
668                 * @param theSql The SELECT or CTE used to determine if the precondition is valid.
669                 * @param reason A String to indicate the text that is logged if the precondition is not met.
670                 * @return The BuilderCompleteTask in order to chain further method calls on this builder.
671                 */
672                public BuilderCompleteTask onlyIf(@Language("SQL") String theSql, String reason) {
673                        if (!theSql.toUpperCase().startsWith("WITH")
674                                        && !theSql.toUpperCase().startsWith("SELECT")) {
675                                throw new IllegalArgumentException(Msg.code(2455)
676                                                + String.format(
677                                                                "Only SELECT statements (including CTEs) are allowed here.  Please check your SQL: [%s]",
678                                                                theSql));
679                        }
680                        ourLog.debug("SQL to evaluate: {}", theSql);
681
682                        myTask.addPrecondition(new ExecuteTaskPrecondition(
683                                        () -> {
684                                                ourLog.debug("Checking precondition for SQL: {}", theSql);
685                                                return MigrationJdbcUtils.queryForSingleBooleanResultMultipleThrowsException(
686                                                                theSql, myTask.newJdbcTemplate());
687                                        },
688                                        reason));
689
690                        return this;
691                }
692
693                public BuilderCompleteTask runEvenDuringSchemaInitialization() {
694                        myTask.addFlag(TaskFlagEnum.RUN_DURING_SCHEMA_INITIALIZATION);
695                        return this;
696                }
697
698                public BuilderCompleteTask setTransactional(boolean theFlag) {
699                        myTask.setTransactional(theFlag);
700                        return this;
701                }
702
703                public BuilderCompleteTask heavyweightSkipByDefault() {
704                        myTask.addFlag(TaskFlagEnum.HEAVYWEIGHT_SKIP_BY_DEFAULT);
705                        return this;
706                }
707
708                public BuilderCompleteTask withFlag(TaskFlagEnum theFlag) {
709                        myTask.addFlag(theFlag);
710                        return this;
711                }
712        }
713
714        public class BuilderAddTableRawSql {
715
716                private final AddTableRawSqlTask myTask;
717
718                protected BuilderAddTableRawSql(String theVersion, String theTableName) {
719                        myTask = new AddTableRawSqlTask(myRelease, theVersion);
720                        myTask.setTableName(theTableName);
721                        addTask(myTask);
722                }
723
724                public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) {
725                        myTask.addSql(theDriverTypeEnum, theSql);
726                        return this;
727                }
728
729                public void addSql(@Language("SQL") String theSql) {
730                        myTask.addSql(theSql);
731                }
732        }
733
734        public class BuilderAddTableByColumns extends BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks {
735                private final String myVersion;
736                private final AddTableByColumnTask myTask;
737
738                public BuilderAddTableByColumns(
739                                String theRelease,
740                                String theVersion,
741                                BaseMigrationTasks.IAcceptsTasks theSink,
742                                String theTableName,
743                                List<String> thePkColumnNames) {
744                        super(theRelease, theSink, theTableName);
745                        myVersion = theVersion;
746                        myTask = new AddTableByColumnTask(myRelease, theVersion);
747                        myTask.setTableName(theTableName);
748                        myTask.setPkColumns(thePkColumnNames);
749                        theSink.addTask(myTask);
750                }
751
752                public BuilderAddColumnWithName addColumn(String theColumnName) {
753                        return new BuilderAddColumnWithName(myRelease, myVersion, theColumnName, null, this);
754                }
755
756                @Override
757                public void addTask(BaseTask theTask) {
758                        if (theTask instanceof AddColumnTask) {
759                                myTask.addAddColumnTask((AddColumnTask) theTask);
760                        } else {
761                                super.addTask(theTask);
762                        }
763                }
764
765                public BuilderCompleteTask withFlags() {
766                        return new BuilderCompleteTask(myTask);
767                }
768        }
769
770        public String getRelease() {
771                return myRelease;
772        }
773}