Skip to content

Make export usable on large accounts (10k SHOW cap) - #69

Merged
noel merged 2 commits into
datacoves:mainfrom
jacodegroothydrab:perf/large-account-export
Aug 27, 2026
Merged

Make export usable on large accounts (10k SHOW cap)#69
noel merged 2 commits into
datacoves:mainfrom
jacodegroothydrab:perf/large-account-export

Conversation

@jacodegroothydrab

@jacodegroothydrab jacodegroothydrab commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Replaces #47, cut to the scope of your review.

snowcap export --all does not complete on a large account (~15k tables, ~90k grants). We faced this challenge after importing from a legacy SAGE instance. Beyond lifting this cap, it also contains a number of performance fixes that speed up the code base, and would benefit users with a large number of tables too:

Three issues:

  1. SHOW ... IN ACCOUNT is capped at 10,000 rows (090153), so list_tables / list_views / list_stages fail outright past that. They now read SNOWFLAKE.ACCOUNT_USAGE, filtering deleted IS NULL.
  2. _show_all_grants_to_role rescanned the whole grant cache per call — 32.7M str.upper() calls on a 300-grant profile. Now indexed by grantee, once per session.
  3. _fetch_grant_to_role scanned a role's grants linearly, building two ResourceName objects per candidate. Now indexed by (granted_on, privilege, name).

ResourceName can't key a dict: __hash__ is hash(str(self)) but __eq__ treats quoted "FOO" and unquoted FOO as equal, and str() keeps the quotes. _grant_name_key normalises to "exact if quoted, upper-cased otherwise", reproducing __eq__ across all four combinations. Types go through _granted_on_label, as the scan did, so the synonyms #66 fixed still match. Both indexes are cleared by reset_account_usage_caches.

Your review:

  1. Quoted/mixed-case databases — fixed; the comparison now happens in ResourceName space. test_a_quoted_database_is_kept fails against the old comparison.
  2. Default + warning + escape flagACCOUNT_USAGE stays the default, warned once per run, with --use-account-usage/--no-use-account-usage on export reaching down to the listers. No cap-detection; the only automatic fallback is the pre-existing missing-IMPORTED PRIVILEGES-or-query-failed one.
  3. Tests — 51 unit tests, no Snowflake required: _grant_name_key against ResourceName.__eq__; _grant_lookup_index (quoting, account grants, type synonyms, first-duplicate-wins, one read per role, per-role isolation, rebuild after reset); _grants_by_role_index (grouping, case-insensitive grantee, non-role grantees, served without a query, cleared on reset); the listing helper (quoted/upper-case databases kept, unmanaged and system databases dropped, opt-out and no-access return None unqueried, failed query stops retrying, warned exactly once); the three listers preferring ACCOUNT_USAGE, falling back, and forwarding the opt-out.

ruff, black, codespell, mypy clean; unit suite passes.

Not carried over from #47: --threads (not what fixed this) and the schema-scoped export qualification (separate concern — happy to open it on its own).

Results — 5,000-grant sample, steady state:

ms/grant grant phase
before 48.32 ~74 min (projected)
after 0.01 ~1 s

export --all --exclude=table,view: did not complete → 125 seconds.

🤖 Generated with Claude Code

@jacodegroothydrab
jacodegroothydrab force-pushed the perf/large-account-export branch from 905c490 to 20ae8a3 Compare August 26, 2026 21:34
@jacodegroothydrab jacodegroothydrab changed the title perf(export): make export usable on large accounts (10k SHOW cap, O(n^2) grant lookup) Make export usable on large accounts (10k SHOW cap) Aug 26, 2026
On a large account (~15k tables, ~90k grants) `snowcap export --all` does not
complete. Three independent causes, all in the read path.

1. `SHOW ... IN ACCOUNT` is capped at 10,000 rows by Snowflake:

     090153 (22000): The result set size exceeded the max number of
     rows(10000) supported for SHOW statements. on SHOW TABLES IN ACCOUNT

   Past that, list_tables / list_views / list_stages fail outright and take the
   whole export or plan with them. They now read SNOWFLAKE.ACCOUNT_USAGE, which
   has no cap, filtering `deleted IS NULL` so dropped objects are not
   resurrected. Databases are compared as ResourceName, the way the SHOW path
   does, so quoted and mixed-case databases are not silently excluded.

   ACCOUNT_USAGE lags live state by up to ~2 hours, so an object created moments
   ago can be missed. On an account past the cap a slightly stale answer beats no
   answer, so this stays the default, announced once per run, with
   `--no-use-account-usage` as the escape hatch to real-time SHOW. Nothing tries
   to detect truncation by counting rows; the only automatic fallback is the
   pre-existing one, on missing IMPORTED PRIVILEGES or a failed query.

2. `_show_all_grants_to_role` re-filtered the entire cached ACCOUNT_USAGE grant
   list on every call. A 300-grant profile showed 32.7M `str.upper()` calls.
   Now indexed by grantee, once per session.

3. `_fetch_grant_to_role` scanned a role's whole grant list, constructing two
   ResourceName objects per candidate. Now indexed by
   (granted_on, privilege, name).

   ResourceName cannot key a dict directly: __hash__ is hash(str(self)), but
   __eq__ treats quoted "FOO" and unquoted FOO as equal while str() keeps the
   quotes, so the two disagree. _grant_name_key normalises to "exact if quoted,
   upper-cased otherwise", reproducing __eq__ across all four quoted/unquoted
   combinations. Object types go through _granted_on_label, as the scan it
   replaces did, so reported synonyms (CORTEX_AGENT_SERVER for MCP SERVER) still
   match.

Both indexes are derived state, so reset_account_usage_caches clears them
alongside the caches they are built from.

Measured on the affected account, 5,000-grant sample, steady state:

  before  48.32 ms/grant  -> ~74 min projected for the grant phase alone
  after    0.01 ms/grant  -> ~1 second

Full `export --all --exclude=table,view`: did not complete -> 125 seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jacodegroothydrab
jacodegroothydrab force-pushed the perf/large-account-export branch from 20ae8a3 to abb15f3 Compare August 26, 2026 21:40
@noel

noel commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

There are two cache/configuration lifecycle issues to address:

  1. snowcap/blueprint.py:1684: sync-mode listing does not forward self._config.use_account_usage to list_resource(). Because list_tables, list_views, and list_stages now default to True, use_account_usage: false still reads stale ACCOUNT_USAGE data for those resources. Please forward the configured value and add a regression test proving sync-mode tables use SHOW when it is false. Keeping the provider-level defaults false would also preserve existing direct-caller behavior.

  2. snowcap/data_provider.py:410: _GRANT_LOOKUP_INDEX_CACHE outlives reset_cache(). Blueprint.plan() resets the SQL cache before each plan, but a second plan on the same session can still return grants indexed during the first plan. Please tie this index to the existing execution-cache lifecycle and test that changed grant results are observed after a reset.

Please also substantially shorten the long rationale comments added throughout data_provider.py. The PR description already preserves the performance history; production comments should remain only where they explain a necessary invariant such as quoted-name normalization or first-match behavior.

…o reset_cache

Two lifecycle issues from review, plus a comment trim.

1. Sync-mode listing did not forward the configured source. list_tables,
   list_views and list_stages had defaulted to ACCOUNT_USAGE, so
   `use_account_usage: false` still read a listing that lags live state by up
   to ~2 hours -- and sync mode drops whatever remote state omits.

   fetch_remote_state now passes self._config.use_account_usage to
   list_resource, which drops the kwarg for list functions that do not take
   it. The provider-level defaults go back to False, so a direct caller keeps
   the real-time SHOW it had before; `snowcap export` still opts in, as its
   --use-account-usage/--no-use-account-usage flag already did.

2. _GRANT_LOOKUP_INDEX_CACHE outlived reset_cache(). The index is built from
   cacheable SHOW GRANTS rows, but only reset_account_usage_caches() cleared
   it, so a second plan on the same session -- Blueprint.plan() calls
   reset_cache() and nothing else -- was answered from the first plan's
   grants.

   client.register_cache_reset_hook lets a module invalidate a cache derived
   from the execution cache without client importing it back; data_provider
   registers the index there, and reset_cache() now fires the hooks.

Regression tests cover both: sync-mode tables issue SHOW TABLES IN ACCOUNT
and no ACCOUNT_USAGE query when the flag is false, and a changed grant is
observed after reset_cache() while the index still survives within one plan.

Also shortened the rationale comments through data_provider.py. The
performance history lives in the PR description; what stays in the code is
the quoted-name normalization, the object-type synonyms, and first-match
behaviour -- the invariants a later edit could silently break.

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

jacodegroothydrab commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Both fixed in c6f1e06.

Sync listingfetch_remote_state now forwards the configured value, and the provider defaults go back to False. export is unchanged; it passes the flag explicitly. The forward covers every synced type, so grant listing honours it too — list_grants already took the kwarg but never received it.

Grant indexreset_cache() now clears it, through a hook registry in client.py, since client cannot import data_provider.

Tests: sync-mode tables issue SHOW TABLES IN ACCOUNT when the flag is false, and a changed grant is seen after a reset. Both fail without the fixes.

Comments in data_provider.py: 32 added lines down to 9, keeping quoted-name normalization and first-match-wins.

@noel
noel merged commit 381d283 into datacoves:main Aug 27, 2026
1 check passed
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.

2 participants