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. ///