| 207 | } |
| 208 | |
| 209 | std::vector<ObjectMergeConflict> AppSession::objectMergeConflicts(const std::vector<DatasetId>& selected) const { |
| 210 | const std::optional<MergePlan> plan = planMerge(selected); |
| 211 | if (!plan.has_value()) { |
| 212 | return {}; |
| 213 | } |
| 214 | |
| 215 | const ObjectStore& object_store = session_manager_->objectStore(); |
| 216 | // Mirror ObjectStore::mergeDatasets' grouping so the gate catches EVERY type clash, not just |
| 217 | // source-vs-anchor: the established type for a name is the anchor's, or — when the anchor lacks it — |
| 218 | // the FIRST source (in plan order) to contribute that name, which becomes the fuse destination. A |
| 219 | // later contributor with the same name but a different canonical type is the conflict, whether it |
| 220 | // disagrees with the anchor or with an earlier source-only contributor. |
| 221 | std::unordered_map<std::string, sdk::BuiltinObjectType> type_by_name; |
| 222 | for (const ObjectTopicId object_topic_id : object_store.listTopics(plan->anchor)) { |
| 223 | const ObjectTopicDescriptor descriptor = object_store.descriptor(object_topic_id); |
| 224 | type_by_name.emplace(descriptor.topic_name, objectTypeFromMetadata(descriptor.metadata_json)); |
| 225 | } |
| 226 | |
| 227 | std::vector<ObjectMergeConflict> conflicts; |
| 228 | for (const DatasetMergeSource& source : plan->sources) { |
| 229 | for (const ObjectTopicId object_topic_id : object_store.listTopics(source.dataset_id)) { |
| 230 | const ObjectTopicDescriptor descriptor = object_store.descriptor(object_topic_id); |
| 231 | const sdk::BuiltinObjectType source_type = objectTypeFromMetadata(descriptor.metadata_json); |
| 232 | const auto [it, inserted] = type_by_name.emplace(descriptor.topic_name, source_type); |
| 233 | if (inserted || source_type == it->second) { |
| 234 | continue; // first contributor for this name (sets the destination type), or a match. |
| 235 | } |
| 236 | conflicts.push_back( |
| 237 | ObjectMergeConflict{ |
| 238 | .topic_name = descriptor.topic_name, |
| 239 | .source_dataset_id = source.dataset_id, |
| 240 | .anchor_type = it->second, |
| 241 | .source_type = source_type, |
| 242 | }); |
| 243 | } |
| 244 | } |
| 245 | return conflicts; |
| 246 | } |
| 247 | |
| 248 | DatasetId AppSession::mergeDatasets(const std::vector<DatasetId>& selected) { |
| 249 | std::optional<MergePlan> plan = planMerge(selected); |