001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.sql.DataSource; 023import java.sql.Connection; 024import java.sql.DatabaseMetaData; 025import java.sql.SQLException; 026import java.util.Locale; 027 028import static java.util.Objects.requireNonNull; 029 030/** 031 * Identifies different types of databases, which allows for special platform-specific handling. 032 * 033 * @author <a href="https://www.revetkn.com">Mark Allen</a> 034 * @since 1.0.0 035 */ 036public enum DatabaseType { 037 /** 038 * A database which requires no special handling. 039 */ 040 GENERIC, 041 /** 042 * A PostgreSQL database. 043 */ 044 POSTGRESQL, 045 /** 046 * An Oracle database. 047 */ 048 ORACLE, 049 /** 050 * A MySQL database. 051 * 052 * @since 4.3.0 053 */ 054 MYSQL, 055 /** 056 * A MariaDB database. 057 * 058 * @since 4.3.0 059 */ 060 MARIA_DB, 061 /** 062 * A SQLite database. 063 * 064 * @since 4.3.0 065 */ 066 SQLITE, 067 /** 068 * A Microsoft SQL Server database. 069 * 070 * @since 4.3.0 071 */ 072 SQL_SERVER, 073 /** 074 * A DuckDB database. 075 * 076 * @since 4.7.0 077 */ 078 DUCK_DB; 079 080 /** 081 * Determines the type of database to which the given {@code dataSource} connects. 082 * <p> 083 * Note: this will establish a {@link Connection} to the database. 084 * 085 * @param dataSource the database connection factory 086 * @return the type of database 087 * @throws DatabaseException if an exception occurs while attempting to read database metadata 088 */ 089 @NonNull 090 public static DatabaseType fromDataSource(@NonNull DataSource dataSource) { 091 requireNonNull(dataSource); 092 093 try (Connection connection = dataSource.getConnection()) { 094 return fromConnection(connection); 095 } catch (SQLException e) { 096 throw new DatabaseException("Unable to connect to database to determine its type", e); 097 } 098 } 099 100 /** 101 * Determines the type of database represented by the given {@code connection}. 102 * 103 * @param connection an active database connection 104 * @return the type of database 105 * @throws DatabaseException if an exception occurs while attempting to read database metadata 106 */ 107 @NonNull 108 public static DatabaseType fromConnection(@NonNull Connection connection) { 109 requireNonNull(connection); 110 111 try { 112 DatabaseMetaData databaseMetaData = connection.getMetaData(); 113 String databaseProductName = databaseMetaData.getDatabaseProductName(); 114 String databaseProductVersion = databaseProductVersion(databaseMetaData); 115 String url = databaseMetaData.getURL(); 116 String driverName = databaseMetaData.getDriverName(); 117 118 // All of our checks are against databases with English names 119 String databaseProductNameLowercase = databaseProductName == null ? "" : databaseProductName.toLowerCase(Locale.ENGLISH); 120 String databaseProductVersionLowercase = databaseProductVersion == null ? "" : databaseProductVersion.toLowerCase(Locale.ENGLISH); 121 String urlLowercase = url == null ? "" : url.toLowerCase(Locale.ENGLISH); 122 String driverNameLowercase = driverName == null ? "" : driverName.toLowerCase(Locale.ENGLISH); 123 124 // Prefer product name 125 if (databaseProductNameLowercase.startsWith("oracle")) 126 return DatabaseType.ORACLE; 127 128 // Strict match for PostgreSQL 129 if (databaseProductNameLowercase.contains("postgresql") || databaseProductNameLowercase.equals("postgres")) // some proxies shorten it 130 return DatabaseType.POSTGRESQL; 131 132 if (databaseProductNameLowercase.contains("mariadb")) 133 return DatabaseType.MARIA_DB; 134 135 if (databaseProductNameLowercase.contains("mysql")) 136 return mysqlFamilyDatabaseType(databaseProductVersionLowercase, driverNameLowercase); 137 138 if (databaseProductNameLowercase.contains("sqlite")) 139 return DatabaseType.SQLITE; 140 141 if (databaseProductNameLowercase.contains("duckdb")) 142 return DatabaseType.DUCK_DB; 143 144 if (isSqlServerProductName(databaseProductNameLowercase)) 145 return DatabaseType.SQL_SERVER; 146 147 // Fallbacks if product name is absent/weird but we're clearly using a vendor driver/URL 148 if (urlLowercase.startsWith("jdbc:postgresql:") || driverNameLowercase.contains("postgresql")) 149 return DatabaseType.POSTGRESQL; 150 151 if (urlLowercase.startsWith("jdbc:oracle:") || driverNameLowercase.contains("oracle jdbc")) 152 return DatabaseType.ORACLE; 153 154 if (urlLowercase.startsWith("jdbc:mariadb:") || driverNameLowercase.contains("mariadb")) 155 return DatabaseType.MARIA_DB; 156 157 if (isMysqlUrl(urlLowercase) || driverNameLowercase.contains("mysql")) 158 return mysqlFamilyDatabaseType(databaseProductVersionLowercase, driverNameLowercase); 159 160 if (urlLowercase.startsWith("jdbc:sqlite:") || driverNameLowercase.contains("sqlite")) 161 return DatabaseType.SQLITE; 162 163 if (urlLowercase.startsWith("jdbc:duckdb:") || driverNameLowercase.contains("duckdb")) 164 return DatabaseType.DUCK_DB; 165 166 if (isSqlServerUrl(urlLowercase) || isSqlServerDriverName(driverNameLowercase)) 167 return DatabaseType.SQL_SERVER; 168 169 return DatabaseType.GENERIC; 170 } catch (SQLException e) { 171 throw new DatabaseException("Unable to inspect database metadata to determine its type", e); 172 } 173 } 174 175 @Nullable 176 private static String databaseProductVersion(@NonNull DatabaseMetaData databaseMetaData) { 177 requireNonNull(databaseMetaData); 178 179 try { 180 return databaseMetaData.getDatabaseProductVersion(); 181 } catch (SQLException e) { 182 return null; 183 } 184 } 185 186 @NonNull 187 DatabaseDialect dialect() { 188 return switch (this) { 189 case POSTGRESQL -> PostgresDialect.INSTANCE; 190 case ORACLE -> OracleDialect.INSTANCE; 191 case MYSQL -> MySqlDialect.INSTANCE; 192 case MARIA_DB -> MariaDbDialect.INSTANCE; 193 case SQLITE -> SqliteDialect.INSTANCE; 194 case SQL_SERVER -> SqlServerDialect.INSTANCE; 195 case DUCK_DB -> DuckDbDialect.INSTANCE; 196 case GENERIC -> GenericDialect.INSTANCE; 197 }; 198 } 199 200 @NonNull 201 private static DatabaseType mysqlFamilyDatabaseType(@NonNull String databaseProductVersionLowercase, 202 @NonNull String driverNameLowercase) { 203 requireNonNull(databaseProductVersionLowercase); 204 requireNonNull(driverNameLowercase); 205 206 if (databaseProductVersionLowercase.contains("mariadb") || driverNameLowercase.contains("mariadb")) 207 return DatabaseType.MARIA_DB; 208 209 return DatabaseType.MYSQL; 210 } 211 212 private static boolean isMysqlUrl(@NonNull String urlLowercase) { 213 requireNonNull(urlLowercase); 214 215 return urlLowercase.startsWith("jdbc:mysql:") || urlLowercase.startsWith("jdbc:mysql+srv:"); 216 } 217 218 private static boolean isSqlServerProductName(@NonNull String databaseProductNameLowercase) { 219 requireNonNull(databaseProductNameLowercase); 220 221 return databaseProductNameLowercase.contains("microsoft sql server") 222 || databaseProductNameLowercase.equals("sql server") 223 || databaseProductNameLowercase.equals("sqlserver"); 224 } 225 226 private static boolean isSqlServerUrl(@NonNull String urlLowercase) { 227 requireNonNull(urlLowercase); 228 229 return urlLowercase.startsWith("jdbc:sqlserver:") || urlLowercase.startsWith("jdbc:jtds:sqlserver:"); 230 } 231 232 private static boolean isSqlServerDriverName(@NonNull String driverNameLowercase) { 233 requireNonNull(driverNameLowercase); 234 235 return driverNameLowercase.contains("microsoft jdbc driver") 236 || driverNameLowercase.contains("sql server") 237 || driverNameLowercase.contains("jtds"); 238 } 239}