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.Collections; 050import java.util.HashMap; 051import java.util.List; 052import java.util.Map; 053import java.util.Set; 054import java.util.stream.Collectors; 055 056public class Builder { 057 058 private final String myRelease; 059 private final BaseMigrationTasks.IAcceptsTasks mySink; 060 061 public Builder(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink) { 062 myRelease = theRelease; 063 mySink = theSink; 064 } 065 066 public BuilderWithTableName onTable(String theTableName) { 067 return new BuilderWithTableName(myRelease, mySink, theTableName); 068 } 069 070 public void addTask(BaseTask theTask) { 071 mySink.addTask(theTask); 072 } 073 074 public BuilderAddTableRawSql addTableRawSql(String theVersion, String theTableName) { 075 return new BuilderAddTableRawSql(theVersion, theTableName); 076 } 077 078 public BuilderCompleteTask executeRawSql(String theVersion, @Language("SQL") String theSql) { 079 ExecuteRawSqlTask task = executeRawSqlOptional(false, theVersion, theSql); 080 return new BuilderCompleteTask(task); 081 } 082 083 public void executeRawSqlStub(String theVersion, @Language("SQL") String theSql) { 084 executeRawSqlOptional(true, theVersion, theSql); 085 } 086 087 private ExecuteRawSqlTask executeRawSqlOptional(boolean theDoNothing, String theVersion, @Language("SQL") String theSql) { 088 ExecuteRawSqlTask task = new ExecuteRawSqlTask(myRelease, theVersion).addSql(theSql); 089 task.setDoNothing(theDoNothing); 090 mySink.addTask(task); 091 return task; 092 } 093 094 public Builder initializeSchema(String theVersion, ISchemaInitializationProvider theSchemaInitializationProvider) { 095 mySink.addTask(new InitializeSchemaTask(myRelease, theVersion, theSchemaInitializationProvider)); 096 return this; 097 } 098 099 @SuppressWarnings("unused") 100 public Builder initializeSchema(String theVersion, String theSchemaName, ISchemaInitializationProvider theSchemaInitializationProvider) { 101 InitializeSchemaTask task = new InitializeSchemaTask(myRelease, theVersion, theSchemaInitializationProvider); 102 task.setDescription("Initialize " + theSchemaName + " schema"); 103 mySink.addTask(task); 104 return this; 105 } 106 107 public Builder executeRawSql(String theVersion, DriverTypeEnum theDriver, @Language("SQL") String theSql) { 108 mySink.addTask(new ExecuteRawSqlTask(myRelease, theVersion).addSql(theDriver, theSql)); 109 return this; 110 } 111 112 /** 113 * Builder method to define a raw SQL execution migration that needs to take place against multiple database types, 114 * and the SQL they need to use is not equal. Provide a map of driver types to SQL statements. 115 * 116 * @param theVersion The version of the migration. 117 * @param theDriverToSql Map of driver types to SQL statements. 118 * @return 119 */ 120 public Builder executeRawSql(String theVersion, Map<DriverTypeEnum, String> theDriverToSql) { 121 Map<DriverTypeEnum, List<String>> singleSqlStatementMap = new HashMap<>(); 122 theDriverToSql.entrySet().stream() 123 .forEach(entry -> { 124 singleSqlStatementMap.put(entry.getKey(), Collections.singletonList(entry.getValue())); 125 }); 126 return executeRawSqls(theVersion, singleSqlStatementMap); 127 } 128 129 /** 130 * Builder method to define a raw SQL execution migration that needs to take place against multiple database types, 131 * and the SQL they need to use is not equal, and there are multiple sql commands for a given database. 132 * Provide a map of driver types to list of SQL statements. 133 * 134 * @param theVersion The version of the migration. 135 * @param theDriverToSqls Map of driver types to list of SQL statements. 136 * @return 137 */ 138 public Builder executeRawSqls(String theVersion, Map<DriverTypeEnum, List<String>> theDriverToSqls) { 139 ExecuteRawSqlTask executeRawSqlTask = new ExecuteRawSqlTask(myRelease, theVersion); 140 theDriverToSqls.entrySet().stream() 141 .forEach(entry -> { 142 entry.getValue().forEach(sql -> executeRawSqlTask.addSql(entry.getKey(), sql)); 143 }); 144 mySink.addTask(executeRawSqlTask); 145 return this; 146 } 147 148 // Flyway doesn't support these kinds of migrations 149 @Deprecated 150 public Builder startSectionWithMessage(String theMessage) { 151 // Do nothing 152 return this; 153 } 154 155 public BuilderAddTableByColumns addTableByColumns(String theVersion, String theTableName, String... thePkColumnNames) { 156 return new BuilderAddTableByColumns(myRelease, theVersion, mySink, theTableName, Arrays.asList(thePkColumnNames)); 157 } 158 159 public void addIdGenerator(String theVersion, String theGeneratorName) { 160 AddIdGeneratorTask task = new AddIdGeneratorTask(myRelease, theVersion, theGeneratorName); 161 addTask(task); 162 } 163 164 public void dropIdGenerator(String theVersion, String theIdGeneratorName) { 165 DropIdGeneratorTask task = new DropIdGeneratorTask(myRelease, theVersion, theIdGeneratorName); 166 addTask(task); 167 } 168 169 public void addNop(String theVersion) { 170 addTask(new NopTask(myRelease, theVersion)); 171 } 172 173 public static class BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks { 174 private final String myRelease; 175 private final BaseMigrationTasks.IAcceptsTasks mySink; 176 private final String myTableName; 177 178 public BuilderWithTableName(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink, String theTableName) { 179 myRelease = theRelease; 180 mySink = theSink; 181 myTableName = theTableName; 182 } 183 184 public String getTableName() { 185 return myTableName; 186 } 187 188 public BuilderCompleteTask dropIndex(String theVersion, String theIndexName) { 189 BaseTask task = dropIndexOptional(false, theVersion, theIndexName); 190 return new BuilderCompleteTask(task); 191 } 192 193 public void dropIndexStub(String theVersion, String theIndexName) { 194 dropIndexOptional(true, theVersion, theIndexName); 195 } 196 197 private DropIndexTask dropIndexOptional(boolean theDoNothing, String theVersion, String theIndexName) { 198 DropIndexTask task = new DropIndexTask(myRelease, theVersion); 199 task.setIndexName(theIndexName); 200 task.setTableName(myTableName); 201 task.setDoNothing(theDoNothing); 202 addTask(task); 203 return task; 204 } 205 206 /** 207 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong 208 */ 209 @Deprecated 210 public void renameIndex(String theVersion, String theOldIndexName, String theNewIndexName) { 211 renameIndexOptional(false, theVersion, theOldIndexName, theNewIndexName); 212 } 213 214 /** 215 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong 216 */ 217 public void renameIndexStub(String theVersion, String theOldIndexName, String theNewIndexName) { 218 renameIndexOptional(true, theVersion, theOldIndexName, theNewIndexName); 219 } 220 221 private void renameIndexOptional(boolean theDoNothing, String theVersion, String theOldIndexName, String theNewIndexName) { 222 RenameIndexTask task = new RenameIndexTask(myRelease, theVersion); 223 task.setOldIndexName(theOldIndexName); 224 task.setNewIndexName(theNewIndexName); 225 task.setTableName(myTableName); 226 task.setDoNothing(theDoNothing); 227 addTask(task); 228 } 229 230 public void dropThisTable(String theVersion) { 231 DropTableTask task = new DropTableTask(myRelease, theVersion); 232 task.setTableName(myTableName); 233 addTask(task); 234 } 235 236 public BuilderWithTableName.BuilderAddIndexWithName addIndex(String theVersion, String theIndexName) { 237 return new BuilderWithTableName.BuilderAddIndexWithName(theVersion, theIndexName); 238 } 239 240 public BuilderWithTableName.BuilderAddColumnWithName addColumn(String theVersion, String theColumnName) { 241 return new BuilderWithTableName.BuilderAddColumnWithName(myRelease, theVersion, theColumnName, this); 242 } 243 244 public BuilderCompleteTask dropColumn(String theVersion, String theColumnName) { 245 Validate.notBlank(theColumnName); 246 DropColumnTask task = new DropColumnTask(myRelease, theVersion); 247 task.setTableName(myTableName); 248 task.setColumnName(theColumnName); 249 addTask(task); 250 return new BuilderCompleteTask(task); 251 } 252 253 @Override 254 public void addTask(BaseTask theTask) { 255 ((BaseTableTask) theTask).setTableName(myTableName); 256 mySink.addTask(theTask); 257 } 258 259 public BuilderWithTableName.BuilderModifyColumnWithName modifyColumn(String theVersion, String theColumnName) { 260 return new BuilderWithTableName.BuilderModifyColumnWithName(theVersion, theColumnName); 261 } 262 263 public BuilderWithTableName.BuilderAddForeignKey addForeignKey(String theVersion, String theForeignKeyName) { 264 return new BuilderWithTableName.BuilderAddForeignKey(theVersion, theForeignKeyName); 265 } 266 267 public BuilderWithTableName renameColumn(String theVersion, String theOldName, String theNewName) { 268 return renameColumn(theVersion, theOldName, theNewName, false, false); 269 } 270 271 /** 272 * @param theOldName The old column name 273 * @param theNewName The new column name 274 * @param isOkayIfNeitherColumnExists Setting this to true means that it's not an error if neither column exists 275 * @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. 276 */ 277 public BuilderWithTableName renameColumn(String theVersion, String theOldName, String theNewName, boolean isOkayIfNeitherColumnExists, boolean theDeleteTargetColumnFirstIfBothExist) { 278 RenameColumnTask task = new RenameColumnTask(myRelease, theVersion); 279 task.setTableName(myTableName); 280 task.setOldName(theOldName); 281 task.setNewName(theNewName); 282 task.setOkayIfNeitherColumnExists(isOkayIfNeitherColumnExists); 283 task.setDeleteTargetColumnFirstIfBothExist(theDeleteTargetColumnFirstIfBothExist); 284 addTask(task); 285 return this; 286 } 287 288 /** 289 * @param theFkName the name of the foreign key 290 * @param theParentTableName the name of the table that exports the foreign key 291 */ 292 public void dropForeignKey(String theVersion, String theFkName, String theParentTableName) { 293 DropForeignKeyTask task = new DropForeignKeyTask(myRelease, theVersion); 294 task.setConstraintName(theFkName); 295 task.setTableName(getTableName()); 296 task.setParentTableName(theParentTableName); 297 addTask(task); 298 } 299 300 public void migratePostgresTextClobToBinaryClob(String theVersion, String theColumnName) { 301 MigratePostgresTextClobToBinaryClobTask task = new MigratePostgresTextClobToBinaryClobTask(myRelease, theVersion); 302 task.setTableName(getTableName()); 303 task.setColumnName(theColumnName); 304 addTask(task); 305 } 306 307 public class BuilderAddIndexWithName { 308 private final String myVersion; 309 private final String myIndexName; 310 311 public BuilderAddIndexWithName(String theVersion, String theIndexName) { 312 myVersion = theVersion; 313 myIndexName = theIndexName; 314 } 315 316 public BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique unique(boolean theUnique) { 317 return new BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique(myVersion, theUnique); 318 } 319 320 public class BuilderAddIndexUnique { 321 private final String myVersion; 322 private final boolean myUnique; 323 private String[] myIncludeColumns; 324 325 public BuilderAddIndexUnique(String theVersion, boolean theUnique) { 326 myVersion = theVersion; 327 myUnique = theUnique; 328 } 329 330 public void withColumnsStub(String... theColumnNames) { 331 withColumnsOptional(true, theColumnNames); 332 } 333 334 public BuilderCompleteTask withColumns(String... theColumnNames) { 335 BaseTask task = withColumnsOptional(false, theColumnNames); 336 return new BuilderCompleteTask(task); 337 } 338 339 private AddIndexTask withColumnsOptional(boolean theDoNothing, String... theColumnNames) { 340 AddIndexTask task = new AddIndexTask(myRelease, myVersion); 341 task.setTableName(myTableName); 342 task.setIndexName(myIndexName); 343 task.setUnique(myUnique); 344 task.setColumns(theColumnNames); 345 task.setDoNothing(theDoNothing); 346 if (myIncludeColumns != null) { 347 task.setIncludeColumns(myIncludeColumns); 348 } 349 addTask(task); 350 return task; 351 } 352 353 public BuilderAddIndexUnique includeColumns(String... theIncludeColumns) { 354 myIncludeColumns = theIncludeColumns; 355 return this; 356 } 357 } 358 } 359 360 public class BuilderModifyColumnWithName { 361 private final String myVersion; 362 private final String myColumnName; 363 364 public BuilderModifyColumnWithName(String theVersion, String theColumnName) { 365 myVersion = theVersion; 366 myColumnName = theColumnName; 367 } 368 369 public String getColumnName() { 370 return myColumnName; 371 } 372 373 public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nullable() { 374 return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(myVersion, true); 375 } 376 377 public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nonNullable() { 378 return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable(myVersion, false); 379 } 380 381 public class BuilderModifyColumnWithNameAndNullable { 382 private final String myVersion; 383 private final boolean myNullable; 384 private boolean myFailureAllowed; 385 386 public BuilderModifyColumnWithNameAndNullable(String theVersion, boolean theNullable) { 387 myVersion = theVersion; 388 myNullable = theNullable; 389 } 390 391 public void withType(ColumnTypeEnum theColumnType) { 392 withType(theColumnType, null); 393 } 394 395 public void withType(ColumnTypeEnum theColumnType, Integer theLength) { 396 if (theColumnType == ColumnTypeEnum.STRING) { 397 if (theLength == null || theLength == 0) { 398 throw new IllegalArgumentException("Can not specify length 0 for column of type " + theColumnType); 399 } 400 } else { 401 if (theLength != null) { 402 throw new IllegalArgumentException("Can not specify length for column of type " + theColumnType); 403 } 404 } 405 406 ModifyColumnTask task = new ModifyColumnTask(myRelease, myVersion); 407 task.setColumnName(myColumnName); 408 task.setTableName(myTableName); 409 if (theLength != null) { 410 task.setColumnLength(theLength); 411 } 412 task.setNullable(myNullable); 413 task.setColumnType(theColumnType); 414 task.setFailureAllowed(myFailureAllowed); 415 addTask(task); 416 } 417 418 public BuilderModifyColumnWithNameAndNullable failureAllowed() { 419 myFailureAllowed = true; 420 return this; 421 } 422 } 423 } 424 425 public class BuilderAddForeignKey { 426 private final String myVersion; 427 private final String myForeignKeyName; 428 429 public BuilderAddForeignKey(String theVersion, String theForeignKeyName) { 430 myVersion = theVersion; 431 myForeignKeyName = theForeignKeyName; 432 } 433 434 public BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn toColumn(String theColumnName) { 435 return new BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn(myVersion, theColumnName); 436 } 437 438 public class BuilderAddForeignKeyToColumn extends BuilderWithTableName.BuilderModifyColumnWithName { 439 public BuilderAddForeignKeyToColumn(String theVersion, String theColumnName) { 440 super(theVersion, theColumnName); 441 } 442 443 public BuilderCompleteTask references(String theForeignTable, String theForeignColumn) { 444 AddForeignKeyTask task = new AddForeignKeyTask(myRelease, myVersion); 445 task.setTableName(myTableName); 446 task.setConstraintName(myForeignKeyName); 447 task.setColumnName(getColumnName()); 448 task.setForeignTableName(theForeignTable); 449 task.setForeignColumnName(theForeignColumn); 450 addTask(task); 451 return new BuilderCompleteTask(task); 452 } 453 } 454 } 455 456 public class BuilderAddColumnWithName { 457 private final String myRelease; 458 private final String myVersion; 459 private final String myColumnName; 460 private final BaseMigrationTasks.IAcceptsTasks myTaskSink; 461 462 public BuilderAddColumnWithName(String theRelease, String theVersion, String theColumnName, BaseMigrationTasks.IAcceptsTasks theTaskSink) { 463 myRelease = theRelease; 464 myVersion = theVersion; 465 myColumnName = theColumnName; 466 myTaskSink = theTaskSink; 467 } 468 469 public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nullable() { 470 return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(myRelease, myVersion, true); 471 } 472 473 public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nonNullable() { 474 return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable(myRelease, myVersion, false); 475 } 476 477 public class BuilderAddColumnWithNameNullable { 478 private final boolean myNullable; 479 private final String myRelease; 480 private final String myVersion; 481 482 public BuilderAddColumnWithNameNullable(String theRelease, String theVersion, boolean theNullable) { 483 myRelease = theRelease; 484 myVersion = theVersion; 485 myNullable = theNullable; 486 } 487 488 public BuilderCompleteTask type(ColumnTypeEnum theColumnType) { 489 return type(theColumnType, null); 490 } 491 492 public BuilderCompleteTask type(ColumnTypeEnum theColumnType, Integer theLength) { 493 AddColumnTask task = new AddColumnTask(myRelease, myVersion); 494 task.setColumnName(myColumnName); 495 task.setNullable(myNullable); 496 task.setColumnType(theColumnType); 497 if (theLength != null) { 498 task.setColumnLength(theLength); 499 } 500 myTaskSink.addTask(task); 501 502 return new BuilderCompleteTask(task); 503 } 504 505 } 506 } 507 } 508 509 public static class BuilderCompleteTask { 510 511 private final BaseTask myTask; 512 513 public BuilderCompleteTask(BaseTask theTask) { 514 myTask = theTask; 515 } 516 517 public BuilderCompleteTask failureAllowed() { 518 myTask.setFailureAllowed(true); 519 return this; 520 } 521 522 public BuilderCompleteTask doNothing() { 523 myTask.setDoNothing(true); 524 return this; 525 } 526 527 public BuilderCompleteTask onlyAppliesToPlatforms(DriverTypeEnum... theTypes) { 528 Set<DriverTypeEnum> typesSet = Arrays.stream(theTypes).collect(Collectors.toSet()); 529 myTask.setOnlyAppliesToPlatforms(typesSet); 530 return this; 531 } 532 533 public BuilderCompleteTask runEvenDuringSchemaInitialization() { 534 myTask.setRunDuringSchemaInitialization(true); 535 return this; 536 } 537 } 538 539 public class BuilderAddTableRawSql { 540 541 private final AddTableRawSqlTask myTask; 542 543 protected BuilderAddTableRawSql(String theVersion, String theTableName) { 544 myTask = new AddTableRawSqlTask(myRelease, theVersion); 545 myTask.setTableName(theTableName); 546 addTask(myTask); 547 } 548 549 550 public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) { 551 myTask.addSql(theDriverTypeEnum, theSql); 552 return this; 553 } 554 555 public void addSql(@Language("SQL") String theSql) { 556 myTask.addSql(theSql); 557 } 558 } 559 560 public class BuilderAddTableByColumns extends BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks { 561 private final String myVersion; 562 private final AddTableByColumnTask myTask; 563 564 public BuilderAddTableByColumns(String theRelease, String theVersion, BaseMigrationTasks.IAcceptsTasks theSink, String theTableName, List<String> thePkColumnNames) { 565 super(theRelease, theSink, theTableName); 566 myVersion = theVersion; 567 myTask = new AddTableByColumnTask(myRelease, theVersion); 568 myTask.setTableName(theTableName); 569 myTask.setPkColumns(thePkColumnNames); 570 theSink.addTask(myTask); 571 } 572 573 public BuilderAddColumnWithName addColumn(String theColumnName) { 574 return new BuilderAddColumnWithName(myRelease, myVersion, theColumnName, this); 575 } 576 577 @Override 578 public void addTask(BaseTask theTask) { 579 if (theTask instanceof AddColumnTask) { 580 myTask.addAddColumnTask((AddColumnTask) theTask); 581 } else { 582 super.addTask(theTask); 583 } 584 } 585 586 public BuilderAddTableByColumns failureAllowed() { 587 myTask.setFailureAllowed(true); 588 return this; 589 } 590 } 591 592}