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