33import logging
44from unittest .mock import Mock , patch
55from pyobvector .schema .reflection import OceanBaseTableDefinitionParser
6+ from sqlalchemy .dialects .mysql .reflection import MySQLTableDefinitionParser
7+ import copy # Added for deepcopy
68
79logger = logging .getLogger (__name__ )
810
@@ -37,7 +39,7 @@ def test_dialect(self):
3739 self .engine = create_async_engine (connection_str )
3840
3941 def test_parse_constraints_with_string_spec (self ):
40- # test the logic directly
42+ """Test that _parse_constraints handles string spec gracefully without crashing."""
4143 from pyobvector .schema .reflection import OceanBaseTableDefinitionParser
4244
4345 # Create a mock parser class to test our specific method
@@ -48,52 +50,141 @@ def __init__(self):
4850
4951 parser = MockParser ()
5052
51- # Test cases for different spec types
53+ # Test cases: we'll mock the parent method to return what we want to test
5254 test_cases = [
53- # Case 1: spec is a string (this was causing the bug)
54- ("fk_constraint" , "some_string_spec" ),
55- # Case 2: spec is a dict with onupdate/ondelete = "restrict"
56- ("fk_constraint" , {
57- "table" : ["test" , "other_table" ],
58- "onupdate" : "restrict" ,
59- "ondelete" : "restrict"
60- }),
61- # Case 3: spec is a dict with onupdate/ondelete = "cascade"
62- ("fk_constraint" , {
63- "table" : ["other_table" ],
64- "onupdate" : "cascade" ,
65- "ondelete" : "cascade"
66- }),
67- # Case 4: spec is a dict without onupdate/ondelete
68- ("fk_constraint" , {
69- "table" : ["other_table" ],
70- "name" : "fk_test"
71- }),
72- # Case 5: spec is None (edge case)
73- ("fk_constraint" , None ),
55+ {
56+ "name" : "String spec (the bug case)" ,
57+ "parent_return" : ("fk_constraint" , "some_string_spec" ),
58+ "expected_result" : ("fk_constraint" , "some_string_spec" ) # Should remain unchanged
59+ },
60+ {
61+ "name" : "Dict spec with restrict values" ,
62+ "parent_return" : ("fk_constraint" , {
63+ "table" : ["test" , "other_table" ],
64+ "onupdate" : "restrict" ,
65+ "ondelete" : "restrict"
66+ }),
67+ "expected_result" : ("fk_constraint" , {
68+ "table" : ["other_table" ], # Should be trimmed
69+ "onupdate" : None , # Should be None
70+ "ondelete" : None # Should be None
71+ })
72+ },
73+ {
74+ "name" : "Dict spec with cascade values" ,
75+ "parent_return" : ("fk_constraint" , {
76+ "table" : ["other_table" ],
77+ "onupdate" : "cascade" ,
78+ "ondelete" : "cascade"
79+ }),
80+ "expected_result" : ("fk_constraint" , {
81+ "table" : ["other_table" ],
82+ "onupdate" : "cascade" , # Should remain unchanged
83+ "ondelete" : "cascade" # Should remain unchanged
84+ })
85+ },
86+ {
87+ "name" : "Dict spec with None values" ,
88+ "parent_return" : ("fk_constraint" , {
89+ "table" : ["other_table" ],
90+ "onupdate" : None ,
91+ "ondelete" : None
92+ }),
93+ "expected_result" : ("fk_constraint" , {
94+ "table" : ["other_table" ],
95+ "onupdate" : None , # Should remain None
96+ "ondelete" : None # Should remain None
97+ })
98+ },
99+ {
100+ "name" : "Dict spec without table key" ,
101+ "parent_return" : ("fk_constraint" , {
102+ "onupdate" : "restrict" ,
103+ "ondelete" : "cascade"
104+ }),
105+ "expected_result" : ("fk_constraint" , {
106+ "onupdate" : None , # Should be None
107+ "ondelete" : "cascade" # Should remain unchanged
108+ })
109+ },
110+ {
111+ "name" : "Dict spec with single table (no trimming)" ,
112+ "parent_return" : ("fk_constraint" , {
113+ "table" : ["other_table" ],
114+ "onupdate" : "restrict"
115+ }),
116+ "expected_result" : ("fk_constraint" , {
117+ "table" : ["other_table" ], # Should remain unchanged (only 1 element)
118+ "onupdate" : None # Should be None
119+ })
120+ },
121+ {
122+ "name" : "Dict spec with empty dict" ,
123+ "parent_return" : ("fk_constraint" , {}),
124+ "expected_result" : ("fk_constraint" , {}) # Should remain unchanged
125+ },
126+ {
127+ "name" : "Dict spec with None table" ,
128+ "parent_return" : ("fk_constraint" , {
129+ "table" : None ,
130+ "onupdate" : "restrict"
131+ }),
132+ "expected_result" : ("fk_constraint" , {
133+ "table" : None , # Should remain unchanged (not a list)
134+ "onupdate" : None # Should be None
135+ })
136+ },
137+ {
138+ "name" : "Dict spec with non-list table" ,
139+ "parent_return" : ("fk_constraint" , {
140+ "table" : "not_a_list" ,
141+ "ondelete" : "restrict"
142+ }),
143+ "expected_result" : ("fk_constraint" , {
144+ "table" : "not_a_list" , # Should remain unchanged (not a list)
145+ "ondelete" : None # Should be None
146+ })
147+ },
148+ {
149+ "name" : "None spec" ,
150+ "parent_return" : ("fk_constraint" , None ),
151+ "expected_result" : ("fk_constraint" , None ) # Should remain unchanged
152+ },
153+ {
154+ "name" : "Non-fk constraint with string spec" ,
155+ "parent_return" : ("unique" , "string_spec" ),
156+ "expected_result" : ("unique" , "string_spec" ) # Should remain unchanged
157+ }
74158 ]
75159
76- for tp , spec in test_cases :
77- with self .subTest (tp = tp , spec = spec ):
78- # Mock the parent class method to return our test case
79- with patch .object (parser .__class__ .__bases__ [0 ], '_parse_constraints' , return_value = (tp , spec )):
80- # This should not raise an exception
160+ for test_case in test_cases :
161+ with self .subTest (name = test_case ["name" ]):
162+ # Create a copy of the input to avoid mutation issues
163+ import copy
164+ parent_return = copy .deepcopy (test_case ["parent_return" ])
165+ expected_result = test_case ["expected_result" ]
166+
167+ # Mock the parent class method to return our test input
168+ with patch .object (MySQLTableDefinitionParser , '_parse_constraints' , return_value = parent_return ):
169+ # Call our method - this should apply our bugfix logic
81170 result = parser ._parse_constraints ("dummy line" )
82171
83172 # Verify the result
84- if result :
85- result_tp , result_spec = result
86- self .assertEqual (result_tp , tp )
87-
88- # If spec was a dict with "restrict" values, they should be None now
89- if isinstance (spec , dict ):
90- if spec .get ("onupdate" ) == "restrict" :
91- self .assertIsNone (result_spec .get ("onupdate" ))
92- if spec .get ("ondelete" ) == "restrict" :
93- self .assertIsNone (result_spec .get ("ondelete" ))
94- else :
95- # If spec was not a dict, it should remain unchanged
96- self .assertEqual (result_spec , spec )
173+ self .assertIsNotNone (result )
174+ result_tp , result_spec = result
175+ expected_tp , expected_spec = expected_result
176+
177+ self .assertEqual (result_tp , expected_tp )
178+
179+ # For detailed comparison
180+ if isinstance (expected_spec , dict ) and isinstance (result_spec , dict ):
181+ for key , expected_value in expected_spec .items ():
182+ actual_value = result_spec .get (key )
183+ self .assertEqual (actual_value , expected_value ,
184+ f"Test '{ test_case ['name' ]} ': Key '{ key } ' expected { expected_value } , got { actual_value } " )
185+ else :
186+ self .assertEqual (result_spec , expected_spec ,
187+ f"Test '{ test_case ['name' ]} ': Expected { expected_spec } , got { result_spec } " )
97188
98189 def test_parse_constraints_string_spec_no_crash (self ):
99190 """Specific test to ensure string spec doesn't cause AttributeError."""
@@ -108,7 +199,7 @@ def __init__(self):
108199 parser = MockParser ()
109200
110201 # Mock parent method to return string spec (the problematic case)
111- with patch .object (parser . __class__ . __bases__ [ 0 ] , '_parse_constraints' , return_value = ("fk_constraint" , "string_spec" )):
202+ with patch .object (MySQLTableDefinitionParser , '_parse_constraints' , return_value = ("fk_constraint" , "string_spec" )):
112203 # This should not raise AttributeError: 'str' object has no attribute 'get'
113204 try :
114205 result = parser ._parse_constraints ("dummy line" )
0 commit comments