diff --git a/csharp/src/DatabricksStatement.cs b/csharp/src/DatabricksStatement.cs
index bd7135fe..395dda83 100644
--- a/csharp/src/DatabricksStatement.cs
+++ b/csharp/src/DatabricksStatement.cs
@@ -671,6 +671,35 @@ private void HandleSparkCatalog()
CatalogName = DatabricksConnection.HandleSparkCatalog(CatalogName);
}
+ ///
+ /// Normalizes the catalog argument for the Thrift metadata path (GetSchemas/GetTables/GetColumns)
+ /// so that catalog behaves as an exact-name identifier rather than a SQL-LIKE pattern (issue #525),
+ /// matching the SEA path and PR #536.
+ ///
+ /// - A bare match-all wildcard ("%" or "*") — like a null argument — means "all catalogs", so it is
+ /// mapped to null. On Thrift the server already expands "%"/"*", but mapping to null keeps the
+ /// behavior identical to the SEA path (StatementExecutionStatement.EffectiveCatalog).
+ /// - Any other catalog name has its "_" and "%" wildcard characters escaped so the Thrift server
+ /// matches it literally. The server honors the "\_"/"\%" escape (verified against a live warehouse);
+ /// without escaping it treats "_" as a single-char wildcard, so e.g. catalog "comparator_tests"
+ /// would also match "comparator-tests".
+ ///
+ /// When is true the base class (HiveServer2Statement) already
+ /// escapes the catalog argument, so the name is returned unescaped here (after the bare-wildcard
+ /// normalization) to avoid double-escaping.
+ ///
+ internal static string? NormalizeCatalogForThrift(string? catalog, bool escapePatternWildcards)
+ {
+ if (catalog == null || catalog == "%" || catalog == "*")
+ {
+ return null;
+ }
+
+ return escapePatternWildcards
+ ? catalog
+ : catalog.Replace("_", "\\_").Replace("%", "\\%");
+ }
+
///
/// Helper method that returns the fully qualified table name enclosed by backtick.
/// The returned value can be used as table name in the SQL statement
@@ -764,6 +793,12 @@ protected override async Task GetSchemasAsync(CancellationToken can
HandleSparkCatalog();
activity?.SetTag("statement.catalog_name_after_spark_handling", CatalogName ?? "(none)");
+ // Issue #525: on Thrift, catalog is an exact-name identifier, not a LIKE pattern.
+ // Map a bare match-all wildcard to null ("all catalogs") and escape any other catalog
+ // name so the server matches it literally (parity with the SEA path / PR #536).
+ CatalogName = NormalizeCatalogForThrift(CatalogName, EscapePatternWildcards);
+ activity?.SetTag("statement.catalog_name_after_pattern_normalization", CatalogName ?? "(none)");
+
// If EnableMultipleCatalogSupport is false and catalog is not null or SPARK, return empty result without RPC call
if (!enableMultipleCatalogSupport && CatalogName != null)
{
@@ -810,6 +845,12 @@ protected override async Task GetTablesAsync(CancellationToken canc
HandleSparkCatalog();
activity?.SetTag("statement.catalog_name_after_spark_handling", CatalogName ?? "(none)");
+ // Issue #525: on Thrift, catalog is an exact-name identifier, not a LIKE pattern.
+ // Map a bare match-all wildcard to null ("all catalogs") and escape any other catalog
+ // name so the server matches it literally (parity with the SEA path / PR #536).
+ CatalogName = NormalizeCatalogForThrift(CatalogName, EscapePatternWildcards);
+ activity?.SetTag("statement.catalog_name_after_pattern_normalization", CatalogName ?? "(none)");
+
// If EnableMultipleCatalogSupport is false and catalog is not null or SPARK, return empty result without RPC call
if (!enableMultipleCatalogSupport && CatalogName != null)
{
@@ -856,6 +897,12 @@ protected override async Task GetColumnsAsync(CancellationToken can
HandleSparkCatalog();
activity?.SetTag("statement.catalog_name_after_spark_handling", CatalogName ?? "(none)");
+ // Issue #525: on Thrift, catalog is an exact-name identifier, not a LIKE pattern.
+ // Map a bare match-all wildcard to null ("all catalogs") and escape any other catalog
+ // name so the server matches it literally (parity with the SEA path / PR #536).
+ CatalogName = NormalizeCatalogForThrift(CatalogName, EscapePatternWildcards);
+ activity?.SetTag("statement.catalog_name_after_pattern_normalization", CatalogName ?? "(none)");
+
// If EnableMultipleCatalogSupport is false and catalog is not null, return empty result without RPC call
if (!enableMultipleCatalogSupport && CatalogName != null)
{
diff --git a/csharp/test/Unit/DatabricksStatementUnitTests.cs b/csharp/test/Unit/DatabricksStatementUnitTests.cs
index 574518bc..2870d64b 100644
--- a/csharp/test/Unit/DatabricksStatementUnitTests.cs
+++ b/csharp/test/Unit/DatabricksStatementUnitTests.cs
@@ -55,6 +55,37 @@ private DatabricksStatement CreateStatement()
return (Dictionary?)field?.GetValue(statement);
}
+ ///
+ /// Issue #525: on the Thrift metadata path the catalog argument must behave as an exact-name
+ /// identifier, not a SQL-LIKE pattern. NormalizeCatalogForThrift maps a bare match-all wildcard
+ /// ("%"/"*") — like null — to null ("all catalogs"), and escapes "_"/"%" in any other catalog
+ /// name so the server matches it literally. When EscapePatternWildcards is already enabled the
+ /// base class escapes catalog itself, so the literal name is returned unchanged to avoid
+ /// double-escaping.
+ ///
+ [Theory]
+ // bare match-all wildcards (and null) -> null ("all catalogs"), regardless of the escape flag
+ [InlineData(null, false, null)]
+ [InlineData("%", false, null)]
+ [InlineData("*", false, null)]
+ [InlineData("%", true, null)]
+ [InlineData("*", true, null)]
+ // plain name with no wildcard chars -> unchanged
+ [InlineData("main", false, "main")]
+ [InlineData("main", true, "main")]
+ // name containing "_" -> escaped so it matches literally (flag off)
+ [InlineData("comparator_tests", false, "comparator\\_tests")]
+ [InlineData("hive_metastore", false, "hive\\_metastore")]
+ // "%" inside a name -> escaped too (flag off)
+ [InlineData("a%b_c", false, "a\\%b\\_c")]
+ // flag on: base class escapes, so leave the literal name unchanged here (no double-escape)
+ [InlineData("comparator_tests", true, "comparator_tests")]
+ public void NormalizeCatalogForThrift_NormalizesAndEscapesCatalog(string? input, bool escapePatternWildcards, string? expected)
+ {
+ string? actual = DatabricksStatement.NormalizeCatalogForThrift(input, escapePatternWildcards);
+ Assert.Equal(expected, actual);
+ }
+
///
/// Tests that query_tags parameter is captured and added to confOverlay.
///