diff --git a/Project.toml b/Project.toml index 729668d..5106aa0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DataAPI" uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" authors = ["quinnj "] -version = "1.10.0" +version = "1.11.0" [compat] julia = "1" diff --git a/src/DataAPI.jl b/src/DataAPI.jl index 567b8e8..2697413 100644 --- a/src/DataAPI.jl +++ b/src/DataAPI.jl @@ -287,4 +287,158 @@ using a `sink` function to materialize the table. """ function allcombinations end +const STYLE_INFO = """ +One of the uses of the metadata `style` is decision +how the metadata should be propagated when `x` is transformed. This interface +defines the `:default` style that indicates that metadata should not be propagated +under any operations (it is only preserved when a copy of the source table is +performed). All types supporting metadata allow at least this style. +""" + +const COL_INFO = """ +`col` must have a type that is supported by table `x` for column indexing. +Following the Tables.jl contract `Symbol` and `Int` are always allowed. +Passing `col` that is not a column of `x` throws an error. +""" + +""" + metadata(x, key::AbstractString; style::Bool=false) + +Return metadata value associated with object `x` for key `key`. +If `x` does not support metadata throw `ArgumentError`. +If `x` supports metadata, but does not have a mapping for `key` throw +`KeyError`. + +If `style=true` return a tuple of metadata value and metadata style. Metadata +style is an additional information about the kind of metadata that is stored +for the `key`. + +$STYLE_INFO +""" +metadata(::T, ::AbstractString; style::Bool=false) where {T} = + throw(ArgumentError("Objects of type $T do not support getting metadata")) + +""" + metadatakeys(x) + +Return an iterator of metadata keys for which `metadata(x, key)` returns a +metadata value. If `x` does not support metadata return `()`. +""" +metadatakeys(::Any) = () + +""" + metadata!(x, key::AbstractString, value; style) + +Set metadata for object `x` for key `key` to have value `value` +and style `style` and return `x`. +If `x` does not support setting metadata throw `ArgumentError`. + +$STYLE_INFO +""" +metadata!(::T, ::AbstractString, ::Any; style) where {T} = + throw(ArgumentError("Objects of type $T do not support setting metadata")) + +""" + deletemetadata!(x, key::AbstractString) + +Delete metadata for object `x` for key `key` and return `x` +(if metadata for `key` is not present do not perform any action). +If `x` does not support metadata deletion throw `ArgumentError`. +""" +deletemetadata!(::T, ::AbstractString) where {T} = + throw(ArgumentError("Objects of type $T do not support metadata deletion")) + +""" + emptymetadata!(x) + +Delete all metadata for object `x`. +If `x` does not support metadata deletion throw `ArgumentError`. +""" +emptymetadata!(::T) where {T} = + throw(ArgumentError("Objects of type $T do not support metadata deletion")) + +""" + colmetadata(x, col, key::AbstractString; style::Bool=false) + +Return metadata value associated with table `x` for column `col` and key `key`. +If `x` does not support metadata for column `col` throw `ArgumentError`. If `x` +supports metadata, but does not have a mapping for column `col` for `key` throw +`KeyError`. + +$COL_INFO + +If `style=true` return a tuple of metadata value and metadata style. Metadata +style is an additional information about the kind of metadata that is stored for +the `key`. + +$STYLE_INFO +""" +colmetadata(::T, ::Int, ::AbstractString; style::Bool=false) where {T} = + throw(ArgumentError("Objects of type $T do not support getting column metadata")) +colmetadata(::T, ::Symbol, ::AbstractString; style::Bool=false) where {T} = + throw(ArgumentError("Objects of type $T do not support getting column metadata")) + +""" + colmetadatakeys(x, [col]) + +If `col` is passed return an iterator of metadata keys for which `metadata(x, +col, key)` returns a metadata value. If `x` does not support metadata for column +`col` return `()`. + +`col` must have a type that is supported by table `x` for column indexing. +Following the Tables.jl contract `Symbol` and `Int` are always allowed. Passing +`col` that is not a column of `x` either throws an error (this is a +preferred behavior if it is possible) or returns `()` (this duality is allowed +as some Tables.jl tables do not have a schema). + +If `col` is not passed return an iterator of `col => colmetadatakeys(x, col)` +pairs for all columns that have metadata, where `col` are `Symbol`. +If `x` does not support column metadata return `()`. +""" +colmetadatakeys(::Any, ::Int) = () +colmetadatakeys(::Any, ::Symbol) = () +colmetadatakeys(::Any) = () + +""" + colmetadata!(x, col, key::AbstractString, value; style) + +Set metadata for table `x` for column `col` for key `key` to have value `value` +and style `style` and return `x`. +If `x` does not support setting metadata for column `col` throw `ArgumentError`. + +$COL_INFO + +$STYLE_INFO +""" +colmetadata!(::T, ::Int, ::AbstractString, ::Any; style) where {T} = + throw(ArgumentError("Objects of type $T do not support setting metadata")) +colmetadata!(::T, ::Symbol, ::AbstractString, ::Any; style) where {T} = + throw(ArgumentError("Objects of type $T do not support setting metadata")) + +""" + deletecolmetadata!(x, col, key::AbstractString) + +Delete metadata for table `x` for column `col` for key `key` and return `x` +(if metadata for `key` is not present do not perform any action). +If `x` does not support metadata deletion for column `col` throw `ArgumentError`. +""" +deletecolmetadata!(::T, ::Symbol, ::AbstractString) where {T} = + throw(ArgumentError("Objects of type $T do not support metadata deletion")) +deletecolmetadata!(::T, ::Int, ::AbstractString) where {T} = + throw(ArgumentError("Objects of type $T do not support metadata deletion")) + +""" + emptycolmetadata!(x, [col]) + +Delete all metadata for table `x` for column `col`. +If `col` is not passed delete all column level metadata for table `x`. +If `x` does not support metadata deletion for column `col` throw `ArgumentError`. +""" +emptycolmetadata!(::T, ::Symbol) where {T} = + throw(ArgumentError("Objects of type $T do not support metadata deletion")) +emptycolmetadata!(::T, ::Int) where {T} = + throw(ArgumentError("Objects of type $T do not support metadata deletion")) +emptycolmetadata!(::T) where {T} = + throw(ArgumentError("Objects of type $T do not support metadata deletion")) + end # module diff --git a/test/runtests.jl b/test/runtests.jl index 9782ac2..c8384de 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,79 @@ Base.size(x::TestArray) = size(x.x) Base.getindex(x::TestArray, i) = x.x[i] DataAPI.levels(x::TestArray) = reverse(DataAPI.levels(x.x)) +# An example implementation of metadata +# For simplicity Int col indexing is not implemented +# and no checking if col is a column of a table is performed + +struct TestMeta + table::Dict{String, Any} + col::Dict{Symbol, Dict{String, Any}} + + TestMeta() = new(Dict{String, Any}(), Dict{Symbol, Dict{String, Any}}()) +end + +function DataAPI.metadata(x::TestMeta, key::AbstractString; style::Bool=false) + return style ? x.table[key] : x.table[key][1] +end + +DataAPI.metadatakeys(x::TestMeta) = keys(x.table) + +function DataAPI.metadata!(x::TestMeta, key::AbstractString, value; style) + x.table[key] = (value, style) + return x +end + +function DataAPI.metadata!(x::TestMeta, key::AbstractString, value; style) + x.table[key] = (value, style) + return x +end + +DataAPI.deletemetadata!(x::TestMeta, key::AbstractString) = delete!(x.table, key) +DataAPI.emptymetadata!(x::TestMeta) = empty!(x.table) + +function DataAPI.colmetadata(x::TestMeta, col::Symbol, key::AbstractString; style::Bool=false) + return style ? x.col[col][key] : x.col[col][key][1] +end + +function DataAPI.colmetadatakeys(x::TestMeta, col::Symbol) + haskey(x.col, col) && return keys(x.col[col]) + return () +end + +function DataAPI.colmetadatakeys(x::TestMeta) + isempty(x.col) && return () + return (col => keys(x.col[col]) for col in keys(x.col)) +end + +function DataAPI.colmetadata!(x::TestMeta, col::Symbol, key::AbstractString, value; style) + if haskey(x.col, col) + x.col[col][key] = (value, style) + else + x.col[col] = Dict{Any, Any}(key => (value, style)) + end + return x +end + +function DataAPI.deletecolmetadata!(x::TestMeta, col::Symbol, key::AbstractString) + if haskey(x.col, col) + delete!(x.col[col], key) + else + throw(ArgumentError("column $col not found")) + end + return x +end + +function DataAPI.emptycolmetadata!(x::TestMeta, col::Symbol) + if haskey(x.col, col) + delete!(x.col, col) + else + throw(ArgumentError("column $col not found")) + end + return x +end + +DataAPI.emptycolmetadata!(x::TestMeta) = empty!(x.col) + @testset "DataAPI" begin @testset "defaultarray" begin @@ -173,4 +246,62 @@ end @test DataAPI.unwrap(missing) === missing end +@testset "metadata" begin + @test_throws ArgumentError DataAPI.metadata!(1, "a", 10, style=:default) + @test_throws ArgumentError DataAPI.deletemetadata!(1, "a") + @test_throws ArgumentError DataAPI.emptymetadata!(1) + @test_throws ArgumentError DataAPI.metadata(1, "a") + @test_throws ArgumentError DataAPI.metadata(1, "a", style=true) + @test DataAPI.metadatakeys(1) == () + + @test_throws ArgumentError DataAPI.colmetadata!(1, :col, "a", 10, style=:default) + @test_throws ArgumentError DataAPI.deletecolmetadata!(1, :col, "a") + @test_throws ArgumentError DataAPI.emptycolmetadata!(1, :col) + @test_throws ArgumentError DataAPI.deletecolmetadata!(1, 1, "a") + @test_throws ArgumentError DataAPI.emptycolmetadata!(1, 1) + @test_throws ArgumentError DataAPI.emptycolmetadata!(1) + @test_throws ArgumentError DataAPI.colmetadata(1, :col, "a") + @test_throws ArgumentError DataAPI.colmetadata(1, :col, "a", style=true) + @test_throws ArgumentError DataAPI.colmetadata!(1, 1, "a", 10, style=:default) + @test_throws ArgumentError DataAPI.colmetadata(1, 1, "a") + @test_throws ArgumentError DataAPI.colmetadata(1, 1, "a", style=true) + @test DataAPI.colmetadatakeys(1, :col) == () + @test DataAPI.colmetadatakeys(1, 1) == () + @test DataAPI.colmetadatakeys(1) == () + + tm = TestMeta() + @test isempty(DataAPI.metadatakeys(tm)) + @test DataAPI.metadata!(tm, "a", "100", style=:note) == tm + @test collect(DataAPI.metadatakeys(tm)) == ["a"] + @test_throws KeyError DataAPI.metadata(tm, "b") + @test_throws KeyError DataAPI.metadata(tm, "b", style=true) + @test DataAPI.metadata(tm, "a") == "100" + @test DataAPI.metadata(tm, "a", style=true) == ("100", :note) + DataAPI.deletemetadata!(tm, "a") + @test isempty(DataAPI.metadatakeys(tm)) + @test DataAPI.metadata!(tm, "a", "100", style=:note) == tm + DataAPI.emptymetadata!(tm) + @test isempty(DataAPI.metadatakeys(tm)) + + @test DataAPI.colmetadatakeys(tm) == () + @test DataAPI.colmetadatakeys(tm, :col) == () + @test DataAPI.colmetadata!(tm, :col, "a", "100", style=:note) == tm + @test [k => collect(v) for (k, v) in DataAPI.colmetadatakeys(tm)] == [:col => ["a"]] + @test collect(DataAPI.colmetadatakeys(tm, :col)) == ["a"] + @test_throws KeyError DataAPI.colmetadata(tm, :col, "b") + @test_throws KeyError DataAPI.colmetadata(tm, :col, "b", style=true) + @test_throws KeyError DataAPI.colmetadata(tm, :col2, "a") + @test_throws KeyError DataAPI.colmetadata(tm, :col2, "a", style=true) + @test DataAPI.colmetadata(tm, :col, "a") == "100" + @test DataAPI.colmetadata(tm, :col, "a", style=true) == ("100", :note) + DataAPI.deletecolmetadata!(tm, :col, "a") + @test isempty(DataAPI.colmetadatakeys(tm, :col)) + @test DataAPI.colmetadata!(tm, :col, "a", "100", style=:note) == tm + DataAPI.emptycolmetadata!(tm, :col) + @test isempty(DataAPI.colmetadatakeys(tm, :col)) + @test DataAPI.colmetadata!(tm, :col, "a", "100", style=:note) == tm + DataAPI.emptycolmetadata!(tm) + @test isempty(DataAPI.colmetadatakeys(tm)) +end + end # @testset "DataAPI"