@@ -290,18 +290,98 @@ public DatasetExpression visitFilterClause(VtlParser.FilterClauseContext ctx) {
290290
291291 @ Override
292292 public DatasetExpression visitRenameClause (VtlParser .RenameClauseContext ctx ) {
293+
294+ // Dataset structure in order + lookup maps
295+ final List <Dataset .Component > componentsInOrder =
296+ new ArrayList <>(datasetExpression .getDataStructure ().values ());
297+ final Set <String > availableColumns =
298+ componentsInOrder .stream ()
299+ .map (Dataset .Component ::getName )
300+ .collect (Collectors .toCollection (LinkedHashSet ::new ));
301+
302+ // Map for detailed error reporting (includes role/type if available)
303+ final Map <String , Dataset .Component > byName =
304+ componentsInOrder .stream ()
305+ .collect (
306+ Collectors .toMap (
307+ Dataset .Component ::getName , c -> c , (a , b ) -> a , LinkedHashMap ::new ));
308+
309+ // Parse the RENAME clause and validate
293310 Map <String , String > fromTo = new LinkedHashMap <>();
294- Set <String > renamed = new HashSet <>();
311+ Set <String > toSeen = new LinkedHashSet <>();
312+ Set <String > fromSeen = new LinkedHashSet <>();
313+
295314 for (VtlParser .RenameClauseItemContext renameCtx : ctx .renameClauseItem ()) {
296- var toNameString = getName (renameCtx .toName );
297- var fromNameString = getName (renameCtx .fromName );
298- if (!renamed .add (toNameString )) {
315+ final String toNameString = getName (renameCtx .toName );
316+ final String fromNameString = getName (renameCtx .fromName );
317+
318+ // Validate: no duplicate "from" names inside the clause
319+ if (!fromSeen .add (fromNameString )) {
299320 throw new VtlRuntimeException (
300321 new InvalidArgumentException (
301- "duplicate column: %s" .formatted (toNameString ), fromContext (renameCtx )));
322+ String .format ("Error: duplicate source name in RENAME clause: '%s" , fromNameString ),
323+ fromContext (ctx )));
302324 }
325+
326+ // Validate: "from" must exist in dataset
327+ if (!availableColumns .contains (fromNameString )) {
328+ Dataset .Component comp = byName .get (fromNameString );
329+ String meta =
330+ (comp != null )
331+ ? String .format (
332+ " (role=%s, type=%s)" ,
333+ comp .getRole (), comp .getType () != null ? comp .getType () : "n/a" )
334+ : "" ;
335+ throw new VtlRuntimeException (
336+ new InvalidArgumentException (
337+ String .format (
338+ "Error: source column to rename not found: '%s'%s" , fromNameString , meta ),
339+ fromContext (ctx )));
340+ }
341+
342+ // Validate: no duplicate "to" names inside the clause
343+ if (!toSeen .add (toNameString )) {
344+ throw new VtlRuntimeException (
345+ new InvalidArgumentException (
346+ String .format (
347+ "Error: duplicate output column name in RENAME clause: '%s." , fromNameString ),
348+ fromContext (ctx )));
349+ }
350+
303351 fromTo .put (fromNameString , toNameString );
304352 }
353+
354+ // Validate collisions with untouched dataset columns ("Untouched" = columns that are not
355+ // being renamed)
356+ final Set <String > untouched =
357+ availableColumns .stream ()
358+ .filter (c -> !fromTo .containsKey (c ))
359+ .collect (Collectors .toCollection (LinkedHashSet ::new ));
360+
361+ for (Map .Entry <String , String > e : fromTo .entrySet ()) {
362+ final String from = e .getKey ();
363+ final String to = e .getValue ();
364+
365+ // If target already exists as untouched, it would cause a collision
366+ if (untouched .contains (to )) {
367+ Dataset .Component comp = byName .get (to );
368+ String meta =
369+ (comp != null )
370+ ? String .format (
371+ " (role=%s, type=%s)" ,
372+ comp .getRole (), comp .getType () != null ? comp .getType () : "n/a" )
373+ : "" ;
374+
375+ throw new VtlRuntimeException (
376+ new InvalidArgumentException (
377+ String .format (
378+ "Error: target name '%s'%s already exists in dataset and is not being renamed." ,
379+ to , meta ),
380+ fromContext (ctx )));
381+ }
382+ }
383+
384+ // Execute rename in processing engine
305385 return processingEngine .executeRename (datasetExpression , fromTo );
306386 }
307387
0 commit comments