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));
+  }
+}


Reply via email to