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