diff --git a/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Files.kt b/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Files.kt index 85f8084a58..0a87e41a33 100644 --- a/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Files.kt +++ b/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Files.kt @@ -19,7 +19,6 @@ package eu.cloudnetservice.cloudnet.gradle.util object Files { const val driver = "driver.jar" - const val common = "common.jar" const val wrapper = "wrapper.jar" const val launcher = "launcher.jar" const val launcherPatcher = "launcher-patcher.jar" @@ -33,10 +32,9 @@ object Files { const val cloudflare = "cloudnet-cloudflare.jar" const val dockerizedServices = "cloudnet-dockerized-services.jar" const val databaseMongo = "cloudnet-database-mongodb.jar" - const val databaseMysql = "cloudnet-database-mysql.jar" + const val databaseSql = "cloudnet-database-sql.jar" const val labymod = "cloudnet-labymod.jar" const val npcs = "cloudnet-npcs.jar" - const val rest = "cloudnet-rest.jar" const val signs = "cloudnet-signs.jar" const val smart = "cloudnet-smart.jar" const val syncproxy = "cloudnet-syncproxy.jar" diff --git a/driver/api/src/main/java/eu/cloudnetservice/driver/database/DatabaseProvider.java b/driver/api/src/main/java/eu/cloudnetservice/driver/database/DatabaseProvider.java index c3c3d50582..caa43bc067 100644 --- a/driver/api/src/main/java/eu/cloudnetservice/driver/database/DatabaseProvider.java +++ b/driver/api/src/main/java/eu/cloudnetservice/driver/database/DatabaseProvider.java @@ -45,6 +45,15 @@ */ public interface DatabaseProvider { + /** + * Gets if this database provider produces databases that are synced to the cluster. This means that every change made + * to the database will be directly visible to all components in the cluster rather than requiring a special sync. + * Normally synced databases are databases which are running as an external process, like MySQL or MongoDB. + * + * @return true if all modify operations are directly visible to all components in a cluster, false otherwise. + */ + boolean synced(); + /** * Retrieves or creates non-blocking a facade for a database to write and read data to. The name of the database * should be unique for later identification. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f116a4a7d1..b69ea4b1c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,11 +29,15 @@ stringSimilarity = "2.0.0" cloudConfirmation = "1.0.0-rc.1" # databases +jooq = "3.20.10" h2 = "1.4.197" # do not update, leads to database incompatibility xodus = "2.0.1" mongodb = "5.6.5" hikariCp = "7.0.2" mysqlConnector = "9.7.0" +sqliteConnector = "3.51.1.0" +postgresqlConnector = "42.7.9" +mariadbConnector = "3.5.7" # general oshi = "6.9.2" @@ -158,11 +162,15 @@ aerogelAuto = { group = "dev.derklaro.aerogel", name = "aerogel-auto", version.r aerogelScopedValues = { group = "dev.derklaro.aerogel", name = "aerogel-scoped-value-context-scope", version.ref = "aerogel" } # databases +jooq = { group = "org.jooq", name = "jooq", version.ref = "jooq" } h2 = { group = "com.h2database", name = "h2", version.ref = "h2" } hikariCp = { group = "com.zaxxer", name = "HikariCP", version.ref = "hikariCp" } mongodb = { group = "org.mongodb", name = "mongodb-driver-sync", version.ref = "mongodb" } xodus = { group = "org.jetbrains.xodus", name = "xodus-environment", version.ref = "xodus" } mysqlConnector = { group = "com.mysql", name = "mysql-connector-j", version.ref = "mysqlConnector" } +sqliteConnector = { group = "org.xerial", name = "sqlite-jdbc", version.ref = "sqliteConnector" } +postgresqlConnector = { group = "org.postgresql", name = "postgresql", version.ref = "postgresqlConnector" } +mariadbConnector = { group = "org.mariadb.jdbc", name = "mariadb-java-client", version.ref = "mariadbConnector" } # platform api nukkitX = { group = "cn.nukkit", name = "nukkit", version.ref = "nukkitX" } @@ -203,7 +211,7 @@ aerogel = ["aerogel", "aerogelScopedValues", "aerogelAuto"] aerogelApi = ["aerogel", "aerogelAuto"] npcLib = ["npcLib", "npcLibLabymod"] unirest = ["unirest", "unirestGson"] -mysql = ["mysqlConnector", "hikariCp"] +sql = ["mysqlConnector", "hikariCp", "postgresqlConnector", "sqliteConnector", "mariadbConnector", "jooq"] jline = ["jlineReader", "jlineTerminal"] cloud = ["cloudCore", "cloudAnnotations", "cloudConfirmationProcessor"] cloudApi = ["cloudCoreApi", "cloudAnnotationsApi", "cloudConfirmationProcessor"] diff --git a/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabase.java b/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabase.java index 5a018ebaf5..4ab24480b2 100644 --- a/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabase.java +++ b/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabase.java @@ -31,7 +31,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; import lombok.NonNull; import org.bson.conversions.Bson; import org.jetbrains.annotations.Nullable; @@ -169,11 +168,6 @@ public boolean delete(@NonNull String key) { return entries; } - @Override - public void iterate(@NonNull BiConsumer consumer) { - this.entries().forEach(consumer); - } - @Override public void clear() { this.collection.deleteMany(new org.bson.Document()); diff --git a/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabaseProvider.java b/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabaseProvider.java index 2a081da663..79db7d1f57 100644 --- a/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabaseProvider.java +++ b/modules/database-mongodb/impl/src/main/java/eu/cloudnetservice/modules/mongodb/impl/MongoDBDatabaseProvider.java @@ -51,6 +51,11 @@ public boolean init() { return true; } + @Override + public boolean synced() { + return true; + } + @Override public @NonNull LocalDatabase database(@NonNull String name) { return this.databaseCache.get(name, $ -> { diff --git a/modules/database-mysql/api/src/main/java/eu/cloudnetservice/modules/mysql/config/MySQLConfiguration.java b/modules/database-mysql/api/src/main/java/eu/cloudnetservice/modules/mysql/config/MySQLConfiguration.java deleted file mode 100644 index 4fc824f846..0000000000 --- a/modules/database-mysql/api/src/main/java/eu/cloudnetservice/modules/mysql/config/MySQLConfiguration.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.cloudnetservice.modules.mysql.config; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; -import lombok.NonNull; - -public record MySQLConfiguration( - @NonNull String username, - @NonNull String password, - @NonNull String databaseServiceName, - @NonNull List endpoints -) { - - public @NonNull MySQLConnectionEndpoint randomEndpoint() { - // check if there are any endpoints - if (this.endpoints.isEmpty()) { - throw new IllegalStateException("No mysql connection endpoints available"); - } - // return a random stream - return this.endpoints.get(ThreadLocalRandom.current().nextInt(0, this.endpoints.size())); - } -} diff --git a/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/CloudNetMySQLDatabaseModule.java b/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/CloudNetMySQLDatabaseModule.java deleted file mode 100644 index 82765430c1..0000000000 --- a/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/CloudNetMySQLDatabaseModule.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.cloudnetservice.modules.mysql.impl; - -import eu.cloudnetservice.driver.document.Document; -import eu.cloudnetservice.driver.document.DocumentFactory; -import eu.cloudnetservice.driver.module.ModuleLifeCycle; -import eu.cloudnetservice.driver.module.ModuleTask; -import eu.cloudnetservice.driver.module.driver.DriverModule; -import eu.cloudnetservice.driver.network.HostAndPort; -import eu.cloudnetservice.driver.registry.ServiceRegistry; -import eu.cloudnetservice.modules.mysql.config.MySQLConfiguration; -import eu.cloudnetservice.modules.mysql.config.MySQLConnectionEndpoint; -import eu.cloudnetservice.node.impl.database.NodeDatabaseProvider; -import io.leangen.geantyref.TypeFactory; -import jakarta.inject.Singleton; -import java.util.List; -import lombok.NonNull; - -@Singleton -public final class CloudNetMySQLDatabaseModule extends DriverModule { - - private volatile MySQLConfiguration configuration; - - @ModuleTask(order = 127, lifecycle = ModuleLifeCycle.LOADED) - public void convertConfig() { - var config = this.readConfig(DocumentFactory.json()); - if (config.contains("addresses")) { - // convert all entries - this.writeConfig(Document.newJsonDocument().appendTree(new MySQLConfiguration( - config.getString("username"), - config.getString("password"), - config.getString("database"), - config.readObject("addresses", TypeFactory.parameterizedClass(List.class, MySQLConnectionEndpoint.class)) - ))); - } - } - - @ModuleTask(order = 125, lifecycle = ModuleLifeCycle.LOADED) - public void registerDatabaseProvider(@NonNull ServiceRegistry serviceRegistry) { - this.configuration = this.readConfig( - MySQLConfiguration.class, - () -> new MySQLConfiguration( - "root", - "123456", - "mysql", - List.of(new MySQLConnectionEndpoint("cloudnet", new HostAndPort("127.0.0.1", 3306)))), - DocumentFactory.json()); - - serviceRegistry.registerProvider( - NodeDatabaseProvider.class, - this.configuration.databaseServiceName(), - new MySQLDatabaseProvider(this.configuration, null)); - } - - @ModuleTask(order = 127, lifecycle = ModuleLifeCycle.STOPPED) - public void unregisterDatabaseProvider(@NonNull ServiceRegistry serviceRegistry) { - var service = serviceRegistry.registration(NodeDatabaseProvider.class, this.configuration.databaseServiceName()); - if (service != null) { - service.unregister(); - } - } -} diff --git a/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabase.java b/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabase.java deleted file mode 100644 index 5fae2748e9..0000000000 --- a/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabase.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.cloudnetservice.modules.mysql.impl; - -import eu.cloudnetservice.driver.document.Document; -import eu.cloudnetservice.driver.document.DocumentFactory; -import eu.cloudnetservice.node.impl.database.sql.SQLDatabase; -import eu.cloudnetservice.node.impl.database.sql.SQLDatabaseProvider; -import java.sql.ResultSet; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiConsumer; -import lombok.NonNull; -import org.jetbrains.annotations.Nullable; - -public final class MySQLDatabase extends SQLDatabase { - - public MySQLDatabase(@NonNull SQLDatabaseProvider provider, @NonNull String name) { - super(provider, name); - - // create the table - provider.executeUpdate(String.format( - "CREATE TABLE IF NOT EXISTS `%s` (%s VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci PRIMARY KEY, %s JSON NOT NULL);", - name, - TABLE_COLUMN_KEY, - TABLE_COLUMN_VAL)); - - // alter mysql tables - provider.executeUpdate(String.format("ALTER TABLE `%s` MODIFY `%s` VARCHAR(512), MODIFY %s JSON NOT NULL", - name, - TABLE_COLUMN_KEY, - TABLE_COLUMN_VAL)); - } - - @Override - public boolean insert(@NonNull String key, @NonNull Document document) { - var serializedDocument = this.serializeDocumentToJsonString(document); - return this.databaseProvider.executeUpdate( - String.format( - "INSERT INTO `%s` (%s, %s) VALUES (?, ?) ON DUPLICATE KEY UPDATE %s = ?;", - this.name, - TABLE_COLUMN_KEY, - TABLE_COLUMN_VAL, - TABLE_COLUMN_VAL), - key, serializedDocument, serializedDocument) > 0; - } - - @Override - public boolean contains(@NonNull String key) { - return this.databaseProvider.executeQuery( - String.format("SELECT %s FROM `%s` WHERE %s = ?;", TABLE_COLUMN_KEY, this.name, TABLE_COLUMN_KEY), - ResultSet::next, - false, - key); - } - - @Override - public boolean delete(@NonNull String key) { - return this.databaseProvider.executeUpdate( - String.format("DELETE FROM %s WHERE `%s` = ?;", this.name, TABLE_COLUMN_KEY), - key) > 0; - } - - @Override - public @Nullable Document get(@NonNull String key) { - return this.databaseProvider.executeQuery( - String.format("SELECT %s FROM `%s` WHERE %s = ?;", TABLE_COLUMN_VAL, this.name, TABLE_COLUMN_KEY), - resultSet -> { - if (resultSet.next()) { - return DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL)); - } - - return null; - }, null, key); - } - - @Override - public @NonNull Collection find(@NonNull String fieldName, @Nullable String fieldValue) { - return this.databaseProvider.executeQuery( - String.format( - "SELECT %s FROM `%s` WHERE JSON_SEARCH(%s, 'one', '%s', NULL, '$.%s') IS NOT NULL;", - TABLE_COLUMN_VAL, - this.name, - TABLE_COLUMN_VAL, - Objects.toString(fieldValue).replaceAll("([_%])", "\\\\$1"), - fieldName), - resultSet -> { - List results = new ArrayList<>(); - while (resultSet.next()) { - results.add(DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL))); - } - - return results; - }, List.of()); - } - - @Override - public @NonNull Collection find(@NonNull Map filters) { - var stringBuilder = new StringBuilder("SELECT ") - .append(TABLE_COLUMN_VAL) - .append(" FROM `") - .append(this.name) - .append('`'); - - if (!filters.isEmpty()) { - stringBuilder.append(" WHERE "); - var iterator = filters.entrySet().iterator(); - while (iterator.hasNext()) { - var entry = iterator.next(); - stringBuilder - .append("JSON_SEARCH(") - .append(TABLE_COLUMN_VAL) - .append(", 'one', '") - .append(entry.getValue().replaceAll("([_%])", "\\\\$1")) - .append("', NULL, '$.") - .append(entry.getKey()) - .append("') IS NOT NULL") - .append(iterator.hasNext() ? " AND " : ';'); - } - } - - return this.databaseProvider.executeQuery(stringBuilder.toString(), resultSet -> { - List results = new ArrayList<>(); - while (resultSet.next()) { - results.add(DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL))); - } - - return results; - }, List.of()); - } - - @Override - public @NonNull Collection keys() { - return this.databaseProvider.executeQuery(String.format("SELECT %s FROM `%s`;", TABLE_COLUMN_KEY, this.name), - resultSet -> { - List results = new ArrayList<>(); - while (resultSet.next()) { - results.add(resultSet.getString(TABLE_COLUMN_KEY)); - } - - return results; - }, Set.of()); - } - - @Override - public @NonNull Collection documents() { - return this.databaseProvider.executeQuery(String.format("SELECT %s FROM `%s`;", TABLE_COLUMN_VAL, this.name), - resultSet -> { - List results = new ArrayList<>(); - while (resultSet.next()) { - results.add(DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL))); - } - - return results; - }, Set.of()); - } - - @Override - public @NonNull Map entries() { - return this.databaseProvider.executeQuery(String.format("SELECT * FROM `%s`;", this.name), resultSet -> { - Map results = new HashMap<>(); - while (resultSet.next()) { - results.put( - resultSet.getString(TABLE_COLUMN_KEY), - DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL))); - } - - return results; - }, Map.of()); - } - - @Override - public void clear() { - this.databaseProvider.executeUpdate(String.format("TRUNCATE TABLE `%s`;", this.name)); - } - - @Override - public long documentCount() { - return this.databaseProvider.executeQuery("SELECT COUNT(*) FROM `" + this.name + "`;", resultSet -> { - if (resultSet.next()) { - return resultSet.getLong(1); - } - return -1L; - }, -1L); - } - - @Override - public boolean synced() { - return true; - } - - @Override - public void iterate(@NonNull BiConsumer consumer) { - this.databaseProvider.executeQuery( - String.format("SELECT * FROM `%s`;", this.name), - resultSet -> { - while (resultSet.next()) { - var key = resultSet.getString(TABLE_COLUMN_KEY); - var document = DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL)); - consumer.accept(key, document); - } - - return null; - }, null); - } - - @Override - public @Nullable Map readChunk(long beginIndex, int chunkSize) { - return this.databaseProvider.executeQuery( - String.format("SELECT * FROM `%s` ORDER BY `%s` LIMIT ? OFFSET ?;", this.name, TABLE_COLUMN_KEY), - resultSet -> { - Map result = new HashMap<>(); - while (resultSet.next()) { - var key = resultSet.getString(TABLE_COLUMN_KEY); - var document = DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL)); - result.put(key, document); - } - - return result.isEmpty() ? null : result; - }, null, chunkSize, beginIndex); - } - - @Override - public void close() { - } -} diff --git a/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabaseProvider.java b/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabaseProvider.java deleted file mode 100644 index 41d1c9cece..0000000000 --- a/modules/database-mysql/impl/src/main/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabaseProvider.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.cloudnetservice.modules.mysql.impl; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import eu.cloudnetservice.modules.mysql.config.MySQLConfiguration; -import eu.cloudnetservice.node.database.LocalDatabase; -import eu.cloudnetservice.node.impl.database.sql.SQLDatabaseProvider; -import io.vavr.CheckedFunction1; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import lombok.NonNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.UnknownNullability; - -public final class MySQLDatabaseProvider extends SQLDatabaseProvider { - - private static final String CONNECT_URL_FORMAT = "jdbc:mysql://%s:%d/%s?serverTimezone=UTC"; - - private final MySQLConfiguration config; - private volatile HikariDataSource hikariDataSource; - - public MySQLDatabaseProvider( - @NonNull MySQLConfiguration config, - @Nullable ExecutorService executorService - ) { - super(DEFAULT_REMOVAL_LISTENER); - this.config = config; - } - - @Override - public boolean init() { - var hikariConfig = new HikariConfig(); - var endpoint = this.config.randomEndpoint(); - - hikariConfig.setJdbcUrl(String.format( - CONNECT_URL_FORMAT, - endpoint.address().host(), endpoint.address().port(), endpoint.database())); - hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); - hikariConfig.setUsername(this.config.username()); - hikariConfig.setPassword(this.config.password()); - - hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); - hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); - hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - hikariConfig.addDataSourceProperty("useServerPrepStmts", "true"); - hikariConfig.addDataSourceProperty("useLocalSessionState", "true"); - hikariConfig.addDataSourceProperty("rewriteBatchedStatements", "true"); - hikariConfig.addDataSourceProperty("cacheResultSetMetadata", "true"); - hikariConfig.addDataSourceProperty("cacheServerConfiguration", "true"); - hikariConfig.addDataSourceProperty("elideSetAutoCommits", "true"); - hikariConfig.addDataSourceProperty("maintainTimeStats", "false"); - - hikariConfig.setMinimumIdle(2); - hikariConfig.setMaximumPoolSize(100); - hikariConfig.setConnectionTimeout(10_000); - hikariConfig.setValidationTimeout(10_000); - - this.hikariDataSource = new HikariDataSource(hikariConfig); - return true; - } - - @Override - public @NonNull LocalDatabase database(@NonNull String name) { - return this.databaseCache.get(name, _ -> new MySQLDatabase(this, name)); - } - - @Override - public boolean deleteDatabase(@NonNull String name) { - return this.executeUpdate(String.format("DROP TABLE IF EXISTS `%s`;", name)) != -1; - } - - @Override - public @NonNull Collection databaseNames() { - try (var connection = this.hikariDataSource.getConnection(); - var meta = connection.getMetaData().getTables(null, null, null, TABLE_TYPE)) { - // now we just need to extract the name from of the tables from the result set - Collection names = new ArrayList<>(); - while (meta.next()) { - names.add(meta.getString("table_name")); - } - return names; - } catch (SQLException exception) { - LOGGER.error("Exception listing tables", exception); - return Set.of(); - } - } - - @Override - public @NonNull String name() { - return this.config.databaseServiceName(); - } - - @Override - public void close() throws Exception { - super.close(); - this.hikariDataSource.close(); - } - - @Override - public @NonNull Connection connection() { - try { - return this.hikariDataSource.getConnection(); - } catch (SQLException exception) { - throw new IllegalStateException("Unable to retrieve connection from pool", exception); - } - } - - @Override - public int executeUpdate(@NonNull String query, @NonNull Object... objects) { - try (var con = this.connection(); var statement = con.prepareStatement(query)) { - // write all parameters - for (var i = 0; i < objects.length; i++) { - statement.setObject(i + 1, objects[i]); - } - - // execute the statement - return statement.executeUpdate(); - } catch (SQLException exception) { - LOGGER.error("Exception while executing database update", exception); - return -1; - } - } - - @Override - public @UnknownNullability T executeQuery( - @NonNull String query, - @NonNull CheckedFunction1 callback, - @Nullable T def, - @NonNull Object... objects - ) { - try (var con = this.connection(); var statement = con.prepareStatement(query)) { - // write all parameters - for (var i = 0; i < objects.length; i++) { - statement.setObject(i + 1, objects[i]); - } - - // execute the statement, apply to the result handler - try (var resultSet = statement.executeQuery()) { - return callback.apply(resultSet); - } - } catch (Throwable throwable) { - LOGGER.error("Exception while executing database query", throwable); - } - - return def; - } -} diff --git a/modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabaseTest.java b/modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabaseTest.java deleted file mode 100644 index 0cba9af7d7..0000000000 --- a/modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/MySQLDatabaseTest.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.cloudnetservice.modules.mysql.impl; - -import eu.cloudnetservice.driver.document.Document; -import eu.cloudnetservice.driver.network.HostAndPort; -import eu.cloudnetservice.modules.mysql.config.MySQLConfiguration; -import eu.cloudnetservice.modules.mysql.config.MySQLConnectionEndpoint; -import eu.cloudnetservice.modules.mysql.impl.junit.EnableServicesInject; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -@EnableServicesInject -@Testcontainers(disabledWithoutDocker = true) -class MySQLDatabaseTest { - - @Container - private final GenericContainer mysqlContainer = new GenericContainer<>("mariadb:latest") - .withExposedPorts(3306) - .withEnv("MYSQL_USER", "test") - .withEnv("MYSQL_PASSWORD", "test") - .withEnv("MYSQL_ROOT_PASSWORD", "test") - .withEnv("MYSQL_DATABASE", "cn_testing") - .withCommand("mariadbd", "--ssl=0"); - - private MySQLDatabaseProvider databaseProvider; - - @BeforeEach - void setup() { - this.databaseProvider = new MySQLDatabaseProvider(new MySQLConfiguration( - "test", - "test", - "mysql", - List.of(new MySQLConnectionEndpoint( - "cn_testing", - new HostAndPort(this.mysqlContainer.getHost(), this.mysqlContainer.getFirstMappedPort())))), - null); - this.databaseProvider.init(); - } - - @Test - void testAccessCreatesDatabase() { - Assertions.assertNotNull(this.databaseProvider.database("hello_world")); - Assertions.assertNotNull(this.databaseProvider.database("hello2_world")); - - var names = this.databaseProvider.databaseNames(); - Assertions.assertTrue(names.contains("hello_world")); - Assertions.assertTrue(names.contains("hello2_world")); - } - - @Test - void testDatabaseDeletion() { - Assertions.assertNotNull(this.databaseProvider.database("hello_world")); - Assertions.assertNotNull(this.databaseProvider.database("hello2_world")); - - var names = this.databaseProvider.databaseNames(); - Assertions.assertTrue(names.contains("hello_world")); - Assertions.assertTrue(names.contains("hello2_world")); - - Assertions.assertTrue(this.databaseProvider.deleteDatabase("hello_world")); - Assertions.assertTrue(this.databaseProvider.deleteDatabase("hello2_world")); - - Assertions.assertTrue(this.databaseProvider.databaseNames().isEmpty()); - } - - @Test - void testBasicDatabaseOperations() { - var database = this.databaseProvider.database("test"); - Assertions.assertNotNull(database); - - Assertions.assertTrue(database.insert("1234", Document.newJsonDocument().append("hello", "world"))); - Assertions.assertTrue(database.insert("12234", Document.newJsonDocument().append("hello", "world2"))); - Assertions.assertTrue(database.insert("122234", Document.newJsonDocument().append("hello", "world_123"))); - - Assertions.assertTrue(database.contains("1234")); - Assertions.assertTrue(database.contains("12234")); - Assertions.assertTrue(database.contains("122234")); - - Assertions.assertEquals(3, database.documentCount()); - - var keys = database.keys(); - Assertions.assertEquals(3, keys.size()); - Assertions.assertTrue(keys.contains("1234")); - Assertions.assertTrue(keys.contains("12234")); - Assertions.assertTrue(keys.contains("122234")); - - var entry = database.get("1234"); - Assertions.assertNotNull(entry); - Assertions.assertEquals("world", entry.getString("hello")); - - var entry2 = database.get("12234"); - Assertions.assertNotNull(entry2); - Assertions.assertEquals("world2", entry2.getString("hello")); - - var entry3 = database.get("122334"); - Assertions.assertNull(entry3); - - var entry4 = database.find("hello", "world"); - Assertions.assertEquals(1, entry4.size()); - Assertions.assertEquals("world", entry4.iterator().next().getString("hello")); - - var entry5 = database.find(Map.of("hello", "world2")); - Assertions.assertEquals(1, entry5.size()); - Assertions.assertEquals("world2", entry5.iterator().next().getString("hello")); - - var entry6 = database.find("hello", "world_123"); - Assertions.assertEquals(1, entry6.size()); - Assertions.assertEquals("world_123", entry6.iterator().next().getString("hello")); - - var entries = database.entries(); - Assertions.assertEquals(3, entries.size()); - Assertions.assertEquals("world", entries.get("1234").getString("hello")); - Assertions.assertEquals("world2", entries.get("12234").getString("hello")); - - var documents = database.documents(); - Assertions.assertEquals(3, documents.size()); - - Assertions.assertTrue(database.delete("12234")); - Assertions.assertEquals(2, database.documentCount()); - - database.clear(); - Assertions.assertEquals(0, database.documentCount()); - - Assertions.assertFalse(database.delete("1234")); - } - - @Test - void testChunkedDataRead() { - var database = this.databaseProvider.database("test"); - Assertions.assertNotNull(database); - - // fill in some data - var entries = 1235; - List keys = new ArrayList<>(); - var expectedReadCounts = (int) Math.ceil(entries / 50D); - - for (var i = 0; i < entries; i++) { - var key = UUID.randomUUID().toString(); - - keys.add(key); - database.insert(key, Document.newJsonDocument().append("this_is", "a_world_test")); - } - - Assertions.assertEquals(entries, database.documentCount()); - - var index = 0; - var readsCalled = 0; - - Map currentChunk; - while ((currentChunk = database.readChunk(index, 50)) != null) { - index += 50; - readsCalled++; - - Assertions.assertFalse(currentChunk.size() > 50); - Assertions.assertTrue(keys.removeAll(currentChunk.keySet())); - } - - Assertions.assertEquals(expectedReadCounts, readsCalled); - Assertions.assertTrue(keys.isEmpty()); - } -} diff --git a/modules/database-mysql/api/build.gradle.kts b/modules/database-sql/api/build.gradle.kts similarity index 100% rename from modules/database-mysql/api/build.gradle.kts rename to modules/database-sql/api/build.gradle.kts diff --git a/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/DatabaseType.java b/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/DatabaseType.java new file mode 100644 index 0000000000..6dce28accc --- /dev/null +++ b/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/DatabaseType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.config; + +public enum DatabaseType { + POSTGRES, + MYSQL, + MARIADB, + SQLITE +} diff --git a/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/SQLConfigurationEntry.java b/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/SQLConfigurationEntry.java new file mode 100644 index 0000000000..677e84cd0a --- /dev/null +++ b/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/SQLConfigurationEntry.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.config; + +import eu.cloudnetservice.driver.network.HostAndPort; +import java.util.Objects; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +public record SQLConfigurationEntry( + boolean enabled, + @NonNull DatabaseType databaseType, + @NonNull String databaseServiceName, + @Nullable String databaseName, + @Nullable String username, + @Nullable String password, + @Nullable HostAndPort address, + @Nullable String overrideConnectionUri +) { + + private static final String JDBC_URI = "jdbc:%s://%s:%d/%s"; + + public @NonNull String buildConnectionUri() { + if (this.overrideConnectionUri != null && !this.overrideConnectionUri.isBlank()) { + return this.overrideConnectionUri; + } + + Objects.requireNonNull(this.address, "Address must be set if no override connection uri is set"); + Objects.requireNonNull(this.username, "Username must be set if no override connection uri is set"); + Objects.requireNonNull(this.password, "Password must be set if no override connection uri is set"); + Objects.requireNonNull(this.databaseName, "Database name must be set if no override connection uri is set"); + var jdbcDescriptor = switch (this.databaseType) { + case MYSQL -> "mysql"; + case MARIADB -> "mariadb"; + case POSTGRES -> "postgresql"; + case SQLITE -> "sqlite"; + }; + + return String.format(JDBC_URI, jdbcDescriptor, this.address.host(), this.address.port(), this.databaseName); + } +} diff --git a/modules/database-mysql/api/src/main/java/eu/cloudnetservice/modules/mysql/config/MySQLConnectionEndpoint.java b/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/SQLModuleConfiguration.java similarity index 76% rename from modules/database-mysql/api/src/main/java/eu/cloudnetservice/modules/mysql/config/MySQLConnectionEndpoint.java rename to modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/SQLModuleConfiguration.java index ca16efa083..cf2f2ed772 100644 --- a/modules/database-mysql/api/src/main/java/eu/cloudnetservice/modules/mysql/config/MySQLConnectionEndpoint.java +++ b/modules/database-sql/api/src/main/java/eu/cloudnetservice/modules/sql/config/SQLModuleConfiguration.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package eu.cloudnetservice.modules.mysql.config; +package eu.cloudnetservice.modules.sql.config; -import eu.cloudnetservice.driver.network.HostAndPort; +import java.util.List; import lombok.NonNull; -public record MySQLConnectionEndpoint(@NonNull String database, @NonNull HostAndPort address) { +public record SQLModuleConfiguration(@NonNull List entries) { } diff --git a/modules/database-mysql/impl/build.gradle.kts b/modules/database-sql/impl/build.gradle.kts similarity index 80% rename from modules/database-mysql/impl/build.gradle.kts rename to modules/database-sql/impl/build.gradle.kts index 96c51ba558..3f1fd59f86 100644 --- a/modules/database-mysql/impl/build.gradle.kts +++ b/modules/database-sql/impl/build.gradle.kts @@ -23,24 +23,24 @@ plugins { } dependencies { - moduleLibrary(libs.bundles.mysql) { + moduleLibrary(libs.bundles.sql) { exclude("com.google.protobuf") } compileOnly(libs.caffeine) compileOnlyApi(projects.node.nodeImpl) - api(projects.modules.databaseMysql.databaseMysqlApi) + api(projects.modules.databaseSql.databaseSqlApi) } tasks.shadowJar.configure { - archiveFileName = Files.databaseMysql + archiveFileName = Files.databaseSql } moduleJson { author = "CloudNetService" - name = "CloudNet-Database-MySQL" - main = "eu.cloudnetservice.modules.mysql.impl.CloudNetMySQLDatabaseModule" - description = "CloudNet extension, which includes the database support for MySQL and MariaDB" + name = "CloudNet-Database-SQL" + main = "eu.cloudnetservice.modules.sql.impl.CloudNetSQLModule" + description = "CloudNet extension, which includes the database support for MySQL, MariaDB, PostgreSQL and SQLite." minJavaVersionId = JavaVersion.VERSION_11 runtimeModule = true storesSensitiveData = true diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/CloudNetSQLDatabaseModule.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/CloudNetSQLDatabaseModule.java new file mode 100644 index 0000000000..9e30ccf7f5 --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/CloudNetSQLDatabaseModule.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.driver.document.Document; +import eu.cloudnetservice.driver.document.DocumentFactory; +import eu.cloudnetservice.driver.module.ModuleLifeCycle; +import eu.cloudnetservice.driver.module.ModuleTask; +import eu.cloudnetservice.driver.module.driver.DriverModule; +import eu.cloudnetservice.driver.network.HostAndPort; +import eu.cloudnetservice.driver.registry.ServiceRegistry; +import eu.cloudnetservice.modules.sql.config.DatabaseType; +import eu.cloudnetservice.modules.sql.config.SQLConfigurationEntry; +import eu.cloudnetservice.modules.sql.config.SQLModuleConfiguration; +import eu.cloudnetservice.node.impl.database.NodeDatabaseProvider; +import io.leangen.geantyref.TypeFactory; +import jakarta.inject.Singleton; +import java.util.List; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +@Singleton +public final class CloudNetSQLDatabaseModule extends DriverModule { + + private volatile SQLModuleConfiguration configuration; + + @ModuleTask(order = 127, lifecycle = ModuleLifeCycle.LOADED) + public void convertConfig() { + var config = this.readConfig(DocumentFactory.json()); + if (!config.contains("overrideConnectionUri")) { + return; + } + + var serviceName = config.getString("databaseServiceName"); + var username = config.getString("username"); + var password = config.getString("password"); + + String database; + HostAndPort address; + if (config.contains("addresses")) { + List addresses = config.readObject( + "addresses", + TypeFactory.parameterizedClass(List.class, LegacyConnectionEndpoint.class)); + database = config.getString("database"); + address = addresses.isEmpty() ? new HostAndPort("127.0.0.1", 3306) : addresses.getFirst().address(); + } else { + List endpoints = config.readObject( + "endpoints", + TypeFactory.parameterizedClass(List.class, LegacyConnectionEndpoint.class)); + var endpoint = endpoints.isEmpty() + ? new LegacyConnectionEndpoint("cloudnet", new HostAndPort("127.0.0.1", 3306)) + : endpoints.getFirst(); + database = endpoint.database(); + address = endpoint.address(); + } + + var convertedConfig = new SQLConfigurationEntry( + true, + DatabaseType.MYSQL, + serviceName, + database, + username, + password, + address, + null); + this.writeConfig(Document.newJsonDocument().appendTree(convertedConfig)); + } + + @ModuleTask(order = 125, lifecycle = ModuleLifeCycle.LOADED) + public void registerDatabaseProvider(@NonNull ServiceRegistry serviceRegistry) { + this.configuration = this.readConfig( + SQLModuleConfiguration.class, + () -> new SQLModuleConfiguration(List.of(new SQLConfigurationEntry( + false, + DatabaseType.MYSQL, + "sql", + "cloudnet", + "cloudnet", + "password", + new HostAndPort("127.0.0.1", 3306), + null + ))), + DocumentFactory.json()); + + for (var entry : this.configuration.entries()) { + if (!entry.enabled()) { + continue; + } + + var databaseType = JooqDatabaseType.fromDatabaseType(entry.databaseType()); + serviceRegistry.registerProvider( + NodeDatabaseProvider.class, + entry.databaseServiceName(), + databaseType.createProvider(entry)); + } + } + + @ModuleTask(order = 127, lifecycle = ModuleLifeCycle.STOPPED) + public void unregisterDatabaseProvider(@NonNull ServiceRegistry serviceRegistry) { + if (this.configuration == null) { + return; + } + + for (var entry : this.configuration.entries()) { + if (!entry.enabled()) { + continue; + } + + var service = serviceRegistry.registration(NodeDatabaseProvider.class, entry.databaseServiceName()); + if (service != null) { + service.unregister(); + } + } + } + + @Deprecated + record LegacyConnectionEndpoint(@Nullable String database, @NonNull HostAndPort address) { + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/DocumentConverter.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/DocumentConverter.java new file mode 100644 index 0000000000..c5d27fd93a --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/DocumentConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.driver.document.Document; +import eu.cloudnetservice.driver.document.DocumentFactory; +import lombok.NonNull; +import org.jooq.Converter; +import org.jooq.JSONB; + +public class DocumentConverter implements Converter { + + public static final DocumentConverter INSTANCE = new DocumentConverter(); + + private DocumentConverter() { + } + + @Override + public Document from(JSONB databaseObject) { + return DocumentFactory.json().parse(databaseObject.data()); + } + + @Override + public JSONB to(Document userObject) { + return JSONB.valueOf(userObject.toString()); + } + + @Override + public @NonNull Class fromType() { + return JSONB.class; + } + + @Override + public @NonNull Class toType() { + return Document.class; + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqDatabase.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqDatabase.java new file mode 100644 index 0000000000..80f5766629 --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqDatabase.java @@ -0,0 +1,165 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.driver.document.Document; +import eu.cloudnetservice.node.impl.database.AbstractDatabase; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.JSONB; +import org.jooq.Name; +import org.jooq.Record; +import org.jooq.Record1; +import org.jooq.Table; +import org.jooq.impl.DSL; + +public class JooqDatabase extends AbstractDatabase { + + static final Name KEY_FIELD_NAME = DSL.name("Name"); + static final Field KEY_FIELD = DSL.field(KEY_FIELD_NAME, String.class); + static final Name DOCUMENT_FIELD_NAME = DSL.name("Document"); + static final Field DOCUMENT_FIELD = DSL + .field(DOCUMENT_FIELD_NAME, JSONB.class) + .convert(DocumentConverter.INSTANCE); + + protected final Name dslName; + protected final Table dslTable; + protected final DSLContext dslContext; + + public JooqDatabase( + @NonNull String name, + @NonNull JooqProvider databaseProvider, + @NonNull DSLContext dslContext + ) { + super(name, databaseProvider); + + this.dslName = DSL.name(name); + this.dslTable = DSL.table(this.dslName); + this.dslContext = dslContext; + } + + @Override + public @Nullable Map readChunk(long beginIndex, int chunkSize) { + var chunk = this.dslContext.select(KEY_FIELD, DOCUMENT_FIELD) + .from(this.dslTable) + .limit(chunkSize) + .offset(beginIndex) + .fetchMap(KEY_FIELD, DOCUMENT_FIELD); + return chunk.isEmpty() ? null : chunk; + } + + @Override + public boolean insert(@NonNull String key, @NonNull Document document) { + return this.dslContext.insertInto(this.dslTable) + .set(KEY_FIELD, key) + .set(DOCUMENT_FIELD, document) + .onConflict(KEY_FIELD) + .doUpdate() + .set(DOCUMENT_FIELD, document) + .execute() > 0; + } + + @Override + public boolean contains(@NonNull String key) { + return this.dslContext.fetchExists(this.dslContext.selectOne().from(this.dslTable).where(KEY_FIELD.eq(key))); + } + + @Override + public boolean delete(@NonNull String key) { + return this.dslContext.delete(this.dslTable).where(KEY_FIELD.eq(key)).execute() > 0; + } + + @Override + public @Nullable Document get(@NonNull String key) { + return this.dslContext + .select(DOCUMENT_FIELD) + .from(this.dslTable) + .where(KEY_FIELD.eq(key)) + .fetchOptional() + .map(Record1::value1) + .orElse(null); + } + + @Override + public @NonNull Collection find(@NonNull String fieldName, @Nullable String fieldValue) { + Map filters = HashMap.newHashMap(1); + filters.put(fieldName, fieldValue); + return this.find(filters); + } + + @Override + public @NonNull Collection find(@NonNull Map filters) { + List conditions = new ArrayList<>(); + for (var entry : filters.entrySet()) { + var jsonAttribute = DSL.jsonbGetAttributeAsText(DSL.field(DOCUMENT_FIELD_NAME, JSONB.class), entry.getKey()); + conditions.add(jsonAttribute.eq(entry.getValue())); + } + + return this.dslContext + .select(DOCUMENT_FIELD) + .from(this.dslTable) + .where(conditions) + .fetch() + .getValues(DOCUMENT_FIELD); + } + + @Override + public @NonNull Collection keys() { + return this.dslContext + .select(KEY_FIELD) + .from(this.dslName) + .fetch() + .getValues(KEY_FIELD); + } + + @Override + public @NonNull Collection documents() { + return this.dslContext.select(DOCUMENT_FIELD).from(this.dslTable).fetch().getValues(DOCUMENT_FIELD); + } + + @Override + public @NonNull Map entries() { + return this.dslContext.select(KEY_FIELD, DOCUMENT_FIELD).from(this.dslTable).fetchMap(KEY_FIELD, DOCUMENT_FIELD); + } + + @Override + public void clear() { + this.dslContext.truncate(this.name).execute(); + } + + @Override + public long documentCount() { + return this.dslContext.fetchCount(DSL.table(this.dslName)); + } + + @Override + public boolean synced() { + return this.databaseProvider.synced(); + } + + @Override + public void close() { + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqDatabaseType.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqDatabaseType.java new file mode 100644 index 0000000000..ed434c6db0 --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqDatabaseType.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.modules.sql.config.DatabaseType; +import eu.cloudnetservice.modules.sql.config.SQLConfigurationEntry; +import eu.cloudnetservice.modules.sql.impl.table.MariaDBTableCreator; +import eu.cloudnetservice.modules.sql.impl.table.MySQLTableCreator; +import eu.cloudnetservice.modules.sql.impl.table.PostgreSQLTableCreator; +import eu.cloudnetservice.modules.sql.impl.table.SQLiteTableCreator; +import eu.cloudnetservice.modules.sql.impl.table.TableCreator; +import lombok.NonNull; +import org.jetbrains.annotations.UnknownNullability; +import org.jooq.SQLDialect; + +public enum JooqDatabaseType { + MYSQL( + "com.mysql.cj.jdbc.Driver", + SQLDialect.MYSQL, + true, + new MySQLTableCreator() + ), + MARIADB( + "org.mariadb.jdbc.Driver", + SQLDialect.MARIADB, + true, + new MariaDBTableCreator() + ), + POSTGRESQL( + "org.postgresql.Driver", + SQLDialect.POSTGRES, + true, + new PostgreSQLTableCreator() + ), + SQLITE( + "org.sqlite.JDBC", + SQLDialect.SQLITE, + false, + new SQLiteTableCreator() + ); + + private final String driverClassName; + private final SQLDialect jooqDialect; + private final boolean synced; + private final TableCreator tableCreator; + + JooqDatabaseType( + @NonNull String driverClassName, + @NonNull SQLDialect jooqDialect, + boolean synced, + @NonNull TableCreator tableCreator + ) { + this.driverClassName = driverClassName; + this.jooqDialect = jooqDialect; + this.synced = synced; + this.tableCreator = tableCreator; + } + + public static @NonNull JooqDatabaseType fromDatabaseType(@UnknownNullability DatabaseType databaseType) { + return switch (databaseType) { + case MYSQL -> MYSQL; + case MARIADB -> MARIADB; + case POSTGRES -> POSTGRESQL; + case SQLITE -> SQLITE; + }; + } + + public @NonNull String driverClassName() { + return this.driverClassName; + } + + public @NonNull SQLDialect jooqDialect() { + return this.jooqDialect; + } + + public boolean synced() { + return this.synced; + } + + public @NonNull JooqProvider createProvider(@NonNull SQLConfigurationEntry config) { + return new JooqProvider(this.tableCreator, this, config); + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqProvider.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqProvider.java new file mode 100644 index 0000000000..8d2f6196f5 --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/JooqProvider.java @@ -0,0 +1,134 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import eu.cloudnetservice.modules.sql.config.SQLConfigurationEntry; +import eu.cloudnetservice.modules.sql.impl.table.TableCreator; +import eu.cloudnetservice.node.database.LocalDatabase; +import eu.cloudnetservice.node.impl.database.AbstractNodeDatabaseProvider; +import java.util.Collection; +import lombok.NonNull; +import org.jooq.DSLContext; +import org.jooq.Table; +import org.jooq.TableOptions; +import org.jooq.impl.DSL; + +public class JooqProvider extends AbstractNodeDatabaseProvider { + + protected final TableCreator tableCreator; + protected final JooqDatabaseType databaseType; + protected final SQLConfigurationEntry config; + + protected DSLContext dslContext; + protected HikariDataSource dataSource; + + protected JooqProvider( + @NonNull TableCreator tableCreator, + @NonNull JooqDatabaseType databaseType, + @NonNull SQLConfigurationEntry config + ) { + super(DEFAULT_REMOVAL_LISTENER); + + this.tableCreator = tableCreator; + this.databaseType = databaseType; + this.config = config; + } + + @Override + public boolean init() { + var hikariConfig = new HikariConfig(); + var endpoint = this.config.buildConnectionUri(); + + hikariConfig.setJdbcUrl(endpoint); + hikariConfig.setDriverClassName(this.databaseType.driverClassName()); + hikariConfig.setUsername(this.config.username()); + hikariConfig.setPassword(this.config.password()); + + hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + hikariConfig.addDataSourceProperty("useServerPrepStmts", "true"); + hikariConfig.addDataSourceProperty("useLocalSessionState", "true"); + hikariConfig.addDataSourceProperty("rewriteBatchedStatements", "true"); + hikariConfig.addDataSourceProperty("cacheResultSetMetadata", "true"); + hikariConfig.addDataSourceProperty("cacheServerConfiguration", "true"); + hikariConfig.addDataSourceProperty("elideSetAutoCommits", "true"); + hikariConfig.addDataSourceProperty("maintainTimeStats", "false"); + + hikariConfig.setMinimumIdle(2); + hikariConfig.setMaximumPoolSize(10); + hikariConfig.setConnectionTimeout(10_000); + hikariConfig.setValidationTimeout(10_000); + + this.dataSource = new HikariDataSource(hikariConfig); + this.dslContext = DSL.using(this.dataSource, this.databaseType.jooqDialect()); + return true; + } + + @Override + public boolean synced() { + return this.databaseType.synced(); + } + + @Override + public @NonNull LocalDatabase database(@NonNull String name) { + return this.databaseCache.get(name, _ -> { + this.tableCreator.createTable( + this.dslContext, + name, + JooqDatabase.KEY_FIELD, + JooqDatabase.DOCUMENT_FIELD); + return new JooqDatabase(name, this, this.dslContext); + }); + } + + @Override + public boolean containsDatabase(@NonNull String name) { + return this.databaseNames().stream().anyMatch(dbName -> dbName.equalsIgnoreCase(name)); + } + + @Override + public boolean deleteDatabase(@NonNull String name) { + this.databaseCache.invalidate(name); + return this.dslContext.dropTableIfExists(name).execute() != -1; + } + + @Override + public @NonNull Collection databaseNames() { + return this.dslContext.meta() + .getTables() + .stream() + .filter(table -> table.getTableType() == TableOptions.TableType.TABLE) + .filter(table -> table.field(JooqDatabase.KEY_FIELD_NAME) != null) + .filter(table -> table.field(JooqDatabase.DOCUMENT_FIELD_NAME) != null) + .map(Table::getName) + .toList(); + } + + @Override + public @NonNull String name() { + return this.config.databaseServiceName(); + } + + @Override + public void close() throws Exception { + super.close(); + this.dataSource.close(); + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/MariaDBTableCreator.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/MariaDBTableCreator.java new file mode 100644 index 0000000000..48864a3a4a --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/MariaDBTableCreator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl.table; + +import eu.cloudnetservice.driver.document.Document; +import lombok.NonNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; + +public class MariaDBTableCreator implements TableCreator { + + @Override + public void createTable( + @NonNull DSLContext dslContext, + @NonNull String name, + @NonNull Field keyField, + @NonNull Field documentField + ) { + dslContext.createTableIfNotExists(DSL.name(name)) + .column(keyField, SQLDataType.VARCHAR(512) + .notNull() + .collation(DSL.collation("utf8mb4_bin"))) + .column(documentField, SQLDataType.JSONB.notNull()) + .primaryKey(keyField) + .execute(); + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/MySQLTableCreator.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/MySQLTableCreator.java new file mode 100644 index 0000000000..35971b01e6 --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/MySQLTableCreator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl.table; + +import eu.cloudnetservice.driver.document.Document; +import lombok.NonNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; + +public class MySQLTableCreator implements TableCreator { + + @Override + public void createTable( + @NonNull DSLContext dslContext, + @NonNull String name, + @NonNull Field keyField, + @NonNull Field documentField + ) { + dslContext.createTableIfNotExists(DSL.name(name)) + .column(keyField, SQLDataType.VARCHAR(512) + .notNull() + .collation(DSL.collation("utf8mb4_bin"))) + .column(documentField, SQLDataType.JSONB.notNull()) + .primaryKey(keyField) + .execute(); + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/PostgreSQLTableCreator.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/PostgreSQLTableCreator.java new file mode 100644 index 0000000000..1fbfaa1844 --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/PostgreSQLTableCreator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl.table; + +import eu.cloudnetservice.driver.document.Document; +import lombok.NonNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; + +public class PostgreSQLTableCreator implements TableCreator { + + @Override + public void createTable( + @NonNull DSLContext dslContext, + @NonNull String name, + @NonNull Field keyField, + @NonNull Field documentField + ) { + dslContext.createTableIfNotExists(DSL.name(name)) + .column(keyField, SQLDataType.VARCHAR(512).notNull()) + .column(documentField, SQLDataType.JSONB.notNull()) + .primaryKey(keyField) + .execute(); + } +} diff --git a/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/SQLiteTableCreator.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/SQLiteTableCreator.java new file mode 100644 index 0000000000..c6669b17ec --- /dev/null +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/SQLiteTableCreator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl.table; + +import eu.cloudnetservice.driver.document.Document; +import lombok.NonNull; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; + +public class SQLiteTableCreator implements TableCreator { + + @Override + public void createTable( + @NonNull DSLContext dslContext, + @NonNull String name, + @NonNull Field keyField, + @NonNull Field documentField + ) { + dslContext.createTableIfNotExists(DSL.name(name)) + .column(keyField, SQLDataType.VARCHAR(512).notNull()) + .column(documentField, SQLDataType.JSON.notNull()) + .primaryKey(keyField) + .execute(); + } +} diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/sql/SQLDatabase.java b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/TableCreator.java similarity index 51% rename from node/impl/src/main/java/eu/cloudnetservice/node/impl/database/sql/SQLDatabase.java rename to modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/TableCreator.java index 9a4a3409b3..d3f2915276 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/sql/SQLDatabase.java +++ b/modules/database-sql/impl/src/main/java/eu/cloudnetservice/modules/sql/impl/table/TableCreator.java @@ -14,23 +14,20 @@ * limitations under the License. */ -package eu.cloudnetservice.node.impl.database.sql; +package eu.cloudnetservice.modules.sql.impl.table; -import eu.cloudnetservice.node.impl.database.AbstractDatabase; +import eu.cloudnetservice.driver.document.Document; import lombok.NonNull; -import org.jetbrains.annotations.ApiStatus; +import org.jooq.DSLContext; +import org.jooq.Field; -@Deprecated -@ApiStatus.ScheduledForRemoval(inVersion = "4.1") -public abstract class SQLDatabase extends AbstractDatabase { +@FunctionalInterface +public interface TableCreator { - protected static final String TABLE_COLUMN_KEY = "Name"; - protected static final String TABLE_COLUMN_VAL = "Document"; + void createTable( + @NonNull DSLContext dslContext, + @NonNull String name, + @NonNull Field keyField, + @NonNull Field documentField); - protected final SQLDatabaseProvider databaseProvider; - - public SQLDatabase(@NonNull SQLDatabaseProvider provider, @NonNull String name) { - super(name, provider); - this.databaseProvider = provider; - } } diff --git a/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/MariaDBDatabaseTest.java b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/MariaDBDatabaseTest.java new file mode 100644 index 0000000000..e5cee93af5 --- /dev/null +++ b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/MariaDBDatabaseTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.driver.network.HostAndPort; +import eu.cloudnetservice.modules.sql.config.DatabaseType; +import eu.cloudnetservice.modules.sql.config.SQLConfigurationEntry; +import eu.cloudnetservice.modules.sql.impl.junit.EnableServicesInject; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@EnableServicesInject +@Testcontainers(disabledWithoutDocker = true) +public class MariaDBDatabaseTest extends SQLDatabaseTest { + + @Container + private static final GenericContainer MARIA_CONTAINER = new GenericContainer<>("mariadb:latest") + .withExposedPorts(3306) + .withEnv("MYSQL_USER", "test") + .withEnv("MYSQL_PASSWORD", "test") + .withEnv("MYSQL_ROOT_PASSWORD", "test") + .withEnv("MYSQL_DATABASE", "cn_testing") + .withCommand("mariadbd", "--ssl=0"); + + @BeforeAll + static void setup() throws Exception { + var config = new SQLConfigurationEntry( + true, + DatabaseType.MARIADB, + "mariadb", + "cn_testing", + "test", + "test", + new HostAndPort(MARIA_CONTAINER.getHost(), MARIA_CONTAINER.getFirstMappedPort()), + null); + databaseProvider = JooqDatabaseType.MARIADB.createProvider(config); + databaseProvider.init(); + } +} diff --git a/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/MySQLDBDatabaseTest.java b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/MySQLDBDatabaseTest.java new file mode 100644 index 0000000000..65071a2636 --- /dev/null +++ b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/MySQLDBDatabaseTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.driver.network.HostAndPort; +import eu.cloudnetservice.modules.sql.config.DatabaseType; +import eu.cloudnetservice.modules.sql.config.SQLConfigurationEntry; +import eu.cloudnetservice.modules.sql.impl.junit.EnableServicesInject; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@EnableServicesInject +@Testcontainers(disabledWithoutDocker = true) +public class MySQLDBDatabaseTest extends SQLDatabaseTest { + + @Container + private static final GenericContainer MYSQL_CONTAINER = new GenericContainer<>("mysql:latest") + .withExposedPorts(3306) + .withEnv("MYSQL_USER", "test") + .withEnv("MYSQL_PASSWORD", "test") + .withEnv("MYSQL_ROOT_PASSWORD", "test") + .withEnv("MYSQL_DATABASE", "cn_testing"); + + @BeforeAll + static void setup() throws Exception { + var config = new SQLConfigurationEntry( + true, + DatabaseType.MYSQL, + "mysql", + "cn_testing", + "test", + "test", + new HostAndPort(MYSQL_CONTAINER.getHost(), MYSQL_CONTAINER.getFirstMappedPort()), + null); + databaseProvider = JooqDatabaseType.MYSQL.createProvider(config); + databaseProvider.init(); + } +} diff --git a/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/PostgreSQLDatabaseTest.java b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/PostgreSQLDatabaseTest.java new file mode 100644 index 0000000000..98401d8212 --- /dev/null +++ b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/PostgreSQLDatabaseTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.driver.network.HostAndPort; +import eu.cloudnetservice.modules.sql.config.DatabaseType; +import eu.cloudnetservice.modules.sql.config.SQLConfigurationEntry; +import eu.cloudnetservice.modules.sql.impl.junit.EnableServicesInject; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@EnableServicesInject +@Testcontainers(disabledWithoutDocker = true) +public class PostgreSQLDatabaseTest extends SQLDatabaseTest { + + @Container + private static final GenericContainer POSTGRES_CONTAINER = new GenericContainer<>("postgres:latest") + .withExposedPorts(5432) + .withEnv("POSTGRES_USER", "test") + .withEnv("POSTGRES_PASSWORD", "test") + .withEnv("POSTGRES_DB", "cn_testing") + .withCommand("postgres", "-c", "fsync=off"); + + @BeforeAll + static void setup() throws Exception { + var config = new SQLConfigurationEntry( + true, + DatabaseType.POSTGRES, + "postgres", + "cn_testing", + "test", + "test", + new HostAndPort(POSTGRES_CONTAINER.getHost(), POSTGRES_CONTAINER.getFirstMappedPort()), + null); + databaseProvider = JooqDatabaseType.POSTGRESQL.createProvider(config); + databaseProvider.init(); + } +} diff --git a/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/SQLDatabaseTest.java b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/SQLDatabaseTest.java new file mode 100644 index 0000000000..53db13b290 --- /dev/null +++ b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/SQLDatabaseTest.java @@ -0,0 +1,294 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.driver.document.Document; +import eu.cloudnetservice.node.impl.database.NodeDatabaseProvider; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +abstract class SQLDatabaseTest { + + protected static NodeDatabaseProvider databaseProvider; + + @AfterEach + void cleanupDatabases() { + for (var dbName : databaseProvider.databaseNames()) { + databaseProvider.deleteDatabase(dbName); + } + } + + @Test + void testDatabaseProviderOperations() { + Assertions.assertNotNull(databaseProvider.database("hello_world")); + Assertions.assertNotNull(databaseProvider.database("hello2_world")); + + var names = databaseProvider.databaseNames(); + Assertions.assertTrue(names.contains("hello_world")); + Assertions.assertTrue(names.contains("hello2_world")); + + Assertions.assertTrue(databaseProvider.deleteDatabase("hello_world")); + Assertions.assertTrue(databaseProvider.deleteDatabase("hello2_world")); + Assertions.assertTrue(databaseProvider.databaseNames().isEmpty()); + } + + @Test + void testBasicOperations() { + var database = databaseProvider.database("test"); + Assertions.assertNotNull(database); + + Assertions.assertTrue(database.insert("1234", Document.newJsonDocument().append("hello", "world"))); + Assertions.assertTrue(database.insert("12234", Document.newJsonDocument().append("hello", "world2"))); + Assertions.assertTrue(database.insert("122234", Document.newJsonDocument().append("hello", "world_123"))); + + Assertions.assertTrue(database.contains("1234")); + Assertions.assertTrue(database.contains("12234")); + Assertions.assertTrue(database.contains("122234")); + + Assertions.assertFalse(database.contains("non_existent_key")); + Assertions.assertFalse(database.contains(UUID.randomUUID().toString())); + + Assertions.assertEquals(3, database.documentCount()); + + var entry = database.get("1234"); + Assertions.assertNotNull(entry); + Assertions.assertEquals("world", entry.getString("hello")); + + var entry2 = database.get("12234"); + Assertions.assertNotNull(entry2); + Assertions.assertEquals("world2", entry2.getString("hello")); + + Assertions.assertNull(database.get("122334")); + Assertions.assertNull(database.get("non_existent_key")); + + Assertions.assertTrue(database.insert("1234", Document.newJsonDocument().append("hello", "updated"))); + var updatedDoc = database.get("1234"); + Assertions.assertNotNull(updatedDoc); + Assertions.assertEquals("updated", updatedDoc.getString("hello")); + Assertions.assertEquals(3, database.documentCount()); + + Assertions.assertTrue(database.delete("12234")); + Assertions.assertEquals(2, database.documentCount()); + Assertions.assertFalse(database.contains("12234")); + + Assertions.assertFalse(database.delete("non_existent_key")); + Assertions.assertFalse(database.delete(UUID.randomUUID().toString())); + } + + @Test + void testCollectionOperations() { + var database = databaseProvider.database("test"); + Assertions.assertNotNull(database); + + Assertions.assertTrue(database.keys().isEmpty()); + Assertions.assertTrue(database.documents().isEmpty()); + Assertions.assertTrue(database.entries().isEmpty()); + Assertions.assertEquals(0, database.documentCount()); + + database.insert("key1", Document.newJsonDocument().append("data", "value1")); + database.insert("key2", Document.newJsonDocument().append("data", "value2")); + database.insert("key3", Document.newJsonDocument().append("data", "value3")); + + var keys = database.keys(); + Assertions.assertEquals(3, keys.size()); + Assertions.assertTrue(keys.contains("key1")); + Assertions.assertTrue(keys.contains("key2")); + Assertions.assertTrue(keys.contains("key3")); + + var documents = database.documents(); + Assertions.assertEquals(3, documents.size()); + + var entries = database.entries(); + Assertions.assertEquals(3, entries.size()); + Assertions.assertNotNull(entries.get("key1")); + Assertions.assertNotNull(entries.get("key2")); + Assertions.assertNotNull(entries.get("key3")); + Assertions.assertEquals("value1", entries.get("key1").getString("data")); + Assertions.assertEquals("value2", entries.get("key2").getString("data")); + Assertions.assertEquals("value3", entries.get("key3").getString("data")); + } + + @Test + void testFindOperations() { + var database = databaseProvider.database("test"); + Assertions.assertNotNull(database); + + database.insert("key1", Document.newJsonDocument() + .append("name", "Alice") + .append("age", "30") + .append("city", "Berlin")); + database.insert("key2", Document.newJsonDocument() + .append("name", "Bob") + .append("age", "30") + .append("city", "Munich")); + database.insert("key3", Document.newJsonDocument() + .append("name", "Charlie") + .append("age", "25") + .append("city", "Berlin")); + + var findByName = database.find("name", "Alice"); + Assertions.assertEquals(1, findByName.size()); + Assertions.assertEquals("Alice", findByName.iterator().next().getString("name")); + + var findByAge = database.find(Map.of("age", "30")); + Assertions.assertEquals(2, findByAge.size()); + + var findByCity = database.find(Map.of("city", "Berlin")); + Assertions.assertEquals(2, findByCity.size()); + + Map multiFilters = new HashMap<>(); + multiFilters.put("age", "30"); + multiFilters.put("city", "Berlin"); + var findMulti = database.find(multiFilters); + Assertions.assertEquals(1, findMulti.size()); + Assertions.assertEquals("Alice", findMulti.iterator().next().getString("name")); + + var noMatches = database.find("name", "NonExistent"); + Assertions.assertNotNull(noMatches); + Assertions.assertTrue(noMatches.isEmpty()); + + var noMatchesMap = database.find(Map.of("name", "NonExistent")); + Assertions.assertNotNull(noMatchesMap); + Assertions.assertTrue(noMatchesMap.isEmpty()); + + var nullResults = database.find("name", null); + Assertions.assertNotNull(nullResults); + } + + @Test + void testClearOperations() { + var database = databaseProvider.database("test"); + Assertions.assertNotNull(database); + + Assertions.assertDoesNotThrow(database::clear); + Assertions.assertEquals(0, database.documentCount()); + + database.insert("key1", Document.newJsonDocument().append("data", "1")); + database.insert("key2", Document.newJsonDocument().append("data", "2")); + database.insert("key3", Document.newJsonDocument().append("data", "3")); + Assertions.assertEquals(3, database.documentCount()); + + database.clear(); + Assertions.assertEquals(0, database.documentCount()); + Assertions.assertTrue(database.keys().isEmpty()); + Assertions.assertTrue(database.documents().isEmpty()); + Assertions.assertTrue(database.entries().isEmpty()); + Assertions.assertFalse(database.contains("key1")); + Assertions.assertFalse(database.contains("key2")); + Assertions.assertFalse(database.contains("key3")); + + Assertions.assertFalse(database.delete("key1")); + } + + @Test + void testChunkedDataRead() { + var database = databaseProvider.database("test"); + Assertions.assertNotNull(database); + + var entries = 1235; + List keys = new ArrayList<>(); + var expectedReadCounts = (int) Math.ceil(entries / 50D); + + for (var i = 0; i < entries; i++) { + var key = UUID.randomUUID().toString(); + keys.add(key); + database.insert(key, Document.newJsonDocument().append("this_is", "a_world_test")); + } + + Assertions.assertEquals(entries, database.documentCount()); + + var index = 0; + var readsCalled = 0; + + Map currentChunk; + while ((currentChunk = database.readChunk(index, 50)) != null) { + index += 50; + readsCalled++; + + Assertions.assertFalse(currentChunk.size() > 50); + Assertions.assertTrue(keys.removeAll(currentChunk.keySet())); + } + + Assertions.assertEquals(expectedReadCounts, readsCalled); + Assertions.assertTrue(keys.isEmpty()); + } + + @Test + void testSpecialKeysAndValues() { + var database = databaseProvider.database("test"); + Assertions.assertNotNull(database); + + database.insert("Key", Document.newJsonDocument().append("value", "uppercase")); + database.insert("key", Document.newJsonDocument().append("value", "lowercase")); + database.insert("KEY", Document.newJsonDocument().append("value", "alluppercase")); + + Assertions.assertEquals(3, database.documentCount()); + Assertions.assertTrue(database.contains("Key")); + Assertions.assertTrue(database.contains("key")); + Assertions.assertTrue(database.contains("KEY")); + + var upperCaseKey = database.get("Key"); + var lowerCaseKey = database.get("key"); + var fullUpperCaseKey = database.get("KEY"); + + Assertions.assertNotNull(upperCaseKey); + Assertions.assertNotNull(lowerCaseKey); + Assertions.assertNotNull(fullUpperCaseKey); + Assertions.assertEquals("uppercase", upperCaseKey.getString("value")); + Assertions.assertEquals("lowercase", lowerCaseKey.getString("value")); + Assertions.assertEquals("alluppercase", fullUpperCaseKey.getString("value")); + + database.clear(); + + var specialKeys = List.of( + "key-with-dashes", + "key.with.dots", + "key_with_underscores", + "key:with:colons", + "key/with/slashes" + ); + + for (var key : specialKeys) { + Assertions.assertTrue(database.insert(key, Document.newJsonDocument().append("key", key))); + } + + Assertions.assertEquals(specialKeys.size(), database.documentCount()); + + for (var key : specialKeys) { + Assertions.assertTrue(database.contains(key)); + var doc = database.get(key); + Assertions.assertNotNull(doc); + Assertions.assertEquals(key, doc.getString("key")); + } + + database.clear(); + + Assertions.assertTrue(database.insert("empty_value_key", Document.newJsonDocument().append("empty", ""))); + var emptyValueDoc = database.get("empty_value_key"); + Assertions.assertNotNull(emptyValueDoc); + Assertions.assertEquals("", emptyValueDoc.getString("empty")); + + var emptyResults = database.find("empty", ""); + Assertions.assertEquals(1, emptyResults.size()); + } +} diff --git a/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/SQLiteDatabaseTest.java b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/SQLiteDatabaseTest.java new file mode 100644 index 0000000000..fe430ef86b --- /dev/null +++ b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/SQLiteDatabaseTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.sql.impl; + +import eu.cloudnetservice.modules.sql.config.DatabaseType; +import eu.cloudnetservice.modules.sql.config.SQLConfigurationEntry; +import eu.cloudnetservice.modules.sql.impl.junit.EnableServicesInject; +import org.junit.jupiter.api.BeforeAll; + +@EnableServicesInject +public class SQLiteDatabaseTest extends SQLDatabaseTest { + + @BeforeAll + static void setup() throws Exception { + var config = new SQLConfigurationEntry( + true, + DatabaseType.SQLITE, + "sqlite", + "cn_testing", + null, + null, + null, + "jdbc:sqlite::memory:"); + databaseProvider = JooqDatabaseType.SQLITE.createProvider(config); + databaseProvider.init(); + } +} diff --git a/modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/junit/EnableServicesInject.java b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/junit/EnableServicesInject.java similarity index 95% rename from modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/junit/EnableServicesInject.java rename to modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/junit/EnableServicesInject.java index 283cb3a857..3cab548a75 100644 --- a/modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/junit/EnableServicesInject.java +++ b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/junit/EnableServicesInject.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package eu.cloudnetservice.modules.mysql.impl.junit; +package eu.cloudnetservice.modules.sql.impl.junit; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; diff --git a/modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/junit/EnableServicesInjectExtension.java b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/junit/EnableServicesInjectExtension.java similarity index 98% rename from modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/junit/EnableServicesInjectExtension.java rename to modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/junit/EnableServicesInjectExtension.java index 62b605c5a3..ad1c28c57d 100644 --- a/modules/database-mysql/impl/src/test/java/eu/cloudnetservice/modules/mysql/impl/junit/EnableServicesInjectExtension.java +++ b/modules/database-sql/impl/src/test/java/eu/cloudnetservice/modules/sql/impl/junit/EnableServicesInjectExtension.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package eu.cloudnetservice.modules.mysql.impl.junit; +package eu.cloudnetservice.modules.sql.impl.junit; import eu.cloudnetservice.driver.DriverEnvironment; import eu.cloudnetservice.driver.impl.registry.DefaultServiceRegistry; diff --git a/modules/database-mysql/impl/src/test/resources/logback-test.xml b/modules/database-sql/impl/src/test/resources/logback-test.xml similarity index 100% rename from modules/database-mysql/impl/src/test/resources/logback-test.xml rename to modules/database-sql/impl/src/test/resources/logback-test.xml diff --git a/node/api/src/main/java/eu/cloudnetservice/node/database/LocalDatabase.java b/node/api/src/main/java/eu/cloudnetservice/node/database/LocalDatabase.java index 43351b9d33..ccf2d8969b 100644 --- a/node/api/src/main/java/eu/cloudnetservice/node/database/LocalDatabase.java +++ b/node/api/src/main/java/eu/cloudnetservice/node/database/LocalDatabase.java @@ -25,14 +25,6 @@ public interface LocalDatabase extends Database { - /** - * Iterates over all entries in the database This option should not be used with big databases Use - * {@link #iterate(BiConsumer, int)}} instead - * - * @param consumer the consumer to pass the entries into - */ - void iterate(@NonNull BiConsumer consumer); - /** * Iterates over all entries in the database, but in chunks in the given size * diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2Database.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2Database.java index edf662a9ae..ce9e4b9cf2 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2Database.java +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2Database.java @@ -18,8 +18,7 @@ import eu.cloudnetservice.driver.document.Document; import eu.cloudnetservice.driver.document.DocumentFactory; -import eu.cloudnetservice.node.impl.database.sql.SQLDatabase; -import eu.cloudnetservice.node.impl.database.sql.SQLDatabaseProvider; +import eu.cloudnetservice.node.impl.database.AbstractDatabase; import java.sql.ResultSet; import java.util.ArrayList; import java.util.Collection; @@ -29,14 +28,21 @@ import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; -import java.util.function.BiConsumer; import lombok.NonNull; import org.jetbrains.annotations.Nullable; -public final class H2Database extends SQLDatabase { +@Deprecated(forRemoval = true) +public final class H2Database extends AbstractDatabase { - public H2Database(@NonNull SQLDatabaseProvider provider, @NonNull String name) { - super(provider, name); + private static final String TABLE_COLUMN_KEY = "Name"; + private static final String TABLE_COLUMN_VAL = "Document"; + + private final H2DatabaseProvider databaseProvider; + + public H2Database(@NonNull H2DatabaseProvider provider, @NonNull String name) { + super(name, provider); + + this.databaseProvider = provider; // create the table provider.executeUpdate(String.format( @@ -203,21 +209,6 @@ public boolean delete0(String key) { }, Map.of()); } - @Override - public void iterate(@NonNull BiConsumer consumer) { - this.databaseProvider.executeQuery( - String.format("SELECT * FROM `%s`;", this.name), - resultSet -> { - while (resultSet.next()) { - var key = resultSet.getString(TABLE_COLUMN_KEY); - var document = DocumentFactory.json().parse(resultSet.getString(TABLE_COLUMN_VAL)); - consumer.accept(key, document); - } - - return null; - }, null); - } - @Override public void clear() { this.databaseProvider.executeUpdate(String.format("TRUNCATE TABLE `%s`", this.name)); diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2DatabaseProvider.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2DatabaseProvider.java index a9e2ffb444..8e2bb4c565 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2DatabaseProvider.java +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/h2/H2DatabaseProvider.java @@ -17,7 +17,7 @@ package eu.cloudnetservice.node.impl.database.h2; import eu.cloudnetservice.node.database.LocalDatabase; -import eu.cloudnetservice.node.impl.database.sql.SQLDatabaseProvider; +import eu.cloudnetservice.node.impl.database.AbstractNodeDatabaseProvider; import eu.cloudnetservice.utils.base.StringUtil; import eu.cloudnetservice.utils.base.io.FileUtil; import io.vavr.CheckedFunction1; @@ -33,14 +33,19 @@ import org.h2.Driver; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Deprecated(forRemoval = true) -public final class H2DatabaseProvider extends SQLDatabaseProvider { +public final class H2DatabaseProvider extends AbstractNodeDatabaseProvider { static { Driver.load(); } + private static final String[] TABLE_TYPE = new String[]{"TABLE"}; + private static final Logger LOGGER = LoggerFactory.getLogger(H2DatabaseProvider.class); + private final Path h2dbFile; private Connection connection; @@ -49,6 +54,11 @@ public H2DatabaseProvider(@NonNull String h2File) { this.h2dbFile = Path.of(h2File); } + @Override + public boolean synced() { + return false; + } + @Override public boolean init() throws Exception { FileUtil.createDirectory(this.h2dbFile.getParent()); @@ -62,6 +72,17 @@ public boolean init() throws Exception { return this.databaseCache.get(name, $ -> new H2Database(this, name)); } + @Override + public boolean containsDatabase(@NonNull String name) { + for (var database : this.databaseNames()) { + if (database.equalsIgnoreCase(name)) { + return true; + } + } + + return false; + } + @Override public boolean deleteDatabase(@NonNull String name) { return this.executeUpdate("DROP TABLE IF EXISTS `" + name + "`") != -1; @@ -96,14 +117,8 @@ public void close() throws Exception { } } - @Override - public @NonNull Connection connection() { - return this.connection; - } - - @Override public int executeUpdate(@NonNull String query, @NonNull Object... objects) { - try (var preparedStatement = this.connection().prepareStatement(query)) { + try (var preparedStatement = this.connection.prepareStatement(query)) { for (var i = 0; i < objects.length; i++) { preparedStatement.setString(i + 1, objects[i].toString()); } @@ -115,14 +130,13 @@ public int executeUpdate(@NonNull String query, @NonNull Object... objects) { } } - @Override public @UnknownNullability T executeQuery( @NonNull String query, @NonNull CheckedFunction1 callback, @Nullable T def, @NonNull Object... objects ) { - try (var preparedStatement = this.connection().prepareStatement(query)) { + try (var preparedStatement = this.connection.prepareStatement(query)) { for (var i = 0; i < objects.length; i++) { preparedStatement.setString(i + 1, objects[i].toString()); } diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/sql/SQLDatabaseProvider.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/sql/SQLDatabaseProvider.java deleted file mode 100644 index c452740df5..0000000000 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/sql/SQLDatabaseProvider.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.cloudnetservice.node.impl.database.sql; - -import com.github.benmanes.caffeine.cache.RemovalListener; -import eu.cloudnetservice.node.database.LocalDatabase; -import eu.cloudnetservice.node.impl.database.AbstractNodeDatabaseProvider; -import io.vavr.CheckedFunction1; -import java.sql.Connection; -import java.sql.ResultSet; -import lombok.NonNull; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.UnknownNullability; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Deprecated -@ApiStatus.ScheduledForRemoval(inVersion = "4.1") -public abstract class SQLDatabaseProvider extends AbstractNodeDatabaseProvider { - - protected static final String[] TABLE_TYPE = new String[]{"TABLE"}; - protected static final Logger LOGGER = LoggerFactory.getLogger(SQLDatabaseProvider.class); - - protected SQLDatabaseProvider(@NonNull RemovalListener removalListener) { - super(removalListener); - } - - @Override - public boolean containsDatabase(@NonNull String name) { - for (var database : this.databaseNames()) { - if (database.equalsIgnoreCase(name)) { - return true; - } - } - - return false; - } - - public abstract @NonNull Connection connection(); - - public abstract int executeUpdate(@NonNull String query, @NonNull Object... objects); - - public abstract @UnknownNullability T executeQuery( - @NonNull String query, - @NonNull CheckedFunction1 callback, - @Nullable T def, - @NonNull Object... objects); -} diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabase.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabase.java index 736c397850..c65ab3b74d 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabase.java +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabase.java @@ -116,11 +116,6 @@ public boolean delete(@NonNull String key) { return result; } - @Override - public void iterate(@NonNull BiConsumer consumer) { - this.acceptWithCursor(consumer); - } - @Override public void clear() { this.environment.executeInExclusiveTransaction(txn -> { diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabaseProvider.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabaseProvider.java index b79ea9ec7a..b62f8a9436 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabaseProvider.java +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/database/xodus/XodusDatabaseProvider.java @@ -63,6 +63,11 @@ public boolean init() { return true; } + @Override + public boolean synced() { + return false; + } + @Override public @NonNull LocalDatabase database(@NonNull String name) { return this.databaseCache.get(name, $ -> this.environment.computeInTransaction(txn -> { diff --git a/settings.gradle.kts b/settings.gradle.kts index 2a3297d00d..c6590bdc93 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -86,8 +86,8 @@ registerSubProjects( subProjects = arrayOf("api", "impl"), ) registerSubProjects( - root = "modules:database-mysql", - prefix = "database-mysql", + root = "modules:database-sql", + prefix = "database-sql", subProjects = arrayOf("api", "impl"), ) registerSubProjects(