Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sql): add SQL bootstrapper #4376

36 changes: 36 additions & 0 deletions extensions/common/sql/sql-bootstrapper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 Daimler TSS GmbH
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
*
* 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:
* Daimler TSS GmbH - Initial build file
*
*/

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(project(":extensions:common:transaction:transaction-local"))
// testImplementation(testFixtures(project(":extensions:common:sql:sql-core")))
// testImplementation(libs.postgres)
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
testImplementation(libs.assertj)
//
// testFixturesImplementation(project(":extensions:common:sql:sql-core"))
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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;

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.
* During the {@link ServiceExtension#prepare()} phase
*
* @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.
* During the {@link ServiceExtension#prepare()} phase
*
* @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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.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;

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) {
((SqlSchemaBootstrapperImpl) getBootstrapper()).executeSql().orElseThrow(f -> new EdcPersistenceException("Failed to bootstrap SQL schema, error '%s'".formatted(f.getFailureDetail())));
}
}

@Provider
public SqlSchemaBootstrapper getBootstrapper() {
if (bootstrapper == null) {
bootstrapper = new SqlSchemaBootstrapperImpl(transactionContext, queryExecutor, datasourceRegistry);
}
return bootstrapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 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.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;
import java.util.stream.Stream;

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

public class SqlSchemaBootstrapperImpl implements SqlSchemaBootstrapper {
private final TransactionContext transactionContext;
private final QueryExecutor queryExecutor;
private final List<QueuedStatementRecord> statements = new ArrayList<>();
private final DataSourceRegistry dataSourceRegistry;

public SqlSchemaBootstrapperImpl(TransactionContext transactionContext, QueryExecutor queryExecutor, DataSourceRegistry dataSourceRegistry) {
this.transactionContext = transactionContext;
this.queryExecutor = queryExecutor;
this.dataSourceRegistry = dataSourceRegistry;
}


public void addStatementFromResource(String datasourceName, String resourceName, ClassLoader classLoader) {
Fixed Show fixed Hide fixed
try (var sqlStream = classLoader.getResourceAsStream(resourceName)) {
var sql = new Scanner(Objects.requireNonNull(sqlStream)).useDelimiter("\\A").next();
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
statements.add(new QueuedStatementRecord(datasourceName, sql));
} catch (IOException e) {
throw new EdcException(e);
}
}

public Result<Void> executeSql() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method should be put in the interface (that will save the explicit cast in the SqlSchemaBootstrapperExtension)

Copy link
Member Author

@paullatzelsperger paullatzelsperger Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want this method in the interface, for it could entice contributing modules to invoke it, which could potentially cause problems. The cast is ugly, but it should be possible to simply use the variable (as opposed to the getBootstrapper() method) in the prepare() phase.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I understand it, but this necessity is not explicit (and being misinterpreted, e.g. from a future-me :) ), that could be achieved by keeping only the addStatement method in the bootstrapper and moving the execution logic to a module-private service, that won't be registered on the runtime, this way the executeSql will effectively become not callable from other modules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats a lot of ceremony for one single method. if executeSql() is package-private and documented, it should be clear enough what is done.
In addition, I didn't put the interface in an SPI package on purpose, because it is not intended as an extension point anyway.

Copy link
Member Author

@paullatzelsperger paullatzelsperger Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it, and it looks OK, but I'm not really sure splitting such a trivial task in two classes (and two test classes) is really worth it TBH. LMK what you think.

return transactionContext.execute(() -> {
Stream<Result<Void>> objectStream = statements.stream().map(statement -> {
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
var connectionResult = getConnection(statement.datasourceName);
return connectionResult.compose(connection -> {
queryExecutor.execute(connection, statement.sql);
return success();
});
});
return objectStream.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());
}
}

private record QueuedStatementRecord(String datasourceName, String sql) {
}
}
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
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.junit.annotations.ComponentTest;
import org.eclipse.edc.junit.extensions.DependencyInjectionExtension;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.configuration.Config;
import org.eclipse.edc.transaction.spi.TransactionContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.eclipse.edc.sql.bootstrapper.SqlSchemaBootstrapperExtension.SCHEMA_AUTOCREATE_PROPERTY;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

@ComponentTest
@ExtendWith(DependencyInjectionExtension.class)
class SqlSchemaBootstrapperExtensionTest {

private final TransactionContext transactionContext = mock();

@BeforeEach
void setUp(ServiceExtensionContext context) {
context.registerService(TransactionContext.class, transactionContext);
}

@Test
void prepare_autocreateDisabled(SqlSchemaBootstrapperExtension extension, ServiceExtensionContext context) {
var config = mock(Config.class);
when(config.getBoolean(eq("edc.sql.schema.autocreate"), anyBoolean())).thenReturn(false);
when(context.getConfig()).thenReturn(config);
extension.initialize(context);
extension.prepare();
verifyNoInteractions(transactionContext);
}

@Test
void prepare(SqlSchemaBootstrapperExtension extension, ServiceExtensionContext context) {
var config = mock(Config.class);
when(config.getBoolean(eq(SCHEMA_AUTOCREATE_PROPERTY), anyBoolean())).thenReturn(true);
when(context.getConfig()).thenReturn(config);
when(transactionContext.execute(isA(TransactionContext.ResultTransactionBlock.class)))
.thenReturn(Result.success("foobar"));

extension.initialize(context);

assertThatNoException().isThrownBy(extension::prepare);
verify(transactionContext).execute(isA(TransactionContext.ResultTransactionBlock.class));
}

}
Loading
Loading