diff --git a/docs/src/pages/api.md b/docs/src/pages/api.md index c1490950..ecb020b9 100644 --- a/docs/src/pages/api.md +++ b/docs/src/pages/api.md @@ -6,7 +6,121 @@ DocTestSetup = quote end ``` -```@autodocs -Modules = [LibPQ] -Pages = ["LibPQ.jl", "utils.jl", "datastreams.jl"] +## Public + +### Connections + +```@docs +LibPQ.Connection +execute +prepare +status(::Connection) +Base.close(::Connection) +Base.isopen(::Connection) +reset!(::Connection) +Base.show(::IO, ::Connection) +``` + +### Results + +```@docs +LibPQ.Result +status(::Result) +clear!(::Result) +num_rows(::Result) +num_columns(::Result) +Base.show(::IO, ::Result) +``` + +### Statements + +```@docs +LibPQ.Statement +num_columns(::Statement) +num_params(::Statement) +Base.show(::IO, ::Statement) +``` + +### DataStreams Integration + +```@docs +LibPQ.Statement(::LibPQ.DataStreams.Data.Schema, ::Type{LibPQ.DataStreams.Data.Row}, ::Bool, ::Connection, ::AbstractString) +LibPQ.fetch! +``` + +## Internals + +### Connections + +```@docs +LibPQ.handle_new_connection +LibPQ.server_version +LibPQ.encoding +LibPQ.set_encoding! +LibPQ.reset_encoding! +LibPQ.transaction_status +LibPQ.unique_id +LibPQ.error_message(::Connection) +``` + +### Connection Info + +```@docs +LibPQ.ConnectionOption +LibPQ.conninfo +LibPQ.ConninfoDisplay +Base.parse(::Type{LibPQ.ConninfoDisplay}, ::AbstractString) +``` + +### Results and Statements + +```@docs +LibPQ.handle_result +LibPQ.column_name +LibPQ.column_names +LibPQ.column_number +LibPQ.column_oids +LibPQ.column_types +LibPQ.num_params(::Result) +LibPQ.error_message(::Result) +``` + +### Type Conversions + +```@docs +LibPQ.oid +LibPQ.PQChar +LibPQ.PQ_SYSTEM_TYPES +LibPQ.PQTypeMap +Base.getindex(::LibPQ.PQTypeMap, typ) +Base.setindex!(::LibPQ.PQTypeMap, ::Type, typ) +LibPQ._DEFAULT_TYPE_MAP +LibPQ.LIBPQ_TYPE_MAP +LibPQ.PQConversions +Base.getindex(::LibPQ.PQConversions, oid_typ::Tuple{Any, Type}) +Base.setindex!(::LibPQ.PQConversions, ::Base.Callable, oid_typ::Tuple{Any, Type}) +LibPQ._DEFAULT_CONVERSIONS +LibPQ.LIBPQ_CONVERSIONS +LibPQ._FALLBACK_CONVERSION +``` + +### Parsing + +```@docs +LibPQ.PQValue +LibPQ.data_pointer +LibPQ.num_bytes +Base.unsafe_string(::LibPQ.PQValue) +LibPQ.string_view +LibPQ.bytes_view +Base.parse(::Type{Any}, pqv::LibPQ.PQValue) +``` + +### Miscellaneous + +```@docs +LibPQ.@pqv_str +LibPQ.string_parameters +LibPQ.parameter_pointers +LibPQ.unsafe_string_or_null ``` diff --git a/src/LibPQ.jl b/src/LibPQ.jl index b8e6b393..ee344841 100644 --- a/src/LibPQ.jl +++ b/src/LibPQ.jl @@ -2,12 +2,7 @@ module LibPQ export Connection, Result, Statement export status, reset!, execute, clear, fetch!, prepare, - server_version, @pqv_str, - encoding, set_encoding!, reset_encoding!, - num_columns, num_rows, num_params, - column_name, column_names, column_number - -export PQChar + num_columns, num_rows, num_params using Compat.Dates using DocStringExtensions @@ -55,7 +50,22 @@ using .libpq_c include("typemaps.jl") +""" + const LIBPQ_TYPE_MAP::PQTypeMap + +The [`PQTypeMap`](@ref) containing LibPQ-level type mappings for LibPQ.jl. +Adding type mappings to this constant will override the default type mappings for all code +using LibPQ.jl. +""" const LIBPQ_TYPE_MAP = PQTypeMap() + +""" + const LIBPQ_CONVERSIONS::PQConversions + +The [`PQConversions`](@ref) containing LibPQ-level conversion functions for LibPQ.jl. +Adding conversions to this constant will override the default conversions for all code using +LibPQ.jl. +""" const LIBPQ_CONVERSIONS = PQConversions() ### CONNECTIONS BEGIN @@ -178,6 +188,22 @@ function Connection(str::AbstractString; throw_error::Bool=true, kwargs...) ) end +""" + Connection(f, args...; kwargs...) -> Connection + +A utility method to support `do` syntax. +Constructs the `Connection`, calls `f` on it, then closes it. +""" +function Connection(f::Base.Callable, args...; kwargs...) + jl_conn = Connection(args...; kwargs...) + + try + return f(jl_conn) + finally + close(jl_conn) + end +end + """ server_version(jl_conn::Connection) -> VersionNumber @@ -221,6 +247,8 @@ Parse a PostgreSQL version. ## Examples ```jldoctest + julia> using LibPQ: @pqv_str + julia> pqv"10.1" == v"10.0.1" true @@ -953,6 +981,9 @@ struct Statement "An autogenerated neame for the prepared statement (using [`unique_id`](@ref)" name::String + "The query string of the prepared statement" + query::String + "A `Result` containing a description of the prepared statement" description::Result @@ -995,7 +1026,22 @@ function prepare(jl_conn::Connection, query::AbstractString) throw_error=true, ) - Statement(jl_conn, uid, description, num_params(description)) + Statement(jl_conn, uid, query, description, num_params(description)) +end + +""" + show(io::IO, jl_result::Statement) + +Show a PostgreSQL prepared statement and its query. +""" +function Base.show(io::IO, stmt::Statement) + print( + io, + "PostgreSQL prepared statement named ", + stmt.name, + " with query ", + stmt.query, + ) end """ diff --git a/src/parsing.jl b/src/parsing.jl index 51d59496..3c4e2917 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -1,9 +1,12 @@ -# wrapper for value +"A wrapper for one value in a PostgreSQL result." struct PQValue{OID} + "PostgreSQL result" jl_result::Result - # 0-indexed + "Row index of the result (0-indexed)" row::Cint + + "Column index of the result (0-indexed)" col::Cint function PQValue{OID}(jl_result::Result, row::Integer, col::Integer) where OID @@ -11,26 +14,81 @@ struct PQValue{OID} end end +""" + PQValue(jl_result::Result, row::Integer, col::Integer) -> PQValue + PQValue{OID}(jl_result::Result, row::Integer, col::Integer) -> PQValue{OID} + +Construct a `PQValue` wrapping one value in a PostgreSQL result. +Row and column positions are provided 1-indexed. +If the `OID` type parameter is not provided, the Oid of the field will be retrieved from +the result. +""" function PQValue(jl_result::Result, row::Integer, col::Integer) oid = libpq_c.PQftype(jl_result.result, col - 1) return PQValue{oid}(jl_result, row, col) end -# strlen +""" + num_bytes(pqv::PQValue) -> Cint + +The length in bytes of the `PQValue`'s corresponding data. +LibPQ.jl currently always uses text format, so this is equivalent to C's `strlen`. + +See also: [`data_pointer`](@ref) +""" num_bytes(pqv::PQValue) = libpq_c.PQgetlength(pqv.jl_result.result, pqv.row, pqv.col) +""" + data_pointer(pqv::PQValue) -> Ptr{UInt8} + +Get a raw pointer to the data for one value in a PostgreSQL result. +This data will be freed by libpq when the result is cleared, and should only be used +temporarily. +""" data_pointer(pqv::PQValue) = libpq_c.PQgetvalue(pqv.jl_result.result, pqv.row, pqv.col) +""" + unsafe_string(pqv::PQValue) -> String + +Construct a `String` from a `PQValue` by copying the data. +""" function Base.unsafe_string(pqv::PQValue) return unsafe_string(data_pointer(pqv), num_bytes(pqv)) end +""" + string_view(pqv::PQValue) -> String + +Wrap a `PQValue`'s underlying data in a `String`. +This function uses [`data_pointer`](@ref) and [`num_bytes`](@ref) and does not copy. + +!!! note + + The underlying data will be freed by libpq when the result is cleared, and should only + be used temporarily. + +See also: [`bytes_view`](@ref) +""" function string_view(pqv::PQValue) return String(unsafe_wrap(Vector{UInt8}, data_pointer(pqv), num_bytes(pqv))) end -# includes null +""" + bytes_view(pqv::PQValue) -> Vector{UInt8} + +Wrap a `PQValue`'s underlying data in a vector of bytes. +This function uses [`data_pointer`](@ref) and [`num_bytes`](@ref) and does not copy. + +This function differs from [`string_view`](@ref) as it keeps the `\0` byte at the end. +`PQValue` parsing functions should use `bytes_view` when the data returned by PostgreSQL +is not in UTF-8. + +!!! note + + The underlying data will be freed by libpq when the result is cleared, and should only + be used temporarily. +""" bytes_view(pqv::PQValue) = unsafe_wrap(Vector{UInt8}, data_pointer(pqv), num_bytes(pqv) + 1) Base.String(pqv::PQValue) = unsafe_string(pqv) @@ -39,7 +97,16 @@ Base.convert(::Type{String}, pqv::PQValue) = String(pqv) Base.length(pqv::PQValue) = length(string_view(pqv)) Base.endof(pqv::PQValue) = endof(string_view(pqv)) -# fallback, because Base is bad with string iteration +# Fallback, because Base requires string iteration state to be indices into the string. +# In an ideal world, PQValue would be an AbstractString and this particular method would +# not be necessary. +""" + parse(::Type{T}, pqv::PQValue) -> T + +Parse a value of type `T` from a `PQValue`. +By default, this uses any existing `parse` method for parsing a value of type `T` from a +`String`. +""" Base.parse(::Type{T}, pqv::PQValue) where {T} = parse(T, string_view(pqv)) # allow parsing as a Symbol anything which works as a String @@ -256,7 +323,6 @@ for pq_eltype in ("int2", "int4", "int8", "float4", "float8", "oid", "numeric") end end - struct FallbackConversion <: AbstractDict{Tuple{Oid, Type}, Base.Callable} end @@ -270,4 +336,9 @@ end Base.haskey(cmap::FallbackConversion, oid_typ::Tuple{Integer, Type}) = true +""" +A fallback conversion mapping (like [`PQConversions`](@ref) which holds a single function +for converting PostgreSQL data of a given Oid to a given Julia type, using the [`parse`](@ref) +function. +""" const _FALLBACK_CONVERSION = FallbackConversion() diff --git a/src/typemaps.jl b/src/typemaps.jl index 5f417025..1a8f3fd6 100644 --- a/src/typemaps.jl +++ b/src/typemaps.jl @@ -1,9 +1,13 @@ -# Symbol keys are PostgreSQL's internal names for its types. -# These may not correspond well to the common names, e.g., "char(n)" is :bpchar. -# This dictionary is generated with the deps/system_type_map.jl script and contains only -# PostgreSQL's system-defined types. -# It is expected (but might not be guaranteed) that these are the same across versions and -# installations. +""" + const PQ_SYSTEM_TYPES::Dict{Symbol, Oid} + +Internal mapping of PostgreSQL's default types from PostgreSQL internal name to Oid. +The names may not correspond well to the common names, e.g., "char(n)" is :bpchar. +This dictionary is generated with the `deps/system_type_map.jl` script and contains only +PostgreSQL's system-defined types. +It is expected (but might not be guaranteed) that these are the same across versions and +installations. +""" const PQ_SYSTEM_TYPES = Dict{Symbol, Oid}( :bool => 16, :bytea => 17, :char => 18, :name => 19, :int8 => 20, :int2 => 21, :int2vector => 22, :int4 => 23, :regproc => 24, :text => 25, :oid => 26, :tid => 27, @@ -125,16 +129,17 @@ const PQ_SYSTEM_TYPES = Dict{Symbol, Oid}( """ oid(typ::Union{Symbol, String, Integer}) -> LibPQ.Oid -Convert a PostgreSQL type from a `String` or `Symbol` representation to its oid +Convert a PostgreSQL type from an `AbstractString` or `Symbol` representation to its oid representation. Integers are converted directly to `LibPQ.Oid`s. """ function oid end oid(typ::Symbol) = PQ_SYSTEM_TYPES[typ] -oid(o::Integer) = PQ_SYSTEM_TYPES[convert(Oid, oid)] -oid(typ::String) = oid(Symbol(typ)) +oid(o::Integer) = convert(Oid, o) +oid(typ::AbstractString) = oid(Symbol(typ)) +"A mapping from PostgreSQL Oid to Julia type." struct PQTypeMap <: AbstractDict{Oid, Type} type_map::Dict{Oid, Type} end @@ -143,6 +148,14 @@ PQTypeMap(type_map::PQTypeMap) = type_map PQTypeMap(type_map::AbstractDict{Oid, Type}) = PQTypeMap(Dict(type_map)) PQTypeMap() = PQTypeMap(Dict{Oid, Type}()) +""" + PQTypeMap(d::AbstractDict) -> PQTypeMap + +Creates a `PQTypeMap` from any mapping from PostgreSQL types to Julia types. +Each PostgreSQL type is passed through [`oid`](@ref) and so can be specified as an Oid or +PostgreSQL's internal name for the type (as a `Symbol` or `AbstractString`). +These names are stored in the keys of [`PQ_SYSTEM_TYPES`](@ref). +""" function PQTypeMap(user_map::AbstractDict) type_map = PQTypeMap() @@ -155,16 +168,22 @@ end const LayerPQTypeMap = LayerDict{Oid, Type} -# TODO: clean up the code below using `oid` -Base.getindex(tmap::PQTypeMap, oid::Integer) = tmap.type_map[convert(Oid, oid)] -Base.getindex(tmap::PQTypeMap, typ::Symbol) = tmap[PQ_SYSTEM_TYPES[typ]] +""" + Base.getindex(tmap::PQTypeMap, typ) -> Type -function Base.setindex!(tmap::PQTypeMap, val::Type, oid::Integer) - setindex!(tmap.type_map, val, convert(Oid, oid)) -end +Get the Julia type corresponding to the given PostgreSQL type (any type accepted by +[`oid`](@ref)) according to `tmap`. +""" +Base.getindex(tmap::PQTypeMap, typ) = tmap.type_map[oid(typ)] + +""" + Base.setindex!(tmap::PQTypeMap, val::Type, typ) -function Base.setindex!(tmap::PQTypeMap, val::Type, typ::Symbol) - setindex!(tmap, val, PQ_SYSTEM_TYPES[typ]) +Set the Julia type corresponding to the given PostgreSQL type (any type accepted by +[`oid`](@ref)) in `tmap`. +""" +function Base.setindex!(tmap::PQTypeMap, val::Type, typ) + setindex!(tmap.type_map, val, oid(typ)) end Base.start(tmap::PQTypeMap) = start(tmap.type_map) @@ -174,10 +193,22 @@ Base.done(tmap::PQTypeMap, state) = done(tmap.type_map, state) Base.length(tmap::PQTypeMap) = length(tmap.type_mapp) Base.keys(tmap::PQTypeMap) = keys(tmap.type_map) +""" + const _DEFAULT_TYPE_MAP::PQTypeMap + +The [`PQTypeMap`](@ref) containing the default type mappings for LibPQ.jl. +This should not be mutated; LibPQ-level type mappings can be added to +[`LIBPQ_TYPE_MAP`](@ref). +""" const _DEFAULT_TYPE_MAP = PQTypeMap(Dict{Oid, Type}()) +# type alias for convenience const ColumnTypeMap = Dict{Cint, Type} +""" +A mapping from Oid and Julia type pairs to the function for converting a PostgreSQL value +with said Oid to said Julia type. +""" struct PQConversions <: AbstractDict{Tuple{Oid, Type}, Base.Callable} func_map::Dict{Tuple{Oid, Type}, Base.Callable} end @@ -188,16 +219,28 @@ function PQConversions(func_map::AbstractDict{Tuple{Oid, Type}, Base.Callable}) end PQConversions() = PQConversions(Dict{Tuple{Oid, Type}, Base.Callable}()) -function Base.getindex(cmap::PQConversions, oid_typ::Tuple{Integer, Type}) - getindex(cmap.func_map, (convert(Oid, oid_typ[1]), oid_typ[2])) +""" + Base.getindex(cmap::PQConversions, oid_typ::Tuple{Any, Type}) -> Base.Callable + +Get the function according to `cmap` for converting a PostgreSQL value of some PostgreSQL +type (any type accepted by [`oid`](@ref)) to some Julia type. +""" +function Base.getindex(cmap::PQConversions, oid_typ::Tuple{Any, Type}) + getindex(cmap.func_map, (oid(oid_typ[1]), oid_typ[2])) end +""" + Base.setindex!(cmap::PQConversions, val::Base.Callable, oid_typ::Tuple{Any, Type}) + +Set the function in `cmap` for converting a PostgreSQL value of some PostgreSQL type (any +type accepted by [`oid`](@ref)) to some Julia type. +""" function Base.setindex!( cmap::PQConversions, val::Base.Callable, - oid_typ::Tuple{Integer, Type}, + oid_typ::Tuple{Any, Type}, ) - setindex!(cmap.func_map, val, (convert(Oid, oid_typ[1]), oid_typ[2])) + setindex!(cmap.func_map, val, (oid(oid_typ[1]), oid_typ[2])) end Base.start(cmap::PQConversions) = start(cmap.func_map) @@ -207,10 +250,18 @@ Base.done(cmap::PQConversions, state) = done(cmap.func_map, state) Base.length(cmap::PQConversions) = length(cmap.func_map) Base.keys(cmap::PQConversions) = keys(cmap.func_map) +""" + const _DEFAULT_CONVERSIONS::PQConversions + +The [`PQConversions`](@ref) containing the default conversion functions for LibPQ.jl. +This should not be mutated; LibPQ-level conversion functions can be added to +[`LIBPQ_CONVERSIONS`](@ref). +""" const _DEFAULT_CONVERSIONS = PQConversions() ## WRAPPER TYPES +"A one-byte character type for correspondence with PostgreSQL's one-byte \"char\" type." primitive type PQChar 8 end Base.UInt8(c::PQChar) = reinterpret(UInt8, c) diff --git a/test/runtests.jl b/test/runtests.jl index 26e8ad85..0ea30926 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -28,12 +28,12 @@ end @testset "Version Numbers" begin valid_versions = [ - (pqv"11", v"11"), - (pqv"11.80", v"11.0.80"), - (pqv"10.1", v"10.0.1"), - (pqv"9.1.5", v"9.1.5"), - (pqv"9.2", v"9.2.0"), - (pqv"8", v"8.0.0"), + (LibPQ.pqv"11", v"11"), + (LibPQ.pqv"11.80", v"11.0.80"), + (LibPQ.pqv"10.1", v"10.0.1"), + (LibPQ.pqv"9.1.5", v"9.1.5"), + (LibPQ.pqv"9.2", v"9.2.0"), + (LibPQ.pqv"8", v"8.0.0"), ] @testset "Valid Versions" for (pg_version, jl_version) in valid_versions @@ -167,8 +167,8 @@ end ) @test num_params(stmt) == 2 @test num_columns(stmt) == 0 # an insert has no results - @test column_number(stmt, "no_nulls") == 0 - @test column_names(stmt) == [] + @test LibPQ.column_number(stmt, "no_nulls") == 0 + @test LibPQ.column_names(stmt) == [] result = execute( conn, @@ -189,35 +189,51 @@ end end @testset "Connection" begin + @testset "do" begin + local saved_conn + + was_open = Connection("dbname=postgres user=$DATABASE_USER"; throw_error=true) do jl_conn + saved_conn = jl_conn + return isopen(jl_conn) + end + + @test was_open + @test !isopen(saved_conn) + + @test_throws ErrorException Connection("dbname=123fake"; throw_error=true) do jl_conn + @test false + end + end + @testset "Version Numbers" begin conn = Connection("dbname=postgres user=$DATABASE_USER"; throw_error=true) # update this test before PostgreSQL 20.0 ;) - @test pqv"7" <= server_version(conn) <= pqv"20" + @test LibPQ.pqv"7" <= LibPQ.server_version(conn) <= LibPQ.pqv"20" end @testset "Encoding" begin conn = Connection("dbname=postgres user=$DATABASE_USER"; throw_error=true) - @test encoding(conn) == "UTF8" + @test LibPQ.encoding(conn) == "UTF8" - set_encoding!(conn, "SQL_ASCII") - @test encoding(conn) == "SQL_ASCII" - reset_encoding!(conn) - @test encoding(conn) == "SQL_ASCII" + LibPQ.set_encoding!(conn, "SQL_ASCII") + @test LibPQ.encoding(conn) == "SQL_ASCII" + LibPQ.reset_encoding!(conn) + @test LibPQ.encoding(conn) == "SQL_ASCII" reset!(conn) - @test encoding(conn) == "SQL_ASCII" - set_encoding!(conn, "UTF8") - @test encoding(conn) == "UTF8" - reset_encoding!(conn) - @test encoding(conn) == "UTF8" + @test LibPQ.encoding(conn) == "SQL_ASCII" + LibPQ.set_encoding!(conn, "UTF8") + @test LibPQ.encoding(conn) == "UTF8" + LibPQ.reset_encoding!(conn) + @test LibPQ.encoding(conn) == "UTF8" conn.encoding = "SQL_ASCII" - reset_encoding!(conn) - @test encoding(conn) == "SQL_ASCII" + LibPQ.reset_encoding!(conn) + @test LibPQ.encoding(conn) == "SQL_ASCII" - @test_throws ErrorException set_encoding!(conn, "NOT A REAL ENCODING") + @test_throws ErrorException LibPQ.set_encoding!(conn, "NOT A REAL ENCODING") close(conn) end @@ -410,11 +426,11 @@ end @test status(result) == LibPQ.libpq_c.PGRES_TUPLES_OK @test LibPQ.num_rows(result) == 3 @test LibPQ.num_columns(result) == 5 - @test LibPQ.column_types(result) == [LibPQ.Oid, String, Int16, Bool, PQChar] + @test LibPQ.column_types(result) == [LibPQ.Oid, String, Int16, Bool, LibPQ.PQChar] data = Data.stream!(result, NamedTuple) - @test map(eltype, values(data)) == map(T -> Union{T, Missing}, [LibPQ.Oid, String, Int16, Bool, PQChar]) + @test map(eltype, values(data)) == map(T -> Union{T, Missing}, [LibPQ.Oid, String, Int16, Bool, LibPQ.PQChar]) @test data[:oid] == LibPQ.Oid[LibPQ.PQ_SYSTEM_TYPES[t] for t in (:bool, :int8, :text)] @test data[:typname] == ["bool", "int8", "text"] @test data[:typlen] == [1, 8, -1] @@ -448,7 +464,7 @@ end ("E'\\\\\\\\'::bytea", UInt8[0o134]), ("E'\\\\001'::bytea", UInt8[0o001]), ("E'\\\\176'::bytea", UInt8[0o176]), - ("'3'::\"char\"", PQChar('3')), + ("'3'::\"char\"", LibPQ.PQChar('3')), ("'t'::bool", true), ("'T'::bool", true), ("'true'::bool", true),