Tables.jl
repository·main·Indexed 43 days ago
https://github.com/juliadata/tables.jlA standardized interface for tabular data in Julia that enables interoperability between different data structures and packages. It provides row-oriented and column-oriented access patterns via Tables.AbstractRow and Tables.AbstractColumns, and includes Tables.Scan for describing column selection, row filtering, and push-down optimizations.
What's inside Tables.jl
- Tables.jl provides a standardized, simple, and powerful interface for working with various types of tabular data in Julia. It acts as a common interface that allows different packages to communicate with each other when handling tables, regardless of the underlying data structure.
Understand the purpose of Tables.jl
mainTables.jl provides a standardized set of interfaces for table data in Julia.
Who is this for?
- Package and Library Developers: This is the primary audience. Use Tables.jl if you are implementing a new data format or consuming existing table data to ensure interoperability with the Julia ecosystem.
- End Users: Most users do not need to use Tables.jl directly. Instead, users should look for existing packages that implement the Tables.jl interface to access data in various formats or workflows.
If you are looking for specific data access, check the INTEGRATIONS.md file in the repository for a list of supported packages.
Perform table operations with TableOperations.jl and TableTransforms.jl
mainWhile Tables.jl defines the interfaces, common high-level operations are handled by specialized packages:
- Common Operations: Use TableOperations.jl for standard operations like
select,transform,filter, andmap. - Sophisticated Transformations: Use TableTransforms.jl for more complex data transformations.
- Push-down Optimization: For declarative projection, filtering, and row bounds that can be pushed down to the data source, use
Tables.Scan(see the Scan Requests documentation).
- Common Operations: Use TableOperations.jl for standard operations like
Handle missing values in filter expressions
mainFilter evaluation follows SQL-like three-valued logic regarding
missingvalues:- Propagation: Comparisons, membership, and string predicates lift a
missingcolumn value tomissing. - Boolean Logic:
&,|, and!propagatemissingaccording to three-valued Boolean rules. - Filtering: A row is only kept if the top-level filter result is exactly
true. - Null Checks: Use
Tables.isnull(column)to returntrueformissingandfalseotherwise.
Note:
Tables.isnullis used instead ofBase.ismissingto avoid invalidating unrelated compiled code when the package loads.- Propagation: Comparisons, membership, and string predicates lift a
Install Tables.jl
mainTo install the
Tables.jlpackage, run the following command in your Julia REPL:import Pkg; Pkg.add("Tables")Handle unknown schemas using Tables.schema
mainWhen consuming a table, you can attempt to retrieve its schema using
Tables.schema(rows).If the table source is exotic (e.g., a lazy transformation) and
Tables.schemareturnsnothing, you can fallback to an 'unknown schema' strategy:- Start iterating the rows using
iterate(rows). - Use the first available row to extract column names via
Tables.columnnames(row). - Construct a partial
Tables.Schema(names, nothing)to allow subsequent processing (likeTables.eachcolumn) to function correctly.
sch = Tables.schema(rows) if sch === nothing # Fallback for unknown schema state = iterate(rows) state === nothing && return row, st = state names = Tables.columnnames(row) sch = Tables.Schema(names, nothing) # Continue iteration using the state from the first row while true Tables.eachcolumn(sch, row) do val, i, nm # process values end state = iterate(rows, st) state === nothing && break row, st = state end end- Start iterating the rows using
Implement the Tables.jl interface for a custom type
mainTo make a custom type
MyTablecompatible with Tables.jl consumers, you must implement a specific set of required methods. You must choose between a row-oriented or column-oriented implementation.1. Declare the type as a table
You must first signal that your type implements the interface:
Tables.istable(::Type{MyTable}) = true
2. Choose an access pattern
You must implement one of the following two patterns:
Row-oriented (Access via
Tables.rows)Use this if your type naturally iterates over rows.
Tables.rowaccess(::Type{MyTable}) = trueTables.rows(x::MyTable): Returns anTables.AbstractRow-compatible iterator.
Column-oriented (Access via
Tables.columns)Use this if your type naturally provides access to columns.
Tables.columnaccess(::Type{MyTable}) = trueTables.columns(x::MyTable): Returns anTables.AbstractColumns-compatible object.
3. Optional methods for optimization
Tables.schema(x::MyTable): Returns aTables.Schemaobject (column names and types) ornothing.Tables.materializer(::Type{MyTable}): A sink function to construct your type from Tables.jl input.Tables.subset(x::MyTable, inds; viewhint): Returns a row or sub-table.DataAPI.nrow(x::MyTable): Returns the number of rows.DataAPI.ncol(x::MyTable): Returns the number of columns.
# Example: Minimal Row-oriented setup struct MyTable # ... fields end Tables.istable(::Type{MyTable}) = true Tables.rowaccess(::Type{MyTable}) = true Tables.rows(x::MyTable) = # return an iterator of AbstractRowPerform round-trip testing with Tables.matrix
mainTo ensure your implementation is robust, perform "round-trip" testing by converting between different table formats and plain Julia
Matrixobjects usingTables.matrix.- Source to Sink: Convert a table (like a
VectorofNamedTuples or aNamedTupleofVectors) into aMatrixusingTables.matrix(table). - Sink to Source: Convert a
Matrixback into a table usingTables.table(matrix)and then userowtableorcolumntableto transform it into specific row-oriented or column-oriented formats.
Note that
VectorofNamedTuples acts as a default row table, andNamedTupleofVectors acts as a default column table.# Define test data rt = [(a=1, b=4.0, c="7"), (a=2, b=5.0, c="8"), (a=3, b=6.0, c="9")] # Row table ct = (a=[1,2,3], b=[4.0, 5.0, 6.0]) # Column table mat_input = [1 4.0 "7"; 2 5.0 "8"; 3 6.0 "9"] # 1. Test consuming a row table mat = Tables.matrix(rt) @test mat[:, 1] == [1, 2, 3] @test size(mat) == (3, 3) # 2. Test consuming a column table mat2 = Tables.matrix(ct) @test mat2[:, 1] == ct.a # 3. Test round-trip: Matrix -> Column Table tbl = Tables.table(mat_input) |> columntable @test keys(tbl) == (:Column1, :Column2, :Column3) # 4. Test round-trip: Matrix -> Row Table tbl2 = Tables.table(mat2) |> rowtable @test length(tbl2) == 3 @test map(x->x.Column1, tbl2) == [1.0, 2.0, 3.0]- Source to Sink: Convert a table (like a
Test a Tables.jl implementation
mainWhen implementing a new table source, you should verify that your type correctly responds to the
Tables.jlinterface. Key checks include:- Identity and Access: Use
Tables.istable(typeof(your_type))to confirm it is recognized as a table, and checkTables.rowaccessandTables.columnaccessto see which interface is supported. - Row/Column Access: Verify that
Tables.rows(your_table)andTables.columns(your_table)return the expected objects. - Column Interface: Test
Tables.getcolumn(table, name_or_index),Tables.columnnames(table), and ensure property access (e.g.,table.ColumnName) works if applicable. - Row Interface: Iterate through rows and verify that
eltype(rows)matches the type of the row object, and thatTables.getcolumn(row, name_or_index)works correctly on the row itself. - Property Consistency: Ensure
propertynames(table)matchespropertynames(row).
mat = [1 4.0 "7"; 2 5.0 "8"; 3 6.0 "9"] # first, create a MatrixTable from our matrix input mattbl = Tables.table(mat) # test that the MatrixTable `istable` @test Tables.istable(typeof(mattbl)) # test that it defines row access @test Tables.rowaccess(typeof(mattbl)) matrows = Tables.rows(mattbl) @test Tables.rows(matrows) === matrows # test that it defines column access @test Tables.columnaccess(typeof(mattbl)) @test Tables.columns(mattbl) === mattbl # test that we can access the first "column" of our matrix table by column name @test mattbl.Column1 == [1,2,3] # test our `Tables.AbstractColumns` interface methods @test Tables.getcolumn(mattbl, :Column1) == [1,2,3] @test Tables.getcolumn(mattbl, 1) == [1,2,3] @test Tables.columnnames(mattbl) == [:Column1, :Column2, :Column3] # now let's iterate our MatrixRowTable to get our first MatrixRow matrow = first(matrows) @test eltype(matrows) == typeof(matrow) # now we can test our `Tables.AbstractRow` interface methods on our MatrixRow @test matrow.Column1 == 1 @test Tables.getcolumn(matrow, :Column1) == 1 @test Tables.getcolumn(matrow, 1) == 1 @test propertynames(mattbl) == propertynames(matrow) == [:Column1, :Column2, :Column3]- Identity and Access: Use
Configure validation for unmatched references
mainThe
validatekeyword controls howTables.Scanhandles references to columns that do not exist in the source schema.validate=true(default): Rejects any selection or filter reference that does not match the source schema.validate=false: Supports schema evolution. Unmatched selections are dropped, and unmatched filter columns behave as an all-missing column.
Complete implementation example: MatrixTable
mainThis example demonstrates how to wrap an
AbstractVecOrMatto make it a valid Tables.jl source by implementing both row and column interfaces.struct MatrixTable{T <: AbstractVecOrMat} <: Tables.AbstractColumns names::Vector{Symbol} lookup::Dict{Symbol, Int} matrix::T end # 1. Table Declaration Tables.istable(::Type{<:MatrixTable}) = true # 2. Schema Tables.schema(m::MatrixTable{T}) where {T} = Tables.Schema(names(m), fill(eltype(T), size(matrix(m), 2))) # 3. Column Interface Tables.columnaccess(::Type{<:MatrixTable}) = true Tables.columns(m::MatrixTable) = m Tables.getcolumn(m::MatrixTable, ::Type{T}, col::Int, nm::Symbol) where {T} = matrix(m)[:, col] Tables.getcolumn(m::MatrixTable, nm::Symbol) = matrix(m)[:, lookup(m)[nm]] Tables.getcolumn(m::MatrixTable, i::Int) = matrix(m)[:, i] Tables.columnnames(m::MatrixTable) = names(m) # 4. Row Interface Tables.rowaccess(::Type{<:MatrixTable}) = true Tables.rows(m::MatrixTable) = m # Iteration Interface Base.eltype(m::MatrixTable{T}) where {T} = MatrixRow{T} Base.length(m::MatrixTable) = size(matrix(m), 1) Base.iterate(m::MatrixTable, st=1) = st > length(m) ? nothing : (MatrixRow(st, m), st + 1) struct MatrixRow{T} <: Tables.AbstractRow row::Int source::MatrixTable{T} end getcolumn(m::MatrixRow, ::Type, col::Int, nm::Symbol) = getfield(getfield(m, :source), :matrix)[getfield(m, :row), col] getcolumn(m::MatrixRow, i::Int) = getfield(getfield(m, :source), :matrix)[getfield(m, :row), i] getcolumn(m::MatrixRow, nm::Symbol) = getfield(getfield(m, :source), :matrix)[getfield(m, :row), getfield(getfield(m, :source), :lookup)[nm]] columnnames(m::MatrixRow) = names(getfield(m, :source))Access data row-by-row with Tables.rows
mainUse
Tables.rows(x)to obtain a row iterator for any Tables.jl-compatible sourcex. This allows you to iterate through the table one row at a time.To access specific values within a row, you can use
Tables.getcolumn(row, col_name). For more efficient processing, useTables.eachcolumnto apply a function to every column value in a row without manually iterating over column names.# Get a row iterator rows = Tables.rows(x) # Iterate through rows for row in rows # Access specific column values val = Tables.getcolumn(row, :column_name) end # Efficiently process each column in a row using eachcolumn Tables.eachcolumn(schema, row) do val, i, nm # val: the value # i: the column index # nm: the column name end