Skip to content

Commit

Permalink
feat(sql): add SQL bootstrapper (#4376)
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger authored Jul 30, 2024
1 parent fd137be commit ae5664e
Show file tree
Hide file tree
Showing 65 changed files with 733 additions and 134 deletions.
2 changes: 1 addition & 1 deletion DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ maven/mavencentral/org.testcontainers/jdbc/1.20.0, MIT, approved, clearlydefined
maven/mavencentral/org.testcontainers/junit-jupiter/1.20.0, MIT, approved, clearlydefined
maven/mavencentral/org.testcontainers/kafka/1.20.0, MIT, approved, clearlydefined
maven/mavencentral/org.testcontainers/postgresql/1.20.0, MIT, approved, clearlydefined
maven/mavencentral/org.testcontainers/testcontainers/1.20.0, None, restricted, #15747
maven/mavencentral/org.testcontainers/testcontainers/1.20.0, MIT, approved, #15747
maven/mavencentral/org.testcontainers/vault/1.20.0, MIT, approved, clearlydefined
maven/mavencentral/org.xerial.snappy/snappy-java/1.1.10.5, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #9098
maven/mavencentral/org.xmlresolver/xmlresolver/5.2.2, Apache-2.0, approved, clearlydefined
Expand Down
31 changes: 31 additions & 0 deletions extensions/common/sql/sql-bootstrapper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

plugins {
`java-library`
`java-test-fixtures`
`maven-publish`
}

dependencies {
api(project(":spi:common:core-spi"))
api(project(":spi:common:transaction-spi"))
implementation(project(":spi:common:transaction-datasource-spi"))
implementation(project(":extensions:common:sql:sql-core")) // SqlQueryExecutor
//
testImplementation(project(":core:common:junit"))
testImplementation(libs.assertj)
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.sql.bootstrapper;

record QueuedStatementRecord(String name, String datasourceName, String sql) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.sql.bootstrapper;

import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.persistence.EdcPersistenceException;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.sql.QueryExecutor;
import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry;
import org.eclipse.edc.transaction.spi.TransactionContext;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

/**
* Internal class to the SQL Bootstrapper Extension module with the intended purpose to execute a series of DML statements against
* the database.
*/
class SqlDmlStatementRunner {

private final TransactionContext transactionContext;
private final QueryExecutor queryExecutor;
private final Monitor monitor;
private final DataSourceRegistry dataSourceRegistry;

SqlDmlStatementRunner(TransactionContext transactionContext, QueryExecutor queryExecutor, Monitor monitor, DataSourceRegistry dataSourceRegistry) {
this.transactionContext = transactionContext;
this.queryExecutor = queryExecutor;
this.monitor = monitor;
this.dataSourceRegistry = dataSourceRegistry;
}

/**
* Executes the queued DML statements one after the other. This method is intended to be called only from the {@link SqlSchemaBootstrapperExtension}.
*
* @param statements A map containing the datasource name as key and the SQL statements as value
* @return A summary result of all the statements.
*/
public Result<Void> executeSql(Map<String, List<String>> statements) {
monitor.debug("Running DML statements: [%s]".formatted(String.join(", ", statements.keySet())));
return transactionContext.execute(() -> statements.entrySet().stream()
.map(statement -> {
var connectionResult = getConnection(statement.getKey());
return connectionResult.compose(connection -> {
try {
queryExecutor.execute(connection, String.join("", statement.getValue()));
} catch (EdcPersistenceException sqlException) {
return failure(sqlException.getMessage());
}
return success();
});
})
.reduce(Result::merge)
.orElse(Result.success()));
}

public Result<Connection> getConnection(String datasourceName) {
try {
var resolve = dataSourceRegistry.resolve(datasourceName);
return resolve != null ? success(resolve.getConnection()) :
failure("No datasource found with name '%s'".formatted(datasourceName));
} catch (SQLException e) {
return failure(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.sql.bootstrapper;

import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;

import java.util.List;
import java.util.Map;

/**
* Provides a convenient way to create database structures in an SQL database. DML statements can be added in the {@code initialize()}
* phase of extensions and the bootstrapper takes care of executing them against the database.
*/
public interface SqlSchemaBootstrapper {
/**
* Extensions that operate a store based on an SQL database and thus require a certain database structure to be present,
* can use this class to have their schema auto-generated. The entire DDL has to be in a file that is available from the resources.
* <p>
* Note that all DDL statements <strong>must</strong> be queued during the {@link ServiceExtension#initialize(ServiceExtensionContext)} phase and
* individual statements <strong>must not</strong> rely on ordering, since that depends on extension ordering.
*
* @param datasourceName The name of the datasource against which the statements are to be run
* @param resourceName An SQL DDL statement. Cannot contain prepared statements. Do not add DML statements here!
*/
default void addStatementFromResource(String datasourceName, String resourceName) {
addStatementFromResource(datasourceName, resourceName, getClass().getClassLoader());
}

/**
* Extensions that operate a store based on an SQL database and thus require a certain database structure to be present,
* can use this class to have their schema auto-generated. The entire DDL has to be in a file that is available from the resources.
* <p>
* Note that all DDL statements <strong>must</strong> be queued during the {@link ServiceExtension#initialize(ServiceExtensionContext)} phase and
* individual statements <strong>must not</strong> rely on ordering, since that depends on extension ordering.
*
* @param datasourceName The name of the datasource against which the statements are to be run
* @param resourceName An SQL DDL statement. Cannot contain prepared statements. Do not add DML statements here!
* @param classLoader A classloader which is used to resolve the resource
*/
void addStatementFromResource(String datasourceName, String resourceName, ClassLoader classLoader);

/**
* Gets all registered DML statements as a map where the datasource name is the key, and the SQL statement(s) is the value.
*/
Map<String, List<String>> getStatements();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.sql.bootstrapper;

import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.persistence.EdcPersistenceException;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.sql.QueryExecutor;
import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry;
import org.eclipse.edc.transaction.spi.TransactionContext;

import static org.eclipse.edc.sql.bootstrapper.SqlSchemaBootstrapperExtension.NAME;

@Extension(value = NAME, categories = { "sql", "persistence", "storage" })
public class SqlSchemaBootstrapperExtension implements ServiceExtension {
public static final String NAME = "SQL Schema Bootstrapper Extension";
public static final String SCHEMA_AUTOCREATE_PROPERTY = "edc.sql.schema.autocreate";
public static final boolean SCHEMA_AUTOCREATE_DEFAULT = false;

@Inject
private TransactionContext transactionContext;
@Inject
private QueryExecutor queryExecutor;
@Inject
private DataSourceRegistry datasourceRegistry;
@Inject
private Monitor monitor;

private SqlSchemaBootstrapperImpl bootstrapper;
private Boolean shouldAutoCreate;

@Override
public void initialize(ServiceExtensionContext context) {
shouldAutoCreate = context.getConfig().getBoolean(SCHEMA_AUTOCREATE_PROPERTY, SCHEMA_AUTOCREATE_DEFAULT);
}

@Override
public void prepare() {
if (shouldAutoCreate) {
var statements = getBootstrapper().getStatements();
new SqlDmlStatementRunner(transactionContext, queryExecutor, monitor, datasourceRegistry).executeSql(statements)
.orElseThrow(f -> new EdcPersistenceException("Failed to bootstrap SQL schema, error '%s'".formatted(f.getFailureDetail())));

} else {
monitor.debug("Automatic SQL schema creation is disabled. To enable it, set '%s' = true".formatted(SCHEMA_AUTOCREATE_PROPERTY));
}
}

@Provider
public SqlSchemaBootstrapper getBootstrapper() {
if (bootstrapper == null) {
bootstrapper = new SqlSchemaBootstrapperImpl();
}
return bootstrapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.sql.bootstrapper;

import org.eclipse.edc.spi.EdcException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;

public class SqlSchemaBootstrapperImpl implements SqlSchemaBootstrapper {

private final List<QueuedStatementRecord> statements = new ArrayList<>();

@Override
public void addStatementFromResource(String datasourceName, String resourceName, ClassLoader classLoader) {
try (var sqlStream = classLoader.getResourceAsStream(resourceName); var scanner = new Scanner(Objects.requireNonNull(sqlStream)).useDelimiter("\\A")) {
var sql = scanner.next();
statements.add(new QueuedStatementRecord(resourceName, datasourceName, sql));
} catch (IOException e) {
throw new EdcException(e);
}
}

@Override
public Map<String, List<String>> getStatements() {
return statements.stream().collect(groupingBy(QueuedStatementRecord::datasourceName, mapping(QueuedStatementRecord::sql, toList())));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#
# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
#
# This program and the accompanying materials are made available under the
# terms of the Apache License, Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
#
# Contributors:
# Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
#
#

org.eclipse.edc.sql.bootstrapper.SqlSchemaBootstrapperExtension
Loading

0 comments on commit ae5664e

Please sign in to comment.