Repository navigation
Merge pull request #93 from jacodegroothydrab/fix/quoting-followup
Escape the remaining hand-quoted string literals
Merge pull request #97 from jacodegroothydrab/fix/split-statements
Tokenize statements in _split_statements
Tokenize statements so backslash escapes and glued literals split correctly
Split out of #93 so it can be reviewed on its own.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Drop byte-escape decoding, escape LIKE patterns, split out the splitter
- Remove the \xhh / \ooo branch and its ValueError from the string-literal
decoder; Snowflake returns characters, not byte escapes, so it was
unreachable and the raise could abort a plan from a parse action. - Escape , % and _ in SHOW ... LIKE patterns so names containing a
backslash are found; the exact-name post-filter stays for case. - Move the _split_statements rewrite to its own PR.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Merge pull request #96 from datacoves/feat/saml2-security-integration
feat(security-integration): add SAML2 resource for SSO
fix(security-integration): move SAML2 IdP validation off the spec
blueprint hydrates remote state with resource_cls.spec(**data) and re-raises
whatever that throws, so validating the IdP fields in the spec's post_init
turned a blank field in DESC output into an aborted plan:
ValueError: saml2_provider must be set, or set metadata_url instead
rather than a reported drift. That is most likely on the metadata_url path,
where Snowflake derives those four fields itself. Validate in init, which
is where user config lands; remote state is not user input.
Also from review:
- Restore the private-key guard that was lost by importing PEM_DELIMITER
instead of calling normalize_public_key. Stripping the armor off a private
key pasted into saml2_x509_cert would ship it to Snowflake, where DESC hands
it back to anyone who can read the integration. - Stop marking the four IdP fields "required" in the docs table. With
metadata_url set they are not optional, they are rejected. - Say on allowed_email_patterns that a pattern containing a comma never
converges, not just that it drifts. - Resolve docs field-type links against the resource classes being documented
instead of files on disk, so a clean generation and a re-run agree. Verified
idempotent.
fix(gitops): treat an empty YAML file as an empty config
A file that is empty, or nothing but comments, parses to None. read_config
returned that None straight to merge_configs, which called .items() on it and
died with a bare AttributeError naming neither the file nor the cause:
AttributeError: 'NoneType' object has no attribute 'items'
Commenting a block out while it is being fixed is a normal thing to do in a
config directory, so return {} instead and let it merge as a no-op.
docs(security-integration): show the SAML2 IdP values as vars
The issuer and SSO URL embed the tenant or org id, so the example committed
only the certificate as a var and left the URLs inline. Declare all three in
a vars block with no default, so a missing value fails the run before it
reaches Snowflake.
feat(security-integration): accept metadata_url for SAML2
Snowflake's SAML2 grammar takes either METADATA_URL or the four explicit IdP
parameters, and METADATA_URL is supported for Okta and Microsoft Entra ID.
It was left out of the first commit on the mistaken belief that it was not a
real parameter.
CREATE-only: Snowflake reads the metadata once and reports the derived
saml2_issuer/sso_url/provider/x509_cert through DESC, so a declared
metadata_url is marked unfetchable to keep it out of the diff. The two forms
are rejected at parse time if combined, or if neither is given.
fix(security-integration): normalize the SAML2 cert after vars resolve
Normalizing saml2_x509_cert in post_init raised AttributeError on a cert
supplied as "{{ var.saml2_x509_cert }}", which is still a VarString at
construction time, and that is the only way to keep the cert out of the repo.
_resolve_vars setattrs past post_init, so normalizing earlier would also
leave a PEM delivered through a var un-stripped and drifting.
Normalize in to_dict instead, the same way User handles rsa_public_key, and
reuse PEM_DELIMITER rather than a second armor stripper.
Also from review:
- Use a parseable certificate in the JSON fixture: the integration lifecycle
and list tests CREATE it on a live account, and a 76-char stub fails rather
than skips. Create it disabled, so the test account doesn't briefly show an
SSO button pointing at a bogus IdP. - Fix the documented var syntax: snowcap uses {{ var.x }}, not ${{ var.x }}.
- Only linkify a field type that has a docs page, so SAML2Provider (and every
other enum type, on the next regeneration) stops shipping a 404. - Note the ceiling on allowed_email_patterns: _parse_list_property splits DESC
lists on "," so a pattern containing one reads back split. - Drop test_resolver_returns_saml2_class, covered by
test_polymorphic_resources[SAML2SecurityIntegration] via the fixture.
feat(security-integration): add SAML2 resource for SSO
Snowcap could not manage SAML2 security integrations, so SSO could not be
configured declaratively. SecurityIntegrationType.SAML2 carried the wrong
value ("SAML") and _resolver() had no branch for it, so type: SAML2 raised
ValueError and type: SAML raised TypeError.
- Fix the enum value and add a SAML2SecurityIntegration resource
- Add a SAML2 branch to fetch_security_integration, so plans show no drift
- Normalize PEM armor off saml2_x509_cert, which is how Snowflake stores it
- Document the resource, SSO token caching via ALLOW_ID_TOKEN, and the
ACCOUNTADMIN requirement
ALLOW_ID_TOKEN needs no new code: the generic AccountParameter path already
applies it and reads it back.
Closes #95
Keep only exact-name rows from SHOW ... LIKE lookups
LIKE reads % and _ as wildcards, so MY_SHARE also matched MYXSHARE. The
1000-object branch of _show_resources and each direct LIKE fetch now
filter the rows on the exact name.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Keep escaped quotes in bare names, unescape column defaults, split glued literals
- _quote_bare_name uses ResourceName.unquoted, so a name with an escaped " still matches
- _fetch_columns_for_table decodes quoted TEXT defaults, so 'it''s' reads back as it's
- \xhh / \ooo runs that are not valid UTF-8 raise instead of becoming U+FFFD
- _split_statements tokenises literals glued to the previous token and stops injecting spaces
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Match bare names in LIKE lookups, escape tag/policy references, decode byte escapes
- SHOW ... LIKE and scanner-package ID lookups strip the identifier's double quotes before escaping
- tag_references and POLICY_REFERENCES arguments go through _single_quote
- \xhh and \ooo decode as UTF-8 bytes; \0 alone stays NUL
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Merge pull request #84 from GClunies/fix/transfer-ownership-object-type
fix(lifecycle): transfer ownership with the object type GRANT OWNERSHIP accepts
fix(lifecycle): apply all resource field updates (#91)
Fixes #89
Co-authored-by: Noel Gomez noel_gomez@yahoo.com
Honour backslash escapes in the statement splitter and test-account tool
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Escape names in SHOW ... LIKE and scanner-package lookups
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Escape and round-trip TagOnConflictProp strings
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Escape the remaining hand-quoted string literals
Tag values, tag references, FROM URLs, and column DEFAULT/COMMENT now render
through _single_quote. _single_quote rejects lone surrogates, and StringLiteral
decodes \xHH escapes. Closes #92.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Quote ALTER SCHEMA SET values (#70)
- fix(schema): quote ALTER SCHEMA SET values instead of hand-rolling the literal
update_schema built its SQL literal with a bare f"'{new_value}'", so an
apostrophe in the value ended the string early and the rest arrived as bare SQL:
001003 (42000): SQL compilation error: syntax error line 1 at position 39
unexpected 'comment'. on ALTER SCHEMA MY_DB.MY_SCHEMA SET comment = '...
Any schema comment containing an apostrophe therefore fails to apply. Only the
ALTER path is affected -- CREATE already renders through props.quote_value,
which dollar-quotes and needs no escaping. update_schema was the one branch
that hand-rolled its quoting; it now goes through quote_value too.
quote_value itself had the mirror-image gap: $$ is a safe delimiter only while
the value does not contain $$. It now falls back to a single-quoted literal
with ' and backslash doubled in that case, so a value cannot break out by
either route.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(props): escape control characters in the single-quoted fallback
Snowflake needs escape sequences, not raw control characters, inside a
single-quoted literal, so the $$ fallback regressed multiline values.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- chore(props): drop the remaining comment on the $$ fallback
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(props): escape control characters via json in the $$ fallback
The hand-rolled replacement chain covered newline, carriage return and
tab but left backspace, form feed, NUL and the rest of the C0 range raw
in the single-quoted literal. json.dumps escapes the whole range using
the same backslash syntax Snowflake accepts, so the fallback now only
has to double apostrophes on top of it.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(scanner_package): quote SET_CONFIGURATION values with quote_value
Same hand-rolled f"'{new_value}'" as update_schema, same fix. Adds a
test with a " in the $$ fallback and collapses the quote_value tests
into one parametrized case.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
- fix(props): route trailing $ values to single-quoted fallback
A value ending in a single trailing $ combines with the closing $$ to form an early $$ (e.g. �bc$ -> $$), causing a syntax error in Snowflake. Route values ending in $ to the single-quoted fallback alongside values containing $$.
- fix(scanner_package): quote package name and attr with quote_value
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- style: black formatting in scanner package test
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- fix(scanner_package): pass package name to quote_value as str
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- test: cover trailing-$ scanner schedule and \x01 in the $$ fallback
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- fix: route remaining hand-quoted literals through quote_value
Account parameters, DictProp, the TimeTravel STREAM clause and all three
scanner-package calls now use quote_value; the package name is passed
unquoted. quote_value rejects lone surrogates, and StringLiteral decodes
the backslash escapes its fallback emits.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- chore: drop docstring on ResourceName.unquoted
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- fix: keep backslashes in $$ literals and escape EnumProp quoted values
Co-Authored-By: Claude Haiku 5.5 noreply@anthropic.com
Co-authored-by: Claude Opus 5 noreply@anthropic.com
refactor(enums): share one integration type check
Five places tested for integration types with their own copy of
"INTEGRATION" in str(...): three in create_grant, one in the ownership
transfer, and one in the Grant owner default. If one copy changed, the
others could drift without a failing test. They now call
resource_type_is_integration in snowcap/enums.py, next to
resource_type_is_grant.
Behavior does not change.
test(lifecycle): check that types near the mapped ones keep their own ownership names
fix(lifecycle): transfer external function ownership as FUNCTION
GRANT OWNERSHIP has no EXTERNAL FUNCTION object type, so a transfer away
from the default SYSADMIN owner failed with a syntax error. External
functions are granted as FUNCTION.
fix(lifecycle): transfer ownership with the object type GRANT OWNERSHIP accepts
GRANT OWNERSHIP rejects integration subtype names (SECURITY INTEGRATION, STORAGE INTEGRATION, ...), MATERIALIZED VIEW and HYBRID TABLE. Snowflake names them INTEGRATION, VIEW and TABLE. A plan that transferred ownership of any of these failed at apply with a SQL compilation error, for example: GRANT OWNERSHIP ON SECURITY INTEGRATION X TO ROLE ACCOUNTADMIN COPY CURRENT GRANTS.
https://docs.snowflake.com/en/sql-reference/sql/grant-ownership
Merge pull request #83 from datacoves/chore/manual-only-pypi-publish
chore(ci): make PyPI publish manual-only
chore(ci): make PyPI publish manual-only
PyPI publishing is done by hand via workflow_dispatch, not via the
release: published trigger -- 30 of the last 39 runs were manual, and the
maintainer doesn't rely on the automatic path. Dropping the event trigger
removes the ambiguity around whether creating a release in bump-version.yml
needs to count as an external actor.