This is an automated email from the ASF dual-hosted git repository.
yuqi4733 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new df061eb57a [#10725] feat(catalog-glue): Add GlueClientProvider and
configuration properties metadata (#10726)
df061eb57a is described below
commit df061eb57a6c86f5192413b2165ec7cbc9f4bb3e
Author: Yuhui <[email protected]>
AuthorDate: Tue Apr 14 15:40:43 2026 +0800
[#10725] feat(catalog-glue): Add GlueClientProvider and configuration
properties metadata (#10726)
### What changes were proposed in this pull request?
Add GlueClientProvider, configuration properties metadata, and
capability declarations for the catalog-glue module.
### Why are the changes needed?
AWS connection layer and property definitions are prerequisites for
Schema/Table CRUD PRs.
Fix: #10725
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
`./gradlew :catalogs:catalog-glue:test -PskipITs`
---
catalogs/catalog-glue/build.gradle.kts | 1 +
.../catalog/glue/GlueCatalogCapability.java | 56 +++++++++-
.../glue/GlueCatalogPropertiesMetadata.java | 82 ++++++++++++--
.../gravitino/catalog/glue/GlueClientProvider.java | 102 +++++++++++++++++
.../gravitino/catalog/glue/GlueConstants.java | 82 ++++++++++++++
.../catalog/glue/GlueTablePropertiesMetadata.java | 33 +++++-
.../catalog/glue/TestGlueCatalogCapability.java | 75 +++++++++++++
.../glue/TestGlueCatalogPropertiesMetadata.java | 99 +++++++++++++++++
.../catalog/glue/TestGlueClientProvider.java | 121 +++++++++++++++++++++
.../glue/TestGlueTablePropertiesMetadata.java | 56 ++++++++++
10 files changed, 694 insertions(+), 13 deletions(-)
diff --git a/catalogs/catalog-glue/build.gradle.kts
b/catalogs/catalog-glue/build.gradle.kts
index e28af8144f..b7787263c5 100644
--- a/catalogs/catalog-glue/build.gradle.kts
+++ b/catalogs/catalog-glue/build.gradle.kts
@@ -37,6 +37,7 @@ dependencies {
implementation(libs.aws.glue)
implementation(libs.aws.sts)
+ implementation(libs.commons.lang3)
implementation(libs.guava)
implementation(libs.slf4j.api)
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogCapability.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogCapability.java
index 9a17cf6a4d..8a613f9018 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogCapability.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogCapability.java
@@ -18,12 +18,62 @@
*/
package org.apache.gravitino.catalog.glue;
+import java.util.Locale;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
/**
* Capability declarations for the AWS Glue Data Catalog connector.
*
- * <p>TODO PR-02: declare actual capabilities (case sensitivity, NOT NULL
support, etc.) based on
- * Glue's known constraints.
+ * <p>AWS Glue constraints that deviate from Gravitino defaults:
+ *
+ * <ul>
+ * <li><b>Case-insensitive names</b>: Glue folds database and table names to
lowercase on storage.
+ * See <a
+ *
href="https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-databases.html">Database
+ * API</a> and <a
+ *
href="https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-tables.html">Table
+ * API</a>: <i>"folded to lowercase when it is stored"</i>.
+ * <li><b>No NOT NULL constraints</b>: The Glue {@code Column} structure has
no nullable /
+ * constraint field. See <a
+ *
href="https://docs.aws.amazon.com/glue/latest/webapi/API_Column.html">Column
API</a>.
+ * <li><b>No DEFAULT values</b>: The Glue {@code Column} structure has no
{@code defaultValue}
+ * field. See <a
href="https://docs.aws.amazon.com/glue/latest/webapi/API_Column.html">Column
+ * API</a>.
+ * </ul>
*/
-public class GlueCatalogCapability implements Capability {}
+public class GlueCatalogCapability implements Capability {
+
+ @Override
+ public CapabilityResult columnNotNull() {
+ // Glue Column structure has no nullable/constraint field — NOT NULL
cannot be expressed.
+ // See https://docs.aws.amazon.com/glue/latest/webapi/API_Column.html
+ return CapabilityResult.unsupported(
+ "AWS Glue Data Catalog does not support NOT NULL constraints on
columns.");
+ }
+
+ @Override
+ public CapabilityResult columnDefaultValue() {
+ // Glue Column structure has no defaultValue field — DEFAULT cannot be
expressed.
+ // See https://docs.aws.amazon.com/glue/latest/webapi/API_Column.html
+ return CapabilityResult.unsupported(
+ "AWS Glue Data Catalog does not support DEFAULT values on columns.");
+ }
+
+ @Override
+ public CapabilityResult caseSensitiveOnName(Scope scope) {
+ switch (scope) {
+ case SCHEMA:
+ case TABLE:
+ // Glue folds database/table names to lowercase on storage.
+ // See
https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-databases.html
+ // See
https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-tables.html
+ return CapabilityResult.unsupported(
+ "AWS Glue Data Catalog is case-insensitive for "
+ + scope.name().toLowerCase(Locale.ROOT)
+ + " names.");
+ default:
+ return CapabilityResult.SUPPORTED;
+ }
+ }
+}
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogPropertiesMetadata.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogPropertiesMetadata.java
index eacaca3950..e3215cf7d6 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogPropertiesMetadata.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogPropertiesMetadata.java
@@ -18,20 +18,88 @@
*/
package org.apache.gravitino.catalog.glue;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_ACCESS_KEY_ID;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_CATALOG_ID;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_ENDPOINT;
+import static org.apache.gravitino.catalog.glue.GlueConstants.AWS_REGION;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_SECRET_ACCESS_KEY;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT_FILTER;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT_VALUE;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.TABLE_FORMAT_FILTER;
+import static
org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyEntry;
+import static
org.apache.gravitino.connector.PropertyEntry.stringRequiredPropertyEntry;
+
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.apache.gravitino.connector.BaseCatalogPropertiesMetadata;
import org.apache.gravitino.connector.PropertyEntry;
-/**
- * Properties metadata for the AWS Glue Data Catalog connector.
- *
- * <p>TODO PR-02: add required properties (aws-region, aws-glue-catalog-id)
and optional properties
- * (credentials, endpoint override, default-table-format, table-type-filter).
- */
+/** Properties metadata for the AWS Glue Data Catalog connector catalog-level
configuration. */
public class GlueCatalogPropertiesMetadata extends
BaseCatalogPropertiesMetadata {
- private static final Map<String, PropertyEntry<?>> PROPERTIES_METADATA =
ImmutableMap.of();
+ private static final Map<String, PropertyEntry<?>> PROPERTIES_METADATA =
+ ImmutableMap.<String, PropertyEntry<?>>builder()
+ .put(
+ AWS_REGION,
+ stringRequiredPropertyEntry(
+ AWS_REGION,
+ "AWS region for the Glue Data Catalog (e.g. us-east-1)",
+ true /* immutable */,
+ false /* hidden */))
+ .put(
+ AWS_GLUE_CATALOG_ID,
+ stringOptionalPropertyEntry(
+ AWS_GLUE_CATALOG_ID,
+ "The 12-digit AWS account ID that owns the Glue catalog."
+ + " When omitted, defaults to the caller's AWS account
ID.",
+ true /* immutable */,
+ null /* defaultValue */,
+ false /* hidden */))
+ .put(
+ AWS_ACCESS_KEY_ID,
+ stringOptionalPropertyEntry(
+ AWS_ACCESS_KEY_ID,
+ "AWS access key ID for static credential authentication."
+ + " When omitted the default credential chain is used.",
+ false /* immutable */,
+ null /* defaultValue */,
+ true /* hidden */))
+ .put(
+ AWS_SECRET_ACCESS_KEY,
+ stringOptionalPropertyEntry(
+ AWS_SECRET_ACCESS_KEY,
+ "AWS secret access key paired with aws-access-key-id."
+ + " When omitted the default credential chain is used.",
+ false /* immutable */,
+ null /* defaultValue */,
+ true /* hidden */))
+ .put(
+ AWS_GLUE_ENDPOINT,
+ stringOptionalPropertyEntry(
+ AWS_GLUE_ENDPOINT,
+ "Custom Glue endpoint URL for VPC endpoints or LocalStack
testing"
+ + " (e.g. http://localhost:4566)",
+ false /* immutable */,
+ null /* defaultValue */,
+ false /* hidden */))
+ .put(
+ DEFAULT_TABLE_FORMAT,
+ stringOptionalPropertyEntry(
+ DEFAULT_TABLE_FORMAT,
+ "Default format for tables created via createTable().
Accepted: iceberg, hive.",
+ false /* immutable */,
+ DEFAULT_TABLE_FORMAT_VALUE,
+ false /* hidden */))
+ .put(
+ TABLE_FORMAT_FILTER,
+ stringOptionalPropertyEntry(
+ TABLE_FORMAT_FILTER,
+ "Comma-separated table formats exposed by listTables() and
loadTable().",
+ false /* immutable */,
+ DEFAULT_TABLE_FORMAT_FILTER,
+ false /* hidden */))
+ .build();
@Override
protected Map<String, PropertyEntry<?>> specificPropertyEntries() {
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
new file mode 100644
index 0000000000..536a3955bc
--- /dev/null
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueClientProvider.java
@@ -0,0 +1,102 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import com.google.common.base.Preconditions;
+import java.net.URI;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.glue.GlueClient;
+import software.amazon.awssdk.services.glue.GlueClientBuilder;
+
+/**
+ * Factory for creating AWS {@link GlueClient} instances from Gravitino
catalog configuration.
+ *
+ * <p>Authentication priority:
+ *
+ * <ol>
+ * <li>Static credentials ({@code aws-access-key-id} + {@code
aws-secret-access-key})
+ * <li>Default credential chain (environment variables, instance profile,
container credentials)
+ * </ol>
+ *
+ * <p>An optional endpoint override ({@code aws-glue-endpoint}) enables
connectivity to VPC
+ * endpoints and LocalStack for integration testing.
+ */
+public final class GlueClientProvider {
+
+ private GlueClientProvider() {}
+
+ /**
+ * Builds a {@link GlueClient} from the given catalog configuration map.
+ *
+ * @param config Catalog configuration properties.
+ * @return A configured and ready-to-use {@link GlueClient}.
+ * @throws IllegalArgumentException if {@code aws-region} is missing or
blank, if only one of the
+ * credential keys is provided, or if {@code aws-glue-endpoint} is not a
valid URI.
+ */
+ public static GlueClient buildClient(Map<String, String> config) {
+ String region = config.get(GlueConstants.AWS_REGION);
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(region),
+ "Property '%s' is required to create a Glue client",
+ GlueConstants.AWS_REGION);
+
+ GlueClientBuilder builder = GlueClient.builder().region(Region.of(region));
+
+ // Static credentials take priority over the default credential chain.
+ // Both keys must be provided together — a partial pair is always a
misconfiguration.
+ // Default credential chain order (when both keys are omitted):
+ // 1. Java system properties (aws.accessKeyId / aws.secretAccessKey)
+ // 2. Environment variables (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)
+ // 3. Web identity token (EKS / Kubernetes)
+ // 4. ~/.aws/credentials profile file
+ // 5. ECS container task role
+ // 6. EC2 instance profile (IMDSv2)
+ String accessKey = config.get(GlueConstants.AWS_ACCESS_KEY_ID);
+ String secretKey = config.get(GlueConstants.AWS_SECRET_ACCESS_KEY);
+ boolean hasAccessKey = StringUtils.isNotBlank(accessKey);
+ boolean hasSecretKey = StringUtils.isNotBlank(secretKey);
+ Preconditions.checkArgument(
+ hasAccessKey == hasSecretKey,
+ "Both '%s' and '%s' must be set together. "
+ + "Either provide both keys for static authentication, "
+ + "or omit both to use the default credential chain.",
+ GlueConstants.AWS_ACCESS_KEY_ID,
+ GlueConstants.AWS_SECRET_ACCESS_KEY);
+
+ if (hasAccessKey) {
+ builder.credentialsProvider(
+
StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey,
secretKey)));
+ } else {
+ builder.credentialsProvider(DefaultCredentialsProvider.create());
+ }
+
+ // Optional custom endpoint override for VPC endpoints or LocalStack
testing.
+ String endpoint = config.get(GlueConstants.AWS_GLUE_ENDPOINT);
+ if (StringUtils.isNotBlank(endpoint)) {
+ builder.endpointOverride(URI.create(endpoint));
+ }
+
+ return builder.build();
+ }
+}
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueConstants.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueConstants.java
new file mode 100644
index 0000000000..1aaa10599a
--- /dev/null
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueConstants.java
@@ -0,0 +1,82 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+/** Constant keys for the AWS Glue Data Catalog connector configuration and
table properties. */
+public final class GlueConstants {
+
+ // -------------------------------------------------------------------------
+ // Catalog-level connection properties
+ // -------------------------------------------------------------------------
+
+ /** AWS region for the Glue Data Catalog (required). */
+ public static final String AWS_REGION = "aws-region";
+
+ /**
+ * Glue catalog ID — the 12-digit AWS account ID (optional). When omitted,
defaults to the
+ * caller's AWS account ID.
+ */
+ public static final String AWS_GLUE_CATALOG_ID = "aws-glue-catalog-id";
+
+ /** AWS access key ID for static credential authentication (optional,
sensitive). */
+ public static final String AWS_ACCESS_KEY_ID = "aws-access-key-id";
+
+ /** AWS secret access key for static credential authentication (optional,
sensitive). */
+ public static final String AWS_SECRET_ACCESS_KEY = "aws-secret-access-key";
+
+ /**
+ * Custom Glue endpoint URL (optional). Used for VPC endpoints or LocalStack
testing. Example:
+ * {@code http://localhost:4566}
+ */
+ public static final String AWS_GLUE_ENDPOINT = "aws-glue-endpoint";
+
+ /**
+ * Default table format used when creating tables via Gravitino's {@code
createTable()} API
+ * (optional). Accepted values: {@code iceberg}, {@code hive}. Defaults to
{@code hive}.
+ */
+ public static final String DEFAULT_TABLE_FORMAT = "default-table-format";
+
+ /** Default value for {@link #DEFAULT_TABLE_FORMAT}: {@code "hive"}. */
+ public static final String DEFAULT_TABLE_FORMAT_VALUE = "hive";
+
+ /**
+ * Comma-separated list of table formats exposed by {@code listTables()} and
{@code loadTable()}
+ * (optional). Defaults to {@code all}.
+ */
+ public static final String TABLE_FORMAT_FILTER = "table-format-filter";
+
+ /** Default value for {@link #TABLE_FORMAT_FILTER}: expose all table
formats. */
+ public static final String DEFAULT_TABLE_FORMAT_FILTER = "all";
+
+ // -------------------------------------------------------------------------
+ // Glue Table.parameters() keys (passthrough properties)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Glue table format parameter key stored in {@code Table.parameters()}.
Common values: {@code
+ * ICEBERG}, {@code HIVE}, {@code DELTA}, {@code PARQUET} (uppercase, as
stored by Glue). Used
+ * internally to determine the table format when reading Glue tables.
+ */
+ public static final String TABLE_FORMAT = "table-format";
+
+ /** Iceberg table metadata location stored in Glue {@code
Table.parameters()}. */
+ public static final String METADATA_LOCATION = "metadata_location";
+
+ private GlueConstants() {}
+}
diff --git
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueTablePropertiesMetadata.java
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueTablePropertiesMetadata.java
index bec87e2e36..79236a1590 100644
---
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueTablePropertiesMetadata.java
+++
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueTablePropertiesMetadata.java
@@ -18,6 +18,10 @@
*/
package org.apache.gravitino.catalog.glue;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.METADATA_LOCATION;
+import static org.apache.gravitino.catalog.glue.GlueConstants.TABLE_FORMAT;
+import static
org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyEntry;
+
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.apache.gravitino.connector.BasePropertiesMetadata;
@@ -26,12 +30,35 @@ import org.apache.gravitino.connector.PropertyEntry;
/**
* Properties metadata for Glue tables.
*
- * <p>TODO PR-02: support passthrough of Glue Table.parameters() keys such as
{@code table_type} and
- * {@code metadata_location} for Iceberg, Delta, and other formats.
+ * <p>Defines well-known Glue {@code Table.parameters()} keys that Gravitino
exposes. All entries
+ * are optional and mutable, reflecting that Glue stores them as free-form
key-value pairs. Unknown
+ * parameters from {@code Table.parameters()} are passed through transparently
by the catalog
+ * operations layer and are not validated here.
+ *
+ * <p>Note: storage location ({@code StorageDescriptor.location}) varies by
table format and is
+ * handled per-format in the Table CRUD layer, not declared here.
*/
public class GlueTablePropertiesMetadata extends BasePropertiesMetadata {
- private static final Map<String, PropertyEntry<?>> PROPERTIES_METADATA =
ImmutableMap.of();
+ private static final Map<String, PropertyEntry<?>> PROPERTIES_METADATA =
+ ImmutableMap.<String, PropertyEntry<?>>builder()
+ .put(
+ TABLE_FORMAT,
+ stringOptionalPropertyEntry(
+ TABLE_FORMAT,
+ "Table format stored in Table.parameters(). Common values:
iceberg, hive.",
+ false /* immutable */,
+ null /* defaultValue */,
+ false /* hidden */))
+ .put(
+ METADATA_LOCATION,
+ stringOptionalPropertyEntry(
+ METADATA_LOCATION,
+ "Iceberg metadata file location stored in
Table.parameters().",
+ false /* immutable */,
+ null /* defaultValue */,
+ false /* hidden */))
+ .build();
@Override
protected Map<String, PropertyEntry<?>> specificPropertyEntries() {
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogCapability.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogCapability.java
new file mode 100644
index 0000000000..9581a89e2a
--- /dev/null
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogCapability.java
@@ -0,0 +1,75 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestGlueCatalogCapability {
+
+ private GlueCatalogCapability capability;
+
+ @BeforeEach
+ void setUp() {
+ capability = new GlueCatalogCapability();
+ }
+
+ @Test
+ void testColumnNotNullIsUnsupported() {
+ CapabilityResult result = capability.columnNotNull();
+ assertFalse(result.supported(), "Glue does not enforce NOT NULL
constraints");
+ assertNotNull(result.unsupportedMessage());
+ assertFalse(result.unsupportedMessage().isEmpty());
+ }
+
+ @Test
+ void testColumnDefaultValueIsUnsupported() {
+ CapabilityResult result = capability.columnDefaultValue();
+ assertFalse(result.supported(), "Glue does not support DEFAULT values on
columns");
+ assertNotNull(result.unsupportedMessage());
+ assertFalse(result.unsupportedMessage().isEmpty());
+ }
+
+ @Test
+ void testCaseSensitiveOnSchemaIsUnsupported() {
+ CapabilityResult result =
capability.caseSensitiveOnName(Capability.Scope.SCHEMA);
+ assertFalse(result.supported(), "Glue folds schema names to lowercase");
+ assertNotNull(result.unsupportedMessage());
+ }
+
+ @Test
+ void testCaseSensitiveOnTableIsUnsupported() {
+ CapabilityResult result =
capability.caseSensitiveOnName(Capability.Scope.TABLE);
+ assertFalse(result.supported(), "Glue folds table names to lowercase");
+ assertNotNull(result.unsupportedMessage());
+ }
+
+ @Test
+ void testCaseSensitiveOnColumnIsSupported() {
+ // Column name case folding is not documented in Glue Column API — treated
as supported.
+ CapabilityResult result =
capability.caseSensitiveOnName(Capability.Scope.COLUMN);
+ assertTrue(result.supported());
+ }
+}
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogPropertiesMetadata.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogPropertiesMetadata.java
new file mode 100644
index 0000000000..9a4a8c8201
--- /dev/null
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogPropertiesMetadata.java
@@ -0,0 +1,99 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_ACCESS_KEY_ID;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_CATALOG_ID;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_ENDPOINT;
+import static org.apache.gravitino.catalog.glue.GlueConstants.AWS_REGION;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_SECRET_ACCESS_KEY;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT_FILTER;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT_VALUE;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.TABLE_FORMAT_FILTER;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestGlueCatalogPropertiesMetadata {
+
+ private GlueCatalogPropertiesMetadata metadata;
+
+ @BeforeEach
+ void setUp() {
+ metadata = new GlueCatalogPropertiesMetadata();
+ }
+
+ @Test
+ void testAwsRegionIsRequired() {
+ assertTrue(metadata.isRequiredProperty(AWS_REGION));
+ }
+
+ @Test
+ void testAwsGlueCatalogIdIsOptional() {
+ assertFalse(metadata.isRequiredProperty(AWS_GLUE_CATALOG_ID));
+ }
+
+ @Test
+ void testAwsRegionIsImmutable() {
+ assertTrue(metadata.isImmutableProperty(AWS_REGION));
+ }
+
+ @Test
+ void testAwsGlueCatalogIdIsImmutable() {
+ assertTrue(metadata.isImmutableProperty(AWS_GLUE_CATALOG_ID));
+ }
+
+ @Test
+ void testCredentialsAreHidden() {
+ assertTrue(metadata.isHiddenProperty(AWS_ACCESS_KEY_ID));
+ assertTrue(metadata.isHiddenProperty(AWS_SECRET_ACCESS_KEY));
+ }
+
+ @Test
+ void testCredentialsAreOptional() {
+ assertFalse(metadata.isRequiredProperty(AWS_ACCESS_KEY_ID));
+ assertFalse(metadata.isRequiredProperty(AWS_SECRET_ACCESS_KEY));
+ }
+
+ @Test
+ void testEndpointIsOptionalAndNotHidden() {
+ assertFalse(metadata.isRequiredProperty(AWS_GLUE_ENDPOINT));
+ assertFalse(metadata.isHiddenProperty(AWS_GLUE_ENDPOINT));
+ }
+
+ @Test
+ void testDefaultTableFormatDefaultValue() {
+ assertEquals(
+ DEFAULT_TABLE_FORMAT_VALUE,
+ metadata.getDefaultValue(DEFAULT_TABLE_FORMAT),
+ "Default table format should be 'hive'");
+ }
+
+ @Test
+ void testTableFormatFilterDefaultValue() {
+ assertEquals(
+ DEFAULT_TABLE_FORMAT_FILTER,
+ metadata.getDefaultValue(TABLE_FORMAT_FILTER),
+ "Default table format filter should be 'all'");
+ }
+}
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
new file mode 100644
index 0000000000..f69a1c0739
--- /dev/null
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueClientProvider.java
@@ -0,0 +1,121 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_ACCESS_KEY_ID;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_ENDPOINT;
+import static org.apache.gravitino.catalog.glue.GlueConstants.AWS_REGION;
+import static
org.apache.gravitino.catalog.glue.GlueConstants.AWS_SECRET_ACCESS_KEY;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.services.glue.GlueClient;
+
+class TestGlueClientProvider {
+
+ @Test
+ void testBuildClientWithStaticCredentials() {
+ Map<String, String> config = new HashMap<>();
+ config.put(AWS_REGION, "us-east-1");
+ config.put(AWS_ACCESS_KEY_ID, "test-access-key");
+ config.put(AWS_SECRET_ACCESS_KEY, "test-secret-key");
+
+ try (GlueClient client = GlueClientProvider.buildClient(config)) {
+ assertNotNull(client);
+ }
+ }
+
+ @Test
+ void testBuildClientWithDefaultCredentialChain() {
+ // Without explicit credentials the default chain is used.
+ Map<String, String> config = new HashMap<>();
+ config.put(AWS_REGION, "eu-west-1");
+
+ try (GlueClient client = GlueClientProvider.buildClient(config)) {
+ assertNotNull(client);
+ }
+ }
+
+ @Test
+ void testBuildClientWithEndpointOverride() {
+ Map<String, String> config = new HashMap<>();
+ config.put(AWS_REGION, "us-east-1");
+ config.put(AWS_ACCESS_KEY_ID, "test");
+ config.put(AWS_SECRET_ACCESS_KEY, "test");
+ config.put(AWS_GLUE_ENDPOINT, "http://localhost:4566");
+
+ try (GlueClient client = GlueClientProvider.buildClient(config)) {
+ assertNotNull(client);
+ }
+ }
+
+ @Test
+ void testBuildClientMissingRegionThrows() {
+ Map<String, String> config = new HashMap<>();
+
+ assertThrows(IllegalArgumentException.class, () ->
GlueClientProvider.buildClient(config));
+ }
+
+ @Test
+ void testBuildClientBlankRegionThrows() {
+ Map<String, String> config = new HashMap<>();
+ config.put(AWS_REGION, " ");
+
+ assertThrows(IllegalArgumentException.class, () ->
GlueClientProvider.buildClient(config));
+ }
+
+ @Test
+ void testBuildClientOnlyAccessKeyThrows() {
+ // Providing only one of the credential pair is always a misconfiguration
— must fail fast.
+ Map<String, String> config = new HashMap<>();
+ config.put(AWS_REGION, "ap-southeast-1");
+ config.put(AWS_ACCESS_KEY_ID, "AKIAIOSFODNN7EXAMPLE");
+ // No AWS_SECRET_ACCESS_KEY.
+
+ IllegalArgumentException ex =
+ assertThrows(IllegalArgumentException.class, () ->
GlueClientProvider.buildClient(config));
+ assertTrue(ex.getMessage().contains(AWS_SECRET_ACCESS_KEY));
+ }
+
+ @Test
+ void testBuildClientOnlySecretKeyThrows() {
+ // Providing only the secret without the access key is also a
misconfiguration.
+ Map<String, String> config = new HashMap<>();
+ config.put(AWS_REGION, "ap-southeast-1");
+ config.put(AWS_SECRET_ACCESS_KEY, "test-secret-key");
+ // No AWS_ACCESS_KEY_ID.
+
+ IllegalArgumentException ex =
+ assertThrows(IllegalArgumentException.class, () ->
GlueClientProvider.buildClient(config));
+ assertTrue(ex.getMessage().contains(AWS_ACCESS_KEY_ID));
+ }
+
+ @Test
+ void testBuildClientInvalidEndpointThrows() {
+ Map<String, String> config = new HashMap<>();
+ config.put(AWS_REGION, "us-east-1");
+ config.put(AWS_GLUE_ENDPOINT, "not a valid uri ://");
+
+ assertThrows(IllegalArgumentException.class, () ->
GlueClientProvider.buildClient(config));
+ }
+}
diff --git
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTablePropertiesMetadata.java
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTablePropertiesMetadata.java
new file mode 100644
index 0000000000..89322b5519
--- /dev/null
+++
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTablePropertiesMetadata.java
@@ -0,0 +1,56 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import static
org.apache.gravitino.catalog.glue.GlueConstants.METADATA_LOCATION;
+import static org.apache.gravitino.catalog.glue.GlueConstants.TABLE_FORMAT;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestGlueTablePropertiesMetadata {
+
+ private GlueTablePropertiesMetadata metadata;
+
+ @BeforeEach
+ void setUp() {
+ metadata = new GlueTablePropertiesMetadata();
+ }
+
+ @Test
+ void testTableFormatIsOptional() {
+ assertFalse(metadata.isRequiredProperty(TABLE_FORMAT));
+ }
+
+ @Test
+ void testTableFormatIsNotHidden() {
+ assertFalse(metadata.isHiddenProperty(TABLE_FORMAT));
+ }
+
+ @Test
+ void testMetadataLocationIsOptional() {
+ assertFalse(metadata.isRequiredProperty(METADATA_LOCATION));
+ }
+
+ @Test
+ void testMetadataLocationIsNotHidden() {
+ assertFalse(metadata.isHiddenProperty(METADATA_LOCATION));
+ }
+}