Skip to content

[CXH-2723] - Add Unity Catalog table and volume sync - Databricks Connector - #73

Open
mateoHernandez123 wants to merge 3 commits into
mateoHernandez123/unity-catalog-schema-syncfrom
mateoHernandez123/unity-catalog-table-and-volume-sync
Open

mateoHernandez123 wants to merge 3 commits into
mateoHernandez123/unity-catalog-schema-syncfrom
mateoHernandez123/unity-catalog-table-and-volume-sync

Conversation

@mateoHernandez123

@mateoHernandez123 mateoHernandez123 commented Oct 9, 2026 •

Copy link
Copy Markdown

Description

  • Bug fix
  • New feature

Last of four stacked PRs for Unity Catalog (CXH-2723). Tables and volumes are the data assets an access review is about. They are siblings under a schema.

#70 is merged. #71 and #72 merge before this one.

What the connector syncs

Before, with #72 applied. The schema is a leaf.

flowchart TD
  account[account]
  metastore[metastore]
  catalog[catalog]
  schema[schema]
  other["user, group, service principal, role, workspace"]
  account --> other
  account --> metastore
  metastore --> catalog
  catalog --> schema
Loading

After this PR. The tree reaches the data assets.

flowchart TD
  account[account]
  metastore[metastore]
  catalog[catalog]
  schema[schema]
  table[table]
  volume[volume]
  other["user, group, service principal, role, workspace"]
  account --> other
  account --> metastore
  metastore --> catalog
  catalog --> schema
  schema --> table
  schema --> volume
  style table fill:#1f6feb,color:#fff,stroke:#1f6feb
  style volume fill:#1f6feb,color:#fff,stroke:#1f6feb
Loading
Order PR Delivers Base State
1 #91 staging landed on main main Open
2 #90 Fix for the metastore layer of #70 #91 Open
3 #71 Catalog #91 Open
4 #72 Schema #71 Open
5 #73 Table and volume #72 This PR

Sync:

table_type decides which privileges the connector offers on each table:

flowchart TD
  table[table securable]
  table --> managed[MANAGED / EXTERNAL / shallow clones]
  table --> view[VIEW / METRIC_VIEW]
  table --> mv[MATERIALIZED_VIEW]
  table --> st[STREAMING_TABLE]
  table --> foreign[FOREIGN]
  table --> empty[missing or unrecognised table_type]
  managed --> tpriv[table privileges]
  view --> vpriv["view set; ALL_PRIVILEGES is not requestable"]
  mv --> mvpriv["view set + REFRESH, requestable"]
  st --> stpriv["SELECT, READ_METADATA, APPLY_TAG, MANAGE, ALL_PRIVILEGES, REFRESH, MODIFY; REFRESH requestable"]
  foreign --> fpriv["read-only; no DELETE, INSERT, UPDATE, or MODIFY"]
  empty --> anypriv["union; REFRESH is not requestable"]
Loading

Provisioning:

  • Grant/Revoke table privileges — the requestable set follows table_type, so ConductorOne does not offer an entitlement the grants API rejects.
    • VIEW / METRIC_VIEW: the view set. ALL_PRIVILEGES is emitted when Databricks already holds it and is not requestable. Grant returns InvalidArgument before any PATCH. Revoke still removes it.
    • MATERIALIZED_VIEW: the view set plus REFRESH. REFRESH stays requestable.
    • STREAMING_TABLE: SELECT, READ_METADATA, APPLY_TAG, MANAGE, ALL_PRIVILEGES, REFRESH and MODIFY. REFRESH stays requestable. The table DML trio (DELETE, INSERT, UPDATE) is not offered.
    • MANAGED, EXTERNAL, MANAGED_SHALLOW_CLONE and EXTERNAL_SHALLOW_CLONE: the table set, which does not include REFRESH.
    • FOREIGN: read-only. DELETE, INSERT, UPDATE and MODIFY are not offered.
    • Missing or unrecognised table_type: the union of every variant, so a held REFRESH is not dropped. That REFRESH is not requestable, and Grant returns InvalidArgument before any PATCH.
  • Grant/Revoke volume privileges — the same PATCH path and the same echo-confirmed idempotency: GrantAlreadyExists and GrantAlreadyRevoked.
  • Owner is read-only.
  • A revoke whose table, schema or catalog Databricks names as gone (TABLE_DOES_NOT_EXIST, SCHEMA_DOES_NOT_EXIST, CATALOG_DOES_NOT_EXIST) is GrantAlreadyRevoked. Any other 404 still fails, because an unbound or unprivileged workspace answers 404 too.
  • A volume has no code of its own. Measured on the tenant, the permissions endpoint answers RESOURCE_DOES_NOT_EXIST for a missing volume, a missing schema and a missing catalog alike, and VOLUME_DOES_NOT_EXIST is never returned at all. A workspace that cannot serve the volume answers the same generic code, so on its own it proves nothing: taking it as proof let a routing miss report GrantAlreadyRevoked while the privilege was still held. The volume level now names that code as ambiguous and confirms it with GET /api/2.1/unity-catalog/volumes/{full_name} through the same workspace, which answers 200 while the volume exists. Only a second absence counts, and a confirmation that does not answer leaves it unproven.
GET permissions/volume/<catalog>.<schema>.<missing>   404 RESOURCE_DOES_NOT_EXIST
GET permissions/volume/<catalog>.<missing schema>.x   404 RESOURCE_DOES_NOT_EXIST
GET volumes/<catalog>.<schema>.<missing>              404 RESOURCE_DOES_NOT_EXIST
GET volumes/<catalog>.<missing schema>.x              404 SCHEMA_DOES_NOT_EXIST
GET volumes/<catalog>.<schema>.<a volume that exists>  200

Verified end to end: a revoke on a volume that does not exist answers GrantAlreadyRevoked and sends no PATCH, and a revoke on a volume that exists is never reported gone.

Auth:

Credentials are unchanged. There is no new config field.

The schema listing from #72 decides what this PR can read. Schema List fails with PermissionDenied unless that listing is complete, and tables and volumes are read through the same workspace. information_schema is skipped.

A direct Admin is enough when the catalog is owned by that workspace's admins group. The connector reads the workspace id off the group name _workspace_admins_<workspace>_<id> and checks that workspace, whatever route the securable is listed through. That rule arrived from #71 and applies to the table and volume trust rules here as well.

Architecture highlights:

  • Table listing sends omit_columns and omit_properties. It does not send omit_username, because that one also drops owner, and every securable's profile and owner grant depend on it. The client test asserts the exact query set, so adding it fails there.
  • The tables endpoint rejects max_results above 50. The client clamps to 50.
  • Shared trust probe: holdsPrivilegeOn is the one uncached read of the connector's own effective privileges. Each level passes the privileges it accepts and the error codes that prove the securable is gone. The table and volume probes carry the codes of the securable they read, so a 404 naming a missing parent is an absence rather than a privilege the caller cannot see.
  • Ambiguous codes: securableLevel separates a code that proves an absence from one that only suggests it. A level may name ambiguousGoneCodes and supply the read that settles them, which is what the volume level does. Both shared call sites go through it, so Grants no longer reports an empty volume on the generic code either.
  • Resource types: table and volume carry no trait. They hold a profile and no app semantics, and a profile goes on any resource now. table keeps per-instance Entitlements() because its privilege set follows table_type, so it must not skip entitlements; volume has a static set and does.
  • Table and volume Grants use the same principal path as [CXH-2722] - Add Unity Catalog schema sync - Databricks Connector #72. They read the full account SCIM only when assignments exist, and an owner-only page uses quoted Find* filters. A 400 on the owner SCIM filter continues to the next principal kind: user, then group, then service principal. A 500 fails Grants. The rejection is logged at Debug.

Useful links:

Merge order and release flow

staging lands on main as its own PR, #91. The rest of the stack is rebased on top of it, so each PR below shows only the layer it adds. #84 is independent of this stack and merges on its own.

  1. Land staging on main - Databricks Connector #91 — staging landed on main (46 files)
  2. [CXH-3600] - Sync metastore privileges held by the workspace admins group - Databricks Connector #90 — metastore privileges held by the workspace admins group (15 files)
  3. [CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector #71 — Unity Catalog catalog sync (25 files)
  4. [CXH-2722] - Add Unity Catalog schema sync - Databricks Connector #72 — Unity Catalog schema sync (17 files)
  5. [CXH-2723] - Add Unity Catalog table and volume sync - Databricks Connector #73 (this PR) — Unity Catalog table and volume sync (19 files)

#90 and #71 target #91 until it merges, and GitHub retargets them to main at that point. #72 and #73 keep their parent branch as base.

Rebased on 2026-11-03, as requested on #90. Before this, #90 and #71 showed 42 staging-only commits and 50+ files, which hid the layer each one adds. Every branch was rebuilt on top of #91. The tree at each head is byte-identical to the one reviewed before the rebase, so no code changed. The commits are squashed into one per PR, which marks earlier review threads as outdated; they stay readable.

Release flow. Cut a test release (-test) from the feature branch and move the PR to draft. The ticket moves to validation. QA validates that release. Merge to main. Cut the official release from main, and the ticket moves to rollout.

@linear-code

linear-code Bot commented Oct 9, 2026

Copy link
Copy Markdown

CXH-2723

@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from 920df46 to f9142b4 Compare October 9, 2026 13:20
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from 0ee9040 to 38e403e Compare October 9, 2026 13:20
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from f9142b4 to 29274ee Compare October 9, 2026 14:27
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from 38e403e to 7c8e7a3 Compare October 9, 2026 14:27
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from 29274ee to e74210e Compare October 9, 2026 16:46
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from 7c8e7a3 to a3bd512 Compare October 9, 2026 16:46
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from e74210e to 7deff05 Compare October 9, 2026 16:54
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from a3bd512 to 5dce9b1 Compare October 9, 2026 16:54
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from 7deff05 to 1253d7e Compare October 9, 2026 18:17
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from 5dce9b1 to 5effb3f Compare October 9, 2026 18:17

@FeliLucero1 FeliLucero1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did a pass on this one. Main things: the per-table perf (the stack-wide one actually bites here), the List/Revoke 404 handling copied from #72, and the table_type mapping dropping REFRESH / ALL_PRIVILEGES grants from the sync. The rest are nits.

Comments tagged (out of scope) are inherited or stack-wide, so treat them as FYI, not blocking for this PR. Build/vet/tests pass locally.

Comment thread pkg/connector/table.go Resolved
Comment thread pkg/connector/table.go Resolved
Comment thread pkg/connector/volume.go Resolved
Comment thread pkg/connector/table.go Resolved
Comment thread pkg/connector/privileges.go Resolved
Comment thread pkg/connector/table.go Resolved
Comment thread pkg/connector/table.go Resolved
Comment thread pkg/connector/table.go Resolved
Comment thread docs/docs-info.md Resolved
Comment thread README.md Resolved

@FeliLucero1 FeliLucero1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Image Image Image Image

@FeliLucero1 FeliLucero1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Round 5. Looks good, the main asks are fixed (children completeness proof, no silent empty list, revoke on dropped targets, route docs) and CI is green. Approvable from my side; the comments below are small follow-ups.

(out of scope, #71 catalog.go:270) built-in catalog skip:

  • __databricks_internal (INTERNAL_CATALOG, Lakeflow pipelines) isn't covered, so it'd fail the sync the same way system did
  • the docs don't say samples is SYSTEM_CATALOG (could be Delta Sharing), so worth confirming
  • this commit duplicates logic already on #71's tip (abee6af5), so watch the rebase

nits (outside the diff): schema.go:195 still says the schema List skips system (only the catalog syncer does), catalog_test.go:356 still says "never synced / deletes nothing", and the README schema-listing rule leaves out metastore owner + the admin path that docs-info has.

Comment thread pkg/connector/table.go Resolved
Comment thread pkg/connector/schema_child.go Resolved
Comment thread pkg/connector/schema_child.go Resolved
Comment thread pkg/connector/resource_types.go Resolved
Comment thread pkg/connector/resource_types.go Resolved
Comment thread pkg/connector/schema.go Resolved

@FeliLucero1 FeliLucero1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Round 6. Looks good, CI green, and thanks for the walk/comment fixes. One thing I'd fix before merge: the RESOURCE_DOES_NOT_EXIST mapping on volumes (inline). I'm fine leaving the non-owner revoke path out as you said.

Leftovers outside the diff: schema.go:195 still says the schema List leaves system out (only the catalog syncer does). (out of scope, #71) the built-in skip still doesn't cover __databricks_internal (INTERNAL_CATALOG).

Comment thread pkg/connector/volume.go Resolved
Comment thread pkg/connector/volume.go Resolved

@sergiocorral-conductorone sergiocorral-conductorone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review (follow-up round on new commits).

Stats: drafted 3 → confirmed 3, low-confidence 0, dropped 0, added 0 (panel: opus-5.5 / gpt-6-astra / gemini-4-argon, all high).

Findings not anchorable to a diff line:

  • pkg/connector/catalog.go:419 — [major] catalogThrough used to answer (nil, nil) for both CATALOG_DOES_NOT_EXIST and a 401/403/404 deny, and every caller's walk treated catalog == nil as "try the next workspace". Now it returns errCatalogGone or the raw deny error, but not every caller was updated:
  • pushSchemaChildRoute (schema_child.go:169-172) still does if err != nil { return err }. A deny or a gone code on usable[0] ends the table/volume walk there.
  • schema pushRoute (schema.go:305-311) continues on a deny, but returns errCatalogGone as a plain error.

This PR's own logic (workspaceForCatalog's allGone and confirmCatalogGone's "still exists but no workspace in sync scope can list it") assumes a workspace the catalog is not bound to answers CATALOG_DOES_NOT_EXIST. So for an ISOLATED catalog bound only to usable[1], the catalog syncer emits it through workspace B. Then the schema List and every table/volume List under it fail on workspace A, and the sync never completes on multi-workspace accounts. That undoes the "walk usable until one lists it" fix from the schema_child.go:163 thread. Suggest treating errors.Is(err, errCatalogGone) || deniesTrust(err) as continue in both walks, keeping lastDeny for the final error message the way schema.go now does. Please add a two-workspace test where wsa answers 404 CATALOG_DOES_NOT_EXIST and wsb serves the catalog, for both schemas and tables.

  • pkg/connector/catalog.go:316 — [minor, perf] With the seen dedupe gone, a catalog bound to W in-scope workspaces is emitted W times, and every OPEN catalog is bound to all of them. The store does collapse the catalog row on upsert. But the SDK syncer queues a child-resource sync for each resource a List returns, so the catalog's whole subtree is listed W times: schemas, then tables and volumes per schema. That subtree includes the uncached per-schema completeness proof (schemaListingIsComplete/childListingIsComplete) that the listSchemaChildren comment already calls quadratic. The live measurement here was a single-workspace tenant, so W=1 hides it. If the old seen map was dropped because it outlived a sync, a sync-safe alternative would be to emit an OPEN catalog only from the first route that serves it, or to carry the names already emitted in the bag state.

Comment thread pkg/connector/volume.go Resolved
@mateoHernandez123
mateoHernandez123 marked this pull request as draft November 2, 2026 17:07
@mateoHernandez123
mateoHernandez123 marked this pull request as ready for review November 2, 2026 19:27
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from 433e33c to b704b02 Compare November 3, 2026 18:27
mateoHernandez123 added a commit that referenced this pull request Nov 3, 2026
CXH-2723. Tables and volumes hang from the schema, each with its own
privilege set and a read-only owner.

The tree is unchanged from d10a7e9, the state reviewed and approved on #73.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from d10a7e9 to a03b5a6 Compare November 3, 2026 18:27
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from b704b02 to f63db64 Compare November 3, 2026 20:10
mateoHernandez123 added a commit that referenced this pull request Nov 3, 2026
CXH-2723. Tables and volumes hang from the schema, each with its own
privilege set and a read-only owner.

The tree is unchanged from d10a7e9, the state reviewed and approved on #73.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from a03b5a6 to 47a3e98 Compare November 3, 2026 20:12
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-schema-sync branch from f63db64 to 068e05d Compare November 3, 2026 20:46
CXH-2723. Tables and volumes hang from the schema, each with its own
privilege set and a read-only owner.

The tree is unchanged from d10a7e9, the state reviewed and approved on #73.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-table-and-volume-sync branch from 47a3e98 to acfcba8 Compare November 3, 2026 20:49
newCatalogBuilder(d.client, d.clientID, workspaces, principals),
newSchemaBuilder(d.client, d.clientID, workspaces, principals),
newTableBuilder(d.client, d.clientID, workspaces, principals),
newVolumeBuilder(d.client, d.clientID, workspaces, principals),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

O(schemas^2) API calls per catalog (schema_child.go:127-229). pushSchemaChildRoute runs on page 1 of every schema, once for tables and once for volumes. Each run lists every schema of the catalog through schemaThrough (50 per page) and repeats the uncached privilege probes. A catalog with 1,000 schemas makes about 2,000 x 20 schema-list pages, around 40k calls before any table is listed, which will hit rate limits. Fix: a single GET schemas/{catalog}.{schema}, or reuse the owner data the schema syncer already wrote onto the schema resource.

Per-table cost in Grants (securable_helpers.go, schema_child.go:356). Each table does its own permissions read, and a non-owner connector adds an uncached effective-privileges read per table. Entitlements run at roughly 10 per table (table.go:285). With 100k tables that is 200k+ extra calls per sync. Consider trusting the route-level proof carried in the profile rather than re-reading per resource.

I am a bit worried about the performance. Could you please confirm with it?

@mateoHernandez123 mateoHernandez123 Nov 4, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You are right on the first one. Sergio flagged the same shape earlier in this PR and I answered that the uhttp GET cache kept it cheap. That was too comfortable: the cache is 5 MB shared by the whole sync, so it only holds while a catalog's entire schema listing fits in it.

Measured against a simulated tenant, one catalog with 1,000 schemas: 43,024 requests with the cache off (your ~40k is right), 5,027 warm, and with 30 KB schema rows the listing count goes from 4 to 108 at only 200 schemas, with the default cache untouched.

Fixed in 01bab61. schemaThrough now reads the one schema through GET /schemas/{catalog}.{schema}, the move catalogThrough already made. Linear in the schema count now. Error classification is unchanged and the branch that matters has a test: a 404 with no code still fails the sync instead of listing nothing.

Per-table cost: correct for a connector that does not own the metastore — 600 extra uncached effective-permissions reads over 200 schemas. This account owns the metastore, so it costs nothing today, and removing it needs the route memoized per catalog, which I would rather do separately.

Entitlements does not apply: tableBuilder.Entitlements reads table_type off the profile and makes no API call. The measurement produced 20,000 entitlements with 0 requests.

mateoHernandez123 and others added 2 commits November 4, 2026 16:15
Listing a schema's tables or volumes resolved its route on the first page,
once per child type, and schemaThrough found the schema by paging the whole
catalog's schema listing. That is one full listing walk per schema per child
type: a catalog with 1,000 schemas spends about 21,000 listing requests, and
43,000 requests in all, before the first table is listed. The uhttp GET cache
hid it while a catalog's listing fit in the 5 MB it holds by default, which a
catalog of 30 KB schema rows does not.

schemaThrough now reads the one schema it needs. The same catalog costs 2,000
schema reads instead, and the cost is linear in the schema count rather than
quadratic.

Error classification is unchanged. A 404 naming SCHEMA_DOES_NOT_EXIST is read
exactly like a schema absent from the listing was: proof of deletion only for
a caller that sees the whole catalog, since Databricks answers the same way
for a schema the caller merely cannot see. Every other refusal, including a
404 carrying no code, still leaves the schema unknown rather than absent --
read as an empty result the SDK would skip the children and ConductorOne
would delete every table and volume under the schema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Listing a schema's tables or volumes returned the catalog read's error for
every refusal, where the schema syncer walks on to the next workspace. A
catalog bound to one workspace of several is answered by the others with a
404, and which of the two answers Databricks gives decides which way this
breaks.

A 404 carrying no error code reached the SDK as codes.NotFound, which it
reads as a warning: the listing finished green with no resources and
ConductorOne deleted every table and volume under the schema. A 404 naming
CATALOG_DOES_NOT_EXIST raised errCatalogGone, which failed the whole sync.

Both now continue the walk, matching the schema syncer. A catalog every
workspace names as gone, with none refusing the read, empties the listing:
the catalog syncer drops it in the same sync. A refusal still fails the
sync and keeps what ConductorOne already has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@FeliLucero1 FeliLucero1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Round 7. Nice: the single-schema GET fixes the O(schemas²), the walks now continue (with two-workspace tests), and volumes confirm the generic 404. CI green. One thing I'd still want before merge: the gone codes trusted from a single workspace (inline on volume.go:61/81).

(out of scope, #71) the catalog is still emitted once per bound workspace (catalog.go:225-238, no dedupe), so its whole subtree gets listed W times, Sergio's perf point. FYI on the trait split: per c1 (uplift_app.go), only TRAIT_APP resources become child apps, so not having it on 100k tables is actually a plus. Dropping it from all five like you asked ggreer sounds good to me.

Comment thread pkg/connector/volume.go
trusts: volumeGrantsAreTrustworthy,
// Dropping a parent drops the volume with it, and both parent codes name
// what is missing. The generic code does not, so it is confirmed first.
goneErrorCodes: []string{errorCodeSchemaGone, errorCodeCatalogGone},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[medium] CATALOG_DOES_NOT_EXIST is taken as proof here (and in table.go:164) that the target is gone → GrantAlreadyRevoked on revoke, zero grants on Grants. But the catalog walk assumes the opposite: an unbound workspace answers that same code, which is why the catalog level runs confirmCatalogGone with include_unbound=true before believing it. Here we read through the workspace saved in the profile with no cross-check. So if the binding changed after the sync, or that workspace stops serving the catalog, the revoke "succeeds" while access stays, and Grants empties the table/volume in C1. Suggest routing CATALOG_DOES_NOT_EXIST through confirmCatalogGone (or an equivalent cross-workspace check) the same way the catalog level does.

Comment thread pkg/connector/volume.go
if err == nil {
return volume == nil, nil
}
if databricks.IsSecurableGone(err, errorCodeGenericGone, errorCodeSchemaGone, errorCodeCatalogGone) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[medium, same root cause] the confirm read goes through the same workspace and also accepts RESOURCE_DOES_NOT_EXIST. Per your own measurement, GET volumes/…nope gives that same generic 404, so for a route miss (a workspace that can't serve the volume) the confirmation proves nothing. Maybe confirm the parent first through the same workspace (GET /schemas/{catalog}.{schema} → 200) and only then trust a volume 404. Or drop the generic code here and keep just the schema/catalog codes (with the catalog caveat above). Also no test covers these paths yet: generic + 404 = gone, generic + 200/500 = error, Grants on a dropped volume.

Comment thread baton_capabilities.json
"id": "table",
"displayName": "Table",
"traits": [
"TRAIT_APP"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: still says TRAIT_APP for table (and volume at :502), but resource_types.go dropped the trait. The capabilities workflow only regenerates on push to main, so CI won't catch it here. Worth regenerating so the file/docs match.

const childListRemedy = "the connector's service principal must own the metastore, the catalog or the schema, " +
"or hold ALL_PRIVILEGES on the catalog"

// pushSchemaChildRoute records a workspace that can list every table or volume

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: comment's stale after 101ce0f. It still says an error from a workspace stops the walk and that an empty result is an error, but catalogThrough denies/gone now continue, and all-gone returns nil (:253). Also a deny from schemaThrough (:213) still stops the walk instead of trying the next workspace. Fails safe, just inconsistent with the catalog step.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants