diff --git a/Project.toml b/Project.toml index 5e9b5dc..b4be0e8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,19 +1,21 @@ name = "Mongoc" uuid = "4fe8b98c-fc19-5c23-8ec2-168ff83495f2" license = "MIT" +version = "0.10.1" authors = ["Felipe Noronha "] repo = "https://github.com/JuliaDatabases/Mongoc.jl.git" -version = "0.10.1" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DecFP = "55939f99-70c6-5e9b-8bb0-5071ed7d61fd" MongoC_jll = "90100e71-7732-535a-9be7-2e9affd1cfc1" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [compat] DecFP = "1" MongoC_jll = "1" +OrderedCollections = "1" julia = "1.6" [extras] diff --git a/src/Mongoc.jl b/src/Mongoc.jl index c46381a..821e19b 100644 --- a/src/Mongoc.jl +++ b/src/Mongoc.jl @@ -3,7 +3,7 @@ module Mongoc using MongoC_jll import Base.UUID -using Dates, DecFP, Serialization +using Dates, DecFP, Serialization, OrderedCollections # # utility functions for date conversion diff --git a/src/bson.jl b/src/bson.jl index 5c652a2..67fad16 100644 --- a/src/bson.jl +++ b/src/bson.jl @@ -10,6 +10,8 @@ Base.convert(::Type{T}, t::BSONType) where {T<:Number} = T(reinterpret(Cint, t)) Base.convert(::Type{BSONType}, n::T) where {T<:Number} = reinterpret(BSONType, Cint(n)) BSONType(u::UInt8) = convert(BSONType, u) +const DEFAULT_DICT_TYPE = Ref{Type{<:AbstractDict}}(Dict) + # # Constants for BSONType # @@ -383,7 +385,7 @@ function BSON(args::Pair...) result = BSON() for (k, v) in args - result[k] = v + result[String(k)] = v end return result @@ -503,16 +505,16 @@ Base.keys(doc::BSON) = BSONIterator(doc, IterateKeys) Base.values(doc::BSON) = BSONIterator(doc, IterateValues) """ - as_dict(document::BSON) :: Dict{String} + as_dict(document::BSON; dicttype::Type{D} = DEFAULT_DICT_TYPE[]) :: Dict{String} -Converts a BSON document to a Julia `Dict`. +Converts a BSON document to a given dicttype, defaulting to DEFAULT_DICT_TYPE[], which defaults to Dict """ -as_dict(document::BSON) = convert(Dict, document) +as_dict(document::BSON; @nospecialize dicttype::Type{D} = DEFAULT_DICT_TYPE[]) where D <: AbstractDict = convert(D, document) -function as_dict(iter_ref::Ref{BSONIter}) - result = Dict{String, Any}() +function as_dict(iter_ref::Ref{BSONIter}; @nospecialize dicttype::Type{D} = DEFAULT_DICT_TYPE[]) where D <: AbstractDict + result = D{String, Any}() while bson_iter_next(iter_ref) - result[unsafe_string(bson_iter_key(iter_ref))] = get_value(iter_ref) + result[unsafe_string(bson_iter_key(iter_ref))] = get_value(iter_ref; dicttype) end return result end @@ -552,7 +554,7 @@ function get_array(iter_ref::Ref{BSONIter}, ::Type{T}) where T return result_array end -function get_value(iter_ref::Ref{BSONIter}) +function get_value(iter_ref::Ref{BSONIter}; @nospecialize dicttype::Type{D} = DEFAULT_DICT_TYPE[]) where D <: AbstractDict bson_type = bson_iter_type(iter_ref) if bson_type == BSON_TYPE_UTF8 @@ -584,7 +586,7 @@ function get_value(iter_ref::Ref{BSONIter}) if !ok error("Couldn't iterate document inside BSON.") end - return as_dict(child_iter_ref) + return as_dict(child_iter_ref; dicttype) elseif bson_type == BSON_TYPE_BINARY @@ -612,10 +614,10 @@ function get_value(iter_ref::Ref{BSONIter}) end end -function Base.getindex(document::BSON, key::AbstractString) +function Base.getindex(document::BSON, key::AbstractString, @nospecialize dicttype::Type{D} = DEFAULT_DICT_TYPE[]) where D <: AbstractDict iter_ref = Ref{BSONIter}() bson_iter_init_find(iter_ref, document.handle, key) || throw(KeyError(key)) - return get_value(iter_ref) + return get_value(iter_ref; dicttype = D) end function Base.get(document::BSON, key::AbstractString, default::Any) @@ -719,7 +721,7 @@ function Base.setindex!(document::BSON, value::BSON, key::AbstractString) nothing end -Base.setindex!(document::BSON, value::Dict, key::AbstractString) = setindex!(document, BSON(value), key) +Base.setindex!(document::BSON, value::AbstractDict, key::AbstractString) = setindex!(document, BSON(value), key) function Base.setindex!(document::BSON, value::Vector{T}, key::AbstractString) where T sub_document = BSON(value) @@ -1092,3 +1094,110 @@ function read_bson_from_json(filepath::AbstractString) :: Vector{BSON} destroy!(reader) end end + +function Base.getindex(document::BSON, index::AbstractVector, dicttype::Type{D} = DEFAULT_DICT_TYPE[]) where D <: AbstractDict + v_iter = values(document) + n = Int(length(v_iter)) + iter_ref = v_iter.bson_iter_ref + i = 0 + vv = Vector{Any}(undef, n) + while(bson_iter_next(iter_ref)) + i += 1 + if i ∈ index + vv[i] = get_value(iter_ref; dicttype = D) + end + end + vv[index] +end + +function Base.merge!(document::BSON) + kk_all = collect(keys(document)) + n = length(kk_all) + + index_dict = OrderedDict{String, Int}() + for (i, k) in enumerate(kk_all) + index_dict[k] = i # always overwrite → keeps the last index + end + + kk = collect(keys(index_dict)) + index = collect(values(index_dict)) + length(kk) == n && return document + + iter_ref = keys(document).bson_iter_ref + vv = document[index] + bson_reinit(document) + for (k, v) in zip(kk, vv) + document[k] = v + end + document +end + +function Base.getindex(document::BSON, key::Symbol, @nospecialize dicttype::Type{D} = DEFAULT_DICT_TYPE[]) where D <: AbstractDict + k = String(key) + iter_ref = keys(document).bson_iter_ref + i = 0 + + while bson_iter_find(iter_ref, k) + i += 1 + end + i == 0 && throw(KeyError(k)) + bson_iter_init(iter_ref, getfield(document, :handle)) + for _ in 1:i + bson_iter_find(iter_ref, k) + end + return get_value(iter_ref; dicttype = D) +end + +function Base.setindex!(document::BSON, value, key::Symbol) + k_str::String = String(key) + iter_ref = Ref{BSONIter}() + key_exists = bson_iter_init_find(iter_ref, document.handle, k_str) + + if key_exists + # append something inexpensive to parse as dummy value + document[k_str] = 0 + kk_all = collect(keys(document)) + n = length(kk_all) + + index_dict = OrderedDict{String, Int}() + for (i, k) in enumerate(kk_all) + index_dict[k] = i # always overwrite → keeps the last index + end + + kk = collect(keys(index_dict)) + index = collect(values(index_dict)) + k_pos = findfirst(==(k_str), kk) + + vv = document[index] + # replace the dummy value by the real value + vv[k_pos] = value + bson_reinit(document) + for (k, v) in zip(kk, vv) + document[k] = k == k_str ? value : v + end + value + else + document[k_str] = value + end +end + +function Base.getproperty(document::BSON, key::Symbol) + key == :__handle__ && !haskey(document, :__handle__) && haskey(document, :handle) && return document[:handle] + key == :handle ? getfield(document, :handle) : document[key] +end + +function Base.setproperty!(document::BSON, key::Symbol, value) + document[key] = value +end + +function Base.setproperty!(document::BSON, key::Symbol, value::Ptr{Nothing}) + if key == :handle + setfield!(document, :handle, value) + else + document[key] = value + end +end + +function Base.propertynames(document::BSON) + Symbol.(keys(document)) +end diff --git a/src/c_api.jl b/src/c_api.jl index 4efe11c..ffa5290 100644 --- a/src/c_api.jl +++ b/src/c_api.jl @@ -40,6 +40,10 @@ function bson_oid_is_valid(str::String) ccall((:bson_oid_is_valid, libbson), Bool, (Cstring, Csize_t), str, str_length) end +function bson_reinit(bson_document::BSON) + ccall((:bson_reinit, libbson), Cvoid, (Ptr{Cvoid},), bson_document.handle) +end + function bson_append_oid(bson_document::Ptr{Cvoid}, key::AbstractString, key_length::Int, value::BSONObjectId) ccall((:bson_append_oid, libbson), Bool, (Ptr{Cvoid}, Cstring, Cint, Ref{BSONObjectId}), diff --git a/test/bson_tests.jl b/test/bson_tests.jl index 4751827..f3aa231 100644 --- a/test/bson_tests.jl +++ b/test/bson_tests.jl @@ -6,6 +6,7 @@ using Test using Dates using DecFP using Distributed +using OrderedCollections @testset "BSON" begin @@ -171,6 +172,7 @@ using Distributed @test doc["_id"] == new_id @test doc["array"] == [1, 2, false, "inner_string"] @test doc["document"] == Dict("a"=>1, "b"=>"b_string") + @test doc["document", OrderedDict] == OrderedDict("a"=>1, "b"=>"b_string") @test doc["null"] == nothing @test_throws KeyError doc["invalid key"] @@ -187,7 +189,7 @@ using Distributed @test doc_dict["_id"] == new_id @test doc_dict["array"] == [1, 2, false, "inner_string"] @test doc_dict["document"] == Dict("a"=>1, "b"=>"b_string") - @test doc_dict["null"] == nothing + @test doc_dict["null"] === nothing @testset "convert(Dict, BSON)" begin doc_dict2 = @inferred(convert(Dict, doc)) @@ -202,6 +204,40 @@ using Distributed @test_throws MethodError Mongoc.get_array(doc, "float_array", String) @test_throws ErrorException Mongoc.get_array(doc, "document", Any) + + @test doc.a == 1 + @test doc.b == 2.2 + @test doc.str == "my string" + @test doc.bool_t + @test !doc.bool_f + @test doc._id == new_id + @test doc.array == [1, 2, false, "inner_string"] + @test doc.document == Dict("a"=>1, "b"=>"b_string") + @test doc.null == nothing + + @test doc[:document, OrderedDict] == OrderedDict("a"=>1, "b"=>"b_string") + Mongoc.DEFAULT_DICT_TYPE[] = OrderedDict + @test doc.document == OrderedDict("a"=>1, "b"=>"b_string") + Mongoc.DEFAULT_DICT_TYPE[] = Dict + + + # setindex!() with a key of type String adds another pair + doc["a"] = "new_a" + pairs = collect(doc) + filter!(p -> p[1] == "a", pairs) + @test length(pairs) == 2 + @test pairs[1][2] == 1 + @test pairs[2][2] == "new_a" + # getindex() retrieves the first pair + @test_broken doc["a"] != 1 + @test_broken doc["a"] == "new_a" + # getindex with a key of type Symbol and getproperty() retrieve the last pair + @test doc[:a] == "new_a" + @test doc.a == "new_a" + merge!(doc) + @test doc["a"] == "new_a" + doc.a = "even_newer_a" + @test doc["a"] == "even_newer_a" end @testset "BSON write" begin