Skip to content

Conversation

Dilli-Babu-Godari
Copy link
Contributor

@Dilli-Babu-Godari Dilli-Babu-Godari commented Sep 3, 2025

Description

Added catalog-level config to support case-sensitive-name-matching

This PR implements case sensitivity handling for Prometheus metric (table) names:

  1. Added case-sensitive name matching support controlled by the case-sensitive-name-matching configuration property
  2. Implemented normalizeIdentifier in PrometheusMetadata to respect case sensitivity settings
  3. Modified PrometheusClient to preserve case when case-sensitive matching is enabled

When case-sensitive-name-matching is set to true:

  1. Metric (table) names will preserve their original case
  2. Queries must use the exact case as defined in Prometheus
  3. Both HTTP_Requests and http_requests can exist as separate metrics

When case-sensitive-name-matching is set to false (default for backward compatibility):

  1. All names are normalized to lowercase
  2. Queries are case-insensitive

Motivation and Context

Prometheus has a fundamentally different data model compared to traditional relational databases:

Table = Metric Name: In Prometheus, each table corresponds to a single metric name. These metric names come directly from the Prometheus API and can have mixed case (e.g., http_requests_total, HTTP_Requests_Total, etc.).

Fixed Column Schema: As seen in PrometheusTable.java, every Prometheus table has exactly the same three columns:

new PrometheusTable(
    tableName,
    ImmutableList.of(
        new PrometheusColumn("labels", varcharMapType),
        new PrometheusColumn("timestamp", TIMESTAMP_WITH_TIME_ZONE),
        new PrometheusColumn("value", DoubleType.DOUBLE)));

These columns are always named "labels", "timestamp", and "value" - they're not user-defined and don't come from Prometheus.

Impact

Test Plan

Contributor checklist

  • Please make sure your submission complies with our contributing guide, in particular code style and commit standards.
  • PR description addresses the issue accurately and concisely. If the change is non-trivial, a GitHub Issue is referenced.
  • Documented new properties (with its default value), SQL syntax, functions, or other functionality.
  • If release notes are required, they follow the release notes guidelines.
  • Adequate tests were added if applicable.
  • CI passed.

Release Notes

Please follow release notes guidelines and fill in the release notes below.

== RELEASE NOTES ==

Prometheus Connector Changes 
* Add support for case-sensitive identifiers in Prometheus. Set the configuration property in the catalog file as follows to enable: `case-sensitive-name-matching=true.

@prestodb-ci prestodb-ci added the from:IBM PR from IBM label Sep 3, 2025
Copy link

linux-foundation-easycla bot commented Sep 3, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: Dilli-Babu-Godari / name: Dilli-Babu-Godari (b39b13b)

@Dilli-Babu-Godari Dilli-Babu-Godari force-pushed the prometheus_mixed_case_support branch 6 times, most recently from 357620a to e175d9a Compare September 5, 2025 17:31
@Dilli-Babu-Godari Dilli-Babu-Godari marked this pull request as ready for review September 6, 2025 06:40
@Dilli-Babu-Godari Dilli-Babu-Godari requested a review from a team as a code owner September 6, 2025 06:40
@prestodb-ci prestodb-ci requested review from a team, Joe-Abraham and namya28 and removed request for a team September 6, 2025 06:40
Copy link
Contributor

sourcery-ai bot commented Sep 6, 2025

Reviewer's Guide

Introduce an optional case-sensitive metric name matching mode in the Prometheus connector by adding a new configuration property and updating metadata and client lookups to normalize or preserve identifiers accordingly, with extended test coverage for both modes.

Sequence diagram for table lookup with case-sensitive name matching

sequenceDiagram
    participant User
    participant "PrometheusMetadata"
    participant "PrometheusClient"
    participant "PrometheusConnectorConfig"
    User->>PrometheusMetadata: getTableHandle(session, tableName)
    PrometheusMetadata->>PrometheusConnectorConfig: isCaseSensitiveNameMatching()
    PrometheusMetadata->>PrometheusMetadata: normalizeIdentifier(session, tableName)
    PrometheusMetadata->>PrometheusClient: getTable(schema, normalizedTableName)
    PrometheusClient->>PrometheusConnectorConfig: isCaseSensitiveNameMatching()
    PrometheusClient->>PrometheusClient: findActualTableName(tableNames, normalizedTableName)
    PrometheusClient-->>PrometheusMetadata: PrometheusTable (actualTableName)
    PrometheusMetadata-->>User: PrometheusTableHandle (actualTableName)
Loading

Class diagram for updated Prometheus connector configuration and metadata handling

classDiagram
    class PrometheusConnectorConfig {
        +URI getPrometheusURI()
        +boolean isCaseSensitiveNameMatching()
        +PrometheusConnectorConfig setCaseSensitiveNameMatching(boolean)
        -boolean caseSensitiveNameMatching
    }
    class PrometheusMetadata {
        +String normalizeIdentifier(ConnectorSession, String)
        -boolean caseSensitiveNameMatching
        +PrometheusMetadata(PrometheusClient, PrometheusConnectorConfig)
    }
    class PrometheusClient {
        +PrometheusTable getTable(String, String)
        -String findActualTableName(List<String>, String)
    }
    PrometheusMetadata --> PrometheusClient
    PrometheusMetadata --> PrometheusConnectorConfig
    PrometheusClient --> PrometheusConnectorConfig
Loading

File-Level Changes

Change Details Files
Introduce case-sensitive name matching config option
  • Add boolean caseSensitiveNameMatching with getter and @config setter in connector config
  • Set default matching mode to false for backward compatibility
  • Update TestPrometheusConnectorConfig to include mappings for the new property
presto-prometheus/src/main/java/com/facebook/presto/plugin/prometheus/PrometheusConnectorConfig.java
presto-prometheus/src/test/java/com/facebook/presto/plugin/prometheus/TestPrometheusConnectorConfig.java
Propagate matching setting into metadata and implement normalization
  • Inject PrometheusConnectorConfig into PrometheusMetadata and store caseSensitive flag
  • Override normalizeIdentifier to lowercase when case-sensitive matching is disabled
  • Modify getTableHandle to use normalized names and return actual table name from metadata
presto-prometheus/src/main/java/com/facebook/presto/plugin/prometheus/PrometheusMetadata.java
Enhance client lookup to preserve or normalize case based on config
  • Extract findActualTableName with branches for case-sensitive and insensitive matching
  • Update getTable to call findActualTableName and return actual Prometheus metric name
presto-prometheus/src/main/java/com/facebook/presto/plugin/prometheus/PrometheusClient.java
Expand and adjust tests for case sensitivity support
  • Update existing metadata integration tests to pass connector config into PrometheusMetadata
  • Add comprehensive mixed-case integration tests covering both matching modes
presto-prometheus/src/test/java/com/facebook/presto/plugin/prometheus/TestPrometheusRetrieveUpValueIntegrationTests.java
presto-prometheus/src/test/java/com/facebook/presto/plugin/prometheus/TestPrometheusIntegrationMixedCase.java

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In case‐insensitive mode, if two metrics differ only by case (e.g., "HTTP_Requests" vs "http_requests"), findActualTableName will arbitrarily pick the first match—consider validating or erroring on ambiguous duplicates to avoid silent mis‐resolution.
  • The TestPrometheusIntegrationMixedCase class is very large—consider extracting shared setup into helper methods or splitting it into smaller, focused test classes for better maintainability.
  • There’s duplicate case‐normalization logic in PrometheusMetadata, PrometheusClient, and the mock client—consider centralizing this into a shared utility to prevent drift and simplify future updates.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In case‐insensitive mode, if two metrics differ only by case (e.g., "HTTP_Requests" vs "http_requests"), findActualTableName will arbitrarily pick the first match—consider validating or erroring on ambiguous duplicates to avoid silent mis‐resolution.
- The TestPrometheusIntegrationMixedCase class is very large—consider extracting shared setup into helper methods or splitting it into smaller, focused test classes for better maintainability.
- There’s duplicate case‐normalization logic in PrometheusMetadata, PrometheusClient, and the mock client—consider centralizing this into a shared utility to prevent drift and simplify future updates.

## Individual Comments

### Comment 1
<location> `presto-prometheus/src/main/java/com/facebook/presto/plugin/prometheus/PrometheusConnectorConfig.java:43` </location>
<code_context>
     private String trustStorePath;
     private String truststorePassword;
     private boolean verifyHostName;
+    private boolean caseSensitiveNameMatching;

     @NotNull
</code_context>

<issue_to_address>
Default value for caseSensitiveNameMatching is not set.

Explicitly initialize caseSensitiveNameMatching to false or add documentation to clarify its default value for maintainability.
</issue_to_address>

### Comment 2
<location> `presto-prometheus/src/test/java/com/facebook/presto/plugin/prometheus/TestPrometheusIntegrationMixedCase.java:196` </location>
<code_context>
+        mockClient.setFetchUriOverride("cpu_usage", mockClient.createEmptyResponse());
+        assertQueryFails(caseSensitiveSession, "SELECT * FROM \"cpu_usage\" WHERE value > 0", ".*Table prometheus_case_sensitive.default.cpu_usage does not exist");
+
+        // Test that uppercase "UP" fails in case-sensitive mode
+        // Make sure we don't have an uppercase "UP" metric in the table names
+        mockClient.removeMetric("UP");
+        mockClient.setTableOverride("UP", null);
+        mockClient.setFetchUriOverride("UP", mockClient.createEmptyResponse());
+        assertQueryFails(caseSensitiveSession, "SELECT * FROM \"UP\" WHERE value > 0", ".*Table prometheus_case_sensitive.default.UP does not exist");
+    }
+    @Test
</code_context>

<issue_to_address>
Consider adding a test for ambiguous metric names (e.g., both 'up' and 'UP' present) in case-insensitive mode.

Add a test with both 'up' and 'UP' in the mock client under case-insensitive mode to verify consistent and predictable metric resolution.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
+        // Test that uppercase "UP" fails in case-sensitive mode
+        // Make sure we don't have an uppercase "UP" metric in the table names
+        mockClient.removeMetric("UP");
+        mockClient.setTableOverride("UP", null);
+        mockClient.setFetchUriOverride("UP", mockClient.createEmptyResponse());
+        assertQueryFails(caseSensitiveSession, "SELECT * FROM \"UP\" WHERE value > 0", ".*Table prometheus_case_sensitive.default.UP does not exist");
+    }
+    @Test
=======
+        // Test that uppercase "UP" fails in case-sensitive mode
+        // Make sure we don't have an uppercase "UP" metric in the table names
+        mockClient.removeMetric("UP");
+        mockClient.setTableOverride("UP", null);
+        mockClient.setFetchUriOverride("UP", mockClient.createEmptyResponse());
+        assertQueryFails(caseSensitiveSession, "SELECT * FROM \"UP\" WHERE value > 0", ".*Table prometheus_case_sensitive.default.UP does not exist");
+
+        // Test ambiguous metric names in case-insensitive mode
+        mockClient.setCaseSensitiveNameMatching(false);
+        // Add both "up" and "UP" metrics
+        mockClient.addMetric("up", mockClient.createMetricResponse("up", 1.0));
+        mockClient.addMetric("UP", mockClient.createMetricResponse("UP", 2.0));
+        // Query for "up" in case-insensitive mode
+        MaterializedResult upResult = getQueryRunner().execute(getSession(), "SELECT value FROM \"up\"");
+        // Assert that the result is either 1.0 or 2.0, depending on resolution strategy
+        // (Assume the implementation picks the first or a deterministic one)
+        Set<Object> upValues = upResult.getOnlyColumnAsSet();
+        assertTrue(upValues.contains(1.0) || upValues.contains(2.0), "Expected value from either 'up' or 'UP' metric");
+        // Clean up
+        mockClient.removeMetric("up");
+        mockClient.removeMetric("UP");
+        mockClient.setCaseSensitiveNameMatching(true);
+    }
+    @Test
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `presto-prometheus/src/test/java/com/facebook/presto/plugin/prometheus/TestPrometheusIntegrationMixedCase.java:249` </location>
<code_context>
+        // Test with http_requests_total - all case variations should return the same result
</code_context>

<issue_to_address>
Missing test for querying a non-existent metric in both case-sensitive and case-insensitive modes.

Add a test that queries a non-existent metric in both modes and asserts the correct error is thrown.

Suggested implementation:

```java
        // Test with api_calls - both lowercase and uppercase should work
        MaterializedResult lowerCaseApi = getQueryRunner().execute(getSession(), "SELECT value FROM \"api_calls\"");

        // Test querying a non-existent metric in case-insensitive mode
        try {
            getQueryRunner().execute(getSession(), "SELECT value FROM \"non_existent_metric\"");
            fail("Expected exception for non-existent metric in case-insensitive mode");
        } catch (RuntimeException e) {
            // Assert that the error message contains information about the missing metric
            assertTrue(e.getMessage().toLowerCase().contains("non_existent_metric"));
        }

        try {
            getQueryRunner().execute(getSession(), "SELECT value FROM \"NON_EXISTENT_METRIC\"");
            fail("Expected exception for non-existent metric in case-insensitive mode (uppercase)");
        } catch (RuntimeException e) {
            assertTrue(e.getMessage().toLowerCase().contains("non_existent_metric"));
        }

        try {
            getQueryRunner().execute(getSession(), "SELECT value FROM \"Non_Existent_Metric\"");
            fail("Expected exception for non-existent metric in case-insensitive mode (mixed case)");
        } catch (RuntimeException e) {
            assertTrue(e.getMessage().toLowerCase().contains("non_existent_metric"));
        }

```

If your test suite supports case-sensitive mode, you should also add similar tests for that mode. 
You may need to adjust the exception type if your code throws a more specific exception for missing metrics.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@@ -40,6 +40,7 @@ public class PrometheusConnectorConfig
private String trustStorePath;
private String truststorePassword;
private boolean verifyHostName;
private boolean caseSensitiveNameMatching;
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: Default value for caseSensitiveNameMatching is not set.

Explicitly initialize caseSensitiveNameMatching to false or add documentation to clarify its default value for maintainability.

Comment on lines +196 to +203
// Test that uppercase "UP" fails in case-sensitive mode
// Make sure we don't have an uppercase "UP" metric in the table names
mockClient.removeMetric("UP");
mockClient.setTableOverride("UP", null);
mockClient.setFetchUriOverride("UP", mockClient.createEmptyResponse());
assertQueryFails(caseSensitiveSession, "SELECT * FROM \"UP\" WHERE value > 0", ".*Table prometheus_case_sensitive.default.UP does not exist");
}
@Test
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding a test for ambiguous metric names (e.g., both 'up' and 'UP' present) in case-insensitive mode.

Add a test with both 'up' and 'UP' in the mock client under case-insensitive mode to verify consistent and predictable metric resolution.

Suggested change
// Test that uppercase "UP" fails in case-sensitive mode
// Make sure we don't have an uppercase "UP" metric in the table names
mockClient.removeMetric("UP");
mockClient.setTableOverride("UP", null);
mockClient.setFetchUriOverride("UP", mockClient.createEmptyResponse());
assertQueryFails(caseSensitiveSession, "SELECT * FROM \"UP\" WHERE value > 0", ".*Table prometheus_case_sensitive.default.UP does not exist");
}
@Test
+ // Test that uppercase "UP" fails in case-sensitive mode
+ // Make sure we don't have an uppercase "UP" metric in the table names
+ mockClient.removeMetric("UP");
+ mockClient.setTableOverride("UP", null);
+ mockClient.setFetchUriOverride("UP", mockClient.createEmptyResponse());
+ assertQueryFails(caseSensitiveSession, "SELECT * FROM \"UP\" WHERE value > 0", ".*Table prometheus_case_sensitive.default.UP does not exist");
+
+ // Test ambiguous metric names in case-insensitive mode
+ mockClient.setCaseSensitiveNameMatching(false);
+ // Add both "up" and "UP" metrics
+ mockClient.addMetric("up", mockClient.createMetricResponse("up", 1.0));
+ mockClient.addMetric("UP", mockClient.createMetricResponse("UP", 2.0));
+ // Query for "up" in case-insensitive mode
+ MaterializedResult upResult = getQueryRunner().execute(getSession(), "SELECT value FROM \"up\"");
+ // Assert that the result is either 1.0 or 2.0, depending on resolution strategy
+ // (Assume the implementation picks the first or a deterministic one)
+ Set<Object> upValues = upResult.getOnlyColumnAsSet();
+ assertTrue(upValues.contains(1.0) || upValues.contains(2.0), "Expected value from either 'up' or 'UP' metric");
+ // Clean up
+ mockClient.removeMetric("up");
+ mockClient.removeMetric("UP");
+ mockClient.setCaseSensitiveNameMatching(true);
+ }
+ @Test

Comment on lines +249 to +256
// Test with http_requests_total - all case variations should return the same result
MaterializedResult lowerCaseHttp = getQueryRunner().execute(getSession(), "SELECT value FROM \"http_requests_total\"");
MaterializedResult upperCaseHttp = getQueryRunner().execute(getSession(), "SELECT value FROM \"HTTP_REQUESTS_TOTAL\"");
MaterializedResult mixedCaseHttp = getQueryRunner().execute(getSession(), "SELECT value FROM \"Http_Requests_Total\"");
// All queries should return the same result
assertEquals(lowerCaseHttp.getOnlyColumnAsSet().iterator().next(), 10.0);
assertEquals(lowerCaseHttp.getOnlyColumnAsSet().iterator().next(), upperCaseHttp.getOnlyColumnAsSet().iterator().next());
assertEquals(lowerCaseHttp.getOnlyColumnAsSet().iterator().next(), mixedCaseHttp.getOnlyColumnAsSet().iterator().next());
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Missing test for querying a non-existent metric in both case-sensitive and case-insensitive modes.

Add a test that queries a non-existent metric in both modes and asserts the correct error is thrown.

Suggested implementation:

        // Test with api_calls - both lowercase and uppercase should work
        MaterializedResult lowerCaseApi = getQueryRunner().execute(getSession(), "SELECT value FROM \"api_calls\"");

        // Test querying a non-existent metric in case-insensitive mode
        try {
            getQueryRunner().execute(getSession(), "SELECT value FROM \"non_existent_metric\"");
            fail("Expected exception for non-existent metric in case-insensitive mode");
        } catch (RuntimeException e) {
            // Assert that the error message contains information about the missing metric
            assertTrue(e.getMessage().toLowerCase().contains("non_existent_metric"));
        }

        try {
            getQueryRunner().execute(getSession(), "SELECT value FROM \"NON_EXISTENT_METRIC\"");
            fail("Expected exception for non-existent metric in case-insensitive mode (uppercase)");
        } catch (RuntimeException e) {
            assertTrue(e.getMessage().toLowerCase().contains("non_existent_metric"));
        }

        try {
            getQueryRunner().execute(getSession(), "SELECT value FROM \"Non_Existent_Metric\"");
            fail("Expected exception for non-existent metric in case-insensitive mode (mixed case)");
        } catch (RuntimeException e) {
            assertTrue(e.getMessage().toLowerCase().contains("non_existent_metric"));
        }

If your test suite supports case-sensitive mode, you should also add similar tests for that mode.
You may need to adjust the exception type if your code throws a more specific exception for missing metrics.

Copy link
Contributor

@steveburnett steveburnett left a comment

Choose a reason for hiding this comment

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

LGTM! (docs)

Pulled branch, made local doc build to review. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
from:IBM PR from IBM
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants