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}