Repository: JuliaData/Tables.jl (julia)

Issue:
`Table.rows(DataFrame)` leads to a StackOverflowError
Originally noticed by user wanderinganon at: https://stackoverflow.com/questions/74085820/writing-csv-file-with-csv-jl-throws-stackoverflowerror-message

They mistakenly passed the type `DataFrame` itself as an argument to `CSV.write`, which then ends up calling `Table.rows(DataFrame)`.

`Table.rows(DataFrame)`  uses the fallback method in Tables.jl, which checks for `columnaccess(DataFrame)`. `columnaccess(::Type{DataFrame})` is defined true with the intention being that objects of this type have column access, but here this leads to the `columnaccess(DataFrame)` check succeeding. 

So `Table.rows` calls `Tables.columns(DataFrame)`, where a similar thing happens with checking `Table.rowaccess`, and the check succeeding. So `Tables.column` ends up calling `Table.rows`, and so it goes. 

Related to https://github.com/JuliaData/Tables.jl/issues/269

Pull request:
…iate error

Fixes #311. We try to present an informative error message to the user that we don't typically expect a _type_ to be passed to `Tables.rows` and instead expect an _instance_.

Patch:
diff --git a/src/fallbacks.jl b/src/fallbacks.jl
index c560889..48ea6ad 100644
--- a/src/fallbacks.jl
+++ b/src/fallbacks.jl
@@ -80,6 +80,8 @@ schema(x::RowIterator) = schema(columns(x))
 end
 
 # this is our generic Tables.rows fallback definition
+@noinline nodefault(T) = throw(ArgumentError("no default `Tables.rows` implementation for type: $T"))
+
 function rows(x::T) where {T}
     isrowtable(x) && return x
     # because this method is being called, we know `x` didn't define it's own Tables.rows
@@ -93,9 +95,11 @@ function rows(x::T) where {T}
     elseif IteratorInterfaceExtensions.isiterable(x)
         return nondatavaluerows(x)
     end
-    throw(ArgumentError("no default `Tables.rows` implementation for type: $T"))
+    nodefault(T)
 end
 
+rows(::Type{T}) where {T} = throw(ArgumentError("no `Tables.rows` implementation for: $T. `Tables.rows` expects to work on a table _instance_ rather than a _type_."))
+
 # for AbstractRow iterators, we define a "collect"-like routine to build up columns from iterated rows
 
 """


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index fbbbc92..84a87a7 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -397,6 +397,25 @@ function genericcolumntable(x)
 end
 ==(a::GenericColumnTable, b::GenericColumnTable) = getfield(a, 1) == getfield(b, 1) && getfield(a, 2) == getfield(b, 2)
 
+# is both row and column access
+struct GenericTable
+end
+Tables.istable(::Type{GenericTable}) = true
+Tables.rowaccess(::Type{GenericTable}) = true
+Tables.rows(x::GenericTable) = x
+Tables.schema(x::GenericTable) = Tables.Schema((:a, :b, :c), Tuple{Int, Float64, String})
+Tables.columnaccess(::Type{GenericTable}) = true
+Tables.columns(x::GenericTable) = x
+Base.eltype(g::GenericTable) = GenericRow
+Base.length(g::GenericTable) = 1
+Base.size(g::GenericTable) = (1,)
+function Base.iterate(g::GenericTable, st=1)
+    st > 1 && return nothing
+    return GenericRow(1, 2.0, "3"), 2
+end
+Base.getproperty(g::GenericTable, nm::Symbol) = GenericColumn([1, 2, 3])
+Base.propertynames(g::GenericTable) = (:a, :b, :c)
+
 @testset "Tables.jl interface" begin
 
     @test !Tables.istable(1)
@@ -436,6 +455,13 @@ end
     rows = Tuple{Int64, Float64, String}[]
     t = Tables.columns(rows)
     @test Tables.schema(t) == Tables.Schema((:Column1, :Column2, :Column3), (Int64, Float64, String))
+
+    # 311
+    gt = GenericTable()
+    @test Tables.schema(gt) == Tables.Schema((:a, :b, :c), (Int, Float64, String))
+    @test Tables.columns(gt) == gt
+    @test Tables.rows(gt) == gt
+    @test_throws ArgumentError Tables.rows(GenericTable)
 end
 
 @testset "isless" begin