Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
71a1007
Make the Relational JDBC schema name configurable
cobed95 Jul 1, 2026
f284f34
Default the Helm chart schemaName to POLARIS_SCHEMA
cobed95 Jul 2, 2026
add93e0
Select the session schema on borrowed connections
cobed95 Jul 9, 2026
1d1d534
Make the bootstrap SQL scripts schema-agnostic
cobed95 Jul 9, 2026
ca6b1ca
Validate the configured schema name with bean validation
cobed95 Jul 9, 2026
79faeed
Document schema name case folding and required privileges
cobed95 Jul 9, 2026
5b73c66
Merge remote-tracking branch 'origin/main' into jdbc-configurable-schema
cobed95 Jul 11, 2026
998e702
Merge remote-tracking branch 'origin/main' into jdbc-configurable-schema
cobed95 Jul 14, 2026
d75c444
Restore the historical bootstrap scripts (v0-v3) unchanged
cobed95 Jul 14, 2026
5433bbe
Merge remote-tracking branch 'origin/main' into jdbc-configurable-schema
cobed95 Jul 19, 2026
2eb56ff
Move the schema-agnostic bootstrap scripts from v4 to v5
cobed95 Jul 19, 2026
b955915
Make the JDBC persistence layer agnostic of the schema name
cobed95 Jul 19, 2026
06dd6f3
Ship a default currentSchema for the shipped JDBC drivers
cobed95 Jul 19, 2026
13a6ed8
Helm: schemaName renders the datasource currentSchema property
cobed95 Jul 19, 2026
4876c41
Document driver-level schema selection and the bootstrap prerequisite
cobed95 Jul 19, 2026
6900912
Helm: expose relationalJdbc.additionalProperties for JDBC driver prop…
cobed95 Jul 21, 2026
eb39400
Align docs and the Helm test fixture with schema-agnostic bootstrap
cobed95 Jul 21, 2026
700e54b
Helm: document that currentSchema is ignored by non-PostgreSQL drivers
cobed95 Jul 22, 2026
142c64c
Simplify the schema-location test query with LIMIT 1
cobed95 Jul 22, 2026
9b9a6c9
Merge remote-tracking branch 'origin/main' into jdbc-configurable-schema
cobed95 Jul 22, 2026
cd827ce
Provide the Polaris schema in test datasources
cobed95 Jul 22, 2026
8dcf2df
Regenerate Helm values schema for additionalProperties
cobed95 Jul 22, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
See the Relational JDBC metastore documentation for details.

### Breaking changes
- The Relational JDBC backend no longer creates its database schema during bootstrap: creating the
schema is a privileged operation that belongs to a database administrator. Fresh installations
must create the schema (by default `CREATE SCHEMA polaris_schema;` on PostgreSQL) before running
the admin tool's `bootstrap` command. Existing deployments are unaffected — their schema already
exists, and the shipped `currentSchema` default (`POLARIS_SCHEMA`) preserves the previous
behavior on upgrade.
- Removed the `--schema-version` (`-v`) option from the admin tool's `bootstrap` command. New realms
are now always bootstrapped with the latest available schema version.
- The `MaintenanceService.performMaintenance()` signature now requires an explicit `OptionalLong overrideRunId` argument to supersede the latest unfinished maintenance run.
Expand All @@ -67,6 +73,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
- Added the `DEFAULT_UNIQUE_TABLE_LOCATION_ENABLED` feature flag (off by default). When enabled, a managed location generated for a table or view created without an explicit location is given a unique, unpredictable suffix, so that no two tables share a path prefix.
- Added the `ALLOW_CLIENT_SPECIFIED_TABLE_LOCATION` feature flag (on by default). When set to false, a caller-specified location (the `location` field, a `SetLocation` update, or the `write.data.path` / `write.metadata.path` properties) on a create-table (including a staged create-table request), create-view, update-table, replace-view, or commit-transaction request is rejected, forcing Polaris to manage all locations. Federated catalogs, committing an already staged create, and `register table` / `register view` are unaffected.
- Added `maintenance` support in Helm chart.
- The database schema used by the Relational JDBC persistence backend is now configurable through standard datasource configuration: the JDBC driver's `currentSchema` connection property (defaulted to `POLARIS_SCHEMA` via `quarkus.datasource.jdbc.additional-jdbc-properties.currentSchema`) selects the schema, and the persistence layer is agnostic of the schema name. Also exposed as `persistence.relationalJdbc.schemaName` in the Helm chart.
- Python CLI: added `--catalog-url` to specify a custom base URL for the Iceberg REST Catalog (IRC) API. Allows use with deployments that map a path (e.g. `/server1`) directly to the catalog root instead of the standard `/api/catalog`. See #4927.
- Added support for publishing histogram buckets for HTTP server request duration as configured SLO boundaries.
- Added an OpenTelemetry event listener for emitting Polaris audit events as OpenTelemetry log records.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void setUp() throws SQLException {

// Execute main schema v4 (includes metrics tables)
ClassLoader classLoader = DatasourceOperations.class.getClassLoader();
InputStream schemaStream = classLoader.getResourceAsStream("h2/schema-v4.sql");
InputStream schemaStream = classLoader.getResourceAsStream("h2/schema-v5.sql");
datasourceOperations.executeScript(schemaStream);

RealmContext realmContext = () -> "TEST_REALM";
Expand Down
17 changes: 17 additions & 0 deletions helm/polaris/ci/fixtures/postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ spec:
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 15
volumeMounts:
# Create the Polaris schema on first init: Polaris does not create it itself.
- name: init-schema
mountPath: /docker-entrypoint-initdb.d

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I love the docker-entrypoint-initdb.d approach 😄

volumes:
- name: init-schema
configMap:
name: postgres-init-schema
---
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-init-schema
data:
create-polaris-schema.sql: |
-- Polaris does not create its database schema; it must exist before Polaris connects.
CREATE SCHEMA IF NOT EXISTS polaris_schema;
---
apiVersion: v1
kind: Service
Expand Down
5 changes: 5 additions & 0 deletions helm/polaris/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ data:
{{- $_ = set $map "polaris.persistence.nosql.backend" .Values.persistence.nosql.backend -}}
{{- $_ = set $map "quarkus.mongodb.database" .Values.persistence.nosql.database -}}
{{- end -}}
{{- if eq .Values.persistence.type "relational-jdbc" -}}
{{- range $name, $value := .Values.persistence.relationalJdbc.additionalProperties -}}
{{- $_ = set $map (print "quarkus.datasource.jdbc.additional-jdbc-properties." $name) $value -}}
{{- end -}}
{{- end -}}

{{- /* File IO */ -}}
{{- $_ = set $map "polaris.file-io.type" .Values.fileIo.type -}}
Expand Down
15 changes: 15 additions & 0 deletions helm/polaris/tests/configmap_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ tests:
persistence: { type: "relational-jdbc", relationalJdbc: { secret: { name: "polaris-persistence" } } }
asserts:
- matchRegex: { path: 'data["application.properties"]', pattern: "polaris.persistence.type=relational-jdbc" }
- matchRegex: { path: 'data["application.properties"]', pattern: "quarkus.datasource.jdbc.additional-jdbc-properties.currentSchema=POLARIS_SCHEMA" }

- it: should configure relational-jdbc persistence with a custom schema name
set:
persistence: { type: "relational-jdbc", relationalJdbc: { additionalProperties: { currentSchema: "custom_schema" }, secret: { name: "polaris-persistence" } } }
asserts:
- matchRegex: { path: 'data["application.properties"]', pattern: "polaris.persistence.type=relational-jdbc" }
- matchRegex: { path: 'data["application.properties"]', pattern: "quarkus.datasource.jdbc.additional-jdbc-properties.currentSchema=custom_schema" }

- it: should configure relational-jdbc persistence with additional jdbc properties
set:
persistence: { type: "relational-jdbc", relationalJdbc: { additionalProperties: { currentSchema: "custom_schema", ApplicationName: "polaris" }, secret: { name: "polaris-persistence" } } }
asserts:
- matchRegex: { path: 'data["application.properties"]', pattern: "quarkus.datasource.jdbc.additional-jdbc-properties.currentSchema=custom_schema" }
- matchRegex: { path: 'data["application.properties"]', pattern: "quarkus.datasource.jdbc.additional-jdbc-properties.ApplicationName=polaris" }

- it: should configure nosql persistence with default values
set:
Expand Down
12 changes: 12 additions & 0 deletions helm/polaris/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,18 @@
"relationalJdbc": {
"type": "object",
"properties": {
"additionalProperties": {
"description": "Additional JDBC connection properties passed to the datasource driver as `quarkus.datasource.jdbc.additional-jdbc-properties.\u003cname\u003e`. The default sets `currentSchema`, which on PostgreSQL and CockroachDB selects the database schema (namespace) holding the Polaris tables; that schema must exist before Polaris connects (creating it is a DBA task) and is passed to the driver unquoted, so the database applies its usual identifier case folding. Note that `currentSchema` is PostgreSQL/CockroachDB-specific and is silently ignored by other drivers such as MySQL, where the schema is instead the database named in the JDBC URL. Add further entries to customize other driver properties.",
"type": "object",
"properties": {
"currentSchema": {
"type": "string"
}
},
"additionalProperties": {
"type": "string"
}
},
"secret": {
"type": "object",
"properties": {
Expand Down
12 changes: 12 additions & 0 deletions helm/polaris/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,18 @@ persistence:
type: in-memory # relational-jdbc, nosql
# The configuration for the relational-jdbc persistence manager.
relationalJdbc:
# @schema additionalProperties: {type: string}
# -- Additional JDBC connection properties passed to the datasource driver as
# `quarkus.datasource.jdbc.additional-jdbc-properties.<name>`. The default sets `currentSchema`,
# which on PostgreSQL and CockroachDB selects the database schema (namespace) holding the Polaris
# tables; that schema must exist before Polaris connects (creating it is a DBA task) and is passed
# to the driver unquoted, so the database applies its usual identifier case folding. Note that
# `currentSchema` is PostgreSQL/CockroachDB-specific and is silently ignored by other drivers such
# as MySQL, where the schema is instead the database named in the JDBC URL. Add further entries to
# customize other driver properties.
# @section -- Persistence
additionalProperties:
currentSchema: POLARIS_SCHEMA
# The secret name to pull the database connection properties from.
secret:
# -- The secret name to pull database connection properties from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ public class DatasourceOperations {
private static final String RELATION_DOES_NOT_EXIST = "42P01";

// H2 STATUS CODES
// 90079 = Schema not found, 42S02 = Table or view not found
// 90079 = Schema not found, 42S02 = Table or view not found, 42S04 = Table or view not found
// (database empty). The latter surfaces for unqualified table references against a fresh
// database, where previously a schema-qualified reference produced a schema-not-found error.
private static final String H2_SCHEMA_DOES_NOT_EXIST = "90079";
private static final String H2_TABLE_DOES_NOT_EXIST = "42S02";
private static final String H2_TABLE_NOT_FOUND_DATABASE_EMPTY = "42S04";

// POSTGRES RETRYABLE EXCEPTIONS
private static final String SERIALIZATION_FAILURE_SQL_CODE = "40001";
Expand Down Expand Up @@ -457,7 +460,8 @@ public boolean isRelationDoesNotExist(SQLException e) {
return (RELATION_DOES_NOT_EXIST.equals(e.getSQLState())
&& (databaseType == DatabaseType.POSTGRES || databaseType == DatabaseType.COCKROACHDB))
|| ((H2_SCHEMA_DOES_NOT_EXIST.equals(e.getSQLState())
|| H2_TABLE_DOES_NOT_EXIST.equals(e.getSQLState()))
|| H2_TABLE_DOES_NOT_EXIST.equals(e.getSQLState())
|| H2_TABLE_NOT_FOUND_DATABASE_EMPTY.equals(e.getSQLState()))
&& databaseType == DatabaseType.H2);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
/**
* Utility class to generate parameterized SQL queries (SELECT, INSERT, UPDATE, DELETE). Ensures
* consistent SQL generation and protects against injection by managing parameters separately.
*
* <p>Generated queries reference tables by their unqualified names; the schema holding the Polaris
* tables is selected through the datasource configuration (for example the PostgreSQL driver's
* {@code currentSchema} connection property), so the persistence code is agnostic of it.
*/
public class QueryGenerator {

Expand Down Expand Up @@ -127,8 +131,7 @@ public static PreparedQuery generateDeleteQueryForEntityGrantRecords(
List<Object> params =
Arrays.asList(
entity.getId(), entity.getCatalogId(), entity.getId(), entity.getCatalogId(), realmId);
return new PreparedQuery(
"DELETE FROM " + getFullyQualifiedTableName(ModelGrantRecord.TABLE_NAME) + where, params);
return new PreparedQuery("DELETE FROM " + ModelGrantRecord.TABLE_NAME + where, params);
}

/**
Expand Down Expand Up @@ -195,14 +198,7 @@ public static PreparedQuery generateInsertQuery(
finalValues.add(realmId);
String columns = String.join(", ", finalColumns);
String placeholders = finalColumns.stream().map(c -> "?").collect(Collectors.joining(", "));
String sql =
"INSERT INTO "
+ getFullyQualifiedTableName(tableName)
+ " ("
+ columns
+ ") VALUES ("
+ placeholders
+ ")";
String sql = "INSERT INTO " + tableName + " (" + columns + ") VALUES (" + placeholders + ")";
return new PreparedQuery(sql, finalValues);
}

Expand All @@ -223,8 +219,7 @@ public static PreparedQuery generateUpdateQuery(
List<Object> bindingParams = new ArrayList<>(values);
QueryFragment where = generateWhereClause(new HashSet<>(allColumns), whereClause, Map.of());
String setClause = allColumns.stream().map(c -> c + " = ?").collect(Collectors.joining(", "));
String sql =
"UPDATE " + getFullyQualifiedTableName(tableName) + " SET " + setClause + where.sql();
String sql = "UPDATE " + tableName + " SET " + setClause + where.sql();
bindingParams.addAll(where.parameters());
return new PreparedQuery(sql, bindingParams);
}
Expand All @@ -242,8 +237,7 @@ public static PreparedQuery generateDeleteQuery(
@NonNull String tableName,
@NonNull Map<String, Object> whereClause) {
QueryFragment where = generateWhereClause(new HashSet<>(tableColumns), whereClause, Map.of());
return new PreparedQuery(
"DELETE FROM " + getFullyQualifiedTableName(tableName) + where.sql(), where.parameters());
return new PreparedQuery("DELETE FROM " + tableName + where.sql(), where.parameters());
}

private static PreparedQuery generateSelectQuery(
Expand All @@ -263,12 +257,7 @@ private static PreparedQuery generateSelectQuery(
if (limit != null && limit <= 0) {
throw new IllegalArgumentException("Limit must be positive");
}
String sql =
"SELECT "
+ String.join(", ", columnNames)
+ " FROM "
+ getFullyQualifiedTableName(tableName)
+ filter;
String sql = "SELECT " + String.join(", ", columnNames) + " FROM " + tableName + filter;
if (orderByColumn != null) {
sql += " ORDER BY " + orderByColumn + " ASC";
}
Expand Down Expand Up @@ -336,7 +325,7 @@ static QueryFragment generateWhereClauseExtended(

@VisibleForTesting
static PreparedQuery generateVersionQuery() {
return new PreparedQuery("SELECT version_value FROM POLARIS_SCHEMA.VERSION", List.of());
return new PreparedQuery("SELECT version_value FROM VERSION", List.of());
}

/**
Expand All @@ -348,17 +337,14 @@ public static PreparedQuery generateExistsQuery(
@NonNull String tableName,
@NonNull Map<String, Object> whereClause) {
QueryFragment where = generateWhereClause(new HashSet<>(tableColumns), whereClause, Map.of());
String sql =
"SELECT 1 FROM " + getFullyQualifiedTableName(tableName) + where.sql() + " LIMIT 1";
String sql = "SELECT 1 FROM " + tableName + where.sql() + " LIMIT 1";
return new PreparedQuery(sql, where.parameters());
}

@VisibleForTesting
static PreparedQuery generateEntityTableExistQuery() {
return new PreparedQuery(
String.format(
"SELECT * FROM %s LIMIT 1", getFullyQualifiedTableName(ModelEntity.TABLE_NAME)),
List.of());
String.format("SELECT * FROM %s LIMIT 1", ModelEntity.TABLE_NAME), List.of());
}

/**
Expand Down Expand Up @@ -413,9 +399,4 @@ public static PreparedQuery generateOverlapQuery(
null);
return new PreparedQuery(query.sql(), where.parameters());
}

static String getFullyQualifiedTableName(String tableName) {
// TODO: make schema name configurable.
return "POLARIS_SCHEMA." + tableName;
}
Comment on lines -417 to -420

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is it possible to have 2 part identifier still ? can we get the schema and add this here ?

  • i know we are setting this in connection context the schema but this seems more easy to debug stuff

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

or to put it in a different way are we logging what is the current_schema

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

IMHO, keeping the 2 part identifier coded into SQL statements seems a little redundant. However, I'm open to suggestions!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I also think it's best to not deal with schemas in the code.

However it should certainly be possible to log the current schema somewhere; it should also be possible to put the schema name in the MDC context if needed.

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
-- policy_mapping_record, events, scan_metrics_report, commit_metrics_report
-- * Compatible with PostgreSQL wire protocol

CREATE SCHEMA IF NOT EXISTS POLARIS_SCHEMA;
SET search_path TO POLARIS_SCHEMA;

CREATE TABLE IF NOT EXISTS version (
version_key TEXT PRIMARY KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
-- backed was never wired into any request path and has been removed)
-- * `events.catalog_id` is nullable; events that are not catalog-scoped store NULL (issue #4674)

CREATE SCHEMA IF NOT EXISTS POLARIS_SCHEMA;
SET SCHEMA POLARIS_SCHEMA;

CREATE TABLE IF NOT EXISTS version (
version_key VARCHAR PRIMARY KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
-- backed was never wired into any request path and has been removed)
-- * `events.catalog_id` is nullable; events that are not catalog-scoped store NULL (issue #4674)

CREATE SCHEMA IF NOT EXISTS POLARIS_SCHEMA;
SET search_path TO POLARIS_SCHEMA;

CREATE TABLE IF NOT EXISTS version (
version_key TEXT PRIMARY KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,30 @@ protected DataSource createDataSource() {
private static DataSource createPostgresDataSource(int schemaVersion) {
String databaseName = "polaris_schema_v" + schemaVersion;
createDatabaseIfNotExists(databaseName);
createPolarisSchema(databaseName);

PGSimpleDataSource postgresDataSource = new PGSimpleDataSource();
postgresDataSource.setURL(jdbcUrlForDatabase(databaseName));
postgresDataSource.setUser(POSTGRES.getUsername());
postgresDataSource.setPassword(POSTGRES.getPassword());
// The schema is provided by the datasource, not by the persistence code (the currentSchema
// driver setting in a real deployment).
postgresDataSource.setCurrentSchema("polaris_schema");
return postgresDataSource;
}

/** The DBA step in a real deployment: the schema must exist before Polaris connects. */
private static void createPolarisSchema(String databaseName) {
try (Connection connection =
DriverManager.getConnection(
jdbcUrlForDatabase(databaseName), POSTGRES.getUsername(), POSTGRES.getPassword());
Statement statement = connection.createStatement()) {
statement.execute("CREATE SCHEMA IF NOT EXISTS polaris_schema");
} catch (SQLException e) {
throw new RuntimeException("Failed to create schema in " + databaseName, e);
}
}

private static void createDatabaseIfNotExists(String databaseName) {
try (Connection connection =
DriverManager.getConnection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ protected DatabaseType databaseType() {
public abstract int schemaVersion();

protected DataSource createDataSource() {
// The schema is provided by the datasource, not by the persistence code: INIT creates the
// schema (the DBA step in a real deployment) and selects it as the session schema on every
// connection (the currentSchema/SCHEMA driver setting in a real deployment).
return JdbcConnectionPool.create(
String.format(
"jdbc:h2:file:./build/test_data/polaris/db_%s_%d",
"jdbc:h2:file:./build/test_data/polaris/db_%s_%d"
+ ";INIT=CREATE SCHEMA IF NOT EXISTS POLARIS_SCHEMA\\;SET SCHEMA POLARIS_SCHEMA",
databaseType().getDisplayName(), schemaVersion()),
"sa",
"");
Expand Down
Loading