@@ -403,33 +403,108 @@ void Binder::BindDeleteReturningColumns(TableCatalogEntry &table, LogicalGet &ge
403403 }
404404}
405405
406- void Binder::BindDeleteReturningColumns (TableCatalogEntry &table, LogicalGet &get, vector<idx_t > &return_columns,
407- vector<unique_ptr<Expression>> &projection_expressions,
408- LogicalOperator &target_binding) {
409- // First, use the base helper to ensure all physical columns are in the scan
410- vector<idx_t > scan_return_columns;
411- BindDeleteReturningColumns (table, get, scan_return_columns);
412-
413- // Resolve types after adding columns to the scan
406+ // ! Helper: convert scan column mapping to projection expression mapping for MERGE INTO
407+ static void ConvertScanToProjectionMapping (TableCatalogEntry &table, const vector<idx_t > &scan_return_columns,
408+ vector<idx_t > &return_columns,
409+ vector<unique_ptr<Expression>> &projection_expressions,
410+ LogicalOperator &target_binding) {
414411 target_binding.ResolveOperatorTypes ();
415412 auto target_bindings = target_binding.GetColumnBindings ();
416413 auto &target_types = target_binding.types ;
417414
418- // Convert scan mapping to projection expression mapping
419- // return_columns[storage_idx] = projection_expr_idx
420415 auto physical_count = table.GetColumns ().PhysicalColumnCount ();
421416 return_columns.resize (physical_count, DConstants::INVALID_INDEX);
422417
423418 for (idx_t storage_idx = 0 ; storage_idx < scan_return_columns.size (); storage_idx++) {
424419 auto scan_idx = scan_return_columns[storage_idx];
425420 if (scan_idx != DConstants::INVALID_INDEX && scan_idx < target_bindings.size ()) {
426421 return_columns[storage_idx] = projection_expressions.size ();
427- auto col_ref = make_uniq<BoundColumnRefExpression>(target_types[scan_idx], target_bindings[scan_idx]);
428- projection_expressions. push_back ( std::move (col_ref ));
422+ projection_expressions. push_back (
423+ make_uniq<BoundColumnRefExpression>(target_types[scan_idx], target_bindings[scan_idx] ));
429424 }
430425 }
431426}
432427
428+ void Binder::BindDeleteReturningColumns (TableCatalogEntry &table, LogicalGet &get, vector<idx_t > &return_columns,
429+ vector<unique_ptr<Expression>> &projection_expressions,
430+ LogicalOperator &target_binding) {
431+ vector<idx_t > scan_return_columns;
432+ BindDeleteReturningColumns (table, get, scan_return_columns);
433+ ConvertScanToProjectionMapping (table, scan_return_columns, return_columns, projection_expressions, target_binding);
434+ }
435+
436+ void Binder::BindDeleteIndexColumns (TableCatalogEntry &table, LogicalGet &get, vector<idx_t > &return_columns) {
437+ // Build a mapping from storage column index to scan chunk index for unique index tracking.
438+ // This is a sparse mapping - only indexed columns have valid indices.
439+ // Used when DELETE has no RETURNING but table has unique indexes.
440+ auto &storage = table.GetStorage ();
441+ auto &info = storage.GetDataTableInfo ();
442+ auto &indexes = info->GetIndexes ();
443+
444+ // Collect column IDs from unique indexes
445+ unordered_set<column_t > indexed_column_ids;
446+ indexes.Scan ([&](Index &index) {
447+ if (index.IsUnique ()) {
448+ auto &col_ids = index.GetColumnIdSet ();
449+ indexed_column_ids.insert (col_ids.begin (), col_ids.end ());
450+ }
451+ return false ;
452+ });
453+
454+ if (indexed_column_ids.empty ()) {
455+ return ;
456+ }
457+
458+ auto &column_ids = get.GetColumnIds ();
459+ auto &columns = table.GetColumns ();
460+ auto physical_count = columns.PhysicalColumnCount ();
461+
462+ // Initialize the mapping with INVALID_INDEX
463+ return_columns.resize (physical_count, DConstants::INVALID_INDEX);
464+
465+ // First, map columns already in the scan to their storage indices
466+ for (idx_t chunk_idx = 0 ; chunk_idx < column_ids.size (); chunk_idx++) {
467+ auto &col_id = column_ids[chunk_idx];
468+ if (col_id.IsVirtualColumn ()) {
469+ continue ;
470+ }
471+ auto logical_idx = col_id.GetPrimaryIndex ();
472+ auto &col = columns.GetColumn (LogicalIndex (logical_idx));
473+ if (!col.Generated ()) {
474+ auto storage_idx = col.StorageOid ();
475+ // Only map if this column is in a unique index
476+ if (indexed_column_ids.count (storage_idx)) {
477+ return_columns[storage_idx] = chunk_idx;
478+ }
479+ }
480+ }
481+
482+ // Add any missing indexed columns to the scan
483+ for (auto col_idx : indexed_column_ids) {
484+ if (return_columns[col_idx] == DConstants::INVALID_INDEX) {
485+ return_columns[col_idx] = column_ids.size ();
486+ // Find the logical index for this storage index
487+ for (auto &col : columns.Physical ()) {
488+ if (col.StorageOid () == col_idx) {
489+ get.AddColumnId (col.Logical ().index );
490+ break ;
491+ }
492+ }
493+ }
494+ }
495+ }
496+
497+ void Binder::BindDeleteIndexColumns (TableCatalogEntry &table, LogicalGet &get, vector<idx_t > &return_columns,
498+ vector<unique_ptr<Expression>> &projection_expressions,
499+ LogicalOperator &target_binding) {
500+ vector<idx_t > scan_return_columns;
501+ BindDeleteIndexColumns (table, get, scan_return_columns);
502+ if (!scan_return_columns.empty ()) {
503+ ConvertScanToProjectionMapping (table, scan_return_columns, return_columns, projection_expressions,
504+ target_binding);
505+ }
506+ }
507+
433508BoundStatement Binder::BindReturning (vector<unique_ptr<ParsedExpression>> returning_list, TableCatalogEntry &table,
434509 const string &alias, idx_t update_table_index,
435510 unique_ptr<LogicalOperator> child_operator, virtual_column_map_t virtual_columns) {
0 commit comments