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