Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
*/
package org.apache.polaris.persistence.relational.jdbc;

import jakarta.annotation.Nonnull;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import org.apache.polaris.core.persistence.bootstrap.SchemaOptions;

public enum DatabaseType {
POSTGRES("postgres"),
Expand All @@ -43,7 +48,29 @@ public static DatabaseType fromDisplayName(String displayName) {
};
}

public String getInitScriptResource() {
return String.format("%s/schema-v2.sql", this.getDisplayName());
/**
* Open an InputStream that contains data from an init script. This stream should be closed by the
* caller.
*/
public InputStream openInitScriptResource(@Nonnull SchemaOptions schemaOptions) {
if (schemaOptions.schemaFile() != null) {
try {
return new FileInputStream(schemaOptions.schemaFile());
} catch (IOException e) {
throw new IllegalArgumentException("Unable to load file " + schemaOptions.schemaFile(), e);
}
} else {
final String schemaSuffix;
switch (schemaOptions.schemaVersion()) {
case null -> schemaSuffix = "schema-v2.sql";
case 1 -> schemaSuffix = "schema-v1.sql";
case 2 -> schemaSuffix = "schema-v2.sql";
default ->
throw new IllegalArgumentException(
"Unknown schema version " + schemaOptions.schemaVersion());
}
ClassLoader classLoader = DatasourceOperations.class.getClassLoader();
return classLoader.getResourceAsStream(this.getDisplayName() + "/" + schemaSuffix);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import jakarta.annotation.Nonnull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
Expand Down Expand Up @@ -75,46 +76,51 @@ DatabaseType getDatabaseType() {
}

/**
* Execute SQL script.
* Execute SQL script and close the associated input stream
Copy link
Contributor

Choose a reason for hiding this comment

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

WDYT about adding @WillClose to the stream parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds like a good idea... is this the one from jsr305? It doesn't seem that we have that dependency currently, and we're not using it anywhere else in the project.

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, that jar 👍 TBH, I was surprised we did not have any existing use cases 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I noticed this is in the banned-dependencies.txt?

# Contains old javax.* annotations that we do not want
com.google.code.findbugs:jsr305

Copy link
Contributor

Choose a reason for hiding this comment

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

TIL... well, I'm fine with not using it in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Me too! I learned this when I went to add the dep 😅

If there is another similar annotation we start using in the future that would be nice as this does sound pretty helpful.

*
* @param scriptFilePath : Path of SQL script.
* @param scriptInputStream : Input stream containing the SQL script.
* @throws SQLException : Exception while executing the script.
*/
public void executeScript(String scriptFilePath) throws SQLException {
ClassLoader classLoader = DatasourceOperations.class.getClassLoader();
runWithinTransaction(
connection -> {
try (Statement statement = connection.createStatement()) {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
Objects.requireNonNull(classLoader.getResourceAsStream(scriptFilePath)),
UTF_8));
StringBuilder sqlBuffer = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.startsWith("--")) { // Ignore empty lines and comments
sqlBuffer.append(line).append("\n");
if (line.endsWith(";")) { // Execute statement when semicolon is found
String sql = sqlBuffer.toString().trim();
try {
// since SQL is directly read from the file, there is close to 0 possibility
// of this being injected plus this run via an Admin tool, if attacker can
// fiddle with this that means lot of other things are already compromised.
statement.execute(sql);
} catch (SQLException e) {
throw new RuntimeException(e);
public void executeScript(InputStream scriptInputStream) throws SQLException {
try {
runWithinTransaction(
connection -> {
try (Statement statement = connection.createStatement();
BufferedReader reader =
new BufferedReader(
new InputStreamReader(Objects.requireNonNull(scriptInputStream), UTF_8))) {
StringBuilder sqlBuffer = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.startsWith("--")) { // Ignore empty lines and comments
sqlBuffer.append(line).append("\n");
if (line.endsWith(";")) { // Execute statement when semicolon is found
String sql = sqlBuffer.toString().trim();
try {
// since SQL is directly read from the file, there is close to 0 possibility
// of this being injected plus this run via an Admin tool, if attacker can
// fiddle with this that means lot of other things are already compromised.
statement.execute(sql);
} catch (SQLException e) {
throw new RuntimeException(e);
}
sqlBuffer.setLength(0); // Clear the buffer for the next statement
}
sqlBuffer.setLength(0); // Clear the buffer for the next statement
}
}
return true;
} catch (IOException e) {
throw new RuntimeException(e);
}
return true;
} catch (IOException e) {
throw new RuntimeException(e);
}
});
});
} finally {
try {
scriptInputStream.close();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure the try on line 88 closes this stream too... Could you double check?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently no, the only resource is the Statement:

try (Statement statement = connection.createStatement())

But if we added the BufferedReader there too, it might close the stream. Do you want to do that instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, if possible... I think try-with-resources is more intuitive and we get better exception propagation OOTB.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need this call to .close() now that we have the BufferedReader inside the try-with-resources?

} catch (IOException e) {
LOGGER.error("Failed to close input stream: {}", e.getMessage());
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.PrincipalSecretsGenerator;
import org.apache.polaris.core.persistence.bootstrap.BootstrapOptions;
import org.apache.polaris.core.persistence.bootstrap.ImmutableBootstrapOptions;
import org.apache.polaris.core.persistence.bootstrap.ImmutableSchemaOptions;
import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.persistence.bootstrap.SchemaOptions;
import org.apache.polaris.core.persistence.cache.EntityCache;
import org.apache.polaris.core.persistence.cache.InMemoryEntityCache;
import org.apache.polaris.core.persistence.dao.entity.BaseResult;
Expand Down Expand Up @@ -93,13 +97,14 @@ protected PolarisMetaStoreManager createNewMetaStoreManager() {
}

private void initializeForRealm(
RealmContext realmContext, RootCredentialsSet rootCredentialsSet, boolean isBootstrap) {
DatasourceOperations databaseOperations = getDatasourceOperations(isBootstrap);
DatasourceOperations datasourceOperations,
RealmContext realmContext,
RootCredentialsSet rootCredentialsSet) {
sessionSupplierMap.put(
realmContext.getRealmIdentifier(),
() ->
new JdbcBasePersistenceImpl(
databaseOperations,
datasourceOperations,
secretsGenerator(realmContext, rootCredentialsSet),
storageIntegrationProvider,
realmContext.getRealmIdentifier()));
Expand All @@ -108,35 +113,52 @@ private void initializeForRealm(
metaStoreManagerMap.put(realmContext.getRealmIdentifier(), metaStoreManager);
}

private DatasourceOperations getDatasourceOperations(boolean isBootstrap) {
public DatasourceOperations getDatasourceOperations() {
DatasourceOperations databaseOperations;
try {
databaseOperations = new DatasourceOperations(dataSource.get(), relationalJdbcConfiguration);
} catch (SQLException sqlException) {
throw new RuntimeException(sqlException);
}
if (isBootstrap) {
try {
// Run the set-up script to create the tables.
databaseOperations.executeScript(
databaseOperations.getDatabaseType().getInitScriptResource());
} catch (SQLException e) {
throw new RuntimeException(
String.format("Error executing sql script: %s", e.getMessage()), e);
}
}
return databaseOperations;
}

@Override
public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(
Iterable<String> realms, RootCredentialsSet rootCredentialsSet) {
SchemaOptions schemaOptions = ImmutableSchemaOptions.builder().build();

BootstrapOptions bootstrapOptions =
ImmutableBootstrapOptions.builder()
.realms(realms)
.rootCredentialsSet(rootCredentialsSet)
.schemaOptions(schemaOptions)
.build();

return bootstrapRealms(bootstrapOptions);
}

@Override
public synchronized Map<String, PrincipalSecretsResult> bootstrapRealms(
BootstrapOptions bootstrapOptions) {
Map<String, PrincipalSecretsResult> results = new HashMap<>();

for (String realm : realms) {
for (String realm : bootstrapOptions.realms()) {
RealmContext realmContext = () -> realm;
if (!metaStoreManagerMap.containsKey(realm)) {
initializeForRealm(realmContext, rootCredentialsSet, true);
DatasourceOperations datasourceOperations = getDatasourceOperations();
try {
// Run the set-up script to create the tables.
datasourceOperations.executeScript(
datasourceOperations
.getDatabaseType()
.openInitScriptResource(bootstrapOptions.schemaOptions()));
} catch (SQLException e) {
throw new RuntimeException(
String.format("Error executing sql script: %s", e.getMessage()), e);
}
initializeForRealm(
datasourceOperations, realmContext, bootstrapOptions.rootCredentialsSet());
PrincipalSecretsResult secretsResult =
bootstrapServiceAndCreatePolarisPrincipalForRealm(
realmContext, metaStoreManagerMap.get(realm));
Expand Down Expand Up @@ -172,7 +194,8 @@ public Map<String, BaseResult> purgeRealms(Iterable<String> realms) {
public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager(
RealmContext realmContext) {
if (!metaStoreManagerMap.containsKey(realmContext.getRealmIdentifier())) {
initializeForRealm(realmContext, null, false);
DatasourceOperations datasourceOperations = getDatasourceOperations();
initializeForRealm(datasourceOperations, realmContext, null);
checkPolarisServiceBootstrappedForRealm(
realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
}
Expand All @@ -183,7 +206,8 @@ public synchronized PolarisMetaStoreManager getOrCreateMetaStoreManager(
public synchronized Supplier<BasePersistence> getOrCreateSessionSupplier(
RealmContext realmContext) {
if (!sessionSupplierMap.containsKey(realmContext.getRealmIdentifier())) {
initializeForRealm(realmContext, null, false);
DatasourceOperations datasourceOperations = getDatasourceOperations();
initializeForRealm(datasourceOperations, realmContext, null);
checkPolarisServiceBootstrappedForRealm(
realmContext, metaStoreManagerMap.get(realmContext.getRealmIdentifier()));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS;

import java.io.InputStream;
import java.sql.SQLException;
import java.time.ZoneId;
import java.util.Optional;
Expand Down Expand Up @@ -49,8 +50,11 @@ protected PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() {
try {
datasourceOperations =
new DatasourceOperations(createH2DataSource(), new H2JdbcConfiguration());
datasourceOperations.executeScript(
String.format("%s/schema-v2.sql", DatabaseType.H2.getDisplayName()));
ClassLoader classLoader = DatasourceOperations.class.getClassLoader();
InputStream scriptStream =
classLoader.getResourceAsStream(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this stream normally needs to be closed, AFAIK.

Copy link
Contributor Author

@eric-maynard eric-maynard Jun 28, 2025

Choose a reason for hiding this comment

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

ditto the above -- I think we just weren't closing it before, but added an explicit close into executeScript to fix this

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not a try-with-resources block?

Copy link
Contributor Author

@eric-maynard eric-maynard Jun 29, 2025

Choose a reason for hiding this comment

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

Per the javadoc, I think it's better to have this method take on the responsibility for closing the stream after it's used rather than scatter try-with-resources amongst the callers. The loan pattern would be better, but we're already getting pretty far away from the intent of this change.

String.format("%s/schema-v2.sql", DatabaseType.H2.getDisplayName()));
datasourceOperations.executeScript(scriptStream);
} catch (SQLException e) {
throw new RuntimeException(
String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Map;
import java.util.function.Supplier;
import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.persistence.bootstrap.BootstrapOptions;
import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet;
import org.apache.polaris.core.persistence.cache.EntityCache;
import org.apache.polaris.core.persistence.dao.entity.BaseResult;
Expand All @@ -41,6 +42,10 @@ public interface MetaStoreManagerFactory {
Map<String, PrincipalSecretsResult> bootstrapRealms(
Iterable<String> realms, RootCredentialsSet rootCredentialsSet);

default Map<String, PrincipalSecretsResult> bootstrapRealms(BootstrapOptions bootstrapOptions) {
return bootstrapRealms(bootstrapOptions.realms(), bootstrapOptions.rootCredentialsSet());
}

/** Purge all metadata for the realms provided */
Map<String, BaseResult> purgeRealms(Iterable<String> realms);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.polaris.core.persistence.bootstrap;

import org.apache.polaris.immutables.PolarisImmutable;

@PolarisImmutable
public interface BootstrapOptions {
Iterable<String> realms();

RootCredentialsSet rootCredentialsSet();

SchemaOptions schemaOptions();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.polaris.core.persistence.bootstrap;

import jakarta.annotation.Nullable;
import org.apache.polaris.immutables.PolarisImmutable;
import org.immutables.value.Value;

@PolarisImmutable
public interface SchemaOptions {
@Nullable
Integer schemaVersion();

@Nullable
String schemaFile();

@Value.Check
default void validate() {
if (schemaVersion() != null && schemaFile() != null) {
throw new IllegalStateException("Only one of schemaVersion or schemaFile can be set.");
}
}
}
Loading
Loading