Skip to content
Open
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
4 changes: 4 additions & 0 deletions docs/releases/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
- All successfully read license states exit 0. Configuration, request, decoding, and output failures exit nonzero. No token or database header is sent to `/health`, and no authentication verification is performed. Normal connection selection, timeout, TLS verification, and identity opt-out settings apply.

License installation and key/file configuration remain server responsibilities. Existing `ping -o json` license pass-through is unchanged. Closes [arcli#34](https://github.com/Basekick-Labs/arcli/issues/34).

### Fixed

- Accept large integers that can be represented exactly as float64 in mixed numeric JSON write columns. Lossy conversions remain rejected, and integer-only columns retain int64 encoding.
76 changes: 76 additions & 0 deletions internal/client/msgpack_numeric_columns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package client

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"testing"
)

func TestJSONToMsgPack_MixedNumericColumnExactIntegers(t *testing.T) {
for _, tc := range []struct {
value int64
exact bool
}{
{1 << 53, true},
{(1 << 53) + 2, true},
{-(1 << 53) - 2, true},
{1 << 54, true},
{math.MaxInt64 - 1023, true},
{math.MinInt64, true},
{(1 << 53) + 1, false},
{-(1 << 53) - 1, false},
{math.MaxInt64, false},
{math.MinInt64 + 1, false},
} {
for _, floatFirst := range []bool{false, true} {
t.Run(fmt.Sprintf("%d/float-first=%t", tc.value, floatFirst), func(t *testing.T) {
values := []any{nil, json.Number(strconv.FormatInt(tc.value, 10)), json.Number("1.5")}
wantValues := []any{nil, float64(tc.value), float64(1.5)}
if floatFirst {
values[1], values[2] = values[2], values[1]
wantValues[1], wantValues[2] = wantValues[2], wantValues[1]
}
in, err := json.Marshal(map[string]any{"m": "cpu", "columns": map[string]any{"value": values}})
if err != nil {
t.Fatal(err)
}
out, sum, err := JSONToMsgPack(bytes.NewReader(in))
if !tc.exact {
if err == nil || !strings.Contains(err.Error(), "cannot be represented exactly") || out != nil {
t.Errorf("inexact integer: output=%x error=%v", out, err)
}
return
}
if err != nil {
t.Fatal(err)
}
want := encodeHex(t, map[string]any{"m": "cpu", "columns": map[string]any{"value": wantValues}})
if got := hex.EncodeToString(out); got != want {
t.Errorf("MessagePack = %s, want %s", got, want)
}
if sum.Items != 1 || sum.Columnar != 1 || sum.Rows != 3 {
t.Errorf("summary = %+v", sum)
}
})
}
}
}

func TestJSONToMsgPack_IntegerColumnRetainsInt64(t *testing.T) {
const in = `{"m":"cpu","columns":{"value":[9007199254740993,9223372036854775807,-9223372036854775808]}}`
out, _, err := JSONToMsgPack(strings.NewReader(in))
if err != nil {
t.Fatal(err)
}
want := encodeHex(t, map[string]any{
"m": "cpu", "columns": map[string]any{"value": []any{int64(9007199254740993), int64(math.MaxInt64), int64(math.MinInt64)}},
})
if got := hex.EncodeToString(out); got != want {
t.Errorf("integer column = %s, want %s", got, want)
}
}
6 changes: 4 additions & 2 deletions internal/client/msgpackenc.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,12 @@ func convertColumn(col string, arr []any, where string) ([]any, error) {
if !isTime && hasInt && hasFloat {
for i, v := range out {
if iv, ok := v.(int64); ok {
if iv > 1<<53 || iv < -(1<<53) {
f := float64(iv)
// MaxInt64 rounds to 2^63; reject that before converting back.
if f >= 1<<63 || int64(f) != iv {
return nil, fmt.Errorf("%s: column %q mixes integers and floats and %d cannot be represented exactly as a float", where, col, iv)
}
out[i] = float64(iv)
out[i] = f
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions internal/commands/write_numeric_columns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package commands

import (
"bytes"
"encoding/hex"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestWriteJSON_ExactLargeIntegerInMixedColumn(t *testing.T) {
bodies := make(chan []byte, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/write/msgpack" {
t.Errorf("path = %q", r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
bodies <- body
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
writeTestConfig(t, srv.URL, "tok")
cmd := newWriteCmd()
cmd.SetIn(strings.NewReader(`{"m":"cpu","columns":{"value":[18014398509481984,1.5]}}`))
out, _, err := execCmd(t, cmd, "--format", "json")
if err != nil {
t.Fatal(err)
}
body := <-bodies
// Both values use float64 tags; 2^54 is exactly representable.
want, err := hex.DecodeString("82a7636f6c756d6e7381a576616c756592cb4350000000000000cb3ff8000000000000a16da3637075")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(body, want) {
t.Errorf("uploaded MessagePack = %x, want %x", body, want)
}
if !strings.Contains(out, "OK: 1 measurement(s), 2 row(s)") {
t.Errorf("stdout = %q", out)
}
}
Loading