Skip to content

Commit 16712da

Browse files
fix: updates _get_transitive_schema_fields and tests (#1200)
* fix: updates _get_transitive_schema_fields and tests * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * refactor of the test_cases --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent ba6c7ac commit 16712da

File tree

2 files changed

+233
-1
lines changed

2 files changed

+233
-1
lines changed

sqlalchemy_bigquery/_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _get_transitive_schema_fields(fields):
8383
results = []
8484
for field in fields:
8585
results += [field]
86-
if field.field_type in STRUCT_FIELD_TYPES:
86+
if field.field_type in STRUCT_FIELD_TYPES and field.mode != "REPEATED":
8787
sub_fields = [
8888
SchemaField.from_api_repr(
8989
dict(f.to_api_repr(), name=f"{field.name}.{f.name}")

tests/unit/test__types.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
17+
from sqlalchemy_bigquery._types import _get_transitive_schema_fields
18+
from google.cloud.bigquery.schema import SchemaField
19+
20+
21+
def create_schema_field_from_dict(schema_dict):
22+
"""
23+
Helper function to create a SchemaField object from a dictionary representation.
24+
"""
25+
api_repr = {
26+
"name": schema_dict["name"],
27+
"type": schema_dict["type"],
28+
"mode": schema_dict.get("mode", "NULLABLE"),
29+
"fields": [
30+
create_schema_field_from_dict(sf_dict).to_api_repr()
31+
for sf_dict in schema_dict.get("fields", [])
32+
],
33+
}
34+
return SchemaField.from_api_repr(api_repr)
35+
36+
37+
test_cases = [
38+
(
39+
"STRUCT field, not REPEATED, with sub-fields, should recurse",
40+
[
41+
create_schema_field_from_dict(
42+
{
43+
"name": "s1",
44+
"type": "STRUCT",
45+
"mode": "NULLABLE",
46+
"fields": [
47+
{"name": "child1", "type": "STRING", "mode": "NULLABLE"}
48+
],
49+
}
50+
)
51+
],
52+
["s1", "s1.child1"],
53+
),
54+
(
55+
"RECORD field (alias for STRUCT), not REPEATED, with sub-fields, should recurse",
56+
[
57+
create_schema_field_from_dict(
58+
{
59+
"name": "r1",
60+
"type": "RECORD",
61+
"mode": "NULLABLE",
62+
"fields": [
63+
{"name": "child_r1", "type": "INTEGER", "mode": "NULLABLE"}
64+
],
65+
}
66+
)
67+
],
68+
["r1", "r1.child_r1"],
69+
),
70+
(
71+
"STRUCT field, REPEATED, with sub-fields, should NOT recurse",
72+
[
73+
create_schema_field_from_dict(
74+
{
75+
"name": "s2",
76+
"type": "STRUCT",
77+
"mode": "REPEATED",
78+
"fields": [
79+
{"name": "child2", "type": "STRING", "mode": "NULLABLE"}
80+
],
81+
}
82+
)
83+
],
84+
["s2"],
85+
),
86+
(
87+
"Non-STRUCT field (STRING), not REPEATED, should NOT recurse",
88+
[
89+
create_schema_field_from_dict(
90+
{"name": "f1", "type": "STRING", "mode": "NULLABLE"}
91+
)
92+
],
93+
["f1"],
94+
),
95+
(
96+
"Non-STRUCT field (INTEGER), REPEATED, should NOT recurse",
97+
[
98+
create_schema_field_from_dict(
99+
{"name": "f2", "type": "INTEGER", "mode": "REPEATED"}
100+
)
101+
],
102+
["f2"],
103+
),
104+
(
105+
"Deeply nested STRUCT, not REPEATED, should recurse fully",
106+
[
107+
create_schema_field_from_dict(
108+
{
109+
"name": "s_outer",
110+
"type": "STRUCT",
111+
"mode": "NULLABLE",
112+
"fields": [
113+
{
114+
"name": "s_inner1",
115+
"type": "STRUCT",
116+
"mode": "NULLABLE",
117+
"fields": [
118+
{
119+
"name": "s_leaf1",
120+
"type": "STRING",
121+
"mode": "NULLABLE",
122+
}
123+
],
124+
},
125+
{"name": "s_sibling", "type": "INTEGER", "mode": "NULLABLE"},
126+
{
127+
"name": "s_inner2_repeated_struct",
128+
"type": "STRUCT",
129+
"mode": "REPEATED",
130+
"fields": [
131+
{
132+
"name": "s_leaf2_ignored",
133+
"type": "BOOLEAN",
134+
"mode": "NULLABLE",
135+
}
136+
],
137+
},
138+
],
139+
}
140+
)
141+
],
142+
[
143+
"s_outer",
144+
"s_outer.s_inner1",
145+
"s_outer.s_inner1.s_leaf1",
146+
"s_outer.s_sibling",
147+
"s_outer.s_inner2_repeated_struct",
148+
],
149+
),
150+
(
151+
"STRUCT field, not REPEATED, but no sub-fields, should not error and not recurse further",
152+
[
153+
create_schema_field_from_dict(
154+
{"name": "s3", "type": "STRUCT", "mode": "NULLABLE", "fields": []}
155+
)
156+
],
157+
["s3"],
158+
),
159+
(
160+
"Multiple top-level fields with mixed conditions",
161+
[
162+
create_schema_field_from_dict(
163+
{"name": "id", "type": "INTEGER", "mode": "REQUIRED"}
164+
),
165+
create_schema_field_from_dict(
166+
{
167+
"name": "user_profile",
168+
"type": "STRUCT",
169+
"mode": "NULLABLE",
170+
"fields": [
171+
{"name": "name", "type": "STRING", "mode": "NULLABLE"},
172+
{
173+
"name": "addresses",
174+
"type": "RECORD",
175+
"mode": "REPEATED",
176+
"fields": [
177+
{
178+
"name": "street",
179+
"type": "STRING",
180+
"mode": "NULLABLE",
181+
},
182+
{"name": "city", "type": "STRING", "mode": "NULLABLE"},
183+
],
184+
},
185+
],
186+
}
187+
),
188+
create_schema_field_from_dict(
189+
{"name": "tags", "type": "STRING", "mode": "REPEATED"}
190+
),
191+
],
192+
["id", "user_profile", "user_profile.name", "user_profile.addresses", "tags"],
193+
),
194+
(
195+
"Empty input list of fields",
196+
[],
197+
[],
198+
),
199+
(
200+
"Field type not in STRUCT_FIELD_TYPES and mode is REPEATED",
201+
[
202+
create_schema_field_from_dict(
203+
{"name": "f_arr", "type": "FLOAT", "mode": "REPEATED"}
204+
)
205+
],
206+
["f_arr"],
207+
),
208+
(
209+
"Field type not in STRUCT_FIELD_TYPES and mode is not REPEATED",
210+
[
211+
create_schema_field_from_dict(
212+
{"name": "f_single", "type": "DATE", "mode": "NULLABLE"}
213+
)
214+
],
215+
["f_single"],
216+
),
217+
]
218+
219+
220+
@pytest.mark.parametrize(
221+
"description, input_fields_list, expected_field_names", test_cases
222+
)
223+
def test_get_transitive_schema_fields_conditions(
224+
description, input_fields_list, expected_field_names
225+
):
226+
"""
227+
Tests the _get_transitive_schema_fields function, focusing on the conditional logic
228+
`if field.field_type in STRUCT_FIELD_TYPES and field.mode != "REPEATED":`.
229+
"""
230+
result_fields = _get_transitive_schema_fields(input_fields_list)
231+
result_names = [field.name for field in result_fields]
232+
assert result_names == expected_field_names, description

0 commit comments

Comments
 (0)