Repository: xataio/pgstream
Pull request #162 (2025-03-06 15:37:16)

Add unix timestamp gm transformer
Transform unix timestamps using greenmask's integer tranformer.

github-actions[bot]: ### Merging this branch changes the coverage (1 decrease, 1 increase)

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 100.00% (ø) |  |
| github.com/xataio/pgstream/pkg/transformers/builder | 10.26% (**-0.85%**) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/greenmask | 95.05% (**+0.31%**) | :thumbsup: |

---

<details>

<summary>Coverage by file</summary>

### Changed files (no unit tests)

| Changed File | Coverage Δ | Total | Covered | Missed | :robot: |
|--------------|------------|-------|---------|--------|---------|
| github.com/xataio/pgstream/pkg/transformers/builder/transformer_builder.go | 10.26% (**-0.85%**) | 39 (+3) | 4 | 35 (+3) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer.go | 100.00% (**+100.00%**) | 12 (+12) | 12 (+12) | 0 | :star2: |
| github.com/xataio/pgstream/pkg/transformers/transformer.go | 100.00% (ø) | 7 | 7 | 0 |  |

_Please note that the "Total", "Covered", and "Missed" counts above refer to ***code statements*** instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code._

### Changed unit test files

- github.com/xataio/pgstream/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer_test.go

</details>

github-actions[bot]: ### Merging this branch changes the coverage (1 decrease, 1 increase)

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 100.00% (ø) |  |
| github.com/xataio/pgstream/pkg/transformers/builder | 10.26% (**-0.85%**) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/greenmask | 95.05% (**+0.31%**) | :thumbsup: |

---

<details>

<summary>Coverage by file</summary>

### Changed files (no unit tests)

| Changed File | Coverage Δ | Total | Covered | Missed | :robot: |
|--------------|------------|-------|---------|--------|---------|
| github.com/xataio/pgstream/pkg/transformers/builder/transformer_builder.go | 10.26% (**-0.85%**) | 39 (+3) | 4 | 35 (+3) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer.go | 100.00% (**+100.00%**) | 12 (+12) | 12 (+12) | 0 | :star2: |
| github.com/xataio/pgstream/pkg/transformers/transformer.go | 100.00% (ø) | 7 | 7 | 0 |  |

_Please note that the "Total", "Covered", and "Missed" counts above refer to ***code statements*** instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code._

### Changed unit test files

- github.com/xataio/pgstream/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer_test.go

</details>


Patch:
diff --git a/pkg/transformers/builder/transformer_builder.go b/pkg/transformers/builder/transformer_builder.go
index 6265100..b403198 100644
--- a/pkg/transformers/builder/transformer_builder.go
+++ b/pkg/transformers/builder/transformer_builder.go
@@ -30,6 +30,8 @@ func New(cfg *transformers.Config) (transformers.Transformer, error) {
 		return greenmask.NewBooleanTransformer(cfg.Generator)
 	case transformers.GreenmaskChoice:
 		return greenmask.NewChoiceTransformer(cfg.Generator, cfg.Parameters)
+	case transformers.GreenmaskUnixTimestamp:
+		return greenmask.NewUnixTimestampTransformer(cfg.Generator, cfg.Parameters)
 	default:
 		return nil, transformers.ErrUnsupportedTransformer
 	}
diff --git a/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer.go b/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer.go
new file mode 100644
index 0000000..10c78c9
--- /dev/null
+++ b/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer.go
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package greenmask
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/xataio/pgstream/pkg/transformers"
+)
+
+var errMinMaxValueNotSpecified = errors.New("greenmask_unix_timestamp: min_value and max_value must be specified")
+
+func NewUnixTimestampTransformer(generatorType transformers.GeneratorType, params transformers.Parameters) (*IntegerTransformer, error) {
+	minValue, foundMin, err := transformers.FindParameter[int64](params, "min_value")
+	if err != nil {
+		return nil, fmt.Errorf("greenmask_unix_timestamp: min_value must be an int64: %w", err)
+	}
+
+	maxValue, foundMax, err := transformers.FindParameter[int64](params, "max_value")
+	if err != nil {
+		return nil, fmt.Errorf("greenmask_unix_timestamp: max_value must be an int64: %w", err)
+	}
+
+	if !foundMin || !foundMax {
+		return nil, errMinMaxValueNotSpecified
+	}
+
+	transformer, err := NewIntegerTransformer(generatorType, transformers.Parameters{
+		"size":      8,
+		"min_value": minValue,
+		"max_value": maxValue,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return transformer, nil
+}
diff --git a/pkg/transformers/transformer.go b/pkg/transformers/transformer.go
index 0133fa9..c8918b4 100644
--- a/pkg/transformers/transformer.go
+++ b/pkg/transformers/transformer.go
@@ -19,16 +19,17 @@ type Config struct {
 type TransformerType string
 
 const (
-	String             TransformerType = "string"
-	NeosyncString      TransformerType = "neosync_string"
-	GreenmaskString    TransformerType = "greenmask_string"
-	NeosyncFirstName   TransformerType = "neosync_firstname"
-	GreenmaskFirstName TransformerType = "greenmask_firstname"
-	GreenmaskInteger   TransformerType = "greenmask_integer"
-	GreenmaskFloat     TransformerType = "greenmask_float"
-	GreenmaskUUID      TransformerType = "greenmask_uuid"
-	GreenmaskBoolean   TransformerType = "greenmask_boolean"
-	GreenmaskChoice    TransformerType = "greenmask_choice"
+	String                 TransformerType = "string"
+	NeosyncString          TransformerType = "neosync_string"
+	GreenmaskString        TransformerType = "greenmask_string"
+	NeosyncFirstName       TransformerType = "neosync_firstname"
+	GreenmaskFirstName     TransformerType = "greenmask_firstname"
+	GreenmaskInteger       TransformerType = "greenmask_integer"
+	GreenmaskFloat         TransformerType = "greenmask_float"
+	GreenmaskUUID          TransformerType = "greenmask_uuid"
+	GreenmaskBoolean       TransformerType = "greenmask_boolean"
+	GreenmaskChoice        TransformerType = "greenmask_choice"
+	GreenmaskUnixTimestamp TransformerType = "greenmask_unix_timestamp"
 )
 
 type GeneratorType string


Test patch:
diff --git a/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer_test.go b/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer_test.go
new file mode 100644
index 0000000..f8cc3c2
--- /dev/null
+++ b/pkg/transformers/greenmask/greenmask_unix_timestamp_transformer_test.go
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package greenmask
+
+import (
+	"testing"
+
+	greenmasktransformers "github.com/eminano/greenmask/pkg/generators/transformers"
+	"github.com/stretchr/testify/require"
+	"github.com/xataio/pgstream/pkg/transformers"
+)
+
+func TestNewUnixTimestampTransformer(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name      string
+		generator transformers.GeneratorType
+		params    transformers.Parameters
+		wantErr   error
+	}{
+		{
+			name:      "ok - valid random",
+			generator: transformers.Random,
+			params: transformers.Parameters{
+				"min_value": int64(-2734919135000),
+				"max_value": int64(72438947289300),
+			},
+			wantErr: nil,
+		},
+		{
+			name:      "error - invalid min_value",
+			generator: transformers.Deterministic,
+			params: transformers.Parameters{
+				"min_value": 3.0,
+			},
+			wantErr: transformers.ErrInvalidParameters,
+		},
+		{
+			name:      "error - invalid max_value",
+			generator: transformers.Deterministic,
+			params: transformers.Parameters{
+				"min_value": int64(1380002000000),
+				"max_value": "invalid",
+			},
+			wantErr: transformers.ErrInvalidParameters,
+		},
+		{
+			name:      "error - min_value missing",
+			generator: transformers.Deterministic,
+			params: transformers.Parameters{
+				"max_value": int64(72438947289300),
+			},
+			wantErr: errMinMaxValueNotSpecified,
+		},
+		{
+			name:      "error - invalid limits",
+			generator: transformers.Random,
+			params: transformers.Parameters{
+				"min_value": int64(72438947289300),
+				"max_value": int64(-72438947289300),
+			},
+			wantErr: greenmasktransformers.ErrWrongLimits,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			transformer, err := NewUnixTimestampTransformer(tt.generator, tt.params)
+			require.ErrorIs(t, err, tt.wantErr)
+			if err != nil {
+				return
+			}
+
+			require.NotNil(t, transformer)
+		})
+	}
+}
+
+func TestUnixTimestampTransformer_Transform(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name      string
+		generator transformers.GeneratorType
+		input     any
+		params    transformers.Parameters
+	}{
+		{
+			name:      "ok - random",
+			generator: transformers.Random,
+			params: transformers.Parameters{
+				"min_value": int64(1625097600),
+				"max_value": int64(1625184000),
+			},
+			input: int64(0),
+		},
+		{
+			name:      "ok - deterministic",
+			generator: transformers.Deterministic,
+			params: transformers.Parameters{
+				"min_value": int64(1625097600),
+				"max_value": int64(1625184000),
+			},
+			input: int64(-1),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			transformer, err := NewUnixTimestampTransformer(tt.generator, tt.params)
+			require.NoError(t, err)
+			got, err := transformer.Transform(tt.input)
+			require.NoError(t, err)
+			require.NotNil(t, got)
+
+			v, ok := got.(int64)
+			require.True(t, ok)
+			minVal, ok := tt.params["min_value"].(int64)
+			require.True(t, ok)
+			maxVal, ok := tt.params["max_value"].(int64)
+			require.True(t, ok)
+			require.GreaterOrEqual(t, v, minVal)
+			require.LessOrEqual(t, v, maxVal)
+
+			// if deterministic, check if we get the same result again
+			if tt.generator == transformers.Deterministic {
+				gotAgain, err := transformer.Transform(tt.input)
+				require.NoError(t, err)
+				require.Equal(t, got, gotAgain)
+			}
+		})
+	}
+}