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+
115import pytest
2- from google .cloud .bigquery .schema import SchemaField
316
4- from sqlalchemy_bigquery ._types import _get_transitive_schema_fields , STRUCT_FIELD_TYPES
17+ from sqlalchemy_bigquery ._types import _get_transitive_schema_fields
18+ from google .cloud .bigquery .schema import SchemaField
519
620
7- def create_fut ( name , field_type , mode = "NULLABLE" , sub_fields = None ):
21+ def create_schema_field_from_dict ( schema_dict ):
822 """
9- Helper function to create a SchemaField object for testing.
10- `sub_fields` should be a list of already created SchemaField objects.
23+ Helper function to create a SchemaField object from a dictionary representation.
1124 """
1225 api_repr = {
13- "name" : name ,
14- "type" : field_type ,
15- "mode" : mode ,
16- "fields" : [sf .to_api_repr () for sf in sub_fields ] if sub_fields else [],
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+ ],
1733 }
1834 return SchemaField .from_api_repr (api_repr )
1935
@@ -22,75 +38,105 @@ def create_fut(name, field_type, mode="NULLABLE", sub_fields=None):
2238 (
2339 "STRUCT field, not REPEATED, with sub-fields, should recurse" ,
2440 [
25- create_fut (
26- "s1" ,
27- "STRUCT" ,
28- "NULLABLE" ,
29- sub_fields = [create_fut ("child1" , "STRING" , "NULLABLE" )],
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+ }
3050 )
3151 ],
3252 ["s1" , "s1.child1" ],
3353 ),
3454 (
3555 "RECORD field (alias for STRUCT), not REPEATED, with sub-fields, should recurse" ,
3656 [
37- create_fut (
38- "r1" ,
39- "RECORD" ,
40- "NULLABLE" ,
41- sub_fields = [create_fut ("child_r1" , "INTEGER" , "NULLABLE" )],
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+ }
4266 )
4367 ],
4468 ["r1" , "r1.child_r1" ],
4569 ),
4670 (
4771 "STRUCT field, REPEATED, with sub-fields, should NOT recurse" ,
4872 [
49- create_fut (
50- "s2" ,
51- "STRUCT" ,
52- "REPEATED" ,
53- sub_fields = [create_fut ("child2" , "STRING" , "NULLABLE" )],
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+ }
5482 )
5583 ],
5684 ["s2" ],
5785 ),
5886 (
5987 "Non-STRUCT field (STRING), not REPEATED, should NOT recurse" ,
60- [create_fut ("f1" , "STRING" , "NULLABLE" )],
88+ [
89+ create_schema_field_from_dict (
90+ {"name" : "f1" , "type" : "STRING" , "mode" : "NULLABLE" }
91+ )
92+ ],
6193 ["f1" ],
6294 ),
6395 (
6496 "Non-STRUCT field (INTEGER), REPEATED, should NOT recurse" ,
65- [create_fut ("f2" , "INTEGER" , "REPEATED" )],
97+ [
98+ create_schema_field_from_dict (
99+ {"name" : "f2" , "type" : "INTEGER" , "mode" : "REPEATED" }
100+ )
101+ ],
66102 ["f2" ],
67103 ),
68104 (
69105 "Deeply nested STRUCT, not REPEATED, should recurse fully" ,
70106 [
71- create_fut (
72- "s_outer" ,
73- "STRUCT" ,
74- "NULLABLE" ,
75- sub_fields = [
76- create_fut (
77- "s_inner1" ,
78- "STRUCT" ,
79- "NULLABLE" ,
80- sub_fields = [create_fut ("s_leaf1" , "STRING" , "NULLABLE" )],
81- ),
82- create_fut ("s_sibling" , "INTEGER" , "NULLABLE" ),
83- create_fut (
84- "s_inner2_repeated_struct" ,
85- "STRUCT" ,
86- "REPEATED" ,
87- sub_fields = [
88- create_fut (
89- "s_leaf2_ignored" , "BOOLEAN" , "NULLABLE"
90- ) # This sub-field should be ignored
91- ],
92- ),
93- ],
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+ }
94140 )
95141 ],
96142 [
@@ -103,35 +149,45 @@ def create_fut(name, field_type, mode="NULLABLE", sub_fields=None):
103149 ),
104150 (
105151 "STRUCT field, not REPEATED, but no sub-fields, should not error and not recurse further" ,
106- [create_fut ("s3" , "STRUCT" , "NULLABLE" , sub_fields = [])],
152+ [
153+ create_schema_field_from_dict (
154+ {"name" : "s3" , "type" : "STRUCT" , "mode" : "NULLABLE" , "fields" : []}
155+ )
156+ ],
107157 ["s3" ],
108158 ),
109159 (
110160 "Multiple top-level fields with mixed conditions" ,
111161 [
112- create_fut ("id" , "INTEGER" , "REQUIRED" ),
113- create_fut (
114- "user_profile" ,
115- "STRUCT" ,
116- "NULLABLE" ,
117- sub_fields = [
118- create_fut ("name" , "STRING" , "NULLABLE" ),
119- create_fut (
120- "addresses" ,
121- "RECORD" ,
122- "REPEATED" ,
123- sub_fields = [ # addresses is REPEATED STRUCT
124- create_fut (
125- "street" , "STRING" , "NULLABLE"
126- ), # This sub-field should be ignored
127- create_fut (
128- "city" , "STRING" , "NULLABLE"
129- ), # This sub-field should be ignored
130- ],
131- ),
132- ],
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" }
133190 ),
134- create_fut ("tags" , "STRING" , "REPEATED" ),
135191 ],
136192 ["id" , "user_profile" , "user_profile.name" , "user_profile.addresses" , "tags" ],
137193 ),
@@ -142,12 +198,20 @@ def create_fut(name, field_type, mode="NULLABLE", sub_fields=None):
142198 ),
143199 (
144200 "Field type not in STRUCT_FIELD_TYPES and mode is REPEATED" ,
145- [create_fut ("f_arr" , "FLOAT" , "REPEATED" )],
201+ [
202+ create_schema_field_from_dict (
203+ {"name" : "f_arr" , "type" : "FLOAT" , "mode" : "REPEATED" }
204+ )
205+ ],
146206 ["f_arr" ],
147207 ),
148208 (
149209 "Field type not in STRUCT_FIELD_TYPES and mode is not REPEATED" ,
150- [create_fut ("f_single" , "DATE" , "NULLABLE" )],
210+ [
211+ create_schema_field_from_dict (
212+ {"name" : "f_single" , "type" : "DATE" , "mode" : "NULLABLE" }
213+ )
214+ ],
151215 ["f_single" ],
152216 ),
153217]
0 commit comments