diff --git a/src/rows.jl b/src/rows.jl index 92190b3..f028f27 100644 --- a/src/rows.jl +++ b/src/rows.jl @@ -105,22 +105,25 @@ struct UnknownSchemaError <: Exception end function Base.showerror(io::IO, e::UnknownSchemaError) - print(io, """ - encountered unknown `Legolas.Schema` type: $(e.schema) - - This generally indicates that this schema has not been defined (i.e. - the schema's corresponding `@row` statement has not been executed) in - the current Julia session. - - In practice, this can arise if you try to read a Legolas table with a - prescribed schema, but haven't actually loaded the schema definition - (or commonly, haven't loaded the dependency that contains the schema - definition - check the versions of loaded packages/modules to confirm - your environment is as expected). - - Note that if you're in this particular situation, you can still load - the raw table as-is without Legolas; e.g., to load an Arrow table, call `Arrow.Table(path)`. - """) + print( + io, + """ + encountered unknown `Legolas.Schema` type: $(e.schema) + + This generally indicates that this schema has not been defined (i.e. + the schema's corresponding `@row` statement has not been executed) in + the current Julia session. + + In practice, this can arise if you try to read a Legolas table with a + prescribed schema, but haven't actually loaded the schema definition + (or commonly, haven't loaded the dependency that contains the schema + definition - check the versions of loaded packages/modules to confirm + your environment is as expected). + + Note that if you're in this particular situation, you can still load + the raw table as-is without Legolas; e.g., to load an Arrow table, call `Arrow.Table(path)`. + """ + ) return nothing end @@ -227,6 +230,26 @@ function Base.show(io::IO, row::Row) return nothing end +""" + schema_field_names(::Type{<:Legolas.Schema}) + +Get a tuple with the names of the fields of this `Legolas.Schema`, including names that +have been inherited from this `Legolas.Schema`'s parent schema. +""" +schema_field_names(s::Schema) = schema_field_names(typeof(s)) +schema_field_names(::Row{S}) where {S} = schema_field_names(S) +schema_field_names(::Type{<:Row{S}}) where {S} = schema_field_names(S) + +""" + schema_field_types(::Legolas.Schema{name,version}) + +Get a tuple with the types of the fields of this `Legolas.Schema`, including types of fields that +have been inherited from this `Legolas.Schema`'s parent schema. +""" +schema_field_types(s::Schema) = schema_field_types(typeof(s)) +schema_field_types(::Row{S}) where {S} = schema_field_types(S) +schema_field_types(::Type{<:Row{S}}) where {S} = schema_field_types(S) + function _parse_schema_expr(x) if x isa Expr && x.head == :call && x.args[1] == :> && length(x.args) == 3 child, _ = _parse_schema_expr(x.args[2]) @@ -277,14 +300,21 @@ macro row(schema_expr, fields...) name, type = f.args[1].args return :(validate_expected_field(tables_schema, $(Base.Meta.quot(name)), $(esc(type)))) end - field_names = [esc(f.args[1].args[1]) for f in fields] + field_exprs = [f.args[1] for f in fields] + field_names = [e.args[1] for e in field_exprs] + field_types = [e.args[2] for e in field_exprs] + escaped_field_names = map(esc, field_names) schema_type = Base.Meta.quot(typeof(schema)) quoted_parent = Base.Meta.quot(parent) schema_qualified_string = string(schema_name(schema), '@', schema_version(schema)) + schema_field_names = Expr(:tuple, map(QuoteNode, field_names)...) + schema_field_types = Expr(:tuple, field_types...) parent_transform = nothing parent_validate = nothing if !isnothing(parent) schema_qualified_string = :(string($schema_qualified_string, '>', Legolas.schema_qualified_string($quoted_parent))) + schema_field_names = :(($schema_field_names..., Legolas.schema_field_names($quoted_parent)...)) + schema_field_types = :(($schema_field_types..., Legolas.schema_field_types($quoted_parent)...)) parent_transform = :(fields = transform($quoted_parent; fields...)) parent_validate = :(validate(tables_schema, $quoted_parent)) end @@ -292,12 +322,14 @@ macro row(schema_expr, fields...) legolas_row_arrow_name = :(Symbol("JuliaLang.", $schema_qualified_string)) return quote Legolas.schema_qualified_string(::$schema_type) = $schema_qualified_string + Legolas.schema_field_names(::Type{$schema_type}) = $schema_field_names + Legolas.schema_field_types(::Type{$schema_type}) = $schema_field_types Legolas.schema_parent(::Type{<:$schema_type}) = $quoted_parent - function Legolas._transform(::$schema_type; $([Expr(:kw, f, :missing) for f in field_names]...), other...) + function Legolas._transform(::$schema_type; $([Expr(:kw, f, :missing) for f in escaped_field_names]...), other...) $(map(esc, fields)...) - return (; $([Expr(:kw, f, f) for f in field_names]...), other...) + return (; $([Expr(:kw, f, f) for f in escaped_field_names]...), other...) end function Legolas._validate(tables_schema::Tables.Schema, legolas_schema::$schema_type) diff --git a/test/runtests.jl b/test/runtests.jl index 7d8ae1c..465a613 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -154,7 +154,7 @@ end @test propertynames(r) == (:z, :x, :y) @test r === Row(Schema("bar", 1), r) @test r === Row(Schema("bar", 1); x=1, y=2, z=3) - @test r === Row(Schema("bar", 1), first(Tables.rows(Arrow.Table(Arrow.tobuffer((x=[1],y=[2],z=[3])))))) + @test r === Row(Schema("bar", 1), first(Tables.rows(Arrow.Table(Arrow.tobuffer((x=[1], y=[2], z=[3])))))) @test r[1] === 3 @test string(r) == "Row(Schema(\"bar@1\"), (z = 3, x = 1, y = 2))" @@ -164,7 +164,7 @@ end long_row = Row(Schema("bar", 1), (x=1, y=2, z=zeros(100, 100))) @test length(sprint(show, long_row; context=(:limit => true))) < 200 - @test_throws Legolas.UnknownSchemaError Legolas.transform(Legolas.Schema("imadethisup@3"); a = 1, b = 2) + @test_throws Legolas.UnknownSchemaError Legolas.transform(Legolas.Schema("imadethisup@3"); a=1, b=2) @test_throws Legolas.UnknownSchemaError Legolas.validate(Tables.Schema((:a, :b), (Int, Int)), Legolas.Schema("imadethisup@3")) @test_throws Legolas.UnknownSchemaError Legolas.schema_qualified_string(Legolas.Schema("imadethisup@3")) @@ -176,15 +176,54 @@ end @test all(tbl.schema .== schemas) end +@testset "schema field name and type tests" begin + Parent = @row("parent@1", + first_parent_field::Int=1, + second_parent_field::String="second") + + parent_fields = (:first_parent_field, :second_parent_field) + parent_field_types = (Int, String) + + @test Legolas.schema_field_names(Schema{:parent,1}) == parent_fields + @test Legolas.schema_field_names(Schema("parent@1")) == parent_fields + @test Legolas.schema_field_names(Parent()) == parent_fields + @test Legolas.schema_field_names(Parent) == parent_fields + + @test Legolas.schema_field_types(Schema{:parent,1}) == parent_field_types + @test Legolas.schema_field_types(Schema("parent@1")) == parent_field_types + @test Legolas.schema_field_types(Parent()) == parent_field_types + @test Legolas.schema_field_types(Parent) == parent_field_types + + Child = @row("child@1" > "parent@1", + first_child_field::Symbol=:first, + second_child_field="I can be anything") + + child_fields = (:first_child_field, :second_child_field, parent_fields...) + child_field_types = (Symbol, Any, parent_field_types...) + + @test Legolas.schema_field_names(Schema{:child,1}) == child_fields + @test Legolas.schema_field_names(Schema("child@1")) == child_fields + @test Legolas.schema_field_names(Child()) == child_fields + @test Legolas.schema_field_names(Child) == child_fields + + @test Legolas.schema_field_types(Schema{:child,1}) == child_field_types + @test Legolas.schema_field_types(Schema("child@1")) == child_field_types + @test Legolas.schema_field_types(Child()) == child_field_types + @test Legolas.schema_field_types(Child) == child_field_types + + @test_throws MethodError Legolas.schema_field_names(Legolas.Schema("imadethisup@3")) + @test_throws MethodError Legolas.schema_field_types(Legolas.Schema("imadethisup@3")) +end + @testset "isequal, hash" begin TestRow = @row("testrow@1", x, y) - foo = TestRow(; x = [1]) - foo2 = TestRow(; x = [1]) + foo = TestRow(; x=[1]) + foo2 = TestRow(; x=[1]) @test isequal(foo, foo2) @test hash(foo) == hash(foo2) - foo3 = TestRow(; x = [3]) + foo3 = TestRow(; x=[3]) @test !isequal(foo, foo3) @test hash(foo) != hash(foo3) end @@ -195,7 +234,7 @@ const MyOuterRow = @row("my-outer-schema@1", x::MyInnerRow=MyInnerRow(x)) @testset "Nested arrow serialization" begin - table = [MyOuterRow(; a="outer_a", x = MyInnerRow())] + table = [MyOuterRow(; a="outer_a", x=MyInnerRow())] roundtripped_table = Legolas.read(Legolas.tobuffer(table, Legolas.Schema("my-outer-schema@1"))) @test table == MyOuterRow.(Tables.rows(roundtripped_table)) end