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}