Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release-19.1: pgwire: fix decimal decoding with trailing zeroes #45671

Merged
merged 1 commit into from
Mar 4, 2020
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
9 changes: 9 additions & 0 deletions pkg/acceptance/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ func TestDockerJava(t *testing.T) {
testDockerFail(ctx, t, "java", []string{"sh", "-c", "cd /mnt/data/java && mvn -o foobar"})
}

func TestDockerElixir(t *testing.T) {
s := log.Scope(t)
defer s.Close(t)

ctx := context.Background()
testDockerSuccess(ctx, t, "elixir", []string{"sh", "-c", "cd /mnt/data/elixir/test_crdb && mix local.hex --force && mix deps.get && psql -c 'CREATE DATABASE IF NOT EXISTS testdb' && mix test"})
testDockerFail(ctx, t, "elixir", []string{"sh", "-c", "cd /mnt/data/elixir/test_crdb && mix local.hex --force && mix deps.get && mix thisshouldfail"})
}

func TestDockerNodeJS(t *testing.T) {
s := log.Scope(t)
defer s.Close(t)
Expand Down
4 changes: 2 additions & 2 deletions pkg/acceptance/cluster/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ func GenerateCerts(ctx context.Context) func() {
// Root user.
maybePanic(security.CreateClientPair(
certsDir, filepath.Join(certsDir, security.EmbeddedCAKey),
512, 48*time.Hour, false, security.RootUser, true /* generate pk8 key */))
1024, 48*time.Hour, false, security.RootUser, true /* generate pk8 key */))

// Test user.
maybePanic(security.CreateClientPair(
certsDir, filepath.Join(certsDir, security.EmbeddedCAKey),
512, 48*time.Hour, false, "testuser", true /* generate pk8 key */))
1024, 48*time.Hour, false, "testuser", true /* generate pk8 key */))

// Certs for starting a cockroach server. Key size is from cli/cert.go:defaultKeySize.
maybePanic(security.CreateNodePair(
Expand Down
12 changes: 9 additions & 3 deletions pkg/acceptance/testdata/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ubuntu:artful-20170916
FROM ubuntu:18.04

# This Dockerfile bundles several language runtimes and test frameworks into one
# container for our acceptance tests.
Expand Down Expand Up @@ -49,12 +49,18 @@ RUN apt-get install --yes --no-install-recommends openjdk-8-jdk \
RUN curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg > /etc/apt/trusted.gpg.d/yarn.asc \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -fsSL https://packages.microsoft.com/keys/microsoft.asc > /etc/apt/trusted.gpg.d/microsoft.asc \
&& echo "deb https://packages.microsoft.com/repos/microsoft-ubuntu-zesty-prod zesty main" > /etc/apt/sources.list.d/microsoft.list \
&& echo "deb https://packages.microsoft.com/repos/microsoft-ubuntu-bionic-prod/ bionic main" > /etc/apt/sources.list.d/microsoft.list \
&& apt-get install --yes --no-install-recommends gnupg \
&& curl https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb > erlang-solutions_2.0_all.deb && dpkg -i erlang-solutions_2.0_all.deb \
&& apt-get update \
&& apt-get install --yes --no-install-recommends \
dotnet-sdk-2.0.0 \
dotnet-sdk-2.1 \
dotnet-runtime-2.1 \
expect \
elixir \
esl-erlang \
libc6-dev \
libcurl4 \
libpq-dev \
libpqtypes-dev \
make \
Expand Down
4 changes: 4 additions & 0 deletions pkg/acceptance/testdata/elixir/test_crdb/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
24 changes: 24 additions & 0 deletions pkg/acceptance/testdata/elixir/test_crdb/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
debug-*.tar

22 changes: 22 additions & 0 deletions pkg/acceptance/testdata/elixir/test_crdb/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Cockroach.MixProject do
use Mix.Project

def project do
[
app: :debug,
version: "0.1.0",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

def application do
[
extra_applications: [:logger]
]
end

defp deps do
[{:postgrex, "~> 0.13", hex: :postgrex_cdb, override: true}]
end
end
6 changes: 6 additions & 0 deletions pkg/acceptance/testdata/elixir/test_crdb/mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
%{
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm", "5f0a16a58312a610d5eb0b07506280c65f5137868ad479045f2a2dc4ced80550"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"postgrex": {:hex, :postgrex_cdb, "0.13.5", "83f818ea1b959d176c694bb60c36f42080c36a7413f0d3b05d58ef2093fe17f5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "1a01ba25472ad33bafbac6f042fde2dbab93e23bdaa49ffa3722926165c1052f"},
}
63 changes: 63 additions & 0 deletions pkg/acceptance/testdata/elixir/test_crdb/test/decimal_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
defmodule Cockroach.TestDecimal do
use ExUnit.Case

test "handles decimal correctly" do
ssl_opts = [certfile: System.get_env("PGSSLCERT"), keyfile: System.get_env("PGSSLKEY")]
{port, _} = Integer.parse(System.get_env("PGPORT") || "26257")
{start_ok, pid} = Postgrex.start_link(
hostname: System.get_env("PGHOST") || "localhost",
username: System.get_env("PGUSER") || "root",
password: "",
database: "testdb",
port: port,
ssl: (System.get_env("PGSSLCERT") != nil && true) || false,
ssl_opts: ssl_opts
)
assert start_ok
for dec <- [
Decimal.new("0"),
Decimal.new("0.0"),
Decimal.new("0.00"),
Decimal.new("0.000"),
Decimal.new("0.0000"),
Decimal.new("0.00000"),
Decimal.new("0.00001"),
Decimal.new("0.000012"),
Decimal.new("0.0000012"),
Decimal.new("0.00000012"),
Decimal.new("0.00000012"),
Decimal.new(".00000012"),
Decimal.new("1"),
Decimal.new("1.0"),
Decimal.new("1.000"),
Decimal.new("1.0000"),
Decimal.new("1.00000"),
Decimal.new("1.000001"),
Decimal.new("12345"),
Decimal.new("12345.0"),
Decimal.new("12345.000"),
Decimal.new("12345.0000"),
Decimal.new("12345.00000"),
Decimal.new("12345.000001"),
Decimal.new("12340"),
Decimal.new("123400"),
Decimal.new("1234000"),
Decimal.new("12340000"),
Decimal.new("12340.00"),
Decimal.new("123400.00"),
Decimal.new("1234000.00"),
Decimal.new("12340000.00"),
Decimal.new("1.2345e-10"),
Decimal.new("1.23450e-10"),
Decimal.new("1.234500e-10"),
Decimal.new("1.2345000e-10"),
Decimal.new("1.23450000e-10"),
Decimal.new("1.234500000e-10"),
] do
{result_ok, result} = Postgrex.query(pid, "SELECT CAST($1 AS DECIMAL)", [dec])
assert result_ok
[[ret]] = result.rows
assert ret == dec
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()
2 changes: 1 addition & 1 deletion pkg/acceptance/util_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func testDockerSuccess(ctx context.Context, t *testing.T, name string, cmd []str
const (
// Iterating against a locally built version of the docker image can be done
// by changing acceptanceImage to the hash of the container.
acceptanceImage = "docker.io/cockroachdb/acceptance:20180416-090309"
acceptanceImage = "docker.io/cockroachdb/acceptance:20200303-091324"
)

func testDocker(
Expand Down
10 changes: 10 additions & 0 deletions pkg/sql/pgwire/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,16 @@ func TestExoticNumericEncodings(t *testing.T) {
{apd.New(100000000, 0), []byte{0, 2, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0}},
{apd.New(100000000, 0), []byte{0, 3, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}},
{apd.New(100000001, 0), []byte{0, 3, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1}},
// Elixir/Postgrex combinations.
{apd.New(1234, 0), []byte{0, 2, 0, 0, 0, 0, 0, 0, 0x4, 0xd2, 0, 0}},
{apd.New(12340, -1), []byte{0, 2, 0, 0, 0, 0, 0, 1, 0x4, 0xd2, 0, 0}},
{apd.New(1234123400, -2), []byte{0, 3, 0, 1, 0, 0, 0, 2, 0x4, 0xd2, 0x4, 0xd2, 0, 0}},
{apd.New(12340000, 0), []byte{0, 3, 0, 1, 0, 0, 0, 0, 0x4, 0xd2, 0, 0, 0, 0}},
{apd.New(123400000, -1), []byte{0, 3, 0, 1, 0, 0, 0, 1, 0x4, 0xd2, 0, 0, 0, 0}},
{apd.New(12341234000000, -2), []byte{0, 4, 0, 2, 0, 0, 0, 2, 0x4, 0xd2, 0x4, 0xd2, 0, 0, 0, 0}},
// Postgrex inspired -- even more trailing zeroes!
{apd.New(0, 0), []byte{0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{apd.New(1234123400, -2), []byte{0, 4, 0, 1, 0, 0, 0, 2, 0x4, 0xd2, 0x4, 0xd2, 0, 0, 0, 0}},
}

evalCtx := tree.MakeTestingEvalContext(nil)
Expand Down
80 changes: 57 additions & 23 deletions pkg/sql/pgwire/pgwirebase/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,48 +441,82 @@ func DecodeOidDatum(

if alloc.pgNum.Ndigits > 0 {
decDigits := make([]byte, 0, int(alloc.pgNum.Ndigits)*PGDecDigits)
nextDigit := func() error {
for i := int16(0); i < alloc.pgNum.Ndigits; i++ {
if err := binary.Read(r, binary.BigEndian, &alloc.i16); err != nil {
return err
return nil, err
}
// Each 16-bit "digit" can represent a 4 digit number.
// In the case where each digit is not 4 digits, we must append
// padding to the beginning, i.e.
// * "1234" stays "1234"
// * "123" becomes "0123"
// * "12" becomes "0012"
// * "1" becomes "0001"
// * "0" becomes "0000"
// * "123456" becomes ["0012", "3456"]
// * "123456.789" becomes ["0012", "3456", "7890"]
// * "120123.45678" becomes ["0012", "0123", "4567", "8000"]
numZeroes := PGDecDigits
for i16 := alloc.i16; i16 > 0; i16 /= 10 {
numZeroes--
}
for ; numZeroes > 0; numZeroes-- {
decDigits = append(decDigits, '0')
}
return nil
}

for i := int16(0); i < alloc.pgNum.Ndigits-1; i++ {
if err := nextDigit(); err != nil {
return nil, err
}
if alloc.i16 > 0 {
decDigits = strconv.AppendUint(decDigits, uint64(alloc.i16), 10)
}
}

// The last digit may contain padding, which we need to deal with.
if err := nextDigit(); err != nil {
return nil, err
}
Dscale := (alloc.pgNum.Ndigits - (alloc.pgNum.Weight + 1)) * PGDecDigits
if overScale := Dscale - alloc.pgNum.Dscale; overScale > 0 {
Dscale -= overScale
for i := int16(0); i < overScale; i++ {
alloc.i16 /= 10
}
}
if alloc.i16 > 0 {
decDigits = strconv.AppendUint(decDigits, uint64(alloc.i16), 10)
// In the case of padding zeros at the end, we may have padded too many
// digits in the loop. This can be determined if the weight (defined as
// number of 4 digit groups left of the decimal point - 1) + the scale
// (total number of digits on the RHS of the decimal point) is less
// than the number of digits given.
//
// In Postgres, this is handled by the "remove trailing zeros" in
// `make_result_opt_error`, as well as `trunc_var`.
// Any zeroes are implicitly added back in when operating on the decimal
// value.
//
// Examples (with "," in the digit string for clarity):
// * for "1234", we have digits ["1234", "0"] for scale 0, which would
// make the digit string "1234,0000". For scale 0, we need to cut it back
// to "1234".
// * for "1234.0", we have digits ["1234", "0"] for scale 1, which would
// make the digit string "1234,0000". For scale 1, we need to cut it back
// to "1234.0".
// * for "1234.000000" we have digits ["1234", "0", "0"] with scale 6,
// which would make the digit string "1234,0000,0000". We need to cut it
// back to "1234,0000,00" for this to be correct.
// * for "123456.00000", we have digits ["12", "3456", "0", "0"] with
// scale 5, which would make digit string "0012,3456,0000,0000". We need
// to cut it back to "0012,3456,0000,0" for this to be correct.
// * for "123456.000000000", we may have digits ["12", "3456", "0", "0", "0"]
// with scale 5, which would make digit string "0012,3456,0000,0000".
// We need to cut it back to "0012,3456,0000,0" for this to be correct.
//
// This is handled by the below code, which truncates the decDigits
// such that it fits into the desired dscale. To do this:
// * ndigits [number of digits provided] - (weight+1) gives the number
// of digits on the RHS of the decimal place value as determined by
// the given input. Note dscale can be negative, meaning we truncated
// the leading zeroes at the front, giving a higher exponent (e.g. 0042,0000
// can omit the trailing 0000, giving dscale of -4, which makes the exponent 4).
// * if the digits we have in the buffer on the RHS, as calculated above,
// is larger than our calculated dscale, truncate our buffer to match the
// desired dscale.
dscale := (alloc.pgNum.Ndigits - (alloc.pgNum.Weight + 1)) * PGDecDigits
if overScale := dscale - alloc.pgNum.Dscale; overScale > 0 {
dscale -= overScale
decDigits = decDigits[:len(decDigits)-int(overScale)]
}

decString := string(decDigits)
if _, ok := alloc.dd.Coeff.SetString(decString, 10); !ok {
return nil, errors.Errorf("could not parse string %q as decimal", decString)
}
alloc.dd.Exponent = -int32(Dscale)
alloc.dd.Exponent = -int32(dscale)
}

switch alloc.pgNum.Sign {
Expand Down
21 changes: 21 additions & 0 deletions pkg/sql/pgwire/testdata/encodings.json
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,27 @@
"TextAsBinary": [52, 50],
"Binary": [0, 1, 0, 0, 0, 0, 0, 0, 0, 42]
},
{
"SQL": "'42.0'::decimal",
"Oid": 1700,
"Text": "42.0",
"TextAsBinary": [52, 50, 46, 48],
"Binary": [0, 1, 0, 0, 0, 0, 0, 1, 0, 42]
},
{
"SQL": "'420000'::decimal",
"Oid": 1700,
"Text": "420000",
"TextAsBinary": [52, 50, 48, 48, 48, 48],
"Binary": [0, 1, 0, 1, 0, 0, 0, 0, 0, 42]
},
{
"SQL": "'420000.0'::decimal",
"Oid": 1700,
"Text": "420000.0",
"TextAsBinary": [52, 50, 48, 48, 48, 48, 46, 48],
"Binary": [0, 1, 0, 1, 0, 0, 0, 1, 0, 42]
},
{
"SQL": "'{-000.000,-0000021234.23246346000000,-1.2,.0,.1,.1234}'::decimal[]",
"Oid": 1231,
Expand Down