Packages
postgrex
0.9.0
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.13
0.15.12
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.13.0-rc.0
0.12.2
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.2
PostgreSQL driver for Elixir
Current section
Files
Jump to
Current section
Files
lib/postgrex/extensions/binary.ex
defmodule Postgrex.Extensions.Binary do
@moduledoc false
alias Postgrex.TypeInfo
alias Postgrex.Types
import Postgrex.BinaryUtils
require Decimal
use Bitwise, only_operators: true
@behaviour Postgrex.Extension
@gd_epoch :calendar.date_to_gregorian_days({2000, 1, 1})
@gs_epoch :calendar.datetime_to_gregorian_seconds({{2000, 1, 1}, {0, 0, 0}})
@days_in_month 30
@secs_in_day 24 * 60 * 60
@date_max_year 5874897
@timestamp_max_year 294276
@numeric_base 10_000
@numeric_float_pad String.length(Integer.to_string(@numeric_base))
@range_empty 0x01
@range_lb_inc 0x02
@range_ub_inc 0x04
@range_lb_inf 0x08
@range_ub_inf 0x10
@int2_range -32768..32767
@int4_range -2147483648..2147483647
@int8_range -9223372036854775808..9223372036854775807
@oid_range 0..4294967295
@oid_senders ~w(oidsend regprocsend regproceduresend regopersend
regoperatorsend regclasssend regtypesend xidsend cidsend)
@senders ~w(boolsend bpcharsend textsend varcharsend byteasend
int2send int4send int8send float4send float8send numeric_send
uuid_send date_send time_send timetz_send timestamp_send
timestamptz_send interval_send enum_send tidsend unknownsend
) ++ @oid_senders
@pg_extensions ~w(hstore citext)
def init(parameters, _opts),
do: parameters["server_version"] |> Postgrex.Utils.parse_version
def matching(version) when version >= {9, 1, 0},
do: [send: "void_send"] ++ matching(0)
def matching(_),
do: unquote(Enum.map(@pg_extensions, &{:type, &1}) ++ Enum.map(@senders, &{:send, &1}))
def format(_),
do: :binary
### ENCODING ###
def encode(%TypeInfo{send: "void_send"}, :void, _, _),
do: ""
def encode(%TypeInfo{send: "boolsend"}, true, _, _),
do: <<1>>
def encode(%TypeInfo{send: "boolsend"}, false, _, _),
do: <<0>>
def encode(%TypeInfo{send: "bpcharsend"}, bin, _, _) when is_binary(bin),
do: bin
def encode(%TypeInfo{send: "textsend"}, bin, _, _) when is_binary(bin),
do: bin
def encode(%TypeInfo{send: "varcharsend"}, bin, _, _) when is_binary(bin),
do: bin
def encode(%TypeInfo{send: "byteasend"}, bin, _, _) when is_binary(bin),
do: bin
def encode(%TypeInfo{send: "enum_send"}, bin, _, _) when is_binary(bin),
do: bin
def encode(%TypeInfo{send: "unknownsend"}, bin, _, _) when is_binary(bin),
do: bin
def encode(%TypeInfo{send: "int2send"}, n, _, _)
when is_integer(n) and n in @int2_range,
do: <<n :: int16>>
def encode(%TypeInfo{send: "int4send"}, n, _, _)
when is_integer(n) and n in @int4_range,
do: <<n :: int32>>
def encode(%TypeInfo{send: "int8send"}, n, _, _)
when is_integer(n) and n in @int8_range,
do: <<n :: int64>>
def encode(%TypeInfo{send: "float4send"}, :NaN, _, _),
do: <<127, 192, 0, 0>>
def encode(%TypeInfo{send: "float4send"}, :inf, _, _),
do: <<127, 128, 0, 0>>
def encode(%TypeInfo{send: "float4send"}, :"-inf", _, _),
do: <<255, 128, 0, 0>>
def encode(%TypeInfo{send: "float4send"}, n, _, _) when is_number(n),
do: <<n :: float32>>
def encode(%TypeInfo{send: "float8send"}, :NaN, _, _),
do: <<127, 248, 0, 0, 0, 0, 0, 0>>
def encode(%TypeInfo{send: "float8send"}, :inf, _, _),
do: <<127, 240, 0, 0, 0, 0, 0, 0>>
def encode(%TypeInfo{send: "float8send"}, :"-inf", _, _),
do: <<255, 240, 0, 0, 0, 0, 0, 0>>
def encode(%TypeInfo{send: "float8send"}, n, _, _) when is_number(n),
do: <<n :: float64>>
def encode(%TypeInfo{send: "numeric_send"}, %Decimal{} = n, _, _),
do: encode_numeric(n)
def encode(%TypeInfo{send: "uuid_send"}, <<_ :: binary(16)>> = bin, _, _),
do: bin
def encode(%TypeInfo{send: "date_send"}, date, _, _),
do: encode_date(date)
def encode(%TypeInfo{send: "time_send"}, time, _, _),
do: encode_time(time)
def encode(%TypeInfo{send: "timetz_send"}, time, _, _),
do: encode_time(time)
def encode(%TypeInfo{send: "timestamp_send"}, timestamp, _, _),
do: encode_timestamp(timestamp)
def encode(%TypeInfo{send: "timestamptz_send"}, timestamp, _, _),
do: encode_timestamp(timestamp)
def encode(%TypeInfo{send: "interval_send"}, interval, _, _),
do: encode_interval(interval)
def encode(%TypeInfo{send: "array_send", array_elem: elem_oid}, list, types, _) when is_list(list),
do: encode_array(list, elem_oid, types)
def encode(%TypeInfo{send: "record_send", comp_elems: elem_oids}, tuple, types, _) when is_tuple(tuple),
do: encode_record(tuple, elem_oids, types)
def encode(%TypeInfo{send: "range_send", base_type: oid}, %Postgrex.Range{} = range, types, _),
do: encode_range(range, oid, types)
def encode(%TypeInfo{send: "tidsend"}, {block, tuple}, _, _),
do: <<block :: uint32, tuple :: uint16>>
# Define encodings for PG extensions. They could be defined inside a schema,
# so only :type field could be matched exactly, b/c other fields may have schema prefix
def encode(%TypeInfo{type: "citext"}, bin, _, _) when is_binary(bin),
do: bin
def encode(%TypeInfo{type: "hstore"}, map, _, _),
do: encode_hstore(map)
# Define encodings for all oid types
for sender <- @oid_senders do
def encode(%TypeInfo{send: unquote(sender)}, n, _, _) when is_integer(n) and n in @oid_range,
do: <<n :: uint32>>
# Catch cases where users want to send binaries as reg types (which is what
# the text protocol supports) and error with a helpful message.
def encode(%TypeInfo{send: unquote(sender)}, value, _, _) when is_binary(value),
do: raise_oid_encoding_error(unquote(sender))
end
defp encode_numeric(dec) do
if Decimal.nan?(dec) do
<<0 :: int16, 0 :: int16, 0xC000 :: uint16, 0 :: int16>>
else
string = Decimal.to_string(dec, :normal) |> :binary.bin_to_list
if List.first(string) == ?- do
[_|string] = string
sign = 0x4000
else
sign = 0x0000
end
{int, float} = Enum.split_while(string, &(&1 != ?.))
{weight, int_digits} = Enum.reverse(int) |> encode_numeric_int(0, [])
if float != [] do
[_|float] = float
scale = length(float)
float_digits = pad_to_numeric_base(float, scale) |> encode_numeric_float([])
else
scale = 0
float_digits = []
end
digits = int_digits ++ float_digits
bin = for digit <- digits, into: "", do: <<digit :: uint16>>
ndigits = div(byte_size(bin), 2)
[<<ndigits :: int16, weight :: int16, sign :: uint16, scale :: int16>> | bin]
end
end
defp pad_to_numeric_base(float, scale) do
if scale < @numeric_float_pad do
float ++ List.duplicate(?0, @numeric_float_pad - scale)
else
float
end
end
defp encode_numeric_float([], [digit|acc]) do
[pad_float(digit)|acc]
|> trim_zeros
|> Enum.reverse
end
defp encode_numeric_float(list, acc) do
{list, rest} = Enum.split(list, 4)
digit = List.to_integer(list)
encode_numeric_float(rest, [digit|acc])
end
defp encode_numeric_int([], weight, acc) do
{weight, acc}
end
defp encode_numeric_int(list, weight, acc) do
{list, rest} = Enum.split(list, 4)
digit = Enum.reverse(list) |> List.to_integer
if rest != [], do: weight = weight + 1
encode_numeric_int(rest, weight, [digit|acc])
end
defp trim_zeros([0|tail]), do: trim_zeros(tail)
defp trim_zeros(list), do: list
defp pad_float(0) do
0
end
defp pad_float(num) do
num10 = num*10
if num10 >= @numeric_base do
num
else
pad_float(num10)
end
end
defp encode_date(%Postgrex.Date{year: year, month: month, day: day}) when year <= @date_max_year do
date = {year, month, day}
<<:calendar.date_to_gregorian_days(date) - @gd_epoch :: int32>>
end
defp encode_time(%Postgrex.Time{hour: hour, min: min, sec: sec, usec: usec})
when hour in 0..23 and min in 0..59 and sec in 0..59 and usec in 0..999_999 do
time = {hour, min, sec}
<<:calendar.time_to_seconds(time) * 1_000_000 + usec :: int64>>
end
defp encode_timestamp(%Postgrex.Timestamp{year: year, month: month, day: day, hour: hour, min: min, sec: sec, usec: usec})
when year <= @timestamp_max_year and hour in 0..23 and min in 0..59 and sec in 0..59 and usec in 0..999_999 do
datetime = {{year, month, day}, {hour, min, sec}}
secs = :calendar.datetime_to_gregorian_seconds(datetime) - @gs_epoch
<<secs * 1_000_000 + usec :: int64>>
end
defp encode_interval(%Postgrex.Interval{months: months, days: days, secs: secs}) do
microsecs = secs * 1_000_000
<<microsecs :: int64, days :: int32, months :: int32>>
end
defp encode_array(list, elem_oid, types) do
encoder = &Types.encode(elem_oid, &1, types)
{data, ndims, lengths} = encode_array(list, 0, [], encoder)
lengths = for len <- Enum.reverse(lengths), do: <<len :: int32, 1 :: int32>>
[<<ndims :: int32, 0 :: int32, elem_oid :: uint32>>, lengths, data]
end
defp encode_array([], ndims, lengths, _encoder) do
{"", ndims, lengths}
end
defp encode_array([head|tail]=list, ndims, lengths, encoder)
when is_list(head) do
lengths = [length(list)|lengths]
{data, ndims, lengths} = encode_array(head, ndims, lengths, encoder)
[dimlength|_] = lengths
rest = Enum.reduce(tail, [], fn sublist, acc ->
{data, _, [len|_]} = encode_array(sublist, ndims, lengths, encoder)
if len != dimlength do
raise ArgumentError, message: "nested lists must have lists with matching lengths"
end
[acc|data]
end)
{[data|rest], ndims+1, lengths}
end
defp encode_array(list, ndims, lengths, encoder) do
{data, length} =
Enum.map_reduce(list, 0, fn
nil, length ->
{<<-1::int32>>, length + 1}
elem, length ->
data = encoder.(elem)
data = [<<IO.iodata_length(data)::int32>>, data]
{data, length + 1}
end)
{data, ndims+1, [length|lengths]}
end
defp encode_record(tuple, elem_oids, types) do
list = Tuple.to_list(tuple)
zipped = :lists.zip(list, elem_oids)
{data, count} =
Enum.map_reduce(zipped, 0, fn
{nil, oid}, count ->
{<<oid::uint32, -1::int32>>, count + 1}
{value, oid}, count ->
data = Types.encode(oid, value, types)
data = [<<oid::uint32>>, <<IO.iodata_length(data)::int32>>, data]
{data, count + 1}
end)
[<<count :: int32>>, data]
end
defp encode_range(%Postgrex.Range{lower: nil, upper: nil}, _oid, _types) do
<<@range_empty>>
end
defp encode_range(range, oid, types) do
flags = 0
if range.lower == nil do
flags = flags ||| @range_lb_inf
bin = ""
else
data = Types.encode(oid, range.lower, types)
bin = [<<IO.iodata_length(data)::int32>>, data]
end
if range.upper == nil do
flags = flags ||| @range_ub_inf
else
data = Types.encode(oid, range.upper, types)
bin = [bin, <<IO.iodata_length(data)::int32>>, data]
end
if range.lower_inclusive do
flags = flags ||| @range_lb_inc
end
if range.upper_inclusive do
flags = flags ||| @range_ub_inc
end
[flags|bin]
end
defp encode_hstore(hstore_map) when is_map(hstore_map) do
keys_and_values = Enum.reduce hstore_map, "", fn ({key, value}, acc) ->
[acc, encode_hstore_key(key), encode_hstore_value(value)]
end
:erlang.iolist_to_binary([<<Map.size(hstore_map)::int32>> | keys_and_values])
end
defp encode_hstore_key(key) when is_binary(key) do
encode_hstore_value key
end
defp encode_hstore_key(key) when is_nil(key) do
raise ArgumentError, message: "hstore keys cannot be nil!"
end
defp encode_hstore_value(nil) do
<<-1::int32>>
end
defp encode_hstore_value(value) when is_binary(value) do
value_byte_size = byte_size(value)
<<value_byte_size::int32>> <> value
end
defp raise_oid_encoding_error(sender) do
raise Postgrex.Error, message: """
You tried to use a binary instead for an oid type (#{sender}) when an
integer was expected. See https://github.com/ericmj/postgrex#oid-type-encoding
"""
end
### DECODING ###
def decode(%TypeInfo{send: "void_send"}, "", _, _),
do: :void
def decode(%TypeInfo{send: "boolsend"}, <<1 :: int8>>, _, _),
do: true
def decode(%TypeInfo{send: "boolsend"}, <<0 :: int8>>, _, _),
do: false
def decode(%TypeInfo{send: "bpcharsend"}, bin, _, _),
do: bin
def decode(%TypeInfo{send: "textsend"}, bin, _, _),
do: bin
def decode(%TypeInfo{send: "varcharsend"}, bin, _, _),
do: bin
def decode(%TypeInfo{send: "byteasend"}, bin, _, _),
do: bin
def decode(%TypeInfo{send: "enum_send"}, bin, _, _),
do: bin
def decode(%TypeInfo{send: "unknownsend"}, bin, _, _),
do: bin
def decode(%TypeInfo{send: "int2send"}, <<n :: int16>>, _, _),
do: n
def decode(%TypeInfo{send: "int4send"}, <<n :: int32>>, _, _),
do: n
def decode(%TypeInfo{send: "int8send"}, <<n :: int64>>, _, _),
do: n
def decode(%TypeInfo{send: "float4send"}, <<127, 192, 0, 0>>, _, _),
do: :NaN
def decode(%TypeInfo{send: "float4send"}, <<127, 128, 0, 0>>, _, _),
do: :inf
def decode(%TypeInfo{send: "float4send"}, <<255, 128, 0, 0>>, _, _),
do: :"-inf"
def decode(%TypeInfo{send: "float4send"}, <<n :: float32>>, _, _),
do: n
def decode(%TypeInfo{send: "float8send"}, <<127, 248, 0, 0, 0, 0, 0, 0>>, _, _),
do: :NaN
def decode(%TypeInfo{send: "float8send"}, <<127, 240, 0, 0, 0, 0, 0, 0>>, _, _),
do: :inf
def decode(%TypeInfo{send: "float8send"}, <<255, 240, 0, 0, 0, 0, 0, 0>>, _, _),
do: :"-inf"
def decode(%TypeInfo{send: "float8send"}, <<n :: float64>>, _, _),
do: n
def decode(%TypeInfo{send: "numeric_send"}, bin, _, _),
do: decode_numeric(bin)
def decode(%TypeInfo{send: "uuid_send"}, bin, _, _),
do: bin
def decode(%TypeInfo{send: "date_send"}, <<n :: int32>>, _, _),
do: decode_date(n)
def decode(%TypeInfo{send: "time_send"}, <<n :: int64>>, _, _),
do: decode_time(n)
def decode(%TypeInfo{send: "timetz_send"}, <<n :: int64, _tz :: int32>>, _, _),
do: decode_time(n)
def decode(%TypeInfo{send: "timestamp_send"}, <<n :: int64>>, _, _),
do: decode_timestamp(n)
def decode(%TypeInfo{send: "timestamptz_send"}, <<n :: int64>>, _, _),
do: decode_timestamp(n)
def decode(%TypeInfo{send: "interval_send"}, <<s :: int64, d :: int32, m :: int32>>, _, _),
do: decode_interval(s, d, m)
def decode(%TypeInfo{send: "array_send"}, bin, types, _),
do: decode_array(bin, types)
def decode(%TypeInfo{send: "record_send"}, bin, types, _),
do: decode_record(bin, types)
def decode(%TypeInfo{send: "range_send", base_type: oid}, bin, types, _),
do: decode_range(bin, oid, types)
def decode(%TypeInfo{send: "tidsend"}, <<block :: uint32, tuple :: uint16>>, _, _),
do: {block, tuple}
# Define decodings for PG extensions. They could be defined inside a schema,
# so only :type field could be matched exactly, b/c other fields may have schema prefix
def decode(%TypeInfo{type: "citext"}, bin, _, _),
do: bin
def decode(%TypeInfo{type: "hstore"}, bin, _, _),
do: decode_hstore(bin)
# Define decodings for all oid types
for sender <- @oid_senders do
def decode(%TypeInfo{send: unquote(sender)}, <<n :: uint32>>, _, _),
do: n
end
defp decode_numeric(<<ndigits :: int16, weight :: int16, sign :: uint16, scale :: int16, tail :: binary>>) do
decode_numeric(ndigits, weight, sign, scale, tail)
end
defp decode_numeric(0, _weight, 0xC000, _scale, "") do
Decimal.new(1, :qNaN, 0)
end
defp decode_numeric(_num_digits, weight, sign, scale, bin) do
{value, weight} = decode_numeric_int(bin, weight, 0)
case sign do
0x0000 -> sign = 1
0x4000 -> sign = -1
end
{coef, exp} = scale(value, (weight+1)*4, -scale)
Decimal.new(sign, coef, exp)
end
defp scale(coef, exp, scale) when scale == exp,
do: {coef, exp}
defp scale(coef, exp, scale) when scale > exp,
do: scale(div(coef, 10), exp+1, scale)
defp scale(coef, exp, scale) when scale < exp,
do: scale(coef * 10, exp-1, scale)
defp decode_numeric_int("", weight, acc), do: {acc, weight}
defp decode_numeric_int(<<digit :: int16, tail :: binary>>, weight, acc) do
acc = (acc * @numeric_base) + digit
decode_numeric_int(tail, weight - 1, acc)
end
defp decode_date(days) do
{year, month, day} = :calendar.gregorian_days_to_date(days + @gd_epoch)
%Postgrex.Date{year: year, month: month, day: day}
end
defp decode_time(microsecs) do
secs = div(microsecs, 1_000_000)
usec = rem(microsecs, 1_000_000)
{hour, min, sec} = :calendar.seconds_to_time(secs)
%Postgrex.Time{hour: hour, min: min, sec: sec, usec: usec}
end
defp decode_timestamp(microsecs) do
secs = div(microsecs, 1_000_000)
usec = rem(microsecs, 1_000_000)
{{year, month, day}, {hour, min, sec}} = :calendar.gregorian_seconds_to_datetime(secs + @gs_epoch)
if year < 2000 and usec != 0 do
sec = sec - 1
usec = 1_000_000 + usec
end
%Postgrex.Timestamp{year: year, month: month, day: day, hour: hour, min: min, sec: sec, usec: usec}
end
defp decode_interval(microsecs, days, months) do
secs = div(microsecs, 1_000_000)
%Postgrex.Interval{months: months, days: days, secs: secs}
end
defp decode_array(<<ndims :: int32, _has_null :: int32, oid :: uint32,
dims :: size(ndims)-binary-unit(64), rest :: binary>>, types) do
lengths = for <<len :: int32, _lbound :: int32 <- dims>>, do: len
decoder = &Types.decode(oid, &1, types)
{array, ""} = decode_array(rest, lengths, decoder)
array
end
defp decode_array("", [], _decoder) do
{[], ""}
end
defp decode_array(rest, [len], decoder) do
array_elements(rest, len, [], decoder)
end
defp decode_array(rest, [len|lens], decoder) do
Enum.map_reduce(1..len, rest, fn _, rest ->
decode_array(rest, lens, decoder)
end)
end
defp array_elements(rest, 0, acc, _decoder) do
{Enum.reverse(acc), rest}
end
defp array_elements(<<-1 :: int32, rest :: binary>>, count, acc, decoder) do
array_elements(rest, count-1, [nil|acc], decoder)
end
defp array_elements(<<size :: int32, elem :: binary(size), rest :: binary>>,
count, acc, decoder) do
value = decoder.(elem)
array_elements(rest, count-1, [value|acc], decoder)
end
defp decode_record(<<num :: int32, rest :: binary>>, types) do
decoder = &Types.decode(&1, &2, types)
record_elements(rest, num, decoder) |> List.to_tuple
end
defp record_elements(<<>>, 0, _decoder) do
[]
end
defp record_elements(<<_oid :: uint32, -1 :: int32, rest :: binary>>, num, decoder) do
[nil | record_elements(rest, num-1, decoder)]
end
defp record_elements(<<oid :: uint32, size :: int32, elem :: binary(size), rest :: binary>>,
num, decoder) do
value = decoder.(oid, elem)
[value | record_elements(rest, num-1, decoder)]
end
defp decode_range(<<flags>>, _oid, _types) when (flags &&& @range_empty) != 0 do
%Postgrex.Range{}
end
defp decode_range(<<flags, rest::binary>>, oid, types) do
if (flags &&& @range_lb_inf) != 0 do
lower = nil
else
<<size::int32, lower::binary(size), rest::binary>> = rest
lower = Postgrex.Types.decode(oid, lower, types)
end
if (flags &&& @range_ub_inf) != 0 do
upper = nil
else
<<size::int32, upper::binary(size), rest::binary>> = rest
upper = Postgrex.Types.decode(oid, upper, types)
end
"" = rest
lower_inclusive = (flags &&& @range_lb_inc) != 0
upper_inclusive = (flags &&& @range_ub_inc) != 0
%Postgrex.Range{lower: lower, upper: upper, lower_inclusive: lower_inclusive,
upper_inclusive: upper_inclusive}
end
def decode_hstore(<<_length::int32, pairs::binary>>) do
decode_hstore_payload(pairs, %{})
end
defp decode_hstore_payload(<<>>, acc) do
acc
end
# in the case of a NULL value, there won't be a length
defp decode_hstore_payload(<<key_length::int32, key::binary(key_length),
-1::int32, rest::binary>>, acc) do
decode_hstore_payload(rest, Map.put(acc, key, nil))
end
defp decode_hstore_payload(<<key_length::int32, key::binary(key_length),
value_length::int32, value::binary(value_length), rest::binary>>, acc) do
decode_hstore_payload(rest, Map.put(acc, key, value))
end
end