Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- +goose Up

-- Update the trigger to also handle SQL NULL (not just JSON 'null' literal).
-- The NOT NULL constraint on metadata rejects SQL NULLs, but the BEFORE trigger
-- runs first and can convert them to '{}' before the constraint is checked.
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION fix_snapshots_metadata_json_null()
RETURNS trigger AS $$
BEGIN
IF NEW.metadata IS NULL OR NEW.metadata = 'null'::jsonb THEN
NEW.metadata := '{}'::jsonb;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose Down

-- Restore original trigger that only handles JSON null.
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION fix_snapshots_metadata_json_null()
RETURNS trigger AS $$
BEGIN
IF NEW.metadata = 'null'::jsonb THEN
NEW.metadata := '{}'::jsonb;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
30 changes: 30 additions & 0 deletions packages/db/pkg/types/types.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
package types

import "encoding/json"

type JSONBStringMap map[string]string

// MarshalJSON ensures a nil map serializes as "{}" instead of "null",
// preventing SQL NULL when pgx encodes the value for jsonb columns.
func (m JSONBStringMap) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("{}"), nil
}
Copy link

Choose a reason for hiding this comment

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

The MarshalJSON override affects all JSON serialization of JSONBStringMap, not only DB persistence. Any API response structs embedding this type that previously returned null for absent metadata will now return {}. Clients distinguishing null (field absent/not set) from {} (explicitly empty) will see a behavioral change. Worth verifying no external API consumers rely on the null sentinel before merging.

Copy link
Contributor

Choose a reason for hiding this comment

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

its ok


return json.Marshal(map[string]string(m))
}

// UnmarshalJSON ensures JSON null deserializes as an empty map instead of nil.
func (m *JSONBStringMap) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
*m = JSONBStringMap{}

return nil
}

var raw map[string]string
if err := json.Unmarshal(data, &raw); err != nil {
return err
}

*m = JSONBStringMap(raw)

return nil
}

type BuildReason struct {
// Message with the status reason, currently reporting only for error status
Message string `json:"message"`
Expand Down
77 changes: 77 additions & 0 deletions packages/db/pkg/types/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package types

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestJSONBStringMap_MarshalJSON_Nil(t *testing.T) {

Check failure on line 11 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_MarshalJSON_Nil missing the call to method parallel (paralleltest)
var m JSONBStringMap
data, err := json.Marshal(m)
require.NoError(t, err)
assert.Equal(t, "{}", string(data))
}

func TestJSONBStringMap_MarshalJSON_Empty(t *testing.T) {

Check failure on line 18 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_MarshalJSON_Empty missing the call to method parallel (paralleltest)
m := JSONBStringMap{}
data, err := json.Marshal(m)
require.NoError(t, err)
assert.Equal(t, "{}", string(data))
}

func TestJSONBStringMap_MarshalJSON_WithValues(t *testing.T) {

Check failure on line 25 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_MarshalJSON_WithValues missing the call to method parallel (paralleltest)
m := JSONBStringMap{"key": "value"}
data, err := json.Marshal(m)
require.NoError(t, err)
assert.Equal(t, `{"key":"value"}`, string(data))

Check failure on line 29 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

encoded-compare: use assert.JSONEq (testifylint)
}

func TestJSONBStringMap_UnmarshalJSON_Null(t *testing.T) {

Check failure on line 32 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_UnmarshalJSON_Null missing the call to method parallel (paralleltest)
var m JSONBStringMap
err := json.Unmarshal([]byte("null"), &m)
require.NoError(t, err)
assert.NotNil(t, m)
assert.Empty(t, m)
}

func TestJSONBStringMap_UnmarshalJSON_EmptyObject(t *testing.T) {

Check failure on line 40 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_UnmarshalJSON_EmptyObject missing the call to method parallel (paralleltest)
var m JSONBStringMap
err := json.Unmarshal([]byte("{}"), &m)
require.NoError(t, err)
assert.NotNil(t, m)
assert.Empty(t, m)
}

func TestJSONBStringMap_UnmarshalJSON_WithValues(t *testing.T) {

Check failure on line 48 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_UnmarshalJSON_WithValues missing the call to method parallel (paralleltest)
var m JSONBStringMap
err := json.Unmarshal([]byte(`{"key":"value"}`), &m)
require.NoError(t, err)
assert.Equal(t, JSONBStringMap{"key": "value"}, m)
}

func TestJSONBStringMap_RoundTrip(t *testing.T) {

Check failure on line 55 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_RoundTrip missing the call to method parallel (paralleltest)
original := JSONBStringMap{"foo": "bar", "baz": "qux"}
data, err := json.Marshal(original)
require.NoError(t, err)

var decoded JSONBStringMap
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, original, decoded)
}

func TestJSONBStringMap_NilRoundTrip(t *testing.T) {

Check failure on line 66 in packages/db/pkg/types/types_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (/home/runner/work/infra/infra/packages/db)

Function TestJSONBStringMap_NilRoundTrip missing the call to method parallel (paralleltest)
var original JSONBStringMap // nil
data, err := json.Marshal(original)
require.NoError(t, err)
assert.Equal(t, "{}", string(data))

var decoded JSONBStringMap
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.NotNil(t, decoded)
assert.Empty(t, decoded)
}
Loading