diff --git a/docs/src/considerations.md b/docs/src/considerations.md index 8b9ab4e0..c8b4b4a1 100644 --- a/docs/src/considerations.md +++ b/docs/src/considerations.md @@ -96,3 +96,34 @@ The interface is implemented in [`src/tables.jl`](https://github.com/yeesian/Arc * `ArchGDAL.FeatureLayer` meets the criteria for an `AbstractRow`-iterator based on the previous bullet and meeting the criteria for [`Iteration`](https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-iteration) in [`base/iterators.jl`](https://github.com/yeesian/ArchGDAL.jl/blob/a665f3407930b8221269f8949c246db022c3a85c/src/base/iterators.jl#L1-L18). * `ArchGDAL.AbstractDataset` might contain multiple layers, and might correspond to multiple tables. The way to construct tables would be to get the layers before forming the corresponding tables. +## Missing and Null Semantics in GDAL + +When reading the fields of a feature using `getfield(feature, i)`, ArchGDAL observes the following behavior: + +| Field | null | notnull | +|-------|-----------|-----------| +| set | `missing` | value | +| unset | N/A | `nothing` | + +This reflects that +* a field that is notnull will never return `missing`: use `isfieldnull(feature, i)` to determine if a field has been set. +* a field is set will never return `nothing` (and a field that unset will always return `nothing`): use `isfieldset(feature, i)` to determine if a field has been set. +* a field that is set and not null will always have a concrete value: use `isfieldsetandnotnull(feature, i)` to test for it. + +When writing the fields of a feature using `setfield!(feature, i, value)`, ArchGDAL observes the following behavior: + +| Field | nullable | notnullable | +|-----------|----------|----------------| +| `nothing` | unset | unset | +| `missing` | null | `getdefault()` | +| value | value | value | + +This reflects that +* writing `nothing` will cause the field to be unset. +* writing `missing` will cause the field to be null. In the cause of a notnullable field, it will take the default value (see https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html for details). If there is no default value, `getdefault()` will return `nothing`, causing the field to be unset. +* writing a value will behave in the usual manner. + +For additional references, see +* [JuliaLang: Nothingness and missing values](https://docs.julialang.org/en/v1/manual/faq/#faq-nothing) +* [GDAL: OGR not-null constraints and default values](https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html) +* [GDAL: Null values in OGR](https://gdal.org/development/rfc/rfc67_nullfieldvalues.html) diff --git a/src/base/display.jl b/src/base/display.jl index 64b6e435..3f7cbe11 100644 --- a/src/base/display.jl +++ b/src/base/display.jl @@ -149,7 +149,7 @@ function Base.show(io::IO, layer::AbstractFeatureLayer)::Nothing nfielddisplay = min(n, 5) for i in 1:nfielddisplay fd = getfielddefn(featuredefn, i - 1) - display = " Field $(i - 1) ($(getname(fd))): [$(gettype(fd))]" + display = " Field $(i - 1) ($(getname(fd))): [$(getfieldtype(fd))]" if length(display) > 75 println(io, "$display[1:70]...") continue diff --git a/src/ogr/feature.jl b/src/ogr/feature.jl index 348af72f..e07aef97 100644 --- a/src/ogr/feature.jl +++ b/src/ogr/feature.jl @@ -149,6 +149,59 @@ function unsetfield!(feature::Feature, i::Integer)::Feature return feature end +""" + isfieldnull(feature::Feature, i::Integer) + +Test if a field is null. + +### Parameters +* `feature`: the feature that owned the field. +* `i`: the field to test, from 0 to GetFieldCount()-1. + +### Returns +`true` if the field is null, otherwise `false`. + +### References +* https://gdal.org/development/rfc/rfc67_nullfieldvalues.html +""" +isfieldnull(feature::Feature, i::Integer)::Bool = + Bool(GDAL.ogr_f_isfieldnull(feature.ptr, i)) + +""" + isfieldsetandnotnull(feature::Feature, i::Integer) + +Test if a field is set and not null. + +### Parameters +* `feature`: the feature that owned the field. +* `i`: the field to test, from 0 to GetFieldCount()-1. + +### Returns +`true` if the field is set and not null, otherwise `false`. + +### References +* https://gdal.org/development/rfc/rfc67_nullfieldvalues.html +""" +isfieldsetandnotnull(feature::Feature, i::Integer)::Bool = + Bool(GDAL.ogr_f_isfieldsetandnotnull(feature.ptr, i)) + +""" + setfieldnull!(feature::Feature, i::Integer) + +Clear a field, marking it as null. + +### Parameters +* `feature`: the feature that owned the field. +* `i`: the field to set to null, from 0 to GetFieldCount()-1. + +### References +* https://gdal.org/development/rfc/rfc67_nullfieldvalues.html +""" +function setfieldnull!(feature::Feature, i::Integer)::Feature + GDAL.ogr_f_setfieldnull(feature.ptr, i) + return feature +end + # """ # OGR_F_GetRawFieldRef(OGRFeatureH hFeat, # int iField) -> OGRField * @@ -381,7 +434,7 @@ end # pfSecond,pnTZFlag) # end -function getdefault(feature::Feature, i::Integer)::Union{String,Missing} +function getdefault(feature::Feature, i::Integer)::Union{String,Nothing} return getdefault(getfielddefn(feature, i)) end @@ -402,13 +455,33 @@ const _FETCHFIELD = Dict{OGRFieldType,Function}( OFTInteger64List => asint64list, ) +""" + getfield(feature, i) + +When the field is unset, it will return `nothing`. When the field is set but +null, it will return `missing`. + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html +* https://gdal.org/development/rfc/rfc67_nullfieldvalues.html +""" function getfield(feature::Feature, i::Integer) - return if isfieldset(feature, i) - _fieldtype = gettype(getfielddefn(feature, i)) - _fetchfield = get(_FETCHFIELD, _fieldtype, getdefault) - _fetchfield(feature, i) + return if !isfieldset(feature, i) + nothing + elseif isfieldnull(feature, i) + missing else - getdefault(feature, i) + _fieldtype = getfieldtype(getfielddefn(feature, i)) + try + _fetchfield = _FETCHFIELD[_fieldtype] + _fetchfield(feature, i) + catch e + if e isa KeyError + error("$_fieldtype not implemented in getfield, please report an issue to https://github.com/yeesian/ArchGDAL.jl/issues") + else + rethrow(e) + end + end end end @@ -439,6 +512,20 @@ field types may be unaffected. """ function setfield! end +function setfield!(feature::Feature, i::Integer, value::Nothing)::Feature + unsetfield!(feature, i) + return feature +end + +function setfield!(feature::Feature, i::Integer, value::Missing)::Feature + if isnullable(getfielddefn(feature, i)) + setfieldnull!(feature, 1) + else + setfield!(feature, i, getdefault(feature, i)) + end + return feature +end + function setfield!(feature::Feature, i::Integer, value::Int32)::Feature GDAL.ogr_f_setfieldinteger(feature.ptr, i, value) return feature @@ -854,6 +941,9 @@ Fill unset fields with default values that might be defined. * `feature`: handle to the feature. * `notnull`: if we should fill only unset fields with a not-null constraint. * `papszOptions`: unused currently. Must be set to `NULL`. + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html """ function fillunsetwithdefault!( feature::Feature; @@ -885,6 +975,9 @@ fails, then it will fail for all interpretations). ### Returns `true` if all enabled validation tests pass. + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html """ validate(feature::Feature, flags::FieldValidation, emiterror::Bool)::Bool = Bool(GDAL.ogr_f_validate(feature.ptr, flags, emiterror)) diff --git a/src/ogr/fielddefn.jl b/src/ogr/fielddefn.jl index ec183f55..2e964b7d 100644 --- a/src/ogr/fielddefn.jl +++ b/src/ogr/fielddefn.jl @@ -65,12 +65,38 @@ OGRFeatureDefn. ### Parameters * `fielddefn`: handle to the field definition to set type to. * `subtype`: the new field subtype. + +### References +* https://gdal.org/development/rfc/rfc50_ogr_field_subtype.html """ function setsubtype!(fielddefn::FieldDefn, subtype::OGRFieldSubType)::FieldDefn GDAL.ogr_fld_setsubtype(fielddefn.ptr, subtype) return fielddefn end +""" + getfieldtype(fielddefn::AbstractFieldDefn) + +Returns the type or subtype (if any) of this field. + +### Parameters +* `fielddefn`: handle to the field definition. + +### Returns +The field type or subtype. + +### References +* https://gdal.org/development/rfc/rfc50_ogr_field_subtype.html +""" +function getfieldtype(fielddefn::AbstractFieldDefn)::Union{OGRFieldType, OGRFieldSubType} + fieldsubtype = getsubtype(fielddefn) + return if fieldsubtype != OFSTNone + fieldsubtype + else + gettype(fielddefn) + end +end + """ getjustify(fielddefn::AbstractFieldDefn) @@ -196,6 +222,9 @@ Even if this method returns `false` (i.e not-nullable field), it doesn't mean that OGRFeature::IsFieldSet() will necessarily return `true`, as fields can be temporarily unset and null/not-null validation is usually done when OGRLayer::CreateFeature()/SetFeature() is called. + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html """ isnullable(fielddefn::AbstractFieldDefn)::Bool = Bool(GDAL.ogr_fld_isnullable(fielddefn.ptr)) @@ -210,8 +239,11 @@ to set a not-null constraint. Drivers that support writing not-null constraint will advertize the GDAL_DCAP_NOTNULL_FIELDS driver metadata item. + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html """ -function setnullable!(fielddefn::FieldDefn, nullable::Bool)::FieldDefn +function setnullable!(fielddefn::T, nullable::Bool)::T where {T <: AbstractFieldDefn} GDAL.ogr_fld_setnullable(fielddefn.ptr, nullable) return fielddefn end @@ -220,12 +252,15 @@ end getdefault(fielddefn::AbstractFieldDefn) Get default field value + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html """ -function getdefault(fielddefn::AbstractFieldDefn)::Union{String,Missing} +function getdefault(fielddefn::AbstractFieldDefn)::Union{String,Nothing} result = @gdal(OGR_Fld_GetDefault::Cstring, fielddefn.ptr::GDAL.OGRFieldDefnH) return if result == C_NULL - missing + nothing else unsafe_string(result) end @@ -252,6 +287,9 @@ datetime literal value, format should be 'YYYY/MM/DD HH:MM:SS[.sss]' Drivers that support writing DEFAULT clauses will advertize the GDAL_DCAP_DEFAULT_FIELDS driver metadata item. + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html """ function setdefault!(fielddefn::T, default)::T where {T<:AbstractFieldDefn} GDAL.ogr_fld_setdefault(fielddefn.ptr, default) @@ -266,6 +304,9 @@ Returns whether the default value is driver specific. Driver specific default values are those that are not NULL, a numeric value, a literal value enclosed between single quote characters, CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE or datetime literal value. + +### References +* https://gdal.org/development/rfc/rfc53_ogr_notnull_default.html """ isdefaultdriverspecific(fielddefn::AbstractFieldDefn)::Bool = Bool(GDAL.ogr_fld_isdefaultdriverspecific(fielddefn.ptr)) diff --git a/src/types.jl b/src/types.jl index 4ef8bdd9..a2936a2b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -619,6 +619,9 @@ getname(obj::OGRFieldType)::String = GDAL.ogr_getfieldtypename(obj) getname(obj::OGRFieldSubType) Fetch human readable name for a field subtype. + +### References +* https://gdal.org/development/rfc/rfc50_ogr_field_subtype.html """ getname(obj::OGRFieldSubType)::String = GDAL.ogr_getfieldsubtypename(obj) @@ -626,6 +629,9 @@ getname(obj::OGRFieldSubType)::String = GDAL.ogr_getfieldsubtypename(obj) arecompatible(dtype::OGRFieldType, subtype::OGRFieldSubType) Return if type and subtype are compatible. + +### References +* https://gdal.org/development/rfc/rfc50_ogr_field_subtype.html """ arecompatible(dtype::OGRFieldType, subtype::OGRFieldSubType)::Bool = Bool(GDAL.ogr_aretypesubtypecompatible(dtype, subtype)) diff --git a/test/test_feature.jl b/test/test_feature.jl index 3f7a31b9..40516d03 100644 --- a/test/test_feature.jl +++ b/test/test_feature.jl @@ -90,15 +90,96 @@ const AG = ArchGDAL; @test AG.validate(f, AG.F_VAL_ALLOW_DIFFERENT_GEOM_DIM, false) == true - @test AG.getfield(f, 1) == "point-a" - @test ismissing(AG.getdefault(f, 1)) - AG.setdefault!(AG.getfielddefn(f, 1), "default value") - @test AG.getdefault(f, 1) == "default value" - @test AG.getfield(f, 1) == "point-a" - AG.unsetfield!(f, 1) - @test AG.getfield(f, 1) == "default value" - AG.fillunsetwithdefault!(f, notnull = false) - @test AG.getfield(f, 1) == AG.getdefault(f, 1) + @testset "Missing and Null Semantics" begin + @test isnothing(AG.getdefault(f, 1)) + AG.setdefault!(AG.getfielddefn(f, 1), "default value") + @test AG.getdefault(f, 1) == "default value" + + @test AG.isfieldsetandnotnull(f, 1) + @test AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) + @test AG.getfield(f, 1) == "point-a" + + AG.unsetfield!(f, 1) + @test !AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) # carried over from earlier + @test isnothing(AG.getfield(f, 1)) + + # unset & notnull: missing + AG.fillunsetwithdefault!(f) + # nothing has changed + @test isnothing(AG.getfield(f, 1)) + # because it is a nullable field + @test AG.isnullable(AG.getfielddefn(f, 1)) + # even though it is not a null value + @test !AG.isfieldnull(f, 1) + # the field is still not set + @test !AG.isfieldset(f, 1) + + # set & notnull: value + AG.fillunsetwithdefault!(f, notnull = false) + # now the field is set to the default + @test AG.getfield(f, 1) == AG.getdefault(f, 1) + @test !AG.isfieldnull(f, 1) # still as expected + @test AG.isfieldset(f, 1) # the field is now set + + # set the field to be notnullable + AG.setnullable!(AG.getfielddefn(f, 1), false) + # now if we unset the field + AG.unsetfield!(f, 1) + @test !AG.isfieldnull(f, 1) + @test !AG.isfieldset(f, 1) + @test isnothing(AG.getfield(f, 1)) + # and we fill unset with default again + AG.fillunsetwithdefault!(f) + # the field is set to the default + @test AG.getfield(f, 1) == AG.getdefault(f, 1) + + # set & null: missing + @test !AG.isfieldnull(f, 1) + @test AG.isfieldset(f, 1) + AG.setfieldnull!(f, 1) + @test AG.isfieldnull(f, 1) + @test AG.isfieldset(f, 1) + @test ismissing(AG.getfield(f, 1)) + + # unset & null: N/A (but nothing otherwise) + AG.unsetfield!(f, 1) + # Observe that OGRUnset and OGRNull are mutually exclusive + @test !AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) # notice the field is notnull + + # setting the field for a notnullable column + AG.setnullable!(AG.getfielddefn(f, 1), false) + AG.setfield!(f, 1, "value") + @test AG.getfield(f, 1) == "value" + @test AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) + AG.setfield!(f, 1, missing) + @test AG.getfield(f, 1) == AG.getdefault(f, 1) + @test AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) + AG.setfield!(f, 1, nothing) + @test isnothing(AG.getfield(f, 1)) + @test !AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) + + # setting the field for a nullable column + AG.setnullable!(AG.getfielddefn(f, 1), true) + AG.setfield!(f, 1, "value") + @test AG.getfield(f, 1) == "value" + @test AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) + AG.setfield!(f, 1, missing) + @test ismissing(AG.getfield(f, 1)) + @test AG.isfieldset(f, 1) + @test AG.isfieldnull(f, 1) # different from that of notnullable + AG.setfield!(f, 1, nothing) + @test isnothing(AG.getfield(f, 1)) + @test !AG.isfieldset(f, 1) + @test !AG.isfieldnull(f, 1) + + end end end @@ -157,6 +238,10 @@ const AG = ArchGDAL; AG.setfield!(feature, 9, Int16(1)) AG.setfield!(feature, 10, Int32(1)) AG.setfield!(feature, 11, Float32(1.0)) + for i in 1:AG.nfield(feature) + @test !AG.isfieldnull(feature, i-1) + @test AG.isfieldsetandnotnull(feature, i-1) + end @test sprint(print, AG.getgeom(feature)) == "NULL Geometry" AG.getgeom(feature) do geom @test sprint(print, geom) == "NULL Geometry" @@ -169,6 +254,10 @@ const AG = ArchGDAL; AG.addfeature(layer) do newfeature AG.setfrom!(newfeature, feature) @test AG.getfield(newfeature, 0) == 1 + for i in 1:AG.nfield(newfeature) + @test !AG.isfieldnull(newfeature, i-1) + @test AG.isfieldsetandnotnull(newfeature, i-1) + end @test AG.getfield(newfeature, 1) ≈ 1.0 @test AG.getfield(newfeature, 2) == Int32[1, 2] @test AG.getfield(newfeature, 3) == Int64[1, 2] @@ -209,6 +298,13 @@ const AG = ArchGDAL; @test AG.getfield(newfeature, 0) == 45 @test AG.getfield(newfeature, 1) ≈ 18.2 @test AG.getfield(newfeature, 5) == String["foo", "bar"] + + @test AG.isfieldsetandnotnull(newfeature, 5) + AG.setfieldnull!(newfeature, 5) + @test !AG.isfieldsetandnotnull(newfeature, 5) + @test AG.isfieldset(newfeature, 5) + @test AG.isfieldnull(newfeature, 5) + @test ismissing(AG.getfield(newfeature, 5)) end @test AG.nfeature(layer) == 1 end diff --git a/test/test_fielddefn.jl b/test/test_fielddefn.jl index 88fa2688..91faacba 100644 --- a/test/test_fielddefn.jl +++ b/test/test_fielddefn.jl @@ -10,14 +10,19 @@ const AG = ArchGDAL; AG.setname!(fd, "newname") @test AG.getname(fd) == "newname" @test AG.gettype(fd) == AG.OFTInteger + @test AG.getfieldtype(fd) == AG.OFTInteger AG.settype!(fd, AG.OFTDate) @test AG.gettype(fd) == AG.OFTDate + @test AG.getfieldtype(fd) == AG.OFTDate AG.settype!(fd, AG.OFTInteger) @test AG.getsubtype(fd) == AG.OFSTNone + @test AG.getfieldtype(fd) == ArchGDAL.OFTInteger AG.setsubtype!(fd, AG.OFSTInt16) @test AG.getsubtype(fd) == AG.OFSTInt16 + @test AG.getfieldtype(fd) == AG.OFSTInt16 AG.setsubtype!(fd, AG.OFSTBoolean) @test AG.getsubtype(fd) == AG.OFSTBoolean + @test AG.getfieldtype(fd) == AG.OFSTBoolean AG.setsubtype!(fd, AG.OFSTNone) @test AG.getjustify(fd) == AG.OJUndefined AG.setjustify!(fd, AG.OJLeft) @@ -54,7 +59,7 @@ const AG = ArchGDAL; AG.setnullable!(fd, true) @test AG.isnullable(fd) == true - @test ismissing(AG.getdefault(fd)) + @test isnothing(AG.getdefault(fd)) AG.setdefault!(fd, "0001/01/01 00:00:00") @test AG.getdefault(fd) == "0001/01/01 00:00:00" @test AG.isdefaultdriverspecific(fd) == true diff --git a/test/test_tables.jl b/test/test_tables.jl index aa79f948..db5a9a5d 100644 --- a/test/test_tables.jl +++ b/test/test_tables.jl @@ -218,7 +218,7 @@ using Tables (i, i + 1) for i in 3.0:5.0 ]), ), - emptygeom = (name = "emptygeom", geom = nothing), + emptygeom = (name = "emptygeom", geom = missing), emptyfield = ( name = "emptyid", geom = AG.createlinestring([ @@ -250,7 +250,7 @@ using Tables (-1.0, -1.0), ]), ), - emptygeom = (name = "emptygeom", geom = nothing), + emptygeom = (name = "emptygeom", geom = missing), emptyfield = ( name = "emptyid", geom = AG.createpolygon([ @@ -334,7 +334,7 @@ using Tables end if withmissingfield AG.addfeature(newlayer) do newfeature - # No Id field set + AG.setfieldnull!(newfeature, id_idx) AG.setfield!( newfeature, name_idx, @@ -408,17 +408,13 @@ using Tables end # Helper functions - function toWKT_withmissings(x) - if ismissing(x) - return missing - elseif typeof(x) <: AG.AbstractGeometry - return AG.toWKT(x) - else - return x - end - end - function columntablevalues_toWKT(x) - return Tuple(toWKT_withmissings.(x[i]) for i in 1:length(x)) + wellknownvalue(obj::Any) = obj + wellknownvalue(obj::AG.AbstractGeometry) = AG.toWKT(obj) + wellknownvalue(obj::AG.AbstractSpatialRef) = AG.toWKT(obj) + wellknownvalue(obj::Missing)::Missing = missing + wellknownvalue(obj::Nothing)::Nothing = nothing + function wellknownvalues(x)::Tuple + return Tuple(wellknownvalue.(x[i]) for i in 1:length(x)) end tupleoftuples_equal = ( (x, y) -> @@ -461,7 +457,7 @@ using Tables return ( names = keys(Tables.columntable(layer)), types = eltype.(values(Tables.columntable(layer)),), - values = columntablevalues_toWKT( + values = wellknownvalues( values(Tables.columntable(layer)), ), ) @@ -488,7 +484,7 @@ using Tables withmissingfield::Bool, withmixedgeomtypes::Bool, reference_geotable::NamedTuple, - )::Bool + ) map_on_test_dataset( drvshortname, geomfamilly; @@ -497,18 +493,14 @@ using Tables withmixedgeomtypes = withmixedgeomtypes, ) do ds layer = AG.getlayer(ds, 0) - return all([ - keys(Tables.columntable(layer)) == - reference_geotable.names, - eltype.(values(Tables.columntable(layer))) == - reference_geotable.types, - tupleoftuples_equal( - columntablevalues_toWKT( - values(Tables.columntable(layer)), - ), - reference_geotable.values, + @test keys(Tables.columntable(layer)) == reference_geotable.names + @test eltype.(values(Tables.columntable(layer))) == reference_geotable.types + @test tupleoftuples_equal( + wellknownvalues( + values(Tables.columntable(layer)), ), - ]) + reference_geotable.values, + ) end end @@ -524,24 +516,21 @@ using Tables function test_layer_to_table( layer::AG.AbstractFeatureLayer, reference_geotable::NamedTuple, - )::Bool - return all([ - keys(Tables.columntable(layer)) == reference_geotable.names, - eltype.(values(Tables.columntable(layer))) == - reference_geotable.types, - tupleoftuples_equal( - columntablevalues_toWKT( - values(Tables.columntable(layer)), - ), - reference_geotable.values, + ) + @test keys(Tables.columntable(layer)) == reference_geotable.names + @test eltype.(values(Tables.columntable(layer))) == reference_geotable.types + @test tupleoftuples_equal( + wellknownvalues( + values(Tables.columntable(layer)), ), - ]) + reference_geotable.values, + ) end @testset "Conversion to table for ESRI Shapefile driver" begin ESRI_Shapefile_test_reference_geotable = ( names = (Symbol(""), :id, :name), - types = (Union{Missing,ArchGDAL.IGeometry}, Int64, String), + types = (Union{Missing,ArchGDAL.IGeometry}, Union{Missing,Int64}, String), values = ( Union{Missing,String}[ "POLYGON ((0 0,0 1,1 1))", @@ -549,11 +538,11 @@ using Tables missing, "POLYGON ((0 0,-1 0,-1 1))", ], - [1, 2, 3, 0], + [1, 2, 3, missing], ["polygon1", "multipolygon1", "emptygeom", "emptyid"], ), ) - @test test_layer_to_table( + test_layer_to_table( "ESRI Shapefile", "polygon", true, @@ -569,7 +558,7 @@ using Tables Missing, ArchGDAL.IGeometry{ArchGDAL.wkbLineString}, }, - Int64, + Union{Missing,Int64}, String, ), values = ( @@ -579,11 +568,11 @@ using Tables missing, "LINESTRING (5 6,6 7,7 8)", ], - [1, 2, 3, 0], + [1, 2, 3, missing], ["line1", "line2", "emptygeom", "emptyid"], ), ) - @test test_layer_to_table( + test_layer_to_table( "ESRI Shapefile", "line", true, @@ -612,7 +601,7 @@ using Tables ["polygon1", "multipolygon1", "emptygeom", "emptyid"], ), ) - @test test_layer_to_table( + test_layer_to_table( "GeoJSON", "polygon", true, @@ -642,7 +631,7 @@ using Tables ["line1", "line2", "emptygeom", "emptyid"], ), ) - @test test_layer_to_table( + test_layer_to_table( "GeoJSON", "line", true, @@ -678,7 +667,7 @@ using Tables ["line1", "multiline1", "emptygeom", "emptyid"], ), ) - @test test_layer_to_table( + test_layer_to_table( "GML", "line", true, @@ -691,7 +680,7 @@ using Tables @testset "Conversion to table for GPKG driver" begin GPKG_test_reference_geotable = ( names = (:geom, :id, :name), - types = (Union{Missing,ArchGDAL.IGeometry}, Int64, String), + types = (Union{Missing,ArchGDAL.IGeometry}, Union{Missing,Int64}, String), values = ( Union{Missing,String}[ "LINESTRING (1 2,2 3,3 4)", @@ -699,11 +688,11 @@ using Tables missing, "LINESTRING (5 6,6 7,7 8)", ], - [1, 2, 3, 0], + Union{Missing,Int64}[1, 2, 3, missing], ["line1", "multiline1", "emptygeom", "emptyid"], ), ) - @test test_layer_to_table( + test_layer_to_table( "GPKG", "line", true, @@ -727,7 +716,7 @@ using Tables ["", "", ""], ), ) - @test test_layer_to_table( + test_layer_to_table( "KML", "line", true, @@ -740,18 +729,18 @@ using Tables @testset "Conversion to table for FlatGeobuf driver" begin FlatGeobuf_test_reference_geotable = ( names = (Symbol(""), :id, :name), - types = (ArchGDAL.IGeometry, Union{Missing,Int64}, String), + types = (ArchGDAL.IGeometry, Union{Nothing,Int64}, String), values = ( [ "LINESTRING (5 6,6 7,7 8)", "MULTILINESTRING ((1 2,2 3,3 4,4 5),(6 7,7 8,8 9,9 10))", "LINESTRING (1 2,2 3,3 4)", ], - Union{Missing,Int64}[missing, 2, 1], + Union{Nothing,Int64}[nothing, 2, 1], ["emptyid", "multiline1", "line1"], ), ) - @test test_layer_to_table( + test_layer_to_table( "FlatGeobuf", "line", true, @@ -762,7 +751,6 @@ using Tables end @testset "Conversion to table for CSV driver" begin - @test begin AG.read( joinpath(@__DIR__, "data/multi_geom.csv"), options = [ @@ -797,11 +785,10 @@ using Tables ["Mumbai", "New Delhi"], ), ) - return test_layer_to_table( + test_layer_to_table( multigeom_test_layer, CSV_multigeom_test_reference_geotable, ) - end end end