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