Repository: JuliaData/Tables.jl (julia)

Issue:
Inconsistent handling of Tuple and NamedTuple
Currently we have this:
```
julia> Tables.columns(NamedTuple{(:a, :b),Tuple{Int,Int}}[])
Tables.CopiedColumns{NamedTuple{(:a, :b),Tuple{Array{Int64,1},Array{Int64,1}}}}: (a = Int64[], b = Int64[])

julia> Tables.columntable(Tuple{Int,Int}[])
NamedTuple()
```
Is this intended?

(I know it is a corner case but I test against it in DataFrames.jl)

Pull request description:
Fixes #204. For empty row interator inputs, we were just returning an
empty `NamedTuple`. This has the disadvantage of not preserving an
input's schema in the case like `NamedTuple{(:a, :b), Tuple{Int64,
Float64}}[]`. Sometimes the input may be empty, but have a queryable
schema, so we should try to preserve that. For `Tuple` rows, we generate
column names like `Column$i`. I think this is fine because we're in the
fallback `buildcolumns` code where we just want to return a standard
"table" object, so returning a NamedTuple instead of `Tuple` of vectors
seems more appropriate; i.e. it's Tables.jl job here to "build the
columns", so we have latitude and control over what we return.

Patch:
diff --git a/src/Tables.jl b/src/Tables.jl
index 976bde3..b13c048 100644
--- a/src/Tables.jl
+++ b/src/Tables.jl
@@ -174,7 +174,7 @@ Base.isempty(r::RorC) = length(r) == 0
 
 function Base.NamedTuple(r::RorC)
     names = columnnames(r)
-    return NamedTuple{Tuple(names)}(Tuple(getcolumn(r, nm) for nm in names))
+    return NamedTuple{Tuple(Base.map(Symbol, names))}(Tuple(getcolumn(r, nm) for nm in names))
 end
 
 function Base.show(io::IO, x::T) where {T <: RorC}
diff --git a/src/fallbacks.jl b/src/fallbacks.jl
index 0fe01a7..31cae9f 100644
--- a/src/fallbacks.jl
+++ b/src/fallbacks.jl
@@ -180,10 +180,27 @@ function _buildcolumns(rowitr, row, st, sch, columns, updated)
     return __buildcolumns(rowitr, st, sch, updated[], 1, updated)
 end
 
+if isdefined(Base, :fieldtypes)
+    _fieldtypes = fieldtypes
+else
+    _fieldtypes(T) = (fieldtype(T, i) for i = 1:fieldcount(T))
+end
+
 # when Tables.schema(x) === nothing
 @inline function buildcolumns(::Nothing, rowitr::T) where {T}
     state = iterate(rowitr)
-    state === nothing && return NamedTuple()
+    if state === nothing
+        # empty input iterator; check if it has eltype and maybe we can return a better typed empty NamedTuple
+        if Base.IteratorEltype(rowitr) == Base.HasEltype()
+            WT = wrappedtype(eltype(rowitr))
+            if WT <: Tuple
+                return allocatecolumns(Schema((Symbol("Column$i") for i = 1:fieldcount(WT)), _fieldtypes(WT)), 0)
+            elseif fieldcount(WT) > 0
+                return allocatecolumns(Schema(fieldnames(WT), _fieldtypes(WT)), 0)
+            end
+        end
+        return NamedTuple()
+    end
     row, st = state
     names = Tuple(columnnames(row))
     len = Base.haslength(T) ? length(rowitr) : 0
diff --git a/src/tofromdatavalues.jl b/src/tofromdatavalues.jl
index dcd05b8..1bf3de0 100644
--- a/src/tofromdatavalues.jl
+++ b/src/tofromdatavalues.jl
@@ -61,6 +61,7 @@ struct IteratorRow{T} <: AbstractRow
 end
 
 getrow(r::IteratorRow) = getfield(r, :row)
+wrappedtype(::Type{IteratorRow{T}}) where {T} = T
 
 unwrap(::Type{T}, x) where {T} = convert(T, x)
 unwrap(::Type{Any}, x) = x.hasvalue ? x.value : missing


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index 885551c..a3d7b3e 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -347,6 +347,17 @@ end
 
     @test_throws ArgumentError Tables.columns(Int64)
     @test_throws ArgumentError Tables.rows(Int64)
+
+    # 204
+    rows = NamedTuple{(:a, :b), Tuple{Int64, Float64}}[]
+    t = Tables.columns(rows)
+    @test Tables.schema(t) == Tables.Schema((:a, :b), (Int64, Float64))
+    rows = GenericRow[]
+    t = Tables.columns(rows)
+    @test Tables.schema(t) == Tables.Schema((:a, :b, :c), (Int, Float64, String))
+    rows = Tuple{Int64, Float64, String}[]
+    t = Tables.columns(rows)
+    @test Tables.schema(t) == Tables.Schema((:Column1, :Column2, :Column3), (Int64, Float64, String))
 end
 
 @testset "isless" begin