fix(csharp): align SEA metadata error behavior with Thrift - #604
Conversation
… + escape fix) The Thrift-vs-SEA comparator reported 1045 deterministic metadata "outcome" divergences (one protocol throws while the other returns). Policy: SEA must match Thrift, not JDBC. Two changes: 1. Revert #388's object-not-found swallowing (885 diffs). #388 added IsObjectNotFoundException() + catch blocks that made SEA return EMPTY on NO_SUCH_CATALOG / SCHEMA_NOT_FOUND / TABLE_OR_VIEW_NOT_FOUND, to match JDBC. Thrift THROWS on these, so under the match-Thrift policy SEA must throw too. Removed the helper and all ~11 catch sites so the exception propagates; the co-located IsDescTableExtendedUnsupportedException catch is preserved. The two test files #388 added were later also edited by #456/#525/#566 — surgically removed only the object-not-found swallow tests, kept the rest. 2. Fix double-escaping in EscapePatternWildcardsInName (160 diffs). With escape_pattern_wildcards=true, a caller-supplied ALREADY-escaped pattern (e.g. "test\_result\_set\_types") was re-escaped to "test\\_..." which ConvertPattern then turned into invalid SHOW-command glob syntax, and the SEA server rejected it with a DatabricksException — while Thrift returned rows. The escape now passes already-escaped \_ \% \\ through unchanged and only escapes bare _ / %. Raw inputs (foo_bar -> foo\_bar) are unaffected. Added 4 unit tests (raw + pre-escaped underscore/percent, and the exact comparator fixture). All 929 unit tests pass. Expected comparator effect: 1280 -> ~250 diffs (the 885 + 160 collapse to matches; ORDINAL_POSITION / nullable / PK-presence divergences remain, tracked separately). Co-authored-by: Isaac
There was a problem hiding this comment.
Verdict: 1 Medium
Mostly a clean, well-tested revert + escape fix. EscapePatternWildcardsInName pass-through logic is correct (including \\\\, trailing-backslash, and pre-escaped \\_/\\% cases), and the new tests are reasonable. One Medium concern: removing the object-not-found catches may reintroduce the Issue #593 divergence for catalog="%" + escape_pattern_wildcards=true, where Thrift returns 0 rows but SEA will now throw SCHEMA_NOT_FOUND (see inline). Nit: the reworked comment above HttpClientCapturingStatements in the test file is garbled by the merge — it duplicates "body of every ExecuteStatement call into" and uses a <paramref> doc tag inside a plain // comment; worth tidying.
There was a problem hiding this comment.
Verdict: 1 High
The double-escaping fix in EscapePatternWildcardsInName is correct and well-tested (verified against ConvertPattern). One HIGH concern: the wholesale removal of the IsObjectNotFoundException catches appears to re-open issue #593 — the knowledge log records that for catalog="%" with escape_pattern_wildcards=true, Thrift returns 0 rows and SEA parity depended on those now-removed catches mapping SCHEMA_NOT_FOUND to empty; SEA will now throw. Worth confirming against the comparator before merge.
Clarify for reviewers: the "leave already-escaped \_ unchanged" behavior matches the JDBC reference (WildcardUtil.escapeCatalogName), and ConvertPattern itself is byte-identical to JDBC's jdbcPatternToHive — so the fix belongs in the escape step (which JDBC lacks and which double-processed the input), not in ConvertPattern. Co-authored-by: Isaac
There was a problem hiding this comment.
Verdict: 1 Medium
Mostly a clean, well-tested revert + escape-idempotency fix; the EscapePatternWildcardsInName rewrite is correct (already-escaped \\_/\\%/\\\\ pass through, bare _/% escaped) and no dangling references to the removed helper remain. One medium concern: removing the object-not-found catches reintroduces the #593-documented divergence for the escape_pattern_wildcards=true + catalog="%" case, where the learning log says Thrift returns empty rows but SEA will now throw.
…Exception; preserve #593) Reworks the earlier blanket #388 revert, which regressed issue #593. A Thrift-vs-SEA comparator showed metadata errors must match Thrift PER-CASE: - Genuine object-not-found: Thrift throws HiveServer2Exception/InternalError(+SqlState). SEA now throws the SAME at the FAILED throw site (StatementExecutionClient), with SqlState + NativeError populated from the server error (parity with Thrift's ThrowErrorResponse). Previously threw DatabricksException/UnknownError/null. - catalog="%"/"*" + escape_pattern_wildcards=true (#593): Thrift escapes %→\% → 0 rows (no throw). SEA now short-circuits GetSchemas/GetTables/GetColumns to an empty result BEFORE emitting the doomed `SHOW ... IN `%`` that 404s — so no SCHEMA_NOT_FOUND is raised. This replaces the removed #388 catch that #593 had relied on. escape=false still routes %→null→"ALL CATALOGS" (#525 untouched). Also: relocate IsDescTableExtendedUnsupportedException to a static helper over AdbcException so the DESC-TABLE-EXTENDED fallback catch works now that the metadata exception type is HiveServer2Exception; keep the idempotent EscapePatternWildcardsInName fix; un-skip PECO-3014 (StringValueTests) now that SEA throws HiveServer2Exception. 934 unit tests pass (5 new #593 short-circuit tests). Comparator re-validation pending. Co-authored-by: Isaac
There was a problem hiding this comment.
Verdict: 1 Low
Solid, well-tested change: reverting #388's object-not-found swallowing and fixing the double-escape are both correct for the match-Thrift policy, and the IsDescTableExtendedUnsupported catch was correctly widened to AdbcException so the new HiveServer2Exception FAILED type is still caught. One Low: the explanatory comment on the FAILED throw in StatementExecutionClient contradicts the actual (intended) control flow that the un-skipped StringValueTests relies on. Note (not blocking): the %+escape short-circuit was added only to GetSchemas/GetTables/GetColumns; GetPrimaryKeys/FetchCrossReference still go through the literal-% → SCHEMA_NOT_FOUND path and now propagate an exception post-revert — likely fine given the PR's acknowledged separately-tracked residuals, but worth confirming against Thrift for wildcard-catalog PK/FK inputs.
…patterns Two complementary layers were under-escaping backslashes so that a schema/ table/column whose name literally contains '\' could never be matched via the SEA metadata path (SHOW ... LIKE), and escape_pattern_wildcards=true silently dropped literal backslashes: 1. EscapePatternWildcardsInName (escape=true path): now escapes '\' -> '\\' in addition to '_' and '%'. Under escape=true the input is a LITERAL name, so a literal backslash must be escaped for the LIKE matcher. (Also removes the unsound "already-escaped" idempotency guess: under escape=true there are no escape sequences in the input to preserve.) 2. New LikePattern() helper (all SHOW ... LIKE builders): runs ConvertPattern (unchanged, still byte-identical to JDBC WildcardUtil.jdbcPatternToHive) then doubles backslashes for the SQL string literal. The glob is embedded in a single-quoted SQL string, so the server's string-literal parser consumes one backslash layer before the LIKE regex matcher sees it; without this a literal backslash collapses and is consumed as a regex escape. Verified live (thrift vs rest) against schemas named a_b / a\b / a\_b / a\\b: all escape=on literal-name lookups now return exactly the named schema on SEA. The JDBC reference driver has the same under-escaping (it interpolates jdbcPatternToHive output straight into LIKE '%s'); filed as a databricks-jdbc issue. Thrift's separate server-side exact-match doubling bug is tracked independently and is not addressed here. Co-authored-by: Isaac
There was a problem hiding this comment.
Verdict: 1 High · 1 Low
The revert of #388 and the exception-type unification are coherent and well-tested. However the core escape fix is internally contradictory: EscapePatternWildcardsInName's code body implements full unconditional escaping (with an explicit "no idempotency" comment), while its own docstring and the four new pre-escaped unit tests require already-escaped sequences to pass through unchanged — under the shipped code, pre-escaped foo\\_bar emits LIKE 'foo\\\\\\\\_bar', not the asserted LIKE 'foo_bar' (F1, high). One inaccurate comment about the FAILED-throw path (F2, low).
Addresses: - #3687197436 at csharp/src/StatementExecution/StatementExecutionStatement.cs:1035 - #3688145746 at csharp/src/StatementExecution/StatementExecutionClient.cs:247 - #3692682966 at csharp/src/StatementExecution/StatementExecutionStatement.cs:1091 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium
Looks solid overall — the #388 revert, the EscapePatternWildcardsInName idempotency fix, the LikePattern backslash-doubling, and the %+escape short-circuits are all intentional, internally consistent, and covered by new unit tests; no dangling references to the removed IsObjectNotFoundException helper. One medium concern: the HiveServer2Exception parity is only partial (HTTP-error failures still throw DatabricksException), which also gates the newly un-skipped VARCHAR E2E assertion on unverified server behavior.
…h Thrift
GetPrimaryKeys and GetCrossReference are JDBC exact-match operations: the table
(resp. foreign/parent table) is required. On the Thrift path, TGetPrimaryKeysReq
/ TGetCrossReferenceReq are rejected server-side with a HiveServer2Exception
(AdbcStatusCode.InternalError, SqlState 42000) when the table is null. SEA
previously returned an EMPTY result in that case, so the two protocols disagreed
on outcome (one throws, one returns) — a large bucket of comparator diffs.
SEA now throws the SAME exception (type + Status + SqlState — the comparator's
exception identity; the message is not compared) when:
- GetPrimaryKeys: table is null/empty -> "tableName may not be null"
- GetCrossReference: BOTH foreign table and parent table are null/empty
-> "foreignTable and parentTableName are both null"
Scope: only the null-TABLE case, which is a stable, well-defined Thrift
validation (verified live on thrift + rest). The catalog-only and schema-null
cases are left as-is — Thrift's errors there are environment-specific server
quirks (assertion failed / SCHEMA_NOT_FOUND naming the live catalog) that are
not cleanly reproducible. The PKFK-disabled early-return
(ShouldReturnEmptyPKFKResult) is preserved unchanged.
Adds unit tests asserting both throws (type + status + sqlstate). Full unit
suite (934) green.
Co-authored-by: Isaac
…rdsInName The engineer-bot's review-thread fix (b634bf0) re-introduced the "already- escaped" idempotency heuristic in EscapePatternWildcardsInName, which this change had deliberately removed. Restoring the correct behavior with the empirical justification the review thread lacked. Under escape_pattern_wildcards=true the input is a LITERAL object name, so there is no such thing as an "already-escaped" sequence to preserve: a '\' in the input is a literal backslash and must itself be escaped ('\' -> '\\'), along with '_' and '%'. The idempotency guess ("if I see \_ , assume the caller pre-escaped it and pass it through") is unsound — it cannot distinguish a caller-intended literal backslash from a pre-escape, and it silently drops literal backslashes. Verified live (thrift vs rest) against schemas named a\b / a\_b / a\\b: with the idempotency heuristic, escape=true lookups matched the WRONG object (or nothing); with full escaping (this change) every escape=true literal-name lookup returns exactly the named schema. Works together with the LikePattern SQL-string-literal escaping (Option B) added in the same PR. See databricks-jdbc#1598 for the parallel JDBC issue. Co-authored-by: Isaac
There was a problem hiding this comment.
Verdict: 1 Medium
Solid, well-tested parity change (revert of #388's object-not-found swallowing + fixing the escape path + switching the FAILED throw to HiveServer2Exception). The escape logic is internally consistent and the tests pin it correctly; I traced foo\\_bar → LIKE 'foo\\\\\\\\_bar' and it matches. One medium concern: the XML <summary> on EscapePatternWildcardsInName still documents the abandoned "pass already-escaped sequences through unchanged" approach and contradicts both the code and its own inline comment.
Addresses: - #3692682966 at csharp/src/StatementExecution/StatementExecutionStatement.cs:1091 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium
Looks solid overall — the #388 revert, the escape-doubling fix, and the %-catalog short-circuits are internally consistent and well-covered by the pinned unit tests; exception API surfaces and DTOs all check out. One medium concern: the FAILED-state exception type is now split (sync path throws HiveServer2Exception with SqlState, but the async polling path still throws bare AdbcException), so Thrift exception-identity parity for metadata only holds when the response resolves synchronously.
Nit (no anchor needed): the doc comment on HttpClientCapturingStatements in csharp/test/Unit/StatementExecution/StatementExecutionMetadataObjectNotFoundTests.cs has a garbled merge — the fragment "body of every ExecuteStatement call into" is duplicated across two lines. Cosmetic only.
Addresses: - #3693651576 at csharp/src/StatementExecution/StatementExecutionClient.cs:266 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium
Solid, well-documented change: reverts #388's object-not-found swallowing and fixes the escape logic, with the exception-type change (DatabricksException → HiveServer2Exception) correctly guarded by InternalsVisibleTo and not breaking any remaining catch sites (all downstream catches are on the AdbcException base). One medium concern: the HTTP-error failure path still throws DatabricksException with a null SqlState, which under the PR's match-Thrift exception-identity policy could leave a residual divergence if object-not-found ever surfaces as an HTTP status error rather than a FAILED state. Minor: the deleted tests removed the empty-result schema-shape assertions (JDBC field names / KEQ_SEQ) — that guard is now only implicitly covered.
|
🚀 Integration tests triggered! View workflow run |
…) for metadata errors Reverts the cross-protocol exception-TYPE unification from earlier in this PR. SEA now throws its own DatabricksException — the natural type for the REST/SEA path — rather than constructing a HiveServer2Exception (a Thrift/HiveServer2 type) via InternalsVisibleTo, which was semantically odd for a path that never touches HiveServer2 (the concern tracked by PECO-3014). Parity with the Thrift path is preserved on the comparator side instead: the comparator's CompareException is being relaxed to treat any AdbcException subclass as equivalent and compare Status + SqlState (the ADBC contract that consumers actually branch on), rather than requiring an identical concrete class. Both throw sites keep the same Status (InternalError) + SqlState, so behavioral parity is unchanged; only the concrete type differs, which is an implementation detail. Affected: - StatementExecutionClient.cs: FAILED-state throw -> DatabricksException - StatementExecutionStatement.cs: NewInvalidArgumentException (PK/XREF null guard) -> DatabricksException - unit tests assert DatabricksException + Status + SqlState (unchanged load-bearing identity) The IsDescTableExtendedUnsupported(AdbcException) static helper stays (accepts the base type; works for either subclass). Co-authored-by: Isaac Also aligns NewFailedStateException (async FAILED polling path, added by the review-thread follow-up) to DatabricksException for the same reason.
|
🔒 Integration test approval reset New commits were pushed to this PR. The A maintainer must re-review the changes and re-add the label to trigger tests again. Why is this necessary?
Latest commit: 2751020 |
There was a problem hiding this comment.
Verdict: 1 High · 1 Low
Mostly sound revert + escape fix, but one high-severity inconsistency: the StringValueTests skip was removed with a comment claiming SEA now throws HiveServer2Exception, yet StatementExecutionClient still throws DatabricksException (a sibling type, not a HiveServer2Exception), so the SEA/rest E2E assertion Assert.ThrowsAsync<HiveServer2Exception> will fail. A stray unused using was added in the same file. The metadata revert and escape rewrite otherwise look internally consistent with the new unit tests.
There was a problem hiding this comment.
Verdict: 1 Medium
Solid, well-tested change; the escaping (two-layer backslash handling), catch-clause ordering (DESC-unsupported vs object-not-found are disjoint), and exception-type/Status/SqlState wiring all check out. One medium concern: GetCrossReferenceAsync validates args before the PKFK-disabled short-circuit while GetPrimaryKeysAsync does the reverse, so with EnablePKFK=false the two exact-match ops diverge (throw vs empty).
… stacked PR) The exact-match argument validation for GetPrimaryKeys / GetCrossReference (throw on null table, and on null schema when catalog is specified — mirroring JDBC's resolveKeyBasedParams) is a distinct concern from this PR's object-not-found / backslash / exception-type work. Moved to a stacked follow-up PR with its own Thrift-vs-JDBC-vs-ADBC behavior matrix. This PR now leaves PK/XREF returning an empty result when required args are missing (consistent with the object-not-found path); the stacked PR adds the throws. Removed: NewInvalidArgumentException helper, the validateArgs parameter, the GetPrimaryKeysAsync / GetCrossReferenceAsync validation blocks, and the three throw-asserting unit tests. Kept GetCrossReference_NullForeignTable_ReturnsEmpty (that empty behavior is independent of the validation). 952 unit tests pass. Co-authored-by: Isaac
|
🚀 Integration tests triggered! View workflow run |
|
🚀 Integration tests triggered! View workflow run |
…rossReference
Adds client-side argument validation for the SEA metadata exact-match operations,
mirroring the JDBC reference driver's resolveKeyBasedParams / listCrossReferences:
GetPrimaryKeys:
- table null/empty -> throw "tableName may not be null" (42000)
- catalog set + schema null -> throw "schema may not be null when catalog is specified" (42000)
GetCrossReference:
- foreign table null -> empty result (JDBC "unspecified")
- foreign catalog set + foreign schema null -> throw (42000)
Validating client-side gives a clean, deterministic error and avoids the Thrift
server's internal "GET_FUNCTIONS assertion failed" (SQLSTATE 08000) bug on null
schema. Validation is on the user-facing command path only — GetColumnsExtended's
internal three-call fallback reuses the PK/FK fetch with a null schema on purpose
and bypasses the checks (GetPrimaryKeysAsync validateArgs:false; the shared
FetchCrossReferenceAsync is unvalidated).
Stacked on #604 (object-not-found → empty, backslash escaping, exception-type pivot);
this modifies the same SEA metadata methods that PR introduces, so it targets that
branch rather than main.
955 unit tests pass.
Co-authored-by: Isaac
Summary
Aligns SEA metadata behavior with the Thrift path so the Thrift-vs-SEA behavioral comparator converges. Policy: SEA must match Thrift on the ADBC-observable contract — result rows, and on error the
Status+SqlState(not the concrete exception subclass).1. Object-not-found → empty result (keep #388, verified)
SEA metadata methods catch
NO_SUCH_CATALOG/SCHEMA_NOT_FOUND/TABLE_OR_VIEW_NOT_FOUND/INVALID_PARAMETER_VALUEand return an empty result — matching both the Thrift path (measured: 0 rows on object-not-found) and the JDBC reference driver (isObjectNotFoundException).DatabricksException.IsObjectNotFoundException+ a catch on both the flat metadata methods (GetSchemas/GetTables/GetColumns/GetColumnsExtended/GetPrimaryKeys/GetCrossReference) and theGetObjectsnested-shape path (IGetObjectsDataProvider).This also subsumes issue #593 (
%-catalog + escape): all four explicit #593 pre-SHOW short-circuits are removed as redundant. On a real server the%-catalog case produces an object-not-found error the catch swallows to empty — verified live for both the SHOW paths (SHOW … IN \%`→ SCHEMA_NOT_FOUND) and the DESC-TABLE-EXTENDED path (DESC TABLE EXTENDED `%`.…→ TABLE_OR_VIEW_NOT_FOUND). (An earlier iteration kept the GetColumnsExtended short-circuit believing its empty-resultFormatExceptionescaped the catch — but thatFormatException` was a test-mock artifact; the real server throws TABLE_OR_VIEW_NOT_FOUND, which the catch handles. The mock was corrected to the realistic failure.)2. Literal-backslash escaping in the SEA LIKE path (two layers)
Under
escape_pattern_wildcards=truethe input is a literal name.EscapePatternWildcardsInNameescapes\/_/%(backslash first, no idempotency); newLikePatterndoubles backslashes for the SQL string literal (the glob is embedded in'…'; the server's string-literal parser consumes one layer before the LIKE regex).ConvertPatternstays byte-identical to JDBCjdbcPatternToHive. Same under-escaping exists in JDBC — filed databricks-jdbc#1598.3. Exact-match ops validate required args client-side
GetPrimaryKeys/GetCrossReferenceare exact-match; SEA now validates their required args client-side before issuing the SHOW, mirroring the JDBC reference driver'sresolveKeyBasedParams:42000"tableName may not be null" (matches Thrift's clean42000).42000"schema may not be null when catalog is specified" (matches JDBC). This also avoids the Thrift server's internalGET_FUNCTIONS assertion failedbug (SQLSTATE08000) on schema-null — SEA gives a clean, deterministic error instead.(The guards run before the SHOW, so §1's object-not-found catch does not apply.)
4. Exception type:
DatabricksException, comparator compares the ADBC contractSEA throws its own
DatabricksException(natural REST/SEA type) on FAILED, on both sync + async-polling paths. The comparator'sCompareExceptionis relaxed to compareStatus+SqlStateand require both sides beAdbcException— not the concrete subclass — avoiding the semantically-oddHiveServer2Exception-from-SEA (PECO-3014). The JDBC comparator already accepts subclass differences.Known residual (Thrift-side bugs — SEA is correct, handled comparator-side)
Two comparator diffs remain where SEA behaves correctly (matching JDBC) and Thrift has a server-side bug; both are addressed on the comparator side, not by making SEA replicate a Thrift quirk:
catalog=""): Thrift throwsINVALID_PARAMETER_VALUE("name '' is not a valid name"); SEA + JDBC return empty. → comparator fixture uses a nonexistent named catalog instead of"".42000; Thrift throws its internal08000"assertion failed" → residual sqlstate diff. → whitelist/skip on the comparator with a reason (Thrift server bug).Tests
StringValueTests.TestVarcharExceptionDataDatabricksskipped for SEA (shared base asserts exactHiveServer2Exceptionin the hiveserver2 submodule; parity covered by comparator). PECO-3014.Companion change
Comparator relax + the
""→nonexistent fixture change land in databricks-driver-test.This pull request and its description were written by Isaac.