Skip to content

feat(athena): resolve cross-account Glue catalogs in Spark Python models - #7

Open
dtaniwaki wants to merge 3 commits into
mainfrom
feat/athena-spark-cross-account-catalog
Open

feat(athena): resolve cross-account Glue catalogs in Spark Python models#7
dtaniwaki wants to merge 3 commits into
mainfrom
feat/athena-spark-cross-account-catalog

Conversation

@dtaniwaki

@dtaniwaki dtaniwaki commented Apr 7, 2026

Copy link
Copy Markdown
Owner

Background

Spark Python models that use dbt.source() / dbt.ref() cannot currently read from cross-account GLUE catalogs registered as Athena Data Catalogs.

athena__py_get_spark_dbt_object's get_spark_df helper strips the catalog component from the identifier before calling spark.table(), so the Spark Glue Catalog Client always targets the local account's Glue. Attempting dbt.source("cross_account", "tbl") from a Python model fails with:

software.amazon.awssdk.services.glue.model.AccessDeniedException:
  User: arn:aws:sts::<local>:assumed-role/... is not authorized to perform:
  glue:GetTable on resource: arn:aws:glue:...:<local>:database/<db>

Today the only workarounds are (a) hard-coding the account ID via spark.table("<account_id>/schema.table") — which breaks dbt lineage (selectors, docs, source freshness) — or (b) copying all cross-account data to the local account, which is extremely heavyweight.

Change

Introduce an adapter method that discovers cross-account GLUE catalogs at compile time and injects the resolved {catalog_name: account_id} mapping into the generated Python code, so get_spark_df can translate identifiers transparently.

Adapter

AthenaAdapter.get_spark_cross_account_catalog_map()

  • @available + @lru_cache() (same pattern as _get_work_group)
  • Paginates athena:ListDataCatalogs
  • For each non-default GLUE catalog, calls athena:GetDataCatalog and extracts Parameters.catalog-id via the existing get_catalog_id() helper
  • Returns Dict[str, str] — the local awsdatacatalog is excluded (not needed) and non-GLUE catalogs are skipped (Spark's Glue Catalog Client can't read them)

Macro

athena__py_get_spark_dbt_object now:

  1. Reads config.spark_cross_account_catalog (model config). When false (default), it skips the adapter call entirely and injects an empty _CROSS_ACCOUNT_CATALOGS = {} — preserving the pre-feature behavior and avoiding identifiers the Spark parser cannot handle.
  2. When the model opts in, calls the new adapter method at compile time and injects the resolved mapping into the generated code as _CROSS_ACCOUNT_CATALOGS = {...} via | tojson.
  3. Updates get_spark_df to:
    • Return spark.table("schema.table") for awsdatacatalog (existing behavior)
    • Return spark.table("<account_id>/schema.table") for known cross-account GLUE catalogs — Spark can resolve this when spark.hadoop.aws.glue.catalog.separator is set to /, which AthenaSparkSessionConfig does automatically when the same spark_cross_account_catalog: true model config is present
    • Fall back to the legacy identifier for unknown catalogs so existing behavior is preserved

Tests

  • test_get_spark_cross_account_catalog_map_only_default — only awsdatacatalog registered → empty dict (regression guard)
  • test_get_spark_cross_account_catalog_map_with_cross_account_glue — one cross-account GLUE catalog is included with its catalog-id
  • test_get_spark_cross_account_catalog_map_excludes_non_glue — LAMBDA / federated catalogs are filtered out (Spark can't read them)

All 101 tests in tests/unit/test_adapter.py + tests/unit/test_python_submissions.py pass.

Required permissions

athena:ListDataCatalogs is new but is typically already granted to any role that can run Athena queries. athena:GetDataCatalog is already used by _get_data_catalog. Both are only called when the model opts in with spark_cross_account_catalog: true, so models that do not use the feature do not require the new permissions.

Runtime requirement

To activate cross-account resolution for a Python model, set spark_cross_account_catalog: true in the model config (e.g. via dbt.config(...), schema.yml, or a project-level +spark_cross_account_catalog: true). This single flag:

  1. Adds spark.hadoop.aws.glue.catalog.separator: / to the Spark session properties via AthenaSparkSessionConfig, so Spark can parse <account_id>/<schema>.<table>.
  2. Gates the macro on the same config: the adapter is invoked and _CROSS_ACCOUNT_CATALOGS is populated only when the flag is true.

Models that do not set the flag get an empty mapping and get_spark_df falls back to the legacy two-part <schema>.<table> form. This is important: without the gating, a model that had a cross-account catalog registered in Athena but had not enabled the separator would emit <account_id>/<schema>.<table> to spark.table() and fail with:

pyspark.sql.utils.ParseException:
  mismatched input '<account_id>' expecting {...}
  == SQL ==
  <account_id>/<schema>.<table>

Backwards compatibility

  • Users with only awsdatacatalog registered get an empty map → identical to the pre-existing behavior (no regression).
  • Users that were already working around this by hard-coding <account_id>/schema.table via session.table() continue to work unchanged.
  • Users who have cross-account catalogs registered in Athena but do not enable spark_cross_account_catalog on their models keep the pre-feature behavior (the adapter is not called and the map is empty).
  • No changes to profile schema are required; the feature is activated per-model via the spark_cross_account_catalog config.

Spark Python models using `dbt.source()` / `dbt.ref()` currently fail to
read from cross-account GLUE catalogs registered as Athena Data Catalogs.
`get_spark_df` strips the catalog component from the identifier, so the
Spark Glue Catalog Client always targets the local account's Glue.

Add `get_spark_cross_account_catalog_map()` on the adapter, which calls
`athena:ListDataCatalogs` + `athena:GetDataCatalog` and returns a
`{catalog_name: account_id}` mapping for all non-default GLUE catalogs.
The result is cached per adapter instance with `@lru_cache` to avoid
repeated API calls.

The mapping is injected into `athena__py_get_spark_dbt_object` as
`_CROSS_ACCOUNT_CATALOGS` at compile time. `get_spark_df` now:

- Returns `spark.table("schema.table")` for the local `awsdatacatalog`
  (existing behavior).
- Returns `spark.table("<account_id>/schema.table")` for known
  cross-account GLUE catalogs, which Spark can resolve when
  `spark_cross_account_catalog` is enabled (sets
  `spark.hadoop.aws.glue.catalog.separator` to `/`).
- Falls back to the legacy identifier for unknown catalogs to preserve
  the current behavior for any edge case.

Users are no longer required to hard-code account IDs or bypass
`dbt.source()` to reference cross-account tables from Spark Python
models, so dbt lineage (selectors, docs, source freshness) remains
intact.
…talog config

The macro `athena__py_get_spark_dbt_object` unconditionally populated
`_CROSS_ACCOUNT_CATALOGS` from `adapter.get_spark_cross_account_catalog_map()`,
so Python models that referenced a registered cross-account GLUE catalog
emitted `<account_id>/<schema>.<table>` identifiers to `spark.table()`.

Those identifiers are only parseable by Spark when
`spark.hadoop.aws.glue.catalog.separator` is set to `/`, which
`AthenaSparkSessionConfig` only adds when the model opts in with
`spark_cross_account_catalog: true`. Models that had not opted in
therefore failed at runtime with a Spark `ParseException`:

  pyspark.sql.utils.ParseException:
  mismatched input '<account_id>' ...
  == SQL ==
  <account_id>/<schema>.<table>

Gate the map population in the macro on the same model config. When the
model has not opted in, `_CROSS_ACCOUNT_CATALOGS` stays empty and
`get_spark_df` falls back to the legacy two-part `schema.table` form,
matching the pre-feature behavior. Opt-in models continue to work
exactly as before the fix.

The adapter method itself is unchanged — its unit tests still pass.
spark.table() internally parses the name as a SQL identifier, and '/'
is not a valid character in an unquoted identifier. Wrapping the
account_id/schema portion in backticks lets the SQL parser accept it,
after which the Glue Catalog Client resolves the cross-account reference
using the configured separator.
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.

1 participant