@@ -92,9 +92,7 @@ def test_replace_by_key_composite_uses_join_delete(
9292):
9393 """Composite key DELETE uses JOIN instead of CONCAT_WS to allow index usage."""
9494 adapter = make_mocked_engine_adapter (MySQLEngineAdapter )
95- temp_table_mock = mocker .patch (
96- "sqlmesh.core.engine_adapter.base.EngineAdapter._get_temp_table"
97- )
95+ temp_table_mock = mocker .patch ("sqlmesh.core.engine_adapter.base.EngineAdapter._get_temp_table" )
9896 temp_table_mock .return_value = exp .to_table ("temporary" )
9997
10098 adapter .merge (
@@ -135,14 +133,81 @@ def test_replace_by_key_composite_uses_join_delete(
135133 )
136134
137135
136+ def test_replace_by_key_three_column_composite_key (
137+ make_mocked_engine_adapter : t .Callable , mocker : MockerFixture
138+ ):
139+ """3-column composite key matching the original issue scenario (#5711)."""
140+ adapter = make_mocked_engine_adapter (MySQLEngineAdapter )
141+ temp_table_mock = mocker .patch ("sqlmesh.core.engine_adapter.base.EngineAdapter._get_temp_table" )
142+ temp_table_mock .return_value = exp .to_table ("temporary" )
143+
144+ adapter .merge (
145+ target_table = "target" ,
146+ source_table = t .cast (exp .Select , parse_one ("SELECT id, region, ts, val FROM source" )),
147+ target_columns_to_types = {
148+ "id" : exp .DataType (this = exp .DataType .Type .INT ),
149+ "region" : exp .DataType (this = exp .DataType .Type .VARCHAR ),
150+ "ts" : exp .DataType (this = exp .DataType .Type .TIMESTAMP ),
151+ "val" : exp .DataType (this = exp .DataType .Type .INT ),
152+ },
153+ unique_key = [parse_one ("id" ), parse_one ("region" ), parse_one ("ts" )],
154+ )
155+
156+ sql_calls = to_sql_calls (adapter )
157+
158+ assert any ("CONCAT_WS" in s for s in sql_calls ) is False
159+ assert any ("INNER JOIN" in s for s in sql_calls ) is True
160+
161+ adapter .cursor .execute .assert_has_calls (
162+ [
163+ call (
164+ "DELETE `_target` FROM `target` AS `_target` INNER JOIN `temporary` AS `_temp` ON `_target`.`id` = `_temp`.`id` AND `_target`.`region` = `_temp`.`region` AND `_target`.`ts` = `_temp`.`ts`"
165+ ),
166+ ]
167+ )
168+
169+
170+ def test_replace_by_key_expression_based_composite_key (
171+ make_mocked_engine_adapter : t .Callable , mocker : MockerFixture
172+ ):
173+ """Expression-based composite keys (e.g. DATE_TRUNC + column) via insert_overwrite_by_partition."""
174+ adapter = make_mocked_engine_adapter (MySQLEngineAdapter )
175+ temp_table_mock = mocker .patch ("sqlmesh.core.engine_adapter.base.EngineAdapter._get_temp_table" )
176+ temp_table_mock .return_value = exp .to_table ("temporary" )
177+
178+ adapter .insert_overwrite_by_partition (
179+ table_name = "target" ,
180+ query_or_df = t .cast (exp .Select , parse_one ("SELECT id, ts, val FROM source" )),
181+ partitioned_by = [
182+ parse_one ("DATE_TRUNC('month', ts)" ),
183+ parse_one ("id" ),
184+ ],
185+ target_columns_to_types = {
186+ "id" : exp .DataType (this = exp .DataType .Type .INT ),
187+ "ts" : exp .DataType (this = exp .DataType .Type .TIMESTAMP ),
188+ "val" : exp .DataType (this = exp .DataType .Type .INT ),
189+ },
190+ )
191+
192+ sql_calls = to_sql_calls (adapter )
193+
194+ assert any ("CONCAT_WS" in s for s in sql_calls ) is False
195+ assert any ("INNER JOIN" in s for s in sql_calls ) is True
196+
197+ # DATE_TRUNC transpiles to STR_TO_DATE in MySQL; both key parts should be qualified
198+ delete_sql = [s for s in sql_calls if "DELETE" in s ][0 ]
199+ assert "`_target`.`ts`" in delete_sql
200+ assert "`_temp`.`ts`" in delete_sql
201+ assert "`_target`.`id`" in delete_sql
202+ assert "`_temp`.`id`" in delete_sql
203+
204+
138205def test_replace_by_key_single_key_uses_in (
139206 make_mocked_engine_adapter : t .Callable , mocker : MockerFixture
140207):
141208 """Single key DELETE still uses the IN-based approach (indexes work fine for single column)."""
142209 adapter = make_mocked_engine_adapter (MySQLEngineAdapter )
143- temp_table_mock = mocker .patch (
144- "sqlmesh.core.engine_adapter.base.EngineAdapter._get_temp_table"
145- )
210+ temp_table_mock = mocker .patch ("sqlmesh.core.engine_adapter.base.EngineAdapter._get_temp_table" )
146211 temp_table_mock .return_value = exp .to_table ("temporary" )
147212
148213 adapter .merge (
0 commit comments