diff --git a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java index c617e505e6..1ae1b35a55 100644 --- a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java +++ b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java @@ -18,12 +18,8 @@ */ 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"), @@ -52,26 +48,16 @@ public static DatabaseType fromDisplayName(String displayName) { * 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-v3.sql"; - case 1 -> schemaSuffix = "schema-v1.sql"; - case 2 -> schemaSuffix = "schema-v2.sql"; - case 3 -> schemaSuffix = "schema-v3.sql"; - default -> - throw new IllegalArgumentException( - "Unknown schema version " + schemaOptions.schemaVersion()); - } - ClassLoader classLoader = DatasourceOperations.class.getClassLoader(); - return classLoader.getResourceAsStream(this.getDisplayName() + "/" + schemaSuffix); + public InputStream openInitScriptResource(int schemaVersion) { + // Preconditions check is simpler and more direct than a switch default + if (schemaVersion <= 0 || schemaVersion > 3) { + throw new IllegalArgumentException("Unknown or invalid schema version " + schemaVersion); } + + final String resourceName = + String.format("%s/schema-v%d.sql", this.getDisplayName(), schemaVersion); + + ClassLoader classLoader = DatasourceOperations.class.getClassLoader(); + return classLoader.getResourceAsStream(resourceName); } } diff --git a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java index 43cfe702c4..e44de3a94c 100644 --- a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java +++ b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java @@ -52,9 +52,13 @@ public class DatasourceOperations { private static final Logger LOGGER = LoggerFactory.getLogger(DatasourceOperations.class); + // PG STATUS CODES private static final String CONSTRAINT_VIOLATION_SQL_CODE = "23505"; private static final String RELATION_DOES_NOT_EXIST = "42P01"; + // H2 STATUS CODES + private static final String H2_RELATION_DOES_NOT_EXIST = "90079"; + // POSTGRES RETRYABLE EXCEPTIONS private static final String SERIALIZATION_FAILURE_SQL_CODE = "40001"; @@ -396,7 +400,9 @@ public boolean isConstraintViolation(SQLException e) { } public boolean isRelationDoesNotExist(SQLException e) { - return RELATION_DOES_NOT_EXIST.equals(e.getSQLState()); + return (RELATION_DOES_NOT_EXIST.equals(e.getSQLState()) + && databaseType == DatabaseType.POSTGRES) + || (H2_RELATION_DOES_NOT_EXIST.equals(e.getSQLState()) && databaseType == DatabaseType.H2); } private Connection borrowConnection() throws SQLException { diff --git a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java index 0d90fe2774..8d6201453f 100644 --- a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java +++ b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java @@ -748,14 +748,28 @@ static int loadSchemaVersion( } return schemaVersion.getFirst().getValue(); } catch (SQLException e) { - LOGGER.error("Failed to load schema version due to {}", e.getMessage(), e); if (fallbackOnDoesNotExist && datasourceOperations.isRelationDoesNotExist(e)) { return SchemaVersion.MINIMUM.getValue(); } + LOGGER.error("Failed to load schema version due to {}", e.getMessage(), e); throw new IllegalStateException("Failed to retrieve schema version", e); } } + static boolean entityTableExists(DatasourceOperations datasourceOperations) { + PreparedQuery query = QueryGenerator.generateEntityTableExistQuery(); + try { + List entities = + datasourceOperations.executeSelect(query, new ModelEntity()); + return entities != null && !entities.isEmpty(); + } catch (SQLException e) { + if (datasourceOperations.isRelationDoesNotExist(e)) { + return false; + } + throw new IllegalStateException("Failed to check if Entities table exists", e); + } + } + /** {@inheritDoc} */ @Override public diff --git a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtils.java b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtils.java new file mode 100644 index 0000000000..814417d1b8 --- /dev/null +++ b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtils.java @@ -0,0 +1,91 @@ +/* + * 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.persistence.relational.jdbc; + +import java.util.Optional; +import org.apache.polaris.core.persistence.bootstrap.BootstrapOptions; +import org.apache.polaris.core.persistence.bootstrap.SchemaOptions; + +public class JdbcBootstrapUtils { + + private JdbcBootstrapUtils() {} + + /** + * Determines the correct schema version to use for bootstrapping a realm. + * + * @param currentSchemaVersion The current version of the database schema. + * @param requiredSchemaVersion The requested schema version (-1 for auto-detection). + * @param hasAlreadyBootstrappedRealms Flag indicating if any realms already exist. + * @return The calculated bootstrap schema version. + * @throws IllegalStateException if the combination of parameters represents an invalid state. + */ + public static int getRealmBootstrapSchemaVersion( + int currentSchemaVersion, int requiredSchemaVersion, boolean hasAlreadyBootstrappedRealms) { + + // If versions already match, no change is needed. + if (currentSchemaVersion == requiredSchemaVersion) { + return requiredSchemaVersion; + } + + // Handle fresh installations where no schema version is recorded (version 0). + if (currentSchemaVersion == 0) { + if (hasAlreadyBootstrappedRealms) { + // System was bootstrapped with v1 before schema versioning was introduced. + if (requiredSchemaVersion == -1 || requiredSchemaVersion == 1) { + return 1; + } + } else { + // A truly fresh start. Default to v3 for auto-detection, otherwise use the specified + // version. + return requiredSchemaVersion == -1 ? 3 : requiredSchemaVersion; + } + } + + // Handle auto-detection on an existing installation (current version > 0). + if (requiredSchemaVersion == -1) { + // Use the current version if realms already exist; otherwise, use v3 for the new realm. + return hasAlreadyBootstrappedRealms ? currentSchemaVersion : 3; + } + + // Any other combination is an unhandled or invalid migration path. + throw new IllegalStateException( + String.format( + "Cannot determine bootstrap schema version. Current: %d, Required: %d, Bootstrapped: %b", + currentSchemaVersion, requiredSchemaVersion, hasAlreadyBootstrappedRealms)); + } + + /** + * Extracts the requested schema version from the provided BootstrapOptions. + * + * @param bootstrapOptions: The bootstrap options containing schema information from which to + * extract the version. + * @return The requested schema version, or -1 if not specified. + */ + public static int getRequestedSchemaVersion(BootstrapOptions bootstrapOptions) { + SchemaOptions schemaOptions = bootstrapOptions.schemaOptions(); + if (schemaOptions != null) { + Optional version = schemaOptions.schemaVersion(); + if (version.isPresent()) { + return version.get(); + } + } + return -1; + } +} diff --git a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java index b1609522c6..e46cc7277e 100644 --- a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java +++ b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java @@ -154,12 +154,27 @@ public synchronized Map bootstrapRealms( RealmContext realmContext = () -> realm; if (!metaStoreManagerMap.containsKey(realm)) { DatasourceOperations datasourceOperations = getDatasourceOperations(); + int currentSchemaVersion = + JdbcBasePersistenceImpl.loadSchemaVersion( + datasourceOperations, + configurationStore.getConfiguration( + realmContext, BehaviorChangeConfiguration.SCHEMA_VERSION_FALL_BACK_ON_DNE)); + int requestedSchemaVersion = JdbcBootstrapUtils.getRequestedSchemaVersion(bootstrapOptions); + int effectiveSchemaVersion = + JdbcBootstrapUtils.getRealmBootstrapSchemaVersion( + currentSchemaVersion, + requestedSchemaVersion, + JdbcBasePersistenceImpl.entityTableExists(datasourceOperations)); + LOGGER.info( + "Effective schema version: {} for bootstrapping realm: {}", + effectiveSchemaVersion, + realm); try { // Run the set-up script to create the tables. datasourceOperations.executeScript( datasourceOperations .getDatabaseType() - .openInitScriptResource(bootstrapOptions.schemaOptions())); + .openInitScriptResource(effectiveSchemaVersion)); } catch (SQLException e) { throw new RuntimeException( String.format("Error executing sql script: %s", e.getMessage()), e); diff --git a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/QueryGenerator.java b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/QueryGenerator.java index 94f6248700..485956ed85 100644 --- a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/QueryGenerator.java +++ b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/QueryGenerator.java @@ -256,6 +256,14 @@ static PreparedQuery generateVersionQuery() { return new PreparedQuery("SELECT version_value FROM POLARIS_SCHEMA.VERSION", List.of()); } + @VisibleForTesting + static PreparedQuery generateEntityTableExistQuery() { + return new PreparedQuery( + String.format( + "SELECT * FROM %s LIMIT 1", getFullyQualifiedTableName(ModelEntity.TABLE_NAME)), + List.of()); + } + /** * Generate a SELECT query to find any entities that have a given realm & parent and that may * overlap with a given location. The check is performed without consideration for the scheme, so diff --git a/persistence/relational-jdbc/src/main/resources/h2/schema-v3.sql b/persistence/relational-jdbc/src/main/resources/h2/schema-v3.sql index 6f2aa87b85..3fb7749a3d 100644 --- a/persistence/relational-jdbc/src/main/resources/h2/schema-v3.sql +++ b/persistence/relational-jdbc/src/main/resources/h2/schema-v3.sql @@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS version ( MERGE INTO version (version_key, version_value) KEY (version_key) - VALUES ('version', 2); + VALUES ('version', 3); -- H2 supports COMMENT, but some modes may ignore it COMMENT ON TABLE version IS 'the version of the JDBC schema in use'; diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtilsTest.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtilsTest.java new file mode 100644 index 0000000000..6a9eb95524 --- /dev/null +++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtilsTest.java @@ -0,0 +1,153 @@ +/* + * 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.persistence.relational.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.apache.polaris.core.persistence.bootstrap.BootstrapOptions; +import org.apache.polaris.core.persistence.bootstrap.SchemaOptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +class JdbcBootstrapUtilsTest { + + @Test + void getVersion_whenVersionsMatch() { + // Arrange + int version = 2; + + // Act & Assert + assertEquals( + version, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(version, version, true)); + assertEquals( + version, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(version, version, false)); + } + + @Test + void getVersion_whenFreshDbAndNoRealms() { + // Arrange + int currentVersion = 0; + boolean hasRealms = false; + + // Act & Assert + assertEquals( + 3, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, -1, hasRealms)); + assertEquals( + 2, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 2, hasRealms)); + assertEquals( + 3, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 3, hasRealms)); + } + + @Test + void getVersion_whenFreshDbAndRealmsExist() { + // Arrange + int currentVersion = 0; + boolean hasRealms = true; + + // Act & Assert + assertEquals( + 1, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, -1, hasRealms)); + assertEquals( + 1, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 1, hasRealms)); + } + + @ParameterizedTest + @CsvSource({"2, true, 2", "3, true, 3", "2, false, 3", "3, false, 3"}) + void getVersion_whenExistingDbAndAutoDetect( + int currentVersion, boolean hasRealms, int expectedVersion) { + // Act & Assert + assertEquals( + expectedVersion, + JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, -1, hasRealms)); + } + + @Test + void throwException_whenFreshDbWithRealmsAndInvalidRequiredVersion() { + // Arrange + int currentVersion = 0; + boolean hasRealms = true; + int invalidRequiredVersion = 2; + + // Act & Assert + assertThrows( + IllegalStateException.class, + () -> + JdbcBootstrapUtils.getRealmBootstrapSchemaVersion( + currentVersion, invalidRequiredVersion, hasRealms)); + } + + @Test + void throwException_whenExistingDbAndInvalidMigrationPath() { + // Arrange + int currentVersion = 2; + int requiredVersion = 3; + + // Act & Assert + assertThrows( + IllegalStateException.class, + () -> + JdbcBootstrapUtils.getRealmBootstrapSchemaVersion( + currentVersion, requiredVersion, true)); + + assertThrows( + IllegalStateException.class, + () -> + JdbcBootstrapUtils.getRealmBootstrapSchemaVersion( + currentVersion, requiredVersion, false)); + } + + @Nested + @ExtendWith(MockitoExtension.class) + class GetRequestedSchemaVersionTests { + + @Mock private BootstrapOptions mockBootstrapOptions; + @Mock private SchemaOptions mockSchemaOptions; + + @BeforeEach + void setUp() { + when(mockBootstrapOptions.schemaOptions()).thenReturn(mockSchemaOptions); + } + + @ParameterizedTest + @CsvSource({"3", "2", "12"}) + void whenVersionIsInFileName_shouldParseAndReturnIt(int expectedVersion) { + when(mockSchemaOptions.schemaVersion()).thenReturn(Optional.of(expectedVersion)); + + int result = JdbcBootstrapUtils.getRequestedSchemaVersion(mockBootstrapOptions); + assertEquals(expectedVersion, result); + } + + @Test + void whenSchemaOptionsIsNull_shouldReturnDefault() { + when(mockBootstrapOptions.schemaOptions()).thenReturn(null); + int result = JdbcBootstrapUtils.getRequestedSchemaVersion(mockBootstrapOptions); + assertEquals(-1, result); + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/SchemaOptions.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/SchemaOptions.java index f0779cc8e1..5cfc20a889 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/SchemaOptions.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/bootstrap/SchemaOptions.java @@ -19,22 +19,10 @@ package org.apache.polaris.core.persistence.bootstrap; -import jakarta.annotation.Nullable; +import java.util.Optional; 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."); - } - } + Optional schemaVersion(); } diff --git a/runtime/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java b/runtime/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java index 097c5e6629..0bdf291b6f 100644 --- a/runtime/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java +++ b/runtime/admin/src/main/java/org/apache/polaris/admintool/BootstrapCommand.java @@ -35,20 +35,28 @@ description = "Bootstraps realms and root principal credentials.") public class BootstrapCommand extends BaseCommand { - @CommandLine.ArgGroup(multiplicity = "1") - InputOptions inputOptions; + @CommandLine.Mixin InputOptions inputOptions; static class InputOptions { - @CommandLine.ArgGroup(multiplicity = "1", exclusive = false) - StandardInputOptions stdinOptions; - + // This ArgGroup enforces the mandatory, exclusive choice. @CommandLine.ArgGroup(multiplicity = "1") - FileInputOptions fileOptions; + RootCredentialsOptions rootCredentialsOptions; - @CommandLine.ArgGroup(multiplicity = "1") - SchemaInputOptions schemaInputOptions; + // This @Mixin provides independent, optional schema flags. + @CommandLine.Mixin SchemaInputOptions schemaInputOptions = new SchemaInputOptions(); + + // This static inner class encapsulates the mutually exclusive choices. + static class RootCredentialsOptions { + + @CommandLine.ArgGroup(exclusive = false, heading = "Standard Input Options:%n") + StandardInputOptions stdinOptions; + @CommandLine.ArgGroup(exclusive = false, heading = "File Input Options:%n") + FileInputOptions fileOptions; + } + + // Option container classes static class StandardInputOptions { @CommandLine.Option( @@ -85,14 +93,6 @@ static class SchemaInputOptions { paramLabel = "", description = "The version of the schema to load in [1, 2, 3, LATEST].") Integer schemaVersion; - - @CommandLine.Option( - names = {"--schema-file"}, - paramLabel = "", - description = - "A schema file to bootstrap from. If unset, the bundled files will be used.", - defaultValue = "") - String schemaFile; } } @@ -102,19 +102,22 @@ public Integer call() { RootCredentialsSet rootCredentialsSet; List realms; // TODO Iterable - if (inputOptions.fileOptions != null) { - rootCredentialsSet = RootCredentialsSet.fromUri(inputOptions.fileOptions.file.toUri()); + if (inputOptions.rootCredentialsOptions.fileOptions != null) { + rootCredentialsSet = + RootCredentialsSet.fromUri( + inputOptions.rootCredentialsOptions.fileOptions.file.toUri()); realms = rootCredentialsSet.credentials().keySet().stream().toList(); } else { - realms = inputOptions.stdinOptions.realms; + realms = inputOptions.rootCredentialsOptions.stdinOptions.realms; rootCredentialsSet = - inputOptions.stdinOptions.credentials == null - || inputOptions.stdinOptions.credentials.isEmpty() + inputOptions.rootCredentialsOptions.stdinOptions.credentials == null + || inputOptions.rootCredentialsOptions.stdinOptions.credentials.isEmpty() ? RootCredentialsSet.EMPTY - : RootCredentialsSet.fromList(inputOptions.stdinOptions.credentials); - if (inputOptions.stdinOptions.credentials == null - || inputOptions.stdinOptions.credentials.isEmpty()) { - if (!inputOptions.stdinOptions.printCredentials) { + : RootCredentialsSet.fromList( + inputOptions.rootCredentialsOptions.stdinOptions.credentials); + if (inputOptions.rootCredentialsOptions.stdinOptions.credentials == null + || inputOptions.rootCredentialsOptions.stdinOptions.credentials.isEmpty()) { + if (!inputOptions.rootCredentialsOptions.stdinOptions.printCredentials) { spec.commandLine() .getErr() .println( @@ -127,11 +130,13 @@ public Integer call() { final SchemaOptions schemaOptions; if (inputOptions.schemaInputOptions != null) { - schemaOptions = - ImmutableSchemaOptions.builder() - .schemaFile(inputOptions.schemaInputOptions.schemaFile) - .schemaVersion(inputOptions.schemaInputOptions.schemaVersion) - .build(); + ImmutableSchemaOptions.Builder builder = ImmutableSchemaOptions.builder(); + + if (inputOptions.schemaInputOptions.schemaVersion != null) { + builder.schemaVersion(inputOptions.schemaInputOptions.schemaVersion); + } + + schemaOptions = builder.build(); } else { schemaOptions = ImmutableSchemaOptions.builder().build(); } @@ -153,7 +158,8 @@ public Integer call() { if (result.getValue().isSuccess()) { String realm = result.getKey(); spec.commandLine().getOut().printf("Realm '%s' successfully bootstrapped.%n", realm); - if (inputOptions.stdinOptions != null && inputOptions.stdinOptions.printCredentials) { + if (inputOptions.rootCredentialsOptions.stdinOptions != null + && inputOptions.rootCredentialsOptions.stdinOptions.printCredentials) { String msg = String.format( "realm: %1s root principal credentials: %2s:%3s", diff --git a/runtime/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTestBase.java b/runtime/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTestBase.java index 70a0b5ed69..9286c53b72 100644 --- a/runtime/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTestBase.java +++ b/runtime/admin/src/test/java/org/apache/polaris/admintool/BootstrapCommandTestBase.java @@ -91,9 +91,7 @@ public void testBootstrapInvalidCredentials(LaunchResult result) { public void testBootstrapInvalidArguments(LaunchResult result) { assertThat(result.getErrorOutput()) .contains( - "(-r= [-r=]... [-c=]... [-p]) and -f= " - + "and (-v= | [--schema-file=]) are mutually exclusive " - + "(specify only one)"); + "Error: [-r= [-r=]... [-c=]... [-p]] and [[-f=]] are mutually exclusive (specify only one)"); } @Test diff --git a/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/RelationalJdbcBootstrapCommandTest.java b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/RelationalJdbcBootstrapCommandTest.java index dcd5eb1408..31f3a9eea0 100644 --- a/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/RelationalJdbcBootstrapCommandTest.java +++ b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/RelationalJdbcBootstrapCommandTest.java @@ -18,8 +18,30 @@ */ package org.apache.polaris.admintool.relational.jdbc; +import static org.assertj.core.api.Assertions.assertThat; + import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.junit.main.LaunchResult; +import io.quarkus.test.junit.main.QuarkusMainLauncher; import org.apache.polaris.admintool.BootstrapCommandTestBase; +import org.junit.jupiter.api.Test; @TestProfile(RelationalJdbcAdminProfile.class) -public class RelationalJdbcBootstrapCommandTest extends BootstrapCommandTestBase {} +public class RelationalJdbcBootstrapCommandTest extends BootstrapCommandTestBase { + + @Test + public void testBootstrapFailsWhenAddingRealmWithDifferentSchemaVersion( + QuarkusMainLauncher launcher) { + // First, bootstrap the schema to version 1 + LaunchResult result1 = + launcher.launch("bootstrap", "-v", "1", "-r", "realm1", "-c", "realm1,root,s3cr3t"); + assertThat(result1.exitCode()).isEqualTo(0); + assertThat(result1.getOutput()).contains("Bootstrap completed successfully."); + + // TODO: enable this once we enable postgres container reuse in the same test. + // LaunchResult result2 = launcher.launch("bootstrap", "-v", "2", "-r", "realm2", "-c", + // "realm2,root,s3cr3t"); + // assertThat(result2.exitCode()).isEqualTo(EXIT_CODE_BOOTSTRAP_ERROR); + // assertThat(result2.getOutput()).contains("Cannot bootstrap due to schema version mismatch."); + } +}