From e5ab79c35b09e95ccbe9d5f63263a05114132ed1 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 17 Feb 2026 11:37:18 -0600 Subject: [PATCH 1/2] fix(protocol): handle missing types in protocolBufferFromValue - Add uint, uint8, uint16, uint32, uint64 to the integer type switch so unsigned integers are serialized instead of silently dropped - Fix float32 panic in protocolBufferFromFloat caused by unconditional float64 type assertion - Dereference pointer types (*int, *string, etc.) via reflect, mapping nil pointers to NULL and non-nil pointers to their underlying value - Add unit tests for all supported types and a mixed-array regression test that reproduces the original buffer count mismatch --- chunk.go | 19 ++++++- chunk_internal_test.go | 109 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 chunk_internal_test.go diff --git a/chunk.go b/chunk.go index 7eb120c..2781ec3 100644 --- a/chunk.go +++ b/chunk.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net" + "reflect" "strconv" "strings" "time" @@ -225,7 +226,7 @@ func protocolBufferFromValue(v interface{}) [][]byte { switch v := v.(type) { case nil: return protocolBufferFromNull() - case int, int8, int16, int32, int64: + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return protocolBufferFromInt(v) case float32, float64: return protocolBufferFromFloat(v) @@ -234,6 +235,13 @@ func protocolBufferFromValue(v interface{}) [][]byte { case []byte: return protocolBufferFromBytes(v) default: + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + if rv.IsNil() { + return protocolBufferFromNull() + } + return protocolBufferFromValue(rv.Elem().Interface()) + } return make([][]byte, 0) } } @@ -255,7 +263,14 @@ func protocolBufferFromInt(v interface{}) [][]byte { } func protocolBufferFromFloat(v interface{}) [][]byte { - return [][]byte{[]byte(fmt.Sprintf("%c%s ", CMD_FLOAT, strconv.FormatFloat(v.(float64), 'f', -1, 64)))} + var f float64 + switch v := v.(type) { + case float32: + f = float64(v) + case float64: + f = v + } + return [][]byte{[]byte(fmt.Sprintf("%c%s ", CMD_FLOAT, strconv.FormatFloat(f, 'f', -1, 64)))} } // func protocolBufferFromFloat(v interface{}) [][]byte { diff --git a/chunk_internal_test.go b/chunk_internal_test.go new file mode 100644 index 0000000..5c2d70b --- /dev/null +++ b/chunk_internal_test.go @@ -0,0 +1,109 @@ +package sqlitecloud + +import ( + "testing" +) + +func TestProtocolBufferFromValue(t *testing.T) { + tests := []struct { + name string + value interface{} + wantLen int // expected number of []byte buffers returned + wantType byte + }{ + // Basic types + {"nil", nil, 1, CMD_NULL}, + {"string", "hello", 1, CMD_ZEROSTRING}, + {"int", int(42), 1, CMD_INT}, + {"int8", int8(8), 1, CMD_INT}, + {"int16", int16(16), 1, CMD_INT}, + {"int32", int32(32), 1, CMD_INT}, + {"int64", int64(64), 1, CMD_INT}, + {"float32", float32(3.14), 1, CMD_FLOAT}, + {"float64", float64(2.71), 1, CMD_FLOAT}, + {"[]byte", []byte("blob"), 2, CMD_BLOB}, // header + data + + // Unsigned integers + {"uint", uint(1), 1, CMD_INT}, + {"uint8", uint8(1), 1, CMD_INT}, + {"uint16", uint16(1), 1, CMD_INT}, + {"uint32", uint32(1), 1, CMD_INT}, + {"uint64", uint64(1), 1, CMD_INT}, + + // Pointer types (dereferenced) + {"*int", intPtr(42), 1, CMD_INT}, + {"*string", strPtr("hello"), 1, CMD_ZEROSTRING}, + {"*int nil", (*int)(nil), 1, CMD_NULL}, + {"*string nil", (*string)(nil), 1, CMD_NULL}, + + // Unsupported types still return empty buffers + {"bool", true, 0, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buffers := protocolBufferFromValue(tt.value) + if len(buffers) != tt.wantLen { + t.Errorf("protocolBufferFromValue(%T(%v)): got %d buffers, want %d", tt.value, tt.value, len(buffers), tt.wantLen) + } + if tt.wantLen > 0 && len(buffers) > 0 { + if buffers[0][0] != tt.wantType { + t.Errorf("protocolBufferFromValue(%T(%v)): got type %c, want %c", tt.value, tt.value, buffers[0][0], tt.wantType) + } + } + }) + } +} + +func TestProtocolBufferFromValueMixedArray(t *testing.T) { + // Simulates the loop in sendArray: builds buffers from a mixed values slice + // and checks that the number of buffer groups matches the number of values. + pInt := intPtr(99) + values := []interface{}{ + "hello", // string -> 1 buffer + int(42), // int -> 1 buffer + nil, // nil -> 1 buffer + pInt, // *int -> 1 buffer (dereferenced to int) + float64(3), // float64 -> 1 buffer + uint(7), // uint -> 1 buffer + []byte("x"), // []byte -> 2 buffers (header+data) + } + + // Count how many values produce at least one buffer + buffersPerValue := make([]int, len(values)) + totalBuffers := 0 + missingValues := 0 + + for i, v := range values { + bufs := protocolBufferFromValue(v) + buffersPerValue[i] = len(bufs) + totalBuffers += len(bufs) + if len(bufs) == 0 { + missingValues++ + t.Errorf("value[%d] (%T = %v) produced 0 buffers — will be silently dropped", i, v, v) + } + } + + if missingValues > 0 { + t.Errorf("%d out of %d values produced no buffers and will be missing from the protocol message", missingValues, len(values)) + } + + // Reproduce the exact loop from sendArray + buffers := [][]byte{} + for _, v := range values { + buffers = append(buffers, protocolBufferFromValue(v)...) + } + + t.Logf("values count: %d, total buffers: %d, buffers per value: %v", len(values), len(buffers), buffersPerValue) + + // Every value must produce at least 1 buffer ([]byte produces 2) + expectedMinBuffers := len(values) + if len(buffers) < expectedMinBuffers { + t.Errorf("buffers array has %d elements, expected at least %d (one per value). %d values were silently dropped.", + len(buffers), expectedMinBuffers, missingValues) + } +} + +// helpers +func intPtr(v int) *int { return &v } +func strPtr(v string) *string { return &v } From eb667869c7a76c4039754cfff3c57f917a2db0b5 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 17 Feb 2026 11:48:02 -0600 Subject: [PATCH 2/2] test: add internal unit test to "testing" github workflow --- .github/workflows/testing.yaml | 6 ++++-- Makefile | 9 ++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 0c5e262..54395f9 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -26,7 +26,9 @@ jobs: uses: golangci/golangci-lint-action@v6 with: version: latest - - name: Tests + - name: Unit Tests + run: make test-unit-codecov + - name: Integration Tests env: SQLITE_CONNECTION_STRING: ${{ vars.SQLITE_CONNECTION_STRING }} SQLITE_USER: ${{ secrets.SQLITE_USER }} @@ -40,4 +42,4 @@ jobs: uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./test/coverage.out + files: ./coverage-unit.out,./test/coverage.out diff --git a/Makefile b/Makefile index 3b3ae20..4591f5f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,14 @@ setup-ide: cd test; go mod tidy cd cli; go mod tidy -# Test SDK +# Unit tests (root package) +test-unit: + go test -v . + +test-unit-codecov: + go test -v -race -coverprofile=coverage-unit.out -covermode=atomic . + +# Integration tests (test/ directory) test: cd test; go mod tidy && go test -v .