001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2026 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 void addIdGenerator(String theVersion, String theGeneratorName, Integer theIncrement) { 178 AddIdGeneratorTask task = new AddIdGeneratorTask(myRelease, theVersion, theGeneratorName, theIncrement); 179 addTask(task); 180 } 181 182 public BuilderCompleteTask dropIdGenerator(String theVersion, String theIdGeneratorName) { 183 DropIdGeneratorTask task = new DropIdGeneratorTask(myRelease, theVersion, theIdGeneratorName); 184 addTask(task); 185 return new BuilderCompleteTask(task); 186 } 187 188 public void addNop(String theVersion) { 189 addTask(new NopTask(myRelease, theVersion)); 190 } 191 192 public static class BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks { 193 private final String myRelease; 194 private final BaseMigrationTasks.IAcceptsTasks mySink; 195 private final String myTableName; 196 private BaseTask myLastAddedTask; 197 198 public BuilderWithTableName(String theRelease, BaseMigrationTasks.IAcceptsTasks theSink, String theTableName) { 199 myRelease = theRelease; 200 mySink = theSink; 201 myTableName = theTableName; 202 } 203 204 public String getTableName() { 205 return myTableName; 206 } 207 208 public BuilderCompleteTask dropIndex(String theVersion, String theIndexName) { 209 BaseTask task = dropIndexOptional(theVersion, theIndexName); 210 return new BuilderCompleteTask(task); 211 } 212 213 /** 214 * Drop index without taking write lock on PG, Oracle, MSSQL. 215 */ 216 public BuilderCompleteTask dropIndexOnline(String theVersion, String theIndexName) { 217 DropIndexTask task = dropIndexOptional(theVersion, theIndexName); 218 task.setOnline(true); 219 return new BuilderCompleteTask(task); 220 } 221 222 public void dropIndexStub(String theVersion, String theIndexName) { 223 DropIndexTask task = dropIndexOptional(theVersion, theIndexName); 224 task.addFlag(TaskFlagEnum.DO_NOTHING); 225 } 226 227 private DropIndexTask dropIndexOptional(String theVersion, String theIndexName) { 228 DropIndexTask task = new DropIndexTask(myRelease, theVersion); 229 task.setIndexName(theIndexName); 230 task.setTableName(myTableName); 231 addTask(task); 232 return task; 233 } 234 235 /** 236 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong 237 */ 238 @Deprecated 239 public void renameIndex(String theVersion, String theOldIndexName, String theNewIndexName) { 240 renameIndexOptional(theVersion, theOldIndexName, theNewIndexName); 241 } 242 243 /** 244 * @deprecated Do not rename indexes - It is too hard to figure out what happened if something goes wrong 245 */ 246 public void renameIndexStub(String theVersion, String theOldIndexName, String theNewIndexName) { 247 RenameIndexTask task = renameIndexOptional(theVersion, theOldIndexName, theNewIndexName); 248 task.addFlag(TaskFlagEnum.DO_NOTHING); 249 } 250 251 private RenameIndexTask renameIndexOptional(String theVersion, String theOldIndexName, String theNewIndexName) { 252 RenameIndexTask task = new RenameIndexTask(myRelease, theVersion); 253 task.setOldIndexName(theOldIndexName); 254 task.setNewIndexName(theNewIndexName); 255 task.setTableName(myTableName); 256 addTask(task); 257 return task; 258 } 259 260 public void dropThisTable(String theVersion) { 261 DropTableTask task = new DropTableTask(myRelease, theVersion); 262 task.setTableName(myTableName); 263 addTask(task); 264 } 265 266 public BuilderWithTableName.BuilderAddIndexWithName addIndex(String theVersion, String theIndexName) { 267 return new BuilderWithTableName.BuilderAddIndexWithName(theVersion, theIndexName); 268 } 269 270 public BuilderWithTableName.BuilderAddColumnWithName addColumn(String theVersion, String theColumnName) { 271 return new BuilderWithTableName.BuilderAddColumnWithName(myRelease, theVersion, theColumnName, null, this); 272 } 273 274 public BuilderWithTableName.BuilderAddColumnWithName addColumn( 275 String theVersion, String theColumnName, Object theDefaultValue) { 276 return new BuilderWithTableName.BuilderAddColumnWithName( 277 myRelease, theVersion, theColumnName, theDefaultValue, this); 278 } 279 280 public BuilderCompleteTask dropColumn(String theVersion, String theColumnName) { 281 Validate.notBlank(theColumnName); 282 DropColumnTask task = new DropColumnTask(myRelease, theVersion); 283 task.setTableName(myTableName); 284 task.setColumnName(theColumnName); 285 addTask(task); 286 return new BuilderCompleteTask(task); 287 } 288 289 @Override 290 public void addTask(BaseTask theTask) { 291 ((BaseTableTask) theTask).setTableName(myTableName); 292 myLastAddedTask = theTask; 293 mySink.addTask(theTask); 294 } 295 296 public BuilderWithTableName.BuilderModifyColumnWithName modifyColumn(String theVersion, String theColumnName) { 297 return new BuilderWithTableName.BuilderModifyColumnWithName(theVersion, theColumnName); 298 } 299 300 public BuilderWithTableName.BuilderAddForeignKey addForeignKey(String theVersion, String theForeignKeyName) { 301 return new BuilderWithTableName.BuilderAddForeignKey(theVersion, theForeignKeyName); 302 } 303 304 public BuilderWithTableName renameColumn(String theVersion, String theOldName, String theNewName) { 305 return renameColumn(theVersion, theOldName, theNewName, false, false); 306 } 307 308 /** 309 * @param theOldName The old column name 310 * @param theNewName The new column name 311 * @param isOkayIfNeitherColumnExists Setting this to true means that it's not an error if neither column exists 312 * @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. 313 */ 314 public BuilderWithTableName renameColumn( 315 String theVersion, 316 String theOldName, 317 String theNewName, 318 boolean isOkayIfNeitherColumnExists, 319 boolean theDeleteTargetColumnFirstIfBothExist) { 320 RenameColumnTask task = new RenameColumnTask(myRelease, theVersion); 321 task.setTableName(myTableName); 322 task.setOldName(theOldName); 323 task.setNewName(theNewName); 324 task.setOkayIfNeitherColumnExists(isOkayIfNeitherColumnExists); 325 task.setDeleteTargetColumnFirstIfBothExist(theDeleteTargetColumnFirstIfBothExist); 326 addTask(task); 327 return this; 328 } 329 330 public Optional<BaseTask> getLastAddedTask() { 331 return Optional.ofNullable(myLastAddedTask); 332 } 333 334 public void addPrimaryKey(String theVersion, String... theColumnsInOrder) { 335 addTask(new AddPrimaryKeyTask(myRelease, theVersion, myTableName, theColumnsInOrder)); 336 } 337 338 /** 339 * @param theFkName the name of the foreign key 340 * @param theParentTableName the name of the table that exports the foreign key 341 */ 342 public BuilderCompleteTask dropForeignKey(String theVersion, String theFkName, String theParentTableName) { 343 DropForeignKeyTask task = new DropForeignKeyTask(myRelease, theVersion); 344 task.setConstraintName(theFkName); 345 task.setTableName(getTableName()); 346 task.setParentTableName(theParentTableName); 347 addTask(task); 348 return new BuilderCompleteTask(task); 349 } 350 351 public BuilderCompleteTask renameTable(String theVersion, String theNewTableName) { 352 RenameTableTask task = new RenameTableTask(myRelease, theVersion, getTableName(), theNewTableName); 353 addTask(task); 354 return new BuilderCompleteTask(task); 355 } 356 357 public BuilderCompleteTask migratePostgresTextClobToBinaryClob(String theVersion, String theColumnName) { 358 MigratePostgresTextClobToBinaryClobTask task = 359 new MigratePostgresTextClobToBinaryClobTask(myRelease, theVersion); 360 task.setTableName(getTableName()); 361 task.setColumnName(theColumnName); 362 addTask(task); 363 return new BuilderCompleteTask(task); 364 } 365 366 public BuilderCompleteTask migrateBlobToBinary( 367 String theVersion, String theFromColumName, String theToColumName) { 368 MigrateColumBlobTypeToBinaryTypeTask task = new MigrateColumBlobTypeToBinaryTypeTask( 369 myRelease, theVersion, getTableName(), theFromColumName, theToColumName); 370 371 addTask(task); 372 return new BuilderCompleteTask(task); 373 } 374 375 public BuilderCompleteTask migrateClobToText( 376 String theVersion, String theFromColumName, String theToColumName) { 377 MigrateColumnClobTypeToTextTypeTask task = new MigrateColumnClobTypeToTextTypeTask( 378 myRelease, theVersion, getTableName(), theFromColumName, theToColumName); 379 380 addTask(task); 381 return new BuilderCompleteTask(task); 382 } 383 384 public void dropPrimaryKey(String theVersion) { 385 final DropPrimaryKeyTask task = new DropPrimaryKeyTask(myRelease, theVersion, myTableName); 386 addTask(task); 387 } 388 389 public class BuilderAddIndexWithName { 390 private final String myVersion; 391 private final String myIndexName; 392 393 public BuilderAddIndexWithName(String theVersion, String theIndexName) { 394 myVersion = theVersion; 395 myIndexName = theIndexName; 396 } 397 398 public BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique unique(boolean theUnique) { 399 return new BuilderWithTableName.BuilderAddIndexWithName.BuilderAddIndexUnique(myVersion, theUnique); 400 } 401 402 public class BuilderAddIndexUnique { 403 private final String myVersion; 404 private final boolean myUnique; 405 private String[] myIncludeColumns; 406 private boolean myOnline = true; 407 408 public BuilderAddIndexUnique(String theVersion, boolean theUnique) { 409 myVersion = theVersion; 410 myUnique = theUnique; 411 } 412 413 public void withColumnsStub(String... theColumnNames) { 414 BuilderCompleteTask task = withColumns(theColumnNames); 415 task.withFlag(TaskFlagEnum.DO_NOTHING); 416 } 417 418 public BuilderCompleteTask withColumns(String... theColumnNames) { 419 AddIndexTask task = new AddIndexTask(myRelease, myVersion); 420 task.setTableName(myTableName); 421 task.setIndexName(myIndexName); 422 task.setUnique(myUnique); 423 task.setColumns(theColumnNames); 424 task.setOnline(myOnline); 425 if (myIncludeColumns != null) { 426 task.setIncludeColumns(myIncludeColumns); 427 } 428 addTask(task); 429 return new BuilderCompleteTask(task); 430 } 431 432 /** 433 * 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. 434 */ 435 public BuilderCompleteTask withPossibleNullableColumns(ColumnAndNullable... theColumns) { 436 String[] columnNames = Arrays.stream(theColumns) 437 .map(ColumnAndNullable::getColumnName) 438 .toArray(String[]::new); 439 String[] nullableColumnNames = Arrays.stream(theColumns) 440 .filter(ColumnAndNullable::isNullable) 441 .map(ColumnAndNullable::getColumnName) 442 .toArray(String[]::new); 443 AddIndexTask task = new AddIndexTask(myRelease, myVersion); 444 task.setTableName(myTableName); 445 task.setIndexName(myIndexName); 446 task.setUnique(myUnique); 447 task.setColumns(columnNames); 448 task.setNullableColumns(nullableColumnNames); 449 task.setOnline(myOnline); 450 if (myIncludeColumns != null) { 451 task.setIncludeColumns(myIncludeColumns); 452 } 453 addTask(task); 454 return new BuilderCompleteTask(task); 455 } 456 457 public BuilderAddIndexUnique includeColumns(String... theIncludeColumns) { 458 myIncludeColumns = theIncludeColumns; 459 return this; 460 } 461 462 /** 463 * Add the index without locking the table. 464 */ 465 public BuilderAddIndexUnique online(boolean theOnlineFlag) { 466 myOnline = theOnlineFlag; 467 return this; 468 } 469 } 470 } 471 472 public class BuilderModifyColumnWithName { 473 private final String myVersion; 474 private final String myColumnName; 475 476 public BuilderModifyColumnWithName(String theVersion, String theColumnName) { 477 myVersion = theVersion; 478 myColumnName = theColumnName; 479 } 480 481 public String getColumnName() { 482 return myColumnName; 483 } 484 485 public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable nullable() { 486 return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable( 487 myVersion, true); 488 } 489 490 public BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable 491 nonNullable() { 492 return new BuilderWithTableName.BuilderModifyColumnWithName.BuilderModifyColumnWithNameAndNullable( 493 myVersion, false); 494 } 495 496 public class BuilderModifyColumnWithNameAndNullable { 497 private final String myVersion; 498 private final boolean myNullable; 499 500 public BuilderModifyColumnWithNameAndNullable(String theVersion, boolean theNullable) { 501 myVersion = theVersion; 502 myNullable = theNullable; 503 } 504 505 public BuilderCompleteTask withType(ColumnTypeEnum theColumnType) { 506 return withType(theColumnType, null); 507 } 508 509 public BuilderCompleteTask withType(ColumnTypeEnum theColumnType, Integer theLength) { 510 if (theColumnType == ColumnTypeEnum.STRING) { 511 if (theLength == null || theLength == 0) { 512 throw new IllegalArgumentException( 513 Msg.code(52) + "Can not specify length 0 for column of type " + theColumnType); 514 } 515 } else { 516 if (theLength != null) { 517 throw new IllegalArgumentException( 518 Msg.code(53) + "Can not specify length for column of type " + theColumnType); 519 } 520 } 521 522 ModifyColumnTask task = new ModifyColumnTask(myRelease, myVersion); 523 524 task.setColumnName(myColumnName); 525 task.setTableName(myTableName); 526 if (theLength != null) { 527 task.setColumnLength(theLength); 528 } 529 task.setNullable(myNullable); 530 task.setColumnType(theColumnType); 531 addTask(task); 532 return new BuilderCompleteTask(task); 533 } 534 } 535 } 536 537 public class BuilderAddForeignKey { 538 private final String myVersion; 539 private final String myForeignKeyName; 540 541 public BuilderAddForeignKey(String theVersion, String theForeignKeyName) { 542 myVersion = theVersion; 543 myForeignKeyName = theForeignKeyName; 544 } 545 546 public BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn toColumn( 547 String theColumnName) { 548 return new BuilderWithTableName.BuilderAddForeignKey.BuilderAddForeignKeyToColumn( 549 myVersion, theColumnName); 550 } 551 552 public class BuilderAddForeignKeyToColumn extends BuilderWithTableName.BuilderModifyColumnWithName { 553 public BuilderAddForeignKeyToColumn(String theVersion, String theColumnName) { 554 super(theVersion, theColumnName); 555 } 556 557 public BuilderCompleteTask references(String theForeignTable, String theForeignColumn) { 558 AddForeignKeyTask task = new AddForeignKeyTask(myRelease, myVersion); 559 task.setTableName(myTableName); 560 task.setConstraintName(myForeignKeyName); 561 task.setColumnName(getColumnName()); 562 task.setForeignTableName(theForeignTable); 563 task.setForeignColumnName(theForeignColumn); 564 addTask(task); 565 return new BuilderCompleteTask(task); 566 } 567 } 568 } 569 570 public static class BuilderAddColumnWithName { 571 private final String myRelease; 572 private final String myVersion; 573 private final String myColumnName; 574 575 @Nullable 576 private final Object myDefaultValue; 577 578 private final BaseMigrationTasks.IAcceptsTasks myTaskSink; 579 580 public BuilderAddColumnWithName( 581 String theRelease, 582 String theVersion, 583 String theColumnName, 584 @Nullable Object theDefaultValue, 585 BaseMigrationTasks.IAcceptsTasks theTaskSink) { 586 myRelease = theRelease; 587 myVersion = theVersion; 588 myColumnName = theColumnName; 589 myDefaultValue = theDefaultValue; 590 myTaskSink = theTaskSink; 591 } 592 593 public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nullable() { 594 return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable( 595 myRelease, myVersion, true); 596 } 597 598 public BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable nonNullable() { 599 return new BuilderWithTableName.BuilderAddColumnWithName.BuilderAddColumnWithNameNullable( 600 myRelease, myVersion, false); 601 } 602 603 public class BuilderAddColumnWithNameNullable { 604 private final boolean myNullable; 605 private final String myRelease; 606 private final String myVersion; 607 608 public BuilderAddColumnWithNameNullable(String theRelease, String theVersion, boolean theNullable) { 609 myRelease = theRelease; 610 myVersion = theVersion; 611 myNullable = theNullable; 612 } 613 614 public BuilderCompleteTask type(ColumnTypeEnum theColumnType) { 615 return type(theColumnType, null); 616 } 617 618 public BuilderCompleteTask type(ColumnTypeEnum theColumnType, Integer theLength) { 619 AddColumnTask task = new AddColumnTask(myRelease, myVersion); 620 task.setColumnName(myColumnName); 621 task.setNullable(myNullable); 622 task.setColumnType(theColumnType); 623 if (theLength != null) { 624 task.setColumnLength(theLength); 625 } 626 task.setDefaultValue(myDefaultValue); 627 myTaskSink.addTask(task); 628 629 return new BuilderCompleteTask(task); 630 } 631 } 632 } 633 } 634 635 public static class BuilderCompleteTask { 636 637 private final BaseTask myTask; 638 639 public BuilderCompleteTask(BaseTask theTask) { 640 myTask = theTask; 641 } 642 643 public BuilderCompleteTask failureAllowed() { 644 myTask.addFlag(TaskFlagEnum.FAILURE_ALLOWED); 645 return this; 646 } 647 648 public BuilderCompleteTask doNothing() { 649 myTask.addFlag(TaskFlagEnum.DO_NOTHING); 650 return this; 651 } 652 653 public BuilderCompleteTask onlyAppliesToPlatforms(DriverTypeEnum... theTypes) { 654 Set<DriverTypeEnum> typesSet = Arrays.stream(theTypes).collect(Collectors.toSet()); 655 myTask.setOnlyAppliesToPlatforms(typesSet); 656 return this; 657 } 658 659 /** 660 * Introduce precondition checking logic into the execution of the enclosed task. This conditional logic will 661 * be implemented by running an SQL SELECT (including CTEs) to obtain a boolean indicating whether a certain 662 * condition has been met. 663 * One example is to check for a specific collation on a column to decide whether to create a new index. 664 * <p/> 665 * This method may be called multiple times to add multiple preconditions. The precondition that evaluates to 666 * false will stop execution of the task irrespective of any or all other tasks evaluating to true. 667 * 668 * @param theSql The SELECT or CTE used to determine if the precondition is valid. 669 * @param reason A String to indicate the text that is logged if the precondition is not met. 670 * @return The BuilderCompleteTask in order to chain further method calls on this builder. 671 */ 672 public BuilderCompleteTask onlyIf(@Language("SQL") String theSql, String reason) { 673 if (!theSql.toUpperCase().startsWith("WITH") 674 && !theSql.toUpperCase().startsWith("SELECT")) { 675 throw new IllegalArgumentException(Msg.code(2455) 676 + String.format( 677 "Only SELECT statements (including CTEs) are allowed here. Please check your SQL: [%s]", 678 theSql)); 679 } 680 ourLog.debug("SQL to evaluate: {}", theSql); 681 682 myTask.addPrecondition(new ExecuteTaskPrecondition( 683 () -> { 684 ourLog.debug("Checking precondition for SQL: {}", theSql); 685 return MigrationJdbcUtils.queryForSingleBooleanResultMultipleThrowsException( 686 theSql, myTask.newJdbcTemplate()); 687 }, 688 reason)); 689 690 return this; 691 } 692 693 public BuilderCompleteTask runEvenDuringSchemaInitialization() { 694 myTask.addFlag(TaskFlagEnum.RUN_DURING_SCHEMA_INITIALIZATION); 695 return this; 696 } 697 698 public BuilderCompleteTask setTransactional(boolean theFlag) { 699 myTask.setTransactional(theFlag); 700 return this; 701 } 702 703 public BuilderCompleteTask heavyweightSkipByDefault() { 704 myTask.addFlag(TaskFlagEnum.HEAVYWEIGHT_SKIP_BY_DEFAULT); 705 return this; 706 } 707 708 public BuilderCompleteTask withFlag(TaskFlagEnum theFlag) { 709 myTask.addFlag(theFlag); 710 return this; 711 } 712 } 713 714 public class BuilderAddTableRawSql { 715 716 private final AddTableRawSqlTask myTask; 717 718 protected BuilderAddTableRawSql(String theVersion, String theTableName) { 719 myTask = new AddTableRawSqlTask(myRelease, theVersion); 720 myTask.setTableName(theTableName); 721 addTask(myTask); 722 } 723 724 public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) { 725 myTask.addSql(theDriverTypeEnum, theSql); 726 return this; 727 } 728 729 public void addSql(@Language("SQL") String theSql) { 730 myTask.addSql(theSql); 731 } 732 } 733 734 public class BuilderAddTableByColumns extends BuilderWithTableName implements BaseMigrationTasks.IAcceptsTasks { 735 private final String myVersion; 736 private final AddTableByColumnTask myTask; 737 738 public BuilderAddTableByColumns( 739 String theRelease, 740 String theVersion, 741 BaseMigrationTasks.IAcceptsTasks theSink, 742 String theTableName, 743 List<String> thePkColumnNames) { 744 super(theRelease, theSink, theTableName); 745 myVersion = theVersion; 746 myTask = new AddTableByColumnTask(myRelease, theVersion); 747 myTask.setTableName(theTableName); 748 myTask.setPkColumns(thePkColumnNames); 749 theSink.addTask(myTask); 750 } 751 752 public BuilderAddColumnWithName addColumn(String theColumnName) { 753 return new BuilderAddColumnWithName(myRelease, myVersion, theColumnName, null, this); 754 } 755 756 @Override 757 public void addTask(BaseTask theTask) { 758 if (theTask instanceof AddColumnTask) { 759 myTask.addAddColumnTask((AddColumnTask) theTask); 760 } else { 761 super.addTask(theTask); 762 } 763 } 764 765 public BuilderCompleteTask withFlags() { 766 return new BuilderCompleteTask(myTask); 767 } 768 } 769 770 public String getRelease() { 771 return myRelease; 772 } 773}