001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2025 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 BuilderCompleteTask 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 return new BuilderCompleteTask(task); 344 } 345 346 public BuilderCompleteTask renameTable(String theVersion, String theNewTableName) { 347 RenameTableTask task = new RenameTableTask(myRelease, theVersion, getTableName(), theNewTableName); 348 addTask(task); 349 return new BuilderCompleteTask(task); 350 } 351 352 public BuilderCompleteTask migratePostgresTextClobToBinaryClob(String theVersion, String theColumnName) { 353 MigratePostgresTextClobToBinaryClobTask task = 354 new MigratePostgresTextClobToBinaryClobTask(myRelease, theVersion); 355 task.setTableName(getTableName()); 356 task.setColumnName(theColumnName); 357 addTask(task); 358 return new BuilderCompleteTask(task); 359 } 360 361 public BuilderCompleteTask migrateBlobToBinary( 362 String theVersion, String theFromColumName, String theToColumName) { 363 MigrateColumBlobTypeToBinaryTypeTask task = new MigrateColumBlobTypeToBinaryTypeTask( 364 myRelease, theVersion, getTableName(), theFromColumName, theToColumName); 365 366 addTask(task); 367 return new BuilderCompleteTask(task); 368 } 369 370 public BuilderCompleteTask migrateClobToText( 371 String theVersion, String theFromColumName, String theToColumName) { 372 MigrateColumnClobTypeToTextTypeTask task = new MigrateColumnClobTypeToTextTypeTask( 373 myRelease, theVersion, getTableName(), theFromColumName, theToColumName); 374 375 addTask(task); 376 return new BuilderCompleteTask(task); 377 } 378 379 public void dropPrimaryKey(String theVersion) { 380 final DropPrimaryKeyTask task = new DropPrimaryKeyTask(myRelease, theVersion, myTableName); 381 addTask(task); 382 } 383 384 public class BuilderAddIndexWithName { 385 private final String myVersion; 386 private final String myIndexName; 387 388 public BuilderAddIndexWithName(String theVersion, String theIndexName) { 389 myVersion = theVersion; 390 myIndexName = theIndexName; 391 } 392 393 public BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique unique(boolean theUnique) { 394 return new BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique(myVersion, theUnique); 395 } 396 397 public class BuilderAddIndexUnique { 398 private final String myVersion; 399 private final boolean myUnique; 400 private String[] myIncludeColumns; 401 private boolean myOnline = true; 402 403 public BuilderAddIndexUnique(String theVersion, boolean theUnique) { 404 myVersion = theVersion; 405 myUnique = theUnique; 406 } 407 408 public void withColumnsStub(String... theColumnNames) { 409 BuilderCompleteTask task = withColumns(theColumnNames); 410 task.withFlag(TaskFlagEnum.DO_NOTHING); 411 } 412 413 public BuilderCompleteTask withColumns(String... theColumnNames) { 414 AddIndexTask task = new AddIndexTask(myRelease, myVersion); 415 task.setTableName(myTableName); 416 task.setIndexName(myIndexName); 417 task.setUnique(myUnique); 418 task.setColumns(theColumnNames); 419 task.setOnline(myOnline); 420 if (myIncludeColumns != null) { 421 task.setIncludeColumns(myIncludeColumns); 422 } 423 addTask(task); 424 return new BuilderCompleteTask(task); 425 } 426 427 /** 428 * 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. 429 */ 430 public BuilderCompleteTask withPossibleNullableColumns(ColumnAndNullable... theColumns) { 431 String[] columnNames = Arrays.stream(theColumns) 432 .map(ColumnAndNullable::getColumnName) 433 .toArray(String[]::new); 434 String[] nullableColumnNames = Arrays.stream(theColumns) 435 .filter(ColumnAndNullable::isNullable) 436 .map(ColumnAndNullable::getColumnName) 437 .toArray(String[]::new); 438 AddIndexTask task = new AddIndexTask(myRelease, myVersion); 439 task.setTableName(myTableName); 440 task.setIndexName(myIndexName); 441 task.setUnique(myUnique); 442 task.setColumns(columnNames); 443 task.setNullableColumns(nullableColumnNames); 444 task.setOnline(myOnline); 445 if (myIncludeColumns != null) { 446 task.setIncludeColumns(myIncludeColumns); 447 } 448 addTask(task); 449 return new BuilderCompleteTask(task); 450 } 451 452 public BuilderAddIndexUnique includeColumns(String... theIncludeColumns) { 453 myIncludeColumns = theIncludeColumns; 454 return this; 455 } 456 457 /** 458 * Add the index without locking the table. 459 */ 460 public BuilderAddIndexUnique online(boolean theOnlineFlag) { 461 myOnline = theOnlineFlag; 462 return this; 463 } 464 } 465 } 466 467 public class BuilderModifyColumnWithName { 468 private final String myVersion; 469 private final String myColumnName; 470 471 public BuilderModifyColumnWithName(String theVersion, String theColumnName) { 472 myVersion = theVersion; 473 myColumnName = theColumnName; 474 } 475 476 public String getColumnName() { 477 return myColumnName; 478 } 479 480 public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nullable() { 481 return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable( 482 myVersion, true); 483 } 484 485 public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable 486 nonNullable() { 487 return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable( 488 myVersion, false); 489 } 490 491 public class BuilderModifyColumnWithNameAndNullable { 492 private final String myVersion; 493 private final boolean myNullable; 494 495 public BuilderModifyColumnWithNameAndNullable(String theVersion, boolean theNullable) { 496 myVersion = theVersion; 497 myNullable = theNullable; 498 } 499 500 public BuilderCompleteTask withType(ColumnTypeEnum theColumnType) { 501 return withType(theColumnType, null); 502 } 503 504 public BuilderCompleteTask withType(ColumnTypeEnum theColumnType, Integer theLength) { 505 if (theColumnType == ColumnTypeEnum.STRING) { 506 if (theLength == null || theLength == 0) { 507 throw new IllegalArgumentException( 508 Msg.code(52) + "Can not specify length 0 for column of type " + theColumnType); 509 } 510 } else { 511 if (theLength != null) { 512 throw new IllegalArgumentException( 513 Msg.code(53) + "Can not specify length for column of type " + theColumnType); 514 } 515 } 516 517 ModifyColumnTask task = new ModifyColumnTask(myRelease, myVersion); 518 519 task.setColumnName(myColumnName); 520 task.setTableName(myTableName); 521 if (theLength != null) { 522 task.setColumnLength(theLength); 523 } 524 task.setNullable(myNullable); 525 task.setColumnType(theColumnType); 526 addTask(task); 527 return new BuilderCompleteTask(task); 528 } 529 } 530 } 531 532 public class BuilderAddForeignKey { 533 private final String myVersion; 534 private final String myForeignKeyName; 535 536 public BuilderAddForeignKey(String theVersion, String theForeignKeyName) { 537 myVersion = theVersion; 538 myForeignKeyName = theForeignKeyName; 539 } 540 541 public BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn toColumn( 542 String theColumnName) { 543 return new BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn( 544 myVersion, theColumnName); 545 } 546 547 public class BuilderAddForeignKeyToColumn extends BuilderWithTableName.BuilderModifyColumnWithName { 548 public BuilderAddForeignKeyToColumn(String theVersion, String theColumnName) { 549 super(theVersion, theColumnName); 550 } 551 552 public BuilderCompleteTask references(String theForeignTable, String theForeignColumn) { 553 AddForeignKeyTask task = new AddForeignKeyTask(myRelease, myVersion); 554 task.setTableName(myTableName); 555 task.setConstraintName(myForeignKeyName); 556 task.setColumnName(getColumnName()); 557 task.setForeignTableName(theForeignTable); 558 task.setForeignColumnName(theForeignColumn); 559 addTask(task); 560 return new BuilderCompleteTask(task); 561 } 562 } 563 } 564 565 public static class BuilderAddColumnWithName { 566 private final String myRelease; 567 private final String myVersion; 568 private final String myColumnName; 569 570 @Nullable 571 private final Object myDefaultValue; 572 573 private final BaseMigrationTasks.IAcceptsTasks myTaskSink; 574 575 public BuilderAddColumnWithName( 576 String theRelease, 577 String theVersion, 578 String theColumnName, 579 @Nullable Object theDefaultValue, 580 BaseMigrationTasks.IAcceptsTasks theTaskSink) { 581 myRelease = theRelease; 582 myVersion = theVersion; 583 myColumnName = theColumnName; 584 myDefaultValue = theDefaultValue; 585 myTaskSink = theTaskSink; 586 } 587 588 public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nullable() { 589 return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable( 590 myRelease, myVersion, true); 591 } 592 593 public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nonNullable() { 594 return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable( 595 myRelease, myVersion, false); 596 } 597 598 public class BuilderAddColumnWithNameNullable { 599 private final boolean myNullable; 600 private final String myRelease; 601 private final String myVersion; 602 603 public BuilderAddColumnWithNameNullable(String theRelease, String theVersion, boolean theNullable) { 604 myRelease = theRelease; 605 myVersion = theVersion; 606 myNullable = theNullable; 607 } 608 609 public BuilderCompleteTask type(ColumnTypeEnum theColumnType) { 610 return type(theColumnType, null); 611 } 612 613 public BuilderCompleteTask type(ColumnTypeEnum theColumnType, Integer theLength) { 614 AddColumnTask task = new AddColumnTask(myRelease, myVersion); 615 task.setColumnName(myColumnName); 616 task.setNullable(myNullable); 617 task.setColumnType(theColumnType); 618 if (theLength != null) { 619 task.setColumnLength(theLength); 620 } 621 task.setDefaultValue(myDefaultValue); 622 myTaskSink.addTask(task); 623 624 return new BuilderCompleteTask(task); 625 } 626 } 627 } 628 } 629 630 public static class BuilderCompleteTask { 631 632 private final BaseTask myTask; 633 634 public BuilderCompleteTask(BaseTask theTask) { 635 myTask = theTask; 636 } 637 638 public BuilderCompleteTask failureAllowed() { 639 myTask.addFlag(TaskFlagEnum.FAILURE_ALLOWED); 640 return this; 641 } 642 643 public BuilderCompleteTask doNothing() { 644 myTask.addFlag(TaskFlagEnum.DO_NOTHING); 645 return this; 646 } 647 648 public BuilderCompleteTask onlyAppliesToPlatforms(DriverTypeEnum... theTypes) { 649 Set<DriverTypeEnum> typesSet = Arrays.stream(theTypes).collect(Collectors.toSet()); 650 myTask.setOnlyAppliesToPlatforms(typesSet); 651 return this; 652 } 653 654 /** 655 * Introduce precondition checking logic into the execution of the enclosed task. This conditional logic will 656 * be implemented by running an SQL SELECT (including CTEs) to obtain a boolean indicating whether a certain 657 * condition has been met. 658 * One example is to check for a specific collation on a column to decide whether to create a new index. 659 * <p/> 660 * This method may be called multiple times to add multiple preconditions. The precondition that evaluates to 661 * false will stop execution of the task irrespective of any or all other tasks evaluating to true. 662 * 663 * @param theSql The SELECT or CTE used to determine if the precondition is valid. 664 * @param reason A String to indicate the text that is logged if the precondition is not met. 665 * @return The BuilderCompleteTask in order to chain further method calls on this builder. 666 */ 667 public BuilderCompleteTask onlyIf(@Language("SQL") String theSql, String reason) { 668 if (!theSql.toUpperCase().startsWith("WITH") 669 && !theSql.toUpperCase().startsWith("SELECT")) { 670 throw new IllegalArgumentException(Msg.code(2455) 671 + String.format( 672 "Only SELECT statements (including CTEs) are allowed here. Please check your SQL: [%s]", 673 theSql)); 674 } 675 ourLog.debug("SQL to evaluate: {}", theSql); 676 677 myTask.addPrecondition(new ExecuteTaskPrecondition( 678 () -> { 679 ourLog.debug("Checking precondition for SQL: {}", theSql); 680 return MigrationJdbcUtils.queryForSingleBooleanResultMultipleThrowsException( 681 theSql, myTask.newJdbcTemplate()); 682 }, 683 reason)); 684 685 return this; 686 } 687 688 public BuilderCompleteTask runEvenDuringSchemaInitialization() { 689 myTask.addFlag(TaskFlagEnum.RUN_DURING_SCHEMA_INITIALIZATION); 690 return this; 691 } 692 693 public BuilderCompleteTask setTransactional(boolean theFlag) { 694 myTask.setTransactional(theFlag); 695 return this; 696 } 697 698 public BuilderCompleteTask heavyweightSkipByDefault() { 699 myTask.addFlag(TaskFlagEnum.HEAVYWEIGHT_SKIP_BY_DEFAULT); 700 return this; 701 } 702 703 public BuilderCompleteTask withFlag(TaskFlagEnum theFlag) { 704 myTask.addFlag(theFlag); 705 return this; 706 } 707 } 708 709 public class BuilderAddTableRawSql { 710 711 private final AddTableRawSqlTask myTask; 712 713 protected BuilderAddTableRawSql(String theVersion, String theTableName) { 714 myTask = new AddTableRawSqlTask(myRelease, theVersion); 715 myTask.setTableName(theTableName); 716 addTask(myTask); 717 } 718 719 public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) { 720 myTask.addSql(theDriverTypeEnum, theSql); 721 return this; 722 } 723 724 public void addSql(@Language("SQL") String theSql) { 725 myTask.addSql(theSql); 726 } 727 } 728 729 public class BuilderAddTableByColumns extends BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks { 730 private final String myVersion; 731 private final AddTableByColumnTask myTask; 732 733 public BuilderAddTableByColumns( 734 String theRelease, 735 String theVersion, 736 BaseMigrationTasks.IAcceptsTasks theSink, 737 String theTableName, 738 List<String> thePkColumnNames) { 739 super(theRelease, theSink, theTableName); 740 myVersion = theVersion; 741 myTask = new AddTableByColumnTask(myRelease, theVersion); 742 myTask.setTableName(theTableName); 743 myTask.setPkColumns(thePkColumnNames); 744 theSink.addTask(myTask); 745 } 746 747 public BuilderAddColumnWithName addColumn(String theColumnName) { 748 return new BuilderAddColumnWithName(myRelease, myVersion, theColumnName, null, this); 749 } 750 751 @Override 752 public void addTask(BaseTask theTask) { 753 if (theTask instanceof AddColumnTask) { 754 myTask.addAddColumnTask((AddColumnTask) theTask); 755 } else { 756 super.addTask(theTask); 757 } 758 } 759 760 public BuilderCompleteTask withFlags() { 761 return new BuilderCompleteTask(myTask); 762 } 763 } 764 765 public String getRelease() { 766 return myRelease; 767 } 768}