001package ca.uhn.fhir.jpa.migrate; 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.taskdef.ColumnTypeEnum; 025import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 026import org.apache.commons.lang3.builder.EqualsBuilder; 027import org.apache.commons.lang3.builder.HashCodeBuilder; 028import org.apache.commons.lang3.builder.ToStringBuilder; 029import org.hibernate.boot.model.naming.Identifier; 030import org.hibernate.dialect.Dialect; 031import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; 032import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; 033import org.hibernate.engine.jdbc.dialect.spi.DialectResolver; 034import org.hibernate.engine.jdbc.env.internal.NormalizingIdentifierHelperImpl; 035import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData; 036import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; 037import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; 038import org.hibernate.engine.jdbc.env.spi.LobCreatorBuilder; 039import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; 040import org.hibernate.engine.jdbc.env.spi.QualifiedObjectNameFormatter; 041import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; 042import org.hibernate.engine.jdbc.spi.TypeInfo; 043import org.hibernate.service.ServiceRegistry; 044import org.hibernate.tool.schema.extract.spi.ExtractionContext; 045import org.hibernate.tool.schema.extract.spi.SequenceInformation; 046import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049import org.springframework.jdbc.core.ColumnMapRowMapper; 050import org.springframework.transaction.support.TransactionTemplate; 051 052import javax.annotation.Nullable; 053import javax.sql.DataSource; 054import java.sql.Connection; 055import java.sql.DatabaseMetaData; 056import java.sql.ResultSet; 057import java.sql.SQLException; 058import java.sql.Types; 059import java.util.ArrayList; 060import java.util.Collections; 061import java.util.HashSet; 062import java.util.List; 063import java.util.Locale; 064import java.util.Objects; 065import java.util.Set; 066import java.util.stream.Collectors; 067 068public class JdbcUtils { 069 private static final Logger ourLog = LoggerFactory.getLogger(JdbcUtils.class); 070 071 /** 072 * Retrieve all index names 073 */ 074 public static Set<String> getIndexNames(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName) throws SQLException { 075 076 if (!getTableNames(theConnectionProperties).contains(theTableName)) { 077 return Collections.emptySet(); 078 } 079 080 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 081 try (Connection connection = dataSource.getConnection()) { 082 return theConnectionProperties.getTxTemplate().execute(t -> { 083 DatabaseMetaData metadata; 084 try { 085 metadata = connection.getMetaData(); 086 087 ResultSet indexes = getIndexInfo(theTableName, connection, metadata, false); 088 Set<String> indexNames = new HashSet<>(); 089 while (indexes.next()) { 090 ourLog.debug("*** Next index: {}", new ColumnMapRowMapper().mapRow(indexes, 0)); 091 String indexName = indexes.getString("INDEX_NAME"); 092 indexNames.add(indexName); 093 } 094 095 indexes = getIndexInfo(theTableName, connection, metadata, true); 096 while (indexes.next()) { 097 ourLog.debug("*** Next index: {}", new ColumnMapRowMapper().mapRow(indexes, 0)); 098 String indexName = indexes.getString("INDEX_NAME"); 099 indexNames.add(indexName); 100 } 101 102 indexNames = indexNames 103 .stream() 104 .filter(Objects::nonNull) // filter out the nulls first 105 .map(s -> s.toUpperCase(Locale.US)) // then convert the non-null entries to upper case 106 .collect(Collectors.toSet()); 107 108 return indexNames; 109 110 } catch (SQLException e) { 111 throw new InternalErrorException(Msg.code(29) + e); 112 } 113 }); 114 } 115 } 116 117 @SuppressWarnings("ConstantConditions") 118 public static boolean isIndexUnique(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, String theIndexName) throws SQLException { 119 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 120 try (Connection connection = dataSource.getConnection()) { 121 return theConnectionProperties.getTxTemplate().execute(t -> { 122 DatabaseMetaData metadata; 123 try { 124 metadata = connection.getMetaData(); 125 ResultSet indexes = getIndexInfo(theTableName, connection, metadata, false); 126 127 while (indexes.next()) { 128 String indexName = indexes.getString("INDEX_NAME"); 129 if (theIndexName.equalsIgnoreCase(indexName)) { 130 boolean nonUnique = indexes.getBoolean("NON_UNIQUE"); 131 return !nonUnique; 132 } 133 } 134 135 } catch (SQLException e) { 136 throw new InternalErrorException(Msg.code(30) + e); 137 } 138 139 throw new InternalErrorException(Msg.code(31) + "Can't find index: " + theIndexName + " on table " + theTableName); 140 }); 141 } 142 } 143 144 private static ResultSet getIndexInfo(String theTableName, Connection theConnection, DatabaseMetaData theMetadata, boolean theUnique) throws SQLException { 145 // FYI Using approximate=false causes a very slow table scan on Oracle 146 boolean approximate = true; 147 return theMetadata.getIndexInfo(theConnection.getCatalog(), theConnection.getSchema(), massageIdentifier(theMetadata, theTableName), theUnique, approximate); 148 } 149 150 /** 151 * Retrieve all index names 152 */ 153 public static ColumnType getColumnType(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, String theColumnName) throws SQLException { 154 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 155 try (Connection connection = dataSource.getConnection()) { 156 return theConnectionProperties.getTxTemplate().execute(t -> { 157 DatabaseMetaData metadata; 158 try { 159 metadata = connection.getMetaData(); 160 String catalog = connection.getCatalog(); 161 String schema = connection.getSchema(); 162 ResultSet indexes = metadata.getColumns(catalog, schema, massageIdentifier(metadata, theTableName), null); 163 164 while (indexes.next()) { 165 166 String tableName = indexes.getString("TABLE_NAME").toUpperCase(Locale.US); 167 if (!theTableName.equalsIgnoreCase(tableName)) { 168 continue; 169 } 170 String columnName = indexes.getString("COLUMN_NAME").toUpperCase(Locale.US); 171 if (!theColumnName.equalsIgnoreCase(columnName)) { 172 continue; 173 } 174 175 int dataType = indexes.getInt("DATA_TYPE"); 176 Long length = indexes.getLong("COLUMN_SIZE"); 177 switch (dataType) { 178 case Types.BIT: 179 case Types.BOOLEAN: 180 return new ColumnType(ColumnTypeEnum.BOOLEAN, length); 181 case Types.VARCHAR: 182 return new ColumnType(ColumnTypeEnum.STRING, length); 183 case Types.NUMERIC: 184 case Types.BIGINT: 185 case Types.DECIMAL: 186 return new ColumnType(ColumnTypeEnum.LONG, length); 187 case Types.INTEGER: 188 return new ColumnType(ColumnTypeEnum.INT, length); 189 case Types.TIMESTAMP: 190 case Types.TIMESTAMP_WITH_TIMEZONE: 191 return new ColumnType(ColumnTypeEnum.DATE_TIMESTAMP, length); 192 case Types.BLOB: 193 return new ColumnType(ColumnTypeEnum.BLOB, length); 194 case Types.LONGVARBINARY: 195 if (DriverTypeEnum.MYSQL_5_7.equals(theConnectionProperties.getDriverType())) { 196 //See git 197 return new ColumnType(ColumnTypeEnum.BLOB, length); 198 } else { 199 throw new IllegalArgumentException(Msg.code(32) + "Don't know how to handle datatype " + dataType + " for column " + theColumnName + " on table " + theTableName); 200 } 201 case Types.VARBINARY: 202 if (DriverTypeEnum.MSSQL_2012.equals(theConnectionProperties.getDriverType())) { 203 // MS SQLServer seems to be mapping BLOB to VARBINARY under the covers, so we need to reverse that mapping 204 return new ColumnType(ColumnTypeEnum.BLOB, length); 205 206 } else { 207 throw new IllegalArgumentException(Msg.code(33) + "Don't know how to handle datatype " + dataType + " for column " + theColumnName + " on table " + theTableName); 208 } 209 case Types.CLOB: 210 return new ColumnType(ColumnTypeEnum.CLOB, length); 211 case Types.DOUBLE: 212 return new ColumnType(ColumnTypeEnum.DOUBLE, length); 213 case Types.FLOAT: 214 return new ColumnType(ColumnTypeEnum.FLOAT, length); 215 default: 216 throw new IllegalArgumentException(Msg.code(34) + "Don't know how to handle datatype " + dataType + " for column " + theColumnName + " on table " + theTableName); 217 } 218 219 } 220 221 ourLog.debug("Unable to find column {} in table {}.", theColumnName, theTableName); 222 return null; 223 224 } catch (SQLException e) { 225 throw new InternalErrorException(Msg.code(35) + e); 226 } 227 228 }); 229 } 230 } 231 232 /** 233 * Retrieve all index names 234 */ 235 public static Set<String> getForeignKeys(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, @Nullable String theForeignTable) throws SQLException { 236 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 237 238 try (Connection connection = dataSource.getConnection()) { 239 TransactionTemplate txTemplate = theConnectionProperties.getTxTemplate(); 240 return txTemplate.execute(t -> { 241 DatabaseMetaData metadata; 242 try { 243 metadata = connection.getMetaData(); 244 String catalog = connection.getCatalog(); 245 String schema = connection.getSchema(); 246 247 248 List<String> parentTables = new ArrayList<>(); 249 if (theTableName != null) { 250 parentTables.add(massageIdentifier(metadata, theTableName)); 251 } else { 252 // If no foreign table is specified, we'll try all of them 253 parentTables.addAll(JdbcUtils.getTableNames(theConnectionProperties)); 254 } 255 256 String foreignTable = massageIdentifier(metadata, theForeignTable); 257 258 Set<String> fkNames = new HashSet<>(); 259 for (String nextParentTable : parentTables) { 260 ResultSet indexes = metadata.getCrossReference(catalog, schema, nextParentTable, catalog, schema, foreignTable); 261 262 while (indexes.next()) { 263 String fkName = indexes.getString("FK_NAME"); 264 fkName = fkName.toUpperCase(Locale.US); 265 fkNames.add(fkName); 266 } 267 } 268 269 return fkNames; 270 } catch (SQLException e) { 271 throw new InternalErrorException(Msg.code(36) + e); 272 } 273 }); 274 } 275 } 276 277 /** 278 * Retrieve names of foreign keys that reference a specified foreign key column. 279 */ 280 public static Set<String> getForeignKeysForColumn(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theForeignKeyColumn, String theForeignTable) throws SQLException { 281 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 282 283 try (Connection connection = dataSource.getConnection()) { 284 return theConnectionProperties.getTxTemplate().execute(t -> { 285 DatabaseMetaData metadata; 286 try { 287 metadata = connection.getMetaData(); 288 String catalog = connection.getCatalog(); 289 String schema = connection.getSchema(); 290 291 292 List<String> parentTables = new ArrayList<>(); 293 parentTables.addAll(JdbcUtils.getTableNames(theConnectionProperties)); 294 295 String foreignTable = massageIdentifier(metadata, theForeignTable); 296 297 Set<String> fkNames = new HashSet<>(); 298 for (String nextParentTable : parentTables) { 299 ResultSet indexes = metadata.getCrossReference(catalog, schema, nextParentTable, catalog, schema, foreignTable); 300 301 while (indexes.next()) { 302 if (theForeignKeyColumn.equals(indexes.getString("FKCOLUMN_NAME"))) { 303 String fkName = indexes.getString("FK_NAME"); 304 fkName = fkName.toUpperCase(Locale.US); 305 fkNames.add(fkName); 306 } 307 } 308 } 309 310 return fkNames; 311 } catch (SQLException e) { 312 throw new InternalErrorException(Msg.code(37) + e); 313 } 314 }); 315 } 316 } 317 318 /** 319 * Retrieve all index names 320 */ 321 public static Set<String> getColumnNames(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName) throws SQLException { 322 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 323 try (Connection connection = dataSource.getConnection()) { 324 return theConnectionProperties.getTxTemplate().execute(t -> { 325 DatabaseMetaData metadata; 326 try { 327 metadata = connection.getMetaData(); 328 ResultSet indexes = metadata.getColumns(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), null); 329 330 Set<String> columnNames = new HashSet<>(); 331 while (indexes.next()) { 332 String tableName = indexes.getString("TABLE_NAME").toUpperCase(Locale.US); 333 if (!theTableName.equalsIgnoreCase(tableName)) { 334 continue; 335 } 336 337 String columnName = indexes.getString("COLUMN_NAME"); 338 columnName = columnName.toUpperCase(Locale.US); 339 columnNames.add(columnName); 340 } 341 342 return columnNames; 343 } catch (SQLException e) { 344 throw new InternalErrorException(Msg.code(38) + e); 345 } 346 }); 347 } 348 } 349 350 public static Set<String> getSequenceNames(DriverTypeEnum.ConnectionProperties theConnectionProperties) throws SQLException { 351 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 352 try (Connection connection = dataSource.getConnection()) { 353 return theConnectionProperties.getTxTemplate().execute(t -> { 354 try { 355 DialectResolver dialectResolver = new StandardDialectResolver(); 356 Dialect dialect = dialectResolver.resolveDialect(new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData())); 357 358 Set<String> sequenceNames = new HashSet<>(); 359 if (dialect.supportsSequences()) { 360 361 // Use Hibernate to get a list of current sequences 362 SequenceInformationExtractor sequenceInformationExtractor = dialect.getSequenceInformationExtractor(); 363 ExtractionContext extractionContext = new ExtractionContext.EmptyExtractionContext() { 364 @Override 365 public Connection getJdbcConnection() { 366 return connection; 367 } 368 369 @Override 370 public ServiceRegistry getServiceRegistry() { 371 return super.getServiceRegistry(); 372 } 373 374 @Override 375 public JdbcEnvironment getJdbcEnvironment() { 376 return new JdbcEnvironment() { 377 @Override 378 public Dialect getDialect() { 379 return dialect; 380 } 381 382 @Override 383 public ExtractedDatabaseMetaData getExtractedDatabaseMetaData() { 384 return null; 385 } 386 387 @Override 388 public Identifier getCurrentCatalog() { 389 return null; 390 } 391 392 @Override 393 public Identifier getCurrentSchema() { 394 return null; 395 } 396 397 @Override 398 public QualifiedObjectNameFormatter getQualifiedObjectNameFormatter() { 399 return null; 400 } 401 402 @Override 403 public IdentifierHelper getIdentifierHelper() { 404 return new NormalizingIdentifierHelperImpl(this, null, true, true, true, null, null, null); 405 } 406 407 @Override 408 public NameQualifierSupport getNameQualifierSupport() { 409 return null; 410 } 411 412 @Override 413 public SqlExceptionHelper getSqlExceptionHelper() { 414 return null; 415 } 416 417 @Override 418 public LobCreatorBuilder getLobCreatorBuilder() { 419 return null; 420 } 421 422 @Override 423 public TypeInfo getTypeInfoForJdbcCode(int jdbcTypeCode) { 424 return null; 425 } 426 }; 427 } 428 }; 429 Iterable<SequenceInformation> sequences = sequenceInformationExtractor.extractMetadata(extractionContext); 430 for (SequenceInformation next : sequences) { 431 sequenceNames.add(next.getSequenceName().getSequenceName().getText()); 432 } 433 434 } 435 return sequenceNames; 436 } catch (SQLException e) { 437 throw new InternalErrorException(Msg.code(39) + e); 438 } 439 }); 440 } 441 } 442 443 public static Set<String> getTableNames(DriverTypeEnum.ConnectionProperties theConnectionProperties) throws SQLException { 444 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 445 try (Connection connection = dataSource.getConnection()) { 446 return theConnectionProperties.getTxTemplate().execute(t -> { 447 DatabaseMetaData metadata; 448 try { 449 metadata = connection.getMetaData(); 450 ResultSet tables = metadata.getTables(connection.getCatalog(), connection.getSchema(), null, null); 451 452 Set<String> columnNames = new HashSet<>(); 453 while (tables.next()) { 454 String tableName = tables.getString("TABLE_NAME"); 455 tableName = tableName.toUpperCase(Locale.US); 456 457 String tableType = tables.getString("TABLE_TYPE"); 458 if ("SYSTEM TABLE".equalsIgnoreCase(tableType)) { 459 continue; 460 } 461 if (SchemaMigrator.HAPI_FHIR_MIGRATION_TABLENAME.equalsIgnoreCase(tableName)) { 462 continue; 463 } 464 465 columnNames.add(tableName); 466 } 467 468 return columnNames; 469 } catch (SQLException e) { 470 throw new InternalErrorException(Msg.code(40) + e); 471 } 472 }); 473 } 474 } 475 476 public static boolean isColumnNullable(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, String theColumnName) throws SQLException { 477 DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); 478 try (Connection connection = dataSource.getConnection()) { 479 //noinspection ConstantConditions 480 return theConnectionProperties.getTxTemplate().execute(t -> { 481 DatabaseMetaData metadata; 482 try { 483 metadata = connection.getMetaData(); 484 ResultSet tables = metadata.getColumns(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), null); 485 486 while (tables.next()) { 487 String tableName = tables.getString("TABLE_NAME").toUpperCase(Locale.US); 488 if (!theTableName.equalsIgnoreCase(tableName)) { 489 continue; 490 } 491 492 if (theColumnName.equalsIgnoreCase(tables.getString("COLUMN_NAME"))) { 493 String nullable = tables.getString("IS_NULLABLE"); 494 if ("YES".equalsIgnoreCase(nullable)) { 495 return true; 496 } else if ("NO".equalsIgnoreCase(nullable)) { 497 return false; 498 } else { 499 throw new IllegalStateException(Msg.code(41) + "Unknown nullable: " + nullable); 500 } 501 } 502 } 503 504 throw new IllegalStateException(Msg.code(42) + "Did not find column " + theColumnName); 505 } catch (SQLException e) { 506 throw new InternalErrorException(Msg.code(43) + e); 507 } 508 }); 509 } 510 } 511 512 private static String massageIdentifier(DatabaseMetaData theMetadata, String theCatalog) throws SQLException { 513 String retVal = theCatalog; 514 if (theCatalog == null) { 515 return null; 516 } else if (theMetadata.storesLowerCaseIdentifiers()) { 517 retVal = retVal.toLowerCase(); 518 } else { 519 retVal = retVal.toUpperCase(); 520 } 521 return retVal; 522 } 523 524 public static class ColumnType { 525 private final ColumnTypeEnum myColumnTypeEnum; 526 private final Long myLength; 527 528 public ColumnType(ColumnTypeEnum theColumnType, Long theLength) { 529 myColumnTypeEnum = theColumnType; 530 myLength = theLength; 531 } 532 533 public ColumnType(ColumnTypeEnum theColumnType, int theLength) { 534 this(theColumnType, (long) theLength); 535 } 536 537 public ColumnType(ColumnTypeEnum theColumnType) { 538 this(theColumnType, null); 539 } 540 541 @Override 542 public boolean equals(Object theO) { 543 if (this == theO) { 544 return true; 545 } 546 547 if (theO == null || getClass() != theO.getClass()) { 548 return false; 549 } 550 551 ColumnType that = (ColumnType) theO; 552 553 return new EqualsBuilder() 554 .append(myColumnTypeEnum, that.myColumnTypeEnum) 555 .append(myLength, that.myLength) 556 .isEquals(); 557 } 558 559 @Override 560 public int hashCode() { 561 return new HashCodeBuilder(17, 37) 562 .append(myColumnTypeEnum) 563 .append(myLength) 564 .toHashCode(); 565 } 566 567 @Override 568 public String toString() { 569 ToStringBuilder b = new ToStringBuilder(this); 570 b.append("type", myColumnTypeEnum); 571 if (myLength != null) { 572 b.append("length", myLength); 573 } 574 return b.toString(); 575 } 576 577 public ColumnTypeEnum getColumnTypeEnum() { 578 return myColumnTypeEnum; 579 } 580 581 public Long getLength() { 582 return myLength; 583 } 584 585 public boolean equals(ColumnTypeEnum theTaskColumnType, Long theTaskColumnLength) { 586 ourLog.debug("Comparing existing {} {} to new {} {}", myColumnTypeEnum, myLength, theTaskColumnType, theTaskColumnLength); 587 return myColumnTypeEnum == theTaskColumnType && (theTaskColumnLength == null || theTaskColumnLength.equals(myLength)); 588 } 589 } 590}