Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions snowcap/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .builder import tidy_sql
from .enums import GrantType, ResourceType
from .identifiers import FQN, URN
from .props import BoolProp, IntProp, Props, StringProp
from .props import BoolProp, IntProp, Props, StringProp, quote_value
from .resource_name import ResourceName

__this__ = sys.modules[__name__]
Expand Down Expand Up @@ -541,7 +541,7 @@ def update_schema(urn: URN, data: dict, props: Props) -> str:
elif attr == "managed_access":
return tidy_sql("ALTER SCHEMA", urn.fqn, "ENABLE" if new_value else "DISABLE", "MANAGED ACCESS")
else:
new_value = f"'{new_value}'" if isinstance(new_value, str) else new_value
new_value = quote_value(new_value) if isinstance(new_value, str) else new_value
return tidy_sql("ALTER SCHEMA", urn.fqn, "SET", attr, "=", new_value)


Expand Down
4 changes: 4 additions & 0 deletions snowcap/props.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
def quote_value(value: str):
if value is None or value == "":
return "''"
if "$$" in str(value):
# JSON and Snowflake share backslash escape syntax for control characters
escaped = json.dumps(str(value), ensure_ascii=False)[1:-1].replace("'", "''")
return f"'{escaped}'"
return f"$${value}$$"


Expand Down
16 changes: 16 additions & 0 deletions tests/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,22 @@ def test_set_other_property(self):
result = update_schema(urn, data, props)
assert "SET data_retention_time_in_days = 7" in result

def test_set_comment_with_apostrophe(self):
"""A comment containing an apostrophe must not break out of the literal."""
urn = make_urn(ResourceType.SCHEMA, "MY_SCHEMA", database="MY_DB")
data = {"comment": "the database's two-limb test"}
props = MockProps("")
result = update_schema(urn, data, props)
assert result == "ALTER SCHEMA MY_DB.MY_SCHEMA SET comment = $$the database's two-limb test$$"

def test_set_comment_containing_dollar_quote(self):
"""A comment containing $$ falls back to a single-quoted literal."""
urn = make_urn(ResourceType.SCHEMA, "MY_SCHEMA", database="MY_DB")
data = {"comment": "costs $$ and it's dear"}
props = MockProps("")
result = update_schema(urn, data, props)
assert result == "ALTER SCHEMA MY_DB.MY_SCHEMA SET comment = 'costs $$ and it''s dear'"


class TestUpdateTable:
"""Tests for update_table function."""
Expand Down
24 changes: 24 additions & 0 deletions tests/test_props.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@ def test_quote_value_multiline(self):
result = quote_value("line1\nline2")
assert result == "$$line1\nline2$$"

def test_quote_value_containing_dollar_quote(self):
result = quote_value("costs $$ and it's dear")
assert result == "'costs $$ and it''s dear'"

def test_quote_value_containing_dollar_quote_and_backslash(self):
result = quote_value("$$ path C:\\tmp")
assert result == "'$$ path C:\\\\tmp'"

def test_quote_value_containing_dollar_quote_and_newline(self):
result = quote_value("costs $$\nper line")
assert result == "'costs $$\\nper line'"

def test_quote_value_containing_dollar_quote_and_carriage_return_tab(self):
result = quote_value("$$\r\tx")
assert result == "'$$\\r\\tx'"

def test_quote_value_containing_dollar_quote_and_backspace_form_feed(self):
result = quote_value("$$\b\fx")
assert result == "'$$\\b\\fx'"

def test_quote_value_containing_dollar_quote_and_nul(self):
result = quote_value("$$\0x")
assert result == "'$$\\u0000x'"


class TestBoolPropExtended:
"""Extended tests for BoolProp class."""
Expand Down