Skip to content

Commit 03c231c

Browse files
committed
added sql_escape filter
1 parent 65b2a91 commit 03c231c

File tree

9 files changed

+251
-153
lines changed

9 files changed

+251
-153
lines changed

examples/databricks/snowflake-interoperability/resources/databricks_workspace/catalog.iql

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,3 @@ SELECT name as catalog_name
2525
FROM databricks_workspace.unitycatalog.catalogs
2626
WHERE name = '{{ name }}' AND
2727
deployment_name = '{{ databricks_deployment_name }}';
28-
29-
/*+ delete */
30-
DELETE FROM databricks_workspace.unitycatalog.catalogs
31-
WHERE name = '{{ name }}' AND
32-
deployment_name = '{{ deployment_name }}';

examples/databricks/snowflake-interoperability/resources/databricks_workspace/schema.iql

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,3 @@ FROM databricks_workspace.unitycatalog.schemas
2525
WHERE deployment_name = '{{ databricks_deployment_name }}'
2626
AND catalog_name = '{{ catalog_name }}'
2727
AND name = '{{ name }}';
28-
29-
/*+ delete */
30-
DELETE FROM databricks_workspace.unitycatalog.schemas
31-
WHERE full_name = '{{ name }}' AND
32-
deployment_name = '{{ databricks_deployment_name }}';

examples/databricks/snowflake-interoperability/resources/snowflake/statement.iql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ data__warehouse,
99
endpoint
1010
)
1111
SELECT
12-
'{{ statement }}',
12+
'{{ statement | sql_escape }}',
1313
{{ timeout }},
1414
'{{ database }}',
1515
'{{ schema }}',

examples/databricks/snowflake-interoperability/stackql_manifest.yml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,21 @@ resources:
195195
value: stackql
196196
- name: statement
197197
value: |
198-
CREATE OR REPLACE CATALOG INTEGRATION unity_catalog_demo_int
198+
CREATE CATALOG INTEGRATION IF NOT EXISTS unity_catalog_demo_int
199199
CATALOG_SOURCE = ICEBERG_REST
200200
TABLE_FORMAT = ICEBERG
201-
CATALOG_NAMESPACE = ''{{ schema_name }}''
201+
CATALOG_NAMESPACE = '{{ schema_name }}'
202202
REST_CONFIG = (
203-
CATALOG_URI = ''https://{{ databricks_deployment_name }}.cloud.databricks.com/api/2.1/unity-catalog/iceberg-rest''
204-
WAREHOUSE = ''{{ catalog_name }}''
203+
CATALOG_URI = 'https://{{ databricks_deployment_name }}.cloud.databricks.com/api/2.1/unity-catalog/iceberg-rest'
204+
WAREHOUSE = '{{ catalog_name }}'
205205
ACCESS_DELEGATION_MODE = VENDED_CREDENTIALS
206206
)
207207
REST_AUTHENTICATION = (
208208
TYPE = OAUTH
209-
OAUTH_TOKEN_URI = ''https://{{ databricks_deployment_name }}.cloud.databricks.com/oidc/v1/token''
210-
OAUTH_CLIENT_ID = ''{{ service_principal_application_id }}''
211-
OAUTH_CLIENT_SECRET = ''{{ secret }}''
212-
OAUTH_ALLOWED_SCOPES = (''all-apis'', ''sql'')
209+
OAUTH_TOKEN_URI = 'https://{{ databricks_deployment_name }}.cloud.databricks.com/oidc/v1/token'
210+
OAUTH_CLIENT_ID = '{{ service_principal_application_id }}'
211+
OAUTH_CLIENT_SECRET = '{{ secret }}'
212+
OAUTH_ALLOWED_SCOPES = ('all-apis', 'sql')
213213
)
214214
ENABLED = TRUE
215215
REFRESH_INTERVAL_SECONDS = 30
@@ -231,8 +231,8 @@ resources:
231231
- name: statement
232232
value: |
233233
CREATE OR REPLACE ICEBERG TABLE retail_sales_bronze
234-
CATALOG = ''unity_catalog_demo_int''
235-
CATALOG_TABLE_NAME = ''retail_sales_bronze''
234+
CATALOG = 'unity_catalog_demo_int'
235+
CATALOG_TABLE_NAME = 'retail_sales_bronze'
236236
AUTO_REFRESH = TRUE
237237
- name: timeout
238238
value: 30
@@ -252,8 +252,8 @@ resources:
252252
- name: statement
253253
value: |
254254
CREATE OR REPLACE ICEBERG TABLE retail_sales_silver
255-
CATALOG = ''unity_catalog_demo_int''
256-
CATALOG_TABLE_NAME = ''retail_sales_silver''
255+
CATALOG = 'unity_catalog_demo_int'
256+
CATALOG_TABLE_NAME = 'retail_sales_silver'
257257
AUTO_REFRESH = TRUE
258258
- name: timeout
259259
value: 30
@@ -273,8 +273,8 @@ resources:
273273
- name: statement
274274
value: |
275275
CREATE OR REPLACE ICEBERG TABLE retail_sales_gold
276-
CATALOG = ''unity_catalog_demo_int''
277-
CATALOG_TABLE_NAME = ''retail_sales_gold''
276+
CATALOG = 'unity_catalog_demo_int'
277+
CATALOG_TABLE_NAME = 'retail_sales_gold'
278278
AUTO_REFRESH = TRUE
279279
- name: timeout
280280
value: 30

stackql_deploy/cmd/teardown.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,27 @@ def collect_exports(self, show_queries, dry_run):
2323
full_context = get_full_context(self.env, self.global_context, resource, self.logger)
2424

2525
# get resource queries
26-
if type == 'query' and 'sql' in resource:
27-
# inline SQL specified in the resource
28-
test_queries = {}
29-
exports_query = render_inline_template(self.env,
30-
resource["name"],
31-
resource["sql"],
32-
full_context,
33-
self.logger)
34-
exports_retries = 1
35-
exports_retry_delay = 0
36-
else:
37-
test_queries = get_queries(self.env,
38-
self.stack_dir,
39-
'resources',
40-
resource,
41-
full_context,
42-
self.logger)
43-
exports_query = test_queries.get('exports', {}).get('rendered')
44-
exports_retries = test_queries.get('exports', {}).get('options', {}).get('retries', 1)
45-
exports_retry_delay = test_queries.get('exports', {}).get('options', {}).get('retry_delay', 0)
26+
if type != 'command':
27+
if type == 'query' and 'sql' in resource:
28+
# inline SQL specified in the resource
29+
test_queries = {}
30+
exports_query = render_inline_template(self.env,
31+
resource["name"],
32+
resource["sql"],
33+
full_context,
34+
self.logger)
35+
exports_retries = 1
36+
exports_retry_delay = 0
37+
else:
38+
test_queries = get_queries(self.env,
39+
self.stack_dir,
40+
'resources',
41+
resource,
42+
full_context,
43+
self.logger)
44+
exports_query = test_queries.get('exports', {}).get('rendered')
45+
exports_retries = test_queries.get('exports', {}).get('options', {}).get('retries', 1)
46+
exports_retry_delay = test_queries.get('exports', {}).get('options', {}).get('retry_delay', 0)
4647

4748
if exports_query:
4849
self.process_exports(

stackql_deploy/lib/filters.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,25 @@ def sql_list(input_data):
109109
quoted_items = [f"'{str(item)}'" for item in python_list]
110110
return f"({','.join(quoted_items)})"
111111

112+
def sql_escape(value):
113+
"""
114+
Escapes a string for use as a SQL string literal by doubling any single quotes.
115+
This is useful for nested SQL statements where single quotes need to be escaped.
116+
117+
Args:
118+
value: The string to escape
119+
120+
Returns:
121+
The escaped string with single quotes doubled
122+
"""
123+
if value is None:
124+
return None
125+
126+
if not isinstance(value, str):
127+
value = str(value)
128+
129+
return value.replace("'", "''")
130+
112131
#
113132
# exported functions
114133
#
@@ -121,11 +140,12 @@ def setup_environment(stack_dir, logger):
121140
loader=FileSystemLoader(os.getcwd()),
122141
autoescape=False
123142
)
124-
env.filters['merge_lists'] = merge_lists
143+
env.filters['from_json'] = from_json
125144
env.filters['base64_encode'] = base64_encode
145+
env.filters['merge_lists'] = merge_lists
126146
env.filters['generate_patch_document'] = generate_patch_document
127-
env.filters['from_json'] = from_json
128147
env.filters['sql_list'] = sql_list
148+
env.filters['sql_escape'] = sql_escape
129149
env.globals['uuid'] = lambda: str(uuid.uuid4())
130150
logger.debug("custom Jinja filters registered: %s", env.filters.keys())
131151
return env

stackql_deploy/lib/templating.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def render_queries(res_name, env, queries, context, logger):
3838
properties, ensure_ascii=False, separators=(',', ':')
3939
).replace('True', 'true').replace('False', 'false')
4040
# Correctly format JSON to use double quotes and pass directly since template handles quoting
41-
json_str = json_str.replace("'", "\\'") # escape single quotes if any within strings
41+
# json_str = json_str.replace("'", "\\'") # escape single quotes if any within strings
4242
temp_context[ctx_key] = json_str
4343
# No need to alter non-JSON strings, assume the template handles them correctly
4444

@@ -147,7 +147,7 @@ def render_inline_template(env, resource_name, template_string, full_context, lo
147147
properties, ensure_ascii=False, separators=(',', ':')
148148
).replace('True', 'true').replace('False', 'false')
149149
# Correctly format JSON to use double quotes and pass directly since template handles quoting
150-
json_str = json_str.replace("'", "\\'") # escape single quotes if any within strings
150+
# json_str = json_str.replace("'", "\\'") # escape single quotes if any within strings
151151
temp_context[ctx_key] = json_str
152152

153153
# Render the template

website/docs/resource-query-files.md

Lines changed: 8 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -206,113 +206,17 @@ AND project = '{{ project }}'
206206
AND zone = '{{ zone }}'
207207
```
208208

209-
## Template filters
209+
## Template Filters
210210

211-
### `from_json`
212-
`from_json` is a custom `stackql-deploy` filter which is used to convert a `json` string to a python dictionary or list. The common use case is to take advantage of Jinja2 templating within `stackql-deploy` to dynamically generate SQL statements for infrastructure provisioning by converting JSON strings into Python dictionaries or lists, allowing for iteration and more flexible configuration management.
211+
StackQL Deploy leverages Jinja2 templating capabilities and extends them with custom filters for infrastructure provisioning. For a complete reference of all available filters, see the [__Template Filters__](template-filters) documentation.
213212

214-
```sql {2}
215-
/*+ create */
216-
{% for network_interface in network_interfaces | from_json %}
217-
INSERT INTO google.compute.instances
218-
(
219-
zone,
220-
project,
221-
data__name,
222-
data__machineType,
223-
data__canIpForward,
224-
data__deletionProtection,
225-
data__scheduling,
226-
data__networkInterfaces,
227-
data__disks,
228-
data__serviceAccounts,
229-
data__tags
230-
)
231-
SELECT
232-
'{{ default_zone }}',
233-
'{{ project }}',
234-
'{{ instance_name_prefix }}-{{ loop.index }}',
235-
'{{ machine_type }}',
236-
true,
237-
false,
238-
'{{ scheduling }}',
239-
'[ {{ network_interface | tojson }} ]',
240-
'{{ disks }}',
241-
'{{ service_accounts }}',
242-
'{{ tags }}';
243-
{% endfor %}
244-
```
245-
246-
### `tojson`
247-
`tojson` is a built-in Jinja2 filter to convert a Python dictionary or list into a `json` string. This may be required if you have used the `from_json` filter as shown here:
248-
249-
```sql {25}
250-
/*+ create */
251-
{% for network_interface in network_interfaces | from_json %}
252-
INSERT INTO google.compute.instances
253-
(
254-
zone,
255-
project,
256-
data__name,
257-
data__machineType,
258-
data__canIpForward,
259-
data__deletionProtection,
260-
data__scheduling,
261-
data__networkInterfaces,
262-
data__disks,
263-
data__serviceAccounts,
264-
data__tags
265-
)
266-
SELECT
267-
'{{ default_zone }}',
268-
'{{ project }}',
269-
'{{ instance_name_prefix }}-{{ loop.index }}',
270-
'{{ machine_type }}',
271-
true,
272-
false,
273-
'{{ scheduling }}',
274-
'[ {{ network_interface | tojson }} ]',
275-
'{{ disks }}',
276-
'{{ service_accounts }}',
277-
'{{ tags }}';
278-
{% endfor %}
279-
```
280-
281-
### `generate_patch_document`
282-
283-
`generate_patch_document` is a custom `stackql-deploy` filter which generates a patch document for the given resource according to https://datatracker.ietf.org/doc/html/rfc6902, this is designed for the AWS Cloud Control API, which requires a patch document to update resources. An example of this filter used to update the `NotificationConfiguration` for an existing AWS bucket is shown here:
284-
285-
```sql {3-5}
286-
/*+ createorupdate */
287-
update aws.s3.buckets
288-
set data__PatchDocument = string('{{ {
289-
"NotificationConfiguration": transfer_notification_config
290-
} | generate_patch_document }}')
291-
WHERE
292-
region = '{{ region }}'
293-
AND data__Identifier = '{{ transfer_bucket_name }}';
294-
```
295-
296-
### `base64_encode`
213+
Here are a few commonly used filters:
297214

298-
`base64_encode` is a custom `stackql-deploy` filter used to generate a `base64` encoded value for a given input string, this is often specified as an input requirement for a free text field in a resource. This example shows how to `base64` encode the `UserData` field for an AWS EC2 instance:
299-
300-
```sql {13}
301-
/*+ create */
302-
INSERT INTO aws.ec2.instances (
303-
ImageId,
304-
InstanceType,
305-
SubnetId,
306-
UserData,
307-
region
308-
)
309-
SELECT
310-
'{{ ami_id }}',
311-
'{{ instance_type }}',
312-
'{{ instance_subnet_id }}',
313-
'{{ user_data | base64_encode }}',
314-
'{{ region }}';
315-
```
215+
- `from_json` - Converts JSON strings to Python objects for iteration and manipulation
216+
- `tojson` - Converts Python objects back to JSON strings
217+
- `sql_escape` - Properly escapes SQL string literals for nested SQL statements
218+
- `generate_patch_document` - Creates RFC6902-compliant patch documents for AWS resources
219+
- `base64_encode` - Encodes strings as base64 for API fields requiring binary data
316220

317221
## Examples
318222

0 commit comments

Comments
 (0)