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