From 5180043e53f85b8730e588493e8f6169bf25d6c8 Mon Sep 17 00:00:00 2001 From: Victor M Varela Date: Sat, 12 Sep 2026 11:47:39 +0200 Subject: [PATCH] fix: duplicate rename colliding with explicit name (Issue #233) --- build.zig | 8 ++++++++ src/loader.zig | 36 +++++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/build.zig b/build.zig index de52975..8047216 100644 --- a/build.zig +++ b/build.zig @@ -357,6 +357,14 @@ pub fn build(b: *std.Build) void { test_dup_col_stdout.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_dup_col_stdout.step); + // Integration test 19b: generated rename colliding with an explicit name stays unique (Issue #233) + const test_dup_col_collision = b.addSystemCommand(&.{ + "bash", "-c", + \\printf 'a,a,a_2\n1,2,3\n' | ./zig-out/bin/sql-pipe --columns 2>/dev/null | diff - <(printf 'a\na_2\na_2_2\n') + }); + test_dup_col_collision.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_dup_col_collision.step); + // Integration test 20: --max-rows under limit succeeds const test_max_rows_under = b.addSystemCommand(&.{ "bash", "-c", diff --git a/src/loader.zig b/src/loader.zig index 1b4b339..146d213 100644 --- a/src/loader.zig +++ b/src/loader.zig @@ -321,29 +321,35 @@ pub fn parseHeader( cols.deinit(allocator); } - // seen: maps a column name to the number of times it has appeared so far. - // Pre: seen is empty - // Post: seen[name] = count of occurrences in record[0..i] - var seen = std.StringHashMap(usize).init(allocator); - defer seen.deinit(); + // emitted: set of column names already produced (bare and generated). + // A generated `base_N` can collide with an explicit header name + // (Issue #233: `a,a,a_2`), so every emitted name is registered and + // the `_N` suffix is bumped until free. Keys borrow `record`/`cols` + // slices; the map dies at function end, so no lifetime issue. + var emitted = std.StringHashMap(void).init(allocator); + defer emitted.deinit(); for (record) |field| { const base = std.mem.trim(u8, field, " \t\r"); if (base.len == 0) return error.EmptyColumnName; - const count = (seen.get(base) orelse 0) + 1; - try seen.put(base, count); - - const col: []const u8 = if (count == 1) + const col: []const u8 = if (!emitted.contains(base)) try allocator.dupe(u8, base) else blk: { - const renamed = try std.fmt.allocPrint(allocator, "{s}_{d}", .{ base, count }); - // Best-effort warning to stderr; write errors are silently ignored - stderr_writer.print("warning: duplicate column \"{s}\" renamed to \"{s}\"\n", .{ base, renamed }) catch |err| { - std.log.err("failed to write warning: {}", .{err}); - }; - break :blk renamed; + var n: usize = 2; + while (true) : (n += 1) { + const cand = try std.fmt.allocPrint(allocator, "{s}_{d}", .{ base, n }); + if (!emitted.contains(cand)) { + // Best-effort warning to stderr; write errors are silently ignored + stderr_writer.print("warning: duplicate column \"{s}\" renamed to \"{s}\"\n", .{ base, cand }) catch |err| { + std.log.err("failed to write warning: {}", .{err}); + }; + break :blk cand; + } + allocator.free(cand); + } }; + try emitted.put(col, {}); try cols.append(allocator, col); }