Skip to content

[JDBC] [DO NOT REVIEW] Support multiple Quarkus datasources #1482

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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ dependencies {
api(project(":polaris-jpa-model"))

api(project(":polaris-quarkus-admin"))
api(project(":polaris-quarkus-test-commons"))
api(project(":polaris-quarkus-common"))
api(project(":polaris-quarkus-test-common"))
api(project(":polaris-quarkus-defaults"))
api(project(":polaris-quarkus-server"))
api(project(":polaris-quarkus-service"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@

public class DatasourceOperations {

private static final String CONSTRAINT_VIOLATION_SQL_CODE = "23505";
private static final String CONSTRAINT_VIOLATION_SQL_CODE_POSTGRES = "23505";
private static final String TABLE_DOES_NOT_EXIST_SQL_CODE_POSTGRES = "42P01";

private final DataSource datasource;

Expand Down Expand Up @@ -182,7 +183,11 @@ public interface TransactionCallback {
}

public boolean isConstraintViolation(SQLException e) {
return CONSTRAINT_VIOLATION_SQL_CODE.equals(e.getSQLState());
return CONSTRAINT_VIOLATION_SQL_CODE_POSTGRES.equals(e.getSQLState());
}

public boolean isTableNotExists(SQLException e) {
return TABLE_DOES_NOT_EXIST_SQL_CODE_POSTGRES.equals(e.getSQLState());
}

private Connection borrowConnection() throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ private PolarisBaseEntity getPolarisBaseEntity(String query) {
return results.getFirst();
}
} catch (SQLException e) {
// This look-up is used for checking if the realm is bootstrap or not.
// If we have 1 DB per realm it might happen that the realm is not boostrap
// at all.
Copy link
Contributor

Choose a reason for hiding this comment

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

So why not throw an exception in that 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.

Its like we checked and table didn't exists, so this looks up should not fail but rather say that he nothing is bootstrappped and hence trigger actual bootstrap or purge or something

Copy link
Contributor

Choose a reason for hiding this comment

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

would returning null help with bootstrapping?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes entity null would mean entity not found and this would prompt bootstrap which inturn would run the bootstrap script. Otherwise RTE here is everything broken, i agree this is not an ideal but this is something we have from persistence we expect EntityResult

Copy link
Contributor

Choose a reason for hiding this comment

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

this logic is too obscure IMHO. Would it be preferable to have a dedicated isRealmBootstrapped(realmId) method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about if take this is in my MetaStoreManager refactor ? #1462
I have assigned it to me

if (datasourceOperations.isTableNotExists(e)) {
return null;
}
throw new RuntimeException(
String.format("Failed to retrieve polaris entity due to %s", e.getMessage()), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import io.smallrye.common.annotation.Identifier;
import jakarta.annotation.Nullable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import java.sql.Connection;
import java.sql.SQLException;
Expand All @@ -41,6 +40,7 @@
import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
import org.apache.polaris.core.persistence.AtomicOperationMetaStoreManager;
import org.apache.polaris.core.persistence.BasePersistence;
import org.apache.polaris.core.persistence.DatasourceSupplier;
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.PrincipalSecretsGenerator;
Expand Down Expand Up @@ -73,7 +73,7 @@ public class JdbcMetaStoreManagerFactory implements MetaStoreManagerFactory {
protected final PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl();

@Inject PolarisStorageIntegrationProvider storageIntegrationProvider;
@Inject Instance<DataSource> dataSource;
@Inject DatasourceSupplier jdbcDatasource;

protected JdbcMetaStoreManagerFactory() {}

Expand All @@ -93,7 +93,8 @@ protected PolarisMetaStoreManager createNewMetaStoreManager() {

private void initializeForRealm(
RealmContext realmContext, RootCredentialsSet rootCredentialsSet, boolean isBootstrap) {
DatasourceOperations databaseOperations = getDatasourceOperations(isBootstrap);
DatasourceOperations databaseOperations =
getDatasourceOperations(isBootstrap, realmContext.getRealmIdentifier());
sessionSupplierMap.put(
realmContext.getRealmIdentifier(),
() ->
Expand All @@ -107,12 +108,13 @@ private void initializeForRealm(
metaStoreManagerMap.put(realmContext.getRealmIdentifier(), metaStoreManager);
}

private DatasourceOperations getDatasourceOperations(boolean isBootstrap) {
DatasourceOperations databaseOperations = new DatasourceOperations(dataSource.get());
private DatasourceOperations getDatasourceOperations(boolean isBootstrap, String realmId) {
DataSource datasource = jdbcDatasource.fromRealmId(realmId);
DatasourceOperations databaseOperations = new DatasourceOperations(datasource);
if (isBootstrap) {
try {
DatabaseType databaseType;
try (Connection connection = dataSource.get().getConnection()) {
try (Connection connection = datasource.getConnection()) {
String productName = connection.getMetaData().getDatabaseProductName();
databaseType = DatabaseType.fromDisplayName(productName);
}
Expand Down
3 changes: 2 additions & 1 deletion gradle/projects.main.properties
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ polaris-quarkus-service=quarkus/service
polaris-quarkus-server=quarkus/server
polaris-quarkus-spark-tests=quarkus/spark-tests
polaris-quarkus-admin=quarkus/admin
polaris-quarkus-test-commons=quarkus/test-commons
polaris-quarkus-common=quarkus/common
polaris-quarkus-test-common=quarkus/test-common
polaris-quarkus-run-script=quarkus/run-script
polaris-eclipselink=extension/persistence/eclipselink
polaris-jpa-model=extension/persistence/jpa-model
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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;

import javax.sql.DataSource;

public interface DatasourceSupplier {
DataSource fromRealmId(String realmId);
}
4 changes: 2 additions & 2 deletions quarkus/admin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ dependencies {
runtimeOnly(project(":polaris-eclipselink"))
runtimeOnly(project(":polaris-relational-jdbc"))
runtimeOnly("org.postgresql:postgresql")

implementation("io.quarkus:quarkus-jdbc-postgresql")
implementation(enforcedPlatform(libs.quarkus.bom))
implementation("io.quarkus:quarkus-picocli")
implementation("io.quarkus:quarkus-container-image-docker")

implementation("org.jboss.slf4j:slf4j-jboss-logmanager")

testImplementation(project(":polaris-quarkus-test-commons"))
implementation(project(":polaris-quarkus-common"))
testImplementation(project(":polaris-quarkus-test-common"))
testFixturesApi(project(":polaris-core"))

testFixturesApi(enforcedPlatform(libs.quarkus.bom))
Expand Down
18 changes: 18 additions & 0 deletions quarkus/admin/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,21 @@ quarkus.index-dependency.guava.artifact-id=guava
quarkus.index-dependency.protobuf.group-id=com.google.protobuf
quarkus.index-dependency.protobuf.artifact-id=protobuf-java
quarkus.datasource.devservices.image-name=postgres:17-alpine

#quarkus.datasource.db-kind=pgsql
#quarkus.datasource.jdbc.url=polaris
#quarkus.datasource.username=polaris
#quarkus.datasource.password=polaris
quarkus.datasource.\"realm1_ds\".db-kind=pgsql
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 still not sure why we need the multitude of test-like datasource configs in the production sections of the admin tool properties 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure whats happening but here is the reason behind : #1482 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

The problem here is that for a Datasource to be created by Quarkus, there has to be at least one build-time property defined for that datasource: db-kind.

And you cannot pass build-time properties through QuarkusTestResourceLifecycleManager#start.

So PostgresRelationalJdbcLifeCycleManagement is trying to create datasources dynamically, but since this is happening at runtime, this is not working.

Thus for tests you have to declare the datasources you intend to use here, along with their db-kind:

quarkus.datasource.\"realm1_ds\".db-kind=pgsql
quarkus.datasource.\"realm2_ds\".db-kind=pgsql
quarkus.datasource.\"realm3_ds\".db-kind=pgsql
# etc

Other runtime properties, like jdbc.url, username and password can (and should) be defined in PostgresRelationalJdbcLifeCycleManagement.

Copy link
Contributor

@dimas-b dimas-b Apr 30, 2025

Choose a reason for hiding this comment

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

If quarkus.datasource.\"realm1_ds\".db-kind=pgsql has to be defined at build time, that will be a problem for users of the binary distribution, I guess.

I'm surprised this is required, because the default datasource does not have to be defined at build time, if I'm not mistaken.

Copy link
Contributor

Choose a reason for hiding this comment

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

If quarkus.datasource."realm1_ds".db-kind=pgsql has to be defined at build time, that will be a problem for users of the binary distribution, I guess.

See https://quarkus.io/guides/datasource#configure-multiple-datasources:

Even when only one database extension is installed, named databases need to specify at least one build-time property so that Quarkus can detect them. Generally, this is the db-kind property [...]

This is why I asked this question, I think there is some misunderstanding going on: #1482 (comment)

I'm surprised this is required, because the default datasource does not have to be defined at build time, if I'm not mistaken.

The default datasource is treated slightly different: if db-kind is missing, the JDBC driver will be guessed from the available JDBC drivers.

But that doesn't help here, since the intent is to use many named datasources.

Copy link
Contributor

@dimas-b dimas-b Apr 30, 2025

Choose a reason for hiding this comment

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

from my POV, it might be fine to support one Data Source (co-located realms) out-of-the-box and instruct users to use custom builds if they want to segregate realms by Data Source (experience similar to EclipseLink).

Copy link
Member

Choose a reason for hiding this comment

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

Why not configure those DS using a quarkus-test-profile?

Copy link
Contributor Author

@singhpk234 singhpk234 May 7, 2025

Choose a reason for hiding this comment

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

@snazy we can't as we need atleast one build time prop please ref this comment from @adutra #1482 (comment)

I tried overriding them by quarkus-test-profiles but the override is not able to create named DS

Copy link
Contributor

Choose a reason for hiding this comment

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

@singhpk234 : This PR accumulated a lot of comments, some of them about non-trivial issues... Would you mind making a dev email with a summary and options for moving forward? I guess it would be clearer than continuing on GH. Once we have a consensus on how to proceed we can revamp this PR. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sounds fair ! its on my list, should be sending out an email soon, meanwhile marking this PR as draft to not draw more attention,

quarkus.datasource.\"realm1_ds\".jdbc.url=polaris
quarkus.datasource.\"realm1_ds\".username=polaris
quarkus.datasource.\"realm1_ds\".password=polaris
quarkus.datasource.\"realm2_ds\".db-kind=pgsql
quarkus.datasource.\"realm2_ds\".jdbc.url=polaris
quarkus.datasource.\"realm2_ds\".username=polaris
quarkus.datasource.\"realm2_ds\".password=polaris
quarkus.datasource.\"realm3_ds\".db-kind=pgsql
quarkus.datasource.\"realm3_ds\".jdbc.url=polaris
quarkus.datasource.\"realm3_ds\".username=polaris
quarkus.datasource.\"realm3_ds\".password=polaris

Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

import java.util.List;
import java.util.Map;
import org.apache.polaris.test.commons.PostgresRelationalJdbcLifeCycleManagement;
import org.apache.polaris.test.commons.RelationalJdbcProfile;
import org.apache.polaris.test.common.PostgresRelationalJdbcLifeCycleManagement;
import org.apache.polaris.test.common.RelationalJdbcProfile;

public class RelationalJdbcAdminProfile extends RelationalJdbcProfile {
@Override
Expand All @@ -36,6 +36,10 @@ public List<TestResourceEntry> testResources() {
return List.of(
new TestResourceEntry(
PostgresRelationalJdbcLifeCycleManagement.class,
Map.of(INIT_SCRIPT, "org/apache/polaris/admintool/init.sql")));
Map.of(
INIT_SCRIPT,
"org/apache/polaris/admintool/jdbc/init.sql",
"databases",
"realm1,realm2,realm3")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.
*/

-- Create two more databases for testing. The first database, polaris_realm1, is created
-- during container initialization. See PostgresTestResourceLifecycleManager.

-- Note: the database names must follow the pattern polaris_{realm}. That's the pattern
-- specified by the persistence.xml file used in tests.

CREATE DATABASE realm2;
GRANT ALL PRIVILEGES ON DATABASE realm2 TO polaris;

CREATE DATABASE realm3;
GRANT ALL PRIVILEGES ON DATABASE realm3 TO polaris;
43 changes: 43 additions & 0 deletions quarkus/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.
*/

plugins {
alias(libs.plugins.jandex)
id("java-test-fixtures")
}

configurations.all {
exclude(group = "org.antlr", module = "antlr4-runtime")
exclude(group = "org.scala-lang", module = "scala-library")
exclude(group = "org.scala-lang", module = "scala-reflect")
}

java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

dependencies {
implementation(project(":polaris-core"))
implementation(project(":polaris-relational-jdbc"))

compileOnly(libs.smallrye.config.core) // @ConfigMap
implementation(enforcedPlatform(libs.quarkus.bom))
implementation("io.quarkus:quarkus-arc")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.common;
Copy link
Member

Choose a reason for hiding this comment

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

Common for everything?


import io.quarkus.arc.All;
import io.quarkus.arc.InstanceHandle;
import java.util.List;
import javax.sql.DataSource;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.polaris.core.persistence.DatasourceSupplier;

@ApplicationScoped
public class QuarkusDatasourceSupplier implements DatasourceSupplier {
private final List<InstanceHandle<DataSource>> dataSources;
private final RelationalJdbcConfiguration relationalJdbcConfiguration;

private static final String DEFAULT_DATA_SOURCE_NAME = "default";

@Inject
public QuarkusDatasourceSupplier(
RelationalJdbcConfiguration relationalJdbcConfiguration,
@All List<InstanceHandle<DataSource>> dataSources) {
this.relationalJdbcConfiguration = relationalJdbcConfiguration;
this.dataSources = dataSources;
}

@Override
public DataSource fromRealmId(String realmId) {
// check if the mapping of realm to DS exists, otherwise fall back to default
String dataSourceName = relationalJdbcConfiguration.realms().getOrDefault(
realmId,
relationalJdbcConfiguration.defaultDatasource().orElse(null)
);

// if neither mapping exists nor default DS exists, fail
if (dataSourceName == null) {
throw new IllegalStateException(String.format(
"No datasource configured with name: %s nor default datasource configured", realmId));
}

// check if there is actually a datasource of that dataSourceName
return dataSources.stream()
.filter(ds -> {
String name = ds.getBean().getName();
name = name == null ? DEFAULT_DATA_SOURCE_NAME : name;
return name.equals(dataSourceName);
})
.map(InstanceHandle::get)
.findFirst()
.orElseThrow(() -> new IllegalStateException(String.format(
"No datasource configured with name: %s", dataSourceName)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.common;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithParentName;

import java.util.Map;
import java.util.Optional;

@ConfigMapping(prefix = "polaris.relational.jdbc.datasource")
public interface RelationalJdbcConfiguration {
/** realmId to configured Datasource name mapping. */
@WithParentName
Map<String, String> realms();

/**
* Default datasource name to be used for a realmId when there is no mapping of realmId to
* Datasource name present.
*/
Optional<String> defaultDatasource();
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ polaris.realm-context.realms=POLARIS,OTHER

polaris.storage.gcp.token=token
polaris.storage.gcp.lifespan=PT1H

quarkus.datasource.realm1_ds.db-kind=pgsql
quarkus.datasource.realm1_ds.jdbc.url=polaris
quarkus.datasource.realm1_ds.username=polaris
quarkus.datasource.realm1_ds.password=polaris
Loading