diff --git a/src/core.jl b/src/core.jl index 314e3ef7..66d67ba9 100644 --- a/src/core.jl +++ b/src/core.jl @@ -4666,18 +4666,24 @@ materialize(v::Vector) = v # A duplicate takes the smallest `name_k` not used by ANY # name — original or already assigned — so `a,a,a_1` becomes `a,a_2,a_1` # (renames never collide with an original that appears later). +# +# `taken` only grows, so every suffix below the last one assigned to a name is +# still taken: the search resumes there, which keeps N copies of one name linear. function makeunique!(names::Vector{Symbol}) taken = Set(names) + length(taken) == length(names) && return names seen = Set{Symbol}() + nextk = Dict{Symbol, Int}() for i in eachindex(names) nm = names[i] if nm in seen - k = 1 + k = get(nextk, nm, 1) newnm = Symbol(nm, :_, k) while newnm in taken k += 1 newnm = Symbol(nm, :_, k) end + nextk[nm] = k + 1 push!(taken, newnm) names[i] = newnm push!(seen, newnm) diff --git a/test/api.jl b/test/api.jl index 8e442686..b3189a20 100644 --- a/test/api.jl +++ b/test/api.jl @@ -276,6 +276,17 @@ end @test_throws ArgumentError A.File(IOBuffer("a,b\nx,y\n1,2\n"); header=[3, 1]) end +@testset "duplicate column names" begin + @test Base.names(A.File(IOBuffer("a,a,a\n"))) == [:a, :a_1, :a_2] + @test Base.names(A.File(IOBuffer("a,a_1,a\n"))) == [:a, :a_1, :a_2] + @test Base.names(A.File(IOBuffer("a,a,a_1\n"))) == [:a, :a_2, :a_1] + # Each copy resumes the suffix search, so many copies of one name stay + # linear; a search that restarts at 1 for every copy allocates gigabytes. + n = 4_000 + @test A.makeunique!(fill(:x, n)) == [:x; [Symbol(:x_, k) for k in 1:(n - 1)]] + @test (@allocated A.makeunique!(fill(:x, n))) < 100_000_000 +end + @testset "row windowing and source modes (raw-row semantics)" begin sourceparity("a,b\n1,2\n3,4\n5,6\n"; kw=(; limit=2)) sourceparity("a,b\n1,2\n3,4\n5,6\n"; kw=(; footerskip=2))