Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions lib/postgrex/extensions/inet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,51 @@ defmodule Postgrex.Extensions.INET do
import Postgrex.BinaryUtils, warn: false
use Postgrex.BinaryExtension, send: "cidr_send", send: "inet_send"

@octet_range 0..255
@hextet_range 0..65535
@ipv4_netmask_range 0..32
@ipv6_netmask_range 0..128

def encode(_) do
octet_range = Macro.escape(@octet_range)
hextet_range = Macro.escape(@hextet_range)
ipv4_netmask_range = Macro.escape(@ipv4_netmask_range)
ipv6_netmask_range = Macro.escape(@ipv6_netmask_range)

quote location: :keep do
%Postgrex.INET{address: {a, b, c, d}, netmask: nil} ->
%Postgrex.INET{address: {a, b, c, d}, netmask: nil}
when a in unquote(octet_range) and b in unquote(octet_range) and
c in unquote(octet_range) and d in unquote(octet_range) ->
<<8::int32(), 2, 32, 0, 4, a, b, c, d>>

%Postgrex.INET{address: {a, b, c, d}, netmask: n} ->
%Postgrex.INET{address: {a, b, c, d}, netmask: n}
when a in unquote(octet_range) and b in unquote(octet_range) and
c in unquote(octet_range) and d in unquote(octet_range) and
n in unquote(ipv4_netmask_range) ->
<<8::int32(), 2, n, 1, 4, a, b, c, d>>

%Postgrex.INET{address: {a, b, c, d, e, f, g, h}, netmask: nil} ->
%Postgrex.INET{address: {a, b, c, d, e, f, g, h}, netmask: nil}
when a in unquote(hextet_range) and b in unquote(hextet_range) and
c in unquote(hextet_range) and d in unquote(hextet_range) and
e in unquote(hextet_range) and f in unquote(hextet_range) and
g in unquote(hextet_range) and h in unquote(hextet_range) ->
<<20::int32(), 3, 128, 0, 16, a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>>

%Postgrex.INET{address: {a, b, c, d, e, f, g, h}, netmask: n} ->
%Postgrex.INET{address: {a, b, c, d, e, f, g, h}, netmask: n}
when a in unquote(hextet_range) and b in unquote(hextet_range) and
c in unquote(hextet_range) and d in unquote(hextet_range) and
e in unquote(hextet_range) and f in unquote(hextet_range) and
g in unquote(hextet_range) and h in unquote(hextet_range) and
n in unquote(ipv6_netmask_range) ->
<<20::int32(), 3, n, 1, 16, a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>>

other ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.INET)
raise DBConnection.EncodeError,
Postgrex.Utils.encode_msg(
other,
"a %Postgrex.INET{} with octets in 0..255 and netmask in 0..32 (IPv4) " <>
"or groups in 0..65535 and netmask in 0..128 (IPv6)"
)
end
end

Expand Down
24 changes: 21 additions & 3 deletions lib/postgrex/extensions/interval.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ defmodule Postgrex.Extensions.Interval do
@int64_min -9_223_372_036_854_775_808
@int32_max 2_147_483_647
@int32_min -2_147_483_648
@int64_range @int64_min..@int64_max
@int32_range @int32_min..@int32_max

def init(opts) do
infinity? = Keyword.get(opts, :allow_infinite_intervals, false)
Expand Down Expand Up @@ -42,7 +44,7 @@ defmodule Postgrex.Extensions.Interval do

%Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} ->
microseconds = 1_000_000 * seconds + microseconds
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>
unquote(__MODULE__).encode_interval(microseconds, days, months)

%Duration{
year: years,
Expand All @@ -57,7 +59,7 @@ defmodule Postgrex.Extensions.Interval do
months = 12 * years + months
days = 7 * weeks + days
microseconds = 1_000_000 * (3600 * hours + 60 * minutes + seconds) + microseconds
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>
unquote(__MODULE__).encode_interval(microseconds, days, months)

other ->
raise DBConnection.EncodeError,
Expand Down Expand Up @@ -133,7 +135,7 @@ defmodule Postgrex.Extensions.Interval do

%Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} ->
microseconds = 1_000_000 * seconds + microseconds
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>
unquote(__MODULE__).encode_interval(microseconds, days, months)

other ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Interval)
Expand Down Expand Up @@ -176,6 +178,22 @@ defmodule Postgrex.Extensions.Interval do
end
end

def encode_interval(microseconds, days, months) do
cond do
months not in @int32_range ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(months, @int32_range)

days not in @int32_range ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(days, @int32_range)

microseconds not in @int64_range ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(microseconds, @int64_range)

true ->
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>
end
end

def infinity_binary(:inf) do
<<16::int32(), @int64_max::int64(), @int32_max::int32(), @int32_max::int32()>>
end
Expand Down
12 changes: 10 additions & 2 deletions lib/postgrex/extensions/macaddr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ defmodule Postgrex.Extensions.MACADDR do
import Postgrex.BinaryUtils, warn: false
use Postgrex.BinaryExtension, send: "macaddr_send"

@octet_range 0..255

def encode(_) do
octet_range = Macro.escape(@octet_range)

quote location: :keep do
%Postgrex.MACADDR{address: {a, b, c, d, e, f}} ->
%Postgrex.MACADDR{address: {a, b, c, d, e, f}}
when a in unquote(octet_range) and b in unquote(octet_range) and
c in unquote(octet_range) and d in unquote(octet_range) and
e in unquote(octet_range) and f in unquote(octet_range) ->
<<6::int32(), a, b, c, d, e, f>>

other ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.MACADDR)
raise DBConnection.EncodeError,
Postgrex.Utils.encode_msg(other, "a %Postgrex.MACADDR{} with octets in 0..255")
end
end

Expand Down
20 changes: 19 additions & 1 deletion lib/postgrex/extensions/numeric.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule Postgrex.Extensions.Numeric do
import Postgrex.BinaryUtils, warn: false
use Postgrex.BinaryExtension, send: "numeric_send"

@int16_range -32_768..32_767

def encode(_) do
quote location: :keep, generated: true do
%Decimal{} = decimal ->
Expand Down Expand Up @@ -54,7 +56,23 @@ defmodule Postgrex.Extensions.Numeric do
weight = max(length(integer_digits) - 1, 0)

bin = for digit <- digits, into: "", do: <<digit::uint16()>>
[<<num_digits::int16(), weight::int16(), sign::uint16(), scale::int16()>> | bin]
[encode_header(num_digits, weight, sign, scale) | bin]
end

defp encode_header(num_digits, weight, sign, scale) do
cond do
num_digits not in @int16_range ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(num_digits, @int16_range)

weight not in @int16_range ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(weight, @int16_range)

scale not in @int16_range ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(scale, @int16_range)

true ->
<<num_digits::int16(), weight::int16(), sign::uint16(), scale::int16()>>
end
end

defp encode_sign(1), do: 0x0000
Expand Down
16 changes: 14 additions & 2 deletions lib/postgrex/extensions/tid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ defmodule Postgrex.Extensions.TID do
import Postgrex.BinaryUtils, warn: false
use Postgrex.BinaryExtension, send: "tidsend"

@block_range 0..4_294_967_295
@tuple_range 0..65_535

def encode(_) do
block_range = Macro.escape(@block_range)
tuple_range = Macro.escape(@tuple_range)

quote location: :keep do
{block, tuple} ->
{block, tuple}
when is_integer(block) and block in unquote(block_range) and
is_integer(tuple) and tuple in unquote(tuple_range) ->
<<6::int32(), block::uint32(), tuple::uint16()>>

other ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a tuple of 2 integers")
raise DBConnection.EncodeError,
Postgrex.Utils.encode_msg(
other,
"a tuple of a 32-bit integer and a 16-bit integer"
)
end
end

Expand Down
10 changes: 10 additions & 0 deletions lib/postgrex/extensions/tsvector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule Postgrex.Extensions.TSVector do
import Postgrex.BinaryUtils, warn: false
use Postgrex.BinaryExtension, send: "tsvectorsend"

@max_position 16383

def encode(_) do
quote location: :keep do
values when is_list(values) ->
Expand Down Expand Up @@ -36,6 +38,14 @@ defmodule Postgrex.Extensions.TSVector do
defp encode_positions(%Postgrex.Lexeme{word: word, positions: positions}) do
positions =
Enum.map(positions, fn {position, weight} ->
# Lexeme positions can range from 1 to 16383 with a silent ceiling at the top.
# Ref: https://www.postgresql.org/docs/current/datatype-textsearch.html#DATATYPE-TSVECTOR
if position < 1 do
raise DBConnection.EncodeError,
Postgrex.Utils.encode_msg(position, 1..@max_position)
end

position = min(position, @max_position)
Comment on lines +41 to +48

@Snehil-Shah Snehil-Shah Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to postgres docs:

"Position values can range from 1 to 16383; larger numbers are silently set to 16383"

Decided to follow their behavior, although I do feel that explicitly raising for max position can be a more consistent experience. WDYT?

<<encode_weight_binary(weight)::2, position::14>>
end)

Expand Down
133 changes: 133 additions & 0 deletions test/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,22 @@ defmodule QueryTest do
assert [[{5, 10}]] = query("select $1::tid;", [{5, 10}])
end

test "encode enforces bounds on tid", context do
# block's range is 0 to 4294967295
assert %DBConnection.EncodeError{} =
catch_error(query("select $1::tid;", [{4_294_967_295 + 1, 0}]))

assert %DBConnection.EncodeError{} =
catch_error(query("select $1::tid;", [{0 - 1, 0}]))

# tuple's range is 0 to 65535
assert %DBConnection.EncodeError{} =
catch_error(query("select $1::tid;", [{0, 65_535 + 1}]))

assert %DBConnection.EncodeError{} =
catch_error(query("select $1::tid;", [{0, 0 - 1}]))
end

test "encoding oids as binary fails with a helpful error message", context do
assert_raise ArgumentError,
~r"See https://github.com/elixir-ecto/postgrex#oid-type-encoding",
Expand Down Expand Up @@ -923,6 +939,15 @@ defmodule QueryTest do
end)
end

test "encode enforces bounds on numeric", context do
# the digit count, weight and scale headers must each fit an int16
assert %DBConnection.EncodeError{} =
catch_error(query("SELECT $1::numeric", [Decimal.new("1E140000")]))

assert %DBConnection.EncodeError{} =
catch_error(query("SELECT $1::numeric", [Decimal.new("1E-140000")]))
end

test "encode integers and floats as numeric", context do
dec = Decimal.new(1)
assert [[dec]] == query("SELECT $1::numeric", [1])
Expand Down Expand Up @@ -1055,6 +1080,45 @@ defmodule QueryTest do
])
end

test "encode enforces bounds on interval", context do
# months' range is -2147483648 to 2147483647
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::interval", [%Postgrex.Interval{months: 2_147_483_647 + 1}])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::interval", [%Postgrex.Interval{months: -2_147_483_648 - 1}])
)

# days' range is -2147483648 to 2147483647
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::interval", [%Postgrex.Interval{days: 2_147_483_647 + 1}])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::interval", [%Postgrex.Interval{days: -2_147_483_648 - 1}])
)

# microseconds' range is -9223372036854775808 to 9223372036854775807
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::interval", [
%Postgrex.Interval{microsecs: 9_223_372_036_854_775_807 + 1}
])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::interval", [
%Postgrex.Interval{microsecs: -9_223_372_036_854_775_808 - 1}
])
)
end

@tag min_pg_version: "17.0"
test "encode infinite interval" do
opts = [database: "postgrex_test", backoff_type: :stop, types: Postgrex.ElixirDurationTypes]
Expand Down Expand Up @@ -1554,6 +1618,75 @@ defmodule QueryTest do
query("SELECT $1::macaddr::text", [%Postgrex.MACADDR{address: {8, 1, 43, 5, 7, 9}}])
end

test "encode enforces bounds on inet", context do
# IPv4 octet's range is 0 to 255
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [
%Postgrex.INET{address: {255 + 1, 0, 0, 0}, netmask: nil}
])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [%Postgrex.INET{address: {0 - 1, 0, 0, 0}, netmask: nil}])
)

# IPv4 netmask's range is 0 to 32
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [%Postgrex.INET{address: {10, 0, 0, 0}, netmask: 32 + 1}])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [%Postgrex.INET{address: {10, 0, 0, 0}, netmask: 0 - 1}])
)

# IPv6 group's range is 0 to 65535
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [
%Postgrex.INET{address: {65_535 + 1, 0, 0, 0, 0, 0, 0, 0}, netmask: nil}
])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [
%Postgrex.INET{address: {0 - 1, 0, 0, 0, 0, 0, 0, 0}, netmask: nil}
])
)

# IPv6 netmask's range is 0 to 128
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [
%Postgrex.INET{address: {1, 0, 0, 0, 0, 0, 0, 0}, netmask: 128 + 1}
])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::inet", [
%Postgrex.INET{address: {1, 0, 0, 0, 0, 0, 0, 0}, netmask: 0 - 1}
])
)
end

test "encode enforces bounds on macaddr", context do
# macaddr octet's range is 0 to 255
assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::macaddr", [%Postgrex.MACADDR{address: {255 + 1, 0, 0, 0, 0, 0}}])
)

assert %DBConnection.EncodeError{} =
catch_error(
query("SELECT $1::macaddr", [%Postgrex.MACADDR{address: {0 - 1, 0, 0, 0, 0, 0}}])
)
end

test "encode bit string", context do
assert [["110"]] == query("SELECT $1::bit(3)::text", [<<1::1, 1::1, 0::1>>])
assert [["110"]] == query("SELECT $1::varbit::text", [<<1::1, 1::1, 0::1>>])
Expand Down
Loading
Loading