@@ -125,6 +125,92 @@ def test_cursor_executemany(mocked_connection):
125125 assert response ["results" ] == result
126126
127127
128+ def test_executemany_with_named_params (mocked_connection ):
129+ """
130+ Verify that executemany() translates pyformat %(name)s placeholders to
131+ positional $N markers and converts each dict row to a positional list.
132+
133+ """
134+ response = {
135+ "col_types" : [],
136+ "cols" : [],
137+ "duration" : 123 ,
138+ "results" : [{"rowcount" : 1 }, {"rowcount" : 1 }],
139+ }
140+ with mock .patch .object (
141+ mocked_connection .client , "sql" , return_value = response
142+ ):
143+ cursor = mocked_connection .cursor ()
144+ cursor .executemany (
145+ "INSERT INTO characters (name, age) VALUES (%(name)s, %(age)s)" ,
146+ [
147+ {"name" : "Arthur" , "age" : 42 },
148+ {"name" : "Bill" , "age" : 35 },
149+ ],
150+ )
151+ sql , _params , bulk_args = mocked_connection .client .sql .call_args [0 ]
152+ assert sql == "INSERT INTO characters (name, age) VALUES ($1, $2)"
153+ assert bulk_args == [["Arthur" , 42 ], ["Bill" , 35 ]]
154+
155+
156+ def test_executemany_with_named_params_missing_key (mocked_connection ):
157+ """
158+ Verify that executemany() raises ProgrammingError when a row is missing a
159+ key that appears as a placeholder in the SQL.
160+ """
161+ cursor = mocked_connection .cursor ()
162+ with pytest .raises (
163+ ProgrammingError , match = "Named parameter 'age' not found"
164+ ):
165+ cursor .executemany (
166+ "INSERT INTO characters (name, age) VALUES (%(name)s, %(age)s)" ,
167+ [
168+ {"name" : "Arthur" , "age" : 42 },
169+ {"name" : "Bill" }, # missing 'age'
170+ ],
171+ )
172+ mocked_connection .client .sql .assert_not_called ()
173+
174+
175+ def test_executemany_with_named_params_repeated (mocked_connection ):
176+ """
177+ Verify that a placeholder name used multiple times in the SQL maps to the
178+ same $N position in every occurrence, and the value appears only once in
179+ each row's positional list.
180+ """
181+ response = {
182+ "col_types" : [],
183+ "cols" : [],
184+ "duration" : 123 ,
185+ "results" : [{"rowcount" : 1 }, {"rowcount" : 1 }],
186+ }
187+ with mock .patch .object (
188+ mocked_connection .client , "sql" , return_value = response
189+ ):
190+ cursor = mocked_connection .cursor ()
191+ cursor .executemany (
192+ "INSERT INTO t (a, b) VALUES (%(x)s, %(x)s)" ,
193+ [{"x" : 1 }, {"x" : 2 }],
194+ )
195+ sql , _params , bulk_args = mocked_connection .client .sql .call_args [0 ]
196+ assert sql == "INSERT INTO t (a, b) VALUES ($1, $1)"
197+ assert bulk_args == [[1 ], [2 ]]
198+
199+
200+ def test_executemany_with_mixed_param_types (mocked_connection ):
201+ """
202+ Verify that executemany() raises a clear ProgrammingError when the
203+ parameter sequence mixes dicts and non-dicts while the SQL uses pyformat.
204+ """
205+ cursor = mocked_connection .cursor ()
206+ with pytest .raises (ProgrammingError , match = "requires all parameter rows" ):
207+ cursor .executemany (
208+ "INSERT INTO characters (name) VALUES (%(name)s)" ,
209+ [{"name" : "Arthur" }, ["Trillian" ]], # second row is a list
210+ )
211+ mocked_connection .client .sql .assert_not_called ()
212+
213+
128214def test_create_with_timezone_as_datetime_object (mocked_connection ):
129215 """
130216 The cursor can return timezone-aware `datetime` objects when requested.
0 commit comments