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