001package ca.uhn.fhir.jpa.migrate.tasks.api;
002
003/*-
004 * #%L
005 * HAPI FHIR Server - SQL Migration
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
024import ca.uhn.fhir.jpa.migrate.taskdef.AddColumnTask;
025import ca.uhn.fhir.jpa.migrate.taskdef.AddForeignKeyTask;
026import ca.uhn.fhir.jpa.migrate.taskdef.AddIdGeneratorTask;
027import ca.uhn.fhir.jpa.migrate.taskdef.AddIndexTask;
028import ca.uhn.fhir.jpa.migrate.taskdef.AddTableByColumnTask;
029import ca.uhn.fhir.jpa.migrate.taskdef.AddTableRawSqlTask;
030import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableTask;
031import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask;
032import ca.uhn.fhir.jpa.migrate.taskdef.ColumnTypeEnum;
033import ca.uhn.fhir.jpa.migrate.taskdef.DropColumnTask;
034import ca.uhn.fhir.jpa.migrate.taskdef.DropForeignKeyTask;
035import ca.uhn.fhir.jpa.migrate.taskdef.DropIdGeneratorTask;
036import ca.uhn.fhir.jpa.migrate.taskdef.DropIndexTask;
037import ca.uhn.fhir.jpa.migrate.taskdef.DropTableTask;
038import ca.uhn.fhir.jpa.migrate.taskdef.ExecuteRawSqlTask;
039import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask;
040import ca.uhn.fhir.jpa.migrate.taskdef.MigratePostgresTextClobToBinaryClobTask;
041import ca.uhn.fhir.jpa.migrate.taskdef.ModifyColumnTask;
042import ca.uhn.fhir.jpa.migrate.taskdef.NopTask;
043import ca.uhn.fhir.jpa.migrate.taskdef.RenameColumnTask;
044import ca.uhn.fhir.jpa.migrate.taskdef.RenameIndexTask;
045import org.apache.commons.lang3.Validate;
046import org.intellij.lang.annotations.Language;
047
048import java.util.Arrays;
049import java.util.List;
050import java.util.Set;
051import java.util.stream.Collectors;
052
053public class Builder {
054
055        private final String myRelease;
056        private final BaseMigrationTasks.IAcceptsTasks mySink;
057
058        public Builder(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink) {
059                myRelease = theRelease;
060                mySink = theSink;
061        }
062
063        public BuilderWithTableName onTable(String theTableName) {
064                return new BuilderWithTableName(myRelease, mySink, theTableName);
065        }
066
067        public void addTask(BaseTask theTask) {
068                mySink.addTask(theTask);
069        }
070
071        public BuilderAddTableRawSql addTableRawSql(String theVersion, String theTableName) {
072                return new BuilderAddTableRawSql(theVersion, theTableName);
073        }
074
075        public BuilderCompleteTask executeRawSql(String theVersion, @Language("SQL") String theSql) {
076                ExecuteRawSqlTask task = new ExecuteRawSqlTask(myRelease, theVersion).addSql(theSql);
077                mySink.addTask(task);
078                return new BuilderCompleteTask(task);
079        }
080
081        public Builder initializeSchema(String theVersion, ISchemaInitializationProvider theSchemaInitializationProvider) {
082                mySink.addTask(new InitializeSchemaTask(myRelease, theVersion, theSchemaInitializationProvider));
083                return this;
084        }
085
086        @SuppressWarnings("unused")
087        public Builder initializeSchema(String theVersion, String theSchemaName, ISchemaInitializationProvider theSchemaInitializationProvider) {
088                InitializeSchemaTask task = new InitializeSchemaTask(myRelease, theVersion, theSchemaInitializationProvider);
089                task.setDescription("Initialize " + theSchemaName + " schema");
090                mySink.addTask(task);
091                return this;
092        }
093
094        public Builder executeRawSql(String theVersion, DriverTypeEnum theDriver, @Language("SQL") String theSql) {
095                mySink.addTask(new ExecuteRawSqlTask(myRelease, theVersion).addSql(theDriver, theSql));
096                return this;
097        }
098
099
100        // Flyway doesn't support these kinds of migrations
101        @Deprecated
102        public Builder startSectionWithMessage(String theMessage) {
103                // Do nothing
104                return this;
105        }
106
107        public BuilderAddTableByColumns addTableByColumns(String theVersion, String theTableName, String... thePkColumnNames) {
108                return new BuilderAddTableByColumns(myRelease, theVersion, mySink, theTableName, Arrays.asList(thePkColumnNames));
109        }
110
111        public void addIdGenerator(String theVersion, String theGeneratorName) {
112                AddIdGeneratorTask task = new AddIdGeneratorTask(myRelease, theVersion, theGeneratorName);
113                addTask(task);
114        }
115
116        public void dropIdGenerator(String theVersion, String theIdGeneratorName) {
117                DropIdGeneratorTask task = new DropIdGeneratorTask(myRelease, theVersion, theIdGeneratorName);
118                addTask(task);
119        }
120
121        public void addNop(String theVersion) {
122                addTask(new NopTask(myRelease, theVersion));
123        }
124
125        public static class BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks {
126                private final String myRelease;
127                private final BaseMigrationTasks.IAcceptsTasks mySink;
128                private final String myTableName;
129
130                public BuilderWithTableName(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink, String theTableName) {
131                        myRelease = theRelease;
132                        mySink = theSink;
133                        myTableName = theTableName;
134                }
135
136                public String getTableName() {
137                        return myTableName;
138                }
139
140                public BuilderCompleteTask dropIndex(String theVersion, String theIndexName) {
141                        BaseTask task = dropIndexOptional(false, theVersion, theIndexName);
142                        return new BuilderCompleteTask(task);
143                }
144
145                public void dropIndexStub(String theVersion, String theIndexName) {
146                        dropIndexOptional(true, theVersion, theIndexName);
147                }
148
149                private DropIndexTask dropIndexOptional(boolean theDoNothing, String theVersion, String theIndexName) {
150                        DropIndexTask task = new DropIndexTask(myRelease, theVersion);
151                        task.setIndexName(theIndexName);
152                        task.setTableName(myTableName);
153                        task.setDoNothing(theDoNothing);
154                        addTask(task);
155                        return task;
156                }
157
158                /**
159                 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong
160                 */
161                @Deprecated
162                public void renameIndex(String theVersion, String theOldIndexName, String theNewIndexName) {
163                        renameIndexOptional(false, theVersion, theOldIndexName, theNewIndexName);
164                }
165
166                /**
167                 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong
168                 */
169                public void renameIndexStub(String theVersion, String theOldIndexName, String theNewIndexName) {
170                        renameIndexOptional(true, theVersion, theOldIndexName, theNewIndexName);
171                }
172
173                private void renameIndexOptional(boolean theDoNothing, String theVersion, String theOldIndexName, String theNewIndexName) {
174                        RenameIndexTask task = new RenameIndexTask(myRelease, theVersion);
175                        task.setOldIndexName(theOldIndexName);
176                        task.setNewIndexName(theNewIndexName);
177                        task.setTableName(myTableName);
178                        task.setDoNothing(theDoNothing);
179                        addTask(task);
180                }
181
182                public void dropThisTable(String theVersion) {
183                        DropTableTask task = new DropTableTask(myRelease, theVersion);
184                        task.setTableName(myTableName);
185                        addTask(task);
186                }
187
188                public BuilderWithTableName.BuilderAddIndexWithName addIndex(String theVersion, String theIndexName) {
189                        return new BuilderWithTableName.BuilderAddIndexWithName(theVersion, theIndexName);
190                }
191
192                public BuilderWithTableName.BuilderAddColumnWithName addColumn(String theVersion, String theColumnName) {
193                        return new BuilderWithTableName.BuilderAddColumnWithName(myRelease, theVersion, theColumnName, this);
194                }
195
196                public BuilderCompleteTask dropColumn(String theVersion, String theColumnName) {
197                        Validate.notBlank(theColumnName);
198                        DropColumnTask task = new DropColumnTask(myRelease, theVersion);
199                        task.setTableName(myTableName);
200                        task.setColumnName(theColumnName);
201                        addTask(task);
202                        return new BuilderCompleteTask(task);
203                }
204
205                @Override
206                public void addTask(BaseTask theTask) {
207                        ((BaseTableTask) theTask).setTableName(myTableName);
208                        mySink.addTask(theTask);
209                }
210
211                public BuilderWithTableName.BuilderModifyColumnWithName modifyColumn(String theVersion, String theColumnName) {
212                        return new BuilderWithTableName.BuilderModifyColumnWithName(theVersion, theColumnName);
213                }
214
215                public BuilderWithTableName.BuilderAddForeignKey addForeignKey(String theVersion, String theForeignKeyName) {
216                        return new BuilderWithTableName.BuilderAddForeignKey(theVersion, theForeignKeyName);
217                }
218
219                public BuilderWithTableName renameColumn(String theVersion, String theOldName, String theNewName) {
220                        return renameColumn(theVersion, theOldName, theNewName, false, false);
221                }
222
223                /**
224                 * @param theOldName                            The old column name
225                 * @param theNewName                            The new column name
226                 * @param isOkayIfNeitherColumnExists           Setting this to true means that it's not an error if neither column exists
227                 * @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.
228                 */
229                public BuilderWithTableName renameColumn(String theVersion, String theOldName, String theNewName, boolean isOkayIfNeitherColumnExists, boolean theDeleteTargetColumnFirstIfBothExist) {
230                        RenameColumnTask task = new RenameColumnTask(myRelease, theVersion);
231                        task.setTableName(myTableName);
232                        task.setOldName(theOldName);
233                        task.setNewName(theNewName);
234                        task.setOkayIfNeitherColumnExists(isOkayIfNeitherColumnExists);
235                        task.setDeleteTargetColumnFirstIfBothExist(theDeleteTargetColumnFirstIfBothExist);
236                        addTask(task);
237                        return this;
238                }
239
240                /**
241                 * @param theFkName          the name of the foreign key
242                 * @param theParentTableName the name of the table that exports the foreign key
243                 */
244                public void dropForeignKey(String theVersion, String theFkName, String theParentTableName) {
245                        DropForeignKeyTask task = new DropForeignKeyTask(myRelease, theVersion);
246                        task.setConstraintName(theFkName);
247                        task.setTableName(getTableName());
248                        task.setParentTableName(theParentTableName);
249                        addTask(task);
250                }
251
252                public void migratePostgresTextClobToBinaryClob(String theVersion, String theColumnName) {
253                        MigratePostgresTextClobToBinaryClobTask task = new MigratePostgresTextClobToBinaryClobTask(myRelease, theVersion);
254                        task.setTableName(getTableName());
255                        task.setColumnName(theColumnName);
256                        addTask(task);
257                }
258
259                public class BuilderAddIndexWithName {
260                        private final String myVersion;
261                        private final String myIndexName;
262
263                        public BuilderAddIndexWithName(String theVersion, String theIndexName) {
264                                myVersion = theVersion;
265                                myIndexName = theIndexName;
266                        }
267
268                        public BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique unique(boolean theUnique) {
269                                return new BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique(myVersion, theUnique);
270                        }
271
272                        public class BuilderAddIndexUnique {
273                                private final String myVersion;
274                                private final boolean myUnique;
275                                private String[] myIncludeColumns;
276
277                                public BuilderAddIndexUnique(String theVersion, boolean theUnique) {
278                                        myVersion = theVersion;
279                                        myUnique = theUnique;
280                                }
281
282                                public void withColumnsStub(String... theColumnNames) {
283                                        withColumnsOptional(true, theColumnNames);
284                                }
285
286                                public BuilderCompleteTask withColumns(String... theColumnNames) {
287                                        BaseTask task = withColumnsOptional(false, theColumnNames);
288                                        return new BuilderCompleteTask(task);
289                                }
290
291                                private AddIndexTask withColumnsOptional(boolean theDoNothing, String... theColumnNames) {
292                                        AddIndexTask task = new AddIndexTask(myRelease, myVersion);
293                                        task.setTableName(myTableName);
294                                        task.setIndexName(myIndexName);
295                                        task.setUnique(myUnique);
296                                        task.setColumns(theColumnNames);
297                                        task.setDoNothing(theDoNothing);
298                                        if (myIncludeColumns != null) {
299                                                task.setIncludeColumns(myIncludeColumns);
300                                        }
301                                        addTask(task);
302                                        return task;
303                                }
304
305                                public BuilderAddIndexUnique includeColumns(String... theIncludeColumns) {
306                                        myIncludeColumns = theIncludeColumns;
307                                        return this;
308                                }
309                        }
310                }
311
312                public class BuilderModifyColumnWithName {
313                        private final String myVersion;
314                        private final String myColumnName;
315
316                        public BuilderModifyColumnWithName(String theVersion, String theColumnName) {
317                                myVersion = theVersion;
318                                myColumnName = theColumnName;
319                        }
320
321                        public String getColumnName() {
322                                return myColumnName;
323                        }
324
325                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nullable() {
326                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(myVersion, true);
327                        }
328
329                        public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nonNullable() {
330                                return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(myVersion, false);
331                        }
332
333                        public class BuilderModifyColumnWithNameAndNullable {
334                                private final String myVersion;
335                                private final boolean myNullable;
336                                private boolean myFailureAllowed;
337
338                                public BuilderModifyColumnWithNameAndNullable(String theVersion, boolean theNullable) {
339                                        myVersion = theVersion;
340                                        myNullable = theNullable;
341                                }
342
343                                public void withType(ColumnTypeEnum theColumnType) {
344                                        withType(theColumnType, null);
345                                }
346
347                                public void withType(ColumnTypeEnum theColumnType, Integer theLength) {
348                                        if (theColumnType == ColumnTypeEnum.STRING) {
349                                                if (theLength == null || theLength == 0) {
350                                                        throw new IllegalArgumentException("Can not specify length 0 for column of type " + theColumnType);
351                                                }
352                                        } else {
353                                                if (theLength != null) {
354                                                        throw new IllegalArgumentException("Can not specify length for column of type " + theColumnType);
355                                                }
356                                        }
357
358                                        ModifyColumnTask task = new ModifyColumnTask(myRelease, myVersion);
359                                        task.setColumnName(myColumnName);
360                                        task.setTableName(myTableName);
361                                        if (theLength != null) {
362                                                task.setColumnLength(theLength);
363                                        }
364                                        task.setNullable(myNullable);
365                                        task.setColumnType(theColumnType);
366                                        task.setFailureAllowed(myFailureAllowed);
367                                        addTask(task);
368                                }
369
370                                public BuilderModifyColumnWithNameAndNullable failureAllowed() {
371                                        myFailureAllowed = true;
372                                        return this;
373                                }
374                        }
375                }
376
377                public class BuilderAddForeignKey {
378                        private final String myVersion;
379                        private final String myForeignKeyName;
380
381                        public BuilderAddForeignKey(String theVersion, String theForeignKeyName) {
382                                myVersion = theVersion;
383                                myForeignKeyName = theForeignKeyName;
384                        }
385
386                        public BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn toColumn(String theColumnName) {
387                                return new BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn(myVersion, theColumnName);
388                        }
389
390                        public class BuilderAddForeignKeyToColumn extends BuilderWithTableName.BuilderModifyColumnWithName {
391                                public BuilderAddForeignKeyToColumn(String theVersion, String theColumnName) {
392                                        super(theVersion, theColumnName);
393                                }
394
395                                public BuilderCompleteTask references(String theForeignTable, String theForeignColumn) {
396                                        AddForeignKeyTask task = new AddForeignKeyTask(myRelease, myVersion);
397                                        task.setTableName(myTableName);
398                                        task.setConstraintName(myForeignKeyName);
399                                        task.setColumnName(getColumnName());
400                                        task.setForeignTableName(theForeignTable);
401                                        task.setForeignColumnName(theForeignColumn);
402                                        addTask(task);
403                                        return new BuilderCompleteTask(task);
404                                }
405                        }
406                }
407
408                public class BuilderAddColumnWithName {
409                        private final String myRelease;
410                        private final String myVersion;
411                        private final String myColumnName;
412                        private final BaseMigrationTasks.IAcceptsTasks myTaskSink;
413
414                        public BuilderAddColumnWithName(String theRelease, String theVersion, String theColumnName, BaseMigrationTasks.IAcceptsTasks theTaskSink) {
415                                myRelease = theRelease;
416                                myVersion = theVersion;
417                                myColumnName = theColumnName;
418                                myTaskSink = theTaskSink;
419                        }
420
421                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nullable() {
422                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(myRelease, myVersion, true);
423                        }
424
425                        public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nonNullable() {
426                                return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(myRelease, myVersion, false);
427                        }
428
429                        public class BuilderAddColumnWithNameNullable {
430                                private final boolean myNullable;
431                                private final String myRelease;
432                                private final String myVersion;
433
434                                public BuilderAddColumnWithNameNullable(String theRelease, String theVersion, boolean theNullable) {
435                                        myRelease = theRelease;
436                                        myVersion = theVersion;
437                                        myNullable = theNullable;
438                                }
439
440                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType) {
441                                        return type(theColumnType, null);
442                                }
443
444                                public BuilderCompleteTask type(ColumnTypeEnum theColumnType, Integer theLength) {
445                                        AddColumnTask task = new AddColumnTask(myRelease, myVersion);
446                                        task.setColumnName(myColumnName);
447                                        task.setNullable(myNullable);
448                                        task.setColumnType(theColumnType);
449                                        if (theLength != null) {
450                                                task.setColumnLength(theLength);
451                                        }
452                                        myTaskSink.addTask(task);
453
454                                        return new BuilderCompleteTask(task);
455                                }
456
457                        }
458                }
459        }
460
461        public static class BuilderCompleteTask {
462
463                private final BaseTask myTask;
464
465                public BuilderCompleteTask(BaseTask theTask) {
466                        myTask = theTask;
467                }
468
469                public BuilderCompleteTask failureAllowed() {
470                        myTask.setFailureAllowed(true);
471                        return this;
472                }
473
474                public BuilderCompleteTask doNothing() {
475                        myTask.setDoNothing(true);
476                        return this;
477                }
478
479                public BuilderCompleteTask onlyAppliesToPlatforms(DriverTypeEnum... theTypes) {
480                        Set<DriverTypeEnum> typesSet = Arrays.stream(theTypes).collect(Collectors.toSet());
481                        myTask.setOnlyAppliesToPlatforms(typesSet);
482                        return this;
483                }
484
485                public BuilderCompleteTask runEvenDuringSchemaInitialization() {
486                        myTask.setRunDuringSchemaInitialization(true);
487                        return this;
488                }
489        }
490
491        public class BuilderAddTableRawSql {
492
493                private final AddTableRawSqlTask myTask;
494
495                protected BuilderAddTableRawSql(String theVersion, String theTableName) {
496                        myTask = new AddTableRawSqlTask(myRelease, theVersion);
497                        myTask.setTableName(theTableName);
498                        addTask(myTask);
499                }
500
501
502                public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) {
503                        myTask.addSql(theDriverTypeEnum, theSql);
504                        return this;
505                }
506
507                public void addSql(@Language("SQL") String theSql) {
508                        myTask.addSql(theSql);
509                }
510        }
511
512        public class BuilderAddTableByColumns extends BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks {
513                private final String myVersion;
514                private final AddTableByColumnTask myTask;
515
516                public BuilderAddTableByColumns(String theRelease, String theVersion, BaseMigrationTasks.IAcceptsTasks theSink, String theTableName, List<String> thePkColumnNames) {
517                        super(theRelease, theSink, theTableName);
518                        myVersion = theVersion;
519                        myTask = new AddTableByColumnTask(myRelease, theVersion);
520                        myTask.setTableName(theTableName);
521                        myTask.setPkColumns(thePkColumnNames);
522                        theSink.addTask(myTask);
523                }
524
525                public BuilderAddColumnWithName addColumn(String theColumnName) {
526                        return new BuilderAddColumnWithName(myRelease, myVersion, theColumnName, this);
527                }
528
529                @Override
530                public void addTask(BaseTask theTask) {
531                        if (theTask instanceof AddColumnTask) {
532                                myTask.addAddColumnTask((AddColumnTask) theTask);
533                        } else {
534                                super.addTask(theTask);
535                        }
536                }
537
538                public BuilderAddTableByColumns failureAllowed() {
539                        myTask.setFailureAllowed(true);
540                        return this;
541                }
542        }
543
544}