Skip to content

Commit 0de0347

Browse files
authored
[AIRFLOW-6855]: Escape project_dataset_table in SQL query in gcs to bq … (#7475)
* [AIRFLOW-6855]: Escape project_dataset_table in SQL query in gcs to bq operator Without escaping, if the project is specified in project_dataset_table and contains a -, the query will fail with an error. * Make string formatting in gcs_to_bigquery f-strings, add unit tests. * pylint appeasement, hopefully. * More not understanding how mocking works in python... * Add task ids to tests, remove tst_gcs_to_bigquery.py as a missing test file. * maybe more correct mocking of class vairables.
1 parent 91557c6 commit 0de0347

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

airflow/providers/google/cloud/operators/gcs_to_bigquery.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,15 @@ def execute(self, context):
298298
cluster_fields=self.cluster_fields,
299299
encryption_configuration=self.encryption_configuration)
300300

301+
if cursor.use_legacy_sql:
302+
escaped_table_name = f'[{self.destination_project_dataset_table}]'
303+
else:
304+
escaped_table_name = f'`{self.destination_project_dataset_table}`'
305+
301306
if self.max_id_key:
302307
cursor.execute('SELECT MAX({}) FROM {}'.format(
303308
self.max_id_key,
304-
self.destination_project_dataset_table))
309+
escaped_table_name))
305310
row = cursor.fetchone()
306311
max_id = row[0] if row[0] else 0
307312
self.log.info(
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
import unittest
20+
21+
import mock
22+
23+
from airflow.providers.google.cloud.operators.gcs_to_bigquery import GCSToBigQueryOperator
24+
25+
TASK_ID = 'test-gcs-to-bq-operator'
26+
TEST_EXPLICIT_DEST = 'test-project.dataset.table'
27+
TEST_BUCKET = 'test-bucket'
28+
MAX_ID_KEY = 'id'
29+
TEST_SOURCE_OBJECTS = ['test/objects/*']
30+
31+
32+
class TestGoogleCloudStorageToBigQueryOperator(unittest.TestCase):
33+
34+
@mock.patch('airflow.providers.google.cloud.operators.gcs_to_bigquery.BigQueryHook')
35+
def test_execute_explicit_project_legacy(self, bq_hook):
36+
operator = GCSToBigQueryOperator(task_id=TASK_ID,
37+
bucket=TEST_BUCKET,
38+
source_objects=TEST_SOURCE_OBJECTS,
39+
destination_project_dataset_table=TEST_EXPLICIT_DEST,
40+
max_id_key=MAX_ID_KEY)
41+
42+
# using legacy SQL
43+
bq_hook.return_value.get_conn.return_value.cursor.return_value.use_legacy_sql = True
44+
45+
operator.execute(None)
46+
47+
bq_hook.return_value \
48+
.get_conn.return_value \
49+
.cursor.return_value \
50+
.execute \
51+
.assert_called_once_with("SELECT MAX(id) FROM [test-project.dataset.table]")
52+
53+
@mock.patch('airflow.providers.google.cloud.operators.gcs_to_bigquery.BigQueryHook')
54+
def test_execute_explicit_project(self, bq_hook):
55+
operator = GCSToBigQueryOperator(task_id=TASK_ID,
56+
bucket=TEST_BUCKET,
57+
source_objects=TEST_SOURCE_OBJECTS,
58+
destination_project_dataset_table=TEST_EXPLICIT_DEST,
59+
max_id_key=MAX_ID_KEY)
60+
61+
# using non-legacy SQL
62+
bq_hook.return_value.get_conn.return_value.cursor.return_value.use_legacy_sql = False
63+
64+
operator.execute(None)
65+
66+
bq_hook.return_value \
67+
.get_conn.return_value \
68+
.cursor.return_value \
69+
.execute \
70+
.assert_called_once_with("SELECT MAX(id) FROM `test-project.dataset.table`")

tests/test_project_structure.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
'tests/providers/apache/spark/hooks/test_spark_jdbc_script.py',
3636
'tests/providers/cncf/kubernetes/operators/test_kubernetes_pod.py',
3737
'tests/providers/google/cloud/operators/test_datastore.py',
38-
'tests/providers/google/cloud/operators/test_gcs_to_bigquery.py',
3938
'tests/providers/google/cloud/operators/test_sql_to_gcs.py',
4039
'tests/providers/google/cloud/sensors/test_bigquery.py',
4140
'tests/providers/google/cloud/utils/test_field_sanitizer.py',

0 commit comments

Comments
 (0)